Browse Source

Don't tick with render loop when app is idle (#20873)

* Don't tick with render loop when app is idle

* Update src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* wip

* wip

* api diff

* fixes

* Address review: clear wakeupPending at tick start, guard CarbonEmissionsHack subscriptions

- Clear _wakeupPending at start of TimerTick so wakeups already processed
  by the current tick don't force an unnecessary extra tick
- Guard CarbonEmissionsHack against duplicate subscriptions using a private
  attached property

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix lock-order inversion in Add/Remove vs TimerTick

Move Wakeup() and Stop() calls outside the _items lock in Add/Remove
to prevent deadlock with TimerTick which acquires _timerLock then _items.
Add/Remove are UI-thread-only so the extracted logic remains safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: remove unused usings, guard double Stop(), fix SleepLoop extra frame

- Remove unused using directives from IRenderLoopTask.cs
- Guard TimerTick Stop() with _running check to prevent double Stop()
  when Remove() already stopped the timer
- SleepLoopRenderTimer: use WaitOne(timeout) instead of Thread.Sleep
  so Stop() can interrupt the sleep, and recheck _stopped before Tick

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix DisplayLinkTimer foreground handler bypassing render loop state

Only resume the display link on WillEnterForeground if the timer was
calling Start() to avoid setting _stopped=false when the render loop
had the timer stopped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix DisplayLinkTimer thread safety, revert global NU5104 suppression

- Stop() now only sets _stopped flag; OnLinkTick() self-pauses the
  CADisplayLink from the timer thread to avoid thread-affinity issues
- Revert NU5104 global suppression in SharedVersion.props

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Cap _ticksSinceLastCommit to prevent int overflow

Stop incrementing once it reaches CommitGraceTicks to prevent
wrapping negative and keeping the render loop awake indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: remove Start/Stop from IRenderTimer, merge into Tick setter

Timer start/stop is now controlled entirely by setting the Tick
property: non-null starts, null stops. This eliminates the explicit
Start()/Stop() methods from IRenderTimer, making the API simpler.

DefaultRenderLoop controls the timer purely through Tick assignment
under its _timerLock. A new _hasItems flag tracks subscriber presence
since Tick is now transient (null when idle).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address review comments on timer thread safety and guards

- ChoreographerTimer: add _frameCallbackActive guard to prevent double
  PostFrameCallback from both Tick setter and SubscribeView
- ServerCompositor: cap _ticksSinceLastCommit at int.MaxValue
- SleepLoopRenderTimer: make _tick volatile, remove _stopped recheck
  (guard moved to DefaultRenderLoop)
- DefaultRenderLoop: add _running check at tick start to drop late ticks
- ThreadProxyRenderTimer: add lock for internal state manipulation
- DisplayLinkTimer: add lock for all internal state manipulation
- Re-add NU5104 suppression to SharedVersion.props

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: make _hasItems volatile for cross-thread visibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: guard against redundant starts in DefaultRenderTimer, make _tick volatile across all timers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove CarbonEmissionsHack, revert iOS/Android timers to always-ticking

- Delete CarbonEmissionsHack class and its XAML reference
- Revert DisplayLinkTimer (iOS) to original always-ticking implementation
- Revert ChoreographerTimer (Android) to original always-ticking implementation
- Add TODO comments for future start/stop on RenderLoop request

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix DirectCompositionConnection WaitOne not respecting process exit cancellation

Use WaitHandle.WaitAny with both _wakeEvent and cts.Token.WaitHandle so
the loop can exit when ProcessExit fires while the timer is stopped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* timers

* XML docs

* Cache delegate

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
maxkatz6-patch-1
Nikita Tsukanov 1 week ago
committed by GitHub
parent
commit
ae6a085ebc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      api/Avalonia.Headless.XUnit.nupkg.xml
  2. 2
      api/Avalonia.Headless.nupkg.xml
  3. 2
      api/Avalonia.Win32.Interoperability.nupkg.xml
  4. 146
      api/Avalonia.nupkg.xml
  5. 2
      build/SharedVersion.props
  6. 5
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  7. 4
      src/Android/Avalonia.Android/AndroidPlatform.cs
  8. 2
      src/Android/Avalonia.Android/AvaloniaView.cs
  9. 58
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  11. 14
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  12. 44
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  13. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
  14. 75
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  15. 44
      src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
  16. 16
      src/Avalonia.Base/Rendering/IRenderLoop.cs
  17. 5
      src/Avalonia.Base/Rendering/IRenderLoopTask.cs
  18. 13
      src/Avalonia.Base/Rendering/IRenderTimer.cs
  19. 138
      src/Avalonia.Base/Rendering/RenderLoop.cs
  20. 59
      src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
  21. 65
      src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs
  22. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  23. 9
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  24. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  25. 48
      src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
  26. 2
      src/Avalonia.X11/X11Platform.cs
  27. 11
      src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
  28. 2
      src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs
  29. 6
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  30. 2
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  31. 30
      src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs
  32. 31
      src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs
  33. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  34. 30
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs
  35. 12
      src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
  36. 2
      src/iOS/Avalonia.iOS/Platform.cs
  37. 2
      tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
  38. 6
      tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
  39. 2
      tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs
  40. 2
      tests/Avalonia.RenderTests/ManualRenderTimer.cs
  41. 2
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  42. 7
      tests/Avalonia.UnitTests/CompositorTestServices.cs
  43. 2
      tests/Avalonia.UnitTests/RendererMocks.cs

