WIP: SDL3 #533
@ -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" />
|
||||
|
24
Ryujinx.sln
24
Ryujinx.sln
@ -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
|
||||
|
@ -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>
|
16
src/Ryujinx.Audio.Backends.SDL3/SDL2AudioBuffer.cs
Normal file
16
src/Ryujinx.Audio.Backends.SDL3/SDL2AudioBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
208
src/Ryujinx.Audio.Backends.SDL3/SDL2HardwareDeviceDriver.cs
Normal file
208
src/Ryujinx.Audio.Backends.SDL3/SDL2HardwareDeviceDriver.cs
Normal 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
234
src/Ryujinx.Audio.Backends.SDL3/SDL2HardwareDeviceSession.cs
Normal file
234
src/Ryujinx.Audio.Backends.SDL3/SDL2HardwareDeviceSession.cs
Normal 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);
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -9,5 +9,6 @@ namespace Ryujinx.Common.Configuration.Hid
|
||||
Invalid,
|
||||
WindowKeyboard,
|
||||
GamepadSDL2,
|
||||
GamepadSDL3
|
||||
}
|
||||
}
|
||||
|
13
src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj
Normal file
13
src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj
Normal 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>
|
384
src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
Normal file
384
src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
231
src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs
Normal file
231
src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
420
src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
Normal file
420
src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
141
src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs
Normal file
141
src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs
Normal 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]));
|
||||
}
|
||||
}
|
||||
}
|
405
src/Ryujinx.Input.SDL3/SDL3Keyboard.cs
Normal file
405
src/Ryujinx.Input.SDL3/SDL3Keyboard.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
55
src/Ryujinx.Input.SDL3/SDL3KeyboardDriver.cs
Normal file
55
src/Ryujinx.Input.SDL3/SDL3KeyboardDriver.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
90
src/Ryujinx.Input.SDL3/SDL3Mouse.cs
Normal file
90
src/Ryujinx.Input.SDL3/SDL3Mouse.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
179
src/Ryujinx.Input.SDL3/SDL3MouseDriver.cs
Normal file
179
src/Ryujinx.Input.SDL3/SDL3MouseDriver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
31
src/Ryujinx.SDL3-CS/Ryujinx.SDL3-CS.csproj
Normal file
31
src/Ryujinx.SDL3-CS/Ryujinx.SDL3-CS.csproj
Normal 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
8052
src/Ryujinx.SDL3-CS/SDL3.cs
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/Ryujinx.SDL3-CS/runtimes/win-x64/native/SDL3.dll
Normal file
BIN
src/Ryujinx.SDL3-CS/runtimes/win-x64/native/SDL3.dll
Normal file
Binary file not shown.
12
src/Ryujinx.SDL3.Common/Ryujinx.SDL3.Common.csproj
Normal file
12
src/Ryujinx.SDL3.Common/Ryujinx.SDL3.Common.csproj
Normal 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>
|
210
src/Ryujinx.SDL3.Common/SDL3Driver.cs
Normal file
210
src/Ryujinx.SDL3.Common/SDL3Driver.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user