diff options
Diffstat (limited to 'Ryujinx.Ava/Ui/Backend/Vulkan')
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); + } + } +} |