2
api/Avalonia.Headless.XUnit.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>

2
api/Avalonia.Headless.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>

2
api/Avalonia.Win32.Interoperability.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>

146
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -1597,6 +1597,42 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)</Target>
@ -1609,6 +1645,30 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})</Target>
@ -3031,6 +3091,42 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.DefaultRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)</Target>
@ -3043,6 +3139,30 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})</Target>
@ -3979,6 +4099,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Input.IInputRoot.FocusRoot</Target>
@ -4291,6 +4423,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Start</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Rendering.IRenderTimer.Stop</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Input.IInputRoot.FocusRoot</Target>

2
build/SharedVersion.props

@ -8,7 +8,7 @@
<PackageProjectUrl>https://avaloniaui.net/?utm_source=nuget&amp;utm_medium=referral&amp;utm_content=project_homepage_link</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<NoWarn>$(NoWarn);CS1591;NU5104</NoWarn>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon>
<PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>

5
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -34,7 +34,10 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
_clipboardLastDataObjectChecker =
new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject);
new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject)
{
IsEnabled = false
};
using var asset = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"));
_defaultImage = new Bitmap(asset);

4
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -76,19 +76,21 @@ namespace Avalonia.Android
public static AndroidPlatformOptions? Options { get; private set; }
internal static Compositor? Compositor { get; private set; }
internal static ChoreographerTimer? Timer { get; private set; }
public static void Initialize()
{
Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl());
Timer = new ChoreographerTimer();
AvaloniaLocator.CurrentMutable
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(Timer))
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToConstant(new AndroidActivatableLifetime());

2
src/Android/Avalonia.Android/AvaloniaView.cs

