diff --git a/Directory.Packages.props b/Directory.Packages.props index f1d7cac614..6009313e79 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ true + diff --git a/Ryujinx.sln b/Ryujinx.sln index 9e197e85ff..a144b674bd 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -75,6 +75,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon", "src\Ryuj EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL3", "src\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj", "{3BF24278-547D-42C2-9D43-182B978F54DD}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Metal", "src\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj", "{C08931FA-1191-417A-864F-3882D93E683B}" @@ -259,6 +261,10 @@ Global {81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Debug|Any CPU.Build.0 = Debug|Any CPU {81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.ActiveCfg = Release|Any CPU {81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.Build.0 = Release|Any CPU + {3BF24278-547D-42C2-9D43-182B978F54DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BF24278-547D-42C2-9D43-182B978F54DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BF24278-547D-42C2-9D43-182B978F54DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BF24278-547D-42C2-9D43-182B978F54DD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs index 40f067aea3..4f218c0d25 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs @@ -56,6 +56,7 @@ public override MotionConfigController Read(ref Utf8JsonReader reader, Type type return motionBackendType switch { MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardMotionConfigController), + MotionInputBackendType.Handheld => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardMotionConfigController), MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, _serializerContext.CemuHookMotionConfigController), _ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"), }; @@ -66,6 +67,7 @@ public override void Write(Utf8JsonWriter writer, MotionConfigController value, switch (value.MotionBackend) { case MotionInputBackendType.GamepadDriver: + case MotionInputBackendType.Handheld: JsonSerializer.Serialize(writer, value as StandardMotionConfigController, _serializerContext.StandardMotionConfigController); break; case MotionInputBackendType.CemuHook: diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs index fd8391289c..fd09b25d83 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs @@ -9,5 +9,6 @@ public enum MotionInputBackendType : byte Invalid, GamepadDriver, CemuHook, + Handheld, } } diff --git a/src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj b/src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj new file mode 100644 index 0000000000..dc61e8b135 --- /dev/null +++ b/src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + true + + + + + + + + diff --git a/src/Ryujinx.Input.SDL3/SDL3MotionDriver.cs b/src/Ryujinx.Input.SDL3/SDL3MotionDriver.cs new file mode 100644 index 0000000000..f86ffbc4b3 --- /dev/null +++ b/src/Ryujinx.Input.SDL3/SDL3MotionDriver.cs @@ -0,0 +1,85 @@ +using SDL3; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using static SDL3.SDL3; + +namespace Ryujinx.Input.SDL3 +{ + public unsafe class SDL3MotionDriver : IHandheld, IDisposable + { + private readonly Dictionary sensors; + private bool _disposed; + + public SDL3MotionDriver() + { + int result = SDL_Init(SDL_InitFlags.Sensor); + if (result < 0) + { + throw new InvalidOperationException($"SDL sensor initialization failed: {SDL_GetError()}"); + } + sensors = SDL_GetSensors().ToArray().ToDictionary(SDL_GetSensorTypeForID, SDL_OpenSensor); + } + + ~SDL3MotionDriver() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing && sensors != null) + { + foreach (var sensor in sensors.Values) + { + if (sensor != IntPtr.Zero) + { + SDL_CloseSensor(sensor); + } + } + } + + _disposed = true; + } + + public Vector3 GetMotionData(MotionInputId inputType) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + return inputType switch + { + MotionInputId.Gyroscope => GetSensorVector(SDL_SensorType.Gyro) * 180 / MathF.PI, + MotionInputId.Accelerometer => GetSensorVector(SDL_SensorType.Accel) / SDL_STANDARD_GRAVITY, + _ => Vector3.Zero + }; + } + + private Vector3 GetSensorVector(SDL_SensorType sensorType) + { + if (!sensors.TryGetValue(sensorType, out SDL_Sensor sensor)) + { + return Vector3.Zero; + } + + var data = stackalloc float[3]; + if (SDL_GetSensorData(sensor, data, 3) < 0) + { + return Vector3.Zero; + } + + return new Vector3(data[0], data[1], data[2]); + } + } +} diff --git a/src/Ryujinx.Input/HLE/InputManager.cs b/src/Ryujinx.Input/HLE/InputManager.cs index 2825542a01..2ccc78a2c2 100644 --- a/src/Ryujinx.Input/HLE/InputManager.cs +++ b/src/Ryujinx.Input/HLE/InputManager.cs @@ -2,12 +2,13 @@ namespace Ryujinx.Input.HLE { - public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) + public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IHandheld handheld) : IDisposable { public IGamepadDriver KeyboardDriver { get; } = keyboardDriver; public IGamepadDriver GamepadDriver { get; } = gamepadDriver; public IGamepadDriver MouseDriver { get; private set; } + public IHandheld Handheld { get; } = handheld; public void SetMouseDriver(IGamepadDriver mouseDriver) { @@ -18,7 +19,7 @@ public void SetMouseDriver(IGamepadDriver mouseDriver) public NpadManager CreateNpadManager() { - return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver); + return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver, Handheld); } public TouchScreenManager CreateTouchScreenManager() @@ -38,6 +39,7 @@ protected virtual void Dispose(bool disposing) KeyboardDriver?.Dispose(); GamepadDriver?.Dispose(); MouseDriver?.Dispose(); + Handheld?.Dispose(); } } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index b77ae5662d..60e0318e7e 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -218,12 +218,14 @@ public HLEKeyboardMappingEntry(Key targetKey, byte target) public string Id { get; private set; } private readonly CemuHookClient _cemuHookClient; + private readonly IHandheld _handheld; - public NpadController(CemuHookClient cemuHookClient) + public NpadController(CemuHookClient cemuHookClient, IHandheld handheld) { State = default; Id = null; _cemuHookClient = cemuHookClient; + _handheld = handheld; } public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config) @@ -287,6 +289,18 @@ public void Update() if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion) { + if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.Handheld) + { + Vector3 accelerometer = _handheld.GetMotionData(MotionInputId.Accelerometer); + Vector3 gyroscope = _handheld.GetMotionData(MotionInputId.Gyroscope); + + accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y); + gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y); + + _leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone); + _rightMotionInput = _leftMotionInput; + } + if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver) { if (gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion)) diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index 21219d91b0..3bec2eb045 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -31,6 +31,7 @@ public class NpadManager : IDisposable private readonly IGamepadDriver _keyboardDriver; private readonly IGamepadDriver _gamepadDriver; private readonly IGamepadDriver _mouseDriver; + private readonly IHandheld _handheld; private bool _isDisposed; private List _inputConfig; @@ -38,7 +39,7 @@ public class NpadManager : IDisposable private bool _enableMouse; private Switch _device; - public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver) + public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver, IHandheld handheld) { _controllers = new NpadController[MaxControllers]; _cemuHookClient = new CemuHookClient(this); @@ -47,6 +48,7 @@ public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, _gamepadDriver = gamepadDriver; _mouseDriver = mouseDriver; _inputConfig = []; + _handheld = handheld; _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; @@ -139,7 +141,7 @@ public void ReloadConfiguration(List inputConfig, bool enableKeyboa } else { - controller = new(_cemuHookClient); + controller = new(_cemuHookClient, _handheld); } bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry); diff --git a/src/Ryujinx.Input/IHandheld.cs b/src/Ryujinx.Input/IHandheld.cs new file mode 100644 index 0000000000..b2290147ba --- /dev/null +++ b/src/Ryujinx.Input/IHandheld.cs @@ -0,0 +1,10 @@ +using System; +using System.Numerics; + +namespace Ryujinx.Input +{ + public interface IHandheld : IDisposable + { + Vector3 GetMotionData(MotionInputId gyroscope); + } +} diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 3c753e1177..1fbbeb30e5 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -8197,6 +8197,31 @@ "zh_TW": "使用與 CemuHook 相容的體感" } }, + { + "ID": "ControllerSettingsMotionUseHandheldCompatibleMotion", + "Translations": { + "ar_SA": "استخدام الحركة المتوافقة مع Hendheld", + "de_DE": "Hendheld kompatible Bewegungssteuerung", + "el_GR": "Κίνηση συμβατή με Hendheld", + "en_US": "Use Hendheld compatible motion", + "es_ES": "Usar movimiento compatible con Hendheld", + "fr_FR": "Utiliser un capteur de mouvements Hendheld", + "he_IL": "השתמש בתנועת Hendheld תואמת ", + "it_IT": "Usa sensore compatibile con Hendheld", + "ja_JP": "Hendheld 互換モーションを使用", + "ko_KR": "Hendheld 호환 모션 사용", + "no_NO": "Bruk Hendheld kompatibel bevegelse", + "pl_PL": "Użyj ruchu zgodnego z Hendheld", + "pt_BR": "Usar sensor compatível com Hendheld", + "ru_RU": "Включить совместимость с Hendheld", + "sv_SE": "Använd Hendheld-kompatibel rörelse", + "th_TH": "ใช้การเคลื่อนไหวที่เข้ากันได้กับ Hendheld", + "tr_TR": "Hendheld uyumlu hareket kullan", + "uk_UA": "Використовувати рух, сумісний з Hendheld", + "zh_CN": "使用 Hendheld 兼容的体感协议", + "zh_TW": "使用與 Hendheld 相容的體感" + } + }, { "ID": "ControllerSettingsMotionControllerSlot", "Translations": { diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index 003b8a31e3..9881531c40 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -23,6 +23,7 @@ using Ryujinx.Input; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; +using Ryujinx.Input.SDL3; using Ryujinx.SDL2.Common; using System; using System.Collections.Generic; @@ -182,7 +183,7 @@ static void Load(string[] originalArgs, Options option) _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile); _userChannelPersistence = new UserChannelPersistence(); - _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); + _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver(), new SDL3MotionDriver()); GraphicsConfig.EnableShaderCache = !option.DisableShaderCache; diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 174aadeb05..a5b08cfbd0 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -76,6 +76,7 @@ + diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 300d7977cc..021240c6f8 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -10,6 +10,7 @@ namespace Ryujinx.Ava.UI.Models.Input public partial class GamepadInputConfig : BaseModel { public bool EnableCemuHookMotion { get; set; } + public bool EnableHandheldMotion { get; set; } public string DsuServerHost { get; set; } public int DsuServerPort { get; set; } public int Slot { get; set; } @@ -162,7 +163,7 @@ public GamepadInputConfig(InputConfig config) EnableMotion = controllerInput.Motion.EnableMotion; GyroDeadzone = controllerInput.Motion.GyroDeadzone; Sensitivity = controllerInput.Motion.Sensitivity; - + EnableHandheldMotion = controllerInput.Motion.MotionBackend == MotionInputBackendType.Handheld; if (controllerInput.Motion is CemuHookMotionConfigController cemuHook) { EnableCemuHookMotion = true; @@ -285,7 +286,7 @@ public InputConfig GetConfig() config.Motion = new StandardMotionConfigController { EnableMotion = EnableMotion, - MotionBackend = MotionInputBackendType.GamepadDriver, + MotionBackend = EnableHandheldMotion ? MotionInputBackendType.Handheld : MotionInputBackendType.GamepadDriver, GyroDeadzone = GyroDeadzone, Sensitivity = Sensitivity, }; diff --git a/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs index ba8686831a..f3628d3b3d 100644 --- a/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/MotionInputViewModel.cs @@ -18,6 +18,34 @@ public partial class MotionInputViewModel : BaseModel [ObservableProperty] private double _gyroDeadzone; - [ObservableProperty] private bool _enableCemuHookMotion; + private bool _enableCemuHookMotion; + public bool EnableCemuHookMotion + { + get => _enableCemuHookMotion; + set + { + if (value) + { + EnableHandheldMotion = false; + } + _enableCemuHookMotion = value; + OnPropertyChanged(); + } + } + + private bool _enableHandheldMotion; + public bool EnableHandheldMotion + { + get => _enableHandheldMotion; + set + { + if (value) + { + EnableCemuHookMotion = false; + } + _enableHandheldMotion = value; + OnPropertyChanged(); + } + } } } diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml index 9096a06d1a..55c2850c19 100644 --- a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml @@ -61,6 +61,17 @@ Margin="5, 0" Text="{Binding GyroDeadzone, StringFormat=\{0:0.00\}}" /> + + + + diff --git a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs index 36068821f8..95a9af316c 100644 --- a/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/MotionInputView.axaml.cs @@ -30,6 +30,7 @@ public MotionInputView(ControllerInputViewModel viewModel) Sensitivity = config.Sensitivity, GyroDeadzone = config.GyroDeadzone, EnableCemuHookMotion = config.EnableCemuHookMotion, + EnableHandheldMotion = config.EnableHandheldMotion, }; InitializeComponent(); @@ -58,6 +59,7 @@ public static async Task Show(ControllerInputViewModel viewModel) config.DsuServerHost = content._viewModel.DsuServerHost; config.DsuServerPort = content._viewModel.DsuServerPort; config.EnableCemuHookMotion = content._viewModel.EnableCemuHookMotion; + config.EnableHandheldMotion = content._viewModel.EnableHandheldMotion; config.MirrorInput = content._viewModel.MirrorInput; }; diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 96b3d08aaa..2e249c8766 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -31,6 +31,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; +using Ryujinx.Input.SDL3; using System; using System.Collections.Generic; using System.Linq; @@ -109,7 +110,7 @@ public MainWindow() if (Program.PreviewerDetached) { - InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); + InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver(), new SDL3MotionDriver()); _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); this.ScalingChanged += OnScalingChanged;