From faac08e63883c74e8602d2e36a76af954d48eb28 Mon Sep 17 00:00:00 2001
From: Mary <me@thog.eu>
Date: Tue, 4 May 2021 18:19:04 +0200
Subject: [PATCH] gtk3: Add base for future Vulkan integration (#2260)

* gtk3: Add base for future Vulkan integration

This PR puts in place the fondation for the future Vulkan integration on
the GTK3 UI.

This also updated SPB to 0.0.3-build14 that fixed a use after free on
XErrorHandler on Linux.

* Address rip's comments

* Merge GLWidget inside GLRenderer

* Clean up and deduplicate renderer implementations

* Address shahil's comments

* Address Ac_K's comments

* Address gdkchan's comments
---
 Ryujinx/Ryujinx.csproj                 |   2 +-
 Ryujinx/Ui/Applet/GtkHostUiHandler.cs  |   2 +-
 Ryujinx/Ui/GLRenderer.cs               | 644 ++++---------------------
 Ryujinx/Ui/GLWidget.cs                 | 118 -----
 Ryujinx/Ui/MainWindow.cs               | 184 ++++---
 Ryujinx/Ui/RendererWidgetBase.cs       | 584 ++++++++++++++++++++++
 Ryujinx/Ui/SPBOpenGLContext.cs         |   2 +-
 Ryujinx/Ui/VKRenderer.cs               |  80 +++
 Ryujinx/Ui/Windows/ControllerWindow.cs |  12 +-
 9 files changed, 868 insertions(+), 760 deletions(-)
 delete mode 100644 Ryujinx/Ui/GLWidget.cs
 create mode 100644 Ryujinx/Ui/RendererWidgetBase.cs
 create mode 100644 Ryujinx/Ui/VKRenderer.cs

diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index 13442c953..6a441a610 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -18,7 +18,7 @@
     <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="4.4.0-build7" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
     <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
     <PackageReference Include="OpenTK.Graphics" Version="4.5.0" />
-    <PackageReference Include="SPB" Version="0.0.2" />
+    <PackageReference Include="SPB" Version="0.0.3-build15" />
     <PackageReference Include="SharpZipLib" Version="1.3.0" />
   </ItemGroup>
 
diff --git a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs
index 804a1a279..d74ea3d59 100644
--- a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs
+++ b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs
@@ -132,7 +132,7 @@ namespace Ryujinx.Ui.Applet
         public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)
         {
             device.UserChannelPersistence.ExecuteProgram(kind, value);
-            ((MainWindow)_parent).GlRendererWidget?.Exit();
+            ((MainWindow)_parent).RendererWidget?.Exit();
         }
 
         public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs
index 99c4698ac..4c3f8ce40 100644
--- a/Ryujinx/Ui/GLRenderer.cs
+++ b/Ryujinx/Ui/GLRenderer.cs
@@ -1,10 +1,10 @@
 using ARMeilleure.Translation;
 using ARMeilleure.Translation.PTC;
 using Gdk;
+using Gtk;
 using OpenTK.Graphics.OpenGL;
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
-using Ryujinx.Common.Logging;
 using Ryujinx.Configuration;
 using Ryujinx.Graphics.OpenGL;
 using Ryujinx.HLE.HOS.Services.Hid;
@@ -13,9 +13,14 @@ using Ryujinx.Input.HLE;
 using Ryujinx.Ui.Widgets;
 using SPB.Graphics;
 using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Platform.GLX;
+using SPB.Platform.WGL;
+using SPB.Windowing;
 using System;
 using System.Diagnostics;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Threading;
 
 using Key = Ryujinx.Input.Key;
@@ -24,606 +29,123 @@ namespace Ryujinx.Ui
 {
     using Switch = HLE.Switch;
 
-    public class GlRenderer : GLWidget
+    public class GlRenderer : RendererWidgetBase
     {
-        private const int SwitchPanelWidth  = 1280;
-        private const int SwitchPanelHeight = 720;
-        private const int TargetFps         = 60;
-
-        public ManualResetEvent WaitEvent { get; set; }
-        public NpadManager NpadManager { get; }
-
-        public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
-
-        private bool _isActive;
-        private bool _isStopped;
-        private bool _isFocused;
-
-        private double _mouseX;
-        private double _mouseY;
-        private bool   _mousePressed;
-
-        private bool _toggleFullscreen;
-        private bool _toggleDockedMode;
-
-        private readonly long _ticksPerFrame;
-
-        private long _ticks = 0;
-
-        private readonly Stopwatch _chrono;
-
-        private readonly Switch _device;
-
-        private Renderer _renderer;
-
-        private KeyboardHotkeyState _prevHotkeyState;
-
         private GraphicsDebugLevel _glLogLevel;
 
-        private readonly ManualResetEvent _exitEvent;
-        
-        // Hide Cursor
-        const int CursorHideIdleTime = 8; // seconds
-        private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
-        private long _lastCursorMoveTime;
-        private bool _hideCursorOnIdle;
-        private InputManager _inputManager;
-        private IKeyboard _keyboardInterface;
+        private bool _initializedOpenGL;
 
-        public GlRenderer(Switch device, InputManager inputManager, GraphicsDebugLevel glLogLevel)
-            : base (GetGraphicsMode(),
-            3, 3,
-            glLogLevel == GraphicsDebugLevel.None
-            ? OpenGLContextFlags.Compat
-            : OpenGLContextFlags.Compat | OpenGLContextFlags.Debug)
+        private OpenGLContextBase _openGLContext;
+        private SwappableNativeWindowBase _nativeWindow;
+
+        public GlRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel)
         {
-            _inputManager = inputManager;
-            NpadManager = _inputManager.CreateNpadManager();
-            _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
-
-            NpadManager.ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value.ToList());
-
-            WaitEvent = new ManualResetEvent(false);
-
-            _device = device;
-
-            Initialized  += GLRenderer_Initialized;
-            Destroyed    += GLRenderer_Destroyed;
-            ShuttingDown += GLRenderer_ShuttingDown;
-
-            Initialize();
-
-            _chrono = new Stopwatch();
-
-            _ticksPerFrame = Stopwatch.Frequency / TargetFps;
-
-            AddEvents((int)(EventMask.ButtonPressMask
-                          | EventMask.ButtonReleaseMask
-                          | EventMask.PointerMotionMask
-                          | EventMask.KeyPressMask
-                          | EventMask.KeyReleaseMask));
-
-            Shown += Renderer_Shown;
-
             _glLogLevel = glLogLevel;
-
-            _exitEvent = new ManualResetEvent(false);
-
-            _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
-            _lastCursorMoveTime = Stopwatch.GetTimestamp();
-
-            ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorStateChanged;
         }
 