@ -100,7 +100,7 @@ namespace Avalonia.Android
return;
if (isVisible && _timerSubscription == null)
{
if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
if (AndroidPlatform.Timer is { } timer)
{
_timerSubscription = timer.SubscribeView(this);
}

58
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -18,10 +18,9 @@ namespace Avalonia.Android
private readonly AutoResetEvent _event = new(false);
private readonly GCHandle _timerHandle;
private readonly HashSet<AvaloniaView> _views = new();
private Action<TimeSpan>? _tick;
private bool _pendingCallback;
private long _lastTime;
private int _count;
public ChoreographerTimer()
{
@ -40,28 +39,13 @@ namespace Avalonia.Android
public bool RunsInBackground => true;
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
{
lock (_lock)
{
_tick += value;
_count++;
if (_count == 1)
{
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
}
remove
get => _tick;
set
{
lock (_lock)
{
_tick -= value;
_count--;
}
_tick = value;
PostFrameCallbackIfNeeded();
}
}
@ -70,20 +54,14 @@ namespace Avalonia.Android
lock (_lock)
{
_views.Add(view);
if (_views.Count == 1)
{
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
PostFrameCallbackIfNeeded();
}
return Disposable.Create(
() =>
{
lock (_lock)
{
_views.Remove(view);
}
}
);
}
@ -109,14 +87,28 @@ namespace Avalonia.Android
}
}
private void PostFrameCallbackIfNeeded()
{
lock (_lock)
{
if(_pendingCallback)
return;
if (_tick == null || _views.Count == 0)
return;
_pendingCallback = true;
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
private void DoFrameCallback(long frameTimeNanos, IntPtr data)
{
lock (_lock)
{
if (_count > 0 && _views.Count > 0)
{
PostFrameCallback(_choreographer.Task.Result, data);
}
_pendingCallback = false;
PostFrameCallbackIfNeeded();
_lastTime = frameTimeNanos;
_event.Set();
}

2
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
[PrivateApi]
public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false)
: this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits)
: this(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), gpu, useUiThreadForSynchronousCommits)
{
}

14
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -40,6 +40,11 @@ namespace Avalonia.Rendering.Composition.Server
public int RenderedVisuals { get; set; }
public int VisitedVisuals { get; set; }
/// <summary>
/// Returns true if the target is enabled and has pending work but its render target was not ready.
/// </summary>
internal bool IsWaitingForReadyRenderTarget { get; private set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<IPlatformRenderSurface>> surfaces)
: base(compositor)
{
@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
public void Render()
{
IsWaitingForReadyRenderTarget = false;
if (_disposed)
return;
@ -143,11 +150,15 @@ namespace Avalonia.Rendering.Composition.Server
try
{
if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces()))
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
_renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
}
catch (RenderTargetNotReadyException)
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
catch (RenderTargetCorruptedException)
@ -164,7 +175,10 @@ namespace Avalonia.Rendering.Composition.Server
return;
if (!_renderTarget.IsReady)
{
IsWaitingForReadyRenderTarget = IsEnabled;
return;
}
var needLayer = _overlays.RequireLayer // Check if we don't need overlays
// Check if render target can be rendered to directly and preserves the previous frame

44
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -45,6 +45,9 @@ namespace Avalonia.Rendering.Composition.Server
public ServerCompositorAnimations Animations { get; }
public ReadbackIndices Readback { get; } = new();
private int _ticksSinceLastCommit;
private const int CommitGraceTicks = 10;
public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
CompositionOptions options,
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
@ -64,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server
{
lock (_batches)
_batches.Enqueue(batch);
_renderLoop.Wakeup();
}
internal void UpdateServerTime() => ServerNow = Clock.Elapsed;
@ -72,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server
readonly List<CompositionBatch> _reusableToNotifyRenderedList = new();
void ApplyPendingBatches()
{
bool hadBatches = false;
while (true)
{
CompositionBatch batch;
@ -119,7 +124,13 @@ namespace Avalonia.Rendering.Composition.Server
_reusableToNotifyProcessedList.Add(batch);
LastBatchId = batch.SequenceId;
hadBatches = true;
}
if (hadBatches)
_ticksSinceLastCommit = 0;
else if (_ticksSinceLastCommit < int.MaxValue)
_ticksSinceLastCommit++;
}
void ReadServerJobs(BatchStreamReader reader, Queue<Action> queue, object endMarker)
@ -171,8 +182,10 @@ namespace Avalonia.Rendering.Composition.Server
_reusableToNotifyRenderedList.Clear();
}
public void Render() => Render(true);
public void Render(bool catchExceptions)
bool IRenderLoopTask.Render() => ExecuteRender(true);
public void Render(bool catchExceptions) => ExecuteRender(catchExceptions);
private bool ExecuteRender(bool catchExceptions)
{
if (Dispatcher.UIThread.CheckAccess())
{
@ -182,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server
try
{
using (Dispatcher.UIThread.DisableProcessing())
RenderReentrancySafe(catchExceptions);
return RenderReentrancySafe(catchExceptions);
}
finally
{
@ -190,10 +203,10 @@ namespace Avalonia.Rendering.Composition.Server
}
}
else
RenderReentrancySafe(catchExceptions);
return RenderReentrancySafe(catchExceptions);
}
private void RenderReentrancySafe(bool catchExceptions)
private bool RenderReentrancySafe(bool catchExceptions)
{
lock (_lock)
{
@ -202,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server
try
{
_safeThread = Thread.CurrentThread;
RenderCore(catchExceptions);
return RenderCore(catchExceptions);
}
finally
{
@ -235,17 +248,16 @@ namespace Avalonia.Rendering.Composition.Server
return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted);
}
private void RenderCore(bool catchExceptions)
private bool RenderCore(bool catchExceptions)
{
UpdateServerTime();
var compositorGlobalPassesElapsed = ExecuteGlobalPasses();
try
{
if(!RenderInterface.IsReady)
return;
if (!RenderInterface.IsReady)
return true;
RenderInterface.EnsureValidBackendContext();
ExecuteServerJobs(_receivedJobQueue);
@ -263,6 +275,18 @@ namespace Avalonia.Rendering.Composition.Server
{
Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e);
}
// Request a tick if we have active animations or if there are recent batches
if (Animations.NeedNextTick || _ticksSinceLastCommit < CommitGraceTicks)
return true;
// Request a tick if we had unready targets in the last tick, to check if they are ready next time
foreach (var target in _activeTargets)
if (target.IsWaitingForReadyRenderTarget)
return true;
// Otherwise there is no need to waste CPU cycles, tell the timer to pause
return false;
}
public void AddCompositionTarget(ServerCompositionTarget target)

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs

@ -30,6 +30,8 @@ internal class ServerCompositorAnimations
_dirtyAnimatedObjects.Clear();
}
public bool NeedNextTick => _clockItems.Count > 0;
public void AddDirtyAnimatedObject(ServerObjectAnimations obj)
{
if (_dirtyAnimatedObjects.Add(obj))

75
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
@ -13,52 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport;
/// </summary>
internal abstract class BatchStreamPoolBase<T> : IDisposable
{
private readonly Action<Func<bool>>? _startTimer;
readonly Stack<T> _pool = new();
bool _disposed;
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
readonly bool _reclaimImmediately;
private readonly WeakReference<BatchStreamPoolBase<T>> _updateRef;
private readonly Dispatcher? _reclaimOnDispatcher;
private bool _timerIsRunning;
private ulong _currentUpdateTick, _lastActivityTick;
public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count;
public BatchStreamPoolBase(bool needsFinalize, bool reclaimImmediately, Action<Func<bool>>? startTimer = null)
{
_startTimer = startTimer;
if(!needsFinalize)
GC.SuppressFinalize(needsFinalize);
GC.SuppressFinalize(this);
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
if (
reclaimImmediately
|| Dispatcher.FromThread(Thread.CurrentThread) == null)
_reclaimImmediately = true;
else
StartUpdateTimer(startTimer, updateRef);
_updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
_reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null;
EnsureUpdateTimer();
}
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
void EnsureUpdateTimer()
{
Func<bool> timerProc = () =>
if (_timerIsRunning || !NeedsTimer)
return;
var timerProc = GetTimerProc(_updateRef);
if (_startTimer != null)
_startTimer(timerProc);
else
{
if (updateRef.TryGetTarget(out var target))
if (_reclaimOnDispatcher != null)
{
target.UpdateStatistics();
return true;
if (_reclaimOnDispatcher.CheckAccess())
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
else
_reclaimOnDispatcher.Post(
() => DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)),
DispatcherPriority.Normal);
}
}
_timerIsRunning = true;
// Explicit capture
static Func<bool> GetTimerProc(WeakReference<BatchStreamPoolBase<T>> updateRef) => () =>
{
if (updateRef.TryGetTarget(out var target))
return target.UpdateTimerTick();
return false;
};
if (startTimer != null)
startTimer(timerProc);
else
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
}
private void UpdateStatistics()
[MemberNotNullWhen(true, nameof(_reclaimOnDispatcher))]
private bool NeedsTimer => _reclaimOnDispatcher != null &&
_currentUpdateTick - _lastActivityTick < (uint)_usageStatistics.Length * 2 + 1;
private bool ReclaimImmediately => _reclaimOnDispatcher == null;
private bool UpdateTimerTick()
{
lock (_pool)
{
_currentUpdateTick++;
var maximumUsage = _usageStatistics.Max();
var recentlyUsedPooledSlots = maximumUsage - _usage;
var keepSlots = Math.Max(recentlyUsedPooledSlots, 10);
@ -67,9 +91,17 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
_usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length;
_usageStatistics[_usageStatisticsSlot] = 0;
return _timerIsRunning = NeedsTimer;
}
}
private void OnActivity()
{
_lastActivityTick = _currentUpdateTick;
EnsureUpdateTimer();
}
protected abstract T CreateItem();
protected virtual void ClearItem(T item)
@ -90,6 +122,8 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
if (_usageStatistics[_usageStatisticsSlot] < _usage)
_usageStatistics[_usageStatisticsSlot] = _usage;
OnActivity();
if (_pool.Count != 0)
return _pool.Pop();
}
@ -103,9 +137,10 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
lock (_pool)
{
_usage--;
if (!_disposed && !_reclaimImmediately)
if (!_disposed && !ReclaimImmediately)
{
_pool.Push(item);
OnActivity();
return;
}
}

