WIP: SDL3 #533

Closed
madwind wants to merge 64 commits from SDL3 into master
24 changed files with 10732 additions and 5 deletions
Showing only changes of commit 2c26388dde - Show all commits

View File

@ -14,10 +14,10 @@
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0"/>
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0"/>
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Concentus" Version="2.2.0" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
@ -41,7 +41,7 @@
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Ryujinx.SDL3-CS" Version="2.30.0-build32" />
<PackageVersion Include="Gommon" Version="2.7.0.1" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />

View File

@ -95,6 +95,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL3.Common", "src\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj", "{7C70B441-F3D1-41FE-A648-19014BFB88D9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL3", "src\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj", "{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL3-CS", "src\Ryujinx.SDL3-CS\Ryujinx.SDL3-CS.csproj", "{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -259,6 +267,22 @@ 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
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Release|Any CPU.Build.0 = Release|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Release|Any CPU.Build.0 = Release|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Release|Any CPU.Build.0 = Release|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
namespace Ryujinx.Audio.Backends.SDL3
{
class SDL3AudioBuffer
{
public readonly ulong DriverIdentifier;
public readonly ulong SampleCount;
public ulong SamplePlayed;
public SDL3AudioBuffer(ulong driverIdentifier, ulong sampleCount)
{
DriverIdentifier = driverIdentifier;
SampleCount = sampleCount;
SamplePlayed = 0;
}
}
}

View File

@ -0,0 +1,208 @@
// using Ryujinx.Audio.Common;
// using Ryujinx.Audio.Integration;
// using Ryujinx.Common.Logging;
// using Ryujinx.Memory;
// using Ryujinx.SDL3.Common;
// using System;
// using System.Collections.Concurrent;
// using System.Runtime.InteropServices;
// using System.Threading;
// using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
// using static SDL3.SDL;
//
// namespace Ryujinx.Audio.Backends.SDL3
// {
// public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
// {
// private readonly ManualResetEvent _updateRequiredEvent;
// private readonly ManualResetEvent _pauseEvent;
// private readonly ConcurrentDictionary<SDL3HardwareDeviceSession, byte> _sessions;
//
// private readonly bool _supportSurroundConfiguration;
//
// public float Volume { get; set; }
//
// // TODO: Add this to SDL3-CS
// // NOTE: We use a DllImport here because of marshaling issue for spec.
// #pragma warning disable SYSLIB1054
// [DllImport("SDL3")]
// private static extern int SDL_GetDefaultAudioInfo(nint name, out SDL_AudioSpec spec, int isCapture);
// #pragma warning restore SYSLIB1054
//
// public SDL3HardwareDeviceDriver()
// {
// _updateRequiredEvent = new ManualResetEvent(false);
// _pauseEvent = new ManualResetEvent(true);
// _sessions = new ConcurrentDictionary<SDL3HardwareDeviceSession, byte>();
//
// SDL3Driver.Instance.Initialize();
//
// int res = SDL_GetDefaultAudioInfo(nint.Zero, out var spec, 0);
//
// if (res != 0)
// {
// Logger.Error?.Print(LogClass.Application,
// $"SDL_GetDefaultAudioInfo failed with error \"{SDL_GetError()}\"");
//
// _supportSurroundConfiguration = true;
// }
// else
// {
// _supportSurroundConfiguration = spec.channels >= 6;
// }
//
// Volume = 1f;
// }
//
// public static bool IsSupported => IsSupportedInternal();
//
// private static bool IsSupportedInternal()
// {
// uint device = OpenStream(SampleFormat.PcmInt16, Constants.TargetSampleRate, Constants.ChannelCountMax, Constants.TargetSampleCount, null);
//
// if (device != 0)
// {
// SDL_CloseAudioDevice(device);
// }
//
// return device != 0;
// }
//
// public ManualResetEvent GetUpdateRequiredEvent()
// {
// return _updateRequiredEvent;
// }
//
// public ManualResetEvent GetPauseEvent()
// {
// return _pauseEvent;
// }
//
// public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
// {
// if (channelCount == 0)
// {
// channelCount = 2;
// }
//
// if (sampleRate == 0)
// {
// sampleRate = Constants.TargetSampleRate;
// }
//
// if (direction != Direction.Output)
// {
// throw new NotImplementedException("Input direction is currently not implemented on SDL3 backend!");
// }
//
// SDL3HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
//
// _sessions.TryAdd(session, 0);
//
// return session;
// }
//
// internal bool Unregister(SDL3HardwareDeviceSession session)
// {
// return _sessions.TryRemove(session, out _);
// }
//
// private static SDL_AudioSpec GetSDL3Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount)
// {
// return new SDL_AudioSpec
// {
// channels = (byte)requestedChannelCount,
// format = GetSDL3Format(requestedSampleFormat),
// freq = (int)requestedSampleRate,
// samples = (ushort)sampleCount,
// };
// }
//
// internal static ushort GetSDL3Format(SampleFormat format)
// {
// return format switch
// {
// SampleFormat.PcmInt8 => AUDIO_S8,
// SampleFormat.PcmInt16 => AUDIO_S16,
// SampleFormat.PcmInt32 => AUDIO_S32,
// SampleFormat.PcmFloat => AUDIO_F32,
// _ => throw new ArgumentException($"Unsupported sample format {format}"),
// };
// }
//
// internal static uint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount, SDL_AudioCallback callback)
// {
// SDL_AudioSpec desired = GetSDL3Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount, sampleCount);
//
// desired.callback = callback;
//
// uint device = SDL_OpenAudioDevice(nint.Zero, 0, ref desired, out SDL_AudioSpec got, 0);
//
// if (device == 0)
// {
// Logger.Error?.Print(LogClass.Application, $"SDL3 open audio device initialization failed with error \"{SDL_GetError()}\"");
//
// return 0;
// }
//
// bool isValid = got.format == desired.format && got.freq == desired.freq && got.channels == desired.channels;
//
// if (!isValid)
// {
// Logger.Error?.Print(LogClass.Application, "SDL3 open audio device is not valid");
// SDL_CloseAudioDevice(device);
//
// return 0;
// }
//
// return device;
// }
//
// public void Dispose()
// {
// GC.SuppressFinalize(this);
// Dispose(true);
// }
//
// protected virtual void Dispose(bool disposing)
// {
// if (disposing)
// {
// foreach (SDL3HardwareDeviceSession session in _sessions.Keys)
// {
// session.Dispose();
// }
//
// SDL3Driver.Instance.Dispose();
//
// _pauseEvent.Dispose();
// }
// }
//
// public bool SupportsSampleRate(uint sampleRate)
// {
// return true;
// }
//
// public bool SupportsSampleFormat(SampleFormat sampleFormat)
// {
// return sampleFormat != SampleFormat.PcmInt24;
// }
//
// public bool SupportsChannelCount(uint channelCount)
// {
// if (channelCount == 6)
// {
// return _supportSurroundConfiguration;
// }
//
// return true;
// }
//
// public bool SupportsDirection(Direction direction)
// {
// // TODO: add direction input when supported.
// return direction == Direction.Output;
// }
// }
// }

View File

@ -0,0 +1,234 @@
// using Ryujinx.Audio.Backends.Common;
// using Ryujinx.Audio.Common;
// using Ryujinx.Common.Logging;
// using Ryujinx.Common.Memory;
// using Ryujinx.Memory;
// using System;
// using System.Buffers;
// using System.Collections.Concurrent;
// using System.Threading;
//
// using static SDL3.SDL;
//
// namespace Ryujinx.Audio.Backends.SDL3
// {
// class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
// {
// private readonly SDL3HardwareDeviceDriver _driver;
// private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
// private readonly DynamicRingBuffer _ringBuffer;
// private ulong _playedSampleCount;
// private readonly ManualResetEvent _updateRequiredEvent;
// private uint _outputStream;
// private bool _hasSetupError;
// private readonly SDL_AudioCallback _callbackDelegate;
// private readonly int _bytesPerFrame;
// private uint _sampleCount;
// private bool _started;
// private float _volume;
// private readonly ushort _nativeSampleFormat;
//
// public SDL3HardwareDeviceSession(SDL3HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
// {
// _driver = driver;
// _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
// _queuedBuffers = new ConcurrentQueue<SDL3AudioBuffer>();
// _ringBuffer = new DynamicRingBuffer();
// _callbackDelegate = Update;
// _bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount;
// _nativeSampleFormat = SDL3HardwareDeviceDriver.GetSDL3Format(RequestedSampleFormat);
// _sampleCount = uint.MaxValue;
// _started = false;
// _volume = 1f;
// }
//
// private void EnsureAudioStreamSetup(AudioBuffer buffer)
// {
// uint bufferSampleCount = (uint)GetSampleCount(buffer);
// bool needAudioSetup = (_outputStream == 0 && !_hasSetupError) ||
// (bufferSampleCount >= Constants.TargetSampleCount && bufferSampleCount < _sampleCount);
//
// if (needAudioSetup)
// {
// _sampleCount = Math.Max(Constants.TargetSampleCount, bufferSampleCount);
//
// uint newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount, _sampleCount, _callbackDelegate);
//
// _hasSetupError = newOutputStream == 0;
//
// if (!_hasSetupError)
// {
// if (_outputStream != 0)
// {
// SDL_CloseAudioDevice(_outputStream);
// }
//
// _outputStream = newOutputStream;
//
// SDL_PauseAudioDevice(_outputStream, _started ? 0 : 1);
//
// Logger.Info?.Print(LogClass.Audio, $"New audio stream setup with a target sample count of {_sampleCount}");
// }
// }
// }
//
// private unsafe void Update(nint userdata, nint stream, int streamLength)
// {
// Span<byte> streamSpan = new((void*)stream, streamLength);
//
// int maxFrameCount = (int)GetSampleCount(streamLength);
// int bufferedFrames = _ringBuffer.Length / _bytesPerFrame;
//
// int frameCount = Math.Min(bufferedFrames, maxFrameCount);
//
// if (frameCount == 0)
// {
// // SDL3 left the responsibility to the user to clear the buffer.
// streamSpan.Clear();
//
// return;
// }
//
// using SpanOwner<byte> samplesOwner = SpanOwner<byte>.Rent(frameCount * _bytesPerFrame);
//
// Span<byte> samples = samplesOwner.Span;
//
// _ringBuffer.Read(samples, 0, samples.Length);
//
// fixed (byte* p = samples)
// {
// nint pStreamSrc = (nint)p;
//
// // Zero the dest buffer
// streamSpan.Clear();
//
// // Apply volume to written data
// SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_driver.Volume * _volume * SDL_MIX_MAXVOLUME));
// }
//
// ulong sampleCount = GetSampleCount(samples.Length);
//
// ulong availaibleSampleCount = sampleCount;
//
// bool needUpdate = false;
//
// while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer))
// {
// ulong sampleStillNeeded = driverBuffer.SampleCount - Interlocked.Read(ref driverBuffer.SamplePlayed);
// ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount);
//
// ulong currentSamplePlayed = Interlocked.Add(ref driverBuffer.SamplePlayed, playedAudioBufferSampleCount);
// availaibleSampleCount -= playedAudioBufferSampleCount;
//
// if (currentSamplePlayed == driverBuffer.SampleCount)
// {
// _queuedBuffers.TryDequeue(out _);
//
// needUpdate = true;
// }
//
// Interlocked.Add(ref _playedSampleCount, playedAudioBufferSampleCount);
// }
//
// // Notify the output if needed.
// if (needUpdate)
// {
// _updateRequiredEvent.Set();
// }
// }
//
// public override ulong GetPlayedSampleCount()
// {
// return Interlocked.Read(ref _playedSampleCount);
// }
//
// public override float GetVolume()
// {
// return _volume;
// }
//
// public override void PrepareToClose() { }
//
// public override void QueueBuffer(AudioBuffer buffer)
// {
// EnsureAudioStreamSetup(buffer);
//
// if (_outputStream != 0)
// {
// SDL3AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer));
//
// _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
//
// _queuedBuffers.Enqueue(driverBuffer);
// }
// else
// {
// Interlocked.Add(ref _playedSampleCount, GetSampleCount(buffer));
//
// _updateRequiredEvent.Set();
// }
// }
//
// public override void SetVolume(float volume)
// {
// _volume = volume;
// }
//
// public override void Start()
// {
// if (!_started)
// {
// if (_outputStream != 0)
// {
// SDL_PauseAudioDevice(_outputStream, 0);
// }
//
// _started = true;
// }
// }
//
// public override void Stop()
// {
// if (_started)
// {
// if (_outputStream != 0)
// {
// SDL_PauseAudioDevice(_outputStream, 1);
// }
//
// _started = false;
// }
// }
//
// public override void UnregisterBuffer(AudioBuffer buffer) { }
//
// public override bool WasBufferFullyConsumed(AudioBuffer buffer)
// {
// if (!_queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer))
// {
// return true;
// }
//
// return driverBuffer.DriverIdentifier != buffer.DataPointer;
// }
//
// protected virtual void Dispose(bool disposing)
// {
// if (disposing && _driver.Unregister(this))
// {
// PrepareToClose();
// Stop();
//
// if (_outputStream != 0)
// {
// SDL_CloseAudioDevice(_outputStream);
// }
// }
// }
//
// public override void Dispose()
// {
// Dispose(true);
// }
// }
// }

View File

@ -9,5 +9,6 @@ namespace Ryujinx.Common.Configuration.Hid
Invalid,
WindowKeyboard,
GamepadSDL2,
GamepadSDL3
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,384 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
class SDL3Gamepad : IGamepad
{
private bool HasConfiguration => _configuration != null;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private StandardControllerInputConfig _configuration;
private static readonly SDL_GamepadButton[] _buttonsDriverMapping =
new SDL_GamepadButton[(int)GamepadButtonInputId.Count]
{
// Unbound, ignored.
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_STICK, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
// NOTE: The left and right trigger are axis, we handle those differently
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_UP, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_DOWN,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_LEFT, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_BACK, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_GUIDE, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_MISC1,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_TOUCHPAD,
// Virtual buttons are invalid, ignored.
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID,
};
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
};
public GamepadFeaturesFlag Features { get; }
private nint _gamepadHandle;
private float _triggerThreshold;
public SDL3Gamepad(nint gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>(20);
Name = SDL_GetGamepadName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
{
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_CloseGamepad(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
_triggerThreshold = triggerThreshold;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
return;
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
{
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = inputId switch
{
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
_ => SDL_SensorType.SDL_SENSOR_INVALID
};
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
return Vector3.Zero;
const int ElementCount = 3;
float[] values = new float[ElementCount];
unsafe
{
fixed (float* valuesPtr = &values[0])
{
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, valuesPtr, ElementCount))
{
return Vector3.Zero;
}
}
}
Vector3 value = new(values[0], values[1], values[2]);
return inputId switch
{
MotionInputId.Gyroscope => RadToDegree(value),
MotionInputId.Accelerometer => GsToMs2(value),
_ => value
};
}
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
//TODO: miss constant SDL_STANDARD_GRAVITY 9.80665f
private static Vector3 GsToMs2(Vector3 gs) => gs / 9.80665f;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
// Then left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick,
(GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
// Finally right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick,
(GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
return rawState;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
continue;
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId>
GetLogicalJoyStickConfig(StickInputId inputId)
{
switch (inputId)
{
case StickInputId.Left:
if (_configuration.RightJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Left)
return _configuration.RightJoyconStick;
else
return _configuration.LeftJoyconStick;
case StickInputId.Right:
if (_configuration.LeftJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Right)
return _configuration.LeftJoyconStick;
else
return _configuration.RightJoyconStick;
}
return null;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
return (0.0f, 0.0f);
(short stickX, short stickY) = GetStickXY(inputId);
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
if (joyconStickConfig != null)
{
if (joyconStickConfig.InvertStickX)
resultX = -resultX;
if (joyconStickConfig.InvertStickY)
resultY = -resultY;
if (joyconStickConfig.Rotate90CW)
{
float temp = resultX;
resultX = resultY;
resultY = -temp;
}
}
}
return (resultX, resultY);
}
// ReSharper disable once InconsistentNaming
private (short, short) GetStickXY(StickInputId inputId) =>
inputId switch
{
StickInputId.Left => (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY)),
StickInputId.Right => (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTY)),
_ => throw new NotSupportedException($"Unsupported stick {inputId}")
};
public bool IsPressed(GamepadButtonInputId inputId)
{
switch (inputId)
{
case GamepadButtonInputId.LeftTrigger:
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) > _triggerThreshold;
case GamepadButtonInputId.RightTrigger:
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) > _triggerThreshold;
}
if (_buttonsDriverMapping[(int)inputId] == SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID)
{
return false;
}
return SDL_GetGamepadButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]);
}
}
}

