Browse Source

[WIP] Run render tests for compositing renderer

pull/8105/head
Nikita Tsukanov 4 years ago
parent
commit
b6b08155d8
  1. 3
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  2. 6
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  3. 7
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  4. 2
      src/Avalonia.Base/Rendering/DefaultRenderTimer.cs
  5. 2
      src/Avalonia.Base/Rendering/IRenderLoop.cs
  6. 5
      src/Avalonia.Base/Rendering/IRenderTimer.cs
  7. 2
      src/Avalonia.Base/Rendering/RenderLoop.cs
  8. 2
      src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs
  9. 3
      src/Avalonia.Base/Visual.cs
  10. 1
      src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs
  11. 1
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
  12. 2
      src/iOS/Avalonia.iOS/DisplayLinkTimer.cs
  13. 19
      tests/Avalonia.RenderTests/ManualRenderTimer.cs
  14. 58
      tests/Avalonia.RenderTests/TestBase.cs
  15. 48
      tests/Avalonia.RenderTests/TestRenderRoot.cs

3
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<TimeSpan> Tick
{

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

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

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

@ -59,6 +59,8 @@ namespace Avalonia.Rendering
}
}
public bool RunsInBackground => true;
/// <summary>
/// Starts the timer.
/// </summary>

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

@ -27,5 +27,7 @@ namespace Avalonia.Rendering
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
bool RunsInBackground { get; }
}
}

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

@ -18,5 +18,10 @@ namespace Avalonia.Rendering
/// switch execution to the right thread.
/// </remarks>
event Action<TimeSpan> Tick;
/// <summary>
/// Indicates if the timer ticks on a non-UI thread
/// </summary>
bool RunsInBackground { get; }
}
}

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

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

@ -43,6 +43,8 @@ namespace Avalonia.Rendering
}
}
public bool RunsInBackground => true;
void LoopProc()
{
var lastTick = _st.Elapsed;

3
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;
}

1
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<TimeSpan>? Tick;
public bool RunsInBackground => false;
}
}

1
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs

@ -302,5 +302,6 @@ namespace Avalonia.Win32.WinRT.Composition
}
public event Action<TimeSpan> Tick;
public bool RunsInBackground => true;
}
}

2
src/iOS/Avalonia.iOS/DisplayLinkTimer.cs

@ -28,6 +28,8 @@ namespace Avalonia.iOS
}
public Thread TimerThread { get; }
public bool RunsInBackground => true;
private void OnLinkTick()
{

19
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<TimeSpan> Tick;
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
public Task TriggerBackgroundTick() => Task.Run(TriggerTick);
}
}

58
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<IPlatformRenderInterface>();
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<Rgba32>(expectedPath))
using (var immediate = Image.Load<Rgba32>(immediatePath))
using (var deferred = Image.Load<Rgba32>(deferredPath))
using (var composited = Image.Load<Rgba32>(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<Action> 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);
});
}
}
}

48
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);
}
}
Loading…
Cancel
Save