44
src/Avalonia.Base/Rendering/DefaultRenderTimer.cs

@ -15,8 +15,7 @@ namespace Avalonia.Rendering
[PrivateApi]
public class DefaultRenderTimer : IRenderTimer
{
private int _subscriberCount;
private Action<TimeSpan>? _tick;
private volatile Action<TimeSpan>? _tick;
private IDisposable? _subscription;
/// <summary>
@ -36,40 +35,28 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; }
/// <inheritdoc/>
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
_tick += value;
if (_subscriberCount++ == 0)
if (value != null)
{
Start();
_tick = value;
_subscription ??= StartCore(InternalTick);
}
}
remove
{
if (--_subscriberCount == 0)
else
{
Stop();
_subscription?.Dispose();
_subscription = null;
_tick = null;
}
_tick -= value;
}
}
/// <inheritdoc />
public virtual bool RunsInBackground => true;
/// <summary>
/// Starts the timer.
/// </summary>
protected void Start()
{
_subscription = StartCore(InternalTick);
}
/// <summary>
/// Provides the implementation of starting the timer.
/// </summary>
@ -85,15 +72,6 @@ namespace Avalonia.Rendering
return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval);
}
/// <summary>
/// Stops the timer.
/// </summary>
protected void Stop()
{
_subscription?.Dispose();
_subscription = null;
}
private void InternalTick(TimeSpan tickCount)
{
_tick?.Invoke(tickCount);

16
src/Avalonia.Base/Rendering/IRenderLoop.cs

@ -9,8 +9,8 @@ namespace Avalonia.Rendering
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
[NotClientImplementable]
internal interface IRenderLoop
[PrivateApi]
public interface IRenderLoop
{
/// <summary>
/// Adds an update task.
@ -20,17 +20,23 @@ namespace Avalonia.Rendering
/// Registered update tasks will be polled on each tick of the render loop after the
/// animation timer has been pulsed.
/// </remarks>
void Add(IRenderLoopTask i);
internal void Add(IRenderLoopTask i);
/// <summary>
/// Removes an update task.
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
internal void Remove(IRenderLoopTask i);
/// <summary>
/// Indicates if the rendering is done on a non-UI thread.
/// </summary>
bool RunsInBackground { get; }
internal bool RunsInBackground { get; }
/// <summary>
/// Wakes up the render loop to schedule the next tick.
/// Thread-safe: can be called from any thread.
/// </summary>
internal void Wakeup();
}
}

5
src/Avalonia.Base/Rendering/IRenderLoopTask.cs

@ -1,10 +1,7 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
internal interface IRenderLoopTask
{
void Render();
bool Render();
}
}

13
src/Avalonia.Base/Rendering/IRenderTimer.cs

@ -10,16 +10,19 @@ namespace Avalonia.Rendering
public interface IRenderTimer
{
/// <summary>
/// Raised when the render timer ticks to signal a new frame should be drawn.
/// Gets or sets the callback to be invoked when the timer ticks.
/// This property can be set from any thread, but it's guaranteed that it's not set concurrently
/// (i. e. render loop always does it under a lock).
/// Setting the value to null suggests the timer to stop ticking, however
/// timer is allowed to produce ticks on the previously set value as long as it stops doing so
/// </summary>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// The callback can be invoked on any thread
/// </remarks>
event Action<TimeSpan> Tick;
Action<TimeSpan>? Tick { get; set; }
/// <summary>
/// Indicates if the timer ticks on a non-UI thread
/// Indicates if the timer ticks on a non-UI thread.
/// </summary>
bool RunsInBackground { get; }
}

138
src/Avalonia.Base/Rendering/RenderLoop.cs

