TestRenderHelper.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. using System.IO;
  2. using System.Runtime.CompilerServices;
  3. using Avalonia.Controls;
  4. using Avalonia.Media.Imaging;
  5. using Avalonia.Rendering;
  6. using SixLabors.ImageSharp;
  7. using Xunit;
  8. using Avalonia.Platform;
  9. using System.Threading.Tasks;
  10. using System;
  11. using System.Collections.Concurrent;
  12. using System.Collections.Generic;
  13. using System.Linq;
  14. using System.Reactive.Disposables;
  15. using System.Threading;
  16. using Avalonia.Controls.Platform.Surfaces;
  17. using Avalonia.Media;
  18. using Avalonia.Rendering.Composition;
  19. using Avalonia.Threading;
  20. using Avalonia.UnitTests;
  21. using Avalonia.Utilities;
  22. using SixLabors.ImageSharp.PixelFormats;
  23. using Image = SixLabors.ImageSharp.Image;
  24. using Avalonia.Skia;
  25. namespace Avalonia.Skia.RenderTests;
  26. static class TestRenderHelper
  27. {
  28. private static readonly TestDispatcherImpl s_dispatcherImpl =
  29. new TestDispatcherImpl();
  30. static TestRenderHelper()
  31. {
  32. SkiaPlatform.Initialize();
  33. AvaloniaLocator.CurrentMutable
  34. .Bind<IDispatcherImpl>()
  35. .ToConstant(s_dispatcherImpl);
  36. AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
  37. }
  38. public static Task RenderToFile(Control target, string path, bool immediate, double dpi = 96)
  39. {
  40. var dir = Path.GetDirectoryName(path);
  41. if (!Directory.Exists(dir))
  42. Directory.CreateDirectory(dir);
  43. var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
  44. var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
  45. var size = new Size(target.Width, target.Height);
  46. var dpiVector = new Vector(dpi, dpi);
  47. if (immediate)
  48. {
  49. using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
  50. {
  51. target.Measure(size);
  52. target.Arrange(new Rect(size));
  53. bitmap.Render(target);
  54. bitmap.Save(path);
  55. }
  56. }
  57. else
  58. {
  59. var timer = new ManualRenderTimer();
  60. var compositor = new Compositor(new RenderLoop(timer), null, true,
  61. new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
  62. using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat,
  63. factory.DefaultAlphaFormat))
  64. {
  65. var root = new TestRenderRoot(dpiVector.X / 96, null!);
  66. using (var renderer = new CompositingRenderer(root, compositor,
  67. () => new[] { new BitmapFramebufferSurface(writableBitmap) }))
  68. {
  69. root.Initialize(renderer, target);
  70. renderer.Start();
  71. Dispatcher.UIThread.RunJobs();
  72. renderer.Paint(new Rect(root.Bounds.Size), false);
  73. }
  74. writableBitmap.Save(path);
  75. }
  76. }
  77. return Task.CompletedTask;
  78. }
  79. class BitmapFramebufferSurface : IFramebufferPlatformSurface
  80. {
  81. private readonly IWriteableBitmapImpl _bitmap;
  82. public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
  83. {
  84. _bitmap = bitmap;
  85. }
  86. public IFramebufferRenderTarget CreateFramebufferRenderTarget()
  87. {
  88. return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
  89. }
  90. }
  91. public static void BeginTest()
  92. {
  93. s_dispatcherImpl.MainThread = Thread.CurrentThread;
  94. }
  95. public static void EndTest()
  96. {
  97. if (Dispatcher.UIThread.CheckAccess())
  98. Dispatcher.UIThread.RunJobs();
  99. }
  100. public static string GetTestsDirectory()
  101. {
  102. var path = Directory.GetCurrentDirectory();
  103. while (path.Length > 0 && Path.GetFileName(path) != "tests")
  104. {
  105. path = Path.GetDirectoryName(path);
  106. }
  107. return path;
  108. }
  109. private class TestDispatcherImpl : IDispatcherImpl
  110. {
  111. public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
  112. public Thread MainThread { get; set; }
  113. #pragma warning disable 67
  114. public event Action Signaled;
  115. public event Action Timer;
  116. #pragma warning restore 67
  117. public void Signal()
  118. {
  119. // No-op
  120. }
  121. public long Now => 0;
  122. public void UpdateTimer(long? dueTimeInMs)
  123. {
  124. // No-op
  125. }
  126. }
  127. public static void AssertCompareImages(string actualPath, string expectedPath)
  128. {
  129. using (var expected = Image.Load<Rgba32>(expectedPath))
  130. using (var actual = Image.Load<Rgba32>(actualPath))
  131. {
  132. double immediateError = TestRenderHelper.CompareImages(actual, expected);
  133. if (immediateError > 0.022)
  134. {
  135. Assert.Fail(actualPath + ": Error = " + immediateError);
  136. }
  137. }
  138. }
  139. /// <summary>
  140. /// Calculates root mean square error for given two images.
  141. /// Based roughly on ImageMagick implementation to ensure consistency.
  142. /// </summary>
  143. public static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
  144. {
  145. if (actual.Width != expected.Width || actual.Height != expected.Height)
  146. {
  147. throw new ArgumentException("Images have different resolutions");
  148. }
  149. var quantity = actual.Width * actual.Height;
  150. double squaresError = 0;
  151. const double scale = 1 / 255d;
  152. for (var x = 0; x < actual.Width; x++)
  153. {
  154. double localError = 0;
  155. for (var y = 0; y < actual.Height; y++)
  156. {
  157. var expectedAlpha = expected[x, y].A * scale;
  158. var actualAlpha = actual[x, y].A * scale;
  159. var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
  160. var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
  161. var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
  162. var a = expectedAlpha - actualAlpha;
  163. var error = r * r + g * g + b * b + a * a;
  164. localError += error;
  165. }
  166. squaresError += localError;
  167. }
  168. var meanSquaresError = squaresError / quantity;
  169. const int channelCount = 4;
  170. meanSquaresError = meanSquaresError / channelCount;
  171. return Math.Sqrt(meanSquaresError);
  172. }
  173. }