diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs index bd9c808eb..7e3cddc88 100644 --- a/Ryujinx.Ava/AppHost.cs +++ b/Ryujinx.Ava/AppHost.cs @@ -12,6 +12,7 @@ using Ryujinx.Audio.Integration; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; +using Ryujinx.Ava.Ui.Backend.Vulkan; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.Vulkan; @@ -334,6 +335,8 @@ namespace Ryujinx.Ava return; } + AvaloniaLocator.Current.GetService()?.MainSurface.Display.ChangeVSyncMode(true); + _isStopped = true; _isActive = false; } @@ -596,12 +599,13 @@ namespace Ryujinx.Ava if (Program.UseVulkan) { var vulkan = AvaloniaLocator.Current.GetService(); + renderer = new VulkanRenderer(vulkan.Instance.InternalHandle, - vulkan.Device.InternalHandle, + vulkan.MainSurface.Device.InternalHandle, vulkan.PhysicalDevice.InternalHandle, - vulkan.Device.Queue.InternalHandle, + vulkan.MainSurface.Device.Queue.InternalHandle, vulkan.PhysicalDevice.QueueFamilyIndex, - vulkan.Device.Lock); + vulkan.MainSurface.Device.Lock); } else { @@ -775,7 +779,10 @@ namespace Ryujinx.Ava Width = (int)e.Width; Height = (int)e.Height; - SetRendererWindowSize(e); + if (!Program.UseVulkan) + { + SetRendererWindowSize(e); + } } private void MainLoop() @@ -815,12 +822,11 @@ namespace Ryujinx.Ava _renderer.ScreenCaptured += Renderer_ScreenCaptured; - if (!Program.UseVulkan) - { - (_renderer as OpenGLRenderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext)); + (_renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext)); - Renderer.MakeCurrent(); - } + Renderer.MakeCurrent(); + + AvaloniaLocator.Current.GetService()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync); Device.Gpu.Renderer.Initialize(_glLogLevel); @@ -837,8 +843,6 @@ namespace Ryujinx.Ava Renderer.Start(); - Renderer.QueueRender(); - while (_isActive) { if (Device.WaitFifo()) @@ -889,6 +893,16 @@ namespace Ryujinx.Ava $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", $"GPU: {_renderer.GetHardwareInfo().GpuVendor}")); + if (Program.UseVulkan) + { + var platformInterface = AvaloniaLocator.Current.GetService(); + if (platformInterface.MainSurface.Display.IsSurfaceChanged()) + { + SetRendererWindowSize(new Size(Width, Height)); + return; + } + } + Renderer.Present(image); } @@ -970,6 +984,9 @@ namespace Ryujinx.Ava { case KeyboardHotkeyState.ToggleVSync: Device.EnableDeviceVsync = !Device.EnableDeviceVsync; + + AvaloniaLocator.Current.GetService()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync); + break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index be27e9cd2..4a5467476 100644 --- a/Ryujinx.Ava/Program.cs +++ b/Ryujinx.Ava/Program.cs @@ -94,7 +94,6 @@ namespace Ryujinx.Ava .With(new Ui.Vulkan.VulkanOptions() { ApplicationName = "Ryujinx.Graphics.Vulkan", - VulkanVersion = new Version(1, 2), MaxQueueCount = 2, PreferDiscreteGpu = true, PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value, @@ -181,6 +180,18 @@ namespace Ryujinx.Ava UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false; + if (UseVulkan) + { + if (VulkanRenderer.GetPhysicalDevices().Length == 0) + { + UseVulkan = false; + + ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl; + + Logger.Warning?.PrintMsg(LogClass.Application, "A suitable Vulkan physical device is not available. Falling back to OpenGL"); + } + } + 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, diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs index b1326dbf2..2a1cd2293 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs @@ -7,7 +7,8 @@ namespace Ryujinx.Ava.Ui.Vulkan { public static void ThrowOnError(this Result result) { - if (result != Result.Success) + // Only negative result codes are errors. + if ((int)result < (int)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 index ba7ddc7a3..70ec39c7c 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs @@ -1,26 +1,90 @@ using System; +using Avalonia; using Avalonia.Skia; using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Silk.NET.Vulkan; using SkiaSharp; namespace Ryujinx.Ava.Ui.Backend.Vulkan { internal class VulkanRenderTarget : ISkiaGpuRenderTarget { - public GRContext GrContext { get; set; } + public GRContext GrContext { get; private set; } private readonly VulkanSurfaceRenderTarget _surface; + private readonly VulkanPlatformInterface _vulkanPlatformInterface; private readonly IVulkanPlatformSurface _vulkanPlatformSurface; + private GRVkBackendContext _grVkBackend; public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface) { _surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface); + _vulkanPlatformInterface = vulkanPlatformInterface; _vulkanPlatformSurface = vulkanPlatformSurface; + + Initialize(); + } + + private void Initialize() + { + GRVkGetProcedureAddressDelegate getProc = GetVulkanProcAddress; + + _grVkBackend = new GRVkBackendContext() + { + VkInstance = _surface.Device.Handle, + VkPhysicalDevice = _vulkanPlatformInterface.PhysicalDevice.Handle, + VkDevice = _surface.Device.Handle, + VkQueue = _surface.Device.Queue.Handle, + GraphicsQueueIndex = _vulkanPlatformInterface.PhysicalDevice.QueueFamilyIndex, + GetProcedureAddress = getProc + }; + + GrContext = GRContext.CreateVulkan(_grVkBackend); + + var gpu = AvaloniaLocator.Current.GetService(); + + if (gpu.MaxResourceBytes.HasValue) + { + GrContext.SetResourceCacheLimit(gpu.MaxResourceBytes.Value); + } + } + + private IntPtr GetVulkanProcAddress(string name, IntPtr instanceHandle, IntPtr deviceHandle) + { + IntPtr addr; + + if (deviceHandle != IntPtr.Zero) + { + addr = _vulkanPlatformInterface.Api.GetDeviceProcAddr(new Device(deviceHandle), name); + + if (addr != IntPtr.Zero) + { + return addr; + } + + addr = _vulkanPlatformInterface.Api.GetDeviceProcAddr(new Device(_surface.Device.Handle), name); + + if (addr != IntPtr.Zero) + { + return addr; + } + } + + addr = _vulkanPlatformInterface.Api.GetInstanceProcAddr(new Instance(_vulkanPlatformInterface.Instance.Handle), name); + + if (addr == IntPtr.Zero) + { + addr = _vulkanPlatformInterface.Api.GetInstanceProcAddr(new Instance(instanceHandle), name); + } + + return addr; } public void Dispose() { + _grVkBackend.Dispose(); + GrContext.Dispose(); _surface.Dispose(); } @@ -45,20 +109,22 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan { GrContext.ResetContext(); + var image = _surface.GetImage(); + var imageInfo = new GRVkImageInfo() { CurrentQueueFamily = disp.QueueFamilyIndex, - Format = _surface.ImageFormat, - Image = _surface.Image.Handle, - ImageLayout = (uint)_surface.Image.CurrentLayout, - ImageTiling = (uint)_surface.Image.Tiling, + Format = (uint)image.Format, + Image = image.Handle, + ImageLayout = (uint)image.CurrentLayout, + ImageTiling = (uint)image.Tiling, ImageUsageFlags = _surface.UsageFlags, LevelCount = _surface.MipLevels, SampleCount = 1, Protected = false, Alloc = new GRVkAlloc() { - Memory = _surface.Image.MemoryHandle, + Memory = image.MemoryHandle, Flags = 0, Offset = 0, Size = _surface.MemorySize diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs index 4fc6b9295..a5c270863 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs @@ -13,71 +13,12 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan public class VulkanSkiaGpu : ISkiaGpu { private readonly VulkanPlatformInterface _vulkan; - private readonly long? _maxResourceBytes; - private GRVkBackendContext _grVkBackend; - private bool _initialized; - - public GRContext GrContext { get; private set; } + public long? MaxResourceBytes { get; } 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); - } + MaxResourceBytes = maxResourceBytes; } public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) @@ -106,10 +47,6 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window); - Initialize(); - - vulkanRenderTarget.GrContext = GrContext; - return vulkanRenderTarget; } diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs index b2b8843d2..510e6724b 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs @@ -1,5 +1,6 @@ using System; using Avalonia; +using Ryujinx.Graphics.Vulkan; using Silk.NET.Vulkan; namespace Ryujinx.Ava.Ui.Vulkan.Surfaces @@ -7,24 +8,35 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces internal class VulkanSurfaceRenderTarget : IDisposable { private readonly VulkanPlatformInterface _platformInterface; - private readonly Format _format; - public VulkanImage Image { get; private set; } - public bool IsCorrupted { get; private set; } = true; + private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer; + private VulkanImage Image { get; set; } + private object _lock = new object(); public uint MipLevels => Image.MipLevels; + public VulkanDevice Device { get; } public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface) { _platformInterface = platformInterface; - Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device, - platformInterface.PhysicalDevice, surface); + var device = VulkanInitialization.CreateDevice(platformInterface.Api, + platformInterface.PhysicalDevice.InternalHandle, + platformInterface.PhysicalDevice.QueueFamilyIndex, + VulkanInitialization.GetSupportedExtensions(platformInterface.Api, platformInterface.PhysicalDevice.InternalHandle), + platformInterface.PhysicalDevice.QueueCount); + + Device = new VulkanDevice(device, platformInterface.PhysicalDevice, platformInterface.Api); + + Display = VulkanDisplay.CreateDisplay( + platformInterface.Instance, + 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; @@ -33,13 +45,13 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces public bool IsRgba { get; } - public uint ImageFormat => (uint) _format; + public uint ImageFormat => (uint)_format; public ulong MemorySize => Image.MemorySize; - public VulkanDisplay Display { get; } + public VulkanDisplay Display { get; private set; } - public VulkanSurface Surface { get; } + public VulkanSurface Surface { get; private set; } public uint UsageFlags => Image.UsageFlags; @@ -47,46 +59,76 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces public void Dispose() { - _platformInterface.Device.WaitIdle(); - DestroyImage(); - Display?.Dispose(); - Surface?.Dispose(); + lock (_lock) + { + DestroyImage(); + Display?.Dispose(); + Surface?.Dispose(); + Device?.Dispose(); + + Display = null; + Surface = null; + } } public VulkanSurfaceRenderingSession BeginDraw(float scaling) { - var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling); + if (Image == null) + { + RecreateImage(); + } - if (IsCorrupted) - { - IsCorrupted = false; - DestroyImage(); - CreateImage(); - } - else - { - Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); - } + _commandBuffer?.WaitForFence(); + _commandBuffer = null; + + var session = new VulkanSurfaceRenderingSession(Display, Device, this, scaling); + + Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); return session; } - public void Invalidate() + public void RecreateImage() { - IsCorrupted = true; + DestroyImage(); + CreateImage(); } private void CreateImage() { Size = Display.Size; - Image = new VulkanImage(_platformInterface.Device, _platformInterface.PhysicalDevice, _platformInterface.Device.CommandBufferPool, ImageFormat, Size); + Image = new VulkanImage(Device, _platformInterface.PhysicalDevice, Display.CommandBufferPool, ImageFormat, Size); } private void DestroyImage() { - _platformInterface.Device.WaitIdle(); + _commandBuffer?.WaitForFence(); + _commandBuffer = null; Image?.Dispose(); + Image = null; + } + + public VulkanImage GetImage() + { + return Image; + } + + public void EndDraw() + { + lock (_lock) + { + if (Display == null) + { + return; + } + + _commandBuffer = Display.StartPresentation(); + + Display.BlitImageToCurrentImage(this, _commandBuffer.InternalHandle); + + Display.EndPresentation(_commandBuffer); + } } } } diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs index 240035cad..a00ecf2b9 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs @@ -10,6 +10,7 @@ namespace Ryujinx.Ava.Ui.Vulkan private readonly CommandPool _commandPool; private readonly List _usedCommandBuffers = new(); + private readonly object _lock = new object(); public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice) { @@ -36,9 +37,12 @@ namespace Ryujinx.Ava.Ui.Vulkan Level = CommandBufferLevel.Primary }; - _device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer); + lock (_lock) + { + _device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer); - return commandBuffer; + return commandBuffer; + } } public VulkanCommandBuffer CreateCommandBuffer() @@ -48,7 +52,7 @@ namespace Ryujinx.Ava.Ui.Vulkan public void FreeUsedCommandBuffers() { - lock (_usedCommandBuffers) + lock (_lock) { foreach (var usedCommandBuffer in _usedCommandBuffers) { @@ -61,7 +65,7 @@ namespace Ryujinx.Ava.Ui.Vulkan private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) { - lock (_usedCommandBuffers) + lock (_lock) { _usedCommandBuffers.Add(commandBuffer); } @@ -69,8 +73,11 @@ namespace Ryujinx.Ava.Ui.Vulkan public void Dispose() { - FreeUsedCommandBuffers(); - _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span.Empty); + lock (_lock) + { + FreeUsedCommandBuffers(); + _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span.Empty); + } } public class VulkanCommandBuffer : IDisposable @@ -80,6 +87,8 @@ namespace Ryujinx.Ava.Ui.Vulkan private readonly Fence _fence; private bool _hasEnded; private bool _hasStarted; + private bool _isDisposed; + private object _lock = new object(); public IntPtr Handle => InternalHandle.Handle; @@ -101,6 +110,22 @@ namespace Ryujinx.Ava.Ui.Vulkan device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence); } + public void WaitForFence() + { + if (_isDisposed) + { + return; + } + + lock (_lock) + { + if (!_isDisposed) + { + _device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue); + } + } + } + public void BeginRecording() { if (!_hasStarted) @@ -173,9 +198,17 @@ namespace Ryujinx.Ava.Ui.Vulkan public void Dispose() { - _device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue); - _device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle); - _device.Api.DestroyFence(_device.InternalHandle, _fence, Span.Empty); + lock (_lock) + { + if (!_isDisposed) + { + _isDisposed = true; + + _device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue); + _device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle); + _device.Api.DestroyFence(_device.InternalHandle, _fence, Span.Empty); + } + } } } } diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs index b03fd7204..3d893e19a 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs @@ -14,12 +14,9 @@ namespace Ryujinx.Ava.Ui.Vulkan api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue); - var vulkanQueue = new VulkanQueue(this, queue); - Queue = vulkanQueue; + Queue = new VulkanQueue(this, queue); - PresentQueue = vulkanQueue; - - CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice); + PresentQueue = Queue; } public IntPtr Handle => InternalHandle.Handle; @@ -29,13 +26,12 @@ namespace Ryujinx.Ava.Ui.Vulkan public VulkanQueue Queue { get; private set; } public VulkanQueue PresentQueue { get; } - public VulkanCommandBufferPool CommandBufferPool { get; } public void Dispose() { WaitIdle(); - CommandBufferPool?.Dispose(); Queue = null; + Api.DestroyDevice(InternalHandle, Span.Empty); } internal void Submit(SubmitInfo submitInfo, Fence fence = default) diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs index bfe5b5a6e..f3116fbda 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using Avalonia; using Ryujinx.Ava.Ui.Vulkan.Surfaces; -using Ryujinx.Ui.Common.Configuration; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; @@ -15,16 +14,19 @@ namespace Ryujinx.Ava.Ui.Vulkan private readonly VulkanInstance _instance; private readonly VulkanPhysicalDevice _physicalDevice; private readonly VulkanSemaphorePair _semaphorePair; + private readonly VulkanDevice _device; private uint _nextImage; private readonly VulkanSurface _surface; private SurfaceFormatKHR _surfaceFormat; private SwapchainKHR _swapchain; private Extent2D _swapchainExtent; private Image[] _swapchainImages; - private VulkanDevice _device { get; } - private ImageView[] _swapchainImageViews = new ImageView[0]; + private ImageView[] _swapchainImageViews = Array.Empty(); private bool _vsyncStateChanged; private bool _vsyncEnabled; + private bool _surfaceChanged; + + public event EventHandler Presented; public VulkanCommandBufferPool CommandBufferPool { get; set; } @@ -73,6 +75,14 @@ namespace Ryujinx.Ava.Ui.Vulkan CommandBufferPool.Dispose(); } + public bool IsSurfaceChanged() + { + var changed = _surfaceChanged; + _surfaceChanged = false; + + return changed; + } + private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device, VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent, SwapchainKHR? oldswapchain = null, bool vsyncEnabled = true) @@ -193,22 +203,23 @@ namespace Ryujinx.Ava.Ui.Vulkan } var modes = presentModes.ToList(); - var presentMode = PresentModeKHR.PresentModeFifoKhr; if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr)) { - presentMode = PresentModeKHR.PresentModeImmediateKhr; + return PresentModeKHR.PresentModeImmediateKhr; } else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr)) { - presentMode = PresentModeKHR.PresentModeMailboxKhr; + return PresentModeKHR.PresentModeMailboxKhr; } - else if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr)) + else if (modes.Contains(PresentModeKHR.PresentModeFifoKhr)) { - presentMode = PresentModeKHR.PresentModeImmediateKhr; + return PresentModeKHR.PresentModeFifoKhr; + } + else + { + return PresentModeKHR.PresentModeImmediateKhr; } - - return presentMode; } internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device, @@ -266,6 +277,8 @@ namespace Ryujinx.Ava.Ui.Vulkan _swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled); CreateSwapchainImages(); + + _surfaceChanged = true; } private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format) @@ -306,7 +319,7 @@ namespace Ryujinx.Ava.Ui.Vulkan return true; } - internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation(VulkanSurfaceRenderTarget renderTarget) + internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation() { _nextImage = 0; while (true) @@ -346,8 +359,10 @@ namespace Ryujinx.Ava.Ui.Vulkan internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer) { + var image = renderTarget.GetImage(); + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer, - renderTarget.Image.InternalHandle.Value, (ImageLayout)renderTarget.Image.CurrentLayout, + image.InternalHandle.Value, (ImageLayout)image.CurrentLayout, AccessFlags.AccessNoneKhr, ImageLayout.TransferSrcOptimal, AccessFlags.AccessTransferReadBit, @@ -381,7 +396,7 @@ namespace Ryujinx.Ava.Ui.Vulkan } }; - _device.Api.CmdBlitImage(commandBuffer, renderTarget.Image.InternalHandle.Value, + _device.Api.CmdBlitImage(commandBuffer, image.InternalHandle.Value, ImageLayout.TransferSrcOptimal, _swapchainImages[_nextImage], ImageLayout.TransferDstOptimal, @@ -390,9 +405,9 @@ namespace Ryujinx.Ava.Ui.Vulkan Filter.Linear); VulkanMemoryHelper.TransitionLayout(_device, commandBuffer, - renderTarget.Image.InternalHandle.Value, ImageLayout.TransferSrcOptimal, + image.InternalHandle.Value, ImageLayout.TransferSrcOptimal, AccessFlags.AccessTransferReadBit, - (ImageLayout)renderTarget.Image.CurrentLayout, + (ImageLayout)image.CurrentLayout, AccessFlags.AccessNoneKhr, renderTarget.MipLevels); } @@ -434,6 +449,8 @@ namespace Ryujinx.Ava.Ui.Vulkan } CommandBufferPool.FreeUsedCommandBuffers(); + + Presented?.Invoke(this, null); } } } diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs index 343ba7605..3fbb8665e 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs @@ -148,20 +148,18 @@ namespace Ryujinx.Ava.Ui.Vulkan _currentAccessFlags = destinationAccessFlags; } - public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags) + public void Dispose() { - TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags); - } + if (InternalHandle != null) + { + _device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, Span.Empty); + _device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, Span.Empty); + _device.Api.FreeMemory(_device.InternalHandle, _imageMemory, Span.Empty); - 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; + _imageView = default; + InternalHandle = null; + _imageMemory = default; + } } } } diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs index a3a9ea611..b50e9c07d 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs @@ -57,8 +57,7 @@ namespace Ryujinx.Ava.Ui.Vulkan var applicationInfo = new ApplicationInfo { PApplicationName = (byte*)applicationName, - ApiVersion = new Version32((uint)options.VulkanVersion.Major, (uint)options.VulkanVersion.Minor, - (uint)options.VulkanVersion.Build), + ApiVersion = Vk.Version12.Value, PEngineName = (byte*)engineName, EngineVersion = new Version32(1, 0, 0), ApplicationVersion = new Version32(1, 0, 0) diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs index 8e8363982..0027753c8 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs @@ -11,11 +11,6 @@ namespace Ryujinx.Ava.Ui.Vulkan /// 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 /// diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs index 47a07949c..ff8d93286 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs @@ -18,13 +18,11 @@ namespace Ryujinx.Ava.Ui.Vulkan public VulkanPhysicalDevice PhysicalDevice { get; private set; } public VulkanInstance Instance { get; } - public VulkanDevice Device { get; set; } public Vk Api { get; private set; } public VulkanSurfaceRenderTarget MainSurface { get; set; } public void Dispose() { - Device?.Dispose(); Instance?.Dispose(); Api?.Dispose(); } @@ -54,16 +52,9 @@ namespace Ryujinx.Ava.Ui.Vulkan { var surface = VulkanSurface.CreateSurface(Instance, platformSurface); - if (Device == null) + if (PhysicalDevice == null) { PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice); - var device = VulkanInitialization.CreateDevice(Instance.Api, - PhysicalDevice.InternalHandle, - PhysicalDevice.QueueFamilyIndex, - VulkanInitialization.GetSupportedExtensions(Instance.Api, PhysicalDevice.InternalHandle), - PhysicalDevice.QueueCount); - - Device = new VulkanDevice(device, PhysicalDevice, Instance.Api); } var renderTarget = new VulkanSurfaceRenderTarget(this, surface); @@ -71,7 +62,6 @@ namespace Ryujinx.Ava.Ui.Vulkan if (MainSurface == null && surface != null) { MainSurface = renderTarget; - MainSurface.Display.ChangeVSyncMode(false); } return renderTarget; diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs index 8833ede5c..71f5f18ac 100644 --- a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs @@ -9,7 +9,6 @@ namespace Ryujinx.Ava.Ui.Vulkan { private readonly VulkanDevice _device; private readonly VulkanSurfaceRenderTarget _renderTarget; - private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer; public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device, VulkanSurfaceRenderTarget renderTarget, float scaling) @@ -32,17 +31,13 @@ namespace Ryujinx.Ava.Ui.Vulkan { if (!Display.EnsureSwapchainAvailable()) { - _renderTarget.Invalidate(); + _renderTarget.RecreateImage(); } } public void Dispose() { - _commandBuffer = Display.StartPresentation(_renderTarget); - - Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle); - - Display.EndPresentation(_commandBuffer); + _renderTarget.EndDraw(); } } } diff --git a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs index db9caca13..e58bdaa0a 100644 --- a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs +++ b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.Ui.Controls public override void DestroyBackgroundContext() { - _image = null; + Image = null; if (_fence != IntPtr.Zero) { @@ -57,6 +57,8 @@ namespace Ryujinx.Ava.Ui.Controls Dispatcher.UIThread.InvokeAsync(() => { Image = (int)image; + + InvalidateVisual(); }).Wait(); if (_fence != IntPtr.Zero) @@ -66,7 +68,7 @@ namespace Ryujinx.Ava.Ui.Controls _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); - QueueRender(); + InvalidateVisual(); _gameBackgroundWindow.SwapBuffers(); } diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs index 130348f24..392f67e35 100644 --- a/Ryujinx.Ava/Ui/Controls/RendererControl.cs +++ b/Ryujinx.Ava/Ui/Controls/RendererControl.cs @@ -11,25 +11,7 @@ namespace Ryujinx.Ava.Ui.Controls { internal abstract class RendererControl : Control { - protected object _image; - - static RendererControl() - { - AffectsRender(ImageProperty); - } - - public readonly static StyledProperty ImageProperty = - AvaloniaProperty.Register( - nameof(Image), - 0, - inherits: true, - defaultBindingMode: BindingMode.TwoWay); - - protected object Image - { - get => _image; - set => SetAndRaise(ImageProperty, ref _image, value); - } + protected object Image { get; set; } public event EventHandler RendererInitialized; public event EventHandler SizeChanged; @@ -60,8 +42,6 @@ namespace Ryujinx.Ava.Ui.Controls if (!rect.IsEmpty) { RenderSize = rect.Size * VisualRoot.RenderScaling; - - DrawOperation?.Dispose(); DrawOperation = CreateDrawOperation(); } } @@ -97,17 +77,11 @@ namespace Ryujinx.Ava.Ui.Controls RendererInitialized?.Invoke(this, EventArgs.Empty); } - public void QueueRender() - { - Program.RenderTimer.TickNow(); - } - internal abstract void Present(object image); internal void Start() { IsStarted = true; - QueueRender(); } internal void Stop() diff --git a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs index fdbd8df97..7b7dfaa10 100644 --- a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs +++ b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; @@ -11,20 +12,23 @@ 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; +using System.Collections.Concurrent; namespace Ryujinx.Ava.Ui.Controls { internal class VulkanRendererControl : RendererControl { + private const int MaxImagesInFlight = 3; + private VulkanPlatformInterface _platformInterface; + private ConcurrentQueue _imagesInFlight; + private PresentImageInfo _currentImage; public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) { _platformInterface = AvaloniaLocator.Current.GetService(); + + _imagesInFlight = new ConcurrentQueue(); } public override void DestroyBackgroundContext() @@ -37,6 +41,40 @@ namespace Ryujinx.Ava.Ui.Controls return new VulkanDrawOperation(this); } + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _imagesInFlight.Clear(); + + if (_platformInterface.MainSurface.Display != null) + { + _platformInterface.MainSurface.Display.Presented -= Window_Presented; + } + + _currentImage?.Put(); + _currentImage = null; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + _platformInterface.MainSurface.Display.Presented += Window_Presented; + } + + private void Window_Presented(object sender, EventArgs e) + { + _platformInterface.MainSurface.Device.QueueWaitIdle(); + _currentImage?.Put(); + _currentImage = null; + } + + public override void Render(DrawingContext context) + { + base.Render(context); + } + protected override void CreateWindow() { } @@ -51,12 +89,29 @@ namespace Ryujinx.Ava.Ui.Controls internal override void Present(object image) { - Dispatcher.UIThread.InvokeAsync(() => - { - Image = image; - }).Wait(); + Image = image; - QueueRender(); + _imagesInFlight.Enqueue((PresentImageInfo)image); + + if (_imagesInFlight.Count > MaxImagesInFlight) + { + _imagesInFlight.TryDequeue(out _); + } + + Dispatcher.UIThread.Post(InvalidateVisual); + } + + private PresentImageInfo GetImage() + { + lock (_imagesInFlight) + { + if (!_imagesInFlight.TryDequeue(out _currentImage)) + { + _currentImage = (PresentImageInfo)Image; + } + + return _currentImage; + } } private class VulkanDrawOperation : ICustomDrawOperation @@ -64,6 +119,7 @@ namespace Ryujinx.Ava.Ui.Controls public Rect Bounds { get; } private readonly VulkanRendererControl _control; + private bool _isDestroyed; public VulkanDrawOperation(VulkanRendererControl control) { @@ -73,7 +129,12 @@ namespace Ryujinx.Ava.Ui.Controls public void Dispose() { + if (_isDestroyed) + { + return; + } + _isDestroyed = true; } public bool Equals(ICustomDrawOperation other) @@ -86,30 +147,33 @@ namespace Ryujinx.Ava.Ui.Controls return Bounds.Contains(p); } - public void Render(IDrawingContextImpl context) + public unsafe void Render(IDrawingContextImpl context) { - if (_control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0) + if (_isDestroyed || _control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0 || + context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) { return; } - var image = (PresentImageInfo)_control.Image; + var image = _control.GetImage(); - if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + if (!image.State.IsValid) { + _control._currentImage = null; + return; } - _control._platformInterface.Device.QueueWaitIdle(); - var gpu = AvaloniaLocator.Current.GetService(); + image.Get(); + var imageInfo = new GRVkImageInfo() { CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex, Format = (uint)Format.R8G8B8A8Unorm, Image = image.Image.Handle, - ImageLayout = (uint)ImageLayout.ColorAttachmentOptimal, + ImageLayout = (uint)ImageLayout.TransferSrcOptimal, ImageTiling = (uint)ImageTiling.Optimal, ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferSrcBit @@ -127,13 +191,15 @@ namespace Ryujinx.Ava.Ui.Controls }; using var backendTexture = new GRBackendRenderTarget( - (int)_control.RenderSize.Width, - (int)_control.RenderSize.Height, + (int)image.Extent.Width, + (int)image.Extent.Height, 1, imageInfo); + + var vulkan = AvaloniaLocator.Current.GetService(); using var surface = SKSurface.Create( - gpu.GrContext, + skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, SKColorType.Rgba8888); @@ -143,10 +209,11 @@ namespace Ryujinx.Ava.Ui.Controls return; } - var rect = new Rect(new Point(), _control.RenderSize); + var rect = new Rect(new Point(), new Size(image.Extent.Width, image.Extent.Height)); using var snapshot = surface.Snapshot(); - skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), + new SKPaint()); } } } diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs index cd756d686..98516159b 100644 --- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs @@ -38,6 +38,8 @@ namespace Ryujinx.Ava.Ui.ViewModels { internal class MainWindowViewModel : BaseModel { + private const int HotKeyPressDelayMs = 500; + private readonly MainWindow _owner; private ObservableCollection _applications; private string _aspectStatusText; @@ -54,6 +56,7 @@ namespace Ryujinx.Ava.Ui.ViewModels private bool _isLoading; private int _progressMaximum; private int _progressValue; + private long _lastFullscreenToggle = Environment.TickCount64; private bool _showLoadProgress; private bool _showMenuAndStatusBar = true; private bool _showStatusSeparator; @@ -929,6 +932,13 @@ namespace Ryujinx.Ava.Ui.ViewModels public void ToggleFullscreen() { + if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs) + { + return; + } + + _lastFullscreenToggle = Environment.TickCount64; + WindowState state = _owner.WindowState; if (state == WindowState.FullScreen) @@ -1085,6 +1095,11 @@ namespace Ryujinx.Ava.Ui.ViewModels { selection.Favorite = !selection.Favorite; + _owner.ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata => + { + appMetadata.Favorite = selection.Favorite; + }); + RefreshView(); } } diff --git a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs index 7b08923e3..a7cf710e7 100644 --- a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs @@ -48,6 +48,10 @@ namespace Ryujinx.Ava.Ui.ViewModels private int _graphicsBackendMultithreadingIndex; private float _previousVolumeLevel; private float _volume; + private bool _isVulkanAvailable = true; + private List _gpuIds = new List(); + private KeyboardHotkeys _keyboardHotkeys; + private int _graphicsBackendIndex; public int ResolutionScale { @@ -97,6 +101,17 @@ namespace Ryujinx.Ava.Ui.ViewModels } } + public bool IsVulkanAvailable + { + get => _isVulkanAvailable; + set + { + _isVulkanAvailable = value; + + OnPropertyChanged(); + } + } + public bool EnableDiscordIntegration { get; set; } public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } @@ -143,10 +158,10 @@ namespace Ryujinx.Ava.Ui.ViewModels public int BaseStyleIndex { get; set; } public int GraphicsBackendIndex { - get => graphicsBackendIndex; + get => _graphicsBackendIndex; set { - graphicsBackendIndex = value; + _graphicsBackendIndex = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsVulkanSelected)); } @@ -170,14 +185,9 @@ namespace Ryujinx.Ava.Ui.ViewModels public DateTimeOffset DateOffset { get; set; } public TimeSpan TimeOffset { get; set; } public AvaloniaList TimeZones { get; set; } - public AvaloniaList GameDirectories { get; set; } public ObservableCollection AvailableGpus { get; set; } - private KeyboardHotkeys _keyboardHotkeys; - private int graphicsBackendIndex; - private List _gpuIds = new List(); - public KeyboardHotkeys KeyboardHotkeys { get => _keyboardHotkeys; @@ -233,20 +243,31 @@ namespace Ryujinx.Ava.Ui.ViewModels if (!Program.UseVulkan) { var devices = VulkanRenderer.GetPhysicalDevices(); - foreach (var device in devices) + + if (devices.Length == 0) { - _gpuIds.Add(device.Id); - names.Add($"{device.Name} {(device.IsDiscrete ? "(dGpu)" : "")}"); + IsVulkanAvailable = false; + GraphicsBackendIndex = 1; + } + else + { + foreach (var device in devices) + { + _gpuIds.Add(device.Id); + names.Add($"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}"); + } } } else { foreach (var device in VulkanPhysicalDevice.SuitableDevices) { - _gpuIds.Add(VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID)); + _gpuIds.Add( + VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID)); var value = device.Value; var name = value.DeviceName; - names.Add($"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGpu)" : "")}"); + names.Add( + $"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGPU)" : "")}"); } } diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs index 73e5e0991..ef22e5c81 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs @@ -656,7 +656,12 @@ namespace Ryujinx.Ava.Ui.Windows { AppHost = null; - Dispatcher.UIThread.Post(Close); + Dispatcher.UIThread.Post(() => + { + MainContent = null; + + Close(); + }); }; AppHost?.Stop(); diff --git a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml index 0e5978b70..b46297601 100644 --- a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml @@ -519,7 +519,7 @@ HorizontalContentAlignment="Left" ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" SelectedIndex="{Binding GraphicsBackendIndex}"> - + diff --git a/Ryujinx.Graphics.GAL/IWindow.cs b/Ryujinx.Graphics.GAL/IWindow.cs index 12ff1debd..043193c9c 100644 --- a/Ryujinx.Graphics.GAL/IWindow.cs +++ b/Ryujinx.Graphics.GAL/IWindow.cs @@ -7,5 +7,7 @@ namespace Ryujinx.Graphics.GAL void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); void SetSize(int width, int height); + + void ChangeVSyncMode(bool vsyncEnabled); } } diff --git a/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index c80455028..21a66e7cb 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -30,5 +30,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading { _impl.Window.SetSize(width, height); } + + public void ChangeVSyncMode(bool vsyncEnabled) { } } } diff --git a/Ryujinx.Graphics.OpenGL/Window.cs b/Ryujinx.Graphics.OpenGL/Window.cs index e5a7ebf0e..f67c6a724 100644 --- a/Ryujinx.Graphics.OpenGL/Window.cs +++ b/Ryujinx.Graphics.OpenGL/Window.cs @@ -58,6 +58,8 @@ namespace Ryujinx.Graphics.OpenGL GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } + public void ChangeVSyncMode(bool vsyncEnabled) { } + private void CreateStagingFramebuffer() { _stagingFrameBuffer = GL.GenFramebuffer(); diff --git a/Ryujinx.Graphics.Vulkan/ImageWindow.cs b/Ryujinx.Graphics.Vulkan/ImageWindow.cs index 5dd23155d..69302fdf7 100644 --- a/Ryujinx.Graphics.Vulkan/ImageWindow.cs +++ b/Ryujinx.Graphics.Vulkan/ImageWindow.cs @@ -7,7 +7,9 @@ namespace Ryujinx.Graphics.Vulkan { class ImageWindow : WindowBase, IWindow, IDisposable { - private const int ImageCount = 5; + internal const VkFormat Format = VkFormat.R8G8B8A8Unorm; + + private const int ImageCount = 3; private const int SurfaceWidth = 1280; private const int SurfaceHeight = 720; @@ -18,52 +20,49 @@ namespace Ryujinx.Graphics.Vulkan private Auto[] _images; private Auto[] _imageViews; private Auto[] _imageAllocationAuto; + private ImageState[] _states; + private PresentImageInfo[] _presentedImages; + private FenceHolder[] _fences; + 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(VulkanRenderer 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]; + _states = new ImageState[ImageCount]; + _presentedImages = new PresentImageInfo[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(); + lock (_states[i]) + { + _states[i].IsValid = false; + _fences[i]?.Wait(); + _fences[i]?.Put(); + _imageViews[i]?.Dispose(); + _imageAllocationAuto[i]?.Dispose(); + _images[i]?.Dispose(); + } } + _presentedImages = null; CreateImages(); } @@ -71,34 +70,35 @@ namespace Ryujinx.Graphics.Vulkan private unsafe void CreateImages() { _imageViews = new Auto[ImageCount]; + _fences = new FenceHolder[ImageCount]; + _presentedImages = new PresentImageInfo[ImageCount]; + _nextImage = 0; var cbs = _gd.CommandBufferPool.Rent(); + + 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 + }; + 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; @@ -108,7 +108,7 @@ namespace Ryujinx.Graphics.Vulkan _gd.Api.BindImageMemory(_device, image, allocation.Memory, allocation.Offset); - _imageViews[i] = CreateImageView(image, _format); + _imageViews[i] = CreateImageView(image, Format); Transition( cbs.CommandBuffer, @@ -116,7 +116,9 @@ namespace Ryujinx.Graphics.Vulkan 0, 0, ImageLayout.Undefined, - ImageLayout.ColorAttachmentOptimal); + ImageLayout.TransferSrcOptimal); + + _states[i] = new ImageState(); } _gd.CommandBufferPool.Return(cbs); @@ -165,7 +167,7 @@ namespace Ryujinx.Graphics.Vulkan image.GetUnsafe().Value, 0, AccessFlags.AccessTransferWriteBit, - ImageLayout.ColorAttachmentOptimal, + ImageLayout.TransferSrcOptimal, ImageLayout.General); var view = (TextureView)texture; @@ -232,7 +234,7 @@ namespace Ryujinx.Graphics.Vulkan _imageViews[_nextImage], _width, _height, - _format, + Format, new Extents2D(srcX0, srcY0, srcX1, srcY1), new Extents2D(dstX0, dstY1, dstX1, dstY0), true, @@ -244,7 +246,7 @@ namespace Ryujinx.Graphics.Vulkan 0, 0, ImageLayout.General, - ImageLayout.ColorAttachmentOptimal); + ImageLayout.TransferSrcOptimal); _gd.CommandBufferPool.Return( cbs, @@ -252,12 +254,30 @@ namespace Ryujinx.Graphics.Vulkan stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit }, null); - var memory = _imageAllocationAuto[_nextImage].GetUnsafe().Memory; - var presentInfo = new PresentImageInfo(image.GetUnsafe().Value, memory, _imageSizes[_nextImage], _imageOffsets[_nextImage], _renderFinishedSemaphore, _imageAvailableSemaphore); + _fences[_nextImage]?.Put(); + _fences[_nextImage] = cbs.GetFence(); + cbs.GetFence().Get(); - swapBuffersCallback(presentInfo); + PresentImageInfo info = _presentedImages[_nextImage]; - _nextImage %= ImageCount; + if (info == null) + { + info = new PresentImageInfo( + image, + _imageAllocationAuto[_nextImage], + _device, + _physicalDevice, + _imageSizes[_nextImage], + _imageOffsets[_nextImage], + new Extent2D((uint)_width, (uint)_height), + _states[_nextImage]); + + _presentedImages[_nextImage] = info; + } + + swapBuffersCallback(info); + + _nextImage = (_nextImage + 1) % ImageCount; } private unsafe void Transition( @@ -320,11 +340,11 @@ namespace Ryujinx.Graphics.Vulkan { unsafe { - _gd.Api.DestroySemaphore(_device, _renderFinishedSemaphore, null); - _gd.Api.DestroySemaphore(_device, _imageAvailableSemaphore, null); - for (int i = 0; i < ImageCount; i++) { + _states[i].IsValid = false; + _fences[i]?.Wait(); + _fences[i]?.Put(); _imageViews[i]?.Dispose(); _imageAllocationAuto[i]?.Dispose(); _images[i]?.Dispose(); @@ -337,25 +357,73 @@ namespace Ryujinx.Graphics.Vulkan { Dispose(true); } + + public override void ChangeVSyncMode(bool vsyncEnabled) { } + } + + public class ImageState + { + private bool _isValid = true; + + public bool IsValid + { + get => _isValid; + internal set + { + _isValid = value; + + StateChanged?.Invoke(this, _isValid); + } + } + + public event EventHandler StateChanged; } 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; } + private readonly Auto _image; + private readonly Auto _memory; - public PresentImageInfo(Image image, DeviceMemory memory, ulong memorySize, ulong memoryOffset, Semaphore readySemaphore, Semaphore availableSemaphore) + public Image Image => _image.GetUnsafe().Value; + + public DeviceMemory Memory => _memory.GetUnsafe().Memory; + + public Device Device { get; } + public PhysicalDevice PhysicalDevice { get; } + public ulong MemorySize { get; } + public ulong MemoryOffset { get; } + public Extent2D Extent { get; } + public ImageState State { get; internal set; } + internal PresentImageInfo( + Auto image, + Auto memory, + Device device, + PhysicalDevice physicalDevice, + ulong memorySize, + ulong memoryOffset, + Extent2D extent2D, + ImageState state) { - this.Image = image; - this.Memory = memory; - this.MemorySize = memorySize; - this.MemoryOffset = memoryOffset; - this.ReadySemaphore = readySemaphore; - this.AvailableSemaphore = availableSemaphore; + _image = image; + _memory = memory; + Device = device; + PhysicalDevice = physicalDevice; + MemorySize = memorySize; + MemoryOffset = memoryOffset; + Extent = extent2D; + State = state; + } + + public void Get() + { + _memory.IncrementReferenceCount(); + _image.IncrementReferenceCount(); + } + + public void Put() + { + _memory.DecrementReferenceCount(); + _image.DecrementReferenceCount(); } } } \ No newline at end of file diff --git a/Ryujinx.Graphics.Vulkan/Window.cs b/Ryujinx.Graphics.Vulkan/Window.cs index 12212a7f4..26f53b395 100644 --- a/Ryujinx.Graphics.Vulkan/Window.cs +++ b/Ryujinx.Graphics.Vulkan/Window.cs @@ -25,6 +25,8 @@ namespace Ryujinx.Graphics.Vulkan private int _width; private int _height; + private bool _vsyncEnabled; + private bool _vsyncModeChanged; private VkFormat _format; public unsafe Window(VulkanRenderer gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device) @@ -47,6 +49,8 @@ namespace Ryujinx.Graphics.Vulkan private void RecreateSwapchain() { + _vsyncModeChanged = false; + for (int i = 0; i < _swapchainImageViews.Length; i++) { _swapchainImageViews[i].Dispose(); @@ -110,7 +114,7 @@ namespace Ryujinx.Graphics.Vulkan ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr, - PresentMode = ChooseSwapPresentMode(presentModes), + PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), Clipped = true, OldSwapchain = oldSwapchain }; @@ -178,9 +182,9 @@ namespace Ryujinx.Graphics.Vulkan return availableFormats[0]; } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) { - if (availablePresentModes.Contains(PresentModeKHR.PresentModeImmediateKhr)) + if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.PresentModeImmediateKhr)) { return PresentModeKHR.PresentModeImmediateKhr; } @@ -188,6 +192,10 @@ namespace Ryujinx.Graphics.Vulkan { return PresentModeKHR.PresentModeMailboxKhr; } + else if (availablePresentModes.Contains(PresentModeKHR.PresentModeFifoKhr)) + { + return PresentModeKHR.PresentModeFifoKhr; + } else { return PresentModeKHR.PresentModeFifoKhr; @@ -224,7 +232,8 @@ namespace Ryujinx.Graphics.Vulkan ref nextImage); if (acquireResult == Result.ErrorOutOfDateKhr || - acquireResult == Result.SuboptimalKhr) + acquireResult == Result.SuboptimalKhr || + _vsyncModeChanged) { RecreateSwapchain(); } @@ -404,6 +413,12 @@ namespace Ryujinx.Graphics.Vulkan // Not needed as we can get the size from the surface. } + public override void ChangeVSyncMode(bool vsyncEnabled) + { + _vsyncEnabled = vsyncEnabled; + _vsyncModeChanged = true; + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/Ryujinx.Graphics.Vulkan/WindowBase.cs b/Ryujinx.Graphics.Vulkan/WindowBase.cs index 4f1f0d165..80b5c0e3f 100644 --- a/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,5 +10,6 @@ namespace Ryujinx.Graphics.Vulkan public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); + public abstract void ChangeVSyncMode(bool vsyncEnabled); } } \ No newline at end of file diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs index 22e8d5c3a..3cdc424ef 100644 --- a/Ryujinx/Ui/RendererWidgetBase.cs +++ b/Ryujinx/Ui/RendererWidgetBase.cs @@ -402,6 +402,8 @@ namespace Ryujinx.Ui Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Translator.IsReadyForTranslation.Set(); + Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + (Toplevel as MainWindow)?.ActivatePauseMenu(); while (_isActive)