@ -2,58 +2,52 @@
using System.Collections.Generic;
using System.Threading;
using Avalonia.Logging;
using Avalonia.Metadata;
using Avalonia.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// The application render loop.
/// Provides factory methods for creating <see cref="IRenderLoop"/> instances.
/// </summary>
[PrivateApi]
public static class RenderLoop
{
/// <summary>
/// Creates an <see cref="IRenderLoop"/> from an <see cref="IRenderTimer"/>.
/// </summary>
public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer);
}
/// <summary>
/// Default implementation of the application render loop.
/// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// graph for visible windows. It owns the sleep/wake state machine: setting
/// <see cref="IRenderTimer.Tick"/> to a non-null callback to start the timer and to null to
/// stop it, under a lock so that timer implementations never see concurrent changes.
/// </remarks>
internal class RenderLoop : IRenderLoop
internal class DefaultRenderLoop : IRenderLoop
{
private readonly List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private readonly List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>();
private IRenderTimer? _timer;
private Action<TimeSpan> _tick;
private readonly IRenderTimer _timer;
private readonly object _timerLock = new();
private int _inTick;
public static IRenderLoop LocatorAutoInstance
{
get
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
if (loop == null)
{
var timer = AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>()
.ToConstant(loop = new RenderLoop(timer));
}
return loop;
}
}
private volatile bool _hasItems;
private bool _running;
private bool _wakeupPending;
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// Initializes a new instance of the <see cref="DefaultRenderLoop"/> class.
/// </summary>
/// <param name="timer">The render timer.</param>
public RenderLoop(IRenderTimer timer)
public DefaultRenderLoop(IRenderTimer timer)
{
_timer = timer;
}
/// <summary>
/// Gets the render timer.
/// </summary>
protected IRenderTimer Timer
{
get
{
return _timer ??= AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
}
_tick = TimerTick;
}
/// <inheritdoc/>
@ -62,14 +56,17 @@ namespace Avalonia.Rendering
_ = i ?? throw new ArgumentNullException(nameof(i));
Dispatcher.UIThread.VerifyAccess();
bool shouldStart;
lock (_items)
{
_items.Add(i);
shouldStart = _items.Count == 1;
}
if (_items.Count == 1)
{
Timer.Tick += TimerTick;
}
if (shouldStart)
{
_hasItems = true;
Wakeup();
}
}
@ -78,19 +75,48 @@ namespace Avalonia.Rendering
{
_ = i ?? throw new ArgumentNullException(nameof(i));
Dispatcher.UIThread.VerifyAccess();
bool shouldStop;
lock (_items)
{
_items.Remove(i);
shouldStop = _items.Count == 0;
}
if (_items.Count == 0)
if (shouldStop)
{
_hasItems = false;
lock (_timerLock)
{
Timer.Tick -= TimerTick;
if (_running)
{
_running = false;
_wakeupPending = false;
_timer.Tick = null;
}
}
}
}
/// <inheritdoc />
public bool RunsInBackground => Timer.RunsInBackground;
public bool RunsInBackground => _timer.RunsInBackground;
/// <inheritdoc />
public void Wakeup()
{
lock (_timerLock)
{
if (_hasItems && !_running)
{
_running = true;
_timer.Tick = _tick;
}
else
{
_wakeupPending = true;
}
}
}
private void TimerTick(TimeSpan time)
{
@ -98,6 +124,15 @@ namespace Avalonia.Rendering
{
try
{
// Consume any pending wakeup — this tick will process its work.
// Only wakeups arriving during task execution will keep the timer running.
// Also drop late ticks that arrive after the timer was stopped.
lock (_timerLock)
{
if (!_running)
return;
_wakeupPending = false;
}
lock (_items)
{
@ -105,14 +140,33 @@ namespace Avalonia.Rendering
_itemsCopy.AddRange(_items);
}
var wantsNextTick = false;
for (int i = 0; i < _itemsCopy.Count; i++)
{
_itemsCopy[i].Render();
wantsNextTick |= _itemsCopy[i].Render();
}
_itemsCopy.Clear();
if (!wantsNextTick)
{
lock (_timerLock)
{
if (!_running)
{
// Already stopped by Remove()
}
else if (_wakeupPending)
{
_wakeupPending = false;
}
else
{
_running = false;
_timer.Tick = null;
}
}
}
}
catch (Exception ex)
{

59
src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs

@ -8,10 +8,10 @@ namespace Avalonia.Rendering
[PrivateApi]
public class SleepLoopRenderTimer : IRenderTimer
{
private Action<TimeSpan>? _tick;
private int _count;
private readonly object _lock = new object();
private bool _running;
private volatile Action<TimeSpan>? _tick;
private volatile bool _stopped = true;
private bool _threadStarted;
private readonly AutoResetEvent _wakeEvent = new(false);
private readonly Stopwatch _st = Stopwatch.StartNew();
private readonly TimeSpan _timeBetweenTicks;
@ -20,27 +20,29 @@ namespace Avalonia.Rendering
_timeBetweenTicks = TimeSpan.FromSeconds(1d / fps);
}
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
lock (_lock)
if (value != null)
{
_tick += value;
_count++;
if (_running)
return;
_running = true;
new Thread(LoopProc) { IsBackground = true }.Start();
_tick = value;
_stopped = false;
if (!_threadStarted)
{
_threadStarted = true;
new Thread(LoopProc) { IsBackground = true }.Start();
}
else
{
_wakeEvent.Set();
}
}
}
remove
{
lock (_lock)
else
{
_tick -= value;
_count--;
_stopped = true;
_tick = null;
}
}
}
@ -52,24 +54,17 @@ namespace Avalonia.Rendering
var lastTick = _st.Elapsed;
while (true)
{
if (_stopped)
_wakeEvent.WaitOne();
var now = _st.Elapsed;
var timeTillNextTick = lastTick + _timeBetweenTicks - now;
if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick);
if (timeTillNextTick.TotalMilliseconds > 1)
_wakeEvent.WaitOne(timeTillNextTick);
lastTick = now = _st.Elapsed;
lock (_lock)
{
if (_count == 0)
{
_running = false;
return;
}
}
_tick?.Invoke(now);
}
}
}
}

