[Headless] Add missing arguments & Fix typos (#4193)

* headless: Fix typos in command line options

* Remove nullable from command line options

Add EnableMacroHLE option
Add HideCursorOnIdle option

* headless: Adjust enable-ptc help text

* headless: Use switch statement instead of if-else chain

* headless: Improve formatting for long constructors

* headless: Remove discards from SDL_ShowCursor()

* headless: Add window icon

* Fix hiding cursor on idle

At least on Wayland, SDL2 doesn't produce any mouse motion events.

* Add new command line args: BaseDataDir and UserProfile

* headless: Read icon from embedded resource

* headless: Skip SetWindowIcon() on Windows if dll isn't present

* headless: Fix division by zero

* headless: Fix command line options not working correctly

* headless: Fix crash when viewing command line options

* headless: Load window icon bmp from memory

* Add comment to the workaround for SDL_LoadBMP_RW

* headless: Enable logging to file by default

* headless: Add 3 options for --hide-cursor

Replaces --disable-hide-cursor-on-idle
This commit is contained in:
TSRBerry 2023-01-09 04:55:37 +01:00 committed by GitHub
parent 610eecc1c1
commit 51b3953cfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 248 additions and 102 deletions

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Headless.SDL2
{
public enum HideCursor
{
Never,
OnIdle,
Always
}
}

View file

@ -5,7 +5,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
using System; using System;
using static SDL2.SDL; using static SDL2.SDL;
namespace Ryujinx.Headless.SDL2.OpenGL namespace Ryujinx.Headless.SDL2.OpenGL
@ -103,7 +102,13 @@ namespace Ryujinx.Headless.SDL2.OpenGL
private GraphicsDebugLevel _glLogLevel; private GraphicsDebugLevel _glLogLevel;
private SDL2OpenGLContext _openGLContext; 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; _glLogLevel = glLogLevel;
} }

View file

