TestRenderHelper.cs 6.7 KB

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