From cee712105850ac3385cd0091a923438167433f9f Mon Sep 17 00:00:00 2001
From: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
Date: Sat, 8 Apr 2023 01:22:00 +0200
Subject: Move solution and projects to src
---
.../HeadlessDynamicTextInputHandler.cs | 51 ++
src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs | 17 +
src/Ryujinx.Headless.SDL2/HideCursor.cs | 9 +
src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs | 169 +++++
src/Ryujinx.Headless.SDL2/Options.cs | 209 ++++++
src/Ryujinx.Headless.SDL2/Program.cs | 704 +++++++++++++++++++++
.../Ryujinx.Headless.SDL2.csproj | 66 ++
src/Ryujinx.Headless.SDL2/Ryujinx.bmp | Bin 0 -> 9354 bytes
src/Ryujinx.Headless.SDL2/SDL2Mouse.cs | 90 +++
src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs | 164 +++++
.../StatusUpdatedEventArgs.cs | 24 +
src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs | 104 +++
src/Ryujinx.Headless.SDL2/WindowBase.cs | 499 +++++++++++++++
13 files changed, 2106 insertions(+)
create mode 100644 src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs
create mode 100644 src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs
create mode 100644 src/Ryujinx.Headless.SDL2/HideCursor.cs
create mode 100644 src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
create mode 100644 src/Ryujinx.Headless.SDL2/Options.cs
create mode 100644 src/Ryujinx.Headless.SDL2/Program.cs
create mode 100644 src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
create mode 100644 src/Ryujinx.Headless.SDL2/Ryujinx.bmp
create mode 100644 src/Ryujinx.Headless.SDL2/SDL2Mouse.cs
create mode 100644 src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
create mode 100644 src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs
create mode 100644 src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
create mode 100644 src/Ryujinx.Headless.SDL2/WindowBase.cs
(limited to 'src/Ryujinx.Headless.SDL2')
diff --git a/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs b/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs
new file mode 100644
index 00000000..7e624152
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/HeadlessDynamicTextInputHandler.cs
@@ -0,0 +1,51 @@
+using Ryujinx.HLE.Ui;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Headless.SDL2
+{
+ ///
+ /// Headless text processing class, right now there is no way to forward the input to it.
+ ///
+ internal class HeadlessDynamicTextInputHandler : IDynamicTextInputHandler
+ {
+ private bool _canProcessInput;
+
+ public event DynamicTextChangedHandler TextChangedEvent;
+ public event KeyPressedHandler KeyPressedEvent { add { } remove { } }
+ public event KeyReleasedHandler KeyReleasedEvent { add { } remove { } }
+
+ public bool TextProcessingEnabled
+ {
+ get
+ {
+ return Volatile.Read(ref _canProcessInput);
+ }
+
+ set
+ {
+ Volatile.Write(ref _canProcessInput, value);
+
+ // Launch a task to update the text.
+ Task.Run(() =>
+ {
+ Thread.Sleep(100);
+ TextChangedEvent?.Invoke("Ryujinx", 7, 7, false);
+ });
+ }
+ }
+
+ public HeadlessDynamicTextInputHandler()
+ {
+ // Start with input processing turned off so the text box won't accumulate text
+ // if the user is playing on the keyboard.
+ _canProcessInput = false;
+ }
+
+ public void SetText(string text, int cursorBegin) { }
+
+ public void SetText(string text, int cursorBegin, int cursorEnd) { }
+
+ public void Dispose() { }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs b/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs
new file mode 100644
index 00000000..4ef00b3c
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/HeadlessHostUiTheme.cs
@@ -0,0 +1,17 @@
+using Ryujinx.HLE.Ui;
+
+namespace Ryujinx.Headless.SDL2
+{
+ internal class HeadlessHostUiTheme : IHostUiTheme
+ {
+ public string FontFamily => "sans-serif";
+
+ public ThemeColor DefaultBackgroundColor => new ThemeColor(1, 0, 0, 0);
+ public ThemeColor DefaultForegroundColor => new ThemeColor(1, 1, 1, 1);
+ public ThemeColor DefaultBorderColor => new ThemeColor(1, 1, 1, 1);
+ public ThemeColor SelectionBackgroundColor => new ThemeColor(1, 1, 1, 1);
+ public ThemeColor SelectionForegroundColor => new ThemeColor(1, 0, 0, 0);
+
+ public HeadlessHostUiTheme() { }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/HideCursor.cs b/src/Ryujinx.Headless.SDL2/HideCursor.cs
new file mode 100644
index 00000000..2dc0bd6a
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/HideCursor.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Headless.SDL2
+{
+ public enum HideCursor
+ {
+ Never,
+ OnIdle,
+ Always
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
new file mode 100644
index 00000000..69b0f42f
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
@@ -0,0 +1,169 @@
+using OpenTK;
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Input.HLE;
+using System;
+using static SDL2.SDL;
+
+namespace Ryujinx.Headless.SDL2.OpenGL
+{
+ class OpenGLWindow : WindowBase
+ {
+ private static void SetupOpenGLAttributes(bool sharedContext, GraphicsDebugLevel debugLevel)
+ {
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_FLAGS, debugLevel != GraphicsDebugLevel.None ? (int)SDL_GLcontext.SDL_GL_CONTEXT_DEBUG_FLAG : 0);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, sharedContext ? 1 : 0);
+
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCELERATED_VISUAL, 1);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ALPHA_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 0);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DOUBLEBUFFER, 1);
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STEREO, 0);
+ }
+
+ private class OpenToolkitBindingsContext : IBindingsContext
+ {
+ public IntPtr GetProcAddress(string procName)
+ {
+ return SDL_GL_GetProcAddress(procName);
+ }
+ }
+
+ private class SDL2OpenGLContext : IOpenGLContext
+ {
+ private IntPtr _context;
+ private IntPtr _window;
+ private bool _shouldDisposeWindow;
+
+ public SDL2OpenGLContext(IntPtr context, IntPtr window, bool shouldDisposeWindow = true)
+ {
+ _context = context;
+ _window = window;
+ _shouldDisposeWindow = shouldDisposeWindow;
+ }
+
+ public static SDL2OpenGLContext CreateBackgroundContext(SDL2OpenGLContext sharedContext)
+ {
+ sharedContext.MakeCurrent();
+
+ // Ensure we share our contexts.
+ SetupOpenGLAttributes(true, GraphicsDebugLevel.None);
+ IntPtr windowHandle = SDL_CreateWindow("Ryujinx background context window", 0, 0, 1, 1, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_HIDDEN);
+ IntPtr context = SDL_GL_CreateContext(windowHandle);
+
+ GL.LoadBindings(new OpenToolkitBindingsContext());
+
+ SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
+
+ SDL_GL_MakeCurrent(windowHandle, IntPtr.Zero);
+
+ return new SDL2OpenGLContext(context, windowHandle);
+ }
+
+ public void MakeCurrent()
+ {
+ if (SDL_GL_GetCurrentContext() == _context || SDL_GL_GetCurrentWindow() == _window)
+ {
+ return;
+ }
+
+ int res = SDL_GL_MakeCurrent(_window, _context);
+
+ if (res != 0)
+ {
+ string errorMessage = $"SDL_GL_CreateContext failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+ }
+
+ public void Dispose()
+ {
+ SDL_GL_DeleteContext(_context);
+
+ if (_shouldDisposeWindow)
+ {
+ SDL_DestroyWindow(_window);
+ }
+ }
+ }
+
+ private GraphicsDebugLevel _glLogLevel;
+ private SDL2OpenGLContext _openGLContext;
+
+ public OpenGLWindow(
+ InputManager inputManager,
+ GraphicsDebugLevel glLogLevel,
+ AspectRatio aspectRatio,
+ bool enableMouse,
+ HideCursor hideCursor)
+ : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
+ {
+ _glLogLevel = glLogLevel;
+ }
+
+ public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_OPENGL;
+
+ protected override void InitializeWindowRenderer()
+ {
+ // Ensure to not share this context with other contexts before this point.
+ SetupOpenGLAttributes(false, _glLogLevel);
+ IntPtr context = SDL_GL_CreateContext(WindowHandle);
+ SDL_GL_SetSwapInterval(1);
+
+ if (context == IntPtr.Zero)
+ {
+ string errorMessage = $"SDL_GL_CreateContext failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+
+ // NOTE: The window handle needs to be disposed by the thread that created it and is handled separately.
+ _openGLContext = new SDL2OpenGLContext(context, WindowHandle, false);
+
+ // First take exclusivity on the OpenGL context.
+ ((OpenGLRenderer)Renderer).InitializeBackgroundContext(SDL2OpenGLContext.CreateBackgroundContext(_openGLContext));
+
+ _openGLContext.MakeCurrent();
+
+ GL.ClearColor(0, 0, 0, 1.0f);
+ GL.Clear(ClearBufferMask.ColorBufferBit);
+ SwapBuffers();
+
+ Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
+ MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+ }
+
+ protected override void InitializeRenderer() { }
+
+ protected override void FinalizeWindowRenderer()
+ {
+ // Try to bind the OpenGL context before calling the gpu disposal.
+ _openGLContext.MakeCurrent();
+
+ Device.DisposeGpu();
+
+ // Unbind context and destroy everything
+ SDL_GL_MakeCurrent(WindowHandle, IntPtr.Zero);
+ _openGLContext.Dispose();
+ }
+
+ protected override void SwapBuffers()
+ {
+ SDL_GL_SwapWindow(WindowHandle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
new file mode 100644
index 00000000..982d0990
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -0,0 +1,209 @@
+using CommandLine;
+using Ryujinx.Common.Configuration;
+using Ryujinx.HLE.HOS.SystemState;
+
+namespace Ryujinx.Headless.SDL2
+{
+ public class Options
+ {
+ // General
+
+ [Option("root-data-dir", Required = false, HelpText = "Set the custom folder path for Ryujinx data.")]
+ public string BaseDataDir { get; set; }
+
+ [Option("profile", Required = false, HelpText = "Set the user profile to launch the game with.")]
+ public string UserProfile { get; set; }
+
+ // Input
+
+ [Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")]
+ public string InputProfile1Name { get; set; }
+
+ [Option("input-profile-2", Required = false, HelpText = "Set the input profile in use for Player 2.")]
+ public string InputProfile2Name { get; set; }
+
+ [Option("input-profile-3", Required = false, HelpText = "Set the input profile in use for Player 3.")]
+ public string InputProfile3Name { get; set; }
+
+ [Option("input-profile-4", Required = false, HelpText = "Set the input profile in use for Player 4.")]
+ public string InputProfile4Name { get; set; }
+
+ [Option("input-profile-5", Required = false, HelpText = "Set the input profile in use for Player 5.")]
+ public string InputProfile5Name { get; set; }
+
+ [Option("input-profile-6", Required = false, HelpText = "Set the input profile in use for Player 6.")]
+ public string InputProfile6Name { get; set; }
+
+ [Option("input-profile-7", Required = false, HelpText = "Set the input profile in use for Player 7.")]
+ public string InputProfile7Name { get; set; }
+
+ [Option("input-profile-8", Required = false, HelpText = "Set the input profile in use for Player 8.")]
+ public string InputProfile8Name { get; set; }
+
+ [Option("input-profile-handheld", Required = false, HelpText = "Set the input profile in use for the Handheld Player.")]
+ public string InputProfileHandheldName { get; set; }
+
+ [Option("input-id-1", Required = false, HelpText = "Set the input id in use for Player 1.")]
+ public string InputId1 { get; set; }
+
+ [Option("input-id-2", Required = false, HelpText = "Set the input id in use for Player 2.")]
+ public string InputId2 { get; set; }
+
+ [Option("input-id-3", Required = false, HelpText = "Set the input id in use for Player 3.")]
+ public string InputId3 { get; set; }
+
+ [Option("input-id-4", Required = false, HelpText = "Set the input id in use for Player 4.")]
+ public string InputId4 { get; set; }
+
+ [Option("input-id-5", Required = false, HelpText = "Set the input id in use for Player 5.")]
+ public string InputId5 { get; set; }
+
+ [Option("input-id-6", Required = false, HelpText = "Set the input id in use for Player 6.")]
+ public string InputId6 { get; set; }
+
+ [Option("input-id-7", Required = false, HelpText = "Set the input id in use for Player 7.")]
+ public string InputId7 { get; set; }
+
+ [Option("input-id-8", Required = false, HelpText = "Set the input id in use for Player 8.")]
+ public string InputId8 { get; set; }
+
+ [Option("input-id-handheld", Required = false, HelpText = "Set the input id in use for the Handheld Player.")]
+ public string InputIdHandheld { get; set; }
+
+ [Option("enable-keyboard", Required = false, Default = false, HelpText = "Enable or disable keyboard support (Independent from controllers binding).")]
+ public bool EnableKeyboard { get; set; }
+
+ [Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
+ public bool EnableMouse { get; set; }
+
+ [Option("hide-cursor", Required = false, Default = HideCursor.OnIdle, HelpText = "Change when the cursor gets hidden.")]
+ public HideCursor HideCursor { get; set; }
+
+ [Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")]
+ public bool ListInputProfiles { get; set; }
+
+ [Option("list-inputs-ids", Required = false, HelpText = "List inputs ids.")]
+ public bool ListInputIds { get; set; }
+
+ // System
+
+ [Option("disable-ptc", Required = false, HelpText = "Disables profiled persistent translation cache.")]
+ public bool DisablePtc { get; set; }
+
+ [Option("enable-internet-connection", Required = false, Default = false, HelpText = "Enables guest Internet connection.")]
+ public bool EnableInternetAccess { get; set; }
+
+ [Option("disable-fs-integrity-checks", Required = false, HelpText = "Disables integrity checks on Game content files.")]
+ public bool DisableFsIntegrityChecks { get; set; }
+
+ [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
+ public int FsGlobalAccessLogMode { get; set; }
+
+ [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
+ public bool DisableVsync { get; set; }
+
+ [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
+ public bool DisableShaderCache { get; set; }
+
+ [Option("enable-texture-recompression", Required = false, Default = false, HelpText = "Enables Texture recompression.")]
+ public bool EnableTextureRecompression { get; set; }
+
+ [Option("disable-docked-mode", Required = false, HelpText = "Disables Docked Mode.")]
+ public bool DisableDockedMode { get; set; }
+
+ [Option("system-language", Required = false, Default = SystemLanguage.AmericanEnglish, HelpText = "Change System Language.")]
+ public SystemLanguage SystemLanguage { get; set; }
+
+ [Option("system-region", Required = false, Default = RegionCode.USA, HelpText = "Change System Region.")]
+ public RegionCode SystemRegion { get; set; }
+
+ [Option("system-timezone", Required = false, Default = "UTC", HelpText = "Change System TimeZone.")]
+ public string SystemTimeZone { get; set; }
+
+ [Option("system-time-offset", Required = false, Default = 0, HelpText = "Change System Time Offset in seconds.")]
+ public long SystemTimeOffset { get; set; }
+
+ [Option("memory-manager-mode", Required = false, Default = MemoryManagerMode.HostMappedUnsafe, HelpText = "The selected memory manager mode.")]
+ public MemoryManagerMode MemoryManagerMode { get; set; }
+
+ [Option("audio-volume", Required = false, Default = 1.0f, HelpText ="The audio level (0 to 1).")]
+ public float AudioVolume { get; set; }
+
+ [Option("use-hypervisor", Required = false, Default = true, HelpText = "Uses Hypervisor over JIT if available.")]
+ public bool UseHypervisor { get; set; }
+
+ [Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")]
+ public string MultiplayerLanInterfaceId { get; set; }
+
+ // Logging
+
+ [Option("disable-file-logging", Required = false, Default = false, HelpText = "Disables logging to a file on disk.")]
+ public bool DisableFileLog { get; set; }
+
+ [Option("enable-debug-logs", Required = false, Default = false, HelpText = "Enables printing debug log messages.")]
+ public bool LoggingEnableDebug { get; set; }
+
+ [Option("disable-stub-logs", Required = false, HelpText = "Disables printing stub log messages.")]
+ public bool LoggingDisableStub { get; set; }
+
+ [Option("disable-info-logs", Required = false, HelpText = "Disables printing info log messages.")]
+ public bool LoggingDisableInfo { get; set; }
+
+ [Option("disable-warning-logs", Required = false, HelpText = "Disables printing warning log messages.")]
+ public bool LoggingDisableWarning { get; set; }
+
+ [Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")]
+ public bool LoggingEnableError { get; set; }
+
+ [Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")]
+ public bool LoggingEnableTrace { get; set; }
+
+ [Option("disable-guest-logs", Required = false, HelpText = "Disables printing guest log messages.")]
+ public bool LoggingDisableGuest { get; set; }
+
+ [Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")]
+ public bool LoggingEnableFsAccessLog { get; set; }
+
+ [Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")]
+ public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }
+
+ // Graphics
+
+ [Option("resolution-scale", Required = false, Default = 1, HelpText = "Resolution Scale. A floating point scale applied to applicable render targets.")]
+ public float ResScale { get; set; }
+
+ [Option("max-anisotropy", Required = false, Default = -1, HelpText = "Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.")]
+ public float MaxAnisotropy { get; set; }
+
+ [Option("aspect-ratio", Required = false, Default = AspectRatio.Fixed16x9, HelpText = "Aspect Ratio applied to the renderer window.")]
+ public AspectRatio AspectRatio { get; set; }
+
+ [Option("backend-threading", Required = false, Default = BackendThreading.Auto, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")]
+ public BackendThreading BackendThreading { get; set; }
+
+ [Option("disable-macro-hle", Required= false, HelpText = "Disables high-level emulation of Macro code. Leaving this enabled improves performance but may cause graphical glitches in some games.")]
+ public bool DisableMacroHLE { get; set; }
+
+ [Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
+ public string GraphicsShadersDumpPath { get; set; }
+
+ [Option("graphics-backend", Required = false, Default = GraphicsBackend.OpenGl, HelpText = "Change Graphics Backend to use.")]
+ public GraphicsBackend GraphicsBackend { get; set; }
+
+ [Option("preferred-gpu-vendor", Required = false, Default = "", HelpText = "When using the Vulkan backend, prefer using the GPU from the specified vendor.")]
+ public string PreferredGpuVendor { get; set; }
+
+ // Hacks
+
+ [Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")]
+ public bool ExpandRam { get; set; }
+
+ [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
+ public bool IgnoreMissingServices { get; set; }
+
+ // Values
+
+ [Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]
+ public string InputPath { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
new file mode 100644
index 00000000..b0bdb97f
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -0,0 +1,704 @@
+using ARMeilleure.Translation;
+using CommandLine;
+using LibHac.Tools.FsSystem;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.SystemInterop;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Cpu;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Gpu;
+using Ryujinx.Graphics.Gpu.Shader;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Graphics.Vulkan;
+using Ryujinx.Headless.SDL2.OpenGL;
+using Ryujinx.Headless.SDL2.Vulkan;
+using Ryujinx.HLE;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using Ryujinx.Input.SDL2;
+using Silk.NET.Vulkan;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
+using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
+using Key = Ryujinx.Common.Configuration.Hid.Key;
+
+namespace Ryujinx.Headless.SDL2
+{
+ class Program
+ {
+ public static string Version { get; private set; }
+
+ private static VirtualFileSystem _virtualFileSystem;
+ private static ContentManager _contentManager;
+ private static AccountManager _accountManager;
+ private static LibHacHorizonManager _libHacHorizonManager;
+ private static UserChannelPersistence _userChannelPersistence;
+ private static InputManager _inputManager;
+ private static Switch _emulationContext;
+ private static WindowBase _window;
+ private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
+ private static List _inputConfiguration;
+ private static bool _enableKeyboard;
+ private static bool _enableMouse;
+
+ private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ static void Main(string[] args)
+ {
+ Version = ReleaseInformation.GetVersion();
+
+ Console.Title = $"Ryujinx Console {Version} (Headless SDL2)";
+
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
+ {
+ AutoResetEvent invoked = new AutoResetEvent(false);
+
+ // MacOS must perform SDL polls from the main thread.
+ Ryujinx.SDL2.Common.SDL2Driver.MainThreadDispatcher = (Action action) =>
+ {
+ invoked.Reset();
+
+ WindowBase.QueueMainThreadAction(() =>
+ {
+ action();
+
+ invoked.Set();
+ });
+
+ invoked.WaitOne();
+ };
+ }
+
+ Parser.Default.ParseArguments(args)
+ .WithParsed(Load)
+ .WithNotParsed(errors => errors.Output());
+ }
+
+ private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
+ {
+ if (inputId == null)
+ {
+ if (index == PlayerIndex.Player1)
+ {
+ Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
+
+ // Default to keyboard
+ inputId = "0";
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.Application, $"{index} not configured");
+
+ return null;
+ }
+ }
+
+ IGamepad gamepad;
+
+ bool isKeyboard = true;
+
+ gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId);
+
+ if (gamepad == null)
+ {
+ gamepad = _inputManager.GamepadDriver.GetGamepad(inputId);
+ isKeyboard = false;
+
+ if (gamepad == null)
+ {
+ Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")");
+
+ return null;
+ }
+ }
+
+ string gamepadName = gamepad.Name;
+
+ gamepad.Dispose();
+
+ InputConfig config;
+
+ if (inputProfileName == null || inputProfileName.Equals("default"))
+ {
+ if (isKeyboard)
+ {
+ config = new StandardKeyboardInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.WindowKeyboard,
+ Id = null,
+ ControllerType = ControllerType.JoyconPair,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = Key.Up,
+ DpadDown = Key.Down,
+ DpadLeft = Key.Left,
+ DpadRight = Key.Right,
+ ButtonMinus = Key.Minus,
+ ButtonL = Key.E,
+ ButtonZl = Key.Q,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound
+ },
+
+ LeftJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = Key.W,
+ StickDown = Key.S,
+ StickLeft = Key.A,
+ StickRight = Key.D,
+ StickButton = Key.F,
+ },
+
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = Key.Z,
+ ButtonB = Key.X,
+ ButtonX = Key.C,
+ ButtonY = Key.V,
+ ButtonPlus = Key.Plus,
+ ButtonR = Key.U,
+ ButtonZr = Key.O,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound
+ },
+
+ RightJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = Key.I,
+ StickDown = Key.K,
+ StickLeft = Key.J,
+ StickRight = Key.L,
+ StickButton = Key.H,
+ }
+ };
+ }
+ else
+ {
+ bool isNintendoStyle = gamepadName.Contains("Nintendo");
+
+ config = new StandardControllerInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.GamepadSDL2,
+ Id = null,
+ ControllerType = ControllerType.JoyconPair,
+ DeadzoneLeft = 0.1f,
+ DeadzoneRight = 0.1f,
+ RangeLeft = 1.0f,
+ RangeRight = 1.0f,
+ TriggerThreshold = 0.5f,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = ConfigGamepadInputId.DpadUp,
+ DpadDown = ConfigGamepadInputId.DpadDown,
+ DpadLeft = ConfigGamepadInputId.DpadLeft,
+ DpadRight = ConfigGamepadInputId.DpadRight,
+ ButtonMinus = ConfigGamepadInputId.Minus,
+ ButtonL = ConfigGamepadInputId.LeftShoulder,
+ ButtonZl = ConfigGamepadInputId.LeftTrigger,
+ ButtonSl = ConfigGamepadInputId.Unbound,
+ ButtonSr = ConfigGamepadInputId.Unbound,
+ },
+
+ LeftJoyconStick = new JoyconConfigControllerStick
+ {
+ Joystick = ConfigStickInputId.Left,
+ StickButton = ConfigGamepadInputId.LeftStick,
+ InvertStickX = false,
+ InvertStickY = false,
+ Rotate90CW = false,
+ },
+
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
+ ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
+ ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
+ ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
+ ButtonPlus = ConfigGamepadInputId.Plus,
+ ButtonR = ConfigGamepadInputId.RightShoulder,
+ ButtonZr = ConfigGamepadInputId.RightTrigger,
+ ButtonSl = ConfigGamepadInputId.Unbound,
+ ButtonSr = ConfigGamepadInputId.Unbound,
+ },
+
+ RightJoyconStick = new JoyconConfigControllerStick
+ {
+ Joystick = ConfigStickInputId.Right,
+ StickButton = ConfigGamepadInputId.RightStick,
+ InvertStickX = false,
+ InvertStickY = false,
+ Rotate90CW = false,
+ },
+
+ Motion = new StandardMotionConfigController
+ {
+ MotionBackend = MotionInputBackendType.GamepadDriver,
+ EnableMotion = true,
+ Sensitivity = 100,
+ GyroDeadzone = 1,
+ },
+ Rumble = new RumbleConfigController
+ {
+ StrongRumble = 1f,
+ WeakRumble = 1f,
+ EnableRumble = false
+ }
+ };
+ }
+ }
+ else
+ {
+ string profileBasePath;
+
+ if (isKeyboard)
+ {
+ profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "keyboard");
+ }
+ else
+ {
+ profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "controller");
+ }
+
+ string path = Path.Combine(profileBasePath, inputProfileName + ".json");
+
+ if (!File.Exists(path))
+ {
+ Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" not found for \"{inputId}\"");
+
+ return null;
+ }
+
+ try
+ {
+ config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
+ }
+ catch (JsonException)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" parsing failed for \"{inputId}\"");
+
+ return null;
+ }
+ }
+
+ config.Id = inputId;
+ config.PlayerIndex = index;
+
+ string inputTypeName = isKeyboard ? "Keyboard" : "Gamepad";
+
+ Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} configured with {inputTypeName} \"{config.Id}\"");
+
+ // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0.
+ if (config is StandardControllerInputConfig controllerConfig)
+ {
+ if (controllerConfig.RangeLeft <= 0.0f && controllerConfig.RangeRight <= 0.0f)
+ {
+ controllerConfig.RangeLeft = 1.0f;
+ controllerConfig.RangeRight = 1.0f;
+
+ Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration");
+ }
+ }
+
+ return config;
+ }
+
+ static void Load(Options option)
+ {
+ AppDataManager.Initialize(option.BaseDataDir);
+
+ _virtualFileSystem = VirtualFileSystem.CreateInstance();
+ _libHacHorizonManager = new LibHacHorizonManager();
+
+ _libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
+ _libHacHorizonManager.InitializeArpServer();
+ _libHacHorizonManager.InitializeBcatServer();
+ _libHacHorizonManager.InitializeSystemClients();
+
+ _contentManager = new ContentManager(_virtualFileSystem);
+ _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
+ _userChannelPersistence = new UserChannelPersistence();
+
+ _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
+
+ GraphicsConfig.EnableShaderCache = true;
+
+ IGamepad gamepad;
+
+ if (option.ListInputIds)
+ {
+ Logger.Info?.Print(LogClass.Application, "Input Ids:");
+
+ foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
+ {
+ gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
+
+ Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")");
+
+ gamepad.Dispose();
+ }
+
+ foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
+ {
+ gamepad = _inputManager.GamepadDriver.GetGamepad(id);
+
+ Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")");
+
+ gamepad.Dispose();
+ }
+
+ return;
+ }
+
+ if (option.InputPath == null)
+ {
+ Logger.Error?.Print(LogClass.Application, "Please provide a file to load");
+
+ return;
+ }
+
+ _inputConfiguration = new List();
+ _enableKeyboard = option.EnableKeyboard;
+ _enableMouse = option.EnableMouse;
+
+ void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
+ {
+ InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, index);
+
+ if (inputConfig != null)
+ {
+ _inputConfiguration.Add(inputConfig);
+ }
+ }
+
+ LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1);
+ LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2);
+ LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3);
+ LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4);
+ LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5);
+ LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6);
+ LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7);
+ LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8);
+ LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld);
+
+ if (_inputConfiguration.Count == 0)
+ {
+ return;
+ }
+
+ // Setup logging level
+ Logger.SetEnable(LogLevel.Debug, option.LoggingEnableDebug);
+ Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub);
+ Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo);
+ Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning);
+ Logger.SetEnable(LogLevel.Error, option.LoggingEnableError);
+ Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
+ Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
+ Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
+
+ if (!option.DisableFileLog)
+ {
+ Logger.AddTarget(new AsyncLogTargetWrapper(
+ new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
+ 1000,
+ AsyncLogTargetOverflowAction.Block
+ ));
+ }
+
+ // Setup graphics configuration
+ GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
+ GraphicsConfig.EnableTextureRecompression = option.EnableTextureRecompression;
+ GraphicsConfig.ResScale = option.ResScale;
+ GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy;
+ GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath;
+ GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
+
+ while (true)
+ {
+ LoadApplication(option);
+
+ if (_userChannelPersistence.PreviousIndex == -1 || !_userChannelPersistence.ShouldRestart)
+ {
+ break;
+ }
+
+ _userChannelPersistence.ShouldRestart = false;
+ }
+
+ _inputManager.Dispose();
+ }
+
+ private static void SetupProgressHandler()
+ {
+ if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null)
+ {
+ _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler;
+ _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler;
+ }
+
+ _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
+ _emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler;
+ }
+
+ private static void ProgressHandler(T state, int current, int total) where T : Enum
+ {
+ string label;
+
+ switch (state)
+ {
+ case LoadState ptcState:
+ label = $"PTC : {current}/{total}";
+ break;
+ case ShaderCacheState shaderCacheState:
+ label = $"Shaders : {current}/{total}";
+ break;
+ default:
+ throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}");
+ }
+
+ Logger.Info?.Print(LogClass.Application, label);
+ }
+
+ private static WindowBase CreateWindow(Options options)
+ {
+ return options.GraphicsBackend == GraphicsBackend.Vulkan
+ ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor)
+ : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor);
+ }
+
+ private static IRenderer CreateRenderer(Options options, WindowBase window)
+ {
+ if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
+ {
+ string preferredGpuId = string.Empty;
+
+ if (!string.IsNullOrEmpty(options.PreferredGpuVendor))
+ {
+ string preferredGpuVendor = options.PreferredGpuVendor.ToLowerInvariant();
+ var devices = VulkanRenderer.GetPhysicalDevices();
+
+ foreach (var device in devices)
+ {
+ if (device.Vendor.ToLowerInvariant() == preferredGpuVendor)
+ {
+ preferredGpuId = device.Id;
+ break;
+ }
+ }
+ }
+
+ return new VulkanRenderer(
+ (instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
+ vulkanWindow.GetRequiredInstanceExtensions,
+ preferredGpuId);
+ }
+ else
+ {
+ return new OpenGLRenderer();
+ }
+ }
+
+ private static Switch InitializeEmulationContext(WindowBase window, IRenderer renderer, Options options)
+ {
+ BackendThreading threadingMode = options.BackendThreading;
+
+ bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
+
+ if (threadedGAL)
+ {
+ renderer = new ThreadedRenderer(renderer);
+ }
+
+ HLEConfiguration configuration = new HLEConfiguration(_virtualFileSystem,
+ _libHacHorizonManager,
+ _contentManager,
+ _accountManager,
+ _userChannelPersistence,
+ renderer,
+ new SDL2HardwareDeviceDriver(),
+ options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
+ window,
+ options.SystemLanguage,
+ options.SystemRegion,
+ !options.DisableVsync,
+ !options.DisableDockedMode,
+ !options.DisablePtc,
+ options.EnableInternetAccess,
+ !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
+ options.FsGlobalAccessLogMode,
+ options.SystemTimeOffset,
+ options.SystemTimeZone,
+ options.MemoryManagerMode,
+ options.IgnoreMissingServices,
+ options.AspectRatio,
+ options.AudioVolume,
+ options.UseHypervisor,
+ options.MultiplayerLanInterfaceId);
+
+ return new Switch(configuration);
+ }
+
+ private static void ExecutionEntrypoint()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
+ }
+
+ DisplaySleep.Prevent();
+
+ _window.Initialize(_emulationContext, _inputConfiguration, _enableKeyboard, _enableMouse);
+
+ _window.Execute();
+
+ _emulationContext.Dispose();
+ _window.Dispose();
+
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution?.Dispose();
+ _windowsMultimediaTimerResolution = null;
+ }
+ }
+
+ private static bool LoadApplication(Options options)
+ {
+ string path = options.InputPath;
+
+ Logger.RestartTime();
+
+ WindowBase window = CreateWindow(options);
+ IRenderer renderer = CreateRenderer(options, window);
+
+ _window = window;
+
+ _emulationContext = InitializeEmulationContext(window, renderer, options);
+
+ SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
+
+ Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
+
+ if (Directory.Exists(path))
+ {
+ string[] romFsFiles = Directory.GetFiles(path, "*.istorage");
+
+ if (romFsFiles.Length == 0)
+ {
+ romFsFiles = Directory.GetFiles(path, "*.romfs");
+ }
+
+ if (romFsFiles.Length > 0)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
+
+ if (!_emulationContext.LoadCart(path, romFsFiles[0]))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
+
+ if (!_emulationContext.LoadCart(path))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ }
+ }
+ else if (File.Exists(path))
+ {
+ switch (Path.GetExtension(path).ToLowerInvariant())
+ {
+ case ".xci":
+ Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
+
+ if (!_emulationContext.LoadXci(path))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ break;
+ case ".nca":
+ Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
+
+ if (!_emulationContext.LoadNca(path))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ break;
+ case ".nsp":
+ case ".pfs0":
+ Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
+
+ if (!_emulationContext.LoadNsp(path))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ break;
+ default:
+ Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
+ try
+ {
+ if (!_emulationContext.LoadProgram(path))
+ {
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
+
+ _emulationContext.Dispose();
+
+ return false;
+ }
+ break;
+ }
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{options.InputPath}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
+
+ _emulationContext.Dispose();
+
+ return false;
+ }
+
+ SetupProgressHandler();
+
+ Translator.IsReadyForTranslation.Reset();
+
+ ExecutionEntrypoint();
+
+ return true;
+ }
+ }
+}
diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
new file mode 100644
index 00000000..fc912d32
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
@@ -0,0 +1,66 @@
+
+
+
+ net7.0
+ win10-x64;osx-x64;linux-x64
+ Exe
+ true
+ 1.0.0-dirty
+ $(DefineConstants);$(ExtraDefineConstants)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+ THIRDPARTY.md
+
+
+ Always
+ LICENSE.txt
+
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+ false
+ ..\Ryujinx\Ryujinx.ico
+
+
+
+ true
+ true
+ partial
+
+
diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.bmp b/src/Ryujinx.Headless.SDL2/Ryujinx.bmp
new file mode 100644
index 00000000..1daa7ce9
Binary files /dev/null and b/src/Ryujinx.Headless.SDL2/Ryujinx.bmp differ
diff --git a/src/Ryujinx.Headless.SDL2/SDL2Mouse.cs b/src/Ryujinx.Headless.SDL2/SDL2Mouse.cs
new file mode 100644
index 00000000..4ce8eafd
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/SDL2Mouse.cs
@@ -0,0 +1,90 @@
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Input;
+using System;
+using System.Drawing;
+using System.Numerics;
+
+namespace Ryujinx.Headless.SDL2
+{
+ class SDL2Mouse : IMouse
+ {
+ private SDL2MouseDriver _driver;
+
+ public GamepadFeaturesFlag Features => throw new NotImplementedException();
+
+ public string Id => "0";
+
+ public string Name => "SDL2Mouse";
+
+ public bool IsConnected => true;
+
+ public bool[] Buttons => _driver.PressedButtons;
+
+ Size IMouse.ClientSize => _driver.GetClientSize();
+
+ public SDL2Mouse(SDL2MouseDriver driver)
+ {
+ _driver = driver;
+ }
+
+ public Vector2 GetPosition()
+ {
+ return _driver.CurrentPosition;
+ }
+
+ public Vector2 GetScroll()
+ {
+ return _driver.Scroll;
+ }
+
+ public GamepadStateSnapshot GetMappedStateSnapshot()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Vector3 GetMotionData(MotionInputId inputId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public GamepadStateSnapshot GetStateSnapshot()
+ {
+ throw new NotImplementedException();
+ }
+
+ public (float, float) GetStick(StickInputId inputId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsButtonPressed(MouseButton button)
+ {
+ return _driver.IsButtonPressed(button);
+ }
+
+ public bool IsPressed(GamepadButtonInputId inputId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetConfiguration(InputConfig configuration)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetTriggerThreshold(float triggerThreshold)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+ _driver = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs b/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
new file mode 100644
index 00000000..8c3412ff
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
@@ -0,0 +1,164 @@
+using Ryujinx.Input;
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using static SDL2.SDL;
+
+namespace Ryujinx.Headless.SDL2
+{
+ class SDL2MouseDriver : IGamepadDriver
+ {
+ private const int CursorHideIdleTime = 5; // seconds
+
+ private bool _isDisposed;
+ private HideCursor _hideCursor;
+ private bool _isHidden;
+ private long _lastCursorMoveTime;
+
+ public bool[] PressedButtons { get; }
+
+ public Vector2 CurrentPosition { get; private set; }
+ public Vector2 Scroll { get; private set; }
+ public Size _clientSize;
+
+ public SDL2MouseDriver(HideCursor hideCursor)
+ {
+ PressedButtons = new bool[(int)MouseButton.Count];
+ _hideCursor = hideCursor;
+
+ if (_hideCursor == HideCursor.Always)
+ {
+ SDL_ShowCursor(SDL_DISABLE);
+ _isHidden = true;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static MouseButton DriverButtonToMouseButton(uint rawButton)
+ {
+ Debug.Assert(rawButton > 0 && rawButton <= (int)MouseButton.Count);
+
+ return (MouseButton)(rawButton - 1);
+ }
+
+ public void UpdatePosition()
+ {
+ SDL_GetMouseState(out int posX, out int posY);
+ Vector2 position = new(posX, posY);
+
+ if (CurrentPosition != position)
+ {
+ CurrentPosition = position;
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+
+ CheckIdle();
+ }
+
+ private void CheckIdle()
+ {
+ if (_hideCursor != HideCursor.OnIdle)
+ {
+ return;
+ }
+
+ long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+
+ if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
+ {
+ if (!_isHidden)
+ {
+ SDL_ShowCursor(SDL_DISABLE);
+ _isHidden = true;
+ }
+ }
+ else
+ {
+ if (_isHidden)
+ {
+ SDL_ShowCursor(SDL_ENABLE);
+ _isHidden = false;
+ }
+ }
+ }
+
+ public void Update(SDL_Event evnt)
+ {
+ switch (evnt.type)
+ {
+ case SDL_EventType.SDL_MOUSEBUTTONDOWN:
+ case SDL_EventType.SDL_MOUSEBUTTONUP:
+ uint rawButton = evnt.button.button;
+
+ if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
+ {
+ PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN;
+
+ CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
+ }
+
+ break;
+
+ // NOTE: On Linux using Wayland mouse motion events won't be received at all.
+ case SDL_EventType.SDL_MOUSEMOTION:
+ CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+
+ break;
+
+ case SDL_EventType.SDL_MOUSEWHEEL:
+ Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
+
+ break;
+ }
+ }
+
+ public void SetClientSize(int width, int height)
+ {
+ _clientSize = new Size(width, height);
+ }
+
+ public bool IsButtonPressed(MouseButton button)
+ {
+ return PressedButtons[(int)button];
+ }
+
+ public Size GetClientSize()
+ {
+ return _clientSize;
+ }
+
+ public string DriverName => "SDL2";
+
+ public event Action OnGamepadConnected
+ {
+ add { }
+ remove { }
+ }
+
+ public event Action OnGamepadDisconnected
+ {
+ add { }
+ remove { }
+ }
+
+ public ReadOnlySpan GamepadsIds => new[] { "0" };
+
+ public IGamepad GetGamepad(string id)
+ {
+ return new SDL2Mouse(this);
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs
new file mode 100644
index 00000000..62e161df
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Ryujinx.Headless.SDL2
+{
+ class StatusUpdatedEventArgs : EventArgs
+ {
+ public bool VSyncEnabled;
+ public string DockedMode;
+ public string AspectRatio;
+ public string GameStatus;
+ public string FifoStatus;
+ public string GpuName;
+
+ public StatusUpdatedEventArgs(bool vSyncEnabled, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
+ {
+ VSyncEnabled = vSyncEnabled;
+ DockedMode = dockedMode;
+ AspectRatio = aspectRatio;
+ GameStatus = gameStatus;
+ FifoStatus = fifoStatus;
+ GpuName = gpuName;
+ }
+ }
+}
diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
new file mode 100644
index 00000000..172b7685
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Input.HLE;
+using Ryujinx.SDL2.Common;
+using System;
+using System.Runtime.InteropServices;
+using static SDL2.SDL;
+
+namespace Ryujinx.Headless.SDL2.Vulkan
+{
+ class VulkanWindow : WindowBase
+ {
+ private GraphicsDebugLevel _glLogLevel;
+
+ public VulkanWindow(
+ InputManager inputManager,
+ GraphicsDebugLevel glLogLevel,
+ AspectRatio aspectRatio,
+ bool enableMouse,
+ HideCursor hideCursor)
+ : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
+ {
+ _glLogLevel = glLogLevel;
+ }
+
+ public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
+
+ protected override void InitializeWindowRenderer() { }
+
+ protected override void InitializeRenderer()
+ {
+ Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
+ MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
+ }
+
+ private void BasicInvoke(Action action)
+ {
+ action();
+ }
+
+ public unsafe IntPtr CreateWindowSurface(IntPtr instance)
+ {
+ ulong surfaceHandle = 0;
+
+ Action createSurface = () =>
+ {
+ if (SDL_Vulkan_CreateSurface(WindowHandle, instance, out surfaceHandle) == SDL_bool.SDL_FALSE)
+ {
+ string errorMessage = $"SDL_Vulkan_CreateSurface failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+ };
+
+ if (SDL2Driver.MainThreadDispatcher != null)
+ {
+ SDL2Driver.MainThreadDispatcher(createSurface);
+ }
+ else
+ {
+ createSurface();
+ }
+
+ return (IntPtr)surfaceHandle;
+ }
+
+ public unsafe string[] GetRequiredInstanceExtensions()
+ {
+ if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out uint extensionsCount, IntPtr.Zero) == SDL_bool.SDL_TRUE)
+ {
+ IntPtr[] rawExtensions = new IntPtr[(int)extensionsCount];
+ string[] extensions = new string[(int)extensionsCount];
+
+ fixed (IntPtr* rawExtensionsPtr = rawExtensions)
+ {
+ if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out extensionsCount, (IntPtr)rawExtensionsPtr) == SDL_bool.SDL_TRUE)
+ {
+ for (int i = 0; i < extensions.Length; i++)
+ {
+ extensions[i] = Marshal.PtrToStringUTF8(rawExtensions[i]);
+ }
+
+ return extensions;
+ }
+ }
+ }
+
+ string errorMessage = $"SDL_Vulkan_GetInstanceExtensions failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+
+ protected override void FinalizeWindowRenderer()
+ {
+ Device.DisposeGpu();
+ }
+
+ protected override void SwapBuffers() { }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
new file mode 100644
index 00000000..e3371042
--- /dev/null
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -0,0 +1,499 @@
+using ARMeilleure.Translation;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
+using Ryujinx.HLE.Ui;
+using Ryujinx.Input;
+using Ryujinx.Input.HLE;
+using Ryujinx.SDL2.Common;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading;
+using static SDL2.SDL;
+using Switch = Ryujinx.HLE.Switch;
+
+namespace Ryujinx.Headless.SDL2
+{
+ abstract partial class WindowBase : IHostUiHandler, IDisposable
+ {
+ protected const int DefaultWidth = 1280;
+ protected const int DefaultHeight = 720;
+ private const SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN;
+ private const int TargetFps = 60;
+
+ private static ConcurrentQueue MainThreadActions = new ConcurrentQueue();
+
+ [LibraryImport("SDL2")]
+ // TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
+ private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
+
+ public static void QueueMainThreadAction(Action action)
+ {
+ MainThreadActions.Enqueue(action);
+ }
+
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+ public Switch Device { get; private set; }
+ public IRenderer Renderer { get; private set; }
+
+ public event EventHandler StatusUpdatedEvent;
+
+ protected IntPtr WindowHandle { get; set; }
+
+ public IHostUiTheme HostUiTheme { get; }
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+
+ protected SDL2MouseDriver MouseDriver;
+ private InputManager _inputManager;
+ private IKeyboard _keyboardInterface;
+ private GraphicsDebugLevel _glLogLevel;
+ private readonly Stopwatch _chrono;
+ private readonly long _ticksPerFrame;
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+ private readonly ManualResetEvent _exitEvent;
+
+ private long _ticks;
+ private bool _isActive;
+ private bool _isStopped;
+ private uint _windowId;
+
+ private string _gpuVendorName;
+
+ private AspectRatio _aspectRatio;
+ private bool _enableMouse;
+
+ public WindowBase(
+ InputManager inputManager,
+ GraphicsDebugLevel glLogLevel,
+ AspectRatio aspectRatio,
+ bool enableMouse,
+ HideCursor hideCursor)
+ {
+ MouseDriver = new SDL2MouseDriver(hideCursor);
+ _inputManager = inputManager;
+ _inputManager.SetMouseDriver(MouseDriver);
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+ _glLogLevel = glLogLevel;
+ _chrono = new Stopwatch();
+ _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+ _exitEvent = new ManualResetEvent(false);
+ _aspectRatio = aspectRatio;
+ _enableMouse = enableMouse;
+ HostUiTheme = new HeadlessHostUiTheme();
+
+ SDL2Driver.Instance.Initialize();
+ }
+
+ public void Initialize(Switch device, List inputConfigs, bool enableKeyboard, bool enableMouse)
+ {
+ Device = device;
+
+ IRenderer renderer = Device.Gpu.Renderer;
+
+ if (renderer is ThreadedRenderer tr)
+ {
+ renderer = tr.BaseRenderer;
+ }
+
+ Renderer = renderer;
+
+ NpadManager.Initialize(device, inputConfigs, enableKeyboard, enableMouse);
+ TouchScreenManager.Initialize(device);
+ }
+
+ private void SetWindowIcon()
+ {
+ Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
+ byte[] iconBytes = new byte[iconStream!.Length];
+
+ if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
+ {
+ Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
+ iconStream.Close();
+
+ return;
+ }
+
+ iconStream.Close();
+
+ unsafe
+ {
+ fixed (byte* iconPtr = iconBytes)
+ {
+ IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
+ IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
+
+ SDL_SetWindowIcon(WindowHandle, iconHandle);
+ SDL_FreeSurface(iconHandle);
+ }
+ }
+ }
+
+ private void InitializeWindow()
+ {
+ var activeProcess = Device.Processes.ActiveApplication;
+ var nacp = activeProcess.ApplicationControlProperties;
+ int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
+
+ string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
+ string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
+ string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
+ string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
+
+ WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags());
+
+ if (WindowHandle == IntPtr.Zero)
+ {
+ string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
+
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ throw new Exception(errorMessage);
+ }
+
+ SetWindowIcon();
+
+ _windowId = SDL_GetWindowID(WindowHandle);
+ SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
+
+ Width = DefaultWidth;
+ Height = DefaultHeight;
+ }
+
+ private void HandleWindowEvent(SDL_Event evnt)
+ {
+ if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
+ {
+ switch (evnt.window.windowEvent)
+ {
+ case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
+ Width = evnt.window.data1;
+ Height = evnt.window.data2;
+ Renderer?.Window.SetSize(Width, Height);
+ MouseDriver.SetClientSize(Width, Height);
+ break;
+
+ case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
+ Exit();
+ break;
+
+ default:
+ break;
+ }
+ }
+ else
+ {
+ MouseDriver.Update(evnt);
+ }
+ }
+
+ protected abstract void InitializeWindowRenderer();
+
+ protected abstract void InitializeRenderer();
+
+ protected abstract void FinalizeWindowRenderer();
+
+ protected abstract void SwapBuffers();
+
+ public abstract SDL_WindowFlags GetWindowFlags();
+
+ private string GetGpuVendorName()
+ {
+ return Renderer.GetHardwareInfo().GpuVendor;
+ }
+
+ public void Render()
+ {
+ InitializeWindowRenderer();
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ InitializeRenderer();
+
+ _gpuVendorName = GetGpuVendorName();
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+ 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 = Device.System.State.DockedMode ? "Docked" : "Handheld";
+ float scale = Graphics.Gpu.GraphicsConfig.ResScale;
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ dockedMode,
+ Device.Configuration.AspectRatio.ToText(),
+ $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
+ $"GPU: {_gpuVendorName}"));
+
+ _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
+ }
+ }
+ });
+
+ FinalizeWindowRenderer();
+ }
+
+ public void Exit()
+ {
+ TouchScreenManager?.Dispose();
+ NpadManager?.Dispose();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _gpuCancellationTokenSource.Cancel();
+
+ _isStopped = true;
+ _isActive = false;
+
+ _exitEvent.WaitOne();
+ _exitEvent.Dispose();
+ }
+
+ public void ProcessMainThreadQueue()
+ {
+ while (MainThreadActions.TryDequeue(out Action action))
+ {
+ action();
+ }
+ }
+
+ public void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ SDL_PumpEvents();
+
+ ProcessMainThreadQueue();
+
+ // Polling becomes expensive if it's not slept
+ Thread.Sleep(1);
+ }
+
+ _exitEvent.Set();
+ }
+
+ 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);
+ }
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return true;
+ }
+
+ if (_isStopped)
+ {
+ return false;
+ }
+
+ NpadManager.Update();
+
+ // Touchscreen
+ bool hasTouch = false;
+
+ // Get screen touch position
+ if (!_enableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ TouchScreenManager.Update(false);
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ // TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
+ MouseDriver.UpdatePosition();
+
+ return true;
+ }
+
+ public void Execute()
+ {
+ _chrono.Restart();
+ _isActive = true;
+
+ InitializeWindow();
+
+ Thread renderLoopThread = new Thread(Render)
+ {
+ Name = "GUI.RenderLoop"
+ };
+ renderLoopThread.Start();
+
+ Thread nvStutterWorkaround = null;
+ if (Renderer is Graphics.OpenGL.OpenGLRenderer)
+ {
+ nvStutterWorkaround = new Thread(NVStutterWorkaround)
+ {
+ Name = "GUI.NVStutterWorkaround"
+ };
+ nvStutterWorkaround.Start();
+ }
+
+ MainLoop();
+
+ renderLoopThread.Join();
+ nvStutterWorkaround?.Join();
+
+ Exit();
+ }
+
+ public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
+ {
+ // SDL2 doesn't support input dialogs
+ userText = "Ryujinx";
+
+ return true;
+ }
+
+ public bool DisplayMessageDialog(string title, string message)
+ {
+ SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
+
+ return true;
+ }
+
+ public bool DisplayMessageDialog(ControllerAppletUiArgs args)
+ {
+ string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
+
+ string message = $"Application requests {playerCount} player(s) with:\n\n"
+ + $"TYPES: {args.SupportedStyles}\n\n"
+ + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n"
+ + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "")
+ + "Please reconfigure Input now and then press OK.";
+
+ return DisplayMessageDialog("Controller Applet", message);
+ }
+
+ public IDynamicTextInputHandler CreateDynamicTextInputHandler()
+ {
+ return new HeadlessDynamicTextInputHandler();
+ }
+
+ public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
+ {
+ device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
+
+ Exit();
+ }
+
+ public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
+ {
+ SDL_MessageBoxData data = new SDL_MessageBoxData
+ {
+ title = title,
+ message = message,
+ buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
+ numbuttons = buttonsText.Length,
+ window = WindowHandle
+ };
+
+ for (int i = 0; i < buttonsText.Length; i++)
+ {
+ data.buttons[i] = new SDL_MessageBoxButtonData
+ {
+ buttonid = i,
+ text = buttonsText[i]
+ };
+ }
+
+ SDL_ShowMessageBox(ref data, out int _);
+
+ return true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _isActive = false;
+ TouchScreenManager?.Dispose();
+ NpadManager.Dispose();
+
+ SDL2Driver.Instance.UnregisterWindow(_windowId);
+
+ SDL_DestroyWindow(WindowHandle);
+
+ SDL2Driver.Instance.Dispose();
+ }
+ }
+ }
+}
\ No newline at end of file
--
cgit v1.2.3