From c8f9292babd5aa6021ce1bd6a977130baebb7de3 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Tue, 16 Aug 2022 16:32:37 +0000
Subject: [PATCH] Avalonia - Couple fixes and improvements to vulkan (#3483)

* drop split devices, rebase

* add fallback to opengl if vulkan is not available

* addressed review

* ensure present image references are incremented and decremented when necessary

* allow changing vsync for vulkan

* fix screenshot on avalonia vulkan

* save favorite when toggled

* improve sync between popups

* use separate devices for each new window

* fix crash when closing window

* addressed review

* don't create the main window with immediate mode

* change skia vk delegate to method

* update vulkan throwonerror

* addressed review
---
 Ryujinx.Ava/AppHost.cs                        |  39 +++-
 Ryujinx.Ava/Program.cs                        |  13 +-
 .../Ui/Backend/Vulkan/ResultExtensions.cs     |   3 +-
 .../Backend/Vulkan/Skia/VulkanRenderTarget.cs |  78 ++++++-
 .../Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs   |  67 +-----
 .../Surfaces/VulkanSurfaceRenderTarget.cs     |  98 ++++++---
 .../Backend/Vulkan/VulkanCommandBufferPool.cs |  51 ++++-
 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs |  10 +-
 .../Ui/Backend/Vulkan/VulkanDisplay.cs        |  47 +++--
 Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs  |  22 +-
 .../Ui/Backend/Vulkan/VulkanInstance.cs       |   3 +-
 .../Ui/Backend/Vulkan/VulkanOptions.cs        |   5 -
 .../Backend/Vulkan/VulkanPlatformInterface.cs |  12 +-
 .../Vulkan/VulkanSurfaceRenderingSession.cs   |   9 +-
 .../Ui/Controls/OpenGLRendererControl.cs      |   6 +-
 Ryujinx.Ava/Ui/Controls/RendererControl.cs    |  28 +--
 .../Ui/Controls/VulkanRendererControl.cs      | 109 ++++++++--
 .../Ui/ViewModels/MainWindowViewModel.cs      |  15 ++
 .../Ui/ViewModels/SettingsViewModel.cs        |  45 ++--
 Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs    |   7 +-
 Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml   |   2 +-
 Ryujinx.Graphics.GAL/IWindow.cs               |   2 +
 .../Multithreading/ThreadedWindow.cs          |   2 +
 Ryujinx.Graphics.OpenGL/Window.cs             |   2 +
 Ryujinx.Graphics.Vulkan/ImageWindow.cs        | 196 ++++++++++++------
 Ryujinx.Graphics.Vulkan/Window.cs             |  23 +-
 Ryujinx.Graphics.Vulkan/WindowBase.cs         |   1 +
 Ryujinx/Ui/RendererWidgetBase.cs              |   2 +
 28 files changed, 585 insertions(+), 312 deletions(-)

diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs
index bd9c808ebb..7e3cddc887 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<VulkanPlatformInterface>()?.MainSurface.Display.ChangeVSyncMode(true);
+
             _isStopped = true;
             _isActive = false;
         }
@@ -596,12 +599,13 @@ namespace Ryujinx.Ava
             if (Program.UseVulkan)
             {
                 var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
+                
                 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<VulkanPlatformInterface>()?.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<VulkanPlatformInterface>();
+                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<VulkanPlatformInterface>()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync);
+
                             break;
                         case KeyboardHotkeyState.Screenshot:
                             ScreenshotRequested = true;
diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs
index be27e9cd21..4a54674768 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 b1326dbf29..2a1cd2293a 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 ba7ddc7a38..70ec39c7cd 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<VulkanSkiaGpu>();
+
+            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 4fc6b92954..a5c2708638 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<VulkanPlatformInterface>();
-            _maxResourceBytes = maxResourceBytes;
-        }
-
-        private void Initialize()
-        {
-            if (_initialized)
-            {
-                return;
-            }
-
-            _initialized = true;
-            GRVkGetProcedureAddressDelegate getProc = (string name, IntPtr instanceHandle, IntPtr deviceHandle) =>
-            {
-                IntPtr addr = IntPtr.Zero;
-
-                if (deviceHandle != IntPtr.Zero)
-                {
-                    addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(deviceHandle), name);
-
-                    if (addr != IntPtr.Zero)
-                    {
-                        return addr;
-                    }
-
-                    addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(_vulkan.Device.Handle), name);
-
-                    if (addr != IntPtr.Zero)
-                    {
-                        return addr;
-                    }
-                }
-
-                addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(_vulkan.Instance.Handle), name);
-
-                if (addr == IntPtr.Zero)
-                {
-                    addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(instanceHandle), name);
-                }
-
-                return addr;
-            };
-
-            _grVkBackend = new GRVkBackendContext()
-            {
-                VkInstance = _vulkan.Device.Handle,
-                VkPhysicalDevice = _vulkan.PhysicalDevice.Handle,
-                VkDevice = _vulkan.Device.Handle,
-                VkQueue = _vulkan.Device.Queue.Handle,
-                GraphicsQueueIndex = _vulkan.PhysicalDevice.QueueFamilyIndex,
-                GetProcedureAddress = getProc
-            };
-            GrContext = GRContext.CreateVulkan(_grVkBackend);
-            if (_maxResourceBytes.HasValue)
-            {
-                GrContext.SetResourceCacheLimit(_maxResourceBytes.Value);
-            }
+            MaxResourceBytes = maxResourceBytes;
         }
 
         public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> 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 b2b8843d29..510e6724b8 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 240035cadc..a00ecf2b97 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<VulkanCommandBuffer> _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<AllocationCallbacks>.Empty);
