TestBase.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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. #if AVALONIA_SKIA
  25. using Avalonia.Skia;
  26. #else
  27. using Avalonia.Direct2D1;
  28. #endif
  29. #if AVALONIA_SKIA
  30. namespace Avalonia.Skia.RenderTests
  31. #else
  32. namespace Avalonia.Direct2D1.RenderTests
  33. #endif
  34. {
  35. public class TestBase : IDisposable
  36. {
  37. #if AVALONIA_SKIA
  38. private static string s_fontUri = "resm:Avalonia.Skia.RenderTests.Assets?assembly=Avalonia.Skia.RenderTests#Noto Mono";
  39. #else
  40. private static string s_fontUri = "resm:Avalonia.Direct2D1.RenderTests.Assets?assembly=Avalonia.Direct2D1.RenderTests#Noto Mono";
  41. #endif
  42. public static FontFamily TestFontFamily = new FontFamily(s_fontUri);
  43. private static readonly TestDispatcherImpl threadingInterface =
  44. new TestDispatcherImpl();
  45. private static readonly IAssetLoader assetLoader = new StandardAssetLoader();
  46. static TestBase()
  47. {
  48. #if AVALONIA_SKIA
  49. SkiaPlatform.Initialize();
  50. #else
  51. Direct2D1Platform.Initialize();
  52. #endif
  53. AvaloniaLocator.CurrentMutable
  54. .Bind<IDispatcherImpl>()
  55. .ToConstant(threadingInterface);
  56. AvaloniaLocator.CurrentMutable
  57. .Bind<IAssetLoader>()
  58. .ToConstant(assetLoader);
  59. }
  60. public TestBase(string outputPath)
  61. {
  62. outputPath = outputPath.Replace('\\', Path.DirectorySeparatorChar);
  63. var testPath = GetTestsDirectory();
  64. var testFiles = Path.Combine(testPath, "TestFiles");
  65. #if AVALONIA_SKIA
  66. var platform = "Skia";
  67. #else
  68. var platform = "Direct2D1";
  69. #endif
  70. OutputPath = Path.Combine(testFiles, platform, outputPath);
  71. threadingInterface.MainThread = Thread.CurrentThread;
  72. }
  73. public string OutputPath
  74. {
  75. get;
  76. }
  77. protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
  78. {
  79. if (!Directory.Exists(OutputPath))
  80. {
  81. Directory.CreateDirectory(OutputPath);
  82. }
  83. var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
  84. var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
  85. var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
  86. var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
  87. var size = new Size(target.Width, target.Height);
  88. var dpiVector = new Vector(dpi, dpi);
  89. using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
  90. {
  91. target.Measure(size);
  92. target.Arrange(new Rect(size));
  93. bitmap.Render(target);
  94. bitmap.Save(immediatePath);
  95. }
  96. var timer = new ManualRenderTimer();
  97. var compositor = new Compositor(new RenderLoop(timer), null, true,
  98. new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
  99. using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat))
  100. {
  101. var root = new TestRenderRoot(dpiVector.X / 96, null!);
  102. using (var renderer = new CompositingRenderer(root, compositor, () => new[]
  103. {
  104. new BitmapFramebufferSurface(writableBitmap)
  105. }))
  106. {
  107. root.Initialize(renderer, target);
  108. renderer.Start();
  109. Dispatcher.UIThread.RunJobs();
  110. timer.TriggerTick();
  111. }
  112. writableBitmap.Save(compositedPath);
  113. }
  114. }
  115. class BitmapFramebufferSurface : IFramebufferPlatformSurface
  116. {
  117. private readonly IWriteableBitmapImpl _bitmap;
  118. public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
  119. {
  120. _bitmap = bitmap;
  121. }
  122. public IFramebufferRenderTarget CreateFramebufferRenderTarget()
  123. {
  124. return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
  125. }
  126. }
  127. protected void CompareImages([CallerMemberName] string testName = "",
  128. bool skipImmediate = false, bool skipCompositor = false)
  129. {
  130. var expectedPath = Path.Combine(OutputPath, testName + ".expected.png");
  131. var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
  132. var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
  133. using (var expected = Image.Load<Rgba32>(expectedPath))
  134. using (var immediate = Image.Load<Rgba32>(immediatePath))
  135. using (var composited = Image.Load<Rgba32>(compositedPath))
  136. {
  137. var immediateError = CompareImages(immediate, expected);
  138. var compositedError = CompareImages(composited, expected);
  139. if (immediateError > 0.022 && !skipImmediate)
  140. {
  141. Assert.True(false, immediatePath + ": Error = " + immediateError);
  142. }
  143. if (compositedError > 0.022 && !skipCompositor)
  144. {
  145. Assert.True(false, compositedPath + ": Error = " + compositedError);
  146. }
  147. }
  148. }
  149. protected void CompareImagesNoRenderer([CallerMemberName] string testName = "", string expectedName = null)
  150. {
  151. var expectedPath = Path.Combine(OutputPath, (expectedName ?? testName) + ".expected.png");
  152. var actualPath = Path.Combine(OutputPath, testName + ".out.png");
  153. using (var expected = Image.Load<Rgba32>(expectedPath))
  154. using (var actual = Image.Load<Rgba32>(actualPath))
  155. {
  156. double immediateError = CompareImages(actual, expected);
  157. if (immediateError > 0.022)
  158. {
  159. Assert.True(false, actualPath + ": Error = " + immediateError);
  160. }
  161. }
  162. }
  163. /// <summary>
  164. /// Calculates root mean square error for given two images.
  165. /// Based roughly on ImageMagick implementation to ensure consistency.
  166. /// </summary>
  167. private static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
  168. {
  169. if (actual.Width != expected.Width || actual.Height != expected.Height)
  170. {
  171. throw new ArgumentException("Images have different resolutions");
  172. }
  173. var quantity = actual.Width * actual.Height;
  174. double squaresError = 0;
  175. const double scale = 1 / 255d;
  176. for (var x = 0; x < actual.Width; x++)
  177. {
  178. double localError = 0;
  179. for (var y = 0; y < actual.Height; y++)
  180. {
  181. var expectedAlpha = expected[x, y].A * scale;
  182. var actualAlpha = actual[x, y].A * scale;
  183. var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
  184. var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
  185. var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
  186. var a = expectedAlpha - actualAlpha;
  187. var error = r * r + g * g + b * b + a * a;
  188. localError += error;
  189. }
  190. squaresError += localError;
  191. }
  192. var meanSquaresError = squaresError / quantity;
  193. const int channelCount = 4;
  194. meanSquaresError = meanSquaresError / channelCount;
  195. return Math.Sqrt(meanSquaresError);
  196. }
  197. private static string GetTestsDirectory()
  198. {
  199. var path = Directory.GetCurrentDirectory();
  200. while (path.Length > 0 && Path.GetFileName(path) != "tests")
  201. {
  202. path = Path.GetDirectoryName(path);
  203. }
  204. return path;
  205. }
  206. private class TestDispatcherImpl : IDispatcherImpl
  207. {
  208. public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
  209. public Thread MainThread { get; set; }
  210. #pragma warning disable 67
  211. public event Action Signaled;
  212. public event Action Timer;
  213. #pragma warning restore 67
  214. public void Signal()
  215. {
  216. // No-op
  217. }
  218. public long Now => 0;
  219. public void UpdateTimer(long? dueTimeInMs)
  220. {
  221. // No-op
  222. }
  223. }
  224. public void Dispose()
  225. {
  226. if (Dispatcher.UIThread.CheckAccess())
  227. {
  228. Dispatcher.UIThread.RunJobs();
  229. }
  230. }
  231. }
  232. }