From eb056218a13fa145adcc9ecafd166b1b1f2caccb Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 5 May 2021 23:37:09 +0200 Subject: audio: Implement a SDL2 backend (#2258) * audio: Implement a SDL2 backend This adds support to SDL2 as an audio backend. It has the same compatibility level as OpenAL without its issues. I also took the liberty of restructuring the SDL2 code to have one shared project between audio and input. The configuration version was also incremented. * Address gdkchan's comments * Fix update logic * Add an heuristic to pick the correct target sample count wanted by the game * Address gdkchan's comments * Address Ac_k's comments * Fix audren output * Address gdkchan's comments --- .../Ryujinx.Audio.Backends.SDL2.csproj | 13 ++ Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs | 16 ++ .../SDL2HardwareDeviceDriver.cs | 184 +++++++++++++++++ .../SDL2HardwareDeviceSession.cs | 223 +++++++++++++++++++++ .../Common/HardwareDeviceSessionOutputBase.cs | 9 +- Ryujinx.Common/Configuration/AudioBackend.cs | 3 +- .../Configuration/ConfigurationFileFormat.cs | 2 +- Ryujinx.Common/Configuration/ConfigurationState.cs | 7 + Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj | 5 +- Ryujinx.Input.SDL2/SDL2Driver.cs | 170 ---------------- Ryujinx.Input.SDL2/SDL2GamepadDriver.cs | 3 +- Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj | 15 ++ Ryujinx.SDL2.Common/SDL2Driver.cs | 170 ++++++++++++++++ Ryujinx.sln | 16 +- Ryujinx/Ryujinx.csproj | 1 + Ryujinx/Ui/MainWindow.cs | 14 +- Ryujinx/Ui/Windows/SettingsWindow.cs | 8 + 17 files changed, 678 insertions(+), 181 deletions(-) create mode 100644 Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj create mode 100644 Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs create mode 100644 Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs create mode 100644 Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs delete mode 100644 Ryujinx.Input.SDL2/SDL2Driver.cs create mode 100644 Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj create mode 100644 Ryujinx.SDL2.Common/SDL2Driver.cs diff --git a/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj new file mode 100644 index 00000000..6619a500 --- /dev/null +++ b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + true + + + + + + + + diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs b/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs new file mode 100644 index 00000000..71ef414a --- /dev/null +++ b/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Audio.Backends.SDL2 +{ + class SDL2AudioBuffer + { + public readonly ulong DriverIdentifier; + public readonly ulong SampleCount; + public ulong SamplePlayed; + + public SDL2AudioBuffer(ulong driverIdentifier, ulong sampleCount) + { + DriverIdentifier = driverIdentifier; + SampleCount = sampleCount; + SamplePlayed = 0; + } + } +} diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs new file mode 100644 index 00000000..07131d1d --- /dev/null +++ b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs @@ -0,0 +1,184 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using Ryujinx.SDL2.Common; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; +using static SDL2.SDL; + +namespace Ryujinx.Audio.Backends.SDL2 +{ + public class SDL2HardwareDeviceDriver : IHardwareDeviceDriver + { + private object _lock = new object(); + + private ManualResetEvent _updateRequiredEvent; + private List _sessions; + + public SDL2HardwareDeviceDriver() + { + _updateRequiredEvent = new ManualResetEvent(false); + _sessions = new List(); + + SDL2Driver.Instance.Initialize(); + } + + 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 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 SDL2 backend!"); + } + + lock (_lock) + { + SDL2HardwareDeviceSession session = new SDL2HardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount); + + _sessions.Add(session); + + return session; + } + } + + internal void Unregister(SDL2HardwareDeviceSession session) + { + lock (_lock) + { + _sessions.Remove(session); + } + } + + private static SDL_AudioSpec GetSDL2Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount) + { + return new SDL_AudioSpec + { + channels = (byte)requestedChannelCount, + format = GetSDL2Format(requestedSampleFormat), + freq = (int)requestedSampleRate, + samples = (ushort)sampleCount + }; + } + + internal static ushort GetSDL2Format(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}"), + }; + } + + // TODO: Fix this in SDL2-CS. + [DllImport("SDL2", EntryPoint = "SDL_OpenAudioDevice", CallingConvention = CallingConvention.Cdecl)] + private static extern uint SDL_OpenAudioDevice_Workaround( + IntPtr name, + int iscapture, + ref SDL_AudioSpec desired, + out SDL_AudioSpec obtained, + uint allowed_changes + ); + + internal static uint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount, SDL_AudioCallback callback) + { + SDL_AudioSpec desired = GetSDL2Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount, sampleCount); + + desired.callback = callback; + + uint device = SDL_OpenAudioDevice_Workaround(IntPtr.Zero, 0, ref desired, out SDL_AudioSpec got, 0); + + if (device == 0) + { + return 0; + } + + bool isValid = got.format == desired.format && got.freq == desired.freq && got.channels == desired.channels; + + if (!isValid) + { + SDL_CloseAudioDevice(device); + + return 0; + } + + return device; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + while (_sessions.Count > 0) + { + SDL2HardwareDeviceSession session = _sessions[_sessions.Count - 1]; + + session.Dispose(); + } + + SDL2Driver.Instance.Dispose(); + } + } + + public bool SupportsSampleRate(uint sampleRate) + { + return true; + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + return sampleFormat != SampleFormat.PcmInt24; + } + + public bool SupportsChannelCount(uint channelCount) + { + return true; + } + + public bool SupportsDirection(Direction direction) + { + // TODO: add direction input when supported. + return direction == Direction.Output; + } + } +} diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs new file mode 100644 index 00000000..344dd9b6 --- /dev/null +++ b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs @@ -0,0 +1,223 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; + +using static SDL2.SDL; + +namespace Ryujinx.Audio.Backends.SDL2 +{ + class SDL2HardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private SDL2HardwareDeviceDriver _driver; + private ConcurrentQueue _queuedBuffers; + private DynamicRingBuffer _ringBuffer; + private ulong _playedSampleCount; + private ManualResetEvent _updateRequiredEvent; + private uint _outputStream; + private SDL_AudioCallback _callbackDelegate; + private int _bytesPerFrame; + private uint _sampleCount; + private bool _started; + private float _volume; + private ushort _nativeSampleFormat; + + public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount) + { + _driver = driver; + _updateRequiredEvent = _driver.GetUpdateRequiredEvent(); + _queuedBuffers = new ConcurrentQueue(); + _ringBuffer = new DynamicRingBuffer(); + _callbackDelegate = Update; + _bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount; + _nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat); + _sampleCount = uint.MaxValue; + _started = false; + _volume = 1.0f; + } + + private void EnsureAudioStreamSetup(AudioBuffer buffer) + { + bool needAudioSetup = _outputStream == 0 || ((uint)GetSampleCount(buffer) % _sampleCount) != 0; + + if (needAudioSetup) + { + _sampleCount = Math.Max(Constants.TargetSampleCount, (uint)GetSampleCount(buffer)); + + uint newOutputStream = SDL2HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount, _sampleCount, _callbackDelegate); + + if (newOutputStream == 0) + { + // No stream in place, this is unexpected. + throw new InvalidOperationException($"OpenStream failed with error: \"{SDL_GetError()}\""); + } + else + { + 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}"); + } + } + } + + // TODO: Add this variant with pointer to SDL2-CS. + [DllImport("SDL2", EntryPoint = "SDL_MixAudioFormat", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe uint SDL_MixAudioFormat(IntPtr dst, IntPtr src, ushort format, uint len, int volume); + + private unsafe void Update(IntPtr userdata, IntPtr stream, int streamLength) + { + Span streamSpan = new Span((void*)stream, streamLength); + + int maxFrameCount = (int)GetSampleCount(streamLength); + int bufferedFrames = _ringBuffer.Length / _bytesPerFrame; + + int frameCount = Math.Min(bufferedFrames, maxFrameCount); + + if (frameCount == 0) + { + // SDL2 left the responsability to the user to clear the buffer. + streamSpan.Fill(0); + + return; + } + + byte[] samples = new byte[frameCount * _bytesPerFrame]; + + _ringBuffer.Read(samples, 0, samples.Length); + + samples.AsSpan().CopyTo(streamSpan); + streamSpan.Slice(samples.Length).Fill(0); + + // Apply volume to written data + SDL_MixAudioFormat(stream, stream, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME)); + + ulong sampleCount = GetSampleCount(samples.Length); + + ulong availaibleSampleCount = sampleCount; + + bool needUpdate = false; + + while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SDL2AudioBuffer 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); + + SDL2AudioBuffer driverBuffer = new SDL2AudioBuffer(buffer.DataPointer, GetSampleCount(buffer)); + + _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); + + _queuedBuffers.Enqueue(driverBuffer); + } + + 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 SDL2AudioBuffer driverBuffer)) + { + return true; + } + + return driverBuffer.DriverIdentifier != buffer.DataPointer; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + PrepareToClose(); + Stop(); + + if (_outputStream != 0) + { + SDL_CloseAudioDevice(_outputStream); + } + + _driver.Unregister(this); + } + } + + public override void Dispose() + { + Dispose(true); + } + } +} diff --git a/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs b/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs index 1e000e4c..f1f0039c 100644 --- a/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs +++ b/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs @@ -18,6 +18,7 @@ using Ryujinx.Audio.Common; using Ryujinx.Audio.Integration; using Ryujinx.Memory; +using System.Runtime.CompilerServices; namespace Ryujinx.Audio.Backends.Common { @@ -52,7 +53,13 @@ namespace Ryujinx.Audio.Backends.Common protected ulong GetSampleCount(AudioBuffer buffer) { - return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, (int)buffer.DataSize); + return GetSampleCount((int)buffer.DataSize); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected ulong GetSampleCount(int dataSize) + { + return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, dataSize); } public abstract void Dispose(); diff --git a/Ryujinx.Common/Configuration/AudioBackend.cs b/Ryujinx.Common/Configuration/AudioBackend.cs index 28233354..e42df039 100644 --- a/Ryujinx.Common/Configuration/AudioBackend.cs +++ b/Ryujinx.Common/Configuration/AudioBackend.cs @@ -4,6 +4,7 @@ { Dummy, OpenAl, - SoundIo + SoundIo, + SDL2 } } \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs index 092bb9dd..be9c6864 100644 --- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 24; + public const int CurrentVersion = 25; public int Version { get; set; } diff --git a/Ryujinx.Common/Configuration/ConfigurationState.cs b/Ryujinx.Common/Configuration/ConfigurationState.cs index e65bcfcb..9ea5c282 100644 --- a/Ryujinx.Common/Configuration/ConfigurationState.cs +++ b/Ryujinx.Common/Configuration/ConfigurationState.cs @@ -803,6 +803,13 @@ namespace Ryujinx.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 25) + { + Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; diff --git a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj index cee18996..2d61dfb8 100644 --- a/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj +++ b/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj @@ -5,12 +5,9 @@ true - - - - + diff --git a/Ryujinx.Input.SDL2/SDL2Driver.cs b/Ryujinx.Input.SDL2/SDL2Driver.cs deleted file mode 100644 index f77bb1d5..00000000 --- a/Ryujinx.Input.SDL2/SDL2Driver.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Ryujinx.Common.Logging; -using System; -using System.IO; -using System.Threading; -using static SDL2.SDL; - -namespace Ryujinx.Input.SDL2 -{ - class SDL2Driver : IDisposable - { - private static SDL2Driver _instance; - - public static bool IsInitialized => _instance != null; - - public static SDL2Driver Instance - { - get - { - if (_instance == null) - { - _instance = new SDL2Driver(); - } - - return _instance; - } - } - - private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK; - - private bool _isRunning; - private uint _refereceCount; - private Thread _worker; - - public event Action OnJoyStickConnected; - public event Action OnJoystickDisconnected; - - private object _lock = new object(); - - private SDL2Driver() {} - - public void Initialize() - { - lock (_lock) - { - _refereceCount++; - - if (_isRunning) - { - return; - } - - 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"); - // TODO: Add this in nuget package once SDL2 2.0.15 hit stable release. - SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0"); - SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1"); - - if (SDL_Init(SdlInitFlags) != 0) - { - string errorMessage = $"SDL2 initlaization 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). - SDL_GameControllerEventState(SDL_DISABLE); - SDL_JoystickEventState(SDL_ENABLE); - - // Disable all joysticks information, we don't need them no need to flood the event queue for that. - SDL_EventState(SDL_EventType.SDL_JOYAXISMOTION, SDL_DISABLE); - SDL_EventState(SDL_EventType.SDL_JOYBALLMOTION, SDL_DISABLE); - SDL_EventState(SDL_EventType.SDL_JOYHATMOTION, SDL_DISABLE); - SDL_EventState(SDL_EventType.SDL_JOYBUTTONDOWN, SDL_DISABLE); - SDL_EventState(SDL_EventType.SDL_JOYBUTTONUP, SDL_DISABLE); - - SDL_EventState(SDL_EventType.SDL_CONTROLLERSENSORUPDATE, SDL_DISABLE); - - string gamepadDbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SDL_GameControllerDB.txt"); - - if (File.Exists(gamepadDbPath)) - { - SDL_GameControllerAddMappingsFromFile(gamepadDbPath); - } - - _worker = new Thread(EventWorker); - _isRunning = true; - _worker.Start(); - } - } - - private void HandleSDLEvent(ref SDL_Event evnt) - { - if (evnt.type == SDL_EventType.SDL_JOYDEVICEADDED) - { - int deviceId = evnt.cbutton.which; - - // SDL2 loves to be inconsistent here by providing the device id instead of the instance id (like on removed event), as such we just grab it and send it inside our system. - int instanceId = SDL_JoystickGetDeviceInstanceID(deviceId); - - if (instanceId == -1) - { - return; - } - - Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}"); - - OnJoyStickConnected?.Invoke(deviceId, instanceId); - } - else if (evnt.type == SDL_EventType.SDL_JOYDEVICEREMOVED) - { - Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {evnt.cbutton.which}"); - - OnJoystickDisconnected?.Invoke(evnt.cbutton.which); - } - } - - private void EventWorker() - { - const int WaitTimeMs = 10; - - using ManualResetEventSlim waitHandle = new ManualResetEventSlim(false); - - while (_isRunning) - { - while (SDL_PollEvent(out SDL_Event evnt) != 0) - { - 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() - { - Dispose(true); - } - } -} diff --git a/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index 62098383..927d7fe6 100644 --- a/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -1,4 +1,5 @@ -using System; +using Ryujinx.SDL2.Common; +using System; using System.Collections.Generic; using static SDL2.SDL; diff --git a/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj new file mode 100644 index 00000000..a35f8743 --- /dev/null +++ b/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + + + + + + + + + + + diff --git a/Ryujinx.SDL2.Common/SDL2Driver.cs b/Ryujinx.SDL2.Common/SDL2Driver.cs new file mode 100644 index 00000000..edd634ee --- /dev/null +++ b/Ryujinx.SDL2.Common/SDL2Driver.cs @@ -0,0 +1,170 @@ +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.Threading; +using static SDL2.SDL; + +namespace Ryujinx.SDL2.Common +{ + public class SDL2Driver : IDisposable + { + private static SDL2Driver _instance; + + public static bool IsInitialized => _instance != null; + + public static SDL2Driver Instance + { + get + { + if (_instance == null) + { + _instance = new SDL2Driver(); + } + + return _instance; + } + } + + private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO; + + private bool _isRunning; + private uint _refereceCount; + private Thread _worker; + + public event Action OnJoyStickConnected; + public event Action OnJoystickDisconnected; + + private object _lock = new object(); + + private SDL2Driver() {} + + public void Initialize() + { + lock (_lock) + { + _refereceCount++; + + if (_isRunning) + { + return; + } + + 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"); + // TODO: Add this in nuget package once SDL2 2.0.15 hit stable release. + SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0"); + SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1"); + + if (SDL_Init(SdlInitFlags) != 0) + { + string errorMessage = $"SDL2 initlaization 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). + SDL_GameControllerEventState(SDL_DISABLE); + SDL_JoystickEventState(SDL_ENABLE); + + // Disable all joysticks information, we don't need them no need to flood the event queue for that. + SDL_EventState(SDL_EventType.SDL_JOYAXISMOTION, SDL_DISABLE); + SDL_EventState(SDL_EventType.SDL_JOYBALLMOTION, SDL_DISABLE); + SDL_EventState(SDL_EventType.SDL_JOYHATMOTION, SDL_DISABLE); + SDL_EventState(SDL_EventType.SDL_JOYBUTTONDOWN, SDL_DISABLE); + SDL_EventState(SDL_EventType.SDL_JOYBUTTONUP, SDL_DISABLE); + + SDL_EventState(SDL_EventType.SDL_CONTROLLERSENSORUPDATE, SDL_DISABLE); + + string gamepadDbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SDL_GameControllerDB.txt"); + + if (File.Exists(gamepadDbPath)) + { + SDL_GameControllerAddMappingsFromFile(gamepadDbPath); + } + + _worker = new Thread(EventWorker); + _isRunning = true; + _worker.Start(); + } + } + + private void HandleSDLEvent(ref SDL_Event evnt) + { + if (evnt.type == SDL_EventType.SDL_JOYDEVICEADDED) + { + int deviceId = evnt.cbutton.which; + + // SDL2 loves to be inconsistent here by providing the device id instead of the instance id (like on removed event), as such we just grab it and send it inside our system. + int instanceId = SDL_JoystickGetDeviceInstanceID(deviceId); + + if (instanceId == -1) + { + return; + } + + Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}"); + + OnJoyStickConnected?.Invoke(deviceId, instanceId); + } + else if (evnt.type == SDL_EventType.SDL_JOYDEVICEREMOVED) + { + Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {evnt.cbutton.which}"); + + OnJoystickDisconnected?.Invoke(evnt.cbutton.which); + } + } + + private void EventWorker() + { + const int WaitTimeMs = 10; + + using ManualResetEventSlim waitHandle = new ManualResetEventSlim(false); + + while (_isRunning) + { + while (SDL_PollEvent(out SDL_Event evnt) != 0) + { + 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() + { + Dispose(true); + } + } +} diff --git a/Ryujinx.sln b/Ryujinx.sln index bd00f000..f4eec573 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -59,9 +59,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Open EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SoundIo", "Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj", "{716364DE-B988-41A6-BAB4-327964266ECC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input", "Ryujinx.Input\Ryujinx.Input.csproj", "{C16F112F-38C3-40BC-9F5F-4791112063D6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input", "Ryujinx.Input\Ryujinx.Input.csproj", "{C16F112F-38C3-40BC-9F5F-4791112063D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL2", "Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj", "{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input.SDL2", "Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj", "{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL2.Common", "Ryujinx.SDL2.Common\Ryujinx.SDL2.Common.csproj", "{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Audio.Backends.SDL2", "Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj", "{D99A395A-8569-4DB0-B336-900647890052}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -177,6 +181,14 @@ Global {DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.Build.0 = Release|Any CPU + {2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.Build.0 = Release|Any CPU + {D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 6a441a61..1cd9595b 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -26,6 +26,7 @@ + diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 56dcf3eb..a2a00992 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -5,6 +5,7 @@ using LibHac.Common; using LibHac.Ns; using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Audio.Integration; using Ryujinx.Common.Configuration; @@ -327,7 +328,18 @@ namespace Ryujinx.Ui IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver(); - if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo) + if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SDL2) + { + if (SDL2HardwareDeviceDriver.IsSupported) + { + deviceDriver = new SDL2HardwareDeviceDriver(); + } + else + { + Logger.Warning?.Print(LogClass.Audio, "SDL2 audio is not supported, falling back to dummy audio out."); + } + } + else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo) { if (SoundIoHardwareDeviceDriver.IsSupported) { diff --git a/Ryujinx/Ui/Windows/SettingsWindow.cs b/Ryujinx/Ui/Windows/SettingsWindow.cs index be9dc271..94afb242 100644 --- a/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -1,5 +1,6 @@ using Gtk; using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; @@ -302,6 +303,7 @@ namespace Ryujinx.Ui.Windows TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl); TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo); + TreeIter sdl2Iter = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2); TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy); _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore); @@ -316,6 +318,9 @@ namespace Ryujinx.Ui.Windows case AudioBackend.SoundIo: _audioBackendSelect.SetActiveIter(soundIoIter); break; + case AudioBackend.SDL2: + _audioBackendSelect.SetActiveIter(sdl2Iter); + break; case AudioBackend.Dummy: _audioBackendSelect.SetActiveIter(dummyIter); break; @@ -328,11 +333,13 @@ namespace Ryujinx.Ui.Windows bool openAlIsSupported = false; bool soundIoIsSupported = false; + bool sdl2IsSupported = false; Task.Run(() => { openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported; soundIoIsSupported = SoundIoHardwareDeviceDriver.IsSupported; + sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported; }); // This function runs whenever the dropdown is opened @@ -342,6 +349,7 @@ namespace Ryujinx.Ui.Windows { AudioBackend.OpenAl => openAlIsSupported, AudioBackend.SoundIo => soundIoIsSupported, + AudioBackend.SDL2 => sdl2IsSupported, AudioBackend.Dummy => true, _ => throw new ArgumentOutOfRangeException() }; -- cgit v1.2.3