-        private void HideCursorStateChanged(object sender, ReactiveEventArgs<bool> state)
+        protected override bool OnDrawn(Cairo.Context cr)
         {
-            Gtk.Application.Invoke(delegate
+            if (!_initializedOpenGL)
             {
-                _hideCursorOnIdle = state.NewValue;
+                IntializeOpenGL();
+            }
 
-                if (_hideCursorOnIdle)
-                {
-                    _lastCursorMoveTime = Stopwatch.GetTimestamp();
-                }
-                else
-                {
-                    Window.Cursor = null;
-                }
-            });
+            return true;
         }
 
+        private void IntializeOpenGL()
+        {
+            _nativeWindow = RetrieveNativeWindow();
+
+            Window.EnsureNative();
+
+            _openGLContext = PlatformHelper.CreateOpenGLContext(GetGraphicsMode(), 3, 3, _glLogLevel == GraphicsDebugLevel.None ? OpenGLContextFlags.Compat : OpenGLContextFlags.Compat | OpenGLContextFlags.Debug);
+            _openGLContext.Initialize(_nativeWindow);
+            _openGLContext.MakeCurrent(_nativeWindow);
+
+            // Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread.
+            _openGLContext.MakeCurrent(null);
+
+            WaitEvent.Set();
+
+            _initializedOpenGL = true;
+        }
+
+        private SwappableNativeWindowBase RetrieveNativeWindow()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
+
+                return new WGLWindow(new NativeHandle(windowHandle));
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
+                IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
+
+                return new GLXWindow(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
+            }
+
+            throw new NotImplementedException();
+        }
+
+        [DllImport("libgdk-3-0.dll")]
+        private static extern IntPtr gdk_win32_window_get_handle(IntPtr d);
+
+        [DllImport("libgdk-3.so.0")]
+        private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
+
+        [DllImport("libgdk-3.so.0")]
+        private static extern IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
+
         private static FramebufferFormat GetGraphicsMode()
         {
             return Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default;
         }
 
-        private void GLRenderer_ShuttingDown(object sender, EventArgs args)
-        {
-            _device.DisposeGpu();
-            NpadManager.Dispose();
-        }
-
-        private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
-        {
-            _isFocused = false;
-        }
-
-        private void Parent_FocusInEvent(object o, Gtk.FocusInEventArgs args)
-        {
-            _isFocused = true;
-        }
-
-        private void GLRenderer_Destroyed(object sender, EventArgs e)
-        {
-            ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
-
-            NpadManager.Dispose();
-            Dispose();
-        }
-
-        protected void Renderer_Shown(object sender, EventArgs e)
-        {
-            _isFocused = this.ParentWindow.State.HasFlag(Gdk.WindowState.Focused);
-        }
-
-        public void HandleScreenState(KeyboardStateSnapshot keyboard)
-        {
-            bool toggleFullscreen =  keyboard.IsPressed(Key.F11)
-                                || ((keyboard.IsPressed(Key.AltLeft)
-                                ||   keyboard.IsPressed(Key.AltRight))
-                                &&   keyboard.IsPressed(Key.Enter))
-                                ||   keyboard.IsPressed(Key.Escape);
-
-            bool fullScreenToggled = ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen);
-
-            if (toggleFullscreen != _toggleFullscreen)
-            {
-                if (toggleFullscreen)
-                {
-                    if (fullScreenToggled)
-                    {
-                        ParentWindow.Unfullscreen();
-                        (Toplevel as MainWindow)?.ToggleExtraWidgets(true);
-                    }
-                    else
-                    {
-                        if (keyboard.IsPressed(Key.Escape))
-                        {
-                            if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
-                            {
-                                Exit();
-                            }
-                        }
-                        else
-                        {
-                            ParentWindow.Fullscreen();
-                            (Toplevel as MainWindow)?.ToggleExtraWidgets(false);
-                        }
-                    }
-                }
-            }
-
-            _toggleFullscreen = toggleFullscreen;
-
-            bool toggleDockedMode = keyboard.IsPressed(Key.F9);
-
-            if (toggleDockedMode != _toggleDockedMode)
-            {
-                if (toggleDockedMode)
-                {
-                    ConfigurationState.Instance.System.EnableDockedMode.Value =
-                        !ConfigurationState.Instance.System.EnableDockedMode.Value;
-                }
-            }
-
-            _toggleDockedMode = toggleDockedMode;
-
-            if (_hideCursorOnIdle)
-            {
-                long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
-                Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
-            }
-        }
-
-        private void GLRenderer_Initialized(object sender, EventArgs e)
-        {
-            // Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread.
-            OpenGLContext.MakeCurrent(null);
-
-            WaitEvent.Set();
-        }
-
-        protected override bool OnConfigureEvent(EventConfigure evnt)
-        {
-            bool result = base.OnConfigureEvent(evnt);
-
-            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
-
-            _renderer.Window.SetSize(evnt.Width * monitor.ScaleFactor, evnt.Height * monitor.ScaleFactor);
-
-            return result;
-        }
-
-        public void Start()
-        {
-            _chrono.Restart();
-
-            _isActive = true;
-
-            Gtk.Window parent = this.Toplevel as Gtk.Window;
-
-            parent.FocusInEvent  += Parent_FocusInEvent;
-            parent.FocusOutEvent += Parent_FocusOutEvent;
-
-            Gtk.Application.Invoke(delegate
-            {
-                parent.Present();
-
-                string titleNameSection = string.IsNullOrWhiteSpace(_device.Application.TitleName) ? string.Empty
-                    : $" - {_device.Application.TitleName}";
-
-                string titleVersionSection = string.IsNullOrWhiteSpace(_device.Application.DisplayVersion) ? string.Empty
-                    : $" v{_device.Application.DisplayVersion}";
-
-                string titleIdSection = string.IsNullOrWhiteSpace(_device.Application.TitleIdText) ? string.Empty
-                    : $" ({_device.Application.TitleIdText.ToUpper()})";
-
-                string titleArchSection = _device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
-
-                parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
-            });
-
-            Thread renderLoopThread = new Thread(Render)
-            {
-                Name = "GUI.RenderLoop"
-            };
-            renderLoopThread.Start();
-
-            Thread nvStutterWorkaround = new Thread(NVStutterWorkaround)
-            {
-                Name = "GUI.NVStutterWorkaround"
-            };
-            nvStutterWorkaround.Start();
-
-            MainLoop();
-
-            renderLoopThread.Join();
-            nvStutterWorkaround.Join();
-
-            Exit();
-        }
-
-        private void NVStutterWorkaround()
-        {
-            while (_isActive)
-            {
-                // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
-                // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
-                // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
-                // This creates a new thread every second or so.
-                // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
-                // This is a little over budget on a frame time of 16ms, so creates a large stutter.
-                // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
-
-                // TODO: This should be removed when the issue with the GateThread is resolved.
-
-                ThreadPool.QueueUserWorkItem((state) => { });
-                Thread.Sleep(300);
-            }
-        }
-
-        protected override bool OnButtonPressEvent(EventButton evnt)
-        {
-            _mouseX = evnt.X;
-            _mouseY = evnt.Y;
-
-            if (evnt.Button == 1)
-            {
-                _mousePressed = true;
-            }
-
-            return false;
-        }
-
-        protected override bool OnButtonReleaseEvent(EventButton evnt)
-        {
-            if (evnt.Button == 1)
-            {
-                _mousePressed = false;
-            }
-
-            return false;
-        }
-
-        protected override bool OnMotionNotifyEvent(EventMotion evnt)
-        {
-            if (evnt.Device.InputSource == InputSource.Mouse)
-            {
-                _mouseX = evnt.X;
-                _mouseY = evnt.Y;
-            }
-
-            if (_hideCursorOnIdle)
-            {
-                _lastCursorMoveTime = Stopwatch.GetTimestamp();
-            } 
-
-            return false;
-        }
-
-        protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight)
-        {
-            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
-
-            // If the monitor is at least 1080p, use the Switch panel size as minimal size.
-            if (monitor.Geometry.Height >= 1080)
-            {
-                minimumHeight = SwitchPanelHeight;
-            }
-            // Otherwise, we default minimal size to 480p 16:9.
-            else
-            {
-                minimumHeight = 480;
-            }
-
-            naturalHeight = minimumHeight;
-        }
-
-        protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth)
-        {
-            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
-
-            // If the monitor is at least 1080p, use the Switch panel size as minimal size.
-            if (monitor.Geometry.Height >= 1080)
-            {
-                minimumWidth = SwitchPanelWidth;
-            }
-            // Otherwise, we default minimal size to 480p 16:9.
-            else
-            {
-                minimumWidth = 854;
-            }
-
-            naturalWidth = minimumWidth;
-        }
-
-        public void Exit()
-        {
-            NpadManager?.Dispose();
-
-            if (_isStopped)
-            {
-                return;
-            }
-
-            _isStopped = true;
-            _isActive  = false;
-
-            _exitEvent.WaitOne();
-            _exitEvent.Dispose();
-        }
-
-        public void Initialize()
-        {
-            if (!(_device.Gpu.Renderer is Renderer))
-            {
-                throw new NotSupportedException($"GPU renderer must be an OpenGL renderer when using {typeof(Renderer).Name}!");
-            }
-
-            _renderer = (Renderer)_device.Gpu.Renderer;
-        }
-
-        public void Render()
+        public override void InitializeRenderer()
         {
             // First take exclusivity on the OpenGL context.
-            _renderer.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(OpenGLContext));
+            ((Renderer)Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(_openGLContext));
 