65
src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs

@ -12,8 +12,9 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
private readonly Stopwatch _stopwatch;
private readonly Thread _timerThread;
private readonly AutoResetEvent _autoResetEvent;
private Action<TimeSpan>? _tick;
private int _subscriberCount;
private readonly object _lock = new();
private volatile Action<TimeSpan>? _tick;
private volatile bool _active;
private bool _registered;
public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024)
@ -24,33 +25,54 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
_timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true };
}
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
_tick += value;
if (!_registered)
lock (_lock)
{
_registered = true;
_timerThread.Start();
if (value != null)
{
_tick = value;
_active = true;
EnsureStarted();
_inner.Tick = InnerTick;
}
else
{
// Don't set _inner.Tick = null here — may be on the wrong thread.
// InnerTick will detect _active=false and clear _inner.Tick on the correct thread.
_active = false;
_tick = null;
}
}
}
}
if (_subscriberCount++ == 0)
{
_inner.Tick += InnerTick;
}
public bool RunsInBackground => true;
private void EnsureStarted()
{
if (!_registered)
{
_registered = true;
_stopwatch.Start();
_timerThread.Start();
}
}
remove
private void InnerTick(TimeSpan obj)
{
lock (_lock)
{
if (--_subscriberCount == 0)
if (!_active)
{
_inner.Tick -= InnerTick;
_inner.Tick = null;
return;
}
_tick -= value;
}
_autoResetEvent.Set();
}
private void RenderTimerThreadFunc()
@ -60,11 +82,4 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer
_tick?.Invoke(_stopwatch.Elapsed);
}
}
private void InnerTick(TimeSpan obj)
{
_autoResetEvent.Set();
}
public bool RunsInBackground => true;
}

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -55,7 +55,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60))
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(new UiThreadRenderTimer(60)))
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

9
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -69,16 +69,11 @@ namespace Avalonia.DesignerSupport.Remote
private sealed class DummyRenderTimer : IRenderTimer
{
public event Action<TimeSpan> Tick
{
add { }
remove { }
}
public Action<TimeSpan>? Tick { get; set; }
public bool RunsInBackground => false;
}
public Compositor Compositor { get; } = new(new RenderLoop(new DummyRenderTimer()), null);
public Compositor Compositor { get; } = new(RenderLoop.FromTimer(new DummyRenderTimer()), null);
public void Dispose()
{

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -122,7 +122,7 @@ namespace Avalonia.Native
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
.Bind<IRenderTimer>().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform)

48
src/Avalonia.Native/AvaloniaNativeRenderTimer.cs

@ -9,9 +9,8 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim
{
private readonly IAvnPlatformRenderTimer _platformRenderTimer;
private readonly Stopwatch _stopwatch;
private Action<TimeSpan>? _tick;
private int _subscriberCount;
private bool registered;
private volatile Action<TimeSpan>? _tick;
private bool _registered;
public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer)
{
@ -19,42 +18,41 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim
_stopwatch = Stopwatch.StartNew();
}
public event Action<TimeSpan> Tick
public Action<TimeSpan>? Tick
{
add
get => _tick;
set
{
_tick += value;
if (!registered)
if (value != null)
{
registered = true;
var registrationResult = _platformRenderTimer.RegisterTick(this);
if (registrationResult != 0)
{
throw new InvalidOperationException(
$"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}");
}
_tick = value;
EnsureRegistered();
_platformRenderTimer.Start();
}
if (_subscriberCount++ == 0)
else
{
_platformRenderTimer.Start();
_platformRenderTimer.Stop();
_tick = null;
}
}
}
remove
public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool();
private void EnsureRegistered()
{
if (!_registered)
{
if (--_subscriberCount == 0)
_registered = true;
var registrationResult = _platformRenderTimer.RegisterTick(this);
if (registrationResult != 0)
{
_platformRenderTimer.Stop();
throw new InvalidOperationException(
$"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}");
}
_tick -= value;
}
}
public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool();
public void Run()
{
_tick?.Invoke(_stopwatch.Elapsed);

2
src/Avalonia.X11/X11Platform.cs

@ -87,7 +87,7 @@ namespace Avalonia.X11
: new X11PlatformThreading(this);
Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl);
AvaloniaLocator.CurrentMutable
.Bind<IRenderTimer>().ToConstant(timer)
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(timer))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))
.Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)

11
src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs

@ -18,19 +18,16 @@ internal class BrowserRenderTimer : IRenderTimer
public bool RunsInBackground { get; }
public event Action<TimeSpan>? Tick
public Action<TimeSpan>? Tick
{
add
set
{
if (!BrowserWindowingPlatform.IsThreadingEnabled)
StartOnThisThread();
_tick += value;
}
remove
{
_tick -= value;
_tick = value;
}
get => _tick;
}
public void StartOnThisThread()

2
src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs

@ -9,5 +9,5 @@ internal static class BrowserSharedRenderLoop
{
private static BrowserRenderTimer? s_browserUiRenderTimer;
public static BrowserRenderTimer RenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false);
public static Lazy<RenderLoop> RenderLoop = new(() => new RenderLoop(RenderTimer), true);
public static Lazy<IRenderLoop> RenderLoop = new(() => Avalonia.Rendering.RenderLoop.FromTimer(RenderTimer), true);
}

