Avalonia icon indicating copy to clipboard operation
Avalonia copied to clipboard

Animation smoothness

Open hacklex opened this issue 4 years ago • 31 comments

Animations in Avalonia are way less smooth than in, for example, WPF (tested on win10). This is partially caused by the fact that DefaultRenderTimer is created at 60 FPS (and that number should be at least twice the monitor refresh rate if we want the real smoothness), and partially by the fact that the message queue is capped at said framerate except during window resizes.

To observe differences between 60FPS and, for example, 120FPS (on a 60 fps monitor), just monotonously resize the window and compare the animation with the regular idle one. On high-fps monitors the difference will be even more visible, although you'd need 240 and 480 fps DefaultRenderTimers in the initialization section.

I'm not yet sure as to how to properly fix it, but creating this issue in hope that someone will eventually consider digging into it.

hacklex avatar Sep 08 '19 23:09 hacklex

possibly related to https://github.com/AvaloniaUI/Avalonia/issues/2792 ?

ahopper avatar Sep 09 '19 08:09 ahopper

@ahopper Highly likely, yes

jmacato avatar Sep 09 '19 08:09 jmacato

Another possible cause of some roughness is the use of System.Threading.Timer for the render timer, looking at the ms source, even when passed a timespan it is rounded down to the closest millisecond. 60fps is 16.6666 ms so that will be rounded down to 16 (62.5 fps) which on a 60hz monitor means 2 or 3 frames will not be shown per second

ahopper avatar Sep 16 '19 15:09 ahopper

@ahopper i've been doing some test, including making my own high resolution timer on linux and it's apparent that our frame render time sometimes exceeds 16.667 ms (using RenderDemo).

jmacato avatar Sep 17 '19 11:09 jmacato

I've also being doing tests with a render timer sync'd to vsync on windows which makes things much smoother

class WindowsDWMRenderTimer : IRenderTimer
    {
        public event Action<TimeSpan> Tick;

        Thread _renderTick;
        public WindowsDWMRenderTimer()
        {
            _renderTick = new Thread(() =>
            {
                while (true)
                {
                    DwmFlush();
                    Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
                }
            });
            _renderTick.IsBackground = true;
            _renderTick.Start();
        }
        [DllImport("Dwmapi.dll")]
        private static extern int DwmFlush();
    }

This won't work on win 7 without aero, there is a solution that should here https://bugs.chromium.org/p/chromium/issues/detail?id=467617 that I'll try next

ahopper avatar Sep 17 '19 11:09 ahopper

DeferredRenderer schedules scene update every other frame due to the IRenderLoopTask system. That effectively halves the frame rate.

kekekeks avatar Sep 17 '19 11:09 kekekeks

@ahopper @kekekeks if y'all could test this branch https://github.com/AvaloniaUI/Avalonia/tree/timer-overload on linux it'd be great to know if there's any difference at all

jmacato avatar Sep 17 '19 11:09 jmacato

Does Skia + OpenGL provide any information as to when a scene has actually been consumed by the system that could be used to sync render?

ahopper avatar Sep 17 '19 11:09 ahopper

There are several platform-specific APIs controlling the swap interval, but all of them are render-target-bound.

kekekeks avatar Sep 17 '19 11:09 kekekeks

@jmacato I tested your mod on linux. Before the mod the animations looked better than on windows for me anyway so it is hard to be sure I'm seeing a difference but it does look smoother. The fps is more steady at 58fps, My linux laptop always has reported around 58 fps which I don't understand, with the old code it should have been 62 and 60 with this code. On windows I get 62 fps without my mod and 59/60 with it. For my own project I have a continuously scrolling waterfall display which is good at showing up problems here, I'll try and make a simple test page doing the same thing.

ahopper avatar Sep 17 '19 14:09 ahopper

@ahopper perhaps you can try doubling the declared framerate on X11Platform.cs ?

jmacato avatar Sep 17 '19 15:09 jmacato

Perhaps it'd be great if we could do z-culling of controls and ignoring their StyledProperties with Animation value priority (value interpolation is cheap anyway, it's the whole update/notifications that makes it slow)

jmacato avatar Sep 17 '19 15:09 jmacato

@jmacato this branch has an extra page in render demo that is a good test. https://github.com/ahopper/Avalonia/tree/vsync-render-on-windows On my windows box the scrolling is silky smooth with my mod and visibly jerky without it. I've not tried on linux yet.

ahopper avatar Sep 17 '19 16:09 ahopper

@jmacato your mod definitely smooths the scrolling on linux here 👍 . Fixing the renderer to go at 60fps will obviously help as well.

ahopper avatar Sep 17 '19 17:09 ahopper

@ahopper great to know that it has some effect :) but yeah, i still see some not so accurate framerates on render demo on linux.. not sure what's the deal with that

jmacato avatar Sep 18 '19 05:09 jmacato

@jmacato I think getting this perfect is not an easy task, especially as the pc gets loaded. There are a few thing happening that I don't understand but I've spent very little time looking at the render and animation code. I suspect the render stuff showing only every second frame may have side effects.

In your code there is the small possibility of your timer thread being suspended between measuring the time and setting the sleep which would cause a longer period.

What are the Linux/Mac options for getting a vsync/compositor presented event?

ahopper avatar Sep 18 '19 07:09 ahopper

It is probably worth looking at the fps code and checking for rounding and making it average over a longer time for this sort of testing.