@ -6,6 +6,14 @@ namespace Ryujinx.Headless.SDL2
{ {
public class Options 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 // Input
[Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")] [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.")] [Option("input-profile-5", Required = false, HelpText = "Set the input profile in use for Player 5.")]
public string InputProfile5Name { get; set; } 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; } public string InputProfile6Name { get; set; }
[Option("input-profile-7", Required = false, HelpText = "Set the input profile in use for Player 7.")] [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; } public string InputIdHandheld { get; set; }
[Option("enable-keyboard", Required = false, Default = false, HelpText = "Enable or disable keyboard support (Independent from controllers binding).")] [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.")] [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.")] [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.")] [Option("list-inputs-ids", Required = false, HelpText = "List inputs ids.")]
public bool ListInputIds { get; set; } public bool ListInputIds { get; set; }
// System // System
[Option("enable-ptc", Required = false, Default = true, HelpText = "Enables profiled translation cache persistency.")] [Option("disable-ptc", Required = false, HelpText = "Disables profiled persistent translation cache.")]
public bool? EnablePtc { get; set; } public bool DisablePtc { get; set; }
[Option("enable-internet-connection", Required = false, Default = false, HelpText = "Enables guest Internet connection.")] [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.")] [Option("disable-fs-integrity-checks", Required = false, HelpText = "Disables integrity checks on Game content files.")]
public bool? EnableFsIntegrityChecks { get; set; } public bool DisableFsIntegrityChecks { get; set; }
[Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
public int FsGlobalAccessLogMode { get; set; } public int FsGlobalAccessLogMode { get; set; }
[Option("enable-vsync", Required = false, Default = true, HelpText = "Enables Vertical Sync.")] [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
public bool? EnableVsync { get; set; } public bool DisableVsync { get; set; }
[Option("enable-shader-cache", Required = false, Default = true, HelpText = "Enables Shader cache.")] [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
public bool? EnableShaderCache { get; set; } public bool DisableShaderCache { get; set; }
[Option("enable-texture-recompression", Required = false, Default = false, HelpText = "Enables Texture recompression.")] [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.")] [Option("disable-docked-mode", Required = false, HelpText = "Disables Docked Mode.")]
public bool? EnableDockedMode { get; set; } public bool DisableDockedMode { get; set; }
[Option("system-language", Required = false, Default = SystemLanguage.AmericanEnglish, HelpText = "Change System Language.")] [Option("system-language", Required = false, Default = SystemLanguage.AmericanEnglish, HelpText = "Change System Language.")]
public SystemLanguage SystemLanguage { get; set; } public SystemLanguage SystemLanguage { get; set; }
@ -120,32 +131,32 @@ namespace Ryujinx.Headless.SDL2
// Logging // Logging
[Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")] [Option("disable-file-logging", Required = false, Default = false, HelpText = "Disables logging to a file on disk.")]
public bool? EnableFileLog { get; set; } public bool DisableFileLog { get; set; }
[Option("enable-debug-logs", Required = false, Default = false, HelpText = "Enables printing debug log messages.")] [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.")] [Option("disable-stub-logs", Required = false, HelpText = "Disables printing stub log messages.")]
public bool? LoggingEnableStub { get; set; } public bool LoggingDisableStub { get; set; }
[Option("enable-info-logs", Required = false, Default = true, HelpText = "Enables printing info log messages.")] [Option("disable-info-logs", Required = false, HelpText = "Disables printing info log messages.")]
public bool? LoggingEnableInfo { get; set; } public bool LoggingDisableInfo { get; set; }
[Option("enable-warning-logs", Required = false, Default = true, HelpText = "Enables printing warning log messages.")] [Option("disable-warning-logs", Required = false, HelpText = "Disables printing warning log messages.")]
public bool? LoggingEnableWarning { get; set; } public bool LoggingDisableWarning { get; set; }
[Option("enable-error-logs", Required = false, Default = true, HelpText = "Enables printing error log messages.")] [Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")]
public bool? LoggingEnableError { get; set; } public bool LoggingEnableError { get; set; }
[Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")] [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.")] [Option("disable-guest-logs", Required = false, HelpText = "Disables printing guest log messages.")]
public bool? LoggingEnableGuest { get; set; } public bool LoggingDisableGuest { get; set; }
[Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")] [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.")] [Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")]
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; } 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.")] [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; } 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)")] [Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
public string GraphicsShadersDumpPath { get; set; } public string GraphicsShadersDumpPath { get; set; }
@ -176,10 +190,10 @@ namespace Ryujinx.Headless.SDL2
// Hacks // Hacks
[Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")] [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.")] [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
public bool? IgnoreMissingServices { get; set; } public bool IgnoreMissingServices { get; set; }
// Values // Values

View file

@ -33,7 +33,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using Key = Ryujinx.Common.Configuration.Hid.Key; using Key = Ryujinx.Common.Configuration.Hid.Key;
@ -63,20 +62,6 @@ namespace Ryujinx.Headless.SDL2
Console.Title = $"Ryujinx Console {Version} (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()) if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{ {
AutoResetEvent invoked = new AutoResetEvent(false); 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<Options>(args) Parser.Default.ParseArguments<Options>(args)
.WithParsed(options => Load(options)) .WithParsed(Load)
.WithNotParsed(errors => errors.Output()); .WithNotParsed(errors => errors.Output());
_inputManager.Dispose();
} }
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
@ -343,6 +322,24 @@ namespace Ryujinx.Headless.SDL2
static void Load(Options option) 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; IGamepad gamepad;
if (option.ListInputIds) if (option.ListInputIds)
@ -378,8 +375,8 @@ namespace Ryujinx.Headless.SDL2
} }
_inputConfiguration = new List<InputConfig>(); _inputConfiguration = new List<InputConfig>();
_enableKeyboard = (bool)option.EnableKeyboard; _enableKeyboard = option.EnableKeyboard;
_enableMouse = (bool)option.EnableMouse; _enableMouse = option.EnableMouse;
void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
{ {
@ -407,16 +404,16 @@ namespace Ryujinx.Headless.SDL2
} }
// Setup logging level // Setup logging level
Logger.SetEnable(LogLevel.Debug, (bool)option.LoggingEnableDebug); Logger.SetEnable(LogLevel.Debug, option.LoggingEnableDebug);
Logger.SetEnable(LogLevel.Stub, (bool)option.LoggingEnableStub); Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub);
Logger.SetEnable(LogLevel.Info, (bool)option.LoggingEnableInfo); Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo);
Logger.SetEnable(LogLevel.Warning, (bool)option.LoggingEnableWarning); Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning);
Logger.SetEnable(LogLevel.Error, (bool)option.LoggingEnableError); Logger.SetEnable(LogLevel.Error, option.LoggingEnableError);
Logger.SetEnable(LogLevel.Trace, (bool)option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
Logger.SetEnable(LogLevel.Guest, (bool)option.LoggingEnableGuest); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
Logger.SetEnable(LogLevel.AccessLog, (bool)option.LoggingEnableFsAccessLog); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
if ((bool)option.EnableFileLog) if (!option.DisableFileLog)
{ {
Logger.AddTarget(new AsyncLogTargetWrapper( Logger.AddTarget(new AsyncLogTargetWrapper(
new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"), new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
@ -426,11 +423,12 @@ namespace Ryujinx.Headless.SDL2
} }
// Setup graphics configuration // Setup graphics configuration
GraphicsConfig.EnableShaderCache = (bool)option.EnableShaderCache; GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
GraphicsConfig.EnableTextureRecompression = (bool)option.EnableTextureRecompression; GraphicsConfig.EnableTextureRecompression = option.EnableTextureRecompression;
GraphicsConfig.ResScale = option.ResScale; GraphicsConfig.ResScale = option.ResScale;
GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy; GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy;
GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath; GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath;
GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
while (true) while (true)
{ {
@ -443,6 +441,8 @@ namespace Ryujinx.Headless.SDL2
_userChannelPersistence.ShouldRestart = false; _userChannelPersistence.ShouldRestart = false;
} }
_inputManager.Dispose();
} }
private static void SetupProgressHandler() private static void SetupProgressHandler()
@ -479,8 +479,8 @@ namespace Ryujinx.Headless.SDL2
private static WindowBase CreateWindow(Options options) private static WindowBase CreateWindow(Options options)
{ {
return options.GraphicsBackend == GraphicsBackend.Vulkan return options.GraphicsBackend == GraphicsBackend.Vulkan
? new VulkanWindow(_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, (bool)options.EnableMouse); : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor);
} }
private static IRenderer CreateRenderer(Options options, WindowBase window) private static IRenderer CreateRenderer(Options options, WindowBase window)
@ -533,20 +533,20 @@ namespace Ryujinx.Headless.SDL2
_userChannelPersistence, _userChannelPersistence,
renderer, renderer,
new SDL2HardwareDeviceDriver(), new SDL2HardwareDeviceDriver(),
(bool)options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB, options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
window, window,
options.SystemLanguage, options.SystemLanguage,
options.SystemRegion, options.SystemRegion,
(bool)options.EnableVsync, !options.DisableVsync,
(bool)options.EnableDockedMode, !options.DisableDockedMode,
(bool)options.EnablePtc, !options.DisablePtc,
(bool)options.EnableInternetAccess, options.EnableInternetAccess,
(bool)options.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
options.FsGlobalAccessLogMode, options.FsGlobalAccessLogMode,
options.SystemTimeOffset, options.SystemTimeOffset,
options.SystemTimeZone, options.SystemTimeZone,
options.MemoryManagerMode, options.MemoryManagerMode,
(bool)options.IgnoreMissingServices, options.IgnoreMissingServices,
options.AspectRatio, options.AspectRatio,
options.AudioVolume); options.AudioVolume);
@ -649,7 +649,7 @@ namespace Ryujinx.Headless.SDL2
} }
else 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(); _emulationContext.Dispose();