6
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -15,6 +15,7 @@ namespace Avalonia.Headless
public static class AvaloniaHeadlessPlatform
{
internal static Compositor? Compositor { get; private set; }
private static RenderTimer? s_renderTimer;
private class RenderTimer : DefaultRenderTimer
{
@ -85,7 +86,7 @@ namespace Avalonia.Headless
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
.Bind<IRenderLoop>().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60)))
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat))
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }));
@ -99,9 +100,8 @@ namespace Avalonia.Headless
/// <param name="count">Count of frames to be ticked on the timer.</param>
public static void ForceRenderTimerTick(int count = 1)
{
var timer = AvaloniaLocator.Current.GetService<IRenderTimer>() as RenderTimer;
for (var c = 0; c < count; c++)
timer?.ForceTick();
s_renderTimer?.ForceTick();
}
}

2
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -64,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer
Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
AvaloniaLocator.CurrentMutable
.Bind<IRenderTimer>().ToConstant(timer)
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(timer))
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformIconLoader>().ToSingleton<LinuxFramebufferIconLoaderStub>()

30
src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs

@ -21,16 +21,37 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor
{
private static readonly Guid IID_IDCompositionDesktopDevice = Guid.Parse("5f4633fe-1e08-4cb8-8c75-ce24333f5602");
public event Action<TimeSpan>? Tick;
private volatile Action<TimeSpan>? _tick;
public bool RunsInBackground => true;
private readonly DirectCompositionShared _shared;
private readonly AutoResetEvent _wakeEvent = new(false);
private volatile bool _stopped = true;
public DirectCompositionConnection(DirectCompositionShared shared)
{
_shared = shared;
}
public Action<TimeSpan>? Tick
{
get => _tick;
set
{
if (value != null)
{
_tick = value;
_stopped = false;
_wakeEvent.Set();
}
else
{
_stopped = true;
_tick = null;
}
}
}
private static bool TryCreateAndRegisterCore()
{
var tcs = new TaskCompletionSource<bool>();
@ -52,7 +73,7 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor
}
AvaloniaLocator.CurrentMutable.Bind<IWindowsSurfaceFactory>().ToConstant(connect);
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(connect);
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(connect));
tcs.SetResult(true);
}
catch (Exception e)
@ -81,8 +102,11 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor
{
try
{
if (_stopped)
WaitHandle.WaitAny([_wakeEvent, cts.Token.WaitHandle]);
device.WaitForCommitCompletion();
Tick?.Invoke(_stopwatch.Elapsed);
_tick?.Invoke(_stopwatch.Elapsed);
}
catch (Exception ex)
{

31
src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Platform.Surfaces;
@ -25,8 +26,10 @@ namespace Avalonia.Win32.DirectX
public bool RunsInBackground => true;
public event Action<TimeSpan>? Tick;
private volatile Action<TimeSpan>? _tick;
private readonly object _syncLock;
private readonly AutoResetEvent _wakeEvent = new(false);
private volatile bool _stopped = true;
private IDXGIOutput? _output;
@ -38,6 +41,25 @@ namespace Avalonia.Win32.DirectX
_syncLock = syncLock;
}
public Action<TimeSpan>? Tick
{
get => _tick;
set
{
if (value != null)
{
_tick = value;
_stopped = false;
_wakeEvent.Set();
}
else
{
_stopped = true;
_tick = null;
}
}
}
public static bool TryCreateAndRegister()
{
try
@ -70,6 +92,9 @@ namespace Avalonia.Win32.DirectX
{
try
{
if (_stopped)
_wakeEvent.WaitOne();
lock (_syncLock)
{
if (_output is not null)
@ -94,7 +119,7 @@ namespace Avalonia.Win32.DirectX
// but theoretically someone could have a weirder setup out there
DwmFlush();
}
Tick?.Invoke(_stopwatch.Elapsed);
_tick?.Invoke(_stopwatch.Elapsed);
}
}
catch (Exception ex)
@ -199,7 +224,7 @@ namespace Avalonia.Win32.DirectX
var connection = new DxgiConnection(pumpLock);
AvaloniaLocator.CurrentMutable.Bind<IWindowsSurfaceFactory>().ToConstant(connection);
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(connection);
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(connection));
tcs.SetResult(true);
connection.RunLoop();
}

2
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -98,7 +98,7 @@ namespace Avalonia.Win32
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>()
.Bind<IScreenImpl>().ToSingleton<ScreenImpl>()
.Bind<IRenderTimer>().ToConstant(renderTimer)
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(renderTimer))
.Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)
{

30
src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs

@ -17,9 +17,30 @@ namespace Avalonia.Win32.WinRT.Composition;
internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFactory
{
private readonly WinUiCompositionShared _shared;
public event Action<TimeSpan>? Tick;
private readonly AutoResetEvent _wakeEvent = new(false);
private volatile bool _stopped = true;
private volatile Action<TimeSpan>? _tick;
public bool RunsInBackground => true;
public Action<TimeSpan>? Tick
{
get => _tick;
set
{
if (value != null)
{
_tick = value;
_stopped = false;
_wakeEvent.Set();
}
else
{
_stopped = true;
_tick = null;
}
}
}
public WinUiCompositorConnection()
{
using var compositor = NativeWinRTMethods.CreateInstance<ICompositor>("Windows.UI.Composition.Compositor");
@ -58,7 +79,7 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa
});
connect = new WinUiCompositorConnection();
AvaloniaLocator.CurrentMutable.Bind<IWindowsSurfaceFactory>().ToConstant(connect);
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(connect);
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(connect));
tcs.SetResult(true);
}
@ -102,8 +123,11 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa
{
_currentCommit?.Dispose();
_currentCommit = null;
_parent.Tick?.Invoke(_st.Elapsed);
_parent._tick?.Invoke(_st.Elapsed);
// Always schedule a commit so the current frame's work reaches DWM.
ScheduleNextCommit();
if (_parent._stopped)
_parent._wakeEvent.WaitOne();
}
private void ScheduleNextCommit()

