Browse Source

Run render tests against llvmpipe/lavapipe MESA rasterizers (#21790)

* Run render tests against llvmpipe/lavapipe MESA rasterizers

* CI?

* Try desktop gl on mac?

* disable vulkan on mac
pull/21823/head
Nikita Tsukanov 2 months ago
committed by GitHub
parent
commit
efaa9260bc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Directory.Packages.props
  2. 3
      src/Avalonia.OpenGL/Egl/EglConsts.cs
  3. 3
      src/Avalonia.OpenGL/Egl/EglDisplay.cs
  4. 6
      src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs
  5. 9
      src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs
  6. 3
      src/Avalonia.OpenGL/GlInterface.cs
  7. 4
      src/Avalonia.Vulkan/Avalonia.Vulkan.csproj
  8. 11
      src/Avalonia.Vulkan/IVulkanPlatformSurface.cs
  9. 8
      src/Avalonia.Vulkan/Interop/VulkanDevice.Create.cs
  10. 5
      src/Avalonia.Vulkan/Interop/VulkanInstance.cs
  11. 38
      src/Avalonia.Vulkan/UnmanagedInterop/VulkanDeviceApi.cs
  12. 7
      src/Avalonia.Vulkan/UnmanagedInterop/VulkanEnums.cs
  13. 10
      src/Avalonia.Vulkan/UnmanagedInterop/VulkanInstanceApi.cs
  14. 27
      src/Avalonia.Vulkan/UnmanagedInterop/VulkanStructs.cs
  15. 3
      src/Avalonia.Vulkan/VulkanContext.cs
  16. 14
      src/Avalonia.Vulkan/VulkanOptions.cs
  17. 14
      src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs
  18. 4
      tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs
  19. 6
      tests/Avalonia.RenderTests/Controls/AdornerTests.cs
  20. 2
      tests/Avalonia.RenderTests/Controls/CommandBarTests.cs
  21. 4
      tests/Avalonia.RenderTests/Media/DrawingContentTests.cs
  22. 2
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  23. 202
      tests/Avalonia.RenderTests/Mesa/FboReadbackGlPlatformSurface.cs
  24. 98
      tests/Avalonia.RenderTests/Mesa/MesaSoftwareRenderer.cs
  25. 200
      tests/Avalonia.RenderTests/Mesa/VulkanReadbackPlatformSurface.cs
  26. 2
      tests/Avalonia.RenderTests/Shapes/EllipseTests.cs
  27. 2
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  28. 48
      tests/Avalonia.RenderTests/TestBase.cs
  29. 109
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  30. 1
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

1
Directory.Packages.props

@ -66,6 +66,7 @@
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="unofficial.mesa.softwarerenderer" Version="25.3.6.29" />
<PackageVersion Include="Tmds.DBus.Generator" Version="0.94.1" />
<PackageVersion Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.3" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0.2" />

3
src/Avalonia.OpenGL/Egl/EglConsts.cs

@ -205,6 +205,9 @@ namespace Avalonia.OpenGL.Egl
// EGL_KHR_platform_gbm / EGL_MESA_platform_gbm
public const int EGL_PLATFORM_GBM_KHR = 0x31D7;
// EGL_MESA_platform_surfaceless
public const int EGL_PLATFORM_SURFACELESS_MESA = 0x31DD;
public const int EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE = 0x3207;
public const int EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE = 0x3208;

3
src/Avalonia.OpenGL/Egl/EglDisplay.cs

@ -45,7 +45,8 @@ namespace Avalonia.OpenGL.Egl
if(_display == IntPtr.Zero)
throw new ArgumentException();
_config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions, options.ProbeConfig);
_config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions, options.ProbeConfig,
options.AllowPbufferOnlyConfigs);
}
public EglInterface EglInterface => _egl;

6
src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs

@ -16,6 +16,12 @@ public class EglDisplayOptions
public EglInterface? Egl { get; set; }
public bool SupportsContextSharing { get; set; }
public bool SupportsMultipleContexts { get; set; }
/// <summary>
/// Also considers configs that can only be used with PBuffer surfaces. Required for EGL platforms
/// that have no window surface support at all, e.g. EGL_MESA_platform_surfaceless.
/// </summary>
public bool AllowPbufferOnlyConfigs { get; set; }
public bool ContextLossIsDisplayLoss { get; set; }
public Func<bool>? DeviceLostCheckCallback { get; set; }
public Action? DisposeCallback { get; set; }

9
src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs

@ -50,7 +50,8 @@ internal static class EglDisplayUtils
}
public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display,
IEnumerable<GlVersion>? versions, EglConfigProbeCallback? probeConfig = null)
IEnumerable<GlVersion>? versions, EglConfigProbeCallback? probeConfig = null,
bool allowPbufferOnlyConfigs = false)
{
if (!egl.Initialize(display, out _, out _))
throw OpenGlException.GetFormattedException("eglInitialize", egl);
@ -123,11 +124,15 @@ internal static class EglDisplayUtils
}
});
var surfaceTypes = allowPbufferOnlyConfigs
? new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT, EGL_PBUFFER_BIT }
: new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT };
foreach (var cfg in cfgs)
{
if (!egl.BindApi(cfg.Api))
continue;
foreach (var surfaceType in new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT })
foreach (var surfaceType in surfaceTypes)
foreach (var stencilSize in new[] { 8, 1, 0 })
foreach (var depthSize in new[] { 8, 1, 0 })
{

3
src/Avalonia.OpenGL/GlInterface.cs

@ -103,6 +103,9 @@ namespace Avalonia.OpenGL
[GetProcAddress("glFinish")]
public partial void Finish();
[GetProcAddress("glReadPixels")]
public partial void ReadPixels(int x, int y, int width, int height, int format, int type, IntPtr data);
[GetProcAddress("glGenFramebuffers")]
public partial void GenFramebuffers(int count, int* res);

4
src/Avalonia.Vulkan/Avalonia.Vulkan.csproj

@ -10,6 +10,10 @@
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\TrimmingEnable.props" />

11
src/Avalonia.Vulkan/IVulkanPlatformSurface.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Metadata;
using Avalonia.Platform.Surfaces;
namespace Avalonia.Vulkan;
@ -9,6 +10,16 @@ public interface IVulkanKhrSurfacePlatformSurface : IDisposable, IPlatformRender
ulong CreateSurface(IVulkanPlatformGraphicsContext context);
}
/// <summary>
/// A platform render surface that provides its own <see cref="IVulkanRenderTarget"/>,
/// e.g. an offscreen image-based target that doesn't involve a swapchain.
/// </summary>
[PrivateApi]
public interface IVulkanRenderTargetPlatformSurface : IPlatformRenderSurface
{
IVulkanRenderTarget CreateRenderTarget(IVulkanPlatformGraphicsContext context);
}
public interface IVulkanKhrSurfacePlatformSurfaceFactory
{
bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, IPlatformRenderSurface surface);

8
src/Avalonia.Vulkan/Interop/VulkanDevice.Create.cs

@ -68,8 +68,10 @@ internal unsafe partial class VulkanDevice
var enableExtensions =
new HashSet<string>(options.DeviceExtensions.Concat(VulkanExternalObjectsFeature.RequiredDeviceExtensions));
var enabledExtensions = enableExtensions
.Intersect(dev.Extensions).Append(VK_KHR_swapchain).Distinct().ToArray();
var filteredExtensions = enableExtensions.Intersect(dev.Extensions);
if (options.RequireSwapchainExtension)
filteredExtensions = filteredExtensions.Append(VK_KHR_swapchain);
var enabledExtensions = filteredExtensions.Distinct().ToArray();
using var pEnabledExtensions = new Utf8BufferArray(enabledExtensions);
@ -127,7 +129,7 @@ internal unsafe partial class VulkanDevice
instance.GetPhysicalDeviceProperties(physicalDevice, out var properties);
var supportedExtensions = GetDeviceExtensions(instance, physicalDevice);
if (!supportedExtensions.Contains(VK_KHR_swapchain))
if (options.RequireSwapchainExtension && !supportedExtensions.Contains(VK_KHR_swapchain))
return null;
uint familyCount = 0;

5
src/Avalonia.Vulkan/Interop/VulkanInstance.cs

@ -48,8 +48,9 @@ internal class VulkanInstance : IVulkanInstance
};
var enabledExtensions = new HashSet<string>(options.InstanceExtensions
.Concat(platformOptions.RequiredInstanceExtensions)
.Append("VK_KHR_surface"));
.Concat(platformOptions.RequiredInstanceExtensions));
if (options.RequireSurfaceExtension)
enabledExtensions.Add("VK_KHR_surface");
var enabledLayers = options.EnabledLayers.ToList();

38
src/Avalonia.Vulkan/UnmanagedInterop/VulkanDeviceApi.cs

@ -103,6 +103,32 @@ internal unsafe partial class VulkanDeviceApi
public partial void DestroyImageView(VkDevice device, VkImageView imageView, IntPtr pAllocator);
[GetProcAddress("vkCreateBuffer")]
public partial VkResult CreateBuffer(VkDevice device, ref VkBufferCreateInfo pCreateInfo, IntPtr pAllocator,
out VkBuffer pBuffer);
[GetProcAddress("vkDestroyBuffer")]
public partial void DestroyBuffer(VkDevice device, VkBuffer buffer, IntPtr pAllocator);
[GetProcAddress("vkGetBufferMemoryRequirements")]
public partial void GetBufferMemoryRequirements(VkDevice device, VkBuffer buffer,
out VkMemoryRequirements pMemoryRequirements);
[GetProcAddress("vkBindBufferMemory")]
public partial VkResult BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory,
VkDeviceSize memoryOffset);
[GetProcAddress("vkMapMemory")]
public partial VkResult MapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size,
uint32_t flags, out IntPtr ppData);
[GetProcAddress("vkUnmapMemory")]
public partial void UnmapMemory(VkDevice device, VkDeviceMemory memory);
[GetProcAddress("vkCmdCopyImageToBuffer")]
public partial void CmdCopyImageToBuffer(VkCommandBuffer commandBuffer, VkImage srcImage,
VkImageLayout srcImageLayout, VkBuffer dstBuffer, uint32_t regionCount, VkBufferImageCopy* pRegions);
[GetProcAddress("vkCmdPipelineBarrier")]
public partial void CmdPipelineBarrier(VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask,
@ -115,14 +141,14 @@ internal unsafe partial class VulkanDeviceApi
uint32_t imageMemoryBarrierCount,
VkImageMemoryBarrier* pImageMemoryBarriers);
[GetProcAddress("vkCreateSwapchainKHR")]
[GetProcAddress("vkCreateSwapchainKHR", true)]
public partial VkResult CreateSwapchainKHR(VkDevice device, ref VkSwapchainCreateInfoKHR pCreateInfo,
IntPtr pAllocator, out VkSwapchainKHR pSwapchain);
[GetProcAddress("vkDestroySwapchainKHR")]
[GetProcAddress("vkDestroySwapchainKHR", true)]
public partial void DestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain, IntPtr pAllocator);
[GetProcAddress("vkGetSwapchainImagesKHR")]
[GetProcAddress("vkGetSwapchainImagesKHR", true)]
public partial VkResult GetSwapchainImagesKHR(VkDevice device, VkSwapchainKHR swapchain, ref uint32_t pSwapchainImageCount,
VkImage* pSwapchainImages);
@ -132,7 +158,7 @@ internal unsafe partial class VulkanDeviceApi
[GetProcAddress("vkQueueWaitIdle")]
public partial VkResult QueueWaitIdle(VkQueue queue);
[GetProcAddress("vkAcquireNextImageKHR")]
[GetProcAddress("vkAcquireNextImageKHR", true)]
public partial VkResult AcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout,
VkSemaphore semaphore, VkFence fence, out uint32_t pImageIndex);
@ -140,7 +166,7 @@ internal unsafe partial class VulkanDeviceApi
public partial void CmdBlitImage(VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout,
VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, VkImageBlit* pRegions, VkFilter filter);
[GetProcAddress("vkQueuePresentKHR")]
[GetProcAddress("vkQueuePresentKHR", true)]
public partial VkResult vkQueuePresentKHR(VkQueue queue, ref VkPresentInfoKHR pPresentInfo);
[GetProcAddress("vkImportSemaphoreFdKHR", true)]