View file

@ -42,6 +42,10 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Ryujinx.bmp" />
</ItemGroup>
<!-- Due to .net core 3.1 embedded resource loading --> <!-- Due to .net core 3.1 embedded resource loading -->
<PropertyGroup> <PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention> <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -10,7 +10,12 @@ namespace Ryujinx.Headless.SDL2
{ {
class SDL2MouseDriver : IGamepadDriver class SDL2MouseDriver : IGamepadDriver
{ {
private const int CursorHideIdleTime = 8; // seconds
private bool _isDisposed; private bool _isDisposed;
private HideCursor _hideCursor;
private bool _isHidden;
private long _lastCursorMoveTime;
public bool[] PressedButtons { get; } public bool[] PressedButtons { get; }
@ -18,9 +23,16 @@ namespace Ryujinx.Headless.SDL2
public Vector2 Scroll { get; private set; } public Vector2 Scroll { get; private set; }
public Size _clientSize; public Size _clientSize;
public SDL2MouseDriver() public SDL2MouseDriver(HideCursor hideCursor)
{ {
PressedButtons = new bool[(int)MouseButton.Count]; PressedButtons = new bool[(int)MouseButton.Count];
_hideCursor = hideCursor;
if (_hideCursor == HideCursor.Always)
{
SDL_ShowCursor(SDL_DISABLE);
_isHidden = true;
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -31,10 +43,53 @@ namespace Ryujinx.Headless.SDL2
return (MouseButton)(rawButton - 1); 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) public void Update(SDL_Event evnt)
{ {
if (evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP) switch (evnt.type)
{ {
case SDL_EventType.SDL_MOUSEBUTTONDOWN:
case SDL_EventType.SDL_MOUSEBUTTONUP:
uint rawButton = evnt.button.button; uint rawButton = evnt.button.button;
if (rawButton > 0 && rawButton <= (int)MouseButton.Count) if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
@ -43,14 +98,20 @@ namespace Ryujinx.Headless.SDL2
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y); CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
} }
}
else if (evnt.type == SDL_EventType.SDL_MOUSEMOTION) 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); CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
} _lastCursorMoveTime = Stopwatch.GetTimestamp();
else if (evnt.type == SDL_EventType.SDL_MOUSEWHEEL)
{ break;
case SDL_EventType.SDL_MOUSEWHEEL:
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y); Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
break;
} }
} }

