TestBase.cs 10 KB

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