TestBase.cs 11 KB

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