View file

@ -12,7 +12,13 @@ namespace Ryujinx.Headless.SDL2.Vulkan
{ {
private GraphicsDebugLevel _glLogLevel; 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; _glLogLevel = glLogLevel;
} }

View file

@ -14,13 +14,16 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using static SDL2.SDL; using static SDL2.SDL;
using Switch = Ryujinx.HLE.Switch; using Switch = Ryujinx.HLE.Switch;
namespace Ryujinx.Headless.SDL2 namespace Ryujinx.Headless.SDL2
{ {
abstract class WindowBase : IHostUiHandler, IDisposable abstract partial class WindowBase : IHostUiHandler, IDisposable
{ {
protected const int DefaultWidth = 1280; protected const int DefaultWidth = 1280;
protected const int DefaultHeight = 720; protected const int DefaultHeight = 720;
@ -29,6 +32,10 @@ namespace Ryujinx.Headless.SDL2
private static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>(); private static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();
[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) public static void QueueMainThreadAction(Action action)
{ {
MainThreadActions.Enqueue(action); MainThreadActions.Enqueue(action);
@ -66,9 +73,14 @@ namespace Ryujinx.Headless.SDL2
private AspectRatio _aspectRatio; private AspectRatio _aspectRatio;
private bool _enableMouse; 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 = inputManager;
_inputManager.SetMouseDriver(MouseDriver); _inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager(); NpadManager = _inputManager.CreateNpadManager();
@ -103,6 +115,34 @@ namespace Ryujinx.Headless.SDL2
TouchScreenManager.Initialize(device); 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() private void InitializeWindow()
{ {
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty
@ -127,6 +167,8 @@ namespace Ryujinx.Headless.SDL2
throw new Exception(errorMessage); throw new Exception(errorMessage);
} }
SetWindowIcon();
_windowId = SDL_GetWindowID(WindowHandle); _windowId = SDL_GetWindowID(WindowHandle);
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent); SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
@ -146,9 +188,11 @@ namespace Ryujinx.Headless.SDL2
Renderer?.Window.SetSize(Width, Height); Renderer?.Window.SetSize(Width, Height);
MouseDriver.SetClientSize(Width, Height); MouseDriver.SetClientSize(Width, Height);
break; break;
case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE: case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
Exit(); Exit();
break; break;
default: default:
break; break;
} }
@ -331,6 +375,9 @@ namespace Ryujinx.Headless.SDL2
Device.Hid.DebugPad.Update(); Device.Hid.DebugPad.Update();
// TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
MouseDriver.UpdatePosition();
return true; return true;
} }

View file

@ -163,7 +163,7 @@ namespace Ryujinx.Input.HLE
ReloadConfiguration(inputConfig, enableKeyboard, enableMouse); ReloadConfiguration(inputConfig, enableKeyboard, enableMouse);
} }
public void Update(float aspectRatio = 0) public void Update(float aspectRatio = 1)
{ {
lock (_lock) lock (_lock)
{ {