From 0d624afeb3d83de0ac0f56beb7edd97cbeb3fb03 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 3 Jun 2022 00:09:51 +0000 Subject: [PATCH] Add support for avalonia (#6) * add avalonia support * only lock around skia flush * addressed review * cleanup * add fallback size if avalonia attempts to render but the window size is 0. read desktop scale after enabling dpi check * fix getting window handle on linux. skip render is size is 0 --- Ryujinx.Ava/AppHost.cs | 32 +- Ryujinx.Ava/Program.cs | 38 +- Ryujinx.Ava/Ryujinx.Ava.csproj | 7 +- .../Applet/AvaloniaDynamicTextInputHandler.cs | 2 +- Ryujinx.Ava/Ui/Backend/BackendSurface.cs | 71 +++ Ryujinx.Ava/Ui/Backend/Interop.cs | 49 +++ Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs | 23 + .../Ui/Backend/Vulkan/ResultExtensions.cs | 13 + .../Backend/Vulkan/Skia/VulkanRenderTarget.cs | 134 ++++++ .../Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs | 118 +++++ .../Ui/Backend/Vulkan/Skia/VulkanSurface.cs | 58 +++ .../Vulkan/Surfaces/IVulkanPlatformSurface.cs | 13 + .../Surfaces/VulkanSurfaceRenderTarget.cs | 92 ++++ .../Backend/Vulkan/VulkanCommandBufferPool.cs | 177 ++++++++ Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs | 67 +++ .../Ui/Backend/Vulkan/VulkanDisplay.cs | 406 ++++++++++++++++++ Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs | 167 +++++++ .../Ui/Backend/Vulkan/VulkanInstance.cs | 138 ++++++ .../Ui/Backend/Vulkan/VulkanMemoryHelper.cs | 59 +++ .../Ui/Backend/Vulkan/VulkanOptions.cs | 48 +++ .../Ui/Backend/Vulkan/VulkanPhysicalDevice.cs | 203 +++++++++ .../Backend/Vulkan/VulkanPlatformInterface.cs | 84 ++++ Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs | 18 + .../Ui/Backend/Vulkan/VulkanSemaphorePair.cs | 32 ++ .../Ui/Backend/Vulkan/VulkanSurface.cs | 77 ++++ .../Vulkan/VulkanSurfaceRenderingSession.cs | 48 +++ .../Ui/Controls/OpenGLRendererControl.cs | 192 +++++++++ Ryujinx.Ava/Ui/Controls/RendererControl.cs | 208 ++------- .../Ui/Controls/VulkanRendererControl.cs | 153 +++++++ Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs | 16 +- Ryujinx.Common/System/ForceDpiAware.cs | 9 +- .../Multithreading/ThreadedRenderer.cs | 2 +- .../Multithreading/ThreadedWindow.cs | 8 +- Ryujinx.Graphics.Vulkan/ImageWindow.cs | 365 ++++++++++++++++ .../VulkanGraphicsDevice.cs | 150 +++++-- .../VulkanInitialization.cs | 105 ++--- Ryujinx.Graphics.Vulkan/Window.cs | 10 +- Ryujinx.Graphics.Vulkan/WindowBase.cs | 14 + 38 files changed, 3095 insertions(+), 311 deletions(-) create mode 100644 Ryujinx.Ava/Ui/Backend/BackendSurface.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Interop.cs create mode 100644 Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs create mode 100644 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs create mode 100644 Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs create mode 100644 Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs create mode 100644 Ryujinx.Graphics.Vulkan/ImageWindow.cs create mode 100644 Ryujinx.Graphics.Vulkan/WindowBase.cs diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs index 55693ed381c0..1e4694cda31d 100644 --- a/Ryujinx.Ava/AppHost.cs +++ b/Ryujinx.Ava/AppHost.cs @@ -1,5 +1,6 @@ using ARMeilleure.Translation; using ARMeilleure.Translation.PTC; +using Avalonia; using Avalonia.Input; using Avalonia.Threading; using LibHac.Tools.FsSystem; @@ -13,6 +14,7 @@ using Ryujinx.Ava.Input; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Models; +using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -22,6 +24,7 @@ using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.OpenGL; +using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; @@ -584,7 +587,23 @@ private void InitializeSwitchInstance() { VirtualFileSystem.ReloadKeySet(); - IRenderer renderer = new Renderer(); + IRenderer renderer; + + if (Program.UseVulkan) + { + var vulkan = AvaloniaLocator.Current.GetService(); + renderer = new VulkanGraphicsDevice(vulkan.Instance.InternalHandle, + vulkan.Device.InternalHandle, + vulkan.PhysicalDevice.InternalHandle, + vulkan.Device.Queue.InternalHandle, + vulkan.PhysicalDevice.QueueFamilyIndex, + vulkan.Device.Lock); + } + else + { + renderer = new Renderer(); + } + IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver(); BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; @@ -792,9 +811,12 @@ private unsafe void RenderLoop() _renderer.ScreenCaptured += Renderer_ScreenCaptured; - (_renderer as Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GameContext)); + if (!Program.UseVulkan) + { + (_renderer as Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext)); - Renderer.MakeCurrent(); + Renderer.MakeCurrent(); + } Device.Gpu.Renderer.Initialize(_glLogLevel); @@ -853,8 +875,6 @@ private void Present(object image) dockedMode += $" ({scale}x)"; } - string vendor = _renderer is Renderer renderer ? renderer.GpuVendor : ""; - StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( Device.EnableDeviceVsync, Device.GetVolume(), @@ -862,7 +882,7 @@ private void Present(object image) ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), LocaleManager.Instance["Game"] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", - $"GPU: {vendor}")); + $"GPU: {_renderer.GetHardwareInfo().GpuVendor}")); Renderer.Present(image); } diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 8af7af7a9232..60bb03e59663 100644 --- a/Ryujinx.Ava/Program.cs +++ b/Ryujinx.Ava/Program.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Rendering; using Avalonia.Threading; +using Ryujinx.Ava.Ui.Backend; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common; @@ -11,9 +12,12 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.System; using Ryujinx.Common.SystemInfo; +using Ryujinx.Graphics.Vulkan; using Ryujinx.Modules; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; +using Silk.NET.Vulkan.Extensions.EXT; +using Silk.NET.Vulkan.Extensions.KHR; using System; using System.Collections.Generic; using System.IO; @@ -25,17 +29,20 @@ namespace Ryujinx.Ava internal class Program { public static double WindowScaleFactor { get; set; } + public static double ActualScaleFactor { get; set; } public static string Version { get; private set; } public static string ConfigurationPath { get; private set; } public static string CommandLineProfile { get; set; } public static bool PreviewerDetached { get; private set; } public static RenderTimer RenderTimer { get; private set; } + public static bool UseVulkan { get; private set; } [DllImport("user32.dll", SetLastError = true)] public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type); private const uint MB_ICONWARNING = 0x30; + private const int BaseDpi = 96; public static void Main(string[] args) { @@ -66,7 +73,7 @@ public static AppBuilder BuildAvaloniaApp() EnableMultiTouch = true, EnableIme = true, UseEGL = false, - UseGpu = true, + UseGpu = !UseVulkan, GlProfiles = new List() { new GlVersion(GlProfileType.OpenGL, 4, 3) @@ -75,7 +82,7 @@ public static AppBuilder BuildAvaloniaApp() .With(new Win32PlatformOptions { EnableMultitouch = true, - UseWgl = true, + UseWgl = !UseVulkan, WglProfiles = new List() { new GlVersion(GlProfileType.OpenGL, 4, 3) @@ -84,6 +91,18 @@ public static AppBuilder BuildAvaloniaApp() CompositionBackdropCornerRadius = 8f, }) .UseSkia() + .With(new Ui.Vulkan.VulkanOptions() + { + ApplicationName = "Ryujinx.Graphics.Vulkan", + VulkanVersion = new Version(1, 2), + MaxQueueCount = 2, + PreferDiscreteGpu = true, + UseDebug = !PreviewerDetached ? false : ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value != GraphicsDebugLevel.None, + }) + .With(new SkiaOptions() + { + CustomGpuFactory = UseVulkan ? SkiaGpuFactory.CreateVulkanGpu : null + }) .AfterSetup(_ => { AvaloniaLocator.CurrentMutable @@ -136,9 +155,6 @@ private static void Initialize(string[] args) } } - // Make process DPI aware for proper window sizing on high-res screens. - WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); - // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); @@ -162,6 +178,18 @@ private static void Initialize(string[] args) ReloadConfig(); + UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false; + + if (UseVulkan) + { + // With a custom gpu backend, avalonia doesn't enable dpi awareness, so the backend must handle it. This isn't so for the opengl backed, + // as that uses avalonia's gpu backend and it's enabled there. + ForceDpiAware.Windows(); + } + + WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); + ActualScaleFactor = ForceDpiAware.GetActualScaleFactor() / BaseDpi; + // Logging system information. PrintSystemInfo(); diff --git a/Ryujinx.Ava/Ryujinx.Ava.csproj b/Ryujinx.Ava/Ryujinx.Ava.csproj index 2f0f342d42ff..44bd85f053a8 100644 --- a/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -26,10 +26,12 @@ - + - + + + @@ -37,6 +39,7 @@ + diff --git a/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs index 294e8965465d..02a99c1d1a91 100644 --- a/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs @@ -135,7 +135,7 @@ public void Dispose() Dispatcher.UIThread.Post(() => { _hiddenTextBox.Clear(); - _parent.GlRenderer.Focus(); + _parent.RendererControl.Focus(); _parent = null; }); diff --git a/Ryujinx.Ava/Ui/Backend/BackendSurface.cs b/Ryujinx.Ava/Ui/Backend/BackendSurface.cs new file mode 100644 index 000000000000..4b54cae2c486 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/BackendSurface.cs @@ -0,0 +1,71 @@ +using Avalonia; +using System; +using System.Runtime.InteropServices; +using static Ryujinx.Ava.Ui.Backend.Interop; + +namespace Ryujinx.Ava.Ui.Backend +{ + public abstract class BackendSurface : IDisposable + { + protected IntPtr Display => _display; + + private IntPtr _display = IntPtr.Zero; + + [DllImport("libX11.so.6")] + public static extern IntPtr XOpenDisplay(IntPtr display); + + [DllImport("libX11.so.6")] + public static extern int XCloseDisplay(IntPtr display); + + private PixelSize _currentSize; + public IntPtr Handle { get; protected set; } + + public bool IsDisposed { get; private set; } + + public BackendSurface(IntPtr handle) + { + Handle = handle; + + if (OperatingSystem.IsLinux()) + { + _display = XOpenDisplay(IntPtr.Zero); + } + } + + public PixelSize Size + { + get + { + PixelSize size = new PixelSize(); + if (OperatingSystem.IsWindows()) + { + GetClientRect(Handle, out var rect); + size = new PixelSize(rect.right, rect.bottom); + } + else if (OperatingSystem.IsLinux()) + { + XWindowAttributes attributes = new XWindowAttributes(); + XGetWindowAttributes(Display, Handle, ref attributes); + + size = new PixelSize(attributes.width, attributes.height); + } + + _currentSize = size; + + return size; + } + } + + public PixelSize CurrentSize => _currentSize; + + public virtual void Dispose() + { + IsDisposed = true; + + if (_display != IntPtr.Zero) + { + XCloseDisplay(_display); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Backend/Interop.cs b/Ryujinx.Ava/Ui/Backend/Interop.cs new file mode 100644 index 000000000000..617e97678fb7 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Interop.cs @@ -0,0 +1,49 @@ +using FluentAvalonia.Interop; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.Ui.Backend +{ + public static class Interop + { + [StructLayout(LayoutKind.Sequential)] + public struct XWindowAttributes + { + public int x; + public int y; + public int width; + public int height; + public int border_width; + public int depth; + public IntPtr visual; + public IntPtr root; + public int c_class; + public int bit_gravity; + public int win_gravity; + public int backing_store; + public IntPtr backing_planes; + public IntPtr backing_pixel; + public int save_under; + public IntPtr colormap; + public int map_installed; + public int map_state; + public IntPtr all_event_masks; + public IntPtr your_event_mask; + public IntPtr do_not_propagate_mask; + public int override_direct; + public IntPtr screen; + } + + [DllImport("user32.dll")] + public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); + + [DllImport("libX11.so.6")] + public static extern int XCloseDisplay(IntPtr display); + + [DllImport("libX11.so.6")] + public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, ref XWindowAttributes attributes); + + [DllImport("libX11.so.6")] + public static extern IntPtr XOpenDisplay(IntPtr display); + } +} diff --git a/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs b/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs new file mode 100644 index 000000000000..db38c3835f44 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Skia; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Ava.Ui.Backend.Vulkan; + +namespace Ryujinx.Ava.Ui.Backend +{ + public static class SkiaGpuFactory + { + public static ISkiaGpu CreateVulkanGpu() + { + var skiaOptions = AvaloniaLocator.Current.GetService() ?? new SkiaOptions(); + var platformInterface = AvaloniaLocator.Current.GetService(); + if (platformInterface == null) + { + VulkanPlatformInterface.TryInitialize(); + } + var gpu = new VulkanSkiaGpu(skiaOptions.MaxGpuResourceSizeBytes); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gpu); + return gpu; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs new file mode 100644 index 000000000000..1fd88321dc4f --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs @@ -0,0 +1,13 @@ +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 000000000000..88aeea92ba15 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs @@ -0,0 +1,134 @@ +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, + session.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, + _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 = vulkanSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; + } + + 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 000000000000..325ddd6cf59c --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs @@ -0,0 +1,118 @@ +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 GRContext _grContext; + private GRVkBackendContext _grVkBackend; + private bool _initialized; + + public GRContext GrContext { get => _grContext; set => _grContext = value; } + + public VulkanSkiaGpu(long? maxResourceBytes) + { + _vulkan = AvaloniaLocator.Current.GetService(); + _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 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; + } + + var 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 000000000000..37a1577c0c0b --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs @@ -0,0 +1,58 @@ +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."); + } + + public override void Dispose() + { + base.Dispose(); + } + } +} \ 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 000000000000..642d8a6a3bdf --- /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 000000000000..f7d9684ce64f --- /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; + + public bool IsCorrupted { get; set; } = true; + private readonly Format _format; + + public VulkanImage Image { get; private set; } + + 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 000000000000..92c57905aa45 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs @@ -0,0 +1,177 @@ +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 _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(); + } + + public unsafe void Dispose() + { + FreeUsedCommandBuffers(); + _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, null); + } + + 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 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 unsafe 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, null); + } + + 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 waitSemaphores, + ReadOnlySpan waitDstStageMask, + ReadOnlySpan 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); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs new file mode 100644 index 000000000000..7eb42cdf06a4 --- /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 = new()) + { + lock (_lock) + { + Api.QueueSubmit(Queue.InternalHandle, 1, submitInfo, fence); + } + } + + 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 000000000000..2fbe7da8d2a6 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs @@ -0,0 +1,406 @@ +using System; +using System.Linq; +using System.Threading; +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +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]; + + 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 unsafe void Dispose() + { + _device.WaitIdle(); + _semaphorePair?.Dispose(); + DestroyCurrentImageViews(); + _swapchainExtension.DestroySwapchain(_device.InternalHandle, _swapchain, null); + CommandBufferPool.Dispose(); + } + + private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device, + VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent, + VulkanDisplay oldDisplay = null) + { + if (_swapchainExtension == null) + { + instance.Api.TryGetDeviceExtension(instance.InternalHandle, device.InternalHandle, out KhrSwapchain extension); + + _swapchainExtension = extension; + } + + while (!surface.CanSurfacePresent(physicalDevice)) + { + Thread.Sleep(16); + } + + VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfaceCapabilities(physicalDevice.InternalHandle, + surface.ApiHandle, out var capabilities); + + 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 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); + + if (capabilities.CurrentExtent.Width != uint.MaxValue) + { + swapchainExtent = capabilities.CurrentExtent; + } + else + { + var surfaceSize = surface.SurfaceSize; + + var width = Math.Max(capabilities.MinImageExtent.Width, + Math.Min(capabilities.MaxImageExtent.Width, (uint)surfaceSize.Width)); + var height = Math.Max(capabilities.MinImageExtent.Height, + Math.Min(capabilities.MaxImageExtent.Height, (uint)surfaceSize.Height)); + + swapchainExtent = new Extent2D(width, height); + } + + PresentModeKHR presentMode; + var modes = presentModes.ToList(); + + if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr)) + presentMode = PresentModeKHR.PresentModeImmediateKhr; + else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr)) + presentMode = PresentModeKHR.PresentModeMailboxKhr; + else + presentMode = PresentModeKHR.PresentModeFifoKhr; + + var compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr; + + if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr)) + { + compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr; + } + else if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr)) + { + compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr; + } + + 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 = oldDisplay?._swapchain ?? new SwapchainKHR() + }; + + _swapchainExtension.CreateSwapchain(device.InternalHandle, swapchainCreateInfo, null, out var swapchain) + .ThrowOnError(); + + if (oldDisplay != null) + { + _swapchainExtension.DestroySwapchain(device.InternalHandle, oldDisplay._swapchain, null); + } + + return swapchain; + } + + + internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device, + VulkanPhysicalDevice physicalDevice, VulkanSurface surface) + { + var swapchain = CreateSwapchain(instance, device, physicalDevice, surface, out var extent); + + 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 unsafe void DestroyCurrentImageViews() + { + if (_swapchainImageViews.Length > 0) + { + for (var i = 0; i < _swapchainImageViews.Length; i++) + { + _instance.Api.DestroyImageView(_device.InternalHandle, _swapchainImageViews[i], null); + } + } + } + + private void Recreate() + { + _device.WaitIdle(); + _swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out var extent, this); + + _swapchainExtent = extent; + + CreateSwapchainImages(); + } + + private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format) + { + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + var aspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(aspectFlags, 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) + { + 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( + new[] { _semaphorePair.ImageAvailableSemaphore }, + new[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit }, + new[] { _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 000000000000..343ba7605a79 --- /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 000000000000..910b0dd06edf --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs @@ -0,0 +1,138 @@ +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 IList RequiredInstanceExtensions + { + get + { + var extensions = new List { "VK_KHR_surface" }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + extensions.Add("VK_KHR_xlib_surface"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + extensions.Add("VK_KHR_win32_surface"); + } + + return extensions; + } + } + + public unsafe void Dispose() + { + Api?.DestroyInstance(InternalHandle, null); + 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(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(); + + if (options.UseDebug) + { + enabledExtensions.Add(ExtDebugUtils.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 000000000000..a705259201db --- /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 000000000000..66087c1d0bbc --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public class VulkanOptions + { + /// + /// Sets the application name of the Vulkan instance + /// + public string ApplicationName { get; set; } + + /// + /// Specifies the Vulkan API version to use + /// + public Version VulkanVersion{ get; set; } = new Version(1, 1, 0); + + /// + /// Specifies additional extensions to enable if available on the instance + /// + public IList InstanceExtensions { get; set; } = new List(); + + /// + /// Specifies layers to enable if available on the instance + /// + public IList EnabledLayers { get; set; } = new List(); + + /// + /// Enables the debug layer + /// + public bool UseDebug { get; set; } + + /// + /// Selects the first suitable discrete GPU available + /// + public bool PreferDiscreteGpu { get; set; } + + /// + /// Sets the device to use if available and suitable. + /// + public uint? PreferredDevice { get; set; } + + /// + /// Max number of device queues to request + /// + 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 000000000000..4f5cb50c9c2b --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; + +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); + + 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 Version ApiVersion { get; } + + internal static unsafe VulkanPhysicalDevice FindSuitablePhysicalDevice(VulkanInstance instance, + VulkanSurface surface, bool preferDiscreteGpu, uint? 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(); + } + + var physicalDeviceProperties = new Dictionary(); + + foreach (var physicalDevice in physicalDevices) + { + instance.Api.GetPhysicalDeviceProperties(physicalDevice, out var properties); + physicalDeviceProperties.Add(physicalDevice, properties); + } + + if (preferredDevice.HasValue && preferredDevice != 0) + { + var physicalDevice = physicalDeviceProperties.FirstOrDefault(x => x.Value.DeviceID == preferredDevice); + if (physicalDevice.Key.Handle != 0 && IsSuitableDevice(instance.Api, physicalDevice.Key, + physicalDevice.Value, surface.ApiHandle, out var queueCount, + out var queueFamilyIndex)) + return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex); + } + + if (preferDiscreteGpu) + { + var discreteGpus = physicalDeviceProperties.Where(p => p.Value.DeviceType == PhysicalDeviceType.DiscreteGpu); + + foreach (var gpu in discreteGpus) + { + if (IsSuitableDevice( + instance.Api, + gpu.Key, + gpu.Value, + surface.ApiHandle, + out var queueCount, + out var queueFamilyIndex)) + { + return new VulkanPhysicalDevice(gpu.Key, instance.Api, queueCount, queueFamilyIndex); + } + + physicalDeviceProperties.Remove(gpu.Key); + } + } + + foreach (var physicalDevice in physicalDeviceProperties) + if (IsSuitableDevice( + instance.Api, + physicalDevice.Key, + physicalDevice.Value, + surface.ApiHandle, + out var queueCount, + out var queueFamilyIndex)) + { + 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 unsafe 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 000000000000..77fedae0c2c7 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs @@ -0,0 +1,84 @@ +using System; +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Silk.NET.Vulkan; +using Ryujinx.Graphics.Vulkan; + +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 void Dispose() + { + Device?.Dispose(); + Instance?.Dispose(); + Api?.Dispose(); + } + + private static VulkanPlatformInterface TryCreate() + { + try + { + _options = AvaloniaLocator.Current.GetService() ?? new VulkanOptions(); + + var instance = VulkanInstance.Create(_options); + + return new VulkanPlatformInterface(instance); + } + catch (Exception ex) + { + return null; + } + } + + public static bool TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + return true; + } + + return false; + } + + public VulkanSurfaceRenderTarget CreateRenderTarget(IVulkanPlatformSurface platformSurface) + { + var surface = VulkanSurface.CreateSurface(Instance, platformSurface); + try + { + 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); + } + } + catch (Exception) + { + surface.Dispose(); + } + + return new VulkanSurfaceRenderTarget(this, surface); + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs new file mode 100644 index 000000000000..a903e21a6125 --- /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 000000000000..3b5fd9cc6b8c --- /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 000000000000..50b76c5ac31a --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs @@ -0,0 +1,77 @@ +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 unsafe void Dispose() + { + SurfaceExtension.DestroySurface(_instance.InternalHandle, ApiHandle, null); + _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 unsafe SurfaceFormatKHR GetSurfaceFormat(VulkanPhysicalDevice physicalDevice) + { + uint surfaceFormatsCount; + + SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, + &surfaceFormatsCount, null); + + var surfaceFormats = new SurfaceFormatKHR[surfaceFormatsCount]; + + fixed (SurfaceFormatKHR* pSurfaceFormats = surfaceFormats) + { + SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, + &surfaceFormatsCount, pSurfaceFormats); + } + + 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 000000000000..87267cd0bf0f --- /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; } + + public bool IsYFlipped { get; } = true; + + public void Dispose() + { + _commandBuffer = Display.StartPresentation(_renderTarget); + + Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle); + + Display.EndPresentation(_commandBuffer); + } + + private void Begin() + { + if (!Display.EnsureSwapchainAvailable()) + _renderTarget.Invalidate(); + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs new file mode 100644 index 000000000000..b25c0621b015 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs @@ -0,0 +1,192 @@ +using Avalonia; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using SkiaSharp; +using SPB.Graphics; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Windowing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class OpenGLRendererControl : RendererControl + { + public int Major { get; } + public int Minor { get; } + public OpenGLContextBase GameContext { get; set; } + + public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService().PrimaryContext.AsOpenGLContextBase(); + + private SwappableNativeWindowBase _gameBackgroundWindow; + + private IntPtr _fence; + + public OpenGLRendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) + { + Major = major; + Minor = minor; + } + + public override void DestroyBackgroundContext() + { + _image = null; + + if (_fence != IntPtr.Zero) + { + DrawOperation.Dispose(); + GL.DeleteSync(_fence); + } + + GlDrawOperation.DeleteFramebuffer(); + + GameContext?.Dispose(); + + _gameBackgroundWindow?.Dispose(); + } + + internal override void Present(object image) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Image = (int)image; + }).Wait(); + + if (_fence != IntPtr.Zero) + { + GL.DeleteSync(_fence); + } + + _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); + + QueueRender(); + + _gameBackgroundWindow.SwapBuffers(); + } + + internal override void MakeCurrent() + { + GameContext.MakeCurrent(_gameBackgroundWindow); + } + + internal override void MakeCurrent(SwappableNativeWindowBase window) + { + GameContext.MakeCurrent(window); + } + + protected override void CreateWindow() + { + var flags = OpenGLContextFlags.Compat; + if (DebugLevel != GraphicsDebugLevel.None) + { + flags |= OpenGLContextFlags.Debug; + } + _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); + _gameBackgroundWindow.Hide(); + + GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext); + GameContext.Initialize(_gameBackgroundWindow); + } + + protected override ICustomDrawOperation CreateDrawOperation() + { + return new GlDrawOperation(this); + } + + private class GlDrawOperation : ICustomDrawOperation + { + private static int _framebuffer; + + public Rect Bounds { get; } + + private readonly OpenGLRendererControl _control; + + public GlDrawOperation(OpenGLRendererControl control) + { + _control = control; + Bounds = _control.Bounds; + } + + public void Dispose() { } + + public static void DeleteFramebuffer() + { + if (_framebuffer == 0) + { + GL.DeleteFramebuffer(_framebuffer); + } + + _framebuffer = 0; + } + + public bool Equals(ICustomDrawOperation other) + { + return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; + } + + public bool HitTest(Point p) + { + return Bounds.Contains(p); + } + + private void CreateRenderTarget() + { + _framebuffer = GL.GenFramebuffer(); + } + + public void Render(IDrawingContextImpl context) + { + if (_control.Image == null) + { + return; + } + + if (_framebuffer == 0) + { + CreateRenderTarget(); + } + + int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding); + + var image = _control.Image; + var fence = _control._fence; + + GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); + GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, (int)image, 0); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer); + + if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + { + return; + } + + var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888); + var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat()); + + GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue); + + using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo); + using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); + + if (surface == null) + { + return; + } + + var rect = new Rect(new Point(), _control.RenderSize); + + using var snapshot = surface.Snapshot(); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs index f81d7e17e700..5658f60a247b 100644 --- a/Ryujinx.Ava/Ui/Controls/RendererControl.cs +++ b/Ryujinx.Ava/Ui/Controls/RendererControl.cs @@ -2,65 +2,45 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; -using Avalonia.OpenGL; -using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; -using Avalonia.Skia; -using Avalonia.Threading; -using OpenTK.Graphics.OpenGL; using Ryujinx.Common.Configuration; -using SkiaSharp; -using SPB.Graphics; -using SPB.Graphics.OpenGL; -using SPB.Platform; using SPB.Windowing; using System; namespace Ryujinx.Ava.Ui.Controls { - internal class RendererControl : Control + internal abstract class RendererControl : Control { - private int _image; + protected object _image; static RendererControl() { AffectsRender(ImageProperty); } - public readonly static StyledProperty ImageProperty = - AvaloniaProperty.Register(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay); + public readonly static StyledProperty ImageProperty = + AvaloniaProperty.Register(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay); - protected int Image + protected object Image { get => _image; set => SetAndRaise(ImageProperty, ref _image, value); } - public event EventHandler GlInitialized; + public event EventHandler RendererInitialized; public event EventHandler SizeChanged; protected Size RenderSize { get; private set; } public bool IsStarted { get; private set; } - public int Major { get; } - public int Minor { get; } public GraphicsDebugLevel DebugLevel { get; } - public OpenGLContextBase GameContext { get; set; } - - public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService().PrimaryContext.AsOpenGLContextBase(); - - private SwappableNativeWindowBase _gameBackgroundWindow; private bool _isInitialized; - private IntPtr _fence; - - private GlDrawOperation _glDrawOperation; + protected ICustomDrawOperation DrawOperation { get; private set; } - public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) + public RendererControl(GraphicsDebugLevel graphicsDebugLevel) { - Major = major; - Minor = minor; DebugLevel = graphicsDebugLevel; IObservable resizeObservable = this.GetObservable(BoundsProperty); @@ -69,7 +49,7 @@ public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLev Focusable = true; } - private void Resized(Rect rect) + protected void Resized(Rect rect) { SizeChanged?.Invoke(this, rect.Size); @@ -77,37 +57,40 @@ private void Resized(Rect rect) { RenderSize = rect.Size * VisualRoot.RenderScaling; - _glDrawOperation?.Dispose(); - _glDrawOperation = new GlDrawOperation(this); + DrawOperation?.Dispose(); + DrawOperation = CreateDrawOperation(); } } + protected abstract ICustomDrawOperation CreateDrawOperation(); + protected abstract void CreateWindow(); + public override void Render(DrawingContext context) { if (!_isInitialized) { CreateWindow(); - OnGlInitialized(); + OnRendererInitialized(); _isInitialized = true; } - if (GameContext == null || !IsStarted || Image == 0) + if (!IsStarted || Image == null) { return; } - if (_glDrawOperation != null) + if (DrawOperation != null) { - context.Custom(_glDrawOperation); + context.Custom(DrawOperation); } base.Render(context); } - protected void OnGlInitialized() + protected void OnRendererInitialized() { - GlInitialized?.Invoke(this, EventArgs.Empty); + RendererInitialized?.Invoke(this, EventArgs.Empty); } public void QueueRender() @@ -115,24 +98,7 @@ public void QueueRender() Program.RenderTimer.TickNow(); } - internal void Present(object image) - { - Dispatcher.UIThread.InvokeAsync(() => - { - Image = (int)image; - }).Wait(); - - if (_fence != IntPtr.Zero) - { - GL.DeleteSync(_fence); - } - - _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); - - QueueRender(); - - _gameBackgroundWindow.SwapBuffers(); - } + internal abstract void Present(object image); internal void Start() { @@ -145,132 +111,8 @@ internal void Stop() IsStarted = false; } - public void DestroyBackgroundContext() - { - _image = 0; - - if (_fence != IntPtr.Zero) - { - _glDrawOperation.Dispose(); - GL.DeleteSync(_fence); - } - - GlDrawOperation.DeleteFramebuffer(); - - GameContext?.Dispose(); - - _gameBackgroundWindow?.Dispose(); - } - - internal void MakeCurrent() - { - GameContext.MakeCurrent(_gameBackgroundWindow); - } - - internal void MakeCurrent(SwappableNativeWindowBase window) - { - GameContext.MakeCurrent(window); - } - - protected void CreateWindow() - { - var flags = OpenGLContextFlags.Compat; - if (DebugLevel != GraphicsDebugLevel.None) - { - flags |= OpenGLContextFlags.Debug; - } - _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); - _gameBackgroundWindow.Hide(); - - GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext); - GameContext.Initialize(_gameBackgroundWindow); - } - - private class GlDrawOperation : ICustomDrawOperation - { - private static int _framebuffer; - - public Rect Bounds { get; } - - private readonly RendererControl _control; - - public GlDrawOperation(RendererControl control) - { - _control = control; - Bounds = _control.Bounds; - } - - public void Dispose() { } - - public static void DeleteFramebuffer() - { - if (_framebuffer == 0) - { - GL.DeleteFramebuffer(_framebuffer); - } - - _framebuffer = 0; - } - - public bool Equals(ICustomDrawOperation other) - { - return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; - } - - public bool HitTest(Point p) - { - return Bounds.Contains(p); - } - - private void CreateRenderTarget() - { - _framebuffer = GL.GenFramebuffer(); - } - - public void Render(IDrawingContextImpl context) - { - if (_control.Image == 0) - { - return; - } - - if (_framebuffer == 0) - { - CreateRenderTarget(); - } - - int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding); - - var image = _control.Image; - var fence = _control._fence; - - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); - GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer); - - if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) - { - return; - } - - var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888); - var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat()); - - GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue); - - using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo); - using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); - - if (surface == null) - { - return; - } - - var rect = new Rect(new Point(), _control.RenderSize); - - using var snapshot = surface.Snapshot(); - skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); - } - } + public abstract void DestroyBackgroundContext(); + internal abstract void MakeCurrent(); + internal abstract void MakeCurrent(SwappableNativeWindowBase window); } -} +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs new file mode 100644 index 000000000000..fdbd8df978a6 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs @@ -0,0 +1,153 @@ +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using Ryujinx.Ava.Ui.Backend.Vulkan; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Common.Configuration; +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Vulkan; +using SkiaSharp; +using SPB.Windowing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class VulkanRendererControl : RendererControl + { + private VulkanPlatformInterface _platformInterface; + + public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) + { + _platformInterface = AvaloniaLocator.Current.GetService(); + } + + public override void DestroyBackgroundContext() + { + + } + + protected override ICustomDrawOperation CreateDrawOperation() + { + return new VulkanDrawOperation(this); + } + + protected override void CreateWindow() + { + } + + internal override void MakeCurrent() + { + } + + internal override void MakeCurrent(SwappableNativeWindowBase window) + { + } + + internal override void Present(object image) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Image = image; + }).Wait(); + + QueueRender(); + } + + private class VulkanDrawOperation : ICustomDrawOperation + { + public Rect Bounds { get; } + + private readonly VulkanRendererControl _control; + + public VulkanDrawOperation(VulkanRendererControl control) + { + _control = control; + Bounds = _control.Bounds; + } + + public void Dispose() + { + + } + + public bool Equals(ICustomDrawOperation other) + { + return other is VulkanDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; + } + + public bool HitTest(Point p) + { + return Bounds.Contains(p); + } + + public void Render(IDrawingContextImpl context) + { + if (_control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0) + { + return; + } + + var image = (PresentImageInfo)_control.Image; + + if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + { + return; + } + + _control._platformInterface.Device.QueueWaitIdle(); + + var gpu = AvaloniaLocator.Current.GetService(); + + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex, + Format = (uint)Format.R8G8B8A8Unorm, + Image = image.Image.Handle, + ImageLayout = (uint)ImageLayout.ColorAttachmentOptimal, + ImageTiling = (uint)ImageTiling.Optimal, + ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit + | ImageUsageFlags.ImageUsageTransferSrcBit + | ImageUsageFlags.ImageUsageTransferDstBit), + LevelCount = 1, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = image.Memory.Handle, + Flags = 0, + Offset = image.MemoryOffset, + Size = image.MemorySize + } + }; + + using var backendTexture = new GRBackendRenderTarget( + (int)_control.RenderSize.Width, + (int)_control.RenderSize.Height, + 1, + imageInfo); + + using var surface = SKSurface.Create( + gpu.GrContext, + backendTexture, + GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888); + + if (surface == null) + { + return; + } + + var rect = new Rect(new Point(), _control.RenderSize); + + using var snapshot = surface.Snapshot(); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs index 2b5c10e206a4..b3e19a02a900 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs @@ -61,7 +61,7 @@ public class MainWindow : StyleableWindow internal AppHost AppHost { get; private set; } public InputManager InputManager { get; private set; } - internal RendererControl GlRenderer { get; private set; } + internal RendererControl RendererControl { get; private set; } public ContentControl ContentFrame { get; private set; } public TextBlock LoadStatus { get; private set; } public TextBlock FirmwareStatus { get; private set; } @@ -256,8 +256,8 @@ await ContentDialogHelper.CreateInfoDialog(this, _mainViewContent = ContentFrame.Content as Control; - GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel); - AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); + RendererControl = Program.UseVulkan ? new VulkanRendererControl(ConfigurationState.Instance.Logger.GraphicsDebugLevel) : new OpenGLRendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel); + AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); if (!AppHost.LoadGuestApplication().Result) { @@ -281,7 +281,7 @@ await ContentDialogHelper.CreateInfoDialog(this, private void InitializeGame() { - GlRenderer.GlInitialized += GlRenderer_Created; + RendererControl.RendererInitialized += GlRenderer_Created; AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.AppExit += AppHost_AppExit; @@ -321,14 +321,14 @@ public void SwitchToGameControl(bool startFullscreen = false) Dispatcher.UIThread.InvokeAsync(() => { - ContentFrame.Content = GlRenderer; + ContentFrame.Content = RendererControl; if (startFullscreen && WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } - GlRenderer.Focus(); + RendererControl.Focus(); }); } @@ -380,8 +380,8 @@ private void AppHost_AppExit(object sender, EventArgs e) HandleRelaunch(); }); - GlRenderer.GlInitialized -= GlRenderer_Created; - GlRenderer = null; + RendererControl.RendererInitialized -= GlRenderer_Created; + RendererControl = null; ViewModel.SelectedIcon = null; diff --git a/Ryujinx.Common/System/ForceDpiAware.cs b/Ryujinx.Common/System/ForceDpiAware.cs index f29630a62810..f1aa3e8e81f5 100644 --- a/Ryujinx.Common/System/ForceDpiAware.cs +++ b/Ryujinx.Common/System/ForceDpiAware.cs @@ -44,7 +44,7 @@ public static void Windows() } } - public static double GetWindowScaleFactor() + public static double GetActualScaleFactor() { double userDpiScale = 96.0; @@ -84,6 +84,13 @@ public static double GetWindowScaleFactor() Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}"); } + return userDpiScale; + } + + public static double GetWindowScaleFactor() + { + double userDpiScale = GetActualScaleFactor(); + return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor); } } diff --git a/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs index e7faf474c757..fb3db8e6da40 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs @@ -76,7 +76,7 @@ public ThreadedRenderer(IRenderer renderer) renderer.ScreenCaptured += (object sender, ScreenCaptureImageInfo info) => ScreenCaptured?.Invoke(this, info); Pipeline = new ThreadedPipeline(this, renderer.Pipeline); - Window = new ThreadedWindow(this, renderer.Window); + Window = new ThreadedWindow(this, renderer); Buffers = new BufferMap(); Sync = new SyncMap(); Programs = new ProgramQueue(renderer); diff --git a/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index dc0b4dc5e357..c8045502835f 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -8,12 +8,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading public class ThreadedWindow : IWindow { private ThreadedRenderer _renderer; - private IWindow _impl; + private IRenderer _impl; - public ThreadedWindow(ThreadedRenderer renderer, IWindow window) + public ThreadedWindow(ThreadedRenderer renderer, IRenderer impl) { _renderer = renderer; - _impl = window; + _impl = impl; } public void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) @@ -28,7 +28,7 @@ public void Present(ITexture texture, ImageCrop crop, Action swapBuffers public void SetSize(int width, int height) { - _impl.SetSize(width, height); + _impl.Window.SetSize(width, height); } } } diff --git a/Ryujinx.Graphics.Vulkan/ImageWindow.cs b/Ryujinx.Graphics.Vulkan/ImageWindow.cs new file mode 100644 index 000000000000..b4d893dfbc6b --- /dev/null +++ b/Ryujinx.Graphics.Vulkan/ImageWindow.cs @@ -0,0 +1,365 @@ +using Ryujinx.Graphics.GAL; +using Silk.NET.Vulkan; +using System; +using VkFormat = Silk.NET.Vulkan.Format; + +namespace Ryujinx.Graphics.Vulkan +{ + class ImageWindow : WindowBase, IWindow, IDisposable + { + private const int ImageCount = 5; + private const int SurfaceWidth = 1280; + private const int SurfaceHeight = 720; + + private readonly VulkanGraphicsDevice _gd; + private readonly PhysicalDevice _physicalDevice; + private readonly Device _device; + + private Auto[] _images; + private Auto[] _imageViews; + private Auto[] _imageAllocationAuto; + private ulong[] _imageSizes; + private ulong[] _imageOffsets; + + private Semaphore _imageAvailableSemaphore; + private Semaphore _renderFinishedSemaphore; + + private int _width = SurfaceWidth; + private int _height = SurfaceHeight; + private VkFormat _format; + private bool _recreateImages; + private int _nextImage; + + internal new bool ScreenCaptureRequested { get; set; } + + public unsafe ImageWindow(VulkanGraphicsDevice gd, PhysicalDevice physicalDevice, Device device) + { + _gd = gd; + _physicalDevice = physicalDevice; + _device = device; + + _format = VkFormat.R8G8B8A8Unorm; + + _images = new Auto[ImageCount]; + _imageAllocationAuto = new Auto[ImageCount]; + _imageSizes = new ulong[ImageCount]; + _imageOffsets = new ulong[ImageCount]; + + CreateImages(); + + var semaphoreCreateInfo = new SemaphoreCreateInfo() + { + SType = StructureType.SemaphoreCreateInfo + }; + + gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _imageAvailableSemaphore).ThrowOnError(); + gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _renderFinishedSemaphore).ThrowOnError(); + } + + private void RecreateImages() + { + for (int i = 0; i < ImageCount; i++) + { + _imageViews[i]?.Dispose(); + _imageAllocationAuto[i]?.Dispose(); + _images[i]?.Dispose(); + } + + CreateImages(); + } + + private void CreateImages() + { + _imageViews = new Auto[ImageCount]; + unsafe + { + var cbs = _gd.CommandBufferPool.Rent(); + for (int i = 0; i < _images.Length; i++) + { + var imageCreateInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = _format, + Extent = + new Extent3D((uint?)_width, + (uint?)_height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = ImageTiling.Optimal, + Usage = ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageTransferDstBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + _gd.Api.CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError(); + _images[i] = new Auto(new DisposableImage(_gd.Api, _device, image)); + + _gd.Api.GetImageMemoryRequirements(_device, image, + out var memoryRequirements); + + var allocation = _gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, memoryRequirements, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit); + + _imageSizes[i] = allocation.Size; + _imageOffsets[i] = allocation.Offset; + + _imageAllocationAuto[i] = new Auto(allocation); + + _gd.Api.BindImageMemory(_device, image, allocation.Memory, allocation.Offset); + + _imageViews[i] = CreateImageView(image, _format); + + Transition( + cbs.CommandBuffer, + image, + 0, + 0, + ImageLayout.Undefined, + ImageLayout.ColorAttachmentOptimal); + } + + _gd.CommandBufferPool.Return(cbs); + } + } + + private unsafe Auto CreateImageView(Image image, VkFormat format) + { + var componentMapping = new ComponentMapping( + ComponentSwizzle.R, + ComponentSwizzle.G, + ComponentSwizzle.B, + ComponentSwizzle.A); + + var aspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(aspectFlags, 0, 1, 0, 1); + + var imageCreateInfo = new ImageViewCreateInfo() + { + SType = StructureType.ImageViewCreateInfo, + Image = image, + ViewType = ImageViewType.ImageViewType2D, + Format = format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + _gd.Api.CreateImageView(_device, imageCreateInfo, null, out var imageView).ThrowOnError(); + return new Auto(new DisposableImageView(_gd.Api, _device, imageView)); + } + + public override unsafe void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) + { + if (_recreateImages) + { + RecreateImages(); + _recreateImages = false; + } + + var image = _images[_nextImage]; + + _gd.FlushAllCommands(); + + var cbs = _gd.CommandBufferPool.Rent(); + + Transition( + cbs.CommandBuffer, + image.GetUnsafe().Value, + 0, + AccessFlags.AccessTransferWriteBit, + ImageLayout.ColorAttachmentOptimal, + ImageLayout.General); + + var view = (TextureView)texture; + + int srcX0, srcX1, srcY0, srcY1; + float scale = view.ScaleFactor; + + if (crop.Left == 0 && crop.Right == 0) + { + srcX0 = 0; + srcX1 = (int)(view.Width / scale); + } + else + { + srcX0 = crop.Left; + srcX1 = crop.Right; + } + + if (crop.Top == 0 && crop.Bottom == 0) + { + srcY0 = 0; + srcY1 = (int)(view.Height / scale); + } + else + { + srcY0 = crop.Top; + srcY1 = crop.Bottom; + } + + if (scale != 1f) + { + srcX0 = (int)(srcX0 * scale); + srcY0 = (int)(srcY0 * scale); + srcX1 = (int)Math.Ceiling(srcX1 * scale); + srcY1 = (int)Math.Ceiling(srcY1 * scale); + } + + if (ScreenCaptureRequested) + { + CaptureFrame(view, srcX0, srcY0, srcX1 - srcX0, srcY1 - srcY0, view.Info.Format.IsBgr(), crop.FlipX, crop.FlipY); + + ScreenCaptureRequested = false; + } + + float ratioX = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _height * crop.AspectRatioX / (_width * crop.AspectRatioY)); + float ratioY = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _width * crop.AspectRatioY / (_height * crop.AspectRatioX)); + + int dstWidth = (int)(_width * ratioX); + int dstHeight = (int)(_height * ratioY); + + int dstPaddingX = (_width - dstWidth) / 2; + int dstPaddingY = (_height - dstHeight) / 2; + + int dstX0 = crop.FlipX ? _width - dstPaddingX : dstPaddingX; + int dstX1 = crop.FlipX ? dstPaddingX : _width - dstPaddingX; + + int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY; + int dstY1 = crop.FlipY ? _height - dstPaddingY : dstPaddingY; + + _gd.HelperShader.Blit( + _gd, + cbs, + view, + _imageViews[_nextImage], + _width, + _height, + _format, + new Extents2D(srcX0, srcY0, srcX1, srcY1), + new Extents2D(dstX0, dstY1, dstX1, dstY0), + true, + true); + + Transition( + cbs.CommandBuffer, + image.GetUnsafe().Value, + 0, + 0, + ImageLayout.General, + ImageLayout.ColorAttachmentOptimal); + + _gd.CommandBufferPool.Return( + cbs, + null, + new[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit }, + null); + + var memory = _imageAllocationAuto[_nextImage].GetUnsafe().Memory; + var presentInfo = new PresentImageInfo(image.GetUnsafe().Value, memory, _imageSizes[_nextImage], _imageOffsets[_nextImage], _renderFinishedSemaphore, _imageAvailableSemaphore); + + swapBuffersCallback(presentInfo); + + _nextImage %= ImageCount; + } + + private unsafe void Transition( + CommandBuffer commandBuffer, + Image image, + AccessFlags srcAccess, + AccessFlags dstAccess, + ImageLayout srcLayout, + ImageLayout dstLayout) + { + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, 1, 0, 1); + + var barrier = new ImageMemoryBarrier() + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = srcAccess, + DstAccessMask = dstAccess, + OldLayout = srcLayout, + NewLayout = dstLayout, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + SubresourceRange = subresourceRange + }; + + _gd.Api.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.PipelineStageTopOfPipeBit, + PipelineStageFlags.PipelineStageAllCommandsBit, + 0, + 0, + null, + 0, + null, + 1, + barrier); + } + + private void CaptureFrame(TextureView texture, int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY) + { + byte[] bitmap = texture.GetData(x, y, width, height); + + _gd.OnScreenCaptured(new ScreenCaptureImageInfo(width, height, isBgra, bitmap, flipX, flipY)); + } + + public override void SetSize(int width, int height) + { + if (_width != width || _height != height) + { + _recreateImages = true; + } + + _width = width; + _height = height; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + unsafe + { + _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphore, null); + _gd.Api.DestroySemaphore(_device, _imageAvailableSemaphore, null); + + for (int i = 0; i < ImageCount; i++) + { + _imageViews[i]?.Dispose(); + _imageAllocationAuto[i]?.Dispose(); + _images[i]?.Dispose(); + } + } + } + } + + public override void Dispose() + { + Dispose(true); + } + } + + public class PresentImageInfo + { + public Image Image { get; } + public DeviceMemory Memory { get; } + public ulong MemorySize { get; set; } + public ulong MemoryOffset { get; set; } + public Semaphore ReadySemaphore { get; } + public Semaphore AvailableSemaphore { get; } + + public PresentImageInfo(Image image, DeviceMemory memory, ulong memorySize, ulong memoryOffset, Semaphore readySemaphore, Semaphore availableSemaphore) + { + this.Image = image; + this.Memory = memory; + this.MemorySize = memorySize; + this.MemoryOffset = memoryOffset; + this.ReadySemaphore = readySemaphore; + this.AvailableSemaphore = availableSemaphore; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Graphics.Vulkan/VulkanGraphicsDevice.cs b/Ryujinx.Graphics.Vulkan/VulkanGraphicsDevice.cs index 42be3cdaaf09..dea627726e8f 100644 --- a/Ryujinx.Graphics.Vulkan/VulkanGraphicsDevice.cs +++ b/Ryujinx.Graphics.Vulkan/VulkanGraphicsDevice.cs @@ -20,7 +20,8 @@ public sealed class VulkanGraphicsDevice : IRenderer private SurfaceKHR _surface; private PhysicalDevice _physicalDevice; private Device _device; - private Window _window; + private uint _queueFamilyIndex; + private WindowBase _window; internal FormatCapabilities FormatCapabilities { get; private set; } internal HardwareCapabilities Capabilities; @@ -42,7 +43,7 @@ public sealed class VulkanGraphicsDevice : IRenderer internal bool SupportsSubgroupSizeControl { get; private set; } internal uint QueueFamilyIndex { get; private set; } - + public bool IsOffScreen { get; } internal Queue Queue { get; private set; } internal Queue BackgroundQueue { get; private set; } internal object BackgroundQueueLock { get; private set; } @@ -96,32 +97,27 @@ public VulkanGraphicsDevice(Func surfaceFunc, Func(); } - private unsafe void SetupContext(GraphicsDebugLevel logLevel) + public VulkanGraphicsDevice(Instance instance, Device device, PhysicalDevice physicalDevice, Queue queue, uint queueFamilyIndex, object lockObject) { - var api = Vk.GetApi(); - - Api = api; - - _instance = VulkanInitialization.CreateInstance(api, logLevel, GetRequiredExtensions(), out ExtDebugReport debugReport, out _debugReportCallback); - - DebugReportApi = debugReport; - - if (api.TryGetInstanceExtension(_instance, out KhrSurface surfaceApi)) - { - SurfaceApi = surfaceApi; - } - - _surface = GetSurface(_instance, api); - _physicalDevice = VulkanInitialization.FindSuitablePhysicalDevice(api, _instance, _surface); + _instance = instance; + _physicalDevice = physicalDevice; + _device = device; + _queueFamilyIndex = queueFamilyIndex; - FormatCapabilities = new FormatCapabilities(api, _physicalDevice); + Queue = queue; + QueueLock = lockObject; - var queueFamilyIndex = VulkanInitialization.FindSuitableQueueFamily(api, _physicalDevice, _surface, out uint maxQueueCount); - var supportedExtensions = VulkanInitialization.GetSupportedExtensions(api, _physicalDevice); - var supportedFeatures = api.GetPhysicalDeviceFeature(_physicalDevice); + IsOffScreen = true; + Shaders = new HashSet(); + Textures = new HashSet(); + Samplers = new HashSet(); + } - _device = VulkanInitialization.CreateDevice(api, _physicalDevice, queueFamilyIndex, supportedExtensions, maxQueueCount); + private unsafe void LoadFeatures(string[] supportedExtensions, uint maxQueueCount, uint queueFamilyIndex) + { + FormatCapabilities = new FormatCapabilities(Api, _physicalDevice); + var supportedFeatures = Api.GetPhysicalDeviceFeature(_physicalDevice); SupportsIndexTypeUint8 = supportedExtensions.Contains("VK_EXT_index_type_uint8"); SupportsCustomBorderColor = supportedExtensions.Contains("VK_EXT_custom_border_color"); SupportsIndirectParameters = supportedExtensions.Contains(KhrDrawIndirectCount.ExtensionName); @@ -129,38 +125,29 @@ private unsafe void SetupContext(GraphicsDebugLevel logLevel) SupportsGeometryShaderPassthrough = supportedExtensions.Contains("VK_NV_geometry_shader_passthrough"); SupportsSubgroupSizeControl = supportedExtensions.Contains("VK_EXT_subgroup_size_control"); - if (api.TryGetDeviceExtension(_instance, _device, out KhrSwapchain swapchainApi)) - { - SwapchainApi = swapchainApi; - } - - if (api.TryGetDeviceExtension(_instance, _device, out ExtConditionalRendering conditionalRenderingApi)) + if (Api.TryGetDeviceExtension(_instance, _device, out ExtConditionalRendering conditionalRenderingApi)) { ConditionalRenderingApi = conditionalRenderingApi; } - if (api.TryGetDeviceExtension(_instance, _device, out ExtExtendedDynamicState extendedDynamicStateApi)) + if (Api.TryGetDeviceExtension(_instance, _device, out ExtExtendedDynamicState extendedDynamicStateApi)) { ExtendedDynamicStateApi = extendedDynamicStateApi; } - if (api.TryGetDeviceExtension(_instance, _device, out ExtTransformFeedback transformFeedbackApi)) + if (Api.TryGetDeviceExtension(_instance, _device, out ExtTransformFeedback transformFeedbackApi)) { TransformFeedbackApi = transformFeedbackApi; } - if (api.TryGetDeviceExtension(_instance, _device, out KhrDrawIndirectCount drawIndirectCountApi)) + if (Api.TryGetDeviceExtension(_instance, _device, out KhrDrawIndirectCount drawIndirectCountApi)) { DrawIndirectCountApi = drawIndirectCountApi; } - api.GetDeviceQueue(_device, queueFamilyIndex, 0, out var queue); - Queue = queue; - QueueLock = new object(); - if (maxQueueCount >= 2) { - api.GetDeviceQueue(_device, queueFamilyIndex, 1, out var backgroundQueue); + Api.GetDeviceQueue(_device, queueFamilyIndex, 1, out var backgroundQueue); BackgroundQueue = backgroundQueue; BackgroundQueueLock = new object(); } @@ -226,9 +213,9 @@ private unsafe void SetupContext(GraphicsDebugLevel logLevel) ref var properties = ref properties2.Properties; - MemoryAllocator = new MemoryAllocator(api, _device, properties.Limits.MaxMemoryAllocationCount); + MemoryAllocator = new MemoryAllocator(Api, _device, properties.Limits.MaxMemoryAllocationCount); - CommandBufferPool = VulkanInitialization.CreateCommandBufferPool(api, _device, queue, QueueLock, queueFamilyIndex); + CommandBufferPool = VulkanInitialization.CreateCommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex); DescriptorSetManager = new DescriptorSetManager(_device); @@ -244,10 +231,73 @@ private unsafe void SetupContext(GraphicsDebugLevel logLevel) HelperShader = new HelperShader(this, _device); _counters = new Counters(this, _device, _pipeline); + } + + private unsafe void SetupContext(GraphicsDebugLevel logLevel) + { + var api = Vk.GetApi(); + + Api = api; + + _instance = VulkanInitialization.CreateInstance(api, logLevel, GetRequiredExtensions(), out ExtDebugReport debugReport, out _debugReportCallback); + + DebugReportApi = debugReport; + + if (api.TryGetInstanceExtension(_instance, out KhrSurface surfaceApi)) + { + SurfaceApi = surfaceApi; + } + + _surface = GetSurface(_instance, api); + _physicalDevice = VulkanInitialization.FindSuitablePhysicalDevice(api, _instance, _surface); + + var queueFamilyIndex = VulkanInitialization.FindSuitableQueueFamily(api, _physicalDevice, _surface, out uint maxQueueCount); + var supportedExtensions = VulkanInitialization.GetSupportedExtensions(api, _physicalDevice); + + _device = VulkanInitialization.CreateDevice(api, _physicalDevice, queueFamilyIndex, supportedExtensions, maxQueueCount); + + if (api.TryGetDeviceExtension(_instance, _device, out KhrSwapchain swapchainApi)) + { + SwapchainApi = swapchainApi; + } + + api.GetDeviceQueue(_device, queueFamilyIndex, 0, out var queue); + Queue = queue; + QueueLock = new object(); + + LoadFeatures(supportedExtensions, maxQueueCount, queueFamilyIndex); _window = new Window(this, _surface, _physicalDevice, _device); } + private unsafe void SetupOffScreenContext(GraphicsDebugLevel logLevel) + { + var api = Vk.GetApi(); + + Api = api; + + VulkanInitialization.CreateDebugCallbacks(api, logLevel, _instance, out var debugReport, out _debugReportCallback); + + DebugReportApi = debugReport; + + var supportedExtensions = VulkanInitialization.GetSupportedExtensions(api, _physicalDevice); + + uint propertiesCount; + + api.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &propertiesCount, null); + + QueueFamilyProperties[] queueFamilyProperties = new QueueFamilyProperties[propertiesCount]; + + fixed (QueueFamilyProperties* pProperties = queueFamilyProperties) + { + api.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &propertiesCount, pProperties); + } + + LoadFeatures(supportedExtensions, queueFamilyProperties[0].QueueCount, _queueFamilyIndex); + + _window = new ImageWindow(this, _physicalDevice, _device); + } + public IShader CompileShader(ShaderStage stage, ShaderBindings bindings, string code) { return new Shader(Api, _device, stage, bindings, code); @@ -465,7 +515,14 @@ private unsafe void PrintGpuInformation() public void Initialize(GraphicsDebugLevel logLevel) { - SetupContext(logLevel); + if (IsOffScreen) + { + SetupOffScreenContext(logLevel); + } + else + { + SetupContext(logLevel); + } PrintGpuInformation(); } @@ -507,8 +564,6 @@ public unsafe void Dispose() DescriptorSetManager.Dispose(); PipelineLayoutCache.Dispose(); - SurfaceApi.DestroySurface(_instance, _surface, null); - MemoryAllocator.Dispose(); if (_debugReportCallback.Handle != 0) @@ -531,10 +586,15 @@ public unsafe void Dispose() sampler.Dispose(); } - Api.DestroyDevice(_device, null); + if (!IsOffScreen) + { + SurfaceApi.DestroySurface(_instance, _surface, null); + + Api.DestroyDevice(_device, null); - // Last step destroy the instance - Api.DestroyInstance(_instance, null); + // Last step destroy the instance + Api.DestroyInstance(_instance, null); + } } public void BackgroundContextAction(Action action, bool alwaysBackground = false) diff --git a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index fa5d8e94c4d7..b8bb69843f0a 100644 --- a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -10,20 +10,13 @@ namespace Ryujinx.Graphics.Vulkan { - unsafe static class VulkanInitialization + public unsafe static class VulkanInitialization { private const uint InvalidIndex = uint.MaxValue; private const string AppName = "Ryujinx.Graphics.Vulkan"; private const int QueuesCount = 2; - private static readonly string[] _requiredExtensions = new string[] - { - KhrSwapchain.ExtensionName, - "VK_EXT_shader_subgroup_vote", - ExtTransformFeedback.ExtensionName - }; - - private static readonly string[] _desirableExtensions = new string[] + public static string[] DesirableExtensions { get; } = new string[] { ExtConditionalRendering.ExtensionName, ExtExtendedDynamicState.ExtensionName, @@ -37,7 +30,14 @@ unsafe static class VulkanInitialization "VK_NV_geometry_shader_passthrough" }; - private static readonly string[] _excludedMessages = new string[] + public static string[] RequiredExtensions { get; } = new string[] + { + KhrSwapchain.ExtensionName, + "VK_EXT_shader_subgroup_vote", + ExtTransformFeedback.ExtensionName + }; + + private static string[] _excludedMessages = new string[] { // NOTE: Done on purpuse right now. "UNASSIGNED-CoreValidation-Shader-OutputNotConsumed", @@ -49,7 +49,7 @@ unsafe static class VulkanInitialization "VUID-VkSubpassDependency-srcSubpass-00867" }; - public static Instance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions, out ExtDebugReport debugReport, out DebugReportCallbackEXT debugReportCallback) + internal static Instance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions, out ExtDebugReport debugReport, out DebugReportCallbackEXT debugReportCallback) { var enabledLayers = new List(); @@ -135,38 +135,7 @@ void AddAvailableLayer(string layerName) Marshal.FreeHGlobal(ppEnabledLayers[i]); } - if (!api.TryGetInstanceExtension(instance, out debugReport)) - { - throw new Exception(); - // TODO: Exception. - } - - if (logLevel != GraphicsDebugLevel.None) - { - var flags = logLevel switch - { - GraphicsDebugLevel.Error => DebugReportFlagsEXT.DebugReportErrorBitExt, - GraphicsDebugLevel.Slowdowns => DebugReportFlagsEXT.DebugReportErrorBitExt | DebugReportFlagsEXT.DebugReportPerformanceWarningBitExt, - GraphicsDebugLevel.All => DebugReportFlagsEXT.DebugReportInformationBitExt | - DebugReportFlagsEXT.DebugReportWarningBitExt | - DebugReportFlagsEXT.DebugReportPerformanceWarningBitExt | - DebugReportFlagsEXT.DebugReportErrorBitExt | - DebugReportFlagsEXT.DebugReportDebugBitExt, - _ => throw new NotSupportedException() - }; - var debugReportCallbackCreateInfo = new DebugReportCallbackCreateInfoEXT() - { - SType = StructureType.DebugReportCallbackCreateInfoExt, - Flags = flags, - PfnCallback = new PfnDebugReportCallbackEXT(DebugReport) - }; - - debugReport.CreateDebugReportCallback(instance, in debugReportCallbackCreateInfo, null, out debugReportCallback).ThrowOnError(); - } - else - { - debugReportCallback = default; - } + CreateDebugCallbacks(api, logLevel, instance, out debugReport, out debugReportCallback); return instance; } @@ -218,7 +187,7 @@ private unsafe static uint DebugReport( return 0; } - public static PhysicalDevice FindSuitablePhysicalDevice(Vk api, Instance instance, SurfaceKHR surface) + internal static PhysicalDevice FindSuitablePhysicalDevice(Vk api, Instance instance, SurfaceKHR surface) { uint physicalDeviceCount; @@ -264,17 +233,17 @@ private static bool IsSuitableDevice(Vk api, PhysicalDevice physicalDevice, Surf { string extensionName = Marshal.PtrToStringAnsi((IntPtr)pExtensionProperties[i].ExtensionName); - if (_requiredExtensions.Contains(extensionName)) + if (RequiredExtensions.Contains(extensionName)) { extensionMatches++; } } } - return extensionMatches == _requiredExtensions.Length && FindSuitableQueueFamily(api, physicalDevice, surface, out _) != InvalidIndex; + return extensionMatches == RequiredExtensions.Length && FindSuitableQueueFamily(api, physicalDevice, surface, out _) != InvalidIndex; } - public static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface, out uint queueCount) + internal static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface, out uint queueCount) { const QueueFlags RequiredFlags = QueueFlags.QueueGraphicsBit | QueueFlags.QueueComputeBit; @@ -443,7 +412,7 @@ public static Device CreateDevice(Vk api, PhysicalDevice physicalDevice, uint qu pExtendedFeatures = &featuresSubgroupSizeControl; } - var enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(supportedExtensions)).ToArray(); + var enabledExtensions = RequiredExtensions.Union(DesirableExtensions.Intersect(supportedExtensions)).ToArray(); IntPtr* ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Length]; @@ -489,9 +458,47 @@ public static string[] GetSupportedExtensions(Vk api, PhysicalDevice physicalDev return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToArray(); } - public static CommandBufferPool CreateCommandBufferPool(Vk api, Device device, Queue queue, object queueLock, uint queueFamilyIndex) + internal static CommandBufferPool CreateCommandBufferPool(Vk api, Device device, Queue queue, object queueLock, uint queueFamilyIndex) { return new CommandBufferPool(api, device, queue, queueLock, queueFamilyIndex); } + + internal unsafe static void CreateDebugCallbacks(Vk api, GraphicsDebugLevel logLevel, Instance instance, out ExtDebugReport debugReport, out DebugReportCallbackEXT debugReportCallback) + { + debugReport = default; + + if (logLevel != GraphicsDebugLevel.None) + { + if (!api.TryGetInstanceExtension(instance, out debugReport)) + { + throw new Exception(); + // TODO: Exception. + } + + var flags = logLevel switch + { + GraphicsDebugLevel.Error => DebugReportFlagsEXT.DebugReportErrorBitExt, + GraphicsDebugLevel.Slowdowns => DebugReportFlagsEXT.DebugReportErrorBitExt | DebugReportFlagsEXT.DebugReportPerformanceWarningBitExt, + GraphicsDebugLevel.All => DebugReportFlagsEXT.DebugReportInformationBitExt | + DebugReportFlagsEXT.DebugReportWarningBitExt | + DebugReportFlagsEXT.DebugReportPerformanceWarningBitExt | + DebugReportFlagsEXT.DebugReportErrorBitExt | + DebugReportFlagsEXT.DebugReportDebugBitExt, + _ => throw new NotSupportedException() + }; + var debugReportCallbackCreateInfo = new DebugReportCallbackCreateInfoEXT() + { + SType = StructureType.DebugReportCallbackCreateInfoExt, + Flags = flags, + PfnCallback = new PfnDebugReportCallbackEXT(DebugReport) + }; + + debugReport.CreateDebugReportCallback(instance, in debugReportCallbackCreateInfo, null, out debugReportCallback).ThrowOnError(); + } + else + { + debugReportCallback = default; + } + } } } diff --git a/Ryujinx.Graphics.Vulkan/Window.cs b/Ryujinx.Graphics.Vulkan/Window.cs index 3e0a267b4c64..88d556819075 100644 --- a/Ryujinx.Graphics.Vulkan/Window.cs +++ b/Ryujinx.Graphics.Vulkan/Window.cs @@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan { - class Window : IWindow, IDisposable + class Window : WindowBase, IDisposable { private const int SurfaceWidth = 1280; private const int SurfaceHeight = 720; @@ -27,8 +27,6 @@ class Window : IWindow, IDisposable private int _height; private VkFormat _format; - internal bool ScreenCaptureRequested { get; set; } - public unsafe Window(VulkanGraphicsDevice gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device) { _gd = gd; @@ -211,7 +209,7 @@ public static Extent2D ChooseSwapExtent(SurfaceCapabilitiesKHR capabilities) } } - public unsafe void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) + public unsafe override void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) { uint nextImage = 0; @@ -401,7 +399,7 @@ private void CaptureFrame(TextureView texture, int x, int y, int width, int heig _gd.OnScreenCaptured(new ScreenCaptureImageInfo(width, height, isBgra, bitmap, flipX, flipY)); } - public void SetSize(int width, int height) + public override void SetSize(int width, int height) { // Not needed as we can get the size from the surface. } @@ -426,7 +424,7 @@ protected virtual void Dispose(bool disposing) } } - public void Dispose() + public override void Dispose() { Dispose(true); } diff --git a/Ryujinx.Graphics.Vulkan/WindowBase.cs b/Ryujinx.Graphics.Vulkan/WindowBase.cs new file mode 100644 index 000000000000..4f1f0d16539d --- /dev/null +++ b/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -0,0 +1,14 @@ +using Ryujinx.Graphics.GAL; +using System; + +namespace Ryujinx.Graphics.Vulkan +{ + internal abstract class WindowBase: IWindow + { + public bool ScreenCaptureRequested { get; set; } + + public abstract void Dispose(); + public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); + public abstract void SetSize(int width, int height); + } +} \ No newline at end of file