7
src/Avalonia.Vulkan/UnmanagedInterop/VulkanEnums.cs

@ -1688,6 +1688,13 @@ namespace Avalonia.Vulkan.UnmanagedInterop
VK_ACCESS_NONE_KHR = VK_ACCESS_NONE,
}
[Flags]
enum VkBufferUsageFlags
{
VK_BUFFER_USAGE_TRANSFER_SRC_BIT = 0x00000001,
VK_BUFFER_USAGE_TRANSFER_DST_BIT = 0x00000002,
}
[Flags]
enum VkImageUsageFlags
{

10
src/Avalonia.Vulkan/UnmanagedInterop/VulkanInstanceApi.cs

@ -32,7 +32,7 @@ internal unsafe partial class VulkanInstanceApi
public partial VkResult EnumerateDeviceExtensionProperties(VkPhysicalDevice physicalDevice, byte* pLayerName,
ref uint32_t pPropertyCount, VkExtensionProperties* pProperties);
[GetProcAddress("vkGetPhysicalDeviceSurfaceSupportKHR")]
[GetProcAddress("vkGetPhysicalDeviceSurfaceSupportKHR", true)]
public partial VkResult GetPhysicalDeviceSurfaceSupportKHR(VkPhysicalDevice physicalDevice,
uint32_t queueFamilyIndex,
VkSurfaceKHR surface, out VkBool32 pSupported);
@ -56,10 +56,10 @@ internal unsafe partial class VulkanInstanceApi
[GetProcAddress("vkGetDeviceProcAddr")]
public partial IntPtr GetDeviceProcAddr(VkDevice device, IntPtr pName);
[GetProcAddress("vkDestroySurfaceKHR")]
[GetProcAddress("vkDestroySurfaceKHR", true)]
public partial void DestroySurfaceKHR(VkInstance instance, VkSurfaceKHR surface, IntPtr pAllocator);
[GetProcAddress("vkGetPhysicalDeviceSurfaceFormatsKHR")]
[GetProcAddress("vkGetPhysicalDeviceSurfaceFormatsKHR", true)]
public partial VkResult GetPhysicalDeviceSurfaceFormatsKHR(
VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface,
@ -70,11 +70,11 @@ internal unsafe partial class VulkanInstanceApi
public partial void GetPhysicalDeviceMemoryProperties(VkPhysicalDevice physicalDevice,
out VkPhysicalDeviceMemoryProperties pMemoryProperties);
[GetProcAddress("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")]
[GetProcAddress("vkGetPhysicalDeviceSurfaceCapabilitiesKHR", true)]
public partial VkResult GetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
out VkSurfaceCapabilitiesKHR pSurfaceCapabilities);
[GetProcAddress("vkGetPhysicalDeviceSurfacePresentModesKHR")]
[GetProcAddress("vkGetPhysicalDeviceSurfacePresentModesKHR", true)]
public partial VkResult GetPhysicalDeviceSurfacePresentModesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
ref uint32_t pPresentModeCount, VkPresentModeKHR* pPresentModes);

27
src/Avalonia.Vulkan/UnmanagedInterop/VulkanStructs.cs

