diff --git a/Ryujinx.Headless.SDL2/HideCursor.cs b/Ryujinx.Headless.SDL2/HideCursor.cs new file mode 100644 index 00000000..2dc0bd6a --- /dev/null +++ b/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/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs index d1d0872b..69b0f42f 100644 --- a/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs +++ b/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Graphics.OpenGL; using Ryujinx.Input.HLE; using System; - using static SDL2.SDL; namespace Ryujinx.Headless.SDL2.OpenGL @@ -103,7 +102,13 @@ namespace Ryujinx.Headless.SDL2.OpenGL private GraphicsDebugLevel _glLogLevel; private SDL2OpenGLContext _openGLContext; - public OpenGLWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse) + public OpenGLWindow( + InputManager inputManager, + GraphicsDebugLevel glLogLevel, + AspectRatio aspectRatio, + bool enableMouse, + HideCursor hideCursor) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor) { _glLogLevel = glLogLevel; } @@ -161,4 +166,4 @@ namespace Ryujinx.Headless.SDL2.OpenGL SDL_GL_SwapWindow(WindowHandle); } } -} +} \ No newline at end of file diff --git a/Ryujinx.Headless.SDL2/Options.cs b/Ryujinx.Headless.SDL2/Options.cs index 209ce228..49233bce 100644 --- a/Ryujinx.Headless.SDL2/Options.cs +++ b/Ryujinx.Headless.SDL2/Options.cs @@ -6,6 +6,14 @@ 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.")] @@ -23,7 +31,7 @@ namespace Ryujinx.Headless.SDL2 [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 5.")] + [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.")] @@ -63,42 +71,45 @@ namespace Ryujinx.Headless.SDL2 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; } + public bool EnableKeyboard { get; set; } [Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")] - public bool? EnableMouse { get; set; } + 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; } + public bool ListInputProfiles { get; set; } [Option("list-inputs-ids", Required = false, HelpText = "List inputs ids.")] public bool ListInputIds { get; set; } // System - [Option("enable-ptc", Required = false, Default = true, HelpText = "Enables profiled translation cache persistency.")] - public bool? EnablePtc { get; set; } + [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; } + public bool EnableInternetAccess { get; set; } - [Option("enable-fs-integrity-checks", Required = false, Default = true, HelpText = "Enables integrity checks on Game content files.")] - public bool? EnableFsIntegrityChecks { 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("enable-vsync", Required = false, Default = true, HelpText = "Enables Vertical Sync.")] - public bool? EnableVsync { get; set; } + [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] + public bool DisableVsync { get; set; } - [Option("enable-shader-cache", Required = false, Default = true, HelpText = "Enables Shader cache.")] - public bool? EnableShaderCache { 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; } + public bool EnableTextureRecompression { get; set; } - [Option("enable-docked-mode", Required = false, Default = true, HelpText = "Enables Docked Mode.")] - public bool? EnableDockedMode { 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; } @@ -120,32 +131,32 @@ namespace Ryujinx.Headless.SDL2 // Logging - [Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")] - public bool? EnableFileLog { get; set; } + [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; } + public bool LoggingEnableDebug { get; set; } - [Option("enable-stub-logs", Required = false, Default = true, HelpText = "Enables printing stub log messages.")] - public bool? LoggingEnableStub { get; set; } + [Option("disable-stub-logs", Required = false, HelpText = "Disables printing stub log messages.")] + public bool LoggingDisableStub { get; set; } - [Option("enable-info-logs", Required = false, Default = true, HelpText = "Enables printing info log messages.")] - public bool? LoggingEnableInfo { get; set; } + [Option("disable-info-logs", Required = false, HelpText = "Disables printing info log messages.")] + public bool LoggingDisableInfo { get; set; } - [Option("enable-warning-logs", Required = false, Default = true, HelpText = "Enables printing warning log messages.")] - public bool? LoggingEnableWarning { get; set; } + [Option("disable-warning-logs", Required = false, HelpText = "Disables printing warning log messages.")] + public bool LoggingDisableWarning { get; set; } - [Option("enable-error-logs", Required = false, Default = true, HelpText = "Enables printing error log messages.")] - public bool? LoggingEnableError { 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; } + public bool LoggingEnableTrace { get; set; } - [Option("enable-guest-logs", Required = false, Default = true, HelpText = "Enables printing guest log messages.")] - public bool? LoggingEnableGuest { 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; } + 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; } @@ -164,6 +175,9 @@ namespace Ryujinx.Headless.SDL2 [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; } @@ -176,14 +190,14 @@ namespace Ryujinx.Headless.SDL2 // 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; } + public bool ExpandRam { get; set; } [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] - public bool? IgnoreMissingServices { get; set; } + 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/Ryujinx.Headless.SDL2/Program.cs b/Ryujinx.Headless.SDL2/Program.cs index 84363e1f..6ea3a98d 100644 --- a/Ryujinx.Headless.SDL2/Program.cs +++ b/Ryujinx.Headless.SDL2/Program.cs @@ -33,7 +33,6 @@ 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; @@ -63,20 +62,6 @@ namespace Ryujinx.Headless.SDL2 Console.Title = $"Ryujinx Console {Version} (Headless SDL2)"; - AppDataManager.Initialize(null); - - _virtualFileSystem = VirtualFileSystem.CreateInstance(); - _libHacHorizonManager = new LibHacHorizonManager(); - - _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); - _libHacHorizonManager.InitializeArpServer(); - _libHacHorizonManager.InitializeBcatServer(); - _libHacHorizonManager.InitializeSystemClients(); - - _contentManager = new ContentManager(_virtualFileSystem); - _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient); - _userChannelPersistence = new UserChannelPersistence(); - if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) { AutoResetEvent invoked = new AutoResetEvent(false); @@ -97,15 +82,9 @@ namespace Ryujinx.Headless.SDL2 }; } - _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); - - GraphicsConfig.EnableShaderCache = true; - Parser.Default.ParseArguments(args) - .WithParsed(options => Load(options)) + .WithParsed(Load) .WithNotParsed(errors => errors.Output()); - - _inputManager.Dispose(); } private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) @@ -343,6 +322,24 @@ namespace Ryujinx.Headless.SDL2 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) @@ -378,8 +375,8 @@ namespace Ryujinx.Headless.SDL2 } _inputConfiguration = new List(); - _enableKeyboard = (bool)option.EnableKeyboard; - _enableMouse = (bool)option.EnableMouse; + _enableKeyboard = option.EnableKeyboard; + _enableMouse = option.EnableMouse; void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) { @@ -407,16 +404,16 @@ namespace Ryujinx.Headless.SDL2 } // Setup logging level - Logger.SetEnable(LogLevel.Debug, (bool)option.LoggingEnableDebug); - Logger.SetEnable(LogLevel.Stub, (bool)option.LoggingEnableStub); - Logger.SetEnable(LogLevel.Info, (bool)option.LoggingEnableInfo); - Logger.SetEnable(LogLevel.Warning, (bool)option.LoggingEnableWarning); - Logger.SetEnable(LogLevel.Error, (bool)option.LoggingEnableError); - Logger.SetEnable(LogLevel.Trace, (bool)option.LoggingEnableTrace); - Logger.SetEnable(LogLevel.Guest, (bool)option.LoggingEnableGuest); - Logger.SetEnable(LogLevel.AccessLog, (bool)option.LoggingEnableFsAccessLog); + 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 ((bool)option.EnableFileLog) + if (!option.DisableFileLog) { Logger.AddTarget(new AsyncLogTargetWrapper( new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"), @@ -426,11 +423,12 @@ namespace Ryujinx.Headless.SDL2 } // Setup graphics configuration - GraphicsConfig.EnableShaderCache = (bool)option.EnableShaderCache; - GraphicsConfig.EnableTextureRecompression = (bool)option.EnableTextureRecompression; + 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) { @@ -443,6 +441,8 @@ namespace Ryujinx.Headless.SDL2 _userChannelPersistence.ShouldRestart = false; } + + _inputManager.Dispose(); } private static void SetupProgressHandler() @@ -479,8 +479,8 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse); + ? 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) @@ -533,20 +533,20 @@ namespace Ryujinx.Headless.SDL2 _userChannelPersistence, renderer, new SDL2HardwareDeviceDriver(), - (bool)options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB, + options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB, window, options.SystemLanguage, options.SystemRegion, - (bool)options.EnableVsync, - (bool)options.EnableDockedMode, - (bool)options.EnablePtc, - (bool)options.EnableInternetAccess, - (bool)options.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, + !options.DisableVsync, + !options.DisableDockedMode, + !options.DisablePtc, + options.EnableInternetAccess, + !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, options.FsGlobalAccessLogMode, options.SystemTimeOffset, options.SystemTimeZone, options.MemoryManagerMode, - (bool)options.IgnoreMissingServices, + options.IgnoreMissingServices, options.AspectRatio, options.AudioVolume); @@ -649,7 +649,7 @@ namespace Ryujinx.Headless.SDL2 } else { - Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); + Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{options.InputPath}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); _emulationContext.Dispose(); diff --git a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index 44a16205..81ef53fe 100644 --- a/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -42,6 +42,10 @@ + + + + false diff --git a/Ryujinx.Headless.SDL2/Ryujinx.bmp b/Ryujinx.Headless.SDL2/Ryujinx.bmp new file mode 100644 index 00000000..413f3b21 Binary files /dev/null and b/Ryujinx.Headless.SDL2/Ryujinx.bmp differ diff --git a/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs b/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs index 236d4769..bdf428cc 100644 --- a/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs +++ b/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs @@ -10,7 +10,12 @@ namespace Ryujinx.Headless.SDL2 { class SDL2MouseDriver : IGamepadDriver { + private const int CursorHideIdleTime = 8; // seconds + private bool _isDisposed; + private HideCursor _hideCursor; + private bool _isHidden; + private long _lastCursorMoveTime; public bool[] PressedButtons { get; } @@ -18,9 +23,16 @@ namespace Ryujinx.Headless.SDL2 public Vector2 Scroll { get; private set; } public Size _clientSize; - public SDL2MouseDriver() + 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)] @@ -31,26 +43,75 @@ namespace Ryujinx.Headless.SDL2 return (MouseButton)(rawButton - 1); } - public void Update(SDL_Event evnt) + public void UpdatePosition() { - if (evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP) + SDL_GetMouseState(out int posX, out int posY); + Vector2 position = new(posX, posY); + + if (CurrentPosition != position) { - uint rawButton = evnt.button.button; + CurrentPosition = position; + _lastCursorMoveTime = Stopwatch.GetTimestamp(); + } - if (rawButton > 0 && rawButton <= (int)MouseButton.Count) + CheckIdle(); + } + + private void CheckIdle() + { + if (_hideCursor != HideCursor.OnIdle) + { + return; + } + + long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime; + + if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) + { + if (!_isHidden) { - PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN; - - CurrentPosition = new Vector2(evnt.button.x, evnt.button.y); + SDL_ShowCursor(SDL_DISABLE); + _isHidden = true; } } - else if (evnt.type == SDL_EventType.SDL_MOUSEMOTION) + else { - CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y); + if (_isHidden) + { + SDL_ShowCursor(SDL_ENABLE); + _isHidden = false; + } } - else if (evnt.type == SDL_EventType.SDL_MOUSEWHEEL) + } + + public void Update(SDL_Event evnt) + { + switch (evnt.type) { - Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y); + 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; } } @@ -100,4 +161,4 @@ namespace Ryujinx.Headless.SDL2 _isDisposed = true; } } -} +} \ No newline at end of file diff --git a/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index 18323339..172b7685 100644 --- a/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -12,7 +12,13 @@ namespace Ryujinx.Headless.SDL2.Vulkan { private GraphicsDebugLevel _glLogLevel; - public VulkanWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse) + public VulkanWindow( + InputManager inputManager, + GraphicsDebugLevel glLogLevel, + AspectRatio aspectRatio, + bool enableMouse, + HideCursor hideCursor) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor) { _glLogLevel = glLogLevel; } @@ -95,4 +101,4 @@ namespace Ryujinx.Headless.SDL2.Vulkan protected override void SwapBuffers() { } } -} +} \ No newline at end of file diff --git a/Ryujinx.Headless.SDL2/WindowBase.cs b/Ryujinx.Headless.SDL2/WindowBase.cs index 88b0d573..db6c8ec4 100644 --- a/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/Ryujinx.Headless.SDL2/WindowBase.cs @@ -14,13 +14,16 @@ 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 class WindowBase : IHostUiHandler, IDisposable + abstract partial class WindowBase : IHostUiHandler, IDisposable { protected const int DefaultWidth = 1280; protected const int DefaultHeight = 720; @@ -29,6 +32,10 @@ namespace Ryujinx.Headless.SDL2 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); @@ -66,9 +73,14 @@ namespace Ryujinx.Headless.SDL2 private AspectRatio _aspectRatio; private bool _enableMouse; - public WindowBase(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) + public WindowBase( + InputManager inputManager, + GraphicsDebugLevel glLogLevel, + AspectRatio aspectRatio, + bool enableMouse, + HideCursor hideCursor) { - MouseDriver = new SDL2MouseDriver(); + MouseDriver = new SDL2MouseDriver(hideCursor); _inputManager = inputManager; _inputManager.SetMouseDriver(MouseDriver); NpadManager = _inputManager.CreateNpadManager(); @@ -103,6 +115,34 @@ namespace Ryujinx.Headless.SDL2 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() { string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty @@ -127,6 +167,8 @@ namespace Ryujinx.Headless.SDL2 throw new Exception(errorMessage); } + SetWindowIcon(); + _windowId = SDL_GetWindowID(WindowHandle); SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent); @@ -146,9 +188,11 @@ namespace Ryujinx.Headless.SDL2 Renderer?.Window.SetSize(Width, Height); MouseDriver.SetClientSize(Width, Height); break; + case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE: Exit(); break; + default: break; } @@ -331,6 +375,9 @@ namespace Ryujinx.Headless.SDL2 Device.Hid.DebugPad.Update(); + // TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform. + MouseDriver.UpdatePosition(); + return true; } @@ -451,4 +498,4 @@ namespace Ryujinx.Headless.SDL2 } } } -} +} \ No newline at end of file diff --git a/Ryujinx.Input/HLE/NpadManager.cs b/Ryujinx.Input/HLE/NpadManager.cs index 34e05687..5290ecbb 100644 --- a/Ryujinx.Input/HLE/NpadManager.cs +++ b/Ryujinx.Input/HLE/NpadManager.cs @@ -163,7 +163,7 @@ namespace Ryujinx.Input.HLE ReloadConfiguration(inputConfig, enableKeyboard, enableMouse); } - public void Update(float aspectRatio = 0) + public void Update(float aspectRatio = 1) { lock (_lock) {