-            Gtk.Window parent = Toplevel as Gtk.Window;
-            parent.Present();
+            _openGLContext.MakeCurrent(_nativeWindow);
 
-            OpenGLContext.MakeCurrent(NativeWindow);
-
-            _device.Gpu.Renderer.Initialize(_glLogLevel);
-
-            // Make sure the first frame is not transparent.
             GL.ClearColor(0, 0, 0, 1.0f);
             GL.Clear(ClearBufferMask.ColorBufferBit);
             SwapBuffers();
-
-            _device.Gpu.InitializeShaderCache();
-            Translator.IsReadyForTranslation.Set();
-
-            while (_isActive)
-            {
-                if (_isStopped)
-                {
-                    return;
-                }
-
-                _ticks += _chrono.ElapsedTicks;
-
-                _chrono.Restart();
-
-                if (_device.WaitFifo())
-                {
-                    _device.Statistics.RecordFifoStart();
-                    _device.ProcessFrame();
-                    _device.Statistics.RecordFifoEnd();
-                }
-
-                while (_device.ConsumeFrameAvailable())
-                {
-                    _device.PresentFrame(SwapBuffers);
-                }
-
-                if (_ticks >= _ticksPerFrame)
-                {
-                    string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
-                    float scale = Graphics.Gpu.GraphicsConfig.ResScale;
-                    if (scale != 1)
-                    {
-                        dockedMode += $" ({scale}x)";
-                    }
-
-                    StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
-                        _device.EnableDeviceVsync,
-                        dockedMode,
-                        ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
-                        $"Game: {_device.Statistics.GetGameFrameRate():00.00} FPS",
-                        $"FIFO: {_device.Statistics.GetFifoPercent():0.00} %",
-                        $"GPU:  {_renderer.GpuVendor}"));
-
-                    _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
-                }
-            }
         }
 
-        public void SwapBuffers()
+        public override void SwapBuffers()
         {
-            NativeWindow.SwapBuffers();
+            _nativeWindow.SwapBuffers();
         }
 
-        public void MainLoop()
+        public override string GetGpuVendorName()
         {
-            while (_isActive)
-            {
-                UpdateFrame();
-
-                // Polling becomes expensive if it's not slept
-                Thread.Sleep(1);
-            }
-
-            _exitEvent.Set();
+            return ((Renderer)Renderer).GpuVendor;
         }
 