ahopper avatar Sep 18 '19 07:09 ahopper

I agree, this isnt a easy task, at least for linux and mac. I think @kekekeks knows more about the vsync events but in my limited understading of linux compositors is that it's a pray-if-the-compositor-vsyncs-this kind of thing. idk if X11 itself got extensions for vsync events at all even.

jmacato avatar Sep 18 '19 09:09 jmacato

My linux render timer has a frameTime property that we could perhaps calculate to determine render time and probably a more real FPS count

jmacato avatar Sep 18 '19 09:09 jmacato

@hacklex do you have a specific animation that performs badly that can be used for testing? I'd be interested to know if this branch https://github.com/ahopper/Avalonia/tree/vsync-render-on-windows works better for you,

ahopper avatar Sep 18 '19 15:09 ahopper

@jmacato just added the red cyan test from here https://www.vsynctester.com/ to my branch. At the current 30fps it is not as clear as it should be but still useful. Beware anyone affected by flashing images.

ahopper avatar Sep 18 '19 17:09 ahopper

Using Environment.TickCount may also be causing issues as it's resolution is gererally much worse than 1ms https://docs.microsoft.com/en-us/dotnet/api/system.environment.tickcount?view=netframework-4.7.2 although I'm not sure if the time is actually used.

ahopper avatar Sep 20 '19 12:09 ahopper

@ahopper yeah i noticed the same too. Hence i prefer native nanosecond resolution timers if possible.

jmacato avatar Sep 20 '19 13:09 jmacato

This is my current work around for windows, it now checks if dwm is enabled and falls back if not

public static AppBuilder BuildAvaloniaApp()
        {
            var builder = AppBuilder.Configure<App>()
                .UsePlatformDetect()
                .UseReactiveUI()
                .UseSkia()
                .LogToDebug();
            
            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
            {
                bool dwmEnabled;
                if(DwmIsCompositionEnabled(out dwmEnabled)==0 && dwmEnabled)
                {
                    var wp = builder.WindowingSubsystemInitializer;
                    return builder.UseWindowingSubsystem(() =>
                    {
                        wp();
                        AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(new WindowsDWMRenderTimer());
                    });
                }
            }
            return builder;
        }
        [DllImport("Dwmapi.dll")]
        private static extern int DwmIsCompositionEnabled(out bool enabled);
    }
    class WindowsDWMRenderTimer : IRenderTimer
    {
        public event Action<TimeSpan> Tick;
        private Thread _renderTick;
        public WindowsDWMRenderTimer()
        {
            _renderTick = new Thread(() =>
            {
                System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
                sw.Start();
                while (true)
                {
                    DwmFlush();
                    Tick?.Invoke(sw.Elapsed);
                }
            });
            _renderTick.IsBackground = true;
            _renderTick.Start();
        }
        [DllImport("Dwmapi.dll")]
        private static extern int DwmFlush();
    }

ahopper avatar Sep 25 '19 07:09 ahopper

We did orginally use Stopwatch.GetTimestamp() to get the tick count, but 5e1e25e6fa8a17e7ead7c55b1b0b17b73b0fbb05 changed it to use Environment.TickCount. @jkoritzinsky do you remember why you made that change? Was there a problem with Stopwatch.GetTimestamp()?

grokys avatar Sep 25 '19 07:09 grokys

I do not remember. I think I did tick count so we’d be consistent with the time unit of ticks?

jkoritzinsky avatar Sep 25 '19 07:09 jkoritzinsky

I kinda recall that there was some serious timing bugs and less precision with TickCount, i think it's above the +/- 16.667 ms error threshold

jmacato avatar Sep 25 '19 07:09 jmacato

It appears hw rendering is possibly already doing what is needed here but is fighting with the low resolution render clock. The following code which just calls Tick continuously works fine on windows if AllowEglInitialization = true is set, I'm guessing it might work on osx following a comment on Gitter that a slow monitor reduced fps. I've not tested on linux so would be interested to know what happens. I have no idea if this is reliable solution across platforms and installations but thought I'd add it to the knowledge base.

class InYourOwnTimeRenderTimer : IRenderTimer
    {
        public event Action<TimeSpan> Tick;
        private Thread _renderTick;
        public  InYourOwnTimeRenderTimer()
        {
            _renderTick = new Thread(() =>
            {
                System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
                sw.Start();
                while (true)
                {
                    Tick?.Invoke(sw.Elapsed);
                }
            });
            _renderTick.IsBackground = true;
            _renderTick.Start();
        }

ahopper avatar Oct 18 '19 15:10 ahopper

What if we used @ahopper 's solution but use something like what is in here for the timer?

https://github.com/HakanL/Haukcode.HighResolutionTimer

It uses the multimedia timer for windows, which I've used with success in a project that needed the highest timer resolution possible. I've gotten it down to 1ms accuracy. I'm guessing the linux implementation works for both mac and linux but I'm not sure.

EDIT: When trying the above solution it doesn't look like the dependency binding for IRenderTimer is getting used. I think the original DefaultRenderTimer is still being used. If I write a sleep for 3 seconds in the while loop, rendering is normal.

jhimes144 avatar Jul 07 '22 16:07 jhimes144

When WinUI composition is enabled the render timer ticks with the display refresh rate. For OSX we still need to wire up CVDisplayLink

kekekeks avatar Jul 08 '22 07:07 kekekeks