Avalonia: Animation smoothness

Created on 9 Sep 2019  路  28Comments  路  Source: AvaloniaUI/Avalonia

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.

bug

All 28 comments

@ahopper Highly likely, yes

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 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).

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

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

@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

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?

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

@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 perhaps you can try doubling the declared framerate on X11Platform.cs ?

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 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.

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

@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 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?

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.

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.

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

@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,

@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.

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 yeah i noticed the same too. Hence i prefer native nanosecond resolution timers if possible.

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();
    }

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()?

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

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

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();
        }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

georgiuk picture georgiuk  路  3Comments

stdcall picture stdcall  路  4Comments

akunchev picture akunchev  路  3Comments

danwalmsley picture danwalmsley  路  4Comments

CreateLab picture CreateLab  路  3Comments