@ -700,6 +700,33 @@ namespace Avalonia.Vulkan.UnmanagedInterop
public VkOffset3D dstOffsets2;
}
struct VkBuffer
{
public ulong Handle;
}
unsafe struct VkBufferCreateInfo
{
public VkStructureType sType;
public void* pNext;
public uint32_t flags;
public VkDeviceSize size;
public VkBufferUsageFlags usage;
public VkSharingMode sharingMode;
public uint32_t queueFamilyIndexCount;
public uint32_t* pQueueFamilyIndices;
}
struct VkBufferImageCopy
{
public VkDeviceSize bufferOffset;
public uint32_t bufferRowLength;
public uint32_t bufferImageHeight;
public VkImageSubresourceLayers imageSubresource;
public VkOffset3D imageOffset;
public VkExtent3D imageExtent;
}
unsafe struct VkPresentInfoKHR
{
public VkStructureType sType;

3
src/Avalonia.Vulkan/VulkanContext.cs

@ -63,6 +63,9 @@ internal class VulkanContext : IVulkanPlatformGraphicsContext
{
foreach (var surf in surfaces)
{
if (surf is IVulkanRenderTargetPlatformSurface renderTargetSurface)
return renderTargetSurface.CreateRenderTarget(this);
IVulkanKhrSurfacePlatformSurface khrSurface;
if (surf is IVulkanKhrSurfacePlatformSurface khr)
khrSurface = khr;

14
src/Avalonia.Vulkan/VulkanOptions.cs

@ -38,6 +38,12 @@ public class VulkanInstanceCreationOptions
/// </summary>
public bool UseDebug { get; set; }
/// <summary>
/// Requires and enables VK_KHR_surface on the created instance. Disable for offscreen-only
/// rendering with drivers that don't expose the extension.
/// </summary>
public bool RequireSurfaceExtension { get; set; } = true;
/*
@ -58,8 +64,14 @@ public class VulkanDeviceCreationOptions
/// Selects the first suitable discrete gpu available
/// </summary>
public bool PreferDiscreteGpu { get; set; }
public bool RequireComputeBit { get; set; }
/// <summary>
/// Requires and enables VK_KHR_swapchain on the created device. Disable for offscreen-only
/// rendering with drivers that don't expose the extension.
/// </summary>
public bool RequireSwapchainExtension { get; set; } = true;
}
public class VulkanPlatformSpecificOptions

14
src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs

@ -63,6 +63,19 @@ internal class VulkanSkiaGpu : ISkiaGpu
public void Dispose()
{
if (Vulkan.IsLost)
{
GrContext.AbandonContext();
GrContext.Dispose();
}
else
// Releasing resources does vkQueueWaitIdle and destroys API objects,
// both of which require external synchronization, i. e. the device lock
using (Vulkan.EnsureCurrent())
{
GrContext.AbandonContext(true);
GrContext.Dispose();
}
Vulkan.Dispose();
}
@ -92,6 +105,7 @@ internal class VulkanSkiaGpu : ISkiaGpu
foreach (var surface in surfaces)
{
if (surface is IVulkanKhrSurfacePlatformSurface
|| surface is IVulkanRenderTargetPlatformSurface
|| (factory?.CanRenderToSurface(Vulkan, surface) == true))
{
return surface.IsReady;

4
tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs

@ -115,8 +115,8 @@ public class DirectFbCompositionTests : TestBase
var image2 =
$"{nameof(Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised)}_advertized-{advertised}_updated";
SaveFile(fb, image2);
CompareImages(image1, skipImmediate: true);
CompareImages(image2, skipImmediate: true);
CompareImages(image1, skipImmediate: true, skipGpu: true);
CompareImages(image2, skipImmediate: true, skipGpu: true);
}

6
tests/Avalonia.RenderTests/Controls/AdornerTests.cs

@ -35,7 +35,11 @@ public class AdornerTests : TestBase
adorned.AttachedToVisualTree += delegate
{
AdornerLayer.SetAdornedElement(adorner, adorned);
AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner);
// The tree gets attached once per composited render, but the adorner stays
// in the layer after detach
var layer = AdornerLayer.GetAdornerLayer(adorned)!;
if (!layer.Children.Contains(adorner))
layer.Children.Add(adorner);
};
tree.Measure(size);

2
tests/Avalonia.RenderTests/Controls/CommandBarTests.cs

@ -81,7 +81,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
target.Styles.Add(new SimpleTheme());
target.Styles.Add(FontStyle);
await RenderToFile(target);
CompareImages(skipImmediate: true);
CompareImages(skipImmediate: true, gpuAllowedError: 0.03);
}
[Fact]

4
tests/Avalonia.RenderTests/Media/DrawingContentTests.cs

@ -45,7 +45,7 @@ public class DrawingContentTests()
};
RenderChange(target, () => drawing.Brush = Brushes.Red);
CompareImages(skipImmediate: true);
CompareImages(skipImmediate: true, skipGpu: true);
}
[Fact]
@ -66,7 +66,7 @@ public class DrawingContentTests()
};
RenderChange(target, () => drawing.Brush = Brushes.Red);
CompareImages(skipImmediate: true);
CompareImages(skipImmediate: true, skipGpu: true);
}
// Renders the target once, applies the change, renders again and writes the second frame to the

2
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -507,7 +507,7 @@ namespace Avalonia.Skia.RenderTests
};
await RenderToFile(target, dpi: 144);
CompareImages();
CompareImages(gpuAllowedError: 0.05);
}
[Fact]

202
tests/Avalonia.RenderTests/Mesa/FboReadbackGlPlatformSurface.cs

@ -0,0 +1,202 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.Skia.RenderTests;
/// <summary>
/// An offscreen GL surface that renders into an FBO and reads the pixels back
/// into a writeable bitmap at the end of each rendering session.
/// </summary>
internal class FboReadbackGlPlatformSurface : IGlPlatformSurface
{
private readonly IWriteableBitmapImpl _bitmap;
private readonly double _scaling;
public FboReadbackGlPlatformSurface(IWriteableBitmapImpl bitmap, double scaling)
{
_bitmap = bitmap;
_scaling = scaling;
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) =>
new RenderTarget(context, _bitmap, _scaling);
private class RenderTarget : IGlPlatformSurfaceRenderTarget
{
private static readonly bool[] s_trueFalse = { true, false };
private readonly IGlContext _context;
private readonly IWriteableBitmapImpl _bitmap;
private readonly double _scaling;
private int _fbo;
private int _color;
private int _depthStencil;
private PixelSize _size;
public RenderTarget(IGlContext context, IWriteableBitmapImpl bitmap, double scaling)
{
_context = context;
_bitmap = bitmap;
_scaling = scaling;
}
public void Dispose()
{
if (_fbo == 0 || _context.IsLost)
return;
using (_context.MakeCurrent())
DeleteFbo(_context.GlInterface);
}
private void DeleteFbo(GlInterface gl)
{
if (_fbo == 0)
return;
gl.DeleteFramebuffer(_fbo);
gl.DeleteRenderbuffer(_color);
if (_depthStencil != 0)
gl.DeleteRenderbuffer(_depthStencil);
_fbo = _color = _depthStencil = 0;
}
private void EnsureFbo(GlInterface gl, PixelSize size)
{
if (_fbo != 0 && size == _size)
return;
DeleteFbo(gl);
_fbo = gl.GenFramebuffer();
gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo);
_color = gl.GenRenderbuffer();
gl.BindRenderbuffer(GL_RENDERBUFFER, _color);
gl.RenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, size.Width, size.Height);
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _color);
var success = false;
foreach (var useStencil8 in s_trueFalse)
{
_depthStencil = gl.GenRenderbuffer();
gl.BindRenderbuffer(GL_RENDERBUFFER, _depthStencil);
if (useStencil8)
{
gl.RenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, size.Width, size.Height);
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
}
else
{
gl.RenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.Width, size.Height);
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
}
if (gl.CheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
{
success = true;
break;
}
gl.DeleteRenderbuffer(_depthStencil);
_depthStencil = 0;
}
if (!success)
{
DeleteFbo(gl);
throw new OpenGlException("Unable to create FBO with stencil");
}
_size = size;
}
public IGlPlatformSurfaceRenderingSession BeginDraw(IRenderTarget.RenderTargetSceneInfo sceneInfo)
{
var restoreContext = _context.MakeCurrent();
var success = false;
try
{
var gl = _context.GlInterface;
var size = _bitmap.PixelSize;
EnsureFbo(gl, size);
gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo);
gl.Viewport(0, 0, size.Width, size.Height);
success = true;
return new Session(this, restoreContext);
}
finally
{
if (!success)
restoreContext.Dispose();
}
}
private void ReadPixels()
{
var gl = _context.GlInterface;
gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo);
using (var fb = _bitmap.Lock())
{
var tightStride = _size.Width * 4;
if (fb.RowBytes == tightStride)
gl.ReadPixels(0, 0, _size.Width, _size.Height, GL_RGBA, GL_UNSIGNED_BYTE, fb.Address);
else
{
var temp = Marshal.AllocHGlobal(tightStride * _size.Height);
try
{
gl.ReadPixels(0, 0, _size.Width, _size.Height, GL_RGBA, GL_UNSIGNED_BYTE, temp);
for (var y = 0; y < _size.Height; y++)
unsafe
{
Buffer.MemoryCopy(
(byte*)temp + y * tightStride,
(byte*)fb.Address + y * fb.RowBytes,
fb.RowBytes, tightStride);
}
}
finally
{
Marshal.FreeHGlobal(temp);
}
}
}
}
private class Session : IGlPlatformSurfaceRenderingSession
{
private readonly RenderTarget _target;
private readonly IDisposable _restoreContext;
public Session(RenderTarget target, IDisposable restoreContext)
{
_target = target;
_restoreContext = restoreContext;
}
public IGlContext Context => _target._context;
public PixelSize Size => _target._size;
public double Scaling => _target._scaling;
// The FBO is never presented anywhere, so we get to pick the orientation that
// makes glReadPixels return rows in top-down order.
public bool IsYFlipped => true;
public void Dispose()
{
try
{
_target._context.GlInterface.Flush();
_target.ReadPixels();
}
finally
{
_restoreContext.Dispose();
}
}
}
}
}

98
tests/Avalonia.RenderTests/Mesa/MesaSoftwareRenderer.cs

@ -0,0 +1,98 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Vulkan;
using Avalonia.Vulkan.Interop;
using static Avalonia.OpenGL.Egl.EglConsts;
namespace Avalonia.Skia.RenderTests;
/// <summary>
/// Provides llvmpipe (GL) and lavapipe (Vulkan) platform graphics backed by the
/// unofficial.mesa.softwarerenderer native library.
/// </summary>
internal static unsafe class MesaSoftwareRenderer
{
public static bool GlEnabled { get; } =
Environment.GetEnvironmentVariable("AVALONIA_RENDER_TESTS_DISABLE_MESA_GL") != "1";
// macOS libSkiaSharp is built without Vulkan support
public static bool VulkanEnabled { get; } =
!OperatingSystem.IsMacOS()
&& Environment.GetEnvironmentVariable("AVALONIA_RENDER_TESTS_DISABLE_MESA_VULKAN") != "1";
private static readonly Lazy<IntPtr> s_library = new(() =>
NativeLibrary.Load("softmesa", typeof(MesaSoftwareRenderer).Assembly, null));
private static readonly Lazy<EglPlatformGraphics> s_gl = new(CreateGl);
private static readonly Lazy<VulkanPlatformGraphics> s_vulkan = new(CreateVulkan);
public static IPlatformGraphics Gl => s_gl.Value;
public static IPlatformGraphics Vulkan => s_vulkan.Value;
private static IntPtr GetProcAddress(IntPtr proc, string name)
{
var bytes = new byte[Encoding.UTF8.GetByteCount(name) + 1];
Encoding.UTF8.GetBytes(name, bytes.AsSpan(0, bytes.Length - 1));
fixed (byte* ptr = bytes)
return ((delegate* unmanaged[Stdcall]<byte*, IntPtr>)proc)(ptr);
}
private static IntPtr GetInstanceProcAddress(IntPtr proc, IntPtr instance, string name)
{
var bytes = new byte[Encoding.UTF8.GetByteCount(name) + 1];
Encoding.UTF8.GetBytes(name, bytes.AsSpan(0, bytes.Length - 1));
fixed (byte* ptr = bytes)
return ((delegate* unmanaged[Stdcall]<IntPtr, byte*, IntPtr>)proc)(instance, ptr);
}
private static EglPlatformGraphics CreateGl()
{
var eglGetProcAddress = NativeLibrary.GetExport(s_library.Value, "eglGetProcAddress");
var egl = new EglInterface(name => GetProcAddress(eglGetProcAddress, name));
var display = new EglDisplay(new EglDisplayCreationOptions
{
Egl = egl,
SupportsMultipleContexts = true,
SupportsContextSharing = true,
AllowPbufferOnlyConfigs = true,
// macOS libSkiaSharp is built with skia_gl_standard="gl" and can't consume GLES contexts
GlVersions = OperatingSystem.IsMacOS()
? new[]
{
new GlVersion(GlProfileType.OpenGL, 4, 0),
new GlVersion(GlProfileType.OpenGL, 3, 2)
}
: null,
// Windows build of Mesa uses the default platform for surfaceless operation
PlatformType = OperatingSystem.IsWindows() ? null : EGL_PLATFORM_SURFACELESS_MESA
});
return new EglPlatformGraphics(display);
}
private static VulkanPlatformGraphics CreateVulkan()
{
var vkGetInstanceProcAddr = NativeLibrary.GetExport(s_library.Value, "vkGetInstanceProcAddr");
VkGetInstanceProcAddressDelegate getProcAddress = (instance, name) =>
GetInstanceProcAddress(vkGetInstanceProcAddr, instance, name);
var platformOptions = new VulkanPlatformSpecificOptions
{
GetProcAddressDelegate = getProcAddress
};
// The softmesa build has no WSI support at all
var instance = VulkanInstance.Create(new VulkanInstanceCreationOptions
{
ApplicationName = "Avalonia render tests",
RequireSurfaceExtension = false
}, platformOptions);
var device = VulkanDevice.Create(instance,
new VulkanDeviceCreationOptions { RequireSwapchainExtension = false }, platformOptions);
return VulkanPlatformGraphics.TryCreate(new VulkanOptions { CustomSharedDevice = device }, platformOptions)
?? throw new InvalidOperationException("Unable to create lavapipe-based VulkanPlatformGraphics");
}
}

200
tests/Avalonia.RenderTests/Mesa/VulkanReadbackPlatformSurface.cs

@ -0,0 +1,200 @@
using System;
using Avalonia.Platform;
using Avalonia.Vulkan;
using Avalonia.Vulkan.Interop;
using Avalonia.Vulkan.UnmanagedInterop;
namespace Avalonia.Skia.RenderTests;
/// <summary>
/// An offscreen Vulkan surface that renders into a VkImage and reads the pixels back
/// into a writeable bitmap at the end of each rendering session.
/// </summary>
internal class VulkanReadbackPlatformSurface : IVulkanRenderTargetPlatformSurface
{
private readonly IWriteableBitmapImpl _bitmap;
private readonly double _scaling;
public VulkanReadbackPlatformSurface(IWriteableBitmapImpl bitmap, double scaling)
{
_bitmap = bitmap;
_scaling = scaling;
}
public IVulkanRenderTarget CreateRenderTarget(IVulkanPlatformGraphicsContext context) =>
new RenderTarget(context, _bitmap, _scaling);
private class RenderTarget : IVulkanRenderTarget
{
private readonly IVulkanPlatformGraphicsContext _context;
private readonly IWriteableBitmapImpl _bitmap;
private readonly double _scaling;
private readonly VulkanCommandBufferPool _pool;
private VulkanImage? _image;
public RenderTarget(IVulkanPlatformGraphicsContext context, IWriteableBitmapImpl bitmap, double scaling)
{
_context = context;
_bitmap = bitmap;
_scaling = scaling;
_pool = new VulkanCommandBufferPool(context);
}
public IVulkanRenderSession BeginDraw()
{
var l = _context.EnsureCurrent();
try
{
_pool.FreeUsedCommandBuffers();
var size = _bitmap.PixelSize;
if (_image == null || _image.Size != size)
{
DestroyImage();
_image = new VulkanImage(_context, _pool, VkFormat.VK_FORMAT_R8G8B8A8_UNORM, size, 1);
}
else
_image.TransitionLayout(VkImageLayout.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VkAccessFlags.VK_ACCESS_NONE);
return new Session(this, _image, l);
}
catch
{
l.Dispose();
throw;
}
}
private void DestroyImage()
{
if (_image == null)
return;
_context.DeviceApi.DeviceWaitIdle(_context.DeviceHandle);
_image.Dispose();
_image = null;
}
public void Dispose()
{
using (_context.EnsureCurrent())
{
DestroyImage();
_pool.Dispose();
}
}
private unsafe void ReadPixels(VulkanImage image)
{
image.TransitionLayout(VkImageLayout.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VkAccessFlags.VK_ACCESS_TRANSFER_READ_BIT);
var api = _context.DeviceApi;
var device = _context.DeviceHandle;
var size = image.Size;
var tightStride = size.Width * 4;
var byteSize = (ulong)(tightStride * size.Height);
var bufferCreateInfo = new VkBufferCreateInfo
{
sType = VkStructureType.VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
size = byteSize,
usage = VkBufferUsageFlags.VK_BUFFER_USAGE_TRANSFER_DST_BIT,
sharingMode = VkSharingMode.VK_SHARING_MODE_EXCLUSIVE
};
api.CreateBuffer(device, ref bufferCreateInfo, IntPtr.Zero, out var buffer)
.ThrowOnError("vkCreateBuffer");
var memory = default(VkDeviceMemory);
try
{
api.GetBufferMemoryRequirements(device, buffer, out var memoryRequirements);
var allocateInfo = new VkMemoryAllocateInfo
{
sType = VkStructureType.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
allocationSize = memoryRequirements.size,
memoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(_context,
memoryRequirements.memoryTypeBits,
VkMemoryPropertyFlags.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VkMemoryPropertyFlags.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
};
api.AllocateMemory(device, ref allocateInfo, IntPtr.Zero, out memory)
.ThrowOnError("vkAllocateMemory");
api.BindBufferMemory(device, buffer, memory, 0).ThrowOnError("vkBindBufferMemory");
var commandBuffer = _pool.CreateCommandBuffer();
commandBuffer.BeginRecording();
var region = new VkBufferImageCopy
{
imageSubresource =
{
aspectMask = VkImageAspectFlags.VK_IMAGE_ASPECT_COLOR_BIT,
layerCount = 1
},
imageExtent =
{
width = (uint)size.Width,
height = (uint)size.Height,
depth = 1
}
};
api.CmdCopyImageToBuffer(commandBuffer.Handle, image.Handle,
VkImageLayout.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, 1, &region);
commandBuffer.EndRecording();
commandBuffer.Submit();
api.QueueWaitIdle(_context.MainQueueHandle).ThrowOnError("vkQueueWaitIdle");
api.MapMemory(device, memory, 0, byteSize, 0, out var mapped).ThrowOnError("vkMapMemory");
try
{
using (var fb = _bitmap.Lock())
for (var y = 0; y < size.Height; y++)
Buffer.MemoryCopy(
(byte*)mapped + y * tightStride,
(byte*)fb.Address + y * fb.RowBytes,
fb.RowBytes, tightStride);
}
finally
{
api.UnmapMemory(device, memory);
}
}
finally
{
api.DestroyBuffer(device, buffer, IntPtr.Zero);
if (memory.Handle != 0)
api.FreeMemory(device, memory, IntPtr.Zero);
}
}
private class Session : IVulkanRenderSession
{
private readonly RenderTarget _target;
private readonly VulkanImage _image;
private readonly IDisposable _lock;
public Session(RenderTarget target, VulkanImage image, IDisposable @lock)
{
_target = target;
_image = image;
_lock = @lock;
}
public double Scaling => _target._scaling;
public PixelSize Size => _image.Size;
public bool IsYFlipped => true;
public VulkanImageInfo ImageInfo => _image.ImageInfo;
public bool IsRgba => true;
public void Dispose()
{
try
{
_target.ReadPixels(_image);
}
finally
{
_lock.Dispose();
}
}
}
}
}

2
tests/Avalonia.RenderTests/Shapes/EllipseTests.cs

@ -51,7 +51,7 @@ namespace Avalonia.Skia.RenderTests
RenderOptions.SetEdgeMode(target, EdgeMode.Aliased);
await RenderToFile(target);
CompareImages();
CompareImages(gpuAllowedError: 0.05);
}
[Fact]

2
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@ -453,7 +453,7 @@ namespace Avalonia.Skia.RenderTests
};
await RenderToFile(target);
CompareImages();
CompareImages(gpuAllowedError: 0.05);
}
[Fact]

48
tests/Avalonia.RenderTests/TestBase.cs

@ -53,39 +53,61 @@ namespace Avalonia.Skia.RenderTests
{
Directory.CreateDirectory(OutputPath);
}
var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
var glesPath = Path.Combine(OutputPath, testName + ".composited.gles.out.png");
var vulkanPath = Path.Combine(OutputPath, testName + ".composited.vulkan.out.png");
await TestRenderHelper.RenderToFile(target, immediatePath, true, dpi);
await TestRenderHelper.RenderToFile(target, compositedPath, false, dpi);
if (MesaSoftwareRenderer.GlEnabled)
await TestRenderHelper.RenderCompositedToFile(target, glesPath, MesaSoftwareRenderer.Gl, dpi);
if (MesaSoftwareRenderer.VulkanEnabled)
await TestRenderHelper.RenderCompositedToFile(target, vulkanPath, MesaSoftwareRenderer.Vulkan, dpi);
}
/// <param name="testName">The base name of the compared images.</param>
/// <param name="skipImmediate">Skips the immediate renderer output comparison.</param>
/// <param name="skipCompositor">Skips all composited output comparisons.</param>
/// <param name="skipGpu">
/// Skips Mesa GPU output comparisons. For tests that render via a custom flow that only
/// produces the regular composited output.
/// </param>
/// <param name="gpuAllowedError">
/// Error tolerance override for Mesa GPU outputs, for tests with known minor
/// rasterization differences between GPU and CPU rendering.
/// </param>
protected void CompareImages([CallerMemberName] string testName = "",
bool skipImmediate = false, bool skipCompositor = false)
bool skipImmediate = false, bool skipCompositor = false, bool skipGpu = false,
double? gpuAllowedError = null)
{
var expectedPath = Path.Combine(OutputPath, testName + ".expected.png");
var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
using (var expected = Image.Load<Rgba32>(expectedPath))
using (var immediate = skipImmediate ? null: Image.Load<Rgba32>(immediatePath))
using (var composited = skipCompositor ? null : Image.Load<Rgba32>(compositedPath))
{
if (!skipImmediate)
void Compare(string outputType, double allowedError)
{
var immediateError = TestRenderHelper.CompareImages(immediate!, expected);
if (immediateError > AllowedError)
var actualPath = Path.Combine(OutputPath, testName + "." + outputType + ".out.png");
using var actual = Image.Load<Rgba32>(actualPath);
var error = TestRenderHelper.CompareImages(actual, expected);
if (error > allowedError)
{
Assert.Fail(immediatePath + ": Error = " + immediateError);
Assert.Fail(actualPath + ": Error = " + error);
}
}
if (!skipImmediate)
Compare("immediate", AllowedError);
if (!skipCompositor)
{
var compositedError = TestRenderHelper.CompareImages(composited!, expected);
if (compositedError > AllowedError)
Compare("composited", AllowedError);
if (!skipGpu)
{
Assert.Fail(compositedPath + ": Error = " + compositedError);
if (MesaSoftwareRenderer.GlEnabled)
Compare("composited.gles", gpuAllowedError ?? AllowedError);
if (MesaSoftwareRenderer.VulkanEnabled)
Compare("composited.vulkan", gpuAllowedError ?? AllowedError);
}
}
}

109
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -23,6 +23,7 @@ using Avalonia.Utilities;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
using Avalonia.Harfbuzz;
using Avalonia.OpenGL.Egl;
using Avalonia.Skia;
namespace Avalonia.Skia.RenderTests;
@ -51,19 +52,14 @@ static class TestRenderHelper
public static Task RenderToFile(Control target, string path, bool immediate, double dpi = 96)
{
var dir = Path.GetDirectoryName(path);
Assert.NotNull(dir);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
var size = new Size(target.Width, target.Height);
var dpiVector = new Vector(dpi, dpi);
if (immediate)
{
EnsureOutputDirectory(path);
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
var size = new Size(target.Width, target.Height);
var dpiVector = new Vector(dpi, dpi);
using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
{
target.Measure(size);
@ -71,34 +67,91 @@ static class TestRenderHelper
bitmap.Render(target);
bitmap.Save(path, PngBitmapEncoderOptions.Default);
}
return Task.CompletedTask;
}
else
{
var timer = new ManualRenderTimer();
var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat,
factory.DefaultAlphaFormat))
return RenderCompositedToFile(target, path, null, dpi);
}
public static Task RenderCompositedToFile(Control target, string path, IPlatformGraphics? gpu, double dpi = 96)
{
EnsureOutputDirectory(path);
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
var dpiVector = new Vector(dpi, dpi);
var scaling = dpi / 96;
var timer = new ManualRenderTimer();
var compositor = new Compositor(RenderLoop.FromTimer(timer), gpu, true,
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
// GPU readback surfaces produce RGBA
var pixelFormat = gpu == null ? factory.DefaultPixelFormat : PixelFormats.Rgba8888;
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, pixelFormat,
factory.DefaultAlphaFormat))
{
try
{
var root = new TestRenderRoot(dpiVector.X / 96, null!);
using (var renderer = new CompositingRenderer(root, compositor,
() => new[] { new BitmapFramebufferSurface(writableBitmap) }))
IPlatformRenderSurface surface = gpu switch
{
null => new BitmapFramebufferSurface(writableBitmap),
EglPlatformGraphics => new FboReadbackGlPlatformSurface(writableBitmap, scaling),
_ => new VulkanReadbackPlatformSurface(writableBitmap, scaling)
};
var root = new TestRenderRoot(scaling, null!);
try
{
root.Initialize(renderer, target);
renderer.Start();
Dispatcher.UIThread.RunJobs();
renderer.Paint(new Rect(root.Bounds.Size), false);
using (var renderer = new CompositingRenderer(root, compositor, () => new[] { surface }))
{
root.Initialize(renderer, target);
renderer.Start();
Dispatcher.UIThread.RunJobs();
renderer.Paint(new Rect(root.Bounds.Size), false);
}
}
using var fileStream = File.Create(path);
writableBitmap.Save(fileStream, PngBitmapEncoderOptions.Default);
finally
{
// Detach the control, so it can be rendered again with a different root
root.Child = null;
}
}
finally
{
if (gpu != null)
DisposeGpuBackendContext(compositor, gpu);
}
using var fileStream = File.Create(path);
writableBitmap.Save(fileStream, PngBitmapEncoderOptions.Default);
}
return Task.CompletedTask;
}
// Compositors are created per render, so their backend contexts (GRContext etc.) have to be
// disposed deterministically instead of piling up until finalization
private static void DisposeGpuBackendContext(Compositor compositor, IPlatformGraphics gpu)
{
var renderInterface = compositor.Server.RenderInterface;
using (renderInterface.EnsureCurrent())
// TODO: Technically it should be disposing the GPU context as well,
// but need to re-check wayland code if it's safe to do
compositor.Server.ResetAllGpuResources();
if (!gpu.UsesSharedContext)
renderInterface.GpuContext?.Dispose();
}
private static void EnsureOutputDirectory(string path)
{
var dir = Path.GetDirectoryName(path);
Assert.NotNull(dir);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
class BitmapFramebufferSurface : IFramebufferPlatformSurface
{
private readonly IWriteableBitmapImpl _bitmap;

1
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -35,6 +35,7 @@
<PackageReference Include="Moq" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="unofficial.mesa.softwarerenderer" />
</ItemGroup>
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\SkiaSharp.props" />

Loading…
Cancel
Save