diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 1d898261a3..19dc7b4ab6 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -29,6 +29,9 @@ namespace Avalonia.Android _thread = new Thread(Loop); _thread.Start(); } + + + public bool RunsInBackground => true; public event Action Tick { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 5bdd7847c4..c67142648a 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -232,7 +232,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { Update(); _target.RequestRedraw(); - if(RenderOnlyOnRenderThread) + if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) Compositor.RequestCommitAsync().Wait(); else _target.ImmediateUIThreadRender(); @@ -249,9 +249,11 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { Stop(); _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that // render target is not used anymore and can be safely disposed - _compositor.RequestCommitAsync().Wait(); + if (Compositor.Loop.RunsInBackground) + _compositor.RequestCommitAsync().Wait(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 03aebba907..e92b69bd60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -15,6 +15,7 @@ namespace Avalonia.Rendering.Composition { public partial class Compositor { + internal IRenderLoop Loop { get; } private ServerCompositor _server; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; @@ -26,6 +27,7 @@ namespace Avalonia.Rendering.Composition public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { + Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, @@ -60,11 +62,6 @@ namespace Avalonia.Rendering.Composition return batch.Completed; } - public void Dispose() - { - - } - public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 82d3892975..d0d3dd9715 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -59,6 +59,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + /// /// Starts the timer. /// diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index 9838967261..e500ecdf8b 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,5 +27,7 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); + + bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index ee74c345be..07af7eeec8 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -18,5 +18,10 @@ namespace Avalonia.Rendering /// switch execution to the right thread. /// event Action Tick; + + /// + /// 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 a5d7e15f93..c66fec92aa 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => Timer.RunsInBackground; + private void TimerTick(TimeSpan time) { if (Interlocked.CompareExchange(ref _inTick, 1, 0) == 0) diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 86595754e9..8544de4bbc 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -43,6 +43,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + void LoopProc() { var lastTick = _st.Elapsed; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 2f4fbbd3f0..0bfdd6d516 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -469,8 +469,11 @@ namespace Avalonia internal CompositionVisual AttachToCompositor(Compositor compositor) { if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + { CompositionVisual = new CompositionDrawListVisual(compositor, new ServerCompositionDrawListVisual(compositor.Server, this), this); + } + return CompositionVisual; } diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs index 1a06d47744..7b9feab2e3 100644 --- a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs @@ -12,5 +12,6 @@ namespace Avalonia.Web.Blazor public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); public event Action? Tick; + public bool RunsInBackground => false; } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 76af12e8ca..5334b90d62 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -302,5 +302,6 @@ namespace Avalonia.Win32.WinRT.Composition } public event Action Tick; + public bool RunsInBackground => true; } } diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index df73479a65..eb124fd450 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -28,6 +28,8 @@ namespace Avalonia.iOS } public Thread TimerThread { get; } + + public bool RunsInBackground => true; private void OnLinkTick() { diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs new file mode 100644 index 0000000000..0dc994aaa5 --- /dev/null +++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class ManualRenderTimer : IRenderTimer + { + public event Action Tick; + public bool RunsInBackground => false; + public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); + public Task TriggerBackgroundTick() => Task.Run(TriggerTick); + } +} \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 39250f2aa7..4d6b313ffc 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -8,8 +8,13 @@ using Xunit; using Avalonia.Platform; using System.Threading.Tasks; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; using System.Threading; using Avalonia.Media; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -38,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests new TestThreadingInterface(); private static readonly IAssetLoader assetLoader = new AssetLoader(); - + static TestBase() { #if AVALONIA_SKIA @@ -84,6 +89,7 @@ namespace Avalonia.Direct2D1.RenderTests var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); var factory = AvaloniaLocator.Current.GetService(); var pixelSize = new PixelSize((int)target.Width, (int)target.Height); var size = new Size(target.Width, target.Height); @@ -96,7 +102,8 @@ namespace Avalonia.Direct2D1.RenderTests bitmap.Render(target); bitmap.Save(immediatePath); } - + + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) using (var renderer = new DeferredRenderer(target, rtb)) { @@ -107,9 +114,30 @@ namespace Avalonia.Direct2D1.RenderTests // Do the deferred render on a background thread to expose any threading errors in // the deferred rendering path. await Task.Run((Action)renderer.UnitTestRender); + threadingInterface.MainThread = Thread.CurrentThread; rtb.Save(deferredPath); } + + var timer = new ManualRenderTimer(); + + var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + { + var root = new TestRenderRoot(dpiVector.X / 96, rtb); + using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + { + root.Initialize(renderer, target); + renderer.Start(); + Dispatcher.UIThread.RunJobs(); + timer.TriggerTick(); + } + + // Free pools + for (var c = 0; c < 11; c++) + TestThreadingInterface.RunTimers(); + rtb.Save(compositedPath); + } } protected void CompareImages([CallerMemberName] string testName = "") @@ -117,13 +145,16 @@ namespace Avalonia.Direct2D1.RenderTests var expectedPath = Path.Combine(OutputPath, testName + ".expected.png"); var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); using (var expected = Image.Load(expectedPath)) using (var immediate = Image.Load(immediatePath)) using (var deferred = Image.Load(deferredPath)) + using (var composited = Image.Load(compositedPath)) { var immediateError = CompareImages(immediate, expected); var deferredError = CompareImages(deferred, expected); + var compositedError = CompareImages(composited, expected); if (immediateError > 0.022) { @@ -134,6 +165,11 @@ namespace Avalonia.Direct2D1.RenderTests { Assert.True(false, deferredPath + ": Error = " + deferredError); } + + if (compositedError > 0.022) + { + Assert.True(false, compositedPath + ": Error = " + compositedError); + } } } @@ -233,9 +269,25 @@ namespace Avalonia.Direct2D1.RenderTests // No-op } + private static List s_timers = new(); + + public static void RunTimers() + { + lock (s_timers) + { + foreach(var t in s_timers.ToList()) + t.Invoke(); + } + } + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - throw new NotImplementedException(); + var act = () => tick(); + lock (s_timers) s_timers.Add(act); + return Disposable.Create(() => + { + lock (s_timers) s_timers.Remove(act); + }); } } } diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs new file mode 100644 index 0000000000..8f2b324d9c --- /dev/null +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -0,0 +1,48 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; +using Avalonia.Controls; +using Avalonia.Platform; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class TestRenderRoot : Decorator, IRenderRoot + { + private readonly IRenderTarget _renderTarget; + public Size ClientSize { get; private set; } + public IRenderer Renderer { get; private set; } + public double RenderScaling { get; } + + public TestRenderRoot(double scaling, IRenderTarget renderTarget) + { + _renderTarget = renderTarget; + RenderScaling = scaling; + } + + public void Initialize(IRenderer renderer, Control child) + { + Renderer = renderer; + Child = child; + Width = child.Width; + Height = child.Height; + ClientSize = new Size(Width, Height); + Measure(ClientSize); + Arrange(new Rect(ClientSize)); + } + + public IRenderTarget CreateRenderTarget() => _renderTarget; + + public void Invalidate(Rect rect) + { + } + + public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); + + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); + } +} \ No newline at end of file