From faac08e63883c74e8602d2e36a76af954d48eb28 Mon Sep 17 00:00:00 2001 From: Mary 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 13442c953e..6a441a610e 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -18,7 +18,7 @@ - + diff --git a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs index 804a1a2799..d74ea3d596 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 99c4698acd..4c3f8ce40c 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 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 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 a465aeef29..0000000000 --- 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 08527ea359..56dcf3ebcd 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 0000000000..5270e31e13 --- /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 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 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 c2b5d6383d..e1a315c9d1 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 0000000000..7b01f709ce --- /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 6e876ad9c5..2732bdcbde 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.