diff --git a/api/Avalonia.Headless.XUnit.nupkg.xml b/api/Avalonia.Headless.XUnit.nupkg.xml
index c87cf909fe..15a56561b9 100644
--- a/api/Avalonia.Headless.XUnit.nupkg.xml
+++ b/api/Avalonia.Headless.XUnit.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -73,4 +73,4 @@
baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll
current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll
-
+
\ No newline at end of file
diff --git a/api/Avalonia.Headless.nupkg.xml b/api/Avalonia.Headless.nupkg.xml
index 229047057a..435df92d13 100644
--- a/api/Avalonia.Headless.nupkg.xml
+++ b/api/Avalonia.Headless.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -37,4 +37,4 @@
baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll
current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll
-
+
\ No newline at end of file
diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml
index 3672bb9b99..33fc2ac062 100644
--- a/api/Avalonia.Win32.Interoperability.nupkg.xml
+++ b/api/Avalonia.Win32.Interoperability.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -37,4 +37,4 @@
baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
-
+
\ No newline at end of file
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index b4c8bba386..5fe2e48f60 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -1597,6 +1597,42 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.Start
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.Stop
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)
@@ -1609,6 +1645,30 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})
@@ -3031,6 +3091,42 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.Start
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.DefaultRenderTimer.Stop
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect)
@@ -3043,6 +3139,30 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}})
@@ -3979,6 +4099,18 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Rendering.IRenderTimer.Start
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Rendering.IRenderTimer.Stop
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0006
P:Avalonia.Input.IInputRoot.FocusRoot
@@ -4291,6 +4423,18 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Rendering.IRenderTimer.Start
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Rendering.IRenderTimer.Stop
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0006
P:Avalonia.Input.IInputRoot.FocusRoot
@@ -5077,4 +5221,4 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
-
+
\ No newline at end of file
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index 37d14a5647..b8c0dd4d43 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -8,7 +8,7 @@
https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link
https://github.com/AvaloniaUI/Avalonia/
true
- $(NoWarn);CS1591
+ $(NoWarn);CS1591;NU5104
MIT
Icon.png
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.
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
index 2b87ceb7b1..4cdde6b824 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
+++ b/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);
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 7a3059cb65..460aaec5ca 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/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() ?? new AndroidPlatformOptions();
Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl());
+ Timer = new ChoreographerTimer();
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
.Bind().ToConstant(new WindowingPlatformStub())
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToSingleton()
- .Bind().ToConstant(new ChoreographerTimer())
+ .Bind().ToConstant(RenderLoop.FromTimer(Timer))
.Bind().ToSingleton()
.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }))
.Bind().ToConstant(new AndroidActivatableLifetime());
diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs
index 86b96772ce..d8df486bb3 100644
--- a/src/Android/Avalonia.Android/AvaloniaView.cs
+++ b/src/Android/Avalonia.Android/AvaloniaView.cs
@@ -100,7 +100,7 @@ namespace Avalonia.Android
return;
if (isVisible && _timerSubscription == null)
{
- if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer)
+ if (AndroidPlatform.Timer is { } timer)
{
_timerSubscription = timer.SubscribeView(this);
}
diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs
index adca9c72ce..9bc8e78a52 100644
--- a/src/Android/Avalonia.Android/ChoreographerTimer.cs
+++ b/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 _views = new();
-
private Action? _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 Tick
+ public Action? Tick
{
- add
+ get => _tick;
+ set
{
- lock (_lock)
- {
- _tick += value;
- _count++;
-
- if (_count == 1)
- {
- PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
- }
- }
- }
- remove
- {
- 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)
- {
+ 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();
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
index 2398468456..2acda6c57d 100644
--- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition
///
[PrivateApi]
public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false)
- : this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits)
+ : this(AvaloniaLocator.Current.GetRequiredService(), gpu, useUiThreadForSynchronousCommits)
{
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
index f8382547b9..81a3c09b35 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
@@ -39,6 +39,11 @@ namespace Avalonia.Rendering.Composition.Server
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public int RenderedVisuals { get; set; }
public int VisitedVisuals { get; set; }
+
+ ///
+ /// Returns true if the target is enabled and has pending work but its render target was not ready.
+ ///
+ internal bool IsWaitingForReadyRenderTarget { get; private set; }
public ServerCompositionTarget(ServerCompositor compositor, Func> 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
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
index 76e649407f..b8cc5afca2 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
@@ -44,6 +44,9 @@ namespace Avalonia.Rendering.Composition.Server
public CompositionOptions Options { get; }
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,
@@ -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 _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 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)
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
index 1f2c7dedb8..0e59cd8f03 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
+++ b/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))
diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
index 7e1c9e711f..d9003659a1 100644
--- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
+++ b/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;
///
internal abstract class BatchStreamPoolBase : IDisposable
{
+ private readonly Action>? _startTimer;
readonly Stack _pool = new();
bool _disposed;
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
- readonly bool _reclaimImmediately;
+ private readonly WeakReference> _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>? startTimer = null)
{
+ _startTimer = startTimer;
if(!needsFinalize)
- GC.SuppressFinalize(needsFinalize);
+ GC.SuppressFinalize(this);
- var updateRef = new WeakReference>(this);
- if (
- reclaimImmediately
- || Dispatcher.FromThread(Thread.CurrentThread) == null)
- _reclaimImmediately = true;
- else
- StartUpdateTimer(startTimer, updateRef);
+ _updateRef = new WeakReference>(this);
+ _reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null;
+ EnsureUpdateTimer();
}
+
- static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef)
+ void EnsureUpdateTimer()
{
- Func 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 GetTimerProc(WeakReference> 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 : 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 : IDisposable
if (_usageStatistics[_usageStatisticsSlot] < _usage)
_usageStatistics[_usageStatisticsSlot] = _usage;
+ OnActivity();
+
if (_pool.Count != 0)
return _pool.Pop();
}
@@ -103,9 +137,10 @@ internal abstract class BatchStreamPoolBase : IDisposable
lock (_pool)
{
_usage--;
- if (!_disposed && !_reclaimImmediately)
+ if (!_disposed && !ReclaimImmediately)
{
_pool.Push(item);
+ OnActivity();
return;
}
}
diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
index 102cc30e87..cc24086305 100644
--- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
+++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
@@ -15,8 +15,7 @@ namespace Avalonia.Rendering
[PrivateApi]
public class DefaultRenderTimer : IRenderTimer
{
- private int _subscriberCount;
- private Action? _tick;
+ private volatile Action? _tick;
private IDisposable? _subscription;
///
@@ -36,40 +35,28 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; }
///
- public event Action Tick
+ public Action? 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;
}
}
///
public virtual bool RunsInBackground => true;
- ///
- /// Starts the timer.
- ///
- protected void Start()
- {
- _subscription = StartCore(InternalTick);
- }
-
///
/// Provides the implementation of starting the timer.
///
@@ -85,15 +72,6 @@ namespace Avalonia.Rendering
return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval);
}
- ///
- /// Stops the timer.
- ///
- protected void Stop()
- {
- _subscription?.Dispose();
- _subscription = null;
- }
-
private void InternalTick(TimeSpan tickCount)
{
_tick?.Invoke(tickCount);
diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs
index bf2c221b03..e887832ebc 100644
--- a/src/Avalonia.Base/Rendering/IRenderLoop.cs
+++ b/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.
///
- [NotClientImplementable]
- internal interface IRenderLoop
+ [PrivateApi]
+ public interface IRenderLoop
{
///
/// 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.
///
- void Add(IRenderLoopTask i);
+ internal void Add(IRenderLoopTask i);
///
/// Removes an update task.
///
/// The update task.
- void Remove(IRenderLoopTask i);
+ internal void Remove(IRenderLoopTask i);
///
/// Indicates if the rendering is done on a non-UI thread.
///
- bool RunsInBackground { get; }
+ internal bool RunsInBackground { get; }
+
+ ///
+ /// Wakes up the render loop to schedule the next tick.
+ /// Thread-safe: can be called from any thread.
+ ///
+ internal void Wakeup();
}
}
diff --git a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs
index f63855e651..67416cc155 100644
--- a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs
+++ b/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();
}
}
diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs
index 396e84d492..772dcf7656 100644
--- a/src/Avalonia.Base/Rendering/IRenderTimer.cs
+++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs
@@ -10,16 +10,19 @@ namespace Avalonia.Rendering
public interface IRenderTimer
{
///
- /// 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
///
///
- /// 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
///
- event Action Tick;
+ Action? Tick { get; set; }
///
- /// Indicates if the timer ticks on a non-UI thread
+ /// Indicates if the timer ticks on a non-UI thread.
///
bool RunsInBackground { get; }
}
diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs
index 846cce7a23..9af9c54443 100644
--- a/src/Avalonia.Base/Rendering/RenderLoop.cs
+++ b/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
{
///
- /// The application render loop.
+ /// Provides factory methods for creating instances.
+ ///
+ [PrivateApi]
+ public static class RenderLoop
+ {
+ ///
+ /// Creates an from an .
+ ///
+ public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer);
+ }
+
+ ///
+ /// Default implementation of the application render loop.
///
///
/// 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
+ /// 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.
///
- internal class RenderLoop : IRenderLoop
+ internal class DefaultRenderLoop : IRenderLoop
{
private readonly List _items = new List();
private readonly List _itemsCopy = new List();
- private IRenderTimer? _timer;
+ private Action _tick;
+ private readonly IRenderTimer _timer;
+ private readonly object _timerLock = new();
private int _inTick;
-
- public static IRenderLoop LocatorAutoInstance
- {
- get
- {
- var loop = AvaloniaLocator.Current.GetService();
- if (loop == null)
- {
- var timer = AvaloniaLocator.Current.GetRequiredService();
- AvaloniaLocator.CurrentMutable.Bind()
- .ToConstant(loop = new RenderLoop(timer));
- }
-
- return loop;
- }
- }
+ private volatile bool _hasItems;
+ private bool _running;
+ private bool _wakeupPending;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The render timer.
- public RenderLoop(IRenderTimer timer)
+ public DefaultRenderLoop(IRenderTimer timer)
{
_timer = timer;
- }
-
- ///
- /// Gets the render timer.
- ///
- protected IRenderTimer Timer
- {
- get
- {
- return _timer ??= AvaloniaLocator.Current.GetRequiredService();
- }
+ _tick = TimerTick;
}
///
@@ -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;
+ }
}
}
}
///
- public bool RunsInBackground => Timer.RunsInBackground;
+ public bool RunsInBackground => _timer.RunsInBackground;
+
+ ///
+ public void Wakeup()
+ {
+ lock (_timerLock)
+ {
+ if (_hasItems && !_running)
+ {
+ _running = true;
+ _timer.Tick = _tick;
+ }
+ else
+ {
+ _wakeupPending = true;
+ }
+ }
+ }
private void TimerTick(TimeSpan time)
{
@@ -98,21 +124,49 @@ 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)
{
_itemsCopy.Clear();
_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)
{
diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
index 3ad4ea94d0..570dc4cb30 100644
--- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
+++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
@@ -8,10 +8,10 @@ namespace Avalonia.Rendering
[PrivateApi]
public class SleepLoopRenderTimer : IRenderTimer
{
- private Action? _tick;
- private int _count;
- private readonly object _lock = new object();
- private bool _running;
+ private volatile Action? _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;
@@ -19,28 +19,30 @@ namespace Avalonia.Rendering
{
_timeBetweenTicks = TimeSpan.FromSeconds(1d / fps);
}
-
- public event Action Tick
+
+ public Action? 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);
-
}
}
-
-
}
}
diff --git a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs
index 0f3387cd1a..d15d3a052e 100644
--- a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs
+++ b/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? _tick;
- private int _subscriberCount;
+ private readonly object _lock = new();
+ private volatile Action? _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 Tick
+ public Action? 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;
}
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
index 43eddb010d..bc94ff4388 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
@@ -55,7 +55,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind().ToSingleton()
.Bind().ToConstant(Keyboard)
.Bind().ToSingleton()
- .Bind().ToConstant(new UiThreadRenderTimer(60))
+ .Bind().ToConstant(RenderLoop.FromTimer(new UiThreadRenderTimer(60)))
.Bind().ToConstant(instance)
.Bind().ToSingleton()
.Bind().ToSingleton();
diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
index d13d442e09..d9c8e333cb 100644
--- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs
+++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
@@ -69,16 +69,11 @@ namespace Avalonia.DesignerSupport.Remote
private sealed class DummyRenderTimer : IRenderTimer
{
- public event Action Tick
- {
- add { }
- remove { }
- }
-
+ public Action? 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()
{
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index 34f0ee766e..825eb254be 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -122,7 +122,7 @@ namespace Avalonia.Native
.Bind().ToConstant(this)
.Bind().ToConstant(clipboardImpl)
.Bind().ToConstant(clipboard)
- .Bind().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))
+ .Bind().ToConstant(RenderLoop.FromTimer(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))))
.Bind().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind().ToConstant(applicationPlatform)
diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
index 625de5d6bc..d484b82ac7 100644
--- a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
+++ b/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? _tick;
- private int _subscriberCount;
- private bool registered;
+ private volatile Action? _tick;
+ private bool _registered;
public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer)
{
@@ -19,42 +18,41 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim
_stopwatch = Stopwatch.StartNew();
}
- public event Action Tick
+ public Action? 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);
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index bff986d2d1..566b0d907a 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -87,7 +87,7 @@ namespace Avalonia.X11
: new X11PlatformThreading(this);
Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl);
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(timer)
+ .Bind().ToConstant(RenderLoop.FromTimer(timer))
.Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super"))
.Bind().ToFunc(() => KeyboardDevice)
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
index fea94d0248..de9a167954 100644
--- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
@@ -18,19 +18,16 @@ internal class BrowserRenderTimer : IRenderTimer
public bool RunsInBackground { get; }
- public event Action? Tick
+ public Action? Tick
{
- add
+ set
{
if (!BrowserWindowingPlatform.IsThreadingEnabled)
StartOnThisThread();
- _tick += value;
- }
- remove
- {
- _tick -= value;
+ _tick = value;
}
+ get => _tick;
}
public void StartOnThisThread()
@@ -50,4 +47,4 @@ internal class BrowserRenderTimer : IRenderTimer
tick.Invoke(TimeSpan.FromMilliseconds(timestamp));
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs
index 8d454ff582..1d9d1248b8 100644
--- a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs
+++ b/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 = new(() => new RenderLoop(RenderTimer), true);
+ public static Lazy RenderLoop = new(() => Avalonia.Rendering.RenderLoop.FromTimer(RenderTimer), true);
}
diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
index b56e686d4b..8e44942d32 100644
--- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
+++ b/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().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(new KeyboardDevice())
- .Bind().ToConstant(new RenderTimer(60))
+ .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60)))
.Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat))
.Bind().ToSingleton()
.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }));
@@ -99,9 +100,8 @@ namespace Avalonia.Headless
/// Count of frames to be ticked on the timer.
public static void ForceRenderTimerTick(int count = 1)
{
- var timer = AvaloniaLocator.Current.GetService() as RenderTimer;
for (var c = 0; c < count; c++)
- timer?.ForceTick();
+ s_renderTimer?.ForceTick();
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index ee8b85919e..3239957d73 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -64,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer
Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(timer)
+ .Bind().ToConstant(RenderLoop.FromTimer(timer))
.Bind().ToTransient()
.Bind().ToConstant(new KeyboardDevice())
.Bind().ToSingleton()
diff --git a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs
index 6c73fbeb9e..55d49ca30c 100644
--- a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs
+++ b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs
@@ -21,15 +21,36 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor
{
private static readonly Guid IID_IDCompositionDesktopDevice = Guid.Parse("5f4633fe-1e08-4cb8-8c75-ce24333f5602");
- public event Action? Tick;
+ private volatile Action? _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? Tick
+ {
+ get => _tick;
+ set
+ {
+ if (value != null)
+ {
+ _tick = value;
+ _stopped = false;
+ _wakeEvent.Set();
+ }
+ else
+ {
+ _stopped = true;
+ _tick = null;
+ }
+ }
+ }
private static bool TryCreateAndRegisterCore()
{
@@ -52,7 +73,7 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor
}
AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect);
- AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect);
+ AvaloniaLocator.CurrentMutable.Bind().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)
{
diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs
index 678b15e0d7..9ee2f25c86 100644
--- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs
+++ b/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? Tick;
+ private volatile Action? _tick;
private readonly object _syncLock;
+ private readonly AutoResetEvent _wakeEvent = new(false);
+ private volatile bool _stopped = true;
private IDXGIOutput? _output;
@@ -37,6 +40,25 @@ namespace Avalonia.Win32.DirectX
{
_syncLock = syncLock;
}
+
+ public Action? Tick
+ {
+ get => _tick;
+ set
+ {
+ if (value != null)
+ {
+ _tick = value;
+ _stopped = false;
+ _wakeEvent.Set();
+ }
+ else
+ {
+ _stopped = true;
+ _tick = null;
+ }
+ }
+ }
public static bool TryCreateAndRegister()
{
@@ -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().ToConstant(connection);
- AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection);
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connection));
tcs.SetResult(true);
connection.RunLoop();
}
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index f158d539ff..7903a62d8f 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -98,7 +98,7 @@ namespace Avalonia.Win32
.Bind().ToConstant(WindowsKeyboardDevice.Instance)
.Bind().ToSingleton()
.Bind().ToSingleton()
- .Bind().ToConstant(renderTimer)
+ .Bind().ToConstant(RenderLoop.FromTimer(renderTimer))
.Bind().ToConstant(s_instance)
.Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)
{
diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs
index fa7ff2e7a3..d4cdfcbe42 100644
--- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs
+++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs
@@ -17,8 +17,29 @@ namespace Avalonia.Win32.WinRT.Composition;
internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFactory
{
private readonly WinUiCompositionShared _shared;
- public event Action? Tick;
+ private readonly AutoResetEvent _wakeEvent = new(false);
+ private volatile bool _stopped = true;
+ private volatile Action? _tick;
public bool RunsInBackground => true;
+
+ public Action? Tick
+ {
+ get => _tick;
+ set
+ {
+ if (value != null)
+ {
+ _tick = value;
+ _stopped = false;
+ _wakeEvent.Set();
+ }
+ else
+ {
+ _stopped = true;
+ _tick = null;
+ }
+ }
+ }
public WinUiCompositorConnection()
{
@@ -58,7 +79,7 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa
});
connect = new WinUiCompositorConnection();
AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect);
- AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect);
+ AvaloniaLocator.CurrentMutable.Bind().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()
diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
index 676554811e..e8a313afa8 100644
--- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
+++ b/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? Tick;
+ private volatile Action? _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? Tick
+ {
+ get => _tick;
+ set => _tick = value;
+ }
+
private void OnLinkTick()
{
- Tick?.Invoke(_st.Elapsed);
+ _tick?.Invoke(_st.Elapsed);
}
}
}
diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs
index 29633a8609..79926a3836 100644
--- a/src/iOS/Avalonia.iOS/Platform.cs
+++ b/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().ToConstant(Timer)
+ .Bind().ToConstant(RenderLoop.FromTimer(Timer))
.Bind().ToConstant(keyboard);
if (appDelegate is not null)
diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
index cf6d7a8aee..21ac9c1ae1 100644
--- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
+++ b/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)
diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
index 7451e3c843..06df626857 100644
--- a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
+++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
@@ -22,7 +22,7 @@ public class CompositionTargetUpdateOnly : IDisposable
class Timer : IRenderTimer
{
- event Action IRenderTimer.Tick { add { } remove { } }
+ public Action 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();
diff --git a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs
index 588785cf69..5c44809a85 100644
--- a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs
+++ b/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
diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs
index 6247691453..1b6e7539a0 100644
--- a/tests/Avalonia.RenderTests/ManualRenderTimer.cs
+++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs
@@ -5,7 +5,7 @@ namespace Avalonia.Skia.RenderTests
{
public class ManualRenderTimer : IRenderTimer
{
- public event Action? Tick;
+ public Action? Tick { get; set; }
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
}
diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs
index 68cf05e9b9..87132881f7 100644
--- a/tests/Avalonia.RenderTests/TestRenderHelper.cs
+++ b/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))
diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs
index e90e1cff0e..cb2a84049c 100644
--- a/tests/Avalonia.UnitTests/CompositorTestServices.cs
+++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs
@@ -42,9 +42,10 @@ public class CompositorTestServices : IDisposable
_app = UnitTestApplication.Start(services);
try
{
- AvaloniaLocator.CurrentMutable.Bind().ToConstant(Timer);
+ var renderLoop = RenderLoop.FromTimer(Timer);
+ AvaloniaLocator.CurrentMutable.Bind().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? Tick;
+ public Action? Tick { get; set; }
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
}
diff --git a/tests/Avalonia.UnitTests/RendererMocks.cs b/tests/Avalonia.UnitTests/RendererMocks.cs
index 32d171e147..9b172fe342 100644
--- a/tests/Avalonia.UnitTests/RendererMocks.cs
+++ b/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