-        private bool UpdateFrame()
+        protected override void Dispose(bool disposing)
         {
-            if (!_isActive)
+            // Try to bind the OpenGL context before calling the shutdown event
+            try
             {
-                return true;
+                _openGLContext?.MakeCurrent(_nativeWindow);
             }
+            catch (Exception) { }
 
-            if (_isStopped)
+            Device.DisposeGpu();
+            NpadManager.Dispose();
+
+            // Unbind context and destroy everything
+            try
             {
-                return false;
+                _openGLContext?.MakeCurrent(null);
             }
+            catch (Exception) { }
 
-            if (_isFocused)
-            {
-                Gtk.Application.Invoke(delegate
-                {
-                    KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
-
-                    HandleScreenState(keyboard);
-
-                    if (keyboard.IsPressed(Key.Delete))
-                    {
-                        if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
-                        {
-                            Ptc.Continue();
-                        }
-                    }
-                });
-            }
-
-            NpadManager.Update(_device.Hid, _device.TamperMachine);
-
-            if(_isFocused)
-            {
-                KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
-
-                if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
-                    !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
-                {
-                    _device.EnableDeviceVsync = !_device.EnableDeviceVsync;
-                }
-
-                _prevHotkeyState = currentHotkeyState;
-            }
-
-            //Touchscreen
-            bool hasTouch = false;
-
-            // Get screen touch position from left mouse click
-            // OpenTK always captures mouse events, even if out of focus, so check if window is focused.
-            if (_isFocused && _mousePressed)
-            {
-                float aspectWidth = SwitchPanelHeight * ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat();
-
-                int screenWidth  = AllocatedWidth;
-                int screenHeight = AllocatedHeight;
-
-                if (AllocatedWidth > AllocatedHeight * aspectWidth / SwitchPanelHeight)
-                {
-                    screenWidth = (int)(AllocatedHeight * aspectWidth) / SwitchPanelHeight;
-                }
-                else
-                {
-                    screenHeight = (AllocatedWidth * SwitchPanelHeight) / (int)aspectWidth;
-                }
-
-                int startX = (AllocatedWidth  - screenWidth)  >> 1;
-                int startY = (AllocatedHeight - screenHeight) >> 1;
-
-                int endX = startX + screenWidth;
-                int endY = startY + screenHeight;
-
-
-                if (_mouseX >= startX &&
-                    _mouseY >= startY &&
-                    _mouseX < endX &&
-                    _mouseY < endY)
-                {
-                    int screenMouseX = (int)_mouseX - startX;
-                    int screenMouseY = (int)_mouseY - startY;
-
-                    int mX = (screenMouseX * (int)aspectWidth)  / screenWidth;
-                    int mY = (screenMouseY * SwitchPanelHeight) / screenHeight;
-
-                    TouchPoint currentPoint = new TouchPoint
-                    {
-                        X = (uint)mX,
-                        Y = (uint)mY,
-
-                        // Placeholder values till more data is acquired
-                        DiameterX = 10,
-                        DiameterY = 10,
-                        Angle     = 90
-                    };
-
-                    hasTouch = true;
-
-                    _device.Hid.Touchscreen.Update(currentPoint);
-                }
-            }
-
-            if (!hasTouch)
-            {
-                _device.Hid.Touchscreen.Update();
-            }
-
-            _device.Hid.DebugPad.Update();
-
-            return true;
-        }
-
-        [Flags]
-        private enum KeyboardHotkeyState
-        {
-            None,
-            ToggleVSync
-        }
-
-        private KeyboardHotkeyState GetHotkeyState()
-        {
-            KeyboardHotkeyState state = KeyboardHotkeyState.None;
-
-            if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
-            {
-                state |= KeyboardHotkeyState.ToggleVSync;
-            }
-
-            return state;
+            _openGLContext.Dispose();
         }
     }
 }
diff --git a/Ryujinx/Ui/GLWidget.cs b/Ryujinx/Ui/GLWidget.cs
deleted file mode 100644
index a465aeef2..000000000
--- a/Ryujinx/Ui/GLWidget.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-using Gtk;
-using SPB.Graphics;
-using SPB.Graphics.OpenGL;
-using SPB.Platform;
-using SPB.Platform.GLX;
-using SPB.Platform.WGL;
-using SPB.Windowing;
-using System;
-using System.ComponentModel;
-using System.Runtime.InteropServices;
-
-namespace Ryujinx.Ui
-{
-    [ToolboxItem(true)]
-    public class GLWidget : DrawingArea
-    {
-        private bool _initialized;
-
-        public event EventHandler Initialized;
-        public event EventHandler ShuttingDown;
-
-        public OpenGLContextBase OpenGLContext { get; private set; }
-        public NativeWindowBase NativeWindow { get; private set; }
-
-        public FramebufferFormat FramebufferFormat { get; }
-        public int GLVersionMajor { get; }
-        public int GLVersionMinor { get; }
-        public OpenGLContextFlags ContextFlags { get; }
-
-        public bool DirectRendering { get; }
-        public OpenGLContextBase SharedContext { get; }
-
-        public GLWidget(FramebufferFormat framebufferFormat, int major, int minor, OpenGLContextFlags flags = OpenGLContextFlags.Default, bool directRendering = true, OpenGLContextBase sharedContext = null)
-        {
-            FramebufferFormat = framebufferFormat;
-            GLVersionMajor = major;
-            GLVersionMinor = minor;
-            ContextFlags = flags;
-            DirectRendering = directRendering;
-            SharedContext = sharedContext;
-        }
-
-        protected override bool OnDrawn(Cairo.Context cr)
-        {
-            if (!_initialized)
-            {
-                Intialize();
-            }
-
-            return true;
-        }
-
-        private NativeWindowBase RetrieveNativeWindow()
-        {
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
-
-                return new WGLWindow(new NativeHandle(windowHandle));
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
-            {
-                IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
-                IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
-
-                return new GLXWindow(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
-            }
-
-            throw new NotImplementedException();
-        }
-
-        [DllImport("libgdk-3-0.dll")]
-        private static extern IntPtr gdk_win32_window_get_handle(IntPtr d);
-
-        [DllImport("libgdk-3.so.0")]
-        private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
-
-        [DllImport("libgdk-3.so.0")]
-        private static extern IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
-
-        private void Intialize()
-        {
-            NativeWindow = RetrieveNativeWindow();
-
-            Window.EnsureNative();
-
-            OpenGLContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat, GLVersionMajor, GLVersionMinor, ContextFlags, DirectRendering, SharedContext);
-
-            OpenGLContext.Initialize(NativeWindow);
-            OpenGLContext.MakeCurrent(NativeWindow);
-
-            _initialized = true;
-
-            Initialized?.Invoke(this, EventArgs.Empty);
-        }
-
-        protected override void Dispose(bool disposing)
-        {
-            // Try to bind the OpenGL context before calling the shutdown event
-            try
-            {
-                OpenGLContext?.MakeCurrent(NativeWindow);
-            }
-            catch (Exception) { }
-
-            ShuttingDown?.Invoke(this, EventArgs.Empty);
-
-            // Unbind context and destroy everything
-            try
-            {
-                OpenGLContext?.MakeCurrent(null);
-            }
-            catch (Exception) { }
-
-            OpenGLContext.Dispose();
-        }
-    }
-}
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 08527ea35..56dcf3ebc 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -67,9 +67,11 @@ namespace Ryujinx.Ui
         private string _lastScannedAmiiboId = "";
         private bool   _lastScannedAmiiboShowAll = false;
 
-        public GlRenderer GlRendererWidget;
+        public RendererWidgetBase RendererWidget;
         public InputManager InputManager;
 
+        private static bool UseVulkan = false;
+
 #pragma warning disable CS0169, CS0649, IDE0044
 
         [GUI] public MenuItem ExitMenuItem;
@@ -161,7 +163,7 @@ namespace Ryujinx.Ui
             _gameTable.ButtonReleaseEvent += Row_Clicked;
             _fullScreen.Activated         += FullScreen_Toggled;
 