+            lock (_lock)
+            {
+                FreeUsedCommandBuffers();
+                _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.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<AllocationCallbacks>.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<AllocationCallbacks>.Empty);
+                    }
+                }
             }
         }
     }
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs
index b03fd72040..3d893e19a2 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<AllocationCallbacks>.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 bfe5b5a6e7..f3116fbda2 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<ImageView>();
         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 343ba7605a..3fbb8665e3 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<AllocationCallbacks>.Empty);
+                _device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, Span<AllocationCallbacks>.Empty);
+                _device.Api.FreeMemory(_device.InternalHandle, _imageMemory, Span<AllocationCallbacks>.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 a3a9ea611f..b50e9c07d5 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 8e83639829..0027753c8b 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
         /// </summary>
         public string ApplicationName { get; set; }
 
-        /// <summary>
-        /// Specifies the Vulkan API version to use
-        /// </summary>
-        public Version VulkanVersion { get; set; } = new Version(1, 1, 0);
-
         /// <summary>
         /// Specifies additional extensions to enable if available on the instance
         /// </summary>
diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs
index 47a07949cd..ff8d932868 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 8833ede5c0..71f5f18ac1 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 db9caca13b..e58bdaa0af 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 130348f24e..392f67e35a 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<RendererControl>(ImageProperty);
-        }
-
-        public readonly static StyledProperty<object> ImageProperty =
-            AvaloniaProperty.Register<RendererControl, object>(
-                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<EventArgs> RendererInitialized;
         public event EventHandler<Size> 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 fdbd8df978..7b7dfaa107 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<PresentImageInfo> _imagesInFlight;
+        private PresentImageInfo _currentImage;
 
         public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
         {
             _platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
+
+            _imagesInFlight = new ConcurrentQueue<PresentImageInfo>();
         }
 
         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<VulkanSkiaGpu>();
 
+                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<VulkanPlatformInterface>();
 
                 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 cd756d6861..98516159bf 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<ApplicationData> _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 7b08923e38..a7cf710e77 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<string> _gpuIds = new List<string>();
+        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<TimeZone> TimeZones { get; set; }
-
         public AvaloniaList<string> GameDirectories { get; set; }
         public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
 
-        private KeyboardHotkeys _keyboardHotkeys;
-        private int graphicsBackendIndex;
-        private List<string> _gpuIds = new List<string>();
-
         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 73e5e09910..ef22e5c816 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 0e5978b702..b462976017 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}">
-                                        <ComboBoxItem>
+                                        <ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
                                             <TextBlock Text="Vulkan" />
                                         </ComboBoxItem>
                                         <ComboBoxItem>
diff --git a/Ryujinx.Graphics.GAL/IWindow.cs b/Ryujinx.Graphics.GAL/IWindow.cs
index 12ff1debd0..043193c9cf 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<object> 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 c804550283..21a66e7cbc 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 e5a7ebf0e9..f67c6a7249 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 5dd23155d4..69302fdf75 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<DisposableImage>[] _images;
         private Auto<DisposableImageView>[] _imageViews;
         private Auto<MemoryAllocation>[] _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<DisposableImage>[ImageCount];
             _imageAllocationAuto = new Auto<MemoryAllocation>[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<DisposableImageView>[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<DisposableImage>(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<bool> 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<DisposableImage> _image;
+        private readonly Auto<MemoryAllocation> _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<DisposableImage> image,
+            Auto<MemoryAllocation> 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 12212a7f40..26f53b3954 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 4f1f0d1653..80b5c0e3f9 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<object> 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 22e8d5c3a6..3cdc424ef6 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)