12
src/iOS/Avalonia.iOS/DisplayLinkTimer.cs

@ -1,7 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Rendering;
using CoreAnimation;
using Foundation;
@ -11,7 +10,7 @@ namespace Avalonia.iOS
{
class DisplayLinkTimer : IRenderTimer
{
public event Action<TimeSpan>? Tick;
private volatile Action<TimeSpan>? _tick;
private Stopwatch _st = Stopwatch.StartNew();
public DisplayLinkTimer()
@ -31,9 +30,16 @@ namespace Avalonia.iOS
public bool RunsInBackground => true;
// TODO: start/stop on RenderLoop request
public Action<TimeSpan>? Tick
{
get => _tick;
set => _tick = value;
}
private void OnLinkTick()
{
Tick?.Invoke(_st.Elapsed);
_tick?.Invoke(_st.Elapsed);
}
}
}

2
src/iOS/Avalonia.iOS/Platform.cs

@ -93,7 +93,7 @@ namespace Avalonia.iOS
{ Key.PageUp , "⇞" }, { Key.Right , "→" }, { Key.Space , "␣" }, { Key.Tab , "⇥" },
{ Key.Up , "↑" }
}, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥"))
.Bind<IRenderTimer>().ToConstant(Timer)
.Bind<IRenderLoop>().ToConstant(RenderLoop.FromTimer(Timer))
.Bind<IKeyboardDevice>().ToConstant(keyboard);
if (appDelegate is not null)

2
tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs

@ -88,7 +88,7 @@ public class CompositionAnimationTests : ScopedTestBase
{
using var scope = AvaloniaLocator.EnterScope();
var compositor =
new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null);
new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null);
var target = compositor.CreateSolidColorVisual();
var ani = new ScalarKeyFrameAnimation(compositor);
foreach (var frame in data.Frames)

6
tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs

@ -22,7 +22,7 @@ public class CompositionTargetUpdateOnly : IDisposable
class Timer : IRenderTimer
{
event Action<TimeSpan> IRenderTimer.Tick { add { } remove { } }
public Action<TimeSpan> Tick { get; set; } = null!;
public bool RunsInBackground => false;
}
@ -52,7 +52,7 @@ public class CompositionTargetUpdateOnly : IDisposable
{
_includeRender = includeRender;
_app = UnitTestApplication.Start(TestServices.StyledWindow);
_compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true,
_compositor = new Compositor(RenderLoop.FromTimer(new Timer()), null, true, new ManualScheduler(), true,
Dispatcher.UIThread, null);
_target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]);
_target.PixelSize = new PixelSize(1000, 1000);
@ -99,7 +99,7 @@ public class CompositionTargetUpdateOnly : IDisposable
{
_target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0);
_compositor.Commit();
_compositor.Server.Render();
_compositor.Server.Render(false);
if (!_includeRender)
_target.Server.Update();

2
tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs

@ -47,7 +47,7 @@ public class DirectFbCompositionTests : TestBase
void Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised(bool advertised)
{
var timer = new ManualRenderTimer();
var compositor = new Compositor(new RenderLoop(timer), null, true,
var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread, new CompositionOptions
{
UseRegionDirtyRectClipping = true

2
tests/Avalonia.RenderTests/ManualRenderTimer.cs

@ -5,7 +5,7 @@ namespace Avalonia.Skia.RenderTests
{
public class ManualRenderTimer : IRenderTimer
{
public event Action<TimeSpan>? Tick;
public Action<TimeSpan>? Tick { get; set; }
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
}

2
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -63,7 +63,7 @@ static class TestRenderHelper
{
var timer = new ManualRenderTimer();
var compositor = new Compositor(new RenderLoop(timer), null, true,
var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat,
factory.DefaultAlphaFormat))

7
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -42,9 +42,10 @@ public class CompositorTestServices : IDisposable
_app = UnitTestApplication.Start(services);
try
{
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer);
var renderLoop = RenderLoop.FromTimer(Timer);
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(renderLoop);
Compositor = new Compositor(new RenderLoop(Timer), null,
Compositor = new Compositor(renderLoop, null,
true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000));
TopLevel = new EmbeddableControlRoot(impl)
@ -136,7 +137,7 @@ public class CompositorTestServices : IDisposable
public class ManualRenderTimer : IRenderTimer
{
public event Action<TimeSpan>? Tick;
public Action<TimeSpan>? Tick { get; set; }
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
}

2
tests/Avalonia.UnitTests/RendererMocks.cs

@ -17,7 +17,7 @@ namespace Avalonia.UnitTests
}
public static Compositor CreateDummyCompositor() =>
new(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null, false,
new(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null, false,
new CompositionCommitScheduler(), true, Dispatcher.UIThread);
class CompositionCommitScheduler : ICompositorScheduler

Loading…
Cancel
Save