View File

@ -0,0 +1,231 @@
using Ryujinx.Common.Logging;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDl3
{
public class SDL3GamepadDriver : IGamepadDriver
{
private readonly Dictionary<uint, string> _gamepadsInstanceIdsMapping;
private readonly List<string> _gamepadsIds;
private readonly Lock _lock = new();
public ReadOnlySpan<string> GamepadsIds
{
get
{
lock (_lock)
{
return _gamepadsIds.ToArray();
}
}
}
public string DriverName => "SDL3";
public event Action<string> OnGamepadConnected;
public event Action<string> OnGamepadDisconnected;
public SDL3GamepadDriver()
{
_gamepadsInstanceIdsMapping = new Dictionary<uint, string>();
_gamepadsIds = new List<string>();
SDL3Driver.Instance.Initialize();
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
SDL3Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
SDL3Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
IntPtr joystickArray = SDL_GetJoysticks(out int count);
var joystickIDs = new int[count];
Marshal.Copy(joystickArray, joystickIDs, 0, count);
for (int i = 0; i < count; i++)
{
HandleJoyStickConnected((uint)joystickIDs[i]);
}
}
private string GenerateGamepadId(uint joystickIndex)
{
int bufferSize = 33;
Span<byte> pszGUID = stackalloc byte[bufferSize];
SDL_GUIDToString(SDL_GetJoystickGUIDForID(joystickIndex), pszGUID, bufferSize);
var guid = Encoding.UTF8.GetString(pszGUID);
// if (guid == new SDL_GUID())
// {
// return null;
// }
string id;
lock (_lock)
{
int guidIndex = 0;
id = guidIndex + guid;
while (_gamepadsIds.Contains(id))
{
id = (++guidIndex) + "-" + guid;
}
}
return id;
}
private uint GetJoystickIndexByGamepadId(string id)
{
lock (_lock)
{
return _gamepadsInstanceIdsMapping.FirstOrDefault(x=>x.Value == id).Key;
}
}
private void HandleJoyStickDisconnected(uint joystickInstanceId)
{
bool joyConPairDisconnected = false;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return;
lock (_lock)
{
_gamepadsIds.Remove(id);
if (!SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
{
_gamepadsIds.Remove(SDL3JoyConPair.Id);
joyConPairDisconnected = true;
}
}
OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected)
{
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
}
}
private void HandleJoyStickConnected(uint joystickInstanceId)
{
bool joyConPairConnected = false;
if (SDL_IsGamepad(joystickInstanceId))
{
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
{
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
// so it is rejected to avoid doubling the entries.
return;
}
string id = GenerateGamepadId(joystickInstanceId);
if (id == null)
{
return;
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
{
lock (_lock)
{
if (joystickInstanceId <= _gamepadsIds.FindLastIndex(_ => true))
{
// _gamepadsIds.Insert(joystickDeviceId, id);
}
else
_gamepadsIds.Add(id);
if (SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
{
_gamepadsIds.Remove(SDL3JoyConPair.Id);
_gamepadsIds.Add(SDL3JoyConPair.Id);
joyConPairConnected = true;
}
}
OnGamepadConnected?.Invoke(id);
if (joyConPairConnected)
{
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
}
}
}
}
private void HandleJoyBatteryUpdated(uint joystickDeviceId, SDL_JoyBatteryEvent joyBatteryEvent)
{
Logger.Info?.Print(LogClass.Hid,
$"{SDL_GetGamepadNameForID(joystickDeviceId)}, Battery percent: {joyBatteryEvent.percent}");
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
SDL3Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL3Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
// Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds)
{
OnGamepadDisconnected?.Invoke(id);
}
lock (_lock)
{
_gamepadsIds.Clear();
}
SDL3Driver.Instance.Dispose();
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
public IGamepad GetGamepad(string id)
{
if (id == SDL3JoyConPair.Id)
{
lock (_lock)
{
return SDL3JoyConPair.GetGamepad(_gamepadsInstanceIdsMapping);
}
}
var instanceId = GetJoystickIndexByGamepadId(id);
if (instanceId == nint.Zero)
{
return null;
}
nint gamepadHandle = SDL_OpenGamepad(instanceId);
if (gamepadHandle == nint.Zero)
{
return null;
}
Console.WriteLine(SDL_GetGamepadName(gamepadHandle));
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
{
return new SDL3JoyCon(gamepadHandle, id);
}
return new SDL3Gamepad(gamepadHandle, id);
}
}
}

View File

@ -0,0 +1,420 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
internal class SDL3JoyCon : IGamepad
{
private bool HasConfiguration => _configuration != null;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private StandardControllerInputConfig _configuration;
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
{
{ GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
{ GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
{ GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
{ GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
{ GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
{ GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1 },
{ GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2 },
{ GamepadButtonInputId.SingleRightTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ GamepadButtonInputId.SingleLeftTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
};
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
{
{ GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
{ GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
{ GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
{ GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
{ GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
{ GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1 },
{ GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2 },
{ GamepadButtonInputId.SingleRightTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ GamepadButtonInputId.SingleLeftTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER }
};
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _buttonsDriverMapping;
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
};
public GamepadFeaturesFlag Features { get; }
private nint _gamepadHandle;
private enum JoyConType
{
Left, Right
}
public const string Prefix = "Nintendo Switch Joy-Con";
public const string LeftName = "Nintendo Switch Joy-Con (L)";
public const string RightName = "Nintendo Switch Joy-Con (R)";
private readonly JoyConType _joyConType;
public SDL3JoyCon(nint gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
Name = SDL_GetGamepadName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
switch (Name)
{
case LeftName:
{
_buttonsDriverMapping = _leftButtonsDriverMapping;
_joyConType = JoyConType.Left;
break;
}
case RightName:
{
_buttonsDriverMapping = _rightButtonsDriverMapping;
_joyConType = JoyConType.Right;
break;
}
}
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
{
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_CloseGamepad(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
return;
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
{
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = inputId switch
{
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
_ => SDL_SensorType.SDL_SENSOR_INVALID
};
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
return Vector3.Zero;
const int ElementCount = 3;
unsafe
{
float* values = stackalloc float[ElementCount];
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, values, ElementCount))
{
return Vector3.Zero;
}
Vector3 value = _joyConType switch
{
JoyConType.Left => new Vector3(-values[2], values[1], values[0]),
JoyConType.Right => new Vector3(values[2], values[1], -values[0])
};
return inputId switch
{
MotionInputId.Gyroscope => RadToDegree(value),
MotionInputId.Accelerometer => GsToMs2(value),
_ => value
};
}
}
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
//TODO: miss constant SDL_STANDARD_GRAVITY 9.80665f
private static Vector3 GsToMs2(Vector3 gs) => gs / 9.80665f;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
switch (_joyConType)
{
case JoyConType.Left:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick,
(GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight,
(GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0,
(GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
break;
case JoyConType.Right:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick,
(GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1,
(GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
break;
default:
throw new ArgumentOutOfRangeException();
}
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
return rawState;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
continue;
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId>
GetLogicalJoyStickConfig(StickInputId inputId)
{
switch (inputId)
{
case StickInputId.Left:
if (_configuration.RightJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Left)
return _configuration.RightJoyconStick;
else
return _configuration.LeftJoyconStick;
case StickInputId.Right:
if (_configuration.LeftJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Right)
return _configuration.LeftJoyconStick;
else
return _configuration.RightJoyconStick;
}
return null;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
return (0.0f, 0.0f);
if (inputId == StickInputId.Left && _joyConType == JoyConType.Right ||
inputId == StickInputId.Right && _joyConType == JoyConType.Left)
{
return (0.0f, 0.0f);
}
(short stickX, short stickY) = GetStickXY();
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
if (joyconStickConfig != null)
{
if (joyconStickConfig.InvertStickX)
resultX = -resultX;
if (joyconStickConfig.InvertStickY)
resultY = -resultY;
if (joyconStickConfig.Rotate90CW)
{
float temp = resultX;
resultX = resultY;
resultY = -temp;
}
}
}
return inputId switch
{
StickInputId.Left when _joyConType == JoyConType.Left => (resultY, -resultX),
StickInputId.Right when _joyConType == JoyConType.Right => (-resultY, resultX),
_ => (0.0f, 0.0f)
};
}
private (short, short) GetStickXY()
{
return (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY));
}
public bool IsPressed(GamepadButtonInputId inputId)
{
if (!_buttonsDriverMapping.TryGetValue(inputId, out var button))
{
return false;
}
// if (SDL_GetGamepadButton(_gamepadHandle, button))
// {
// Console.WriteLine(inputId+", "+button);
// }
return SDL_GetGamepadButton(_gamepadHandle, button);
}
}
}

View File

@ -0,0 +1,141 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
internal class SDL3JoyConPair(IGamepad left, IGamepad right) : IGamepad
{
private StandardControllerInputConfig _configuration;
private readonly StickInputId[] _stickUserMapping =
[
StickInputId.Unbound,
StickInputId.Left,
StickInputId.Right
];
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
(right?.Features ?? GamepadFeaturesFlag.None);
public const string Id = "JoyConPair";
string IGamepad.Id => Id;
public string Name => "Nintendo Switch Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose()
{
left?.Dispose();
right?.Dispose();
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
return GetStateSnapshot();
}
public Vector3 GetMotionData(MotionInputId inputId)
{
return inputId switch
{
MotionInputId.Accelerometer or
MotionInputId.Gyroscope => left.GetMotionData(inputId),
MotionInputId.SecondAccelerometer => right.GetMotionData(MotionInputId.Accelerometer),
MotionInputId.SecondGyroscope => right.GetMotionData(MotionInputId.Gyroscope),
_ => Vector3.Zero
};
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public (float, float) GetStick(StickInputId inputId)
{
return inputId switch
{
StickInputId.Left => left.GetStick(StickInputId.Left),
StickInputId.Right => right.GetStick(StickInputId.Right),
_ => (0, 0)
};
}
public bool IsPressed(GamepadButtonInputId inputId)
{
return left.IsPressed(inputId) || right.IsPressed(inputId);
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (lowFrequency != 0)
{
right.Rumble(lowFrequency, lowFrequency, durationMs);
}
if (highFrequency != 0)
{
left.Rumble(highFrequency, highFrequency, durationMs);
}
if (lowFrequency == 0 && highFrequency == 0)
{
left.Rumble(0, 0, durationMs);
right.Rumble(0, 0, durationMs);
}
}
public void SetConfiguration(InputConfig configuration)
{
left.SetConfiguration(configuration);
right.SetConfiguration(configuration);
}
public void SetTriggerThreshold(float triggerThreshold)
{
left.SetTriggerThreshold(triggerThreshold);
right.SetTriggerThreshold(triggerThreshold);
}
public static bool IsCombinable(Dictionary<uint, string> gamepadsInstanceIdsMapping)
{
(uint leftIndex, uint rightIndex) = DetectJoyConPair(gamepadsInstanceIdsMapping);
return leftIndex != 0 && rightIndex != 0;
}
private static (uint leftInstance, uint rightInstance) DetectJoyConPair(
Dictionary<uint, string> gamepadsInstanceIdsMapping)
{
var leftInstance = gamepadsInstanceIdsMapping
.FirstOrDefault(item => SDL_GetGamepadNameForID(item.Key) == SDL3JoyCon.LeftName).Key;
var rightInstance = gamepadsInstanceIdsMapping
.FirstOrDefault(item => SDL_GetGamepadNameForID(item.Key) == SDL3JoyCon.RightName).Key;
return (leftInstance, rightInstance);
}
public static IGamepad GetGamepad(Dictionary<uint, string> gamepadsInstanceIdsMapping)
{
(uint leftInstance, uint rightInstance) = DetectJoyConPair(gamepadsInstanceIdsMapping);
if (leftInstance == 0 || rightInstance == 0)
{
return null;
}
nint leftGamepadHandle = SDL_OpenGamepad(leftInstance);
nint rightGamepadHandle = SDL_OpenGamepad(rightInstance);
if (leftGamepadHandle == nint.Zero || rightGamepadHandle == nint.Zero)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsInstanceIdsMapping[leftInstance]),
new SDL3JoyCon(rightGamepadHandle, gamepadsInstanceIdsMapping[rightInstance]));
}
}
}

View File

@ -0,0 +1,405 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using static SDL3.SDL;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Input.SDL3
{
class SDL3Keyboard : IKeyboard
{
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
}
private readonly Lock _userMappingLock = new();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping = new SDL_Keycode[(int)Key.Count]
{
// INVALID
SDL_Keycode.SDLK_0,
// Presented as modifiers, so invalid here.
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_F1,
SDL_Keycode.SDLK_F2,
SDL_Keycode.SDLK_F3,
SDL_Keycode.SDLK_F4,
SDL_Keycode.SDLK_F5,
SDL_Keycode.SDLK_F6,
SDL_Keycode.SDLK_F7,
SDL_Keycode.SDLK_F8,
SDL_Keycode.SDLK_F9,
SDL_Keycode.SDLK_F10,
SDL_Keycode.SDLK_F11,
SDL_Keycode.SDLK_F12,
SDL_Keycode.SDLK_F13,
SDL_Keycode.SDLK_F14,
SDL_Keycode.SDLK_F15,
SDL_Keycode.SDLK_F16,
SDL_Keycode.SDLK_F17,
SDL_Keycode.SDLK_F18,
SDL_Keycode.SDLK_F19,
SDL_Keycode.SDLK_F20,
SDL_Keycode.SDLK_F21,
SDL_Keycode.SDLK_F22,
SDL_Keycode.SDLK_F23,
SDL_Keycode.SDLK_F24,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_UP,
SDL_Keycode.SDLK_DOWN,
SDL_Keycode.SDLK_LEFT,
SDL_Keycode.SDLK_RIGHT,
SDL_Keycode.SDLK_RETURN,
SDL_Keycode.SDLK_ESCAPE,
SDL_Keycode.SDLK_SPACE,
SDL_Keycode.SDLK_TAB,
SDL_Keycode.SDLK_BACKSPACE,
SDL_Keycode.SDLK_INSERT,
SDL_Keycode.SDLK_DELETE,
SDL_Keycode.SDLK_PAGEUP,
SDL_Keycode.SDLK_PAGEDOWN,
SDL_Keycode.SDLK_HOME,
SDL_Keycode.SDLK_END,
SDL_Keycode.SDLK_CAPSLOCK,
SDL_Keycode.SDLK_SCROLLLOCK,
SDL_Keycode.SDLK_PRINTSCREEN,
SDL_Keycode.SDLK_PAUSE,
SDL_Keycode.SDLK_NUMLOCKCLEAR,
SDL_Keycode.SDLK_CLEAR,
SDL_Keycode.SDLK_KP_0,
SDL_Keycode.SDLK_KP_1,
SDL_Keycode.SDLK_KP_2,
SDL_Keycode.SDLK_KP_3,
SDL_Keycode.SDLK_KP_4,
SDL_Keycode.SDLK_KP_5,
SDL_Keycode.SDLK_KP_6,
SDL_Keycode.SDLK_KP_7,
SDL_Keycode.SDLK_KP_8,
SDL_Keycode.SDLK_KP_9,
SDL_Keycode.SDLK_KP_DIVIDE,
SDL_Keycode.SDLK_KP_MULTIPLY,
SDL_Keycode.SDLK_KP_MINUS,
SDL_Keycode.SDLK_KP_PLUS,
SDL_Keycode.SDLK_KP_DECIMAL,
SDL_Keycode.SDLK_KP_ENTER,
SDL_Keycode.SDLK_A,
SDL_Keycode.SDLK_B,
SDL_Keycode.SDLK_C,
SDL_Keycode.SDLK_D,
SDL_Keycode.SDLK_E,
SDL_Keycode.SDLK_F,
SDL_Keycode.SDLK_G,
SDL_Keycode.SDLK_H,
SDL_Keycode.SDLK_I,
SDL_Keycode.SDLK_J,
SDL_Keycode.SDLK_K,
SDL_Keycode.SDLK_L,
SDL_Keycode.SDLK_M,
SDL_Keycode.SDLK_N,
SDL_Keycode.SDLK_O,
SDL_Keycode.SDLK_P,
SDL_Keycode.SDLK_Q,
SDL_Keycode.SDLK_R,
SDL_Keycode.SDLK_S,
SDL_Keycode.SDLK_T,
SDL_Keycode.SDLK_U,
SDL_Keycode.SDLK_V,
SDL_Keycode.SDLK_W,
SDL_Keycode.SDLK_X,
SDL_Keycode.SDLK_Y,
SDL_Keycode.SDLK_Z,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_1,
SDL_Keycode.SDLK_2,
SDL_Keycode.SDLK_3,
SDL_Keycode.SDLK_4,
SDL_Keycode.SDLK_5,
SDL_Keycode.SDLK_6,
SDL_Keycode.SDLK_7,
SDL_Keycode.SDLK_8,
SDL_Keycode.SDLK_9,
SDL_Keycode.SDLK_GRAVE,
SDL_Keycode.SDLK_GRAVE,
SDL_Keycode.SDLK_MINUS,
SDL_Keycode.SDLK_PLUS,
SDL_Keycode.SDLK_LEFTBRACKET,
SDL_Keycode.SDLK_RIGHTBRACKET,
SDL_Keycode.SDLK_SEMICOLON,
SDL_Keycode.SDLK_APOSTROPHE,
SDL_Keycode.SDLK_COMMA,
SDL_Keycode.SDLK_PERIOD,
SDL_Keycode.SDLK_SLASH,
SDL_Keycode.SDLK_BACKSLASH,
// Invalids
SDL_Keycode.SDLK_0,
};
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = new List<ButtonMappingEntry>();
}
private bool HasConfiguration => _configuration != null;
public string Id { get; }
public string Name { get; }
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
public void Dispose()
{
// No operations
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ToSDL2Scancode(Key key)
{
if (key >= Key.Unknown && key <= Key.Menu)
{
return -1;
}
IntPtr modstate = (int)SDL_Keymod.SDL_KMOD_NONE;
return (int)SDL_GetScancodeFromKey((uint)_keysDriverMapping[(int)key], modstate);
}
private static SDL_Keymod GetKeyboardModifierMask(Key key)
{
return key switch
{
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL2.
_ => SDL_Keymod.SDL_KMOD_NONE,
};
}
public KeyboardStateSnapshot GetKeyboardStateSnapshot()
{
Span<SDLBool> rawKeyboardState;
SDL_Keymod rawKeyboardModifierState = SDL_GetModState();
unsafe
{
rawKeyboardState = SDL_GetKeyboardState(out int numKeys);
}
bool[] keysState = new bool[(int)Key.Count];
for (Key key = 0; key < Key.Count; key++)
{
int index = ToSDL2Scancode(key);
if (index == -1)
{
SDL_Keymod modifierMask = GetKeyboardModifierMask(key);
if (modifierMask == SDL_Keymod.SDL_KMOD_NONE)
{
continue;
}
keysState[(int)key] = (rawKeyboardModifierState & modifierMask) == modifierMask;
}
else
{
keysState[(int)key] = rawKeyboardState[index];
}
}
return new KeyboardStateSnapshot(keysState);
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (!HasConfiguration)
{
return result;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
}
return result;
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotSupportedException();
}
public (float, float) GetStick(StickInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(GamepadButtonInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(Key key)
{
// We only implement GetKeyboardStateSnapshot.
throw new NotSupportedException();
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
// Then configure left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Finally configure right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
}
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
// No operations
}
public Vector3 GetMotionData(MotionInputId inputId)
{
// No operations
return Vector3.Zero;
}
}
}

View File

@ -0,0 +1,55 @@
using Ryujinx.SDL3.Common;
using System;
namespace Ryujinx.Input.SDL3
{
public class SDL3KeyboardDriver : IGamepadDriver
{
public SDL3KeyboardDriver()
{
SDL3Driver.Instance.Initialize();
}
public string DriverName => "SDL3";
private static readonly string[] _keyboardIdentifers = new string[1] { "0" };
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
SDL3Driver.Instance.Dispose();
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
{
return null;
}
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
}
}

View File

@ -0,0 +1,90 @@
using Ryujinx.Common.Configuration.Hid;
using System;
using System.Drawing;
using System.Numerics;
namespace Ryujinx.Input.SDL3
{
public class SDL3Mouse : IMouse
{
private SDL3MouseDriver _driver;
public GamepadFeaturesFlag Features => throw new NotImplementedException();
public string Id => "0";
public string Name => "SDL3Mouse";
public bool IsConnected => true;
public bool[] Buttons => _driver.PressedButtons;
Size IMouse.ClientSize => _driver.GetClientSize();
public SDL3Mouse(SDL3MouseDriver 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()
{
GC.SuppressFinalize(this);
_driver = null;
}
}
}

View File

@ -0,0 +1,179 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
public class SDL3MouseDriver : IGamepadDriver
{
private const int CursorHideIdleTime = 5; // seconds
private bool _isDisposed;
private readonly HideCursorMode _hideCursorMode;
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 SDL3MouseDriver(HideCursorMode hideCursorMode)
{
PressedButtons = new bool[(int)MouseButton.Count];
_hideCursorMode = hideCursorMode;
if (_hideCursorMode == HideCursorMode.Always)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
}
_isHidden = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static MouseButton DriverButtonToMouseButton(uint rawButton)
{
Debug.Assert(rawButton is > 0 and <= (int)MouseButton.Count);
return (MouseButton)(rawButton - 1);
}
public void UpdatePosition()
{
_ = SDL_GetMouseState(out float posX, out float posY);
Vector2 position = new(posX, posY);
if (CurrentPosition != position)
{
CurrentPosition = position;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
CheckIdle();
}
private void CheckIdle()
{
if (_hideCursorMode != HideCursorMode.OnIdle)
{
return;
}
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
{
if (!_isHidden)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
}
_isHidden = true;
}
}
else
{
if (_isHidden)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to enable the cursor.");
}
_isHidden = false;
}
}
}
public void Update(SDL_Event evnt)
{
var type = (SDL_EventType)evnt.type;
switch (type)
{
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
uint rawButton = evnt.button.button;
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
{
PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN;
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_EVENT_MOUSE_MOTION:
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
_lastCursorMoveTime = Stopwatch.GetTimestamp();
break;
case SDL_EventType.SDL_EVENT_MOUSE_WHEEL:
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 => "SDL3";
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
public IGamepad GetGamepad(string id)
{
return new SDL3Mouse(this);
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
GC.SuppressFinalize(this);
_isDisposed = true;
}
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Ryujinx.SDL3_CS</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<None Include="runtimes\win-x64\native\SDL3.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>SDL3.dll</Link>
</None>
</ItemGroup>
<!-- <ItemGroup Condition="'$(OS)' == 'Linux'">-->
<!-- <None Include="runtimes\linux-x64\native\SDL3.dll">-->
<!-- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>-->
<!-- <DestinationFolder>$(ProjectDir)</DestinationFolder>-->
<!-- </None>-->
<!-- </ItemGroup>-->
<!-- <ItemGroup Condition="'$(OS)' == 'Darwin'">-->
<!-- <None Include="runtimes\osx-x64\native\SDL3.dll">-->
<!-- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>-->
<!-- <DestinationFolder>$(ProjectDir)</DestinationFolder>-->
<!-- </None>-->
<!-- </ItemGroup>-->
</Project>

8052
src/Ryujinx.SDL3-CS/SDL3.cs Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3-CS\Ryujinx.SDL3-CS.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,210 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.SDL3.Common
{
public class SDL3Driver : IDisposable
{
private static SDL3Driver _instance;
public static SDL3Driver Instance
{
get
{
_instance ??= new SDL3Driver();
return _instance;
}
}
public static Action<Action> MainThreadDispatcher { get; set; }
private const SDL_InitFlags SdlInitFlags = SDL_InitFlags.SDL_INIT_GAMEPAD;
private bool _isRunning;
private uint _refereceCount;
private Thread _worker;
public event Action<uint> OnJoyStickConnected;
public event Action<uint> OnJoystickDisconnected;
public event Action<uint, SDL_JoyBatteryEvent> OnJoyBatteryUpdated;
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
private readonly Lock _lock = new();
private SDL3Driver() { }
public void Initialize()
{
lock (_lock)
{
_refereceCount++;
if (_isRunning)
{
return;
}
// SDL_SetHint(SDL_HINT_APP_NAME, "Ryujinx");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
// SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
// SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
//
//
// // NOTE: As of SDL2 2.24.0, joycons are combined by default but the motion source only come from one of them.
// // We disable this behavior for now.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
if (!SDL_Init(SdlInitFlags))
{
string errorMessage = $"SDL2 initialization failed with error \"{SDL_GetError()}\"";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
// First ensure that we only enable joystick events (for connected/disconnected).
if (!SDL_GamepadEventsEnabled())
{
Logger.Error?.PrintMsg(LogClass.Application,
"Couldn't change the state of game controller events.");
}
if (!SDL_JoystickEventsEnabled())
{
Logger.Error?.PrintMsg(LogClass.Application,
$"Failed to enable joystick event polling: {SDL_GetError()}");
}
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_AXIS_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BALL_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_HAT_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_DOWN, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_UP, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
string gamepadDbPath = Path.Combine(AppDataManager.BaseDirPath, "SDL_GameControllerDB.txt");
if (File.Exists(gamepadDbPath))
{
SDL_AddGamepadMappingsFromFile(gamepadDbPath);
}
_registeredWindowHandlers = new ConcurrentDictionary<uint, Action<SDL_Event>>();
_worker = new Thread(EventWorker);
_isRunning = true;
_worker.Start();
}
}
public bool RegisterWindow(uint windowId, Action<SDL_Event> windowEventHandler)
{
return _registeredWindowHandlers.TryAdd(windowId, windowEventHandler);
}
public void UnregisterWindow(uint windowId)
{
_registeredWindowHandlers.Remove(windowId, out _);
}
private void HandleSDLEvent(ref SDL_Event evnt)
{
var type = (SDL_EventType)evnt.type;
if (type == SDL_EventType.SDL_EVENT_JOYSTICK_ADDED)
{
uint instanceId = evnt.jdevice.which;
Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}");
OnJoyStickConnected?.Invoke(instanceId);
}
else if (type == SDL_EventType.SDL_EVENT_JOYSTICK_REMOVED)
{
uint instanceId = evnt.jdevice.which;
Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {instanceId}");
OnJoystickDisconnected?.Invoke(instanceId);
}
else if (type == SDL_EventType.SDL_EVENT_JOYSTICK_BATTERY_UPDATED)
{
OnJoyBatteryUpdated?.Invoke(evnt.jbattery.which, evnt.jbattery);
}
else if (type is >= SDL_EventType.SDL_EVENT_WINDOW_FIRST and <= SDL_EventType.SDL_EVENT_WINDOW_LAST or SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN
or SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP)
{
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
{
handler(evnt);
}
}
}
private void EventWorker()
{
const int WaitTimeMs = 10;
using ManualResetEventSlim waitHandle = new(false);
while (_isRunning)
{
MainThreadDispatcher?.Invoke(() =>
{
while (SDL_PollEvent(out SDL_Event evnt))
{
HandleSDLEvent(ref evnt);
}
});
waitHandle.Wait(WaitTimeMs);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
lock (_lock)
{
if (_isRunning)
{
_refereceCount--;
if (_refereceCount == 0)
{
_isRunning = false;
_worker?.Join();
SDL_Quit();
OnJoyStickConnected = null;
OnJoystickDisconnected = null;
}
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
}
}

View File

@ -20,6 +20,7 @@ using Ryujinx.Common.SystemInterop;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL2.Common;
using Ryujinx.SDL3.Common;
using System;
using System.IO;
using System.Linq;
@ -128,6 +129,9 @@ namespace Ryujinx.Ava
// Initialize SDL2 driver
SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
// Initialize SDL3 driver
SDL3Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
ReloadConfig();
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();

View File

@ -76,6 +76,7 @@
<ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
@ -177,5 +178,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
<Folder Include="bin\Debug\net9.0\" />
</ItemGroup>
</Project>

View File

@ -28,6 +28,7 @@ using Ryujinx.HLE.HOS;
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;
@ -106,7 +107,10 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached)
{
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver());
//TODO: after sdl3 delete it
new SDL2GamepadDriver();
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
this.ScalingChanged += OnScalingChanged;