aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Backend/Vulkan
diff options
context:
space:
mode:
Diffstat (limited to 'Ryujinx.Ava/Ui/Backend/Vulkan')
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs16
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs135
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs124
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs53
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs13
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs92
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs182
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs67
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs439
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs167
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs136
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs59
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs49
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs219
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs80
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs18
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs32
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs75
-rw-r--r--Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs48
19 files changed, 2004 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs
new file mode 100644
index 00000000..b1326dbf
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs
@@ -0,0 +1,16 @@
+using System;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ public static class ResultExtensions
+ {
+ public static void ThrowOnError(this Result result)
+ {
+ if (result != Result.Success)
+ {
+ throw new Exception($"Unexpected API error \"{result}\".");
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs
new file mode 100644
index 00000000..ba7ddc7a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs
@@ -0,0 +1,135 @@
+using System;
+using Avalonia.Skia;
+using Ryujinx.Ava.Ui.Vulkan;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using SkiaSharp;
+
+namespace Ryujinx.Ava.Ui.Backend.Vulkan
+{
+ internal class VulkanRenderTarget : ISkiaGpuRenderTarget
+ {
+ public GRContext GrContext { get; set; }
+
+ private readonly VulkanSurfaceRenderTarget _surface;
+ private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
+
+ public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface)
+ {
+ _surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface);
+ _vulkanPlatformSurface = vulkanPlatformSurface;
+ }
+
+ public void Dispose()
+ {
+ _surface.Dispose();
+ }
+
+ public ISkiaGpuRenderSession BeginRenderingSession()
+ {
+ var session = _surface.BeginDraw(_vulkanPlatformSurface.Scaling);
+ bool success = false;
+ try
+ {
+ var disp = session.Display;
+ var api = session.Api;
+
+ var size = session.Size;
+ var scaling = session.Scaling;
+ if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
+ {
+ size = new Avalonia.PixelSize(1, 1);
+ scaling = 1;
+ }
+
+ lock (GrContext)
+ {
+ GrContext.ResetContext();
+
+ var imageInfo = new GRVkImageInfo()
+ {
+ CurrentQueueFamily = disp.QueueFamilyIndex,
+ Format = _surface.ImageFormat,
+ Image = _surface.Image.Handle,
+ ImageLayout = (uint)_surface.Image.CurrentLayout,
+ ImageTiling = (uint)_surface.Image.Tiling,
+ ImageUsageFlags = _surface.UsageFlags,
+ LevelCount = _surface.MipLevels,
+ SampleCount = 1,
+ Protected = false,
+ Alloc = new GRVkAlloc()
+ {
+ Memory = _surface.Image.MemoryHandle,
+ Flags = 0,
+ Offset = 0,
+ Size = _surface.MemorySize
+ }
+ };
+
+ var renderTarget =
+ new GRBackendRenderTarget((int)size.Width, (int)size.Height, 1,
+ imageInfo);
+ var surface = SKSurface.Create(GrContext, renderTarget,
+ GRSurfaceOrigin.TopLeft,
+ _surface.IsRgba ? SKColorType.Rgba8888 : SKColorType.Bgra8888, SKColorSpace.CreateSrgb());
+
+ if (surface == null)
+ {
+ throw new InvalidOperationException(
+ "Surface can't be created with the provided render target");
+ }
+
+ success = true;
+
+ return new VulkanGpuSession(GrContext, renderTarget, surface, session);
+ }
+ }
+ finally
+ {
+ if (!success)
+ {
+ session.Dispose();
+ }
+ }
+ }
+
+ public bool IsCorrupted { get; }
+
+ internal class VulkanGpuSession : ISkiaGpuRenderSession
+ {
+ private readonly GRBackendRenderTarget _backendRenderTarget;
+ private readonly VulkanSurfaceRenderingSession _vulkanSession;
+
+ public VulkanGpuSession(GRContext grContext,
+ GRBackendRenderTarget backendRenderTarget,
+ SKSurface surface,
+ VulkanSurfaceRenderingSession vulkanSession)
+ {
+ GrContext = grContext;
+ _backendRenderTarget = backendRenderTarget;
+ SkSurface = surface;
+ _vulkanSession = vulkanSession;
+
+ SurfaceOrigin = GRSurfaceOrigin.TopLeft;
+ }
+
+ public void Dispose()
+ {
+ lock (_vulkanSession.Display.Lock)
+ {
+ SkSurface.Canvas.Flush();
+
+ SkSurface.Dispose();
+ _backendRenderTarget.Dispose();
+ GrContext.Flush();
+
+ _vulkanSession.Dispose();
+ }
+ }
+
+ public GRContext GrContext { get; }
+ public SKSurface SkSurface { get; }
+ public double ScaleFactor => _vulkanSession.Scaling;
+ public GRSurfaceOrigin SurfaceOrigin { get; }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs
new file mode 100644
index 00000000..4fc6b929
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Platform;
+using Avalonia.Skia;
+using Avalonia.X11;
+using Ryujinx.Ava.Ui.Vulkan;
+using Silk.NET.Vulkan;
+using SkiaSharp;
+
+namespace Ryujinx.Ava.Ui.Backend.Vulkan
+{
+ public class VulkanSkiaGpu : ISkiaGpu
+ {
+ private readonly VulkanPlatformInterface _vulkan;
+ private readonly long? _maxResourceBytes;
+ private GRVkBackendContext _grVkBackend;
+ private bool _initialized;
+
+ public GRContext GrContext { get; private set; }
+
+ public VulkanSkiaGpu(long? maxResourceBytes)
+ {
+ _vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
+ _maxResourceBytes = maxResourceBytes;
+ }
+
+ private void Initialize()
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ _initialized = true;
+ GRVkGetProcedureAddressDelegate getProc = (string name, IntPtr instanceHandle, IntPtr deviceHandle) =>
+ {
+ IntPtr addr = IntPtr.Zero;
+
+ if (deviceHandle != IntPtr.Zero)
+ {
+ addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(deviceHandle), name);
+
+ if (addr != IntPtr.Zero)
+ {
+ return addr;
+ }
+
+ addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(_vulkan.Device.Handle), name);
+
+ if (addr != IntPtr.Zero)
+ {
+ return addr;
+ }
+ }
+
+ addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(_vulkan.Instance.Handle), name);
+
+ if (addr == IntPtr.Zero)
+ {
+ addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(instanceHandle), name);
+ }
+
+ return addr;
+ };
+
+ _grVkBackend = new GRVkBackendContext()
+ {
+ VkInstance = _vulkan.Device.Handle,
+ VkPhysicalDevice = _vulkan.PhysicalDevice.Handle,
+ VkDevice = _vulkan.Device.Handle,
+ VkQueue = _vulkan.Device.Queue.Handle,
+ GraphicsQueueIndex = _vulkan.PhysicalDevice.QueueFamilyIndex,
+ GetProcedureAddress = getProc
+ };
+ GrContext = GRContext.CreateVulkan(_grVkBackend);
+ if (_maxResourceBytes.HasValue)
+ {
+ GrContext.SetResourceCacheLimit(_maxResourceBytes.Value);
+ }
+ }
+
+ public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
+ {
+ foreach (var surface in surfaces)
+ {
+ VulkanWindowSurface window;
+
+ if (surface is IPlatformHandle handle)
+ {
+ window = new VulkanWindowSurface(handle.Handle);
+ }
+ else if (surface is X11FramebufferSurface x11FramebufferSurface)
+ {
+ // As of Avalonia 0.10.13, an IPlatformHandle isn't passed for linux, so use reflection to otherwise get the window id
+ var xId = (IntPtr)x11FramebufferSurface.GetType().GetField(
+ "_xid",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(x11FramebufferSurface);
+
+ window = new VulkanWindowSurface(xId);
+ }
+ else
+ {
+ continue;
+ }
+
+ VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window);
+
+ Initialize();
+
+ vulkanRenderTarget.GrContext = GrContext;
+
+ return vulkanRenderTarget;
+ }
+
+ return null;
+ }
+
+ public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs
new file mode 100644
index 00000000..fd2d379b
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs
@@ -0,0 +1,53 @@
+using Avalonia;
+using Ryujinx.Ava.Ui.Vulkan;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+using System;
+
+namespace Ryujinx.Ava.Ui.Backend.Vulkan
+{
+ internal class VulkanWindowSurface : BackendSurface, IVulkanPlatformSurface
+ {
+ public float Scaling => (float)Program.ActualScaleFactor;
+
+ public PixelSize SurfaceSize => Size;
+
+ public VulkanWindowSurface(IntPtr handle) : base(handle)
+ {
+ }
+
+ public unsafe SurfaceKHR CreateSurface(VulkanInstance instance)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrWin32Surface surfaceExtension))
+ {
+ var createInfo = new Win32SurfaceCreateInfoKHR() { Hinstance = 0, Hwnd = Handle, SType = StructureType.Win32SurfaceCreateInfoKhr };
+
+ surfaceExtension.CreateWin32Surface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
+
+ return surface;
+ }
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrXlibSurface surfaceExtension))
+ {
+ var createInfo = new XlibSurfaceCreateInfoKHR()
+ {
+ SType = StructureType.XlibSurfaceCreateInfoKhr,
+ Dpy = (nint*)Display,
+ Window = Handle
+ };
+
+ surfaceExtension.CreateXlibSurface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
+
+ return surface;
+ }
+ }
+
+ throw new PlatformNotSupportedException("The current platform does not support surface creation.");
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs
new file mode 100644
index 00000000..642d8a6a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs
@@ -0,0 +1,13 @@
+using System;
+using Avalonia;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
+{
+ public interface IVulkanPlatformSurface : IDisposable
+ {
+ float Scaling { get; }
+ PixelSize SurfaceSize { get; }
+ SurfaceKHR CreateSurface(VulkanInstance instance);
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs
new file mode 100644
index 00000000..b2b8843d
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs
@@ -0,0 +1,92 @@
+using System;
+using Avalonia;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
+{
+ internal class VulkanSurfaceRenderTarget : IDisposable
+ {
+ private readonly VulkanPlatformInterface _platformInterface;
+
+ private readonly Format _format;
+
+ public VulkanImage Image { get; private set; }
+ public bool IsCorrupted { get; private set; } = true;
+
+ public uint MipLevels => Image.MipLevels;
+
+ public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface)
+ {
+ _platformInterface = platformInterface;
+
+ Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device,
+ platformInterface.PhysicalDevice, surface);
+ Surface = surface;
+
+ // Skia seems to only create surfaces from images with unorm format
+
+ IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm &&
+ Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb;
+
+ _format = IsRgba ? Format.R8G8B8A8Unorm : Format.B8G8R8A8Unorm;
+ }
+
+ public bool IsRgba { get; }
+
+ public uint ImageFormat => (uint) _format;
+
+ public ulong MemorySize => Image.MemorySize;
+
+ public VulkanDisplay Display { get; }
+
+ public VulkanSurface Surface { get; }
+
+ public uint UsageFlags => Image.UsageFlags;
+
+ public PixelSize Size { get; private set; }
+
+ public void Dispose()
+ {
+ _platformInterface.Device.WaitIdle();
+ DestroyImage();
+ Display?.Dispose();
+ Surface?.Dispose();
+ }
+
+ public VulkanSurfaceRenderingSession BeginDraw(float scaling)
+ {
+ var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling);
+
+ if (IsCorrupted)
+ {
+ IsCorrupted = false;
+ DestroyImage();
+ CreateImage();
+ }
+ else
+ {
+ Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
+ }
+
+ return session;
+ }
+
+ public void Invalidate()
+ {
+ IsCorrupted = true;
+ }
+
+ private void CreateImage()
+ {
+ Size = Display.Size;
+
+ Image = new VulkanImage(_platformInterface.Device, _platformInterface.PhysicalDevice, _platformInterface.Device.CommandBufferPool, ImageFormat, Size);
+ }
+
+ private void DestroyImage()
+ {
+ _platformInterface.Device.WaitIdle();
+ Image?.Dispose();
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs
new file mode 100644
index 00000000..240035ca
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanCommandBufferPool : IDisposable
+ {
+ private readonly VulkanDevice _device;
+ private readonly CommandPool _commandPool;
+
+ private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
+
+ public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice)
+ {
+ _device = device;
+
+ var commandPoolCreateInfo = new CommandPoolCreateInfo
+ {
+ SType = StructureType.CommandPoolCreateInfo,
+ Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit,
+ QueueFamilyIndex = physicalDevice.QueueFamilyIndex
+ };
+
+ device.Api.CreateCommandPool(_device.InternalHandle, commandPoolCreateInfo, null, out _commandPool)
+ .ThrowOnError();
+ }
+
+ private CommandBuffer AllocateCommandBuffer()
+ {
+ var commandBufferAllocateInfo = new CommandBufferAllocateInfo
+ {
+ SType = StructureType.CommandBufferAllocateInfo,
+ CommandPool = _commandPool,
+ CommandBufferCount = 1,
+ Level = CommandBufferLevel.Primary
+ };
+
+ _device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer);
+
+ return commandBuffer;
+ }
+
+ public VulkanCommandBuffer CreateCommandBuffer()
+ {
+ return new(_device, this);
+ }
+
+ public void FreeUsedCommandBuffers()
+ {
+ lock (_usedCommandBuffers)
+ {
+ foreach (var usedCommandBuffer in _usedCommandBuffers)
+ {
+ usedCommandBuffer.Dispose();
+ }
+
+ _usedCommandBuffers.Clear();
+ }
+ }
+
+ private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer)
+ {
+ lock (_usedCommandBuffers)
+ {
+ _usedCommandBuffers.Add(commandBuffer);
+ }
+ }
+
+ public void Dispose()
+ {
+ FreeUsedCommandBuffers();
+ _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
+ }
+
+ public class VulkanCommandBuffer : IDisposable
+ {
+ private readonly VulkanCommandBufferPool _commandBufferPool;
+ private readonly VulkanDevice _device;
+ private readonly Fence _fence;
+ private bool _hasEnded;
+ private bool _hasStarted;
+
+ public IntPtr Handle => InternalHandle.Handle;
+
+ internal CommandBuffer InternalHandle { get; }
+
+ internal unsafe VulkanCommandBuffer(VulkanDevice device, VulkanCommandBufferPool commandBufferPool)
+ {
+ _device = device;
+ _commandBufferPool = commandBufferPool;
+
+ InternalHandle = _commandBufferPool.AllocateCommandBuffer();
+
+ var fenceCreateInfo = new FenceCreateInfo()
+ {
+ SType = StructureType.FenceCreateInfo,
+ Flags = FenceCreateFlags.FenceCreateSignaledBit
+ };
+
+ device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence);
+ }
+
+ public void BeginRecording()
+ {
+ if (!_hasStarted)
+ {
+ _hasStarted = true;
+
+ var beginInfo = new CommandBufferBeginInfo
+ {
+ SType = StructureType.CommandBufferBeginInfo,
+ Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit
+ };
+
+ _device.Api.BeginCommandBuffer(InternalHandle, beginInfo);
+ }
+ }
+
+ public void EndRecording()
+ {
+ if (_hasStarted && !_hasEnded)
+ {
+ _hasEnded = true;
+
+ _device.Api.EndCommandBuffer(InternalHandle);
+ }
+ }
+
+ public void Submit()
+ {
+ Submit(null, null, null, _fence);
+ }
+
+ public unsafe void Submit(
+ ReadOnlySpan<Semaphore> waitSemaphores,
+ ReadOnlySpan<PipelineStageFlags> waitDstStageMask,
+ ReadOnlySpan<Semaphore> signalSemaphores,
+ Fence? fence = null)
+ {
+ EndRecording();
+
+ if (!fence.HasValue)
+ {
+ fence = _fence;
+ }
+
+ fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores)
+ {
+ fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask)
+ {
+ var commandBuffer = InternalHandle;
+ var submitInfo = new SubmitInfo
+ {
+ SType = StructureType.SubmitInfo,
+ WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0,
+ PWaitSemaphores = pWaitSemaphores,
+ PWaitDstStageMask = pWaitDstStageMask,
+ CommandBufferCount = 1,
+ PCommandBuffers = &commandBuffer,
+ SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0,
+ PSignalSemaphores = pSignalSemaphores,
+ };
+
+ _device.Api.ResetFences(_device.InternalHandle, 1, fence.Value);
+
+ _device.Submit(submitInfo, fence.Value);
+ }
+ }
+
+ _commandBufferPool.DisposeCommandBuffer(this);
+ }
+
+ public void Dispose()
+ {
+ _device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue);
+ _device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle);
+ _device.Api.DestroyFence(_device.InternalHandle, _fence, Span<AllocationCallbacks>.Empty);
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs
new file mode 100644
index 00000000..b03fd720
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs
@@ -0,0 +1,67 @@
+using System;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanDevice : IDisposable
+ {
+ private static object _lock = new object();
+
+ public VulkanDevice(Device apiHandle, VulkanPhysicalDevice physicalDevice, Vk api)
+ {
+ InternalHandle = apiHandle;
+ Api = api;
+
+ api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue);
+
+ var vulkanQueue = new VulkanQueue(this, queue);
+ Queue = vulkanQueue;
+
+ PresentQueue = vulkanQueue;
+
+ CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice);
+ }
+
+ public IntPtr Handle => InternalHandle.Handle;
+
+ internal Device InternalHandle { get; }
+ public Vk Api { get; }
+
+ public VulkanQueue Queue { get; private set; }
+ public VulkanQueue PresentQueue { get; }
+ public VulkanCommandBufferPool CommandBufferPool { get; }
+
+ public void Dispose()
+ {
+ WaitIdle();
+ CommandBufferPool?.Dispose();
+ Queue = null;
+ }
+
+ internal void Submit(SubmitInfo submitInfo, Fence fence = default)
+ {
+ lock (_lock)
+ {
+ Api.QueueSubmit(Queue.InternalHandle, 1, submitInfo, fence).ThrowOnError();
+ }
+ }
+
+ public void WaitIdle()
+ {
+ lock (_lock)
+ {
+ Api.DeviceWaitIdle(InternalHandle);
+ }
+ }
+
+ public void QueueWaitIdle()
+ {
+ lock (_lock)
+ {
+ Api.QueueWaitIdle(Queue.InternalHandle);
+ }
+ }
+
+ public object Lock => _lock;
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs
new file mode 100644
index 00000000..bfe5b5a6
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs
@@ -0,0 +1,439 @@
+using System;
+using System.Linq;
+using System.Threading;
+using Avalonia;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using Ryujinx.Ui.Common.Configuration;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanDisplay : IDisposable
+ {
+ private static KhrSwapchain _swapchainExtension;
+ private readonly VulkanInstance _instance;
+ private readonly VulkanPhysicalDevice _physicalDevice;
+ private readonly VulkanSemaphorePair _semaphorePair;
+ private uint _nextImage;
+ private readonly VulkanSurface _surface;
+ private SurfaceFormatKHR _surfaceFormat;
+ private SwapchainKHR _swapchain;
+ private Extent2D _swapchainExtent;
+ private Image[] _swapchainImages;
+ private VulkanDevice _device { get; }
+ private ImageView[] _swapchainImageViews = new ImageView[0];
+ private bool _vsyncStateChanged;
+ private bool _vsyncEnabled;
+
+ public VulkanCommandBufferPool CommandBufferPool { get; set; }
+
+ public object Lock => _device.Lock;
+
+ private VulkanDisplay(VulkanInstance instance, VulkanDevice device,
+ VulkanPhysicalDevice physicalDevice, VulkanSurface surface, SwapchainKHR swapchain,
+ Extent2D swapchainExtent)
+ {
+ _instance = instance;
+ _device = device;
+ _physicalDevice = physicalDevice;
+ _swapchain = swapchain;
+ _swapchainExtent = swapchainExtent;
+ _surface = surface;
+
+ CreateSwapchainImages();
+
+ _semaphorePair = new VulkanSemaphorePair(_device);
+
+ CommandBufferPool = new VulkanCommandBufferPool(device, physicalDevice);
+ }
+
+ public PixelSize Size { get; private set; }
+ public uint QueueFamilyIndex => _physicalDevice.QueueFamilyIndex;
+
+ internal SurfaceFormatKHR SurfaceFormat
+ {
+ get
+ {
+ if (_surfaceFormat.Format == Format.Undefined)
+ {
+ _surfaceFormat = _surface.GetSurfaceFormat(_physicalDevice);
+ }
+
+ return _surfaceFormat;
+ }
+ }
+
+ public void Dispose()
+ {
+ _device.WaitIdle();
+ _semaphorePair?.Dispose();
+ DestroyCurrentImageViews();
+ _swapchainExtension.DestroySwapchain(_device.InternalHandle, _swapchain, Span<AllocationCallbacks>.Empty);
+ CommandBufferPool.Dispose();
+ }
+
+ private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device,
+ VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent,
+ SwapchainKHR? oldswapchain = null, bool vsyncEnabled = true)
+ {
+ if (_swapchainExtension == null)
+ {
+ instance.Api.TryGetDeviceExtension(instance.InternalHandle, device.InternalHandle, out _swapchainExtension);
+ }
+
+ while (!surface.CanSurfacePresent(physicalDevice))
+ {
+ Thread.Sleep(16);
+ }
+
+ VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfaceCapabilities(physicalDevice.InternalHandle,
+ surface.ApiHandle, out var capabilities);
+
+ var imageCount = capabilities.MinImageCount + 1;
+ if (capabilities.MaxImageCount > 0 && imageCount > capabilities.MaxImageCount)
+ {
+ imageCount = capabilities.MaxImageCount;
+ }
+
+ var surfaceFormat = surface.GetSurfaceFormat(physicalDevice);
+
+ bool supportsIdentityTransform = capabilities.SupportedTransforms.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr);
+ bool isRotated = capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate90BitKhr) ||
+ capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate270BitKhr);
+
+ swapchainExtent = GetSwapchainExtent(surface, capabilities);
+
+ CompositeAlphaFlagsKHR compositeAlphaFlags = GetSuitableCompositeAlphaFlags(capabilities);
+
+ PresentModeKHR presentMode = GetSuitablePresentMode(physicalDevice, surface, vsyncEnabled);
+
+ var swapchainCreateInfo = new SwapchainCreateInfoKHR
+ {
+ SType = StructureType.SwapchainCreateInfoKhr,
+ Surface = surface.ApiHandle,
+ MinImageCount = imageCount,
+ ImageFormat = surfaceFormat.Format,
+ ImageColorSpace = surfaceFormat.ColorSpace,
+ ImageExtent = swapchainExtent,
+ ImageUsage =
+ ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit,
+ ImageSharingMode = SharingMode.Exclusive,
+ ImageArrayLayers = 1,
+ PreTransform = supportsIdentityTransform && isRotated ?
+ SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr :
+ capabilities.CurrentTransform,
+ CompositeAlpha = compositeAlphaFlags,
+ PresentMode = presentMode,
+ Clipped = true,
+ OldSwapchain = oldswapchain ?? new SwapchainKHR()
+ };
+
+ _swapchainExtension.CreateSwapchain(device.InternalHandle, swapchainCreateInfo, null, out var swapchain)
+ .ThrowOnError();
+
+ if (oldswapchain != null)
+ {
+ _swapchainExtension.DestroySwapchain(device.InternalHandle, oldswapchain.Value, null);
+ }
+
+ return swapchain;
+ }
+
+ private static unsafe Extent2D GetSwapchainExtent(VulkanSurface surface, SurfaceCapabilitiesKHR capabilities)
+ {
+ Extent2D swapchainExtent;
+ if (capabilities.CurrentExtent.Width != uint.MaxValue)
+ {
+ swapchainExtent = capabilities.CurrentExtent;
+ }
+ else
+ {
+ var surfaceSize = surface.SurfaceSize;
+
+ var width = Math.Clamp((uint)surfaceSize.Width, capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width);
+ var height = Math.Clamp((uint)surfaceSize.Height, capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height);
+
+ swapchainExtent = new Extent2D(width, height);
+ }
+
+ return swapchainExtent;
+ }
+
+ private static unsafe CompositeAlphaFlagsKHR GetSuitableCompositeAlphaFlags(SurfaceCapabilitiesKHR capabilities)
+ {
+ var compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr;
+
+ if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr))
+ {
+ compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr;
+ }
+ else if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr))
+ {
+ compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr;
+ }
+
+ return compositeAlphaFlags;
+ }
+
+ private static unsafe PresentModeKHR GetSuitablePresentMode(VulkanPhysicalDevice physicalDevice, VulkanSurface surface, bool vsyncEnabled)
+ {
+ uint presentModesCount;
+
+ VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
+ surface.ApiHandle,
+ &presentModesCount, null);
+
+ var presentModes = new PresentModeKHR[presentModesCount];
+
+ fixed (PresentModeKHR* pPresentModes = presentModes)
+ {
+ VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
+ surface.ApiHandle, &presentModesCount, pPresentModes);
+ }
+
+ var modes = presentModes.ToList();
+ var presentMode = PresentModeKHR.PresentModeFifoKhr;
+
+ if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
+ {
+ presentMode = PresentModeKHR.PresentModeImmediateKhr;
+ }
+ else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr))
+ {
+ presentMode = PresentModeKHR.PresentModeMailboxKhr;
+ }
+ else if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
+ {
+ presentMode = PresentModeKHR.PresentModeImmediateKhr;
+ }
+
+ return presentMode;
+ }
+
+ internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device,
+ VulkanPhysicalDevice physicalDevice, VulkanSurface surface)
+ {
+ var swapchain = CreateSwapchain(instance, device, physicalDevice, surface, out var extent, null, true);
+
+ return new VulkanDisplay(instance, device, physicalDevice, surface, swapchain, extent);
+ }
+
+ private unsafe void CreateSwapchainImages()
+ {
+ DestroyCurrentImageViews();
+
+ Size = new PixelSize((int)_swapchainExtent.Width, (int)_swapchainExtent.Height);
+
+ uint imageCount = 0;
+
+ _swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, null);
+
+ _swapchainImages = new Image[imageCount];
+
+ fixed (Image* pSwapchainImages = _swapchainImages)
+ {
+ _swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, pSwapchainImages);
+ }
+
+ _swapchainImageViews = new ImageView[imageCount];
+
+ var surfaceFormat = SurfaceFormat;
+
+ for (var i = 0; i < imageCount; i++)
+ {
+ _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format);
+ }
+ }
+
+ private void DestroyCurrentImageViews()
+ {
+ for (var i = 0; i < _swapchainImageViews.Length; i++)
+ {
+ _instance.Api.DestroyImageView(_device.InternalHandle, _swapchainImageViews[i], Span<AllocationCallbacks>.Empty);
+ }
+ }
+
+ internal void ChangeVSyncMode(bool vsyncEnabled)
+ {
+ _vsyncStateChanged = true;
+ _vsyncEnabled = vsyncEnabled;
+ }
+
+ private void Recreate()
+ {
+ _device.WaitIdle();
+ _swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled);
+
+ CreateSwapchainImages();
+ }
+
+ private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format)
+ {
+ var componentMapping = new ComponentMapping(
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity);
+
+ var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, 1, 0, 1);
+
+ var imageCreateInfo = new ImageViewCreateInfo
+ {
+ SType = StructureType.ImageViewCreateInfo,
+ Image = swapchainImage,
+ ViewType = ImageViewType.ImageViewType2D,
+ Format = format,
+ Components = componentMapping,
+ SubresourceRange = subresourceRange
+ };
+
+ _instance.Api.CreateImageView(_device.InternalHandle, imageCreateInfo, null, out var imageView).ThrowOnError();
+ return imageView;
+ }
+
+ public bool EnsureSwapchainAvailable()
+ {
+ if (Size != _surface.SurfaceSize || _vsyncStateChanged)
+ {
+ _vsyncStateChanged = false;
+
+ Recreate();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation(VulkanSurfaceRenderTarget renderTarget)
+ {
+ _nextImage = 0;
+ while (true)
+ {
+ var acquireResult = _swapchainExtension.AcquireNextImage(
+ _device.InternalHandle,
+ _swapchain,
+ ulong.MaxValue,
+ _semaphorePair.ImageAvailableSemaphore,
+ new Fence(),
+ ref _nextImage);
+
+ if (acquireResult == Result.ErrorOutOfDateKhr ||
+ acquireResult == Result.SuboptimalKhr)
+ {
+ Recreate();
+ }
+ else
+ {
+ acquireResult.ThrowOnError();
+ break;
+ }
+ }
+
+ var commandBuffer = CommandBufferPool.CreateCommandBuffer();
+ commandBuffer.BeginRecording();
+
+ VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
+ _swapchainImages[_nextImage], ImageLayout.Undefined,
+ AccessFlags.AccessNoneKhr,
+ ImageLayout.TransferDstOptimal,
+ AccessFlags.AccessTransferWriteBit,
+ 1);
+
+ return commandBuffer;
+ }
+
+ internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer)
+ {
+ VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
+ renderTarget.Image.InternalHandle.Value, (ImageLayout)renderTarget.Image.CurrentLayout,
+ AccessFlags.AccessNoneKhr,
+ ImageLayout.TransferSrcOptimal,
+ AccessFlags.AccessTransferReadBit,
+ renderTarget.MipLevels);
+
+ var srcBlitRegion = new ImageBlit
+ {
+ SrcOffsets = new ImageBlit.SrcOffsetsBuffer
+ {
+ Element0 = new Offset3D(0, 0, 0),
+ Element1 = new Offset3D(renderTarget.Size.Width, renderTarget.Size.Height, 1),
+ },
+ DstOffsets = new ImageBlit.DstOffsetsBuffer
+ {
+ Element0 = new Offset3D(0, 0, 0),
+ Element1 = new Offset3D(Size.Width, Size.Height, 1),
+ },
+ SrcSubresource = new ImageSubresourceLayers
+ {
+ AspectMask = ImageAspectFlags.ImageAspectColorBit,
+ BaseArrayLayer = 0,
+ LayerCount = 1,
+ MipLevel = 0
+ },
+ DstSubresource = new ImageSubresourceLayers
+ {
+ AspectMask = ImageAspectFlags.ImageAspectColorBit,
+ BaseArrayLayer = 0,
+ LayerCount = 1,
+ MipLevel = 0
+ }
+ };
+
+ _device.Api.CmdBlitImage(commandBuffer, renderTarget.Image.InternalHandle.Value,
+ ImageLayout.TransferSrcOptimal,
+ _swapchainImages[_nextImage],
+ ImageLayout.TransferDstOptimal,
+ 1,
+ srcBlitRegion,
+ Filter.Linear);
+
+ VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
+ renderTarget.Image.InternalHandle.Value, ImageLayout.TransferSrcOptimal,
+ AccessFlags.AccessTransferReadBit,
+ (ImageLayout)renderTarget.Image.CurrentLayout,
+ AccessFlags.AccessNoneKhr,
+ renderTarget.MipLevels);
+ }
+
+ internal unsafe void EndPresentation(VulkanCommandBufferPool.VulkanCommandBuffer commandBuffer)
+ {
+ VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
+ _swapchainImages[_nextImage], ImageLayout.TransferDstOptimal,
+ AccessFlags.AccessNoneKhr,
+ ImageLayout.PresentSrcKhr,
+ AccessFlags.AccessNoneKhr,
+ 1);
+
+ commandBuffer.Submit(
+ stackalloc[] { _semaphorePair.ImageAvailableSemaphore },
+ stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit },
+ stackalloc[] { _semaphorePair.RenderFinishedSemaphore });
+
+ var semaphore = _semaphorePair.RenderFinishedSemaphore;
+ var swapchain = _swapchain;
+ var nextImage = _nextImage;
+
+ Result result;
+
+ var presentInfo = new PresentInfoKHR
+ {
+ SType = StructureType.PresentInfoKhr,
+ WaitSemaphoreCount = 1,
+ PWaitSemaphores = &semaphore,
+ SwapchainCount = 1,
+ PSwapchains = &swapchain,
+ PImageIndices = &nextImage,
+ PResults = &result
+ };
+
+ lock (_device.Lock)
+ {
+ _swapchainExtension.QueuePresent(_device.PresentQueue.InternalHandle, presentInfo);
+ }
+
+ CommandBufferPool.FreeUsedCommandBuffers();
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs
new file mode 100644
index 00000000..343ba760
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs
@@ -0,0 +1,167 @@
+using System;
+using Avalonia;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanImage : IDisposable
+ {
+ private readonly VulkanDevice _device;
+ private readonly VulkanPhysicalDevice _physicalDevice;
+ private readonly VulkanCommandBufferPool _commandBufferPool;
+ private ImageLayout _currentLayout;
+ private AccessFlags _currentAccessFlags;
+ private ImageUsageFlags _imageUsageFlags { get; }
+ private ImageView? _imageView { get; set; }
+ private DeviceMemory _imageMemory { get; set; }
+
+ internal Image? InternalHandle { get; private set; }
+ internal Format Format { get; }
+ internal ImageAspectFlags AspectFlags { get; private set; }
+
+ public ulong Handle => InternalHandle?.Handle ?? 0;
+ public ulong ViewHandle => _imageView?.Handle ?? 0;
+ public uint UsageFlags => (uint)_imageUsageFlags;
+ public ulong MemoryHandle => _imageMemory.Handle;
+ public uint MipLevels { get; private set; }
+ public PixelSize Size { get; }
+ public ulong MemorySize { get; private set; }
+ public uint CurrentLayout => (uint)_currentLayout;
+
+ public VulkanImage(
+ VulkanDevice device,
+ VulkanPhysicalDevice physicalDevice,
+ VulkanCommandBufferPool commandBufferPool,
+ uint format,
+ PixelSize size,
+ uint mipLevels = 0)
+ {
+ _device = device;
+ _physicalDevice = physicalDevice;
+ _commandBufferPool = commandBufferPool;
+ Format = (Format)format;
+ Size = size;
+ MipLevels = mipLevels;
+ _imageUsageFlags =
+ ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit |
+ ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit;
+
+ Initialize();
+ }
+
+ public unsafe void Initialize()
+ {
+ if (!InternalHandle.HasValue)
+ {
+ MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2));
+
+ var imageCreateInfo = new ImageCreateInfo
+ {
+ SType = StructureType.ImageCreateInfo,
+ ImageType = ImageType.ImageType2D,
+ Format = Format,
+ Extent = new Extent3D((uint?)Size.Width, (uint?)Size.Height, 1),
+ MipLevels = MipLevels,
+ ArrayLayers = 1,
+ Samples = SampleCountFlags.SampleCount1Bit,
+ Tiling = Tiling,
+ Usage = _imageUsageFlags,
+ SharingMode = SharingMode.Exclusive,
+ InitialLayout = ImageLayout.Undefined,
+ Flags = ImageCreateFlags.ImageCreateMutableFormatBit
+ };
+
+ _device.Api.CreateImage(_device.InternalHandle, imageCreateInfo, null, out var image).ThrowOnError();
+ InternalHandle = image;
+
+ _device.Api.GetImageMemoryRequirements(_device.InternalHandle, InternalHandle.Value,
+ out var memoryRequirements);
+
+ var memoryAllocateInfo = new MemoryAllocateInfo
+ {
+ SType = StructureType.MemoryAllocateInfo,
+ AllocationSize = memoryRequirements.Size,
+ MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
+ _physicalDevice,
+ memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
+ };
+
+ _device.Api.AllocateMemory(_device.InternalHandle, memoryAllocateInfo, null,
+ out var imageMemory);
+
+ _imageMemory = imageMemory;
+
+ _device.Api.BindImageMemory(_device.InternalHandle, InternalHandle.Value, _imageMemory, 0);
+
+ MemorySize = memoryRequirements.Size;
+
+ var componentMapping = new ComponentMapping(
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity,
+ ComponentSwizzle.Identity);
+
+ AspectFlags = ImageAspectFlags.ImageAspectColorBit;
+
+ var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1);
+
+ var imageViewCreateInfo = new ImageViewCreateInfo
+ {
+ SType = StructureType.ImageViewCreateInfo,
+ Image = InternalHandle.Value,
+ ViewType = ImageViewType.ImageViewType2D,
+ Format = Format,
+ Components = componentMapping,
+ SubresourceRange = subresourceRange
+ };
+
+ _device.Api
+ .CreateImageView(_device.InternalHandle, imageViewCreateInfo, null, out var imageView)
+ .ThrowOnError();
+
+ _imageView = imageView;
+
+ _currentLayout = ImageLayout.Undefined;
+
+ TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
+ }
+ }
+
+ public ImageTiling Tiling => ImageTiling.Optimal;
+
+ internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
+ {
+ var commandBuffer = _commandBufferPool.CreateCommandBuffer();
+ commandBuffer.BeginRecording();
+
+ VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, InternalHandle.Value,
+ _currentLayout,
+ _currentAccessFlags,
+ destinationLayout, destinationAccessFlags,
+ MipLevels);
+
+ commandBuffer.EndRecording();
+
+ commandBuffer.Submit();
+
+ _currentLayout = destinationLayout;
+ _currentAccessFlags = destinationAccessFlags;
+ }
+
+ public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags)
+ {
+ TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags);
+ }
+
+ public unsafe void Dispose()
+ {
+ _device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, null);
+ _device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, null);
+ _device.Api.FreeMemory(_device.InternalHandle, _imageMemory, null);
+
+ _imageView = default;
+ InternalHandle = default;
+ _imageMemory = default;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs
new file mode 100644
index 00000000..a3a9ea61
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Silk.NET.Core;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.EXT;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ public class VulkanInstance : IDisposable
+ {
+ private const string EngineName = "Avalonia Vulkan";
+
+ private VulkanInstance(Instance apiHandle, Vk api)
+ {
+ InternalHandle = apiHandle;
+ Api = api;
+ }
+
+ public IntPtr Handle => InternalHandle.Handle;
+
+ internal Instance InternalHandle { get; }
+ public Vk Api { get; }
+
+ internal static IEnumerable<string> RequiredInstanceExtensions
+ {
+ get
+ {
+ yield return "VK_KHR_surface";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ yield return "VK_KHR_xlib_surface";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ yield return "VK_KHR_win32_surface";
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Api?.DestroyInstance(InternalHandle, Span<AllocationCallbacks>.Empty);
+ Api?.Dispose();
+ }
+
+ internal static unsafe VulkanInstance Create(VulkanOptions options)
+ {
+ var api = Vk.GetApi();
+ var applicationName = Marshal.StringToHGlobalAnsi(options.ApplicationName);
+ var engineName = Marshal.StringToHGlobalAnsi(EngineName);
+ var enabledExtensions = new List<string>(options.InstanceExtensions);
+
+ enabledExtensions.AddRange(RequiredInstanceExtensions);
+
+ var applicationInfo = new ApplicationInfo
+ {
+ PApplicationName = (byte*)applicationName,
+ ApiVersion = new Version32((uint)options.VulkanVersion.Major, (uint)options.VulkanVersion.Minor,
+ (uint)options.VulkanVersion.Build),
+ PEngineName = (byte*)engineName,
+ EngineVersion = new Version32(1, 0, 0),
+ ApplicationVersion = new Version32(1, 0, 0)
+ };
+
+ var enabledLayers = new HashSet<string>();
+
+ if (options.UseDebug)
+ {
+ enabledExtensions.Add(ExtDebugUtils.ExtensionName);
+ enabledExtensions.Add(ExtDebugReport.ExtensionName);
+ if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation"))
+ enabledLayers.Add("VK_LAYER_KHRONOS_validation");
+ }
+
+ foreach (var layer in options.EnabledLayers)
+ enabledLayers.Add(layer);
+
+ var ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Count];
+ var ppEnabledLayers = stackalloc IntPtr[enabledLayers.Count];
+
+ for (var i = 0; i < enabledExtensions.Count; i++)
+ ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]);
+
+ var layers = enabledLayers.ToList();
+
+ for (var i = 0; i < enabledLayers.Count; i++)
+ ppEnabledLayers[i] = Marshal.StringToHGlobalAnsi(layers[i]);
+
+ var instanceCreateInfo = new InstanceCreateInfo
+ {
+ SType = StructureType.InstanceCreateInfo,
+ PApplicationInfo = &applicationInfo,
+ PpEnabledExtensionNames = (byte**)ppEnabledExtensions,
+ PpEnabledLayerNames = (byte**)ppEnabledLayers,
+ EnabledExtensionCount = (uint)enabledExtensions.Count,
+ EnabledLayerCount = (uint)enabledLayers.Count
+ };
+
+ api.CreateInstance(in instanceCreateInfo, null, out var instance).ThrowOnError();
+
+ Marshal.FreeHGlobal(applicationName);
+ Marshal.FreeHGlobal(engineName);
+
+ for (var i = 0; i < enabledExtensions.Count; i++) Marshal.FreeHGlobal(ppEnabledExtensions[i]);
+
+ for (var i = 0; i < enabledLayers.Count; i++) Marshal.FreeHGlobal(ppEnabledLayers[i]);
+
+ return new VulkanInstance(instance, api);
+ }
+
+ private static unsafe bool IsLayerAvailable(Vk api, string layerName)
+ {
+ uint layerPropertiesCount;
+
+ api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError();
+
+ var layerProperties = new LayerProperties[layerPropertiesCount];
+
+ fixed (LayerProperties* pLayerProperties = layerProperties)
+ {
+ api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError();
+
+ for (var i = 0; i < layerPropertiesCount; i++)
+ {
+ var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName);
+
+ if (currentLayerName == layerName) return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs
new file mode 100644
index 00000000..a7052592
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs
@@ -0,0 +1,59 @@
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal static class VulkanMemoryHelper
+ {
+ internal static int FindSuitableMemoryTypeIndex(VulkanPhysicalDevice physicalDevice, uint memoryTypeBits,
+ MemoryPropertyFlags flags)
+ {
+ physicalDevice.Api.GetPhysicalDeviceMemoryProperties(physicalDevice.InternalHandle, out var properties);
+
+ for (var i = 0; i < properties.MemoryTypeCount; i++)
+ {
+ var type = properties.MemoryTypes[i];
+
+ if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i;
+ }
+
+ return -1;
+ }
+
+ internal static unsafe void TransitionLayout(VulkanDevice device,
+ CommandBuffer commandBuffer,
+ Image image,
+ ImageLayout sourceLayout,
+ AccessFlags sourceAccessMask,
+ ImageLayout destinationLayout,
+ AccessFlags destinationAccessMask,
+ uint mipLevels)
+ {
+ var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1);
+
+ var barrier = new ImageMemoryBarrier
+ {
+ SType = StructureType.ImageMemoryBarrier,
+ SrcAccessMask = sourceAccessMask,
+ DstAccessMask = destinationAccessMask,
+ OldLayout = sourceLayout,
+ NewLayout = destinationLayout,
+ SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
+ Image = image,
+ SubresourceRange = subresourceRange
+ };
+
+ device.Api.CmdPipelineBarrier(
+ commandBuffer,
+ PipelineStageFlags.PipelineStageAllCommandsBit,
+ PipelineStageFlags.PipelineStageAllCommandsBit,
+ 0,
+ 0,
+ null,
+ 0,
+ null,
+ 1,
+ barrier);
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs
new file mode 100644
index 00000000..8e836398
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ public class VulkanOptions
+ {
+ /// <summary>
+ /// Sets the application name of the Vulkan instance
+ /// </summary>
+ public string ApplicationName { get; set; }
+
+ /// <summary>
+ /// Specifies the Vulkan API version to use
+ /// </summary>
+ public Version VulkanVersion { get; set; } = new Version(1, 1, 0);
+
+ /// <summary>
+ /// Specifies additional extensions to enable if available on the instance
+ /// </summary>
+ public IEnumerable<string> InstanceExtensions { get; set; } = Enumerable.Empty<string>();
+
+ /// <summary>
+ /// Specifies layers to enable if available on the instance
+ /// </summary>
+ public IEnumerable<string> EnabledLayers { get; set; } = Enumerable.Empty<string>();
+
+ /// <summary>
+ /// Enables the debug layer
+ /// </summary>
+ public bool UseDebug { get; set; }
+
+ /// <summary>
+ /// Selects the first suitable discrete GPU available
+ /// </summary>
+ public bool PreferDiscreteGpu { get; set; }
+
+ /// <summary>
+ /// Sets the device to use if available and suitable.
+ /// </summary>
+ public string PreferredDevice { get; set; }
+
+ /// <summary>
+ /// Max number of device queues to request
+ /// </summary>
+ public uint MaxQueueCount { get; set; }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs
new file mode 100644
index 00000000..11444d30
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs
@@ -0,0 +1,219 @@
+using Ryujinx.Graphics.Vulkan;
+using Silk.NET.Core;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ public unsafe class VulkanPhysicalDevice
+ {
+ private VulkanPhysicalDevice(PhysicalDevice apiHandle, Vk api, uint queueCount, uint queueFamilyIndex)
+ {
+ InternalHandle = apiHandle;
+ Api = api;
+ QueueCount = queueCount;
+ QueueFamilyIndex = queueFamilyIndex;
+
+ api.GetPhysicalDeviceProperties(apiHandle, out var properties);
+
+ DeviceName = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName);
+ DeviceId = VulkanInitialization.StringFromIdPair(properties.VendorID, properties.DeviceID);
+
+ var version = (Version32)properties.ApiVersion;
+ ApiVersion = new Version((int)version.Major, (int)version.Minor, 0, (int)version.Patch);
+ }
+
+ internal PhysicalDevice InternalHandle { get; }
+ internal Vk Api { get; }
+ public uint QueueCount { get; }
+ public uint QueueFamilyIndex { get; }
+ public IntPtr Handle => InternalHandle.Handle;
+
+ public string DeviceName { get; }
+ public string DeviceId { get; }
+ public Version ApiVersion { get; }
+ public static Dictionary<PhysicalDevice, PhysicalDeviceProperties> PhysicalDevices { get; private set; }
+ public static IEnumerable<KeyValuePair<PhysicalDevice, PhysicalDeviceProperties>> SuitableDevices { get; private set; }
+
+ internal static void SelectAvailableDevices(VulkanInstance instance,
+ VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
+ {
+ uint physicalDeviceCount;
+
+ instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, null).ThrowOnError();
+
+ var physicalDevices = new PhysicalDevice[physicalDeviceCount];
+
+ fixed (PhysicalDevice* pPhysicalDevices = physicalDevices)
+ {
+ instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, pPhysicalDevices)
+ .ThrowOnError();
+ }
+
+ PhysicalDevices = new Dictionary<PhysicalDevice, PhysicalDeviceProperties>();
+
+ foreach (var physicalDevice in physicalDevices)
+ {
+ instance.Api.GetPhysicalDeviceProperties(physicalDevice, out var properties);
+ PhysicalDevices.Add(physicalDevice, properties);
+ }
+
+ SuitableDevices = PhysicalDevices.Where(x => IsSuitableDevice(
+ instance.Api,
+ x.Key,
+ x.Value,
+ surface.ApiHandle,
+ out _,
+ out _));
+ }
+
+ internal static VulkanPhysicalDevice FindSuitablePhysicalDevice(VulkanInstance instance,
+ VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
+ {
+ SelectAvailableDevices(instance, surface, preferDiscreteGpu, preferredDevice);
+
+ uint queueFamilyIndex = 0;
+ uint queueCount = 0;
+
+ if (!string.IsNullOrWhiteSpace(preferredDevice))
+ {
+ var physicalDevice = SuitableDevices.FirstOrDefault(x => VulkanInitialization.StringFromIdPair(x.Value.VendorID, x.Value.DeviceID) == preferredDevice);
+
+ queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
+ surface.ApiHandle, out queueCount);
+ if (queueFamilyIndex != int.MaxValue)
+ {
+ return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
+ }
+ }
+
+ if (preferDiscreteGpu)
+ {
+ var discreteGpus = SuitableDevices.Where(p => p.Value.DeviceType == PhysicalDeviceType.DiscreteGpu);
+
+ foreach (var gpu in discreteGpus)
+ {
+ queueFamilyIndex = FindSuitableQueueFamily(instance.Api, gpu.Key,
+ surface.ApiHandle, out queueCount);
+ if (queueFamilyIndex != int.MaxValue)
+ {
+ return new VulkanPhysicalDevice(gpu.Key, instance.Api, queueCount, queueFamilyIndex);
+ }
+ }
+ }
+
+ foreach (var physicalDevice in SuitableDevices)
+ {
+ queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
+ surface.ApiHandle, out queueCount);
+ if (queueFamilyIndex != int.MaxValue)
+ {
+ return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
+ }
+ }
+
+ throw new Exception("No suitable physical device found");
+ }
+
+ private static unsafe bool IsSuitableDevice(Vk api, PhysicalDevice physicalDevice, PhysicalDeviceProperties properties, SurfaceKHR surface,
+ out uint queueCount, out uint familyIndex)
+ {
+ queueCount = 0;
+ familyIndex = 0;
+
+ if (properties.DeviceType == PhysicalDeviceType.Cpu) return false;
+
+ var extensionMatches = 0;
+ uint propertiesCount;
+
+ api.EnumerateDeviceExtensionProperties(physicalDevice, (byte*)null, &propertiesCount, null).ThrowOnError();
+
+ var extensionProperties = new ExtensionProperties[propertiesCount];
+
+ fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
+ {
+ api.EnumerateDeviceExtensionProperties(
+ physicalDevice,
+ (byte*)null,
+ &propertiesCount,
+ pExtensionProperties).ThrowOnError();
+
+ for (var i = 0; i < propertiesCount; i++)
+ {
+ var extensionName = Marshal.PtrToStringAnsi((IntPtr)pExtensionProperties[i].ExtensionName);
+
+ if (VulkanInitialization.RequiredExtensions.Contains(extensionName))
+ {
+ extensionMatches++;
+ }
+ }
+ }
+
+ if (extensionMatches == VulkanInitialization.RequiredExtensions.Length)
+ {
+ familyIndex = FindSuitableQueueFamily(api, physicalDevice, surface, out queueCount);
+
+ return familyIndex != uint.MaxValue;
+ }
+
+ return false;
+ }
+
+ internal unsafe string[] GetSupportedExtensions()
+ {
+ uint propertiesCount;
+
+ Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, null).ThrowOnError();
+
+ var extensionProperties = new ExtensionProperties[propertiesCount];
+
+ fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
+ {
+ Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, pExtensionProperties)
+ .ThrowOnError();
+ }
+
+ return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToArray();
+ }
+
+ private static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface,
+ out uint queueCount)
+ {
+ const QueueFlags RequiredFlags = QueueFlags.QueueGraphicsBit | QueueFlags.QueueComputeBit;
+
+ var khrSurface = new KhrSurface(api.Context);
+
+ uint propertiesCount;
+
+ api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, null);
+
+ var properties = new QueueFamilyProperties[propertiesCount];
+
+ fixed (QueueFamilyProperties* pProperties = properties)
+ {
+ api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, pProperties);
+ }
+
+ for (uint index = 0; index < propertiesCount; index++)
+ {
+ var queueFlags = properties[index].QueueFlags;
+
+ khrSurface.GetPhysicalDeviceSurfaceSupport(physicalDevice, index, surface, out var surfaceSupported)
+ .ThrowOnError();
+
+ if (queueFlags.HasFlag(RequiredFlags) && surfaceSupported)
+ {
+ queueCount = properties[index].QueueCount;
+ return index;
+ }
+ }
+
+ queueCount = 0;
+ return uint.MaxValue;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs
new file mode 100644
index 00000000..47a07949
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs
@@ -0,0 +1,80 @@
+using Avalonia;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using Ryujinx.Graphics.Vulkan;
+using Silk.NET.Vulkan;
+using System;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanPlatformInterface : IDisposable
+ {
+ private static VulkanOptions _options;
+
+ private VulkanPlatformInterface(VulkanInstance instance)
+ {
+ Instance = instance;
+ Api = instance.Api;
+ }
+
+ public VulkanPhysicalDevice PhysicalDevice { get; private set; }
+ public VulkanInstance Instance { get; }
+ public VulkanDevice Device { get; set; }
+ public Vk Api { get; private set; }
+ public VulkanSurfaceRenderTarget MainSurface { get; set; }
+
+ public void Dispose()
+ {
+ Device?.Dispose();
+ Instance?.Dispose();
+ Api?.Dispose();
+ }
+
+ private static VulkanPlatformInterface TryCreate()
+ {
+ _options = AvaloniaLocator.Current.GetService<VulkanOptions>() ?? new VulkanOptions();
+
+ var instance = VulkanInstance.Create(_options);
+
+ return new VulkanPlatformInterface(instance);
+ }
+
+ public static bool TryInitialize()
+ {
+ var feature = TryCreate();
+ if (feature != null)
+ {
+ AvaloniaLocator.CurrentMutable.Bind<VulkanPlatformInterface>().ToConstant(feature);
+ return true;
+ }
+
+ return false;
+ }
+
+ public VulkanSurfaceRenderTarget CreateRenderTarget(IVulkanPlatformSurface platformSurface)
+ {
+ var surface = VulkanSurface.CreateSurface(Instance, platformSurface);
+
+ if (Device == null)
+ {
+ PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice);
+ var device = VulkanInitialization.CreateDevice(Instance.Api,
+ PhysicalDevice.InternalHandle,
+ PhysicalDevice.QueueFamilyIndex,
+ VulkanInitialization.GetSupportedExtensions(Instance.Api, PhysicalDevice.InternalHandle),
+ PhysicalDevice.QueueCount);
+
+ Device = new VulkanDevice(device, PhysicalDevice, Instance.Api);
+ }
+
+ var renderTarget = new VulkanSurfaceRenderTarget(this, surface);
+
+ if (MainSurface == null && surface != null)
+ {
+ MainSurface = renderTarget;
+ MainSurface.Display.ChangeVSyncMode(false);
+ }
+
+ return renderTarget;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs
new file mode 100644
index 00000000..a903e21a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs
@@ -0,0 +1,18 @@
+using System;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanQueue
+ {
+ public VulkanQueue(VulkanDevice device, Queue apiHandle)
+ {
+ Device = device;
+ InternalHandle = apiHandle;
+ }
+
+ public VulkanDevice Device { get; }
+ public IntPtr Handle => InternalHandle.Handle;
+ internal Queue InternalHandle { get; }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs
new file mode 100644
index 00000000..3b5fd9cc
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs
@@ -0,0 +1,32 @@
+using System;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanSemaphorePair : IDisposable
+ {
+ private readonly VulkanDevice _device;
+
+ public unsafe VulkanSemaphorePair(VulkanDevice device)
+ {
+ _device = device;
+
+ var semaphoreCreateInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo };
+
+ _device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
+ ImageAvailableSemaphore = semaphore;
+
+ _device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out semaphore).ThrowOnError();
+ RenderFinishedSemaphore = semaphore;
+ }
+
+ internal Semaphore ImageAvailableSemaphore { get; }
+ internal Semaphore RenderFinishedSemaphore { get; }
+
+ public unsafe void Dispose()
+ {
+ _device.Api.DestroySemaphore(_device.InternalHandle, ImageAvailableSemaphore, null);
+ _device.Api.DestroySemaphore(_device.InternalHandle, RenderFinishedSemaphore, null);
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs
new file mode 100644
index 00000000..2452cdcd
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs
@@ -0,0 +1,75 @@
+using System;
+using Avalonia;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ public class VulkanSurface : IDisposable
+ {
+ private readonly VulkanInstance _instance;
+ private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
+
+ private VulkanSurface(IVulkanPlatformSurface vulkanPlatformSurface, VulkanInstance instance)
+ {
+ _vulkanPlatformSurface = vulkanPlatformSurface;
+ _instance = instance;
+ ApiHandle = vulkanPlatformSurface.CreateSurface(instance);
+ }
+
+ internal SurfaceKHR ApiHandle { get; }
+
+ internal static KhrSurface SurfaceExtension { get; private set; }
+
+ internal PixelSize SurfaceSize => _vulkanPlatformSurface.SurfaceSize;
+
+ public void Dispose()
+ {
+ SurfaceExtension.DestroySurface(_instance.InternalHandle, ApiHandle, Span<AllocationCallbacks>.Empty);
+ _vulkanPlatformSurface.Dispose();
+ }
+
+ internal static VulkanSurface CreateSurface(VulkanInstance instance, IVulkanPlatformSurface vulkanPlatformSurface)
+ {
+ if (SurfaceExtension == null)
+ {
+ instance.Api.TryGetInstanceExtension(instance.InternalHandle, out KhrSurface extension);
+
+ SurfaceExtension = extension;
+ }
+
+ return new VulkanSurface(vulkanPlatformSurface, instance);
+ }
+
+ internal bool CanSurfacePresent(VulkanPhysicalDevice physicalDevice)
+ {
+ SurfaceExtension.GetPhysicalDeviceSurfaceSupport(physicalDevice.InternalHandle, physicalDevice.QueueFamilyIndex, ApiHandle, out var isSupported);
+
+ return isSupported;
+ }
+
+ internal SurfaceFormatKHR GetSurfaceFormat(VulkanPhysicalDevice physicalDevice)
+ {
+ Span<uint> surfaceFormatsCount = stackalloc uint[1];
+ SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, Span<SurfaceFormatKHR>.Empty);
+ Span<SurfaceFormatKHR> surfaceFormats = stackalloc SurfaceFormatKHR[(int)surfaceFormatsCount[0]];
+ SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, surfaceFormats);
+
+ if (surfaceFormats.Length == 1 && surfaceFormats[0].Format == Format.Undefined)
+ {
+ return new SurfaceFormatKHR(Format.B8G8R8A8Unorm, ColorSpaceKHR.ColorspaceSrgbNonlinearKhr);
+ }
+
+ foreach (var format in surfaceFormats)
+ {
+ if (format.Format == Format.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.ColorspaceSrgbNonlinearKhr)
+ {
+ return format;
+ }
+ }
+
+ return surfaceFormats[0];
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs
new file mode 100644
index 00000000..8833ede5
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs
@@ -0,0 +1,48 @@
+using System;
+using Avalonia;
+using Ryujinx.Ava.Ui.Vulkan.Surfaces;
+using Silk.NET.Vulkan;
+
+namespace Ryujinx.Ava.Ui.Vulkan
+{
+ internal class VulkanSurfaceRenderingSession : IDisposable
+ {
+ private readonly VulkanDevice _device;
+ private readonly VulkanSurfaceRenderTarget _renderTarget;
+ private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer;
+
+ public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device,
+ VulkanSurfaceRenderTarget renderTarget, float scaling)
+ {
+ Display = display;
+ _device = device;
+ _renderTarget = renderTarget;
+ Scaling = scaling;
+ Begin();
+ }
+
+ public VulkanDisplay Display { get; }
+
+ public PixelSize Size => _renderTarget.Size;
+ public Vk Api => _device.Api;
+
+ public float Scaling { get; }
+
+ private void Begin()
+ {
+ if (!Display.EnsureSwapchainAvailable())
+ {
+ _renderTarget.Invalidate();
+ }
+ }
+
+ public void Dispose()
+ {
+ _commandBuffer = Display.StartPresentation(_renderTarget);
+
+ Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle);
+
+ Display.EndPresentation(_commandBuffer);
+ }
+ }
+}