-            GlRenderer.StatusUpdatedEvent += Update_StatusBar;
+            RendererWidgetBase.StatusUpdatedEvent += Update_StatusBar;
 
             if (ConfigurationState.Instance.Ui.StartFullscreen)
             {
@@ -312,7 +314,17 @@ namespace Ryujinx.Ui
         {
             _virtualFileSystem.Reload();
 
-            IRenderer renderer = new Renderer();
+            IRenderer renderer;
+
+            if (UseVulkan)
+            {
+                throw new NotImplementedException();
+            }
+            else
+            {
+                renderer = new Renderer();
+            }
+
             IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver();
 
             if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo)
@@ -482,6 +494,10 @@ namespace Ryujinx.Ui
 
                 Logger.RestartTime();
 
+                RendererWidget = CreateRendererWidget();
+
+                SwitchToRenderWidget();
+
                 InitializeSwitchInstance();
 
                 UpdateGraphicsConfig();
@@ -507,6 +523,8 @@ namespace Ryujinx.Ui
                                 UserErrorDialog.CreateUserErrorDialog(userError);
 
                                 _emulationContext.Dispose();
+                                SwitchToGameTable();
+                                RendererWidget.Dispose();
 
                                 return;
                             }
@@ -517,6 +535,8 @@ namespace Ryujinx.Ui
                             UserErrorDialog.CreateUserErrorDialog(userError);
 
                             _emulationContext.Dispose();
+                            SwitchToGameTable();
+                            RendererWidget.Dispose();
 
                             return;
                         }
@@ -538,6 +558,8 @@ namespace Ryujinx.Ui
                         UserErrorDialog.CreateUserErrorDialog(userError);
 
                         _emulationContext.Dispose();
+                        SwitchToGameTable();
+                        RendererWidget.Dispose();
 
                         return;
                     }
@@ -600,6 +622,7 @@ namespace Ryujinx.Ui
                     Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
 
                     _emulationContext.Dispose();
+                    RendererWidget.Dispose();
 
                     return;
                 }
@@ -640,6 +663,83 @@ namespace Ryujinx.Ui
             }
         }
 
+        private RendererWidgetBase CreateRendererWidget()
+        {
+            if (UseVulkan)
+            {
+                return new VKRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
+            }
+            else
+            {
+                return new GlRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
+            }
+        }
+
+        private void SwitchToRenderWidget()
+        {
+            _viewBox.Remove(_gameTableWindow);
+            RendererWidget.Expand = true;
+            _viewBox.Child = RendererWidget;
+
+            RendererWidget.ShowAll();
+            EditFooterForGameRenderer();
+
+            if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
+            {
+                ToggleExtraWidgets(false);
+            }
+            else if (ConfigurationState.Instance.Ui.StartFullscreen.Value)
+            {
+                FullScreen_Toggled(null, null);
+            }
+        }
+
+        private void SwitchToGameTable()
+        {
+            if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
+            {
+                ToggleExtraWidgets(true);
+            }
+
+            RendererWidget.Exit();
+
+            if (RendererWidget.Window != Window && RendererWidget.Window != null)
+            {
+                RendererWidget.Window.Dispose();
+            }
+
+            RendererWidget.Dispose();
+
+            _windowsMultimediaTimerResolution?.Dispose();
+            _windowsMultimediaTimerResolution = null;
+            DisplaySleep.Restore();
+
+            _viewBox.Remove(RendererWidget);
+            _viewBox.Add(_gameTableWindow);
+
+            _gameTableWindow.Expand = true;
+
+            Window.Title = $"Ryujinx {Program.Version}";
+
+            _emulationContext = null;
+            _gameLoaded = false;
+            RendererWidget = null;
+
+            DiscordIntegrationModule.SwitchToMainMenu();
+
+            RecreateFooterForMenu();
+
+            UpdateColumns();
+            UpdateGameTable();
+
+            Task.Run(RefreshFirmwareLabel);
+            Task.Run(HandleRelaunch);
+
+            _actionMenu.Sensitive = false;
+            _firmwareInstallFile.Sensitive = true;
+            _firmwareInstallDirectory.Sensitive = true;
+        }
+
         private void CreateGameWindow()
         {
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -649,30 +749,11 @@ namespace Ryujinx.Ui
 
             DisplaySleep.Prevent();
 
-            GlRendererWidget = new GlRenderer(_emulationContext, InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
+            RendererWidget.Initialize(_emulationContext);
 
-            Application.Invoke(delegate
-            {
-                _viewBox.Remove(_gameTableWindow);
-                GlRendererWidget.Expand = true;
-                _viewBox.Child = GlRendererWidget;
+            RendererWidget.WaitEvent.WaitOne();
 
-                GlRendererWidget.ShowAll();
-                EditFooterForGameRenderer();
-
-                if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
-                {
-                    ToggleExtraWidgets(false);
-                }
-                else if (ConfigurationState.Instance.Ui.StartFullscreen.Value)
-                {
-                    FullScreen_Toggled(null, null);
-                }
-            });
-
-            GlRendererWidget.WaitEvent.WaitOne();
-
-            GlRendererWidget.Start();
+            RendererWidget.Start();
 
             Ptc.Close();
             PtcProfiler.Stop();
@@ -683,48 +764,7 @@ namespace Ryujinx.Ui
             // NOTE: Everything that is here will not be executed when you close the UI.
             Application.Invoke(delegate
             {
-                if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
-                {
-                    ToggleExtraWidgets(true);
-                }
-
-                GlRendererWidget.Exit();
-
-                if (GlRendererWidget.Window != Window && GlRendererWidget.Window != null)
-                {
-                    GlRendererWidget.Window.Dispose();
-                }
-
-                GlRendererWidget.Dispose();
-
-                _windowsMultimediaTimerResolution?.Dispose();
-                _windowsMultimediaTimerResolution = null;
-                DisplaySleep.Restore();
-
-                _viewBox.Remove(GlRendererWidget);
-                _viewBox.Add(_gameTableWindow);
-
-                _gameTableWindow.Expand = true;
-
-                Window.Title = $"Ryujinx {Program.Version}";
-
-                _emulationContext = null;
-                _gameLoaded       = false;
-                GlRendererWidget  = null;
-
-                DiscordIntegrationModule.SwitchToMainMenu();
-
-                RecreateFooterForMenu();
-
-                UpdateColumns();
-                UpdateGameTable();
-
-                Task.Run(RefreshFirmwareLabel);
-                Task.Run(HandleRelaunch);
-
-                _actionMenu.Sensitive               = false;
-                _firmwareInstallFile.Sensitive      = true;
-                _firmwareInstallDirectory.Sensitive = true;
+                SwitchToGameTable();
             });
         }
 
@@ -742,7 +782,7 @@ namespace Ryujinx.Ui
 
         public void ToggleExtraWidgets(bool show)
         {
-            if (GlRendererWidget != null)
+            if (RendererWidget != null)
             {
                 if (show)
                 {
@@ -801,14 +841,14 @@ namespace Ryujinx.Ui
             {
                 UpdateGameMetadata(_emulationContext.Application.TitleIdText);
 
-                if (GlRendererWidget != null)
+                if (RendererWidget != null)
                 {
                     // We tell the widget that we are exiting.
-                    GlRendererWidget.Exit();
+                    RendererWidget.Exit();
 
                     // Wait for the other thread to dispose the HLE context before exiting.
                     _deviceExitStatus.WaitOne();
-                    GlRendererWidget.Dispose();
+                    RendererWidget.Dispose();
                 }
             }
 
@@ -1027,7 +1067,7 @@ namespace Ryujinx.Ui
 
         private void StopEmulation_Pressed(object sender, EventArgs args)
         {
-            GlRendererWidget?.Exit();
+            RendererWidget?.Exit();
         }
 
         private void Installer_File_Pressed(object o, EventArgs args)
diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs
new file mode 100644
index 000000000..5270e31e1
--- /dev/null
+++ b/Ryujinx/Ui/RendererWidgetBase.cs
@@ -0,0 +1,584 @@
+using ARMeilleure.Translation;
+using ARMeilleure.Translation.PTC;
+using Gdk;
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Configuration;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.HLE.HOS.Services.Hid;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+
+namespace Ryujinx.Ui
+{
+    using Key = Input.Key;
+    using Switch = HLE.Switch;
+
+    public abstract class RendererWidgetBase : DrawingArea
+    {
+        private const int SwitchPanelWidth = 1280;
+        private const int SwitchPanelHeight = 720;
+        private const int TargetFps = 60;
+
+        public ManualResetEvent WaitEvent { get; set; }
+        public NpadManager NpadManager { get; }
+        public Switch Device { get; private set; }
+        public IRenderer Renderer { get; private set; }
+
+        public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
+
+        private bool _isActive;
+        private bool _isStopped;
+        private bool _isFocused;
+
+        private double _mouseX;
+        private double _mouseY;
+        private bool _mousePressed;
+
+        private bool _toggleFullscreen;
+        private bool _toggleDockedMode;
+
+        private readonly long _ticksPerFrame;
+
+        private long _ticks = 0;
+
+        private readonly Stopwatch _chrono;
+
+        private KeyboardHotkeyState _prevHotkeyState;
+
+        private readonly ManualResetEvent _exitEvent;
+
+        // Hide Cursor
+        const int CursorHideIdleTime = 8; // seconds
+        private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
+        private long _lastCursorMoveTime;
+        private bool _hideCursorOnIdle;
+        private InputManager _inputManager;
+        private IKeyboard _keyboardInterface;
+        private GraphicsDebugLevel _glLogLevel;
+        private string _gpuVendorName;
+
+        public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel)
+        {
+            _inputManager = inputManager;
+            NpadManager = _inputManager.CreateNpadManager();
+            _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+
+            NpadManager.ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value.ToList());
+
+            WaitEvent = new ManualResetEvent(false);
+
+            _glLogLevel = glLogLevel;
+
+            Destroyed += Renderer_Destroyed;
+
+            _chrono = new Stopwatch();
+
+            _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+
+            AddEvents((int)(EventMask.ButtonPressMask
+                          | EventMask.ButtonReleaseMask
+                          | EventMask.PointerMotionMask
+                          | EventMask.KeyPressMask
+                          | EventMask.KeyReleaseMask));
+
+            Shown += Renderer_Shown;
+
+            _exitEvent = new ManualResetEvent(false);
+
+            _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
+            _lastCursorMoveTime = Stopwatch.GetTimestamp();
+
+            ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorStateChanged;
+        }
+
+        public abstract void InitializeRenderer();
+
+        public abstract void SwapBuffers();
+
+        public abstract string GetGpuVendorName();
+
+        private void HideCursorStateChanged(object sender, ReactiveEventArgs<bool> state)
+        {
+            Gtk.Application.Invoke(delegate
+            {
+                _hideCursorOnIdle = state.NewValue;
+
+                if (_hideCursorOnIdle)
+                {
+                    _lastCursorMoveTime = Stopwatch.GetTimestamp();
+                }
+                else
+                {
+                    Window.Cursor = null;
+                }
+            });
+        }
+
+        private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
+        {
+            _isFocused = false;
+        }
+
+        private void Parent_FocusInEvent(object o, Gtk.FocusInEventArgs args)
+        {
+            _isFocused = true;
+        }
+
+        private void Renderer_Destroyed(object sender, EventArgs e)
+        {
+            ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
+
+            NpadManager.Dispose();
+            Dispose();
+        }
+
+        private void Renderer_Shown(object sender, EventArgs e)
+        {
+            _isFocused = ParentWindow.State.HasFlag(Gdk.WindowState.Focused);
+        }
+
+        protected override bool OnButtonPressEvent(EventButton evnt)
+        {
+            _mouseX = evnt.X;
+            _mouseY = evnt.Y;
+
+            if (evnt.Button == 1)
+            {
+                _mousePressed = true;
+            }
+
+            return false;
+        }
+
+        protected override bool OnButtonReleaseEvent(EventButton evnt)
+        {
+            if (evnt.Button == 1)
+            {
+                _mousePressed = false;
+            }
+
+            return false;
+        }
+
+        protected override bool OnMotionNotifyEvent(EventMotion evnt)
+        {
+            if (evnt.Device.InputSource == InputSource.Mouse)
+            {
+                _mouseX = evnt.X;
+                _mouseY = evnt.Y;
+            }
+
+            if (_hideCursorOnIdle)
+            {
+                _lastCursorMoveTime = Stopwatch.GetTimestamp();
+            }
+
+            return false;
+        }
+
+        protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight)
+        {
+            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+            // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+            if (monitor.Geometry.Height >= 1080)
+            {
+                minimumHeight = SwitchPanelHeight;
+            }
+            // Otherwise, we default minimal size to 480p 16:9.
+            else
+            {
+                minimumHeight = 480;
+            }
+
+            naturalHeight = minimumHeight;
+        }
+
+        protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth)
+        {
+            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+            // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+            if (monitor.Geometry.Height >= 1080)
+            {
+                minimumWidth = SwitchPanelWidth;
+            }
+            // Otherwise, we default minimal size to 480p 16:9.
+            else
+            {
+                minimumWidth = 854;
+            }
+
+            naturalWidth = minimumWidth;
+        }
+
+        protected override bool OnConfigureEvent(EventConfigure evnt)
+        {
+            bool result = base.OnConfigureEvent(evnt);
+
+            Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+            Renderer?.Window.SetSize(evnt.Width * monitor.ScaleFactor, evnt.Height * monitor.ScaleFactor);
+
+            return result;
+        }
+
+        private void HandleScreenState(KeyboardStateSnapshot keyboard)
+        {
+            bool toggleFullscreen = keyboard.IsPressed(Key.F11)
+                                || ((keyboard.IsPressed(Key.AltLeft)
+                                || keyboard.IsPressed(Key.AltRight))
+                                && keyboard.IsPressed(Key.Enter))
+                                || keyboard.IsPressed(Key.Escape);
+
+            bool fullScreenToggled = ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen);
+
+            if (toggleFullscreen != _toggleFullscreen)
+            {
+                if (toggleFullscreen)
+                {
+                    if (fullScreenToggled)
+                    {
+                        ParentWindow.Unfullscreen();
+                        (Toplevel as MainWindow)?.ToggleExtraWidgets(true);
+                    }
+                    else
+                    {
+                        if (keyboard.IsPressed(Key.Escape))
+                        {
+                            if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
+                            {
+                                Exit();
+                            }
+                        }
+                        else
+                        {
+                            ParentWindow.Fullscreen();
+                            (Toplevel as MainWindow)?.ToggleExtraWidgets(false);
+                        }
+                    }
+                }
+            }
+
+            _toggleFullscreen = toggleFullscreen;
+
+            bool toggleDockedMode = keyboard.IsPressed(Key.F9);
+
+            if (toggleDockedMode != _toggleDockedMode)
+            {
+                if (toggleDockedMode)
+                {
+                    ConfigurationState.Instance.System.EnableDockedMode.Value =
+                        !ConfigurationState.Instance.System.EnableDockedMode.Value;
+                }
+            }
+
+            _toggleDockedMode = toggleDockedMode;
+
+            if (_hideCursorOnIdle)
+            {
+                long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+                Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
+            }
+        }
+
+        public void Initialize(Switch device)
+        {
+            Device = device;
+            Renderer = Device.Gpu.Renderer;
+        }
+
+        public void Render()
+        {
+            Gtk.Window parent = Toplevel as Gtk.Window;
+            parent.Present();
+
+            InitializeRenderer();
+
+            Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+            _gpuVendorName = GetGpuVendorName();
+
+            Device.Gpu.InitializeShaderCache();
+            Translator.IsReadyForTranslation.Set();
+
+            while (_isActive)
+            {
+                if (_isStopped)
+                {
+                    return;
+                }
+
+                _ticks += _chrono.ElapsedTicks;
+
+                _chrono.Restart();
+
+                if (Device.WaitFifo())
+                {
+                    Device.Statistics.RecordFifoStart();
+                    Device.ProcessFrame();
+                    Device.Statistics.RecordFifoEnd();
+                }
+
+                while (Device.ConsumeFrameAvailable())
+                {
+                    Device.PresentFrame(SwapBuffers);
+                }
+
+                if (_ticks >= _ticksPerFrame)
+                {
+                    string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
+                    float scale = Graphics.Gpu.GraphicsConfig.ResScale;
+                    if (scale != 1)
+                    {
+                        dockedMode += $" ({scale}x)";
+                    }
+
+                    StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+                        Device.EnableDeviceVsync,
+                        dockedMode,
+                        ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
+                        $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS",
+                        $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
+                        $"GPU: {_gpuVendorName}"));
+
+                    _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
+                }
+            }
+        }
+
+        public void Start()
+        {
+            _chrono.Restart();
+
+            _isActive = true;
+
+            Gtk.Window parent = this.Toplevel as Gtk.Window;
+
+            parent.FocusInEvent += Parent_FocusInEvent;
+            parent.FocusOutEvent += Parent_FocusOutEvent;
+
+            Application.Invoke(delegate
+            {
+                parent.Present();
+
+                string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty
+                    : $" - {Device.Application.TitleName}";
+
+                string titleVersionSection = string.IsNullOrWhiteSpace(Device.Application.DisplayVersion) ? string.Empty
+                    : $" v{Device.Application.DisplayVersion}";
+
+                string titleIdSection = string.IsNullOrWhiteSpace(Device.Application.TitleIdText) ? string.Empty
+                    : $" ({Device.Application.TitleIdText.ToUpper()})";
+
+                string titleArchSection = Device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
+
+                parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
+            });
+
+            Thread renderLoopThread = new Thread(Render)
+            {
+                Name = "GUI.RenderLoop"
+            };
+            renderLoopThread.Start();
+
+            Thread nvStutterWorkaround = new Thread(NVStutterWorkaround)
+            {
+                Name = "GUI.NVStutterWorkaround"
+            };
+            nvStutterWorkaround.Start();
+
+            MainLoop();
+
+            renderLoopThread.Join();
+            nvStutterWorkaround.Join();
+
+            Exit();
+        }
+
+        public void Exit()
+        {
+            NpadManager?.Dispose();
+
+            if (_isStopped)
+            {
+                return;
+            }
+
+            _isStopped = true;
+            _isActive = false;
+
+            _exitEvent.WaitOne();
+            _exitEvent.Dispose();
+        }
+
+        private void NVStutterWorkaround()
+        {
+            while (_isActive)
+            {
+                // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
+                // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
+                // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
+                // This creates a new thread every second or so.
+                // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
+                // This is a little over budget on a frame time of 16ms, so creates a large stutter.
+                // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
+
+                // TODO: This should be removed when the issue with the GateThread is resolved.
+
+                ThreadPool.QueueUserWorkItem((state) => { });
+                Thread.Sleep(300);
+            }
+        }
+
+        public void MainLoop()
+        {
+            while (_isActive)
+            {
+                UpdateFrame();
+
+                // Polling becomes expensive if it's not slept
+                Thread.Sleep(1);
+            }
+
+            _exitEvent.Set();
+        }
+
+        private bool UpdateFrame()
+        {
+            if (!_isActive)
+            {
+                return true;
+            }
+
+            if (_isStopped)
+            {
+                return false;
+            }
+
+            if (_isFocused)
+            {
+                Gtk.Application.Invoke(delegate
+                {
+                    KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
+
+                    HandleScreenState(keyboard);
+
+                    if (keyboard.IsPressed(Key.Delete))
+                    {
+                        if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
+                        {
+                            Ptc.Continue();
+                        }
+                    }
+                });
+            }
+
+            NpadManager.Update(Device.Hid, Device.TamperMachine);
+
+            if (_isFocused)
+            {
+                KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
+
+                if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
+                    !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
+                {
+                    Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
+                }
+
+                _prevHotkeyState = currentHotkeyState;
+            }
+
+            // Touchscreen
+            bool hasTouch = false;
+
+            // Get screen touch position from left mouse click
+            // OpenTK always captures mouse events, even if out of focus, so check if window is focused.
+            if (_isFocused && _mousePressed)
+            {
+                float aspectWidth = SwitchPanelHeight * ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat();
+
+                int screenWidth = AllocatedWidth;
+                int screenHeight = AllocatedHeight;
+
+                if (AllocatedWidth > AllocatedHeight * aspectWidth / SwitchPanelHeight)
+                {
+                    screenWidth = (int)(AllocatedHeight * aspectWidth) / SwitchPanelHeight;
+                }
+                else
+                {
+                    screenHeight = (AllocatedWidth * SwitchPanelHeight) / (int)aspectWidth;
+                }
+
+                int startX = (AllocatedWidth - screenWidth) >> 1;
+                int startY = (AllocatedHeight - screenHeight) >> 1;
+
+                int endX = startX + screenWidth;
+                int endY = startY + screenHeight;
+
+                if (_mouseX >= startX &&
+                    _mouseY >= startY &&
+                    _mouseX < endX &&
+                    _mouseY < endY)
+                {
+                    int screenMouseX = (int)_mouseX - startX;
+                    int screenMouseY = (int)_mouseY - startY;
+
+                    int mX = (screenMouseX * (int)aspectWidth) / screenWidth;
+                    int mY = (screenMouseY * SwitchPanelHeight) / screenHeight;
+
+                    TouchPoint currentPoint = new TouchPoint
+                    {
+                        X = (uint)mX,
+                        Y = (uint)mY,
+
+                        // Placeholder values till more data is acquired
+                        DiameterX = 10,
+                        DiameterY = 10,
+                        Angle = 90
+                    };
+
+                    hasTouch = true;
+
+                    Device.Hid.Touchscreen.Update(currentPoint);
+                }
+            }
+
+            if (!hasTouch)
+            {
+                Device.Hid.Touchscreen.Update();
+            }
+
+            Device.Hid.DebugPad.Update();
+
+            return true;
+        }
+
+
+        [Flags]
+        private enum KeyboardHotkeyState
+        {
+            None,
+            ToggleVSync
+        }
+
+        private KeyboardHotkeyState GetHotkeyState()
+        {
+            KeyboardHotkeyState state = KeyboardHotkeyState.None;
+
+            if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
+            {
+                state |= KeyboardHotkeyState.ToggleVSync;
+            }
+
+            return state;
+        }
+    }
+}
diff --git a/Ryujinx/Ui/SPBOpenGLContext.cs b/Ryujinx/Ui/SPBOpenGLContext.cs
index c2b5d6383..e1a315c9d 100644
--- a/Ryujinx/Ui/SPBOpenGLContext.cs
+++ b/Ryujinx/Ui/SPBOpenGLContext.cs
@@ -34,7 +34,7 @@ namespace Ryujinx.Ui
         public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
         {
             OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
-            NativeWindowBase window = PlatformHelper.CreateWindow(FramebufferFormat.Default, 0, 0, 100, 100);
+            NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
 
             context.Initialize(window);
             context.MakeCurrent(window);
diff --git a/Ryujinx/Ui/VKRenderer.cs b/Ryujinx/Ui/VKRenderer.cs
new file mode 100644
index 000000000..7b01f709c
--- /dev/null
+++ b/Ryujinx/Ui/VKRenderer.cs
@@ -0,0 +1,80 @@
+using Gdk;
+using Gtk;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Input.HLE;
+using SPB.Graphics.Vulkan;
+using SPB.Platform.Win32;
+using SPB.Platform.X11;
+using SPB.Windowing;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Ui
+{
+    public class VKRenderer : RendererWidgetBase
+    {
+        public NativeWindowBase NativeWindow { get; private set; }
+
+        public VKRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel) { }
+
+        private NativeWindowBase RetrieveNativeWindow()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
+
+                return new SimpleWin32Window(new NativeHandle(windowHandle));
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
+                IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
+
+                return new SimpleX11Window(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
+            }
+
+            throw new NotImplementedException();
+        }
+
+        [DllImport("libgdk-3-0.dll")]
+        private static extern IntPtr gdk_win32_window_get_handle(IntPtr d);
+
+        [DllImport("libgdk-3.so.0")]
+        private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
+
+        [DllImport("libgdk-3.so.0")]
+        private static extern IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
+
+        protected override bool OnConfigureEvent(EventConfigure evnt)
+        {
+            if (NativeWindow == null)
+            {
+                NativeWindow = RetrieveNativeWindow();
+
+                WaitEvent.Set();
+            }
+
+            return base.OnConfigureEvent(evnt);
+        }
+
+        public unsafe IntPtr CreateWindowSurface(IntPtr instance)
+        {
+            return VulkanHelper.CreateWindowSurface(instance, NativeWindow);
+        }
+
+        public override void InitializeRenderer() { }
+
+        public override void SwapBuffers() { }
+
+        public override string GetGpuVendorName()
+        {
+            return "Vulkan (Unknown)";
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            Device.DisposeGpu();
+            NpadManager.Dispose();
+        }
+    }
+}
diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs
index 6e876ad9c..2732bdcbd 100644
--- a/Ryujinx/Ui/Windows/ControllerWindow.cs
+++ b/Ryujinx/Ui/Windows/ControllerWindow.cs
@@ -187,9 +187,9 @@ namespace Ryujinx.Ui.Windows
             mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
             mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
 
-            if (_mainWindow.GlRendererWidget != null)
+            if (_mainWindow.RendererWidget != null)
             {
-                _mainWindow.GlRendererWidget.NpadManager.BlockInputUpdates();
+                _mainWindow.RendererWidget.NpadManager.BlockInputUpdates();
             }
         }
 
@@ -219,9 +219,9 @@ namespace Ryujinx.Ui.Windows
             _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
             _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
 
-            if (_mainWindow.GlRendererWidget != null)
+            if (_mainWindow.RendererWidget != null)
             {
-                _mainWindow.GlRendererWidget.NpadManager.UnblockInputUpdates();
+                _mainWindow.RendererWidget.NpadManager.UnblockInputUpdates();
             }
 
             _selectedGamepad?.Dispose();
@@ -1141,9 +1141,9 @@ namespace Ryujinx.Ui.Windows
                 }
             }
 
-            if (_mainWindow.GlRendererWidget != null)
+            if (_mainWindow.RendererWidget != null)
             {
-                _mainWindow.GlRendererWidget.NpadManager.ReloadConfiguration(newConfig);
+                _mainWindow.RendererWidget.NpadManager.ReloadConfiguration(newConfig);
             }
 
             // Atomically replace and signal input change.