Browse Source
* Run render tests against llvmpipe/lavapipe MESA rasterizers * CI? * Try desktop gl on mac? * disable vulkan on macpull/21823/head
committed by
GitHub
30 changed files with 776 additions and 70 deletions
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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, ®ion); |
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue