From c154f66f26b66af461f075a1a50dcd1e7f60f1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Wed, 21 Jan 2026 18:23:34 -0600 Subject: [PATCH 01/23] Update Korean translation (ryubing/ryujinx!251) See merge request ryubing/ryujinx!251 --- assets/Locales/RenderDoc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json index 894ff07ca..b3f9462eb 100644 --- a/assets/Locales/RenderDoc.json +++ b/assets/Locales/RenderDoc.json @@ -12,7 +12,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 시작", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -37,7 +37,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 종료", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -62,7 +62,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "RenderDoc 프레임 캡처 폐기", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -87,7 +87,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", From d271abe19aa308b3ba12f51c6eb21b288e2ea63b Mon Sep 17 00:00:00 2001 From: Stossy11 Date: Wed, 28 Jan 2026 10:03:59 +1100 Subject: [PATCH 02/23] [ci skip] Add macOS native Audio Backend (ryubing/ryujinx!252) See merge request ryubing/ryujinx!252 THIS IS CURRENTLY NOT EXPOSED BY THE UI OR HANDLED BY THE EMULATOR. Expect a commit later to add it to configs, UI, etc. --- .../AppleAudioBuffer.cs | 16 + .../AppleHardwareDeviceDriver.cs | 241 +++++++++++++++ .../AppleHardwareDeviceSession.cs | 288 ++++++++++++++++++ .../Native/AudioToolbox.cs | 103 +++++++ .../Ryujinx.Audio.Backends.Apple.csproj | 13 + 5 files changed, 661 insertions(+) create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs create mode 100644 src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs b/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs new file mode 100644 index 000000000..995236889 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Audio.Backends.Apple +{ + class AppleAudioBuffer + { + public readonly ulong DriverIdentifier; + public readonly ulong SampleCount; + public ulong SamplePlayed; + + public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount) + { + DriverIdentifier = driverIdentifier; + SampleCount = sampleCount; + SamplePlayed = 0; + } + } +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs new file mode 100644 index 000000000..62d81c6cc --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -0,0 +1,241 @@ +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.Versioning; +using Ryujinx.Audio.Backends.Apple.Native; +using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.Apple +{ + [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] + public class AppleHardwareDeviceDriver : IHardwareDeviceDriver + { + private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; + private readonly ConcurrentDictionary _sessions; + private readonly bool _supportSurroundConfiguration; + + public float Volume { get; set; } + + public AppleHardwareDeviceDriver() + { + _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); + _sessions = new ConcurrentDictionary(); + + _supportSurroundConfiguration = TestSurroundSupport(); + + Volume = 1f; + } + + private bool TestSurroundSupport() + { + try + { + var format = GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); + + int result = AudioQueueNewOutput( + ref format, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + 0, + out IntPtr testQueue); + + if (result == 0) + { + AudioChannelLayout layout = new AudioChannelLayout + { + AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A, + AudioChannelBitmap = 0, + NumberChannelDescriptions = 0 + }; + + int layoutResult = AudioQueueSetProperty( + testQueue, + kAudioQueueProperty_ChannelLayout, + ref layout, + (uint)Marshal.SizeOf()); + + if (layoutResult == 0) + { + AudioQueueDispose(testQueue, true); + return true; + } + + AudioQueueDispose(testQueue, true); + } + + return false; + } + catch + { + return false; + } + } + + public static bool IsSupported => IsSupportedInternal(); + + private static bool IsSupportedInternal() + { + try + { + var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); + int result = AudioQueueNewOutput( + ref format, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + 0, + out IntPtr testQueue); + + if (result == 0) + { + AudioQueueDispose(testQueue, true); + return true; + } + + return false; + } + catch + { + return false; + } + } + + 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 Apple backend!"); + } + + AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount); + + _sessions.TryAdd(session, 0); + + return session; + } + + internal bool Unregister(AppleHardwareDeviceSession session) + { + return _sessions.TryRemove(session, out _); + } + + internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, uint channelCount) + { + uint formatFlags; + uint bitsPerChannel; + + switch (sampleFormat) + { + case SampleFormat.PcmInt8: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 8; + break; + case SampleFormat.PcmInt16: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 16; + break; + case SampleFormat.PcmInt32: + formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + bitsPerChannel = 32; + break; + case SampleFormat.PcmFloat: + formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + bitsPerChannel = 32; + break; + default: + throw new ArgumentException($"Unsupported sample format {sampleFormat}"); + } + + uint bytesPerFrame = (bitsPerChannel / 8) * channelCount; + + return new AudioStreamBasicDescription + { + SampleRate = sampleRate, + FormatID = kAudioFormatLinearPCM, + FormatFlags = formatFlags, + BytesPerPacket = bytesPerFrame, + FramesPerPacket = 1, + BytesPerFrame = bytesPerFrame, + ChannelsPerFrame = channelCount, + BitsPerChannel = bitsPerChannel, + Reserved = 0 + }; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (AppleHardwareDeviceSession session in _sessions.Keys) + { + session.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) + { + return direction != Direction.Input; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs new file mode 100644 index 000000000..95826b4f4 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -0,0 +1,288 @@ +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 System.Runtime.Versioning; +using Ryujinx.Audio.Backends.Apple.Native; +using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; +using static Ryujinx.Audio.Backends.Apple.AppleHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.Apple +{ + [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] + class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private const int NumBuffers = 3; + + private readonly AppleHardwareDeviceDriver _driver; + private readonly ConcurrentQueue _queuedBuffers = new(); + private readonly DynamicRingBuffer _ringBuffer = new(); + private readonly ManualResetEvent _updateRequiredEvent; + + private readonly AudioQueueOutputCallback _callbackDelegate; + private readonly GCHandle _gcHandle; + + private IntPtr _audioQueue; + private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers]; + private readonly int[] _bufferBytesFilled = new int[NumBuffers]; + + private readonly int _bytesPerFrame; + + private ulong _playedSampleCount; + private bool _started; + private float _volume = 1f; + + private readonly object _lock = new(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void AudioQueueOutputCallback( + IntPtr userData, + IntPtr audioQueue, + IntPtr buffer); + + public AppleHardwareDeviceSession( + AppleHardwareDeviceDriver driver, + IVirtualMemoryManager memoryManager, + SampleFormat requestedSampleFormat, + uint requestedSampleRate, + uint requestedChannelCount) + : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount) + { + _driver = driver; + _updateRequiredEvent = driver.GetUpdateRequiredEvent(); + _callbackDelegate = OutputCallback; + _bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount; + + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + + SetupAudioQueue(); + } + + private void SetupAudioQueue() + { + lock (_lock) + { + var format = AppleHardwareDeviceDriver.GetAudioFormat( + RequestedSampleFormat, + RequestedSampleRate, + RequestedChannelCount); + + IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); + IntPtr userData = GCHandle.ToIntPtr(_gcHandle); + + int result = AudioQueueNewOutput( + ref format, + callbackPtr, + userData, + IntPtr.Zero, + IntPtr.Zero, + 0, + out _audioQueue); + + if (result != 0) + { + throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}"); + } + + uint framesPerBuffer = RequestedSampleRate / 100; + uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame; + + for (int i = 0; i < NumBuffers; i++) + { + AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]); + _bufferBytesFilled[i] = 0; + + PrimeBuffer(_audioQueueBuffers[i], i); + } + } + } + + private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex) + { + AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr; + + int capacityBytes = (int)buffer->AudioDataBytesCapacity; + int framesPerBuffer = capacityBytes / _bytesPerFrame; + + int availableFrames = _ringBuffer.Length / _bytesPerFrame; + int framesToRead = Math.Min(availableFrames, framesPerBuffer); + int bytesToRead = framesToRead * _bytesPerFrame; + + Span dst = new((void*)buffer->AudioData, capacityBytes); + dst.Clear(); + + if (bytesToRead > 0) + { + Span audio = dst.Slice(0, bytesToRead); + _ringBuffer.Read(audio, 0, bytesToRead); + ApplyVolume(buffer->AudioData, bytesToRead); + } + + buffer->AudioDataByteSize = (uint)capacityBytes; + _bufferBytesFilled[bufferIndex] = bytesToRead; + + AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero); + } + + private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr) + { + if (!_started || bufferPtr == IntPtr.Zero) + return; + + int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr); + if (bufferIndex < 0) + return; + + int bytesPlayed = _bufferBytesFilled[bufferIndex]; + if (bytesPlayed > 0) + { + ProcessPlayedSamples(bytesPlayed); + } + + PrimeBuffer(bufferPtr, bufferIndex); + } + + private void ProcessPlayedSamples(int bytesPlayed) + { + ulong samplesPlayed = GetSampleCount(bytesPlayed); + ulong remaining = samplesPlayed; + bool needUpdate = false; + + while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer)) + { + ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed); + ulong take = Math.Min(needed, remaining); + + ulong played = Interlocked.Add(ref buffer.SamplePlayed, take); + remaining -= take; + + if (played == buffer.SampleCount) + { + _queuedBuffers.TryDequeue(out _); + needUpdate = true; + } + + Interlocked.Add(ref _playedSampleCount, take); + } + + if (needUpdate) + { + _updateRequiredEvent.Set(); + } + } + + private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize) + { + float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f); + if (volume >= 0.999f) + return; + + int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat); + + switch (RequestedSampleFormat) + { + case SampleFormat.PcmInt16: + short* s16 = (short*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s16[i] = (short)(s16[i] * volume); + break; + + case SampleFormat.PcmFloat: + float* f32 = (float*)dataPtr; + for (int i = 0; i < sampleCount; i++) + f32[i] *= volume; + break; + + case SampleFormat.PcmInt32: + int* s32 = (int*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s32[i] = (int)(s32[i] * volume); + break; + + case SampleFormat.PcmInt8: + sbyte* s8 = (sbyte*)dataPtr; + for (int i = 0; i < sampleCount; i++) + s8[i] = (sbyte)(s8[i] * volume); + break; + } + } + + public override void QueueBuffer(AudioBuffer buffer) + { + _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); + _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.HostTag, GetSampleCount(buffer))); + } + + public override void Start() + { + lock (_lock) + { + if (_started) + return; + + _started = true; + AudioQueueStart(_audioQueue, IntPtr.Zero); + } + } + + public override void Stop() + { + lock (_lock) + { + if (!_started) + return; + + _started = false; + AudioQueuePause(_audioQueue); + } + } + + public override ulong GetPlayedSampleCount() + => Interlocked.Read(ref _playedSampleCount); + + public override float GetVolume() => _volume; + public override void SetVolume(float volume) => _volume = volume; + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer)) + return true; + + return driverBuffer.DriverIdentifier != buffer.HostTag; + } + + public override void PrepareToClose() { } + public override void UnregisterBuffer(AudioBuffer buffer) { } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Stop(); + + if (_audioQueue != IntPtr.Zero) + { + AudioQueueStop(_audioQueue, true); + AudioQueueDispose(_audioQueue, true); + _audioQueue = IntPtr.Zero; + } + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + } + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs new file mode 100644 index 000000000..9a6e8e189 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs @@ -0,0 +1,103 @@ +using Ryujinx.Common.Memory; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Backends.Apple.Native +{ + public static partial class AudioToolbox + { + [StructLayout(LayoutKind.Sequential)] + internal struct AudioStreamBasicDescription + { + public double SampleRate; + public uint FormatID; + public uint FormatFlags; + public uint BytesPerPacket; + public uint FramesPerPacket; + public uint BytesPerFrame; + public uint ChannelsPerFrame; + public uint BitsPerChannel; + public uint Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AudioChannelLayout + { + public uint AudioChannelLayoutTag; + public uint AudioChannelBitmap; + public uint NumberChannelDescriptions; + } + + internal const uint kAudioFormatLinearPCM = 0x6C70636D; + internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c; + internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006; + internal const uint kAudioFormatFlagIsFloat = (1 << 0); + internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2); + internal const uint kAudioFormatFlagIsPacked = (1 << 3); + internal const uint kAudioFormatFlagIsBigEndian = (1 << 1); + internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4); + internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueNewOutput( + ref AudioStreamBasicDescription format, + nint callback, + nint userData, + nint callbackRunLoop, + nint callbackRunLoopMode, + uint flags, + out nint audioQueue); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueSetProperty( + nint audioQueue, + uint propertyID, + ref AudioChannelLayout layout, + uint layoutSize); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueAllocateBuffer( + nint audioQueue, + uint bufferByteSize, + out nint buffer); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueStart(nint audioQueue, nint startTime); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueuePause(nint audioQueue); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueSetParameter( + nint audioQueue, + uint parameterID, + float value); + + [LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")] + internal static partial int AudioQueueEnqueueBuffer( + nint audioQueue, + nint buffer, + uint numPacketDescs, + nint packetDescs); + + [StructLayout(LayoutKind.Sequential)] + internal struct AudioQueueBuffer + { + public uint AudioDataBytesCapacity; + public nint AudioData; + public uint AudioDataByteSize; + public nint UserData; + public uint PacketDescriptionCapacity; + public nint PacketDescriptions; + public uint PacketDescriptionCount; + } + + internal const uint kAudioQueueParam_Volume = 1; + } +} \ No newline at end of file diff --git a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj new file mode 100644 index 000000000..b7e1b6d84 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + true + + + + + + + + From bd388cf4f9421febac45060d63f9ac3754222464 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:28:59 -0600 Subject: [PATCH 03/23] Expose AudioToolkit in UI --- Ryujinx.sln | 4 +++ assets/Locales/Root.json | 27 ++++++++++++++++++- .../AppleHardwareDeviceDriver.cs | 4 ++- .../AppleHardwareDeviceSession.cs | 4 +-- .../Ryujinx.Audio.Backends.Apple.csproj | 2 +- src/Ryujinx/Ryujinx.csproj | 3 ++- src/Ryujinx/Systems/AppHost.cs | 7 +++++ .../Systems/Configuration/AudioBackend.cs | 1 + .../UI/ViewModels/SettingsViewModel.cs | 4 +++ .../UI/Views/Settings/SettingsAudioView.axaml | 3 +++ 10 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Ryujinx.sln b/Ryujinx.sln index b89d5da0a..deddb97a0 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -47,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vic", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Apple", "src\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj", "{AC26EFF0-8593-4184-9A09-98E37EFFB32E}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}" @@ -569,6 +571,8 @@ Global {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU + {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index aa8937247..3578f689d 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -4850,6 +4850,31 @@ "zh_TW": null } }, + { + "ID": "SettingsTabSystemAudioBackendAudioToolbox", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Apple Audio (macOS only)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, { "ID": "SettingsTabSystemHacks", "Translations": { @@ -24776,4 +24801,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 62d81c6cc..2c659f6a0 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -86,6 +86,8 @@ namespace Ryujinx.Audio.Backends.Apple private static bool IsSupportedInternal() { + if (!OperatingSystem.IsMacOS()) return false; + try { var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); @@ -238,4 +240,4 @@ namespace Ryujinx.Audio.Backends.Apple return direction != Direction.Input; } } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 95826b4f4..08aefe4a5 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -215,7 +215,7 @@ namespace Ryujinx.Audio.Backends.Apple public override void QueueBuffer(AudioBuffer buffer) { _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); - _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.HostTag, GetSampleCount(buffer))); + _queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer))); } public override void Start() @@ -253,7 +253,7 @@ namespace Ryujinx.Audio.Backends.Apple if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer)) return true; - return driverBuffer.DriverIdentifier != buffer.HostTag; + return driverBuffer.DriverIdentifier != buffer.DataPointer; } public override void PrepareToClose() { } diff --git a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj index b7e1b6d84..c27fdee5b 100644 --- a/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj +++ b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 true diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 28aec175b..5da152501 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -77,12 +77,13 @@ - + + diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 2eba0d26b..4b1e9cdb5 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -6,6 +6,7 @@ using Avalonia.Threading; using DiscordRPC; using LibHac.Common; using LibHac.Ns; +using Ryujinx.Audio.Backends.Apple; using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL3; @@ -949,6 +950,9 @@ namespace Ryujinx.Ava.Systems AudioBackend.Dummy ]; + if (OperatingSystem.IsMacOS()) + availableBackends.Insert(0, AudioBackend.AudioToolbox); + AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value; if (preferredBackend is AudioBackend.SDL2) @@ -985,6 +989,9 @@ namespace Ryujinx.Ava.Systems deviceDriver = currentBackend switch { +#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run. + AudioBackend.AudioToolbox => InitializeAudioBackend(AudioBackend.AudioToolbox, nextBackend), +#pragma warning restore CA1416 AudioBackend.SDL3 => InitializeAudioBackend(AudioBackend.SDL3, nextBackend), AudioBackend.SoundIo => InitializeAudioBackend(AudioBackend.SoundIo, nextBackend), AudioBackend.OpenAl => InitializeAudioBackend(AudioBackend.OpenAl, nextBackend), diff --git a/src/Ryujinx/Systems/Configuration/AudioBackend.cs b/src/Ryujinx/Systems/Configuration/AudioBackend.cs index da75c9f7c..12d87151d 100644 --- a/src/Ryujinx/Systems/Configuration/AudioBackend.cs +++ b/src/Ryujinx/Systems/Configuration/AudioBackend.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Ava.Systems.Configuration OpenAl, SoundIo, SDL3, + AudioToolbox, SDL2 = SDL3 } } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index d5d9b8218..abb284960 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.Apple; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL3; using Ryujinx.Audio.Backends.SoundIo; @@ -277,6 +278,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenAlEnabled { get; set; } public bool IsSoundIoEnabled { get; set; } public bool IsSDL3Enabled { get; set; } + public bool IsAudioToolboxEnabled { get; set; } public bool IsCustomResolutionScaleActive => _resolutionScale == 4; public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; @@ -524,12 +526,14 @@ namespace Ryujinx.Ava.UI.ViewModels IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported; + IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported; await Dispatcher.UIThread.InvokeAsync(() => { OnPropertyChanged(nameof(IsOpenAlEnabled)); OnPropertyChanged(nameof(IsSoundIoEnabled)); OnPropertyChanged(nameof(IsSDL3Enabled)); + OnPropertyChanged(nameof(IsAudioToolboxEnabled)); }); } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml index 22dfc57ac..e9b4e7acc 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml @@ -46,6 +46,9 @@ + From 82074eb19121fb96c82454a2a63e8e3bedff5137 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:34:51 -0600 Subject: [PATCH 04/23] audio backend projects code cleanup --- .../AppleHardwareDeviceDriver.cs | 66 +++++++------------ .../AppleHardwareDeviceSession.cs | 2 +- .../Native/AudioToolbox.cs | 5 +- .../OpenALHardwareDeviceDriver.cs | 5 +- .../OpenALHardwareDeviceSession.cs | 5 +- .../SDL3HardwareDeviceDriver.cs | 4 +- .../SDL3HardwareDeviceSession.cs | 7 +- .../SoundIoHardwareDeviceDriver.cs | 4 +- .../SoundIoHardwareDeviceSession.cs | 4 +- 9 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 2c659f6a0..80e55b9e5 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Audio.Backends.Apple { [SupportedOSPlatform("macos")] [SupportedOSPlatform("ios")] - public class AppleHardwareDeviceDriver : IHardwareDeviceDriver + public sealed class AppleHardwareDeviceDriver : IHardwareDeviceDriver { private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _pauseEvent; @@ -38,9 +38,10 @@ namespace Ryujinx.Audio.Backends.Apple private bool TestSurroundSupport() { try - { - var format = GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); - + { + AudioStreamBasicDescription format = + GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); + int result = AudioQueueNewOutput( ref format, IntPtr.Zero, @@ -60,9 +61,9 @@ namespace Ryujinx.Audio.Backends.Apple }; int layoutResult = AudioQueueSetProperty( - testQueue, - kAudioQueueProperty_ChannelLayout, - ref layout, + testQueue, + kAudioQueueProperty_ChannelLayout, + ref layout, (uint)Marshal.SizeOf()); if (layoutResult == 0) @@ -70,7 +71,7 @@ namespace Ryujinx.Audio.Backends.Apple AudioQueueDispose(testQueue, true); return true; } - + AudioQueueDispose(testQueue, true); } @@ -90,7 +91,8 @@ namespace Ryujinx.Audio.Backends.Apple try { - var format = GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); + AudioStreamBasicDescription format = + GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); int result = AudioQueueNewOutput( ref format, IntPtr.Zero, @@ -115,16 +117,13 @@ namespace Ryujinx.Audio.Backends.Apple } public ManualResetEvent GetUpdateRequiredEvent() - { - return _updateRequiredEvent; - } + => _updateRequiredEvent; public ManualResetEvent GetPauseEvent() - { - return _pauseEvent; - } + => _pauseEvent; - public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, + SampleFormat sampleFormat, uint sampleRate, uint channelCount) { if (channelCount == 0) { @@ -149,11 +148,10 @@ namespace Ryujinx.Audio.Backends.Apple } internal bool Unregister(AppleHardwareDeviceSession session) - { - return _sessions.TryRemove(session, out _); - } + => _sessions.TryRemove(session, out _); - internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, uint channelCount) + internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, + uint channelCount) { uint formatFlags; uint bitsPerChannel; @@ -202,7 +200,7 @@ namespace Ryujinx.Audio.Backends.Apple Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { @@ -215,29 +213,15 @@ namespace Ryujinx.Audio.Backends.Apple } } - public bool SupportsSampleRate(uint sampleRate) - { - return true; - } + public bool SupportsDirection(Direction direction) + => direction != Direction.Input; + + public bool SupportsSampleRate(uint sampleRate) => true; public bool SupportsSampleFormat(SampleFormat sampleFormat) - { - return sampleFormat != SampleFormat.PcmInt24; - } + => sampleFormat != SampleFormat.PcmInt24; public bool SupportsChannelCount(uint channelCount) - { - if (channelCount == 6) - { - return _supportSurroundConfiguration; - } - - return true; - } - - public bool SupportsDirection(Direction direction) - { - return direction != Direction.Input; - } + => channelCount != 6 || _supportSurroundConfiguration; } } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 08aefe4a5..c9443dcd3 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -67,7 +67,7 @@ namespace Ryujinx.Audio.Backends.Apple { lock (_lock) { - var format = AppleHardwareDeviceDriver.GetAudioFormat( + AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat( RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount); diff --git a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs index 9a6e8e189..ea2a7867a 100644 --- a/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs +++ b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs @@ -1,6 +1,5 @@ -using Ryujinx.Common.Memory; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming namespace Ryujinx.Audio.Backends.Apple.Native { @@ -100,4 +99,4 @@ namespace Ryujinx.Audio.Backends.Apple.Native internal const uint kAudioQueueParam_Volume = 1; } -} \ No newline at end of file +} diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs index 8be6197f6..911b131ed 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs @@ -10,7 +10,8 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.OpenAL { - public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver + // ReSharper disable once InconsistentNaming + public sealed class OpenALHardwareDeviceDriver : IHardwareDeviceDriver { private readonly ALDevice _device; private readonly ALContext _context; @@ -148,7 +149,7 @@ namespace Ryujinx.Audio.Backends.OpenAL Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs index 7292450a6..61fb4a369 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs @@ -9,7 +9,8 @@ using System.Threading; namespace Ryujinx.Audio.Backends.OpenAL { - class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase + // ReSharper disable once InconsistentNaming + sealed class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly OpenALHardwareDeviceDriver _driver; private readonly int _sourceId; @@ -190,7 +191,7 @@ namespace Ryujinx.Audio.Backends.OpenAL } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs index bdc9f02f4..598de8835 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Backends.SDL3 using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]; - public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver + public sealed class SDL3HardwareDeviceDriver : IHardwareDeviceDriver { private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _pauseEvent; @@ -162,7 +162,7 @@ namespace Ryujinx.Audio.Backends.SDL3 Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs index 377d86d2b..ca7b131dd 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs @@ -12,10 +12,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.Audio.Backends.SDL3 { - - - - unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase + sealed unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly SDL3HardwareDeviceDriver _driver; private readonly ConcurrentQueue _queuedBuffers; @@ -226,7 +223,7 @@ namespace Ryujinx.Audio.Backends.SDL3 return driverBuffer.DriverIdentifier != buffer.DataPointer; } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs index e3e5d2913..1aed0744c 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs @@ -10,7 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.SoundIo { - public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver + public sealed class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver { private readonly SoundIoContext _audioContext; private readonly SoundIoDeviceContext _audioDevice; @@ -227,7 +227,7 @@ namespace Ryujinx.Audio.Backends.SoundIo } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs index 1540cd0e3..39ceac08a 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs @@ -11,7 +11,7 @@ using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; namespace Ryujinx.Audio.Backends.SoundIo { - class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase + sealed class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly SoundIoHardwareDeviceDriver _driver; private readonly ConcurrentQueue _queuedBuffers; @@ -428,7 +428,7 @@ namespace Ryujinx.Audio.Backends.SoundIo } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing && _driver.Unregister(this)) { From fef93a453ac6a2032155a80feaf77d8f5db9cc5c Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:41:46 -0600 Subject: [PATCH 05/23] [ci skip] replace all usages of IntPtr with nint --- src/ARMeilleure/Common/EntryTable.cs | 2 +- .../AppleHardwareDeviceDriver.cs | 20 +++++------ .../AppleHardwareDeviceSession.cs | 34 +++++++++---------- .../Native/SoundIoOutStreamContext.cs | 2 +- src/Ryujinx.Cpu/AddressTable.cs | 32 ++++++++--------- .../Image/TextureGroup.cs | 2 +- .../MoltenVK/MVKInitialization.cs | 2 +- .../VulkanInitialization.cs | 4 +-- .../Sockets/Bsd/Impl/ManagedSocket.cs | 2 +- .../WindowsShared/PlaceholderManager.cs | 2 +- src/Ryujinx.Tests/Memory/PartialUnmaps.cs | 2 +- 11 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/ARMeilleure/Common/EntryTable.cs b/src/ARMeilleure/Common/EntryTable.cs index 7b8c1e134..1c154570a 100644 --- a/src/ARMeilleure/Common/EntryTable.cs +++ b/src/ARMeilleure/Common/EntryTable.cs @@ -168,7 +168,7 @@ namespace ARMeilleure.Common { _allocated.Dispose(); - foreach (IntPtr page in _pages.Values) + foreach (nint page in _pages.Values) { NativeAllocator.Instance.Free((void*)page); } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 80e55b9e5..f20da5557 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -44,12 +44,12 @@ namespace Ryujinx.Audio.Backends.Apple int result = AudioQueueNewOutput( ref format, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero, 0, - out IntPtr testQueue); + out nint testQueue); if (result == 0) { @@ -95,12 +95,12 @@ namespace Ryujinx.Audio.Backends.Apple GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); int result = AudioQueueNewOutput( ref format, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero, 0, - out IntPtr testQueue); + out nint testQueue); if (result == 0) { diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index c9443dcd3..05f9e2a3f 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -27,8 +27,8 @@ namespace Ryujinx.Audio.Backends.Apple private readonly AudioQueueOutputCallback _callbackDelegate; private readonly GCHandle _gcHandle; - private IntPtr _audioQueue; - private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers]; + private nint _audioQueue; + private readonly nint[] _audioQueueBuffers = new nint[NumBuffers]; private readonly int[] _bufferBytesFilled = new int[NumBuffers]; private readonly int _bytesPerFrame; @@ -41,9 +41,9 @@ namespace Ryujinx.Audio.Backends.Apple [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void AudioQueueOutputCallback( - IntPtr userData, - IntPtr audioQueue, - IntPtr buffer); + nint userData, + nint audioQueue, + nint buffer); public AppleHardwareDeviceSession( AppleHardwareDeviceDriver driver, @@ -72,15 +72,15 @@ namespace Ryujinx.Audio.Backends.Apple RequestedSampleRate, RequestedChannelCount); - IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); - IntPtr userData = GCHandle.ToIntPtr(_gcHandle); + nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); + nint userData = GCHandle.ToIntPtr(_gcHandle); int result = AudioQueueNewOutput( ref format, callbackPtr, userData, - IntPtr.Zero, - IntPtr.Zero, + nint.Zero, + nint.Zero, 0, out _audioQueue); @@ -102,7 +102,7 @@ namespace Ryujinx.Audio.Backends.Apple } } - private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex) + private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex) { AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr; @@ -126,12 +126,12 @@ namespace Ryujinx.Audio.Backends.Apple buffer->AudioDataByteSize = (uint)capacityBytes; _bufferBytesFilled[bufferIndex] = bytesToRead; - AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero); + AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero); } - private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr) + private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr) { - if (!_started || bufferPtr == IntPtr.Zero) + if (!_started || bufferPtr == nint.Zero) return; int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr); @@ -176,7 +176,7 @@ namespace Ryujinx.Audio.Backends.Apple } } - private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize) + private unsafe void ApplyVolume(nint dataPtr, int byteSize) { float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f); if (volume >= 0.999f) @@ -226,7 +226,7 @@ namespace Ryujinx.Audio.Backends.Apple return; _started = true; - AudioQueueStart(_audioQueue, IntPtr.Zero); + AudioQueueStart(_audioQueue, nint.Zero); } } @@ -265,11 +265,11 @@ namespace Ryujinx.Audio.Backends.Apple { Stop(); - if (_audioQueue != IntPtr.Zero) + if (_audioQueue != nint.Zero) { AudioQueueStop(_audioQueue, true); AudioQueueDispose(_audioQueue, true); - _audioQueue = IntPtr.Zero; + _audioQueue = nint.Zero; } if (_gcHandle.IsAllocated) diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs index 072e49d8c..56bd65e6d 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs @@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native unsafe { int* frameCountPtr = &nativeFrameCount; - IntPtr* arenasPtr = &arenas; + nint* arenasPtr = &arenas; CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr)); frameCount = *frameCountPtr; diff --git a/src/Ryujinx.Cpu/AddressTable.cs b/src/Ryujinx.Cpu/AddressTable.cs index 91a65e899..19e405491 100644 --- a/src/Ryujinx.Cpu/AddressTable.cs +++ b/src/Ryujinx.Cpu/AddressTable.cs @@ -30,9 +30,9 @@ namespace ARMeilleure.Common /// /// Base address for the page. /// - public readonly IntPtr Address; + public readonly nint Address; - public AddressTablePage(bool isSparse, IntPtr address) + public AddressTablePage(bool isSparse, nint address) { IsSparse = isSparse; Address = address; @@ -47,20 +47,20 @@ namespace ARMeilleure.Common public readonly SparseMemoryBlock Block; private readonly TrackingEventDelegate _trackingEvent; - public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) + public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) { SparseMemoryBlock block = new(size, pageInit, null); _trackingEvent = (address, size, write) => { ulong pointer = (ulong)block.Block.Pointer + address; - ensureMapped((IntPtr)pointer); + ensureMapped((nint)pointer); return pointer; }; bool added = NativeSignalHandler.AddTrackedRegion( (nuint)block.Block.Pointer, - (nuint)(block.Block.Pointer + (IntPtr)block.Block.Size), + (nuint)(block.Block.Pointer + (nint)block.Block.Size), Marshal.GetFunctionPointerForDelegate(_trackingEvent)); if (!added) @@ -116,7 +116,7 @@ namespace ARMeilleure.Common } /// - public IntPtr Base + public nint Base { get { @@ -124,7 +124,7 @@ namespace ARMeilleure.Common lock (_pages) { - return (IntPtr)GetRootPage(); + return (nint)GetRootPage(); } } } @@ -240,7 +240,7 @@ namespace ARMeilleure.Common long index = Levels[^1].GetValue(address); - EnsureMapped((IntPtr)(page + index)); + EnsureMapped((nint)(page + index)); return ref page[index]; } @@ -284,7 +284,7 @@ namespace ARMeilleure.Common /// Ensure the given pointer is mapped in any overlapping sparse reservations. /// /// Pointer to be mapped - private void EnsureMapped(IntPtr ptr) + private void EnsureMapped(nint ptr) { if (Sparse) { @@ -299,7 +299,7 @@ namespace ARMeilleure.Common { SparseMemoryBlock sparse = reserved.Block; - if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size) + if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size) { sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer)); @@ -319,15 +319,15 @@ namespace ARMeilleure.Common /// /// Level to get the fill value for /// The fill value - private IntPtr GetFillValue(int level) + private nint GetFillValue(int level) { if (_fillBottomLevel != null && level == Levels.Length - 2) { - return (IntPtr)_fillBottomLevelPtr; + return (nint)_fillBottomLevelPtr; } else { - return IntPtr.Zero; + return nint.Zero; } } @@ -379,7 +379,7 @@ namespace ARMeilleure.Common /// Fill value /// if leaf; otherwise /// Allocated block - private IntPtr Allocate(int length, T fill, bool leaf) where T : unmanaged + private nint Allocate(int length, T fill, bool leaf) where T : unmanaged { int size = sizeof(T) * length; @@ -405,7 +405,7 @@ namespace ARMeilleure.Common } } - page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset); + page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset); _sparseReservedOffset += (ulong)size; @@ -413,7 +413,7 @@ namespace ARMeilleure.Common } else { - IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size); + nint address = (nint)NativeAllocator.Instance.Allocate((uint)size); page = new AddressTablePage(false, address); Span span = new((void*)page.Address, length); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index e7a1afe1a..bfb4b839e 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel; - IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; + nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size)) { diff --git a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs index c1c59939e..4f5dbf6b9 100644 --- a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK public static void Initialize() { - IntPtr configSize = (nint)Marshal.SizeOf(); + nint configSize = (nint)Marshal.SizeOf(); vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index c4dbf41f6..02c4e6873 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray(); } - IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); + nint appName = Marshal.StringToHGlobalAnsi(AppName); ApplicationInfo applicationInfo = new() { @@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api) { - IntPtr appName = Marshal.StringToHGlobalAnsi(AppName); + nint appName = Marshal.StringToHGlobalAnsi(AppName); ApplicationInfo applicationInfo = new() { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index d99488d85..0624fcf91 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public nint Handle => IntPtr.Zero; + public nint Handle => nint.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; diff --git a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs index 344a48be6..4d85fa400 100644 --- a/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs +++ b/src/Ryujinx.Memory/WindowsShared/PlaceholderManager.cs @@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared { SplitForMap((ulong)location, (ulong)size, srcOffset); - IntPtr ptr = WindowsApi.MapViewOfFile3( + nint ptr = WindowsApi.MapViewOfFile3( sharedMemory, WindowsApi.CurrentProcessHandle, location, diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs index 73a7f7dfc..313a85a41 100644 --- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs +++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs @@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory // Create some info to be used for managing the native writing loop. int stateSize = Unsafe.SizeOf(); - IntPtr statePtr = Marshal.AllocHGlobal(stateSize); + nint statePtr = Marshal.AllocHGlobal(stateSize); Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize); ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef((void*)statePtr); From 5ed94c365bd5792e4234d4533d08e499bb11b1a4 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Tue, 27 Jan 2026 17:52:45 -0600 Subject: [PATCH 06/23] add a stack trace for the catch branch of AppleHardwareDeviceDriver.IsSupported --- src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs | 3 ++- src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index f20da5557..2e3b97517 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -110,8 +110,9 @@ namespace Ryujinx.Audio.Backends.Apple return false; } - catch + catch (Exception e) { + Logger.Error?.Print(LogClass.Audio, $"Failed to check if AudioToolbox is supported: {e.Message}\n{e.StackTrace}"); return false; } } diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs index 05f9e2a3f..1606e9954 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -1,15 +1,12 @@ 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 System.Runtime.Versioning; -using Ryujinx.Audio.Backends.Apple.Native; using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; -using static Ryujinx.Audio.Backends.Apple.AppleHardwareDeviceDriver; namespace Ryujinx.Audio.Backends.Apple { From cc5b60bbcad2fe43f593d5201e1900e46a9d75af Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 28 Jan 2026 00:05:02 -0600 Subject: [PATCH 07/23] fix AppleHardwareDeviceDriver.IsSupported (no fancy check is needed; it's on any macOS version 10.5 (Leopard) and above) --- .../AppleHardwareDeviceDriver.cs | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs index 2e3b97517..12fa5f0cc 100644 --- a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -83,39 +83,7 @@ namespace Ryujinx.Audio.Backends.Apple } } - public static bool IsSupported => IsSupportedInternal(); - - private static bool IsSupportedInternal() - { - if (!OperatingSystem.IsMacOS()) return false; - - try - { - AudioStreamBasicDescription format = - GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2); - int result = AudioQueueNewOutput( - ref format, - nint.Zero, - nint.Zero, - nint.Zero, - nint.Zero, - 0, - out nint testQueue); - - if (result == 0) - { - AudioQueueDispose(testQueue, true); - return true; - } - - return false; - } - catch (Exception e) - { - Logger.Error?.Print(LogClass.Audio, $"Failed to check if AudioToolbox is supported: {e.Message}\n{e.StackTrace}"); - return false; - } - } + public static bool IsSupported => OperatingSystem.IsMacOSVersionAtLeast(10, 5); public ManualResetEvent GetUpdateRequiredEvent() => _updateRequiredEvent; From a4a0fcd4da1f4287c6a68a335fb73283af922672 Mon Sep 17 00:00:00 2001 From: Babib3l Date: Wed, 28 Jan 2026 14:01:39 +0100 Subject: [PATCH 08/23] General translations updates + fixes (ryubing/ryujinx!248) See merge request ryubing/ryujinx!248 --- assets/Locales/Root.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 3578f689d..9ecf9826c 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -3733,7 +3733,7 @@ "el_GR": "DLC/Ενημερώσεις για αρχεία/παιχνίδια που λείπουν θα αποφορτωθούν αυτόματα", "en_US": "DLC/Updates Referring To Missing Files/Games Will Unload Automatically", "es_ES": "DLC/Actualizaciones que hacen referencia a archivos/juegos ausentes se descargarán automáticamente", - "fr_FR": "DLC/Mises à jour concernant des fichiers/jeux manquants seront déchargées automatiquement", + "fr_FR": "DLC/Mises à Jour Concernant des Fichiers/Jeux Manquants Seront Déchargées Automatiquement", "he_IL": "DLC/עדכונים המתייחסים לקבצים/משחקים חסרים יוסרו אוטומטית", "it_IT": "DLC/Aggiornamenti relativi a file/gioco mancanti verranno disabilitati automaticamente", "ja_JP": "DLC/欠損ファイル/ゲームを参照するアップデートは自動的にアンロードされます", @@ -13933,7 +13933,7 @@ "el_GR": "\n\nΑυτό θα αντικαταστήσει την τρέχουσα έκδοση συστήματος {0}.", "en_US": "\n\nThis will replace the current system version {0}.", "es_ES": "\n\nEsto reemplazará la versión de sistema actual, {0}.", - "fr_FR": "\n\nCela remplacera la version actuelle du système {0}.", + "fr_FR": "\n\nCeci remplacera la version actuelle du système {0}.", "he_IL": "\n\nזה יחליף את גרסת המערכת הנוכחית {0}.", "it_IT": "\n\nQuesta sostituirà l'attuale versione del sistema ({0}).", "ja_JP": "\n\n現在のシステムバージョン {0} を置き換えます.", @@ -14108,7 +14108,7 @@ "el_GR": "", "en_US": "\n\nThis may replace some of the current installed Keys.", "es_ES": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.", - "fr_FR": "\n\nCela peut remplacer certaines des Clés actuellement installées.", + "fr_FR": "\n\nCeci peut remplacer certaines des Clés actuellement installées.", "he_IL": "", "it_IT": "\n\nAlcune delle chiavi già installate potrebbero essere sovrascritte.", "ja_JP": "", @@ -16583,7 +16583,7 @@ "el_GR": "Ενεργοποίηση Πολυνηματικής Επεξεργασίας Γραφικών", "en_US": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", "es_ES": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", - "fr_FR": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore la performance sur les pilotes GPU sans support natif du multithreading. Offre une légère amélioration des performances sur les pilotes multithreadés.\n\nRéglez sur AUTO si vous n’êtes pas sûr.", + "fr_FR": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore la performance sur les pilotes GPU sans support natif du multithreading. Offre une légère amélioration des performances sur les pilotes multithreadés.\n\nSéléctionnez Auto si vous n’êtes pas sûr.", "he_IL": "מריץ פקודות גראפיקה בתהליך שני נפרד.\n\nמאיץ עיבוד הצללות, מפחית תקיעות ומשפר ביצועים של דרייבר כרטיסי מסך אשר לא תומכים בהרצה רב-תהליכית.\n\nמוטב להשאיר על אוטומטי אם לא בטוחים.", "it_IT": "Esegue i comandi del backend grafico su un secondo thread.\n\nVelocizza la compilazione degli shader, riduce lo stuttering e migliora le prestazioni sui driver grafici senza il supporto integrato al multithreading. Migliora leggermente le prestazioni sui driver che supportano il multithreading.\n\nNel dubbio, imposta l'opzione su Automatico.", "ja_JP": "グラフィックスバックエンドのコマンドを別スレッドで実行します.\n\nシェーダのコンパイルを高速化し, 遅延を軽減し, マルチスレッド非対応の GPU ドライバにおいてパフォーマンスを改善します. マルチスレッド対応のドライバでも若干パフォーマンス改善が見られます.\n\nよくわからない場合は自動に設定してください.", @@ -16608,7 +16608,7 @@ "el_GR": "Εκτελεί εντολές γραφικών σε ένα δεύτερο νήμα. Επιτρέπει την πολυνηματική μεταγλώττιση Shader σε χρόνο εκτέλεσης, μειώνει το τρεμόπαιγμα και βελτιώνει την απόδοση των προγραμμάτων οδήγησης χωρίς τη δική τους υποστήριξη πολλαπλών νημάτων. Ποικίλες κορυφαίες επιδόσεις σε προγράμματα οδήγησης με multithreading. Μπορεί να χρειαστεί επανεκκίνηση του Ryujinx για να απενεργοποιήσετε σωστά την ενσωματωμένη λειτουργία πολλαπλών νημάτων του προγράμματος οδήγησης ή ίσως χρειαστεί να το κάνετε χειροκίνητα για να έχετε την καλύτερη απόδοση.", "en_US": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.", "es_ES": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", - "fr_FR": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore la performance sur les pilotes GPU sans support de multithreading. Légère augementation des performances sur les pilotes avec multithreading intégrer.\n\nRéglez sur Auto en cas d'incertitude.", + "fr_FR": "Exécute des commandes du backend graphiques sur un second thread.\n\nAccélère la compilation des shaders, réduit les crashs et les lags, améliore la performance sur les pilotes GPU sans support de multithreading. Légère augementation des performances sur les pilotes avec multithreading intégrer.\n\nSéléctionnez Auto en cas d'incertitude.", "he_IL": "מריץ פקודות גראפיקה בתהליך שני נפרד.\n\nמאיץ עיבוד הצללות, מפחית תקיעות ומשפר ביצועים של דרייבר כרטיסי מסך אשר לא תומכים בהרצה רב-תהליכית.\n\nמוטב להשאיר על אוטומטי אם לא בטוחים.", "it_IT": "Esegue i comandi del backend grafico su un secondo thread.\n\nVelocizza la compilazione degli shader, riduce lo stuttering e migliora le prestazioni sui driver grafici senza il supporto integrato al multithreading. Migliora leggermente le prestazioni sui driver che supportano il multithreading.\n\nNel dubbio, imposta l'opzione su Automatico.", "ja_JP": "グラフィックスバックエンドのコマンドを別スレッドで実行します.\n\nシェーダのコンパイルを高速化し, 遅延を軽減し, マルチスレッド非対応の GPU ドライバにおいてパフォーマンスを改善します. マルチスレッド対応のドライバでも若干パフォーマンス改善が見られます.\n\nよくわからない場合は自動に設定してください.", @@ -16708,7 +16708,7 @@ "el_GR": "", "en_US": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", "es_ES": "Nivel de filtrado anisotrópico. Setear en Auto para utilizar el valor solicitado por el juego.", - "fr_FR": "Niveau de filtrage anisotrope. Réglez sur Auto pour utiliser la valeur demandée par le jeu.", + "fr_FR": "Niveau de filtrage anisotrope. Séléctionnez Auto pour utiliser la valeur demandée par le jeu.", "he_IL": "", "it_IT": "Livello del filtro anisotropico. Imposta su Automatico per usare il valore richiesto dal gioco.", "ja_JP": "異方性フィルタリングのレベルです. ゲームが要求する値を使用する場合は「自動」を設定してください.", @@ -16733,7 +16733,7 @@ "el_GR": "", "en_US": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", "es_ES": "Relación de aspecto aplicada a la ventana del renderizador.\n\nSolamente modificar esto si estás utilizando un mod de relación de aspecto para su juego, en cualquier otro caso los gráficos se estirarán.\n\nDejar en 16:9 si no sabe que hacer.", - "fr_FR": "Format\u00A0d'affichage appliqué à la fenêtre du moteur de rendu.\n\nChangez cela uniquement si vous utilisez un mod changeant le format\u00A0d'affichage pour votre jeu, sinon les graphismes seront étirés.\n\nLaissez sur 16:9 si vous n'êtes pas sûr.", + "fr_FR": "Format\u00A0d'affichage appliqué à la fenêtre du moteur de rendu.\n\nChangez celui-ci uniquement si vous utilisez un mod changeant le format\u00A0d'affichage pour votre jeu, sinon les graphismes seront étirés.\n\nLaissez sur 16:9 si vous n'êtes pas sûr.", "he_IL": "", "it_IT": "Proporzioni dello schermo applicate alla finestra di renderizzazione.\n\nCambialo solo se stai usando una mod di proporzioni per il tuo gioco, altrimenti la grafica verrà allungata.\n\nLasciare il 16:9 se incerto.", "ja_JP": "レンダリングウインドウに適用するアスペクト比です.\n\nゲームにアスペクト比を変更する mod を使用している場合のみ変更してください.\n\nわからない場合は16:9のままにしておいてください.\n", @@ -21955,23 +21955,23 @@ "Translations": { "ar_SA": "اختر فلتر التكبير الذي سيتم تطبيقه عند استخدام مقياس الدقة.\n\nيعمل Bilinear بشكل جيد مع الألعاب ثلاثية الأبعاد وهو خيار افتراضي آمن.\n\nيوصى باستخدام Nearest لألعاب البكسل الفنية.\n\nFSR 1.0 هو مجرد مرشح توضيحي، ولا ينصح باستخدامه مع FXAA أو SMAA.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على Bilinear إذا لم تكن متأكدا.", "de_DE": "Wählen Sie den Skalierungsfilter, der bei der Auflösungsskalierung angewendet werden soll.\n\nBilinear eignet sich gut für 3D-Spiele und ist eine sichere Standardoption.\n\nNearest wird für Pixel-Art-Spiele empfohlen.\n\nFSR 1.0 ist lediglich ein Schärfungsfilter und wird nicht für die Verwendung mit FXAA oder SMAA empfohlen.\n\nDiese Option kann geändert werden, während ein Spiel läuft, indem Sie unten auf \"Anwenden\" klicken; Sie können das Einstellungsfenster einfach zur Seite schieben und experimentieren, bis Sie Ihr bevorzugtes Aussehen für ein Spiel gefunden haben.\n\nBleiben Sie auf BILINEAR, wenn Sie unsicher sind.", - "el_GR": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "el_GR": "", "en_US": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nArea scaling is recommended when downscaling resolutions that are larger than the output window. It can be used to achieve a supersampled anti-aliasing effect when downscaling by more than 2x.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", "es_ES": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.", - "fr_FR": "Choisis le filtre de mise à l'échelle qui sera appliqué lors de l'utilisation de la mise à l'échelle de la résolution.\n\nLe filtre bilinéaire fonctionne bien pour les jeux en 3D et constitue une option par défaut sûre.\n\nLe filtre le plus proche est recommandé pour les jeux de pixel art.\n\nFSR 1.0 est simplement un filtre de netteté, non recommandé pour une utilisation avec FXAA ou SMAA.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres de côté et expérimenter jusqu'à ce que vous trouviez l'aspect souhaité pour un jeu.\n\nLaissez sur BILINÉAIRE si vous n'êtes pas sûr.", - "he_IL": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "fr_FR": "Choisit le filtre de mise à l'échelle qui sera appliqué lors de l'utilisation de la mise à l'échelle de la résolution.\n\nLe filtre bilinéaire fonctionne le mieux pour les jeux en 3D et constitue une option par défaut fiable.\n\nLe filtre le plus proche est recommandé pour les jeux de pixel art.\n\nFSR 1.0 est simplement un filtre de netteté, non recommandé pour une utilisation avec FXAA ou SMAA.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres de côté et expérimenter jusqu'à ce que vous trouviez l'aspect souhaité pour un jeu.\n\nLaissez sur BILINÉAIRE si vous n'êtes pas sûr.", + "he_IL": "", "it_IT": "Scegli il filtro di scaling che verrà applicato quando si utilizza lo scaling della risoluzione.\n\nBilineare funziona bene per i giochi 3D ed è un'opzione predefinita affidabile.\n\nNearest è consigliato per i giochi in pixel art.\n\nFSR 1.0 è solo un filtro di nitidezza, sconsigliato per l'uso con FXAA o SMAA.\n\nLo scaling ad area è consigliato quando si riducono delle risoluzioni che sono più grandi della finestra di output. Può essere usato per ottenere un effetto di anti-aliasing supercampionato quando si riduce di più di 2x.\n\nQuesta opzione può essere modificata mentre un gioco è in esecuzione facendo clic su \"Applica\" qui sotto; puoi semplicemente spostare la finestra delle impostazioni da parte e sperimentare fino a quando non trovi il tuo look preferito per un gioco.\n\nNel dubbio, lascia su Bilineare.", "ja_JP": "解像度変更時に適用されるスケーリングフィルタを選択します.\n\nBilinearは3Dゲームに適しており, 安全なデフォルトオプションです.\n\nピクセルアートゲームにはNearestを推奨します.\n\nFSR 1.0は単なるシャープニングフィルタであり, FXAAやSMAAとの併用は推奨されません.\n\nこのオプションは, ゲーム実行中に下の「適用」をクリックすることで変更できます. 設定ウィンドウを脇に移動し, ゲームが好みの表示になるように試してみてください.\n\n不明な場合はBilinearのままにしておいてください.", "ko_KR": "해상도 스케일을 사용할 때 적용될 스케일링 필터를 선택합니다.\n\n쌍선형은 3D 게임에 적합하며 안전한 기본 옵션입니다.\n\nNearest는 픽셀 아트 게임에 권장됩니다.\n\nFSR 1.0은 단순히 선명도 필터일 뿐이며 FXAA 또는 SMAA와 함께 사용하는 것은 권장되지 않습니다.\n\nArea 스케일링은 출력 창보다 큰 해상도를 다운스케일링할 때 권장됩니다. 2배 이상 다운스케일링할 때 슈퍼샘플링된 앤티앨리어싱 효과를 얻는 데 사용할 수 있습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임을 실행하는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮겨 원하는 게임 모양을 찾을 때까지 실험하면 됩니다.\n\n모르면 쌍선형을 그대로 두세요.", "no_NO": "Velg det skaleringsfilteret som skal brukes når du bruker oppløsningsskalaen.\n\nBilinear fungerer godt for 3D-spill og er et trygt standardalternativ.\n\nNærmeste anbefales for pixel kunst-spill.\n\nFSR 1.0 er bare et skarpere filter, ikke anbefalt for bruk med FXAA eller SMAA.\n\nOmrådeskalering anbefales når nedskalering er større enn utgangsvinduet. Den kan brukes til å oppnå en superprøvetaket anti-aliasingseffekt når en nedskalerer med mer enn 2x.\n\nDette valget kan endres mens et spill kjører ved å klikke \"Apply\" nedenfor; du kan bare flytte innstillingsvinduet til du finner det foretrukne utseendet til et spill.\n\nLa være på BILINEAR hvis usikker.", - "pl_PL": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "pl_PL": "", "pt_BR": "Escolha o filtro de escala que será aplicado ao usar a escala de resolução.\n\nBilinear funciona bem para jogos 3D e é uma opção padrão segura.\n\nNearest é recomendado para jogos em pixel art.\n\nFSR 1.0 é apenas um filtro de nitidez, não recomendado para uso com FXAA ou SMAA.\n\nEssa opção pode ser alterada enquanto o jogo está em execução, clicando em \"Aplicar\" abaixo; basta mover a janela de configurações para o lado e experimentar até encontrar o visual preferido para o jogo.\n\nMantenha em BILINEAR se estiver em dúvida.", - "ru_RU": "Фильтрация текстур, которая будет применяться при масштабировании.\n\nБилинейная хорошо работает для 3D-игр и является настройкой по умолчанию.\n\nСтупенчатая рекомендуется для пиксельных игр.\n\nFSR это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЗональная рекомендуется в случае использования разрешения больше разрешения окна. Можно использовать для достижения эффекта суперсемплига (SSAA) при даунскейле более чем в 2 раза.\n\nЭта опция может быть изменена во время игры по нажатию кнопки «Применить» ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать «Билинейная».", + "ru_RU": "Фильтрация текстур, которая будет применяться при масштабировании.\n\nБилинейная хорошо работает для 3D-игр и является настройкой по умолчанию.\n\nСтупенчатая рекомендуется для пиксельных игр.\n\nFSR 1.0 это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЗональная рекомендуется в случае использования разрешения больше разрешения окна. Можно использовать для достижения эффекта суперсемплига (SSAA) при даунскейле более чем в 2 раза.\n\nЭта опция может быть изменена во время игры по нажатию кнопки «Применить» ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать «Билинейная».", "sv_SE": "Välj det skalfilter som ska tillämpas vid användning av upplösningsskala.\n\nBilinjär fungerar bra för 3D-spel och är ett säkert standardalternativ.\n\nNärmast rekommenderas för pixel art-spel.\n\nFSR 1.0 är bara ett skarpningsfilter, rekommenderas inte för FXAA eller SMAA.\n\nOmrådesskalning rekommenderas vid nedskalning av upplösning som är större än utdatafönstret. Det kan användas för att uppnå en supersamplad anti-alias-effekt vid nedskalning med mer än 2x.\n\nDetta alternativ kan ändras medan ett spel körs genom att klicka på \"Tillämpa\" nedan. du kan helt enkelt flytta inställningsfönstret åt sidan och experimentera tills du hittar ditt föredragna utseende för ett spel.\n\nLämna som BILINJÄR om du är osäker.", "th_TH": "เลือกตัวกรองสเกลที่จะใช้เมื่อใช้สเกลความละเอียด\n\nBilinear ทำงานได้ดีกับเกม 3D และเป็นตัวเลือกเริ่มต้นที่ปลอดภัย\n\nแนะนำให้ใช้เกมภาพพิกเซลที่ใกล้เคียงที่สุด\n\nFSR 1.0 เป็นเพียงตัวกรองความคมชัด ไม่แนะนำให้ใช้กับ FXAA หรือ SMAA\n\nตัวเลือกนี้สามารถเปลี่ยนแปลงได้ในขณะที่เกมกำลังทำงานอยู่โดยคลิก \"นำไปใช้\" ด้านล่าง คุณสามารถย้ายหน้าต่างการตั้งค่าไปด้านข้างและทดลองจนกว่าคุณจะพบรูปลักษณ์ที่คุณต้องการสำหรับเกม", - "tr_TR": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.", + "tr_TR": "", "uk_UA": "Оберіть фільтр масштабування, який буде використовуватися при збільшенні роздільної здатності.\n\n\"Білінійний (Bilinear)\" добре виглядає в 3D іграх та є хорошим варіантом за замовчуванням.\n\n\"Найближчий (Nearest)\" рекомендується для піксельних ігор.\n\n\"FSR 1.0\" - фільтр різкості. Не варто використовувати разом з FXAA або SMAA.\n\nЦю опцію можна міняти під час гри натисканням \"Застосувати\" в цьому вікні; щоб знайти найкращий варіант, просто відсуньте вікно налаштувань і поекспериментуйте.\n\nЗалиште \"Білінійний\", якщо не впевнені.", - "zh_CN": "选择在分辨率缩放时将使用的缩放过滤器。\n\nBilinear(双线性过滤)对于3D游戏效果较好,是一个安全的默认选项。\n\nNearest(最近邻过滤)推荐用于像素艺术游戏。\n\nFSR(超级分辨率锐画)只是一个锐化过滤器,不推荐与 FXAA 或 SMAA 抗锯齿一起使用。\n\nArea(局部过滤),当渲染分辨率大于窗口实际分辨率,推荐该选项。该选项在渲染比例大于2.0的情况下,可以实现超采样的效果。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n如果不确定,请保持为“Bilinear(双线性过滤)”。", + "zh_CN": "选择在分辨率缩放时将使用的缩放过滤器。\n\nBilinear(双线性过滤)对于3D游戏效果较好,是一个安全的默认选项。\n\nNearest(最近邻过滤)推荐用于像素艺术游戏。\n\nFSR 1.0(超级分辨率锐画)只是一个锐化过滤器,不推荐与 FXAA 或 SMAA 抗锯齿一起使用。\n\nArea(局部过滤),当渲染分辨率大于窗口实际分辨率,推荐该选项。该选项在渲染比例大于2.0的情况下,可以实现超采样的效果。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n如果不确定,请保持为“Bilinear(双线性过滤)”。", "zh_TW": "選擇使用解析度縮放時套用的縮放過濾器。\n\n雙線性 (Bilinear) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用近鄰性 (Nearest) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持雙線性 (Bilinear) 狀態。" } }, @@ -22031,7 +22031,7 @@ "ar_SA": null, "de_DE": null, "el_GR": null, - "en_US": "FSR", + "en_US": "FSR 1.0", "es_ES": null, "fr_FR": null, "he_IL": null, @@ -22108,7 +22108,7 @@ "el_GR": "", "en_US": "Set FSR 1.0 sharpening level. Higher is sharper.", "es_ES": "Ajuste el nivel de nitidez FSR 1.0. Mayor es más nítido.", - "fr_FR": "Définis le niveau de netteté FSR 1.0. Plus la valeur est élevée, plus l'image est nette.", + "fr_FR": "Définit le niveau de netteté FSR 1.0. Plus la valeur est élevée, plus l'image est nette.", "he_IL": "", "it_IT": "Imposta il livello di nitidezza di FSR 1.0. Valori più alti comportano una maggiore nitidezza.", "ja_JP": "FSR 1.0のシャープ化レベルを設定します. 高い値ほどシャープになります.", From a16a072155196d433ceca1e2aa53026b833ff831 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Thu, 29 Jan 2026 13:45:35 -0600 Subject: [PATCH 09/23] HLE: Implement 10106 and 10107 in IPrepoService (ryubing/ryujinx!254) See merge request ryubing/ryujinx!254 --- src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs | 30 +++++++++++++++++-- .../Sdk/Prepo/IPrepoService.cs | 6 ++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index 8336adb18..5a1c4edd8 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -33,7 +33,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc [CmifCommand(10100)] // 1.0.0-5.1.0 [CmifCommand(10102)] // 6.0.0-9.2.0 [CmifCommand(10104)] // 10.0.0+ - public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid) + public Result SaveReportOld([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid) + { + if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) + { + return PrepoResult.PermissionDenied; + } + + ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, Uid.Null); + + return Result.Success; + } + + [CmifCommand(10106)] // 21.0.0+ + public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled) { if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) { @@ -48,7 +61,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc [CmifCommand(10101)] // 1.0.0-5.1.0 [CmifCommand(10103)] // 6.0.0-9.2.0 [CmifCommand(10105)] // 10.0.0+ - public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid) + public Result SaveReportWithUserOld(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid) + { + if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) + { + return PrepoResult.PermissionDenied; + } + + ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, userId, true); + + return Result.Success; + } + + [CmifCommand(10107)] // 21.0.0+ + public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled) { if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) { diff --git a/src/Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs b/src/Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs index fd63755e3..a55570d54 100644 --- a/src/Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs +++ b/src/Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs @@ -8,8 +8,10 @@ namespace Ryujinx.Horizon.Sdk.Prepo { interface IPrepoService : IServiceObject { - Result SaveReport(ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid); - Result SaveReportWithUser(Uid userId, ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid); + Result SaveReportOld(ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid); + Result SaveReport(ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid, bool optInCheckEnabled); + Result SaveReportWithUserOld(Uid userId, ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid); + Result SaveReportWithUser(Uid userId, ReadOnlySpan gameRoomBuffer, ReadOnlySpan reportBuffer, ulong pid, bool optInCheckEnabled); Result RequestImmediateTransmission(); Result GetTransmissionStatus(out int status); Result GetSystemSessionId(out ulong systemSessionId); From 478b66fd49aa03fe7b4e78453246db4a2ab89975 Mon Sep 17 00:00:00 2001 From: sh0inx Date: Fri, 30 Jan 2026 20:48:41 -0600 Subject: [PATCH 10/23] HLE: Stubbed IUserLocalCommuniationService SetProtocol (106) (ryubing/ryujinx!253) See merge request ryubing/ryujinx!253 --- .../IUserLocalCommunicationService.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 62a86ad91..71d1623f3 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; +using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; @@ -14,6 +15,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; +using System.ComponentModel; using System.IO; using System.Net; using System.Net.NetworkInformation; @@ -487,6 +489,23 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator return ResultCode.Success; } + [CommandCmif(106)] // 20.0.0+ + // SetProtocol + public ResultCode SetProtocol(ServiceCtx context) + { + uint protocolValue = context.RequestData.ReadUInt32(); + + // On NX only input value 1 or 3 is allowed, with an error being thrown otherwise. + + if (protocolValue != 1 && protocolValue != 3) + { + throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}"); + } + + Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}"); + return ResultCode.Success; + } + [CommandCmif(200)] // OpenAccessPoint() public ResultCode OpenAccessPoint(ServiceCtx context) From 922775664cd41073289afef3c5b11cadbd192287 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Sat, 31 Jan 2026 11:22:14 -0600 Subject: [PATCH 11/23] audio: Fix crash due to invalid Splitter size (ryubing/ryujinx!257) See merge request ryubing/ryujinx!257 --- src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs | 2 +- src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs index 98b224ebf..636935055 100644 --- a/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs +++ b/src/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Common public uint MixesSize; public uint SinksSize; public uint PerformanceBufferSize; - public uint Unknown24; + public uint SplitterSize; public uint RenderInfoSize; #pragma warning disable IDE0051, CS0169 // Remove unused field diff --git a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs index 917d63716..cf5004eae 100644 --- a/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs +++ b/src/Ryujinx.Audio/Renderer/Server/StateUpdater.cs @@ -433,8 +433,12 @@ namespace Ryujinx.Audio.Renderer.Server public ResultCode UpdateSplitter(SplitterContext context) { + long initialInputConsumed = _inputReader.Consumed; + if (context.Update(ref _inputReader)) { + _inputReader.SetConsumed(initialInputConsumed + _inputHeader.SplitterSize); + return ResultCode.Success; } From 081cdcab0c2fbc683f9d8f4a48fdc441e0c673bd Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 31 Jan 2026 17:58:31 -0600 Subject: [PATCH 12/23] remap joy-cons (ryubing/ryujinx!258) See merge request ryubing/ryujinx!258 --- src/Ryujinx.Input.SDL3/SDL3JoyCon.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index d71b06dda..33bab7739 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -24,10 +24,10 @@ namespace Ryujinx.Input.SDL3 private readonly Dictionary _leftButtonsDriverMapping = new() { {GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK}, - {GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH}, - {GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH}, - {GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST}, - {GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST}, + {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}, @@ -37,10 +37,10 @@ namespace Ryujinx.Input.SDL3 private readonly Dictionary _rightButtonsDriverMapping = new() { {GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK}, - {GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST}, - {GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH}, - {GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH}, - {GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST}, + {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}, From 1b3bf1473d7513d51e2c32ddea4d9a18f9e38b14 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 31 Jan 2026 23:12:29 -0600 Subject: [PATCH 13/23] Fix Dual Joy-Con driver and InputView (ryubing/ryujinx!259) See merge request ryubing/ryujinx!259 --- src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs | 147 +++++++++++++++--- src/Ryujinx.Input.SDL3/SDL3JoyCon.cs | 10 ++ src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs | 49 ++---- .../UI/ViewModels/Input/InputViewModel.cs | 23 ++- 4 files changed, 168 insertions(+), 61 deletions(-) diff --git a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs index b1100384f..897966689 100644 --- a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs @@ -9,10 +9,20 @@ using static SDL.SDL3; namespace Ryujinx.Input.SDL3 { + + public unsafe class SDL3GamepadDriver : IGamepadDriver { private readonly Dictionary _gamepadsInstanceIdsMapping; private readonly Dictionary _gamepadsIds; + /// + /// Unlinked joy-cons + /// + private readonly Dictionary _joyConsIds; + /// + /// Linked joy-cons, remove dual joy-con from _gamepadsIds when a linked joy-con is removed + /// + private readonly Dictionary _linkedJoyConsIds; private readonly Lock _lock = new(); public ReadOnlySpan GamepadsIds @@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3 { lock (_lock) { - return _gamepadsIds.Values.ToArray(); + List temp = []; + temp.AddRange(_gamepadsIds.Values); + temp.AddRange(_joyConsIds.Values); + temp.AddRange(_linkedJoyConsIds.Values); + return temp.ToArray(); } } } @@ -35,6 +49,8 @@ namespace Ryujinx.Input.SDL3 { _gamepadsInstanceIdsMapping = new Dictionary(); _gamepadsIds = []; + _joyConsIds = []; + _linkedJoyConsIds = []; SDL3Driver.Instance.Initialize(); SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected; @@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3 int guidIndex = 0; id = guidIndex + "-" + guidString; - while (_gamepadsIds.ContainsValue(id)) + while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id)) { id = (++guidIndex) + "-" + guidString; } @@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3 private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId) { bool joyConPairDisconnected = false; + string fakeId = null; if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id)) return; lock (_lock) { - _gamepadsIds.Remove(joystickInstanceId); - if (!SDL3JoyConPair.IsCombinable(_gamepadsIds)) + if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId)) { - _gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id)); + if (!_joyConsIds.Remove(joystickInstanceId)) + { + _gamepadsIds.Remove(joystickInstanceId); + } + } + else + { + foreach (string matchId in _gamepadsIds.Values) + { + if (matchId.Contains(id)) + { + fakeId = matchId; + break; + } + } + + string leftId = fakeId!.Split('_')[0]; + string rightId = fakeId!.Split('_')[1]; + + if (leftId == id) + { + _linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId)); + _joyConsIds.Add(GetInstanceIdFromId(rightId), rightId); + } + else + { + _linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId)); + _joyConsIds.Add(GetInstanceIdFromId(leftId), leftId); + } + + _linkedJoyConsIds.Remove(joystickInstanceId); + _gamepadsIds.Remove(GetInstanceIdFromId(fakeId)); joyConPairDisconnected = true; } } @@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3 OnGamepadDisconnected?.Invoke(id); if (joyConPairDisconnected) { - OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id); + OnGamepadDisconnected?.Invoke(fakeId); } } private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId) { bool joyConPairConnected = false; + string fakeId = null; if (SDL_IsGamepad(joystickInstanceId)) { @@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3 { lock (_lock) { - - _gamepadsIds.Add(joystickInstanceId, id); - - if (SDL3JoyConPair.IsCombinable(_gamepadsIds)) + if (!SDL3JoyCon.IsJoyCon(joystickInstanceId)) { - // TODO - It appears that you can only have one joy con pair connected at a time? - // This was also the behavior before SDL3 - _gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id)); - uint fakeInstanceID = uint.MaxValue; - while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id)) + _gamepadsIds.Add(joystickInstanceId, id); + } + else + { + if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match)) { - fakeInstanceID--; + _joyConsIds.Remove(match, out string matchId); + _linkedJoyConsIds.Add(joystickInstanceId, id); + _linkedJoyConsIds.Add(match, matchId); + + uint fakeInstanceId = uint.MaxValue; + fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId) + ? $"{id}_{matchId}" + : $"{matchId}_{id}"; + while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId)) + { + fakeInstanceId--; + } + _gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId); + joyConPairConnected = true; + } + else + { + _joyConsIds.Add(joystickInstanceId, id); } - joyConPairConnected = true; } } OnGamepadConnected?.Invoke(id); if (joyConPairConnected) { - OnGamepadConnected?.Invoke(SDL3JoyConPair.Id); + OnGamepadConnected?.Invoke(fakeId); } } } @@ -193,10 +254,22 @@ namespace Ryujinx.Input.SDL3 { OnGamepadDisconnected?.Invoke(gamepad.Value); } + + foreach (var gamepad in _joyConsIds) + { + OnGamepadDisconnected?.Invoke(gamepad.Value); + } + + foreach (var gamepad in _linkedJoyConsIds) + { + OnGamepadDisconnected?.Invoke(gamepad.Value); + } lock (_lock) { _gamepadsIds.Clear(); + _joyConsIds.Clear(); + _linkedJoyConsIds.Clear(); } SDL3Driver.Instance.Dispose(); @@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3 public IGamepad GetGamepad(string id) { - if (id == SDL3JoyConPair.Id) + // joy-con pair ids is the combined ids of its parts which are split using a '_' + if (id.Contains('_')) { lock (_lock) { - return SDL3JoyConPair.GetGamepad(_gamepadsIds); + string leftId = id.Split('_')[0]; + string rightId = id.Split('_')[1]; + + SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId); + SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId); + + SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId); + SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId); + + if (leftGamepadHandle == null || rightGamepadHandle == null) + { + return null; + } + + return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId), + new SDL3JoyCon(rightGamepadHandle, rightId)); } } @@ -232,7 +321,7 @@ namespace Ryujinx.Input.SDL3 return null; } - if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix)) + if (SDL3JoyCon.IsJoyCon(instanceId)) { return new SDL3JoyCon(gamepadHandle, id); } @@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3 yield return GetGamepad(gamepad.Value); } } + + lock (_joyConsIds) + { + foreach (var gamepad in _joyConsIds) + { + yield return GetGamepad(gamepad.Value); + } + } + + lock (_linkedJoyConsIds) + { + foreach (var gamepad in _linkedJoyConsIds) + { + yield return GetGamepad(gamepad.Value); + } + } } } } diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 33bab7739..5311a256c 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3 return SDL_GetGamepadButton(_gamepadHandle, button); } + + public static bool IsJoyCon(SDL_JoystickID gamepadsId) + { + return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName; + } + + public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId) + { + return SDL_GetGamepadNameForID(gamepadsId) is LeftName; + } } } diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs index 303875eac..14352e5a4 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3 public const string Id = "JoyConPair"; string IGamepad.Id => Id; - public string Name => "* Nintendo Switch Joy-Con (L/R)"; + public string Name => "Nintendo Switch Dual Joy-Con (L/R)"; public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true }; public void Dispose() @@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3 right.SetTriggerThreshold(triggerThreshold); } - public static bool IsCombinable(Dictionary gamepadsIds) + public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary joyConIds, out SDL_JoystickID match) { - (int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds); - return leftIndex >= 0 && rightIndex >= 0; - } + bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1); + string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName; + match = 0; - private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary gamepadsIds) - { - Dictionary gamepadNames = gamepadsIds - .Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName) - .Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key)) - .ToDictionary(); - SDL_JoystickID idx; - int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1; - int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1; - - return (leftIndex, rightIndex); - } - - public unsafe static IGamepad GetGamepad(Dictionary gamepadsIds) - { - (int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds); - - if (leftIndex <= 0 || rightIndex <= 0) + foreach (var joyConId in joyConIds.Keys) { - return null; + if (SDL_GetGamepadNameForID(joyConId) == matchName) + { + match = joyConId; + + return true; + } } - - SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex); - SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex); - - if (leftGamepadHandle == null || rightGamepadHandle == null) - { - return null; - } - - return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]), - new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex])); + + return false; } } } diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 289dc0e9c..e5f085e0f 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _controller = 0; } - if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1) + if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1) { ControllerType controller = Controllers[_controller].Type; @@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input IsModified = true; RevertChanges(); FindPairedDeviceInConfigFile(); - + _isChangeTrackingActive = true; // Enable configuration change tracking } @@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) { - Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); + int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); + + // Avalonia bug: setting a newly instanced ComboBox to 0 + // causes the selected item to show up blank + // Workaround: set the box to 1 and then 0 + if (controllerIndex == 0) + { + Controller = 1; + } + + Controller = controllerIndex; } else { @@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input DeviceList.Clear(); Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); - int controllerNumber = 0; + foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) { using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); @@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (gamepad != null) { + int controllerNumber = 0; string name = GetUniqueGamepadName(gamepad, ref controllerNumber); Devices.Add((DeviceType.Controller, id, name)); } @@ -950,8 +961,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId); - LoadDevice(); + _isLoaded = false; LoadConfiguration(); + LoadDevice(); + _isLoaded = true; OnPropertyChanged(); IsModified = false; From 1260f93aaf7b2cf30084c668709ec7b2d6d9ee99 Mon Sep 17 00:00:00 2001 From: shinyoyo Date: Mon, 9 Feb 2026 15:07:22 +0800 Subject: [PATCH 14/23] =?UTF-8?q?Updated=20=20=E2=80=8CSimplified=20Chines?= =?UTF-8?q?e=E2=80=8C=20translation.=20(ryubing/ryujinx!260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See merge request ryubing/ryujinx!260 --- assets/Locales/RenderDoc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/Locales/RenderDoc.json b/assets/Locales/RenderDoc.json index b3f9462eb..71e3c77da 100644 --- a/assets/Locales/RenderDoc.json +++ b/assets/Locales/RenderDoc.json @@ -21,7 +21,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "启动 RenderDoc 帧捕获", "zh_TW": "" } }, @@ -46,7 +46,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "结束 RenderDoc 帧捕获", "zh_TW": "" } }, @@ -71,7 +71,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "丢弃 RenderDoc 帧捕获", "zh_TW": "" } }, @@ -96,7 +96,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。", "zh_TW": "" } } From 8208d43d9ef1cf819bc68ee150a98e77a4b6c250 Mon Sep 17 00:00:00 2001 From: Princess Piplup Date: Wed, 18 Feb 2026 00:57:50 +0000 Subject: [PATCH 15/23] compatiblity/2026-02-17 (ryubing/ryujinx!263) See merge request ryubing/ryujinx!263 --- docs/compatibility.csv | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index e476f9253..d33185201 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2050,7 +2050,9 @@ 010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26 0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29 0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07 +010057D00ECE4000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07 0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00 +010037A0170D2000,"NINTENDO 64™ – Nintendo Switch Online 18+",,ingame,2025-02-03 22:27:00 0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06 0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07 01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11 @@ -3307,6 +3309,7 @@ 0100AFA011068000,"Voxel Pirates",,playable,2022-09-28 22:55:02 0100BFB00D1F4000,"Voxel Sword",,playable,2022-08-30 14:57:27 01004E90028A2000,"Vroom in the night sky",Needs Update;vulkan-backend-bug,playable,2023-02-20 02:32:29 +0100BFC01D976000,"Virtual Boy – Nintendo Classics",services,nothing,2026-02-17 11:26:59 0100C7C00AE6C000,"VSR: Void Space Racing",,playable,2021-01-27 14:08:59 0100B130119D0000,"Waifu Uncovered",crash,ingame,2023-02-27 01:17:46 0100E29010A4A000,"Wanba Warriors",,playable,2020-10-04 17:56:22 From 6f95172bb6c6e897a027b8bae217659495f2d0f6 Mon Sep 17 00:00:00 2001 From: Awesomeangotti Date: Tue, 17 Feb 2026 19:24:01 -0600 Subject: [PATCH 16/23] Compatability Data Update (ryubing/ryujinx!264) See merge request ryubing/ryujinx!264 --- docs/compatibility.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index d33185201..fa804c909 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2640,6 +2640,7 @@ 0100B16009C10000,"SINNER: Sacrifice for Redemption",nvdec;UE4;vulkan-backend-bug,playable,2022-08-12 20:37:33 0100E9201410E000,"Sir Lovelot",,playable,2021-04-05 16:21:46 0100134011E32000,"Skate City",,playable,2022-11-04 11:37:39 +0100a8501b66e000,"Skateboard Drifting with Maxwell Cat: The Game Simulator",,playable,2026-02-17 19:05:00 0100B2F008BD8000,"Skee-Ball",,playable,2020-11-16 04:44:07 01001A900F862000,"Skelattack",,playable,2021-06-09 15:26:26 01008E700F952000,"Skelittle: A Giant Party!",,playable,2021-06-09 19:08:34 From d1205dc95d80bf0e9ca7e2a618e00a83bbf449c9 Mon Sep 17 00:00:00 2001 From: BowedCascade <110-BowedCascade@users.noreply.git.ryujinx.app> Date: Wed, 18 Feb 2026 18:13:15 -0600 Subject: [PATCH 17/23] Fix backslash key not mappable in controller settings (ryubing/ryujinx!265) See merge request ryubing/ryujinx!265 --- src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs index 48e49a7fa..4aa8692dd 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs @@ -141,7 +141,7 @@ namespace Ryujinx.Ava.Input AvaKey.OemComma, AvaKey.OemPeriod, AvaKey.OemQuestion, - AvaKey.OemBackslash, + AvaKey.OemPipe, // NOTE: invalid AvaKey.None From 012d1d6886dc462a36fb9e2ca22152154f805bf5 Mon Sep 17 00:00:00 2001 From: sh0inx Date: Sat, 21 Feb 2026 04:37:02 -0600 Subject: [PATCH 18/23] Fixed spelling in LocalesValidationTask.cs (ryubing/ryujinx!269) See merge request ryubing/ryujinx!269 --- src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs index a97e7b409..24a318be4 100644 --- a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs +++ b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs @@ -107,12 +107,12 @@ namespace Ryujinx.BuildValidationTasks { locale.Translations[langCode] = string.Empty; Console.WriteLine( - $"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it..."); + $"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it..."); } else { Console.WriteLine( - $"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!"); + $"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!"); } } From b70e2e44cb675e82ed75e02fee3619e778c62225 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Sat, 21 Feb 2026 12:45:00 +0100 Subject: [PATCH 19/23] NFC Mifare Manager (ryubing/ryujinx!270) See merge request ryubing/ryujinx!270 --- assets/Locales/Root.json | 50 ++ src/Ryujinx.HLE/HOS/Horizon.cs | 60 +++ .../HOS/Services/Nfc/Mifare/IUserManager.cs | 11 + .../Nfc/Mifare/MifareManager/IMifare.cs | 477 ++++++++++++++++++ .../Mifare/MifareManager/Types/NfcDevice.cs | 21 + .../MifareManager/Types/NfcMifareCommand.cs | 14 + .../Types/NfcMifareReadBlockData.cs | 13 + .../Types/NfcMifareReadBlockParameter.cs | 13 + .../Types/NfcMifareWriteBlockParameter.cs | 14 + .../Mifare/MifareManager/Types/NfcProtocol.cs | 11 + .../MifareManager/Types/NfcSectorKey.cs | 16 + .../Mifare/MifareManager/Types/NfcTagType.cs | 15 + .../MifareManager/Types/NfpDeviceState.cs | 13 + .../Nfc/Mifare/MifareManager/Types/State.cs | 8 + .../Nfc/Mifare/MifareManager/Types/TagInfo.cs | 16 + .../HOS/Services/Nfc/Mifare/ResultCode.cs | 17 + .../UI/ViewModels/MainWindowViewModel.cs | 73 +++ .../UI/Views/Main/MainMenuBarView.axaml | 16 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 14 + 19 files changed, 872 insertions(+) create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 9ecf9826c..3d23d4286 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -700,6 +700,56 @@ "zh_TW": "掃描 Amiibo" } }, + { + "ID": "MenuBarActionsScanSkylander", + "Translations": { + "ar_SA": "‫فحص Skylander", + "de_DE": "Skylander scannen", + "el_GR": "Σάρωση Skylander", + "en_US": "Scan A Skylander", + "es_ES": "Escanear Skylander", + "fr_FR": "Scanner un Skylander", + "he_IL": "סרוק אמיבו", + "it_IT": "Scansiona un Skylander", + "ja_JP": "Skylander をスキャン", + "ko_KR": "Skylander 스캔", + "no_NO": "Skann en Skylander", + "pl_PL": "Skanuj Skylander", + "pt_BR": "Escanear um Skylander", + "ru_RU": "Сканировать Skylander", + "sv_SE": "Skanna en Skylander", + "th_TH": "สแกนหา Skylander", + "tr_TR": "Bir Skylander Tara", + "uk_UA": "Сканувати Skylander", + "zh_CN": "扫描 Skylander", + "zh_TW": "掃描 Skylander" + } + }, + { + "ID": "MenuBarActionsRemoveSkylander", + "Translations": { + "ar_SA": "إزالة Skylander", + "de_DE": "Skylander entfernen", + "el_GR": "Αφαίρεση Skylander", + "en_US": "Remove Skylander", + "es_ES": "Eliminar Skylander", + "fr_FR": "Supprimer un Skylander", + "he_IL": "הסר Skylander", + "it_IT": "Rimuovi Skylander", + "ja_JP": "Skylander を削除", + "ko_KR": "Skylander 제거", + "no_NO": "Fjern Skylander", + "pl_PL": "Usuń Skylander", + "pt_BR": "Remover um Skylander", + "ru_RU": "Удалить Skylander", + "sv_SE": "Ta bort Skylander", + "th_TH": "ลบ Skylander", + "tr_TR": "Skylander'ı Kaldır", + "uk_UA": "Видалити Skylander", + "zh_CN": "移除 Skylander", + "zh_TW": "移除 Skylander" + } + }, { "ID": "MenuBarActionsScanAmiiboBin", "Translations": { diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 83aaa1f4d..38d504bbf 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -20,6 +20,7 @@ using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption; using Ryujinx.HLE.HOS.Services.Nfc.Nfp; using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager; using Ryujinx.HLE.HOS.Services.Nv; using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl; using Ryujinx.HLE.HOS.Services.Pcv.Bpc; @@ -66,6 +67,8 @@ namespace Ryujinx.HLE.HOS internal List NfpDevices { get; private set; } + internal List NfcDevices { get; private set; } + internal SmRegistry SmRegistry { get; private set; } internal ServerBase SmServer { get; private set; } @@ -132,6 +135,7 @@ namespace Ryujinx.HLE.HOS PerformanceState = new PerformanceState(); NfpDevices = []; + NfcDevices = []; // Note: This is not really correct, but with HLE of services, the only memory // region used that is used is Application, so we can use the other ones for anything. @@ -372,6 +376,15 @@ namespace Ryujinx.HLE.HOS } } + public void ScanSkylander(int nfcDeviceId, byte[] data) + { + if (NfcDevices[nfcDeviceId].State == NfcDeviceState.SearchingForTag) + { + NfcDevices[nfcDeviceId].State = NfcDeviceState.TagFound; + NfcDevices[nfcDeviceId].Data = data; + } + } + public bool SearchingForAmiibo(out int nfpDeviceId) { nfpDeviceId = default; @@ -389,6 +402,53 @@ namespace Ryujinx.HLE.HOS return false; } + public bool SearchingForSkylander(out int nfcDeviceId) + { + nfcDeviceId = default; + + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.SearchingForTag) + { + nfcDeviceId = i; + + return true; + } + } + + return false; + } + + public bool HasSkylander(out int nfcDeviceId) + { + nfcDeviceId = default; + + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.TagFound) + { + nfcDeviceId = i; + + return true; + } + } + + return false; + } + + public void RemoveSkylander() + { + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.TagFound) + { + NfcDevices[i].State = NfcDeviceState.Initialized; + NfcDevices[i].SignalDeactivate(); + Thread.Sleep(100); // NOTE: Simulate skylander scanning delay. + } + } + } + public void SignalDisplayResolutionChange() { DisplayResolutionChangeEvent.ReadableEvent.Signal(); diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs index 295c7e71a..53b6549c5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs @@ -1,8 +1,19 @@ +using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager; + namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare { [Service("nfc:mf:u")] class IUserManager : IpcService { public IUserManager(ServiceCtx context) { } + + [CommandCmif(0)] + // CreateUserInterface() -> object + public ResultCode CreateUserInterface(ServiceCtx context) + { + MakeObject(context, new IMifare()); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs new file mode 100644 index 000000000..43e28b5cf --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs @@ -0,0 +1,477 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Hid; +using Ryujinx.HLE.HOS.Services.Hid.HidServer; +using Ryujinx.Horizon.Common; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + class IMifare : IpcService + { + private State _state; + + private KEvent _availabilityChangeEvent; + + private CancellationTokenSource _cancelTokenSource; + + public IMifare() + { + _state = State.NonInitialized; + } + + [CommandCmif(0)] + public ResultCode Initialize(ServiceCtx context) + { + _state = State.Initialized; + + NfcDevice devicePlayer1 = new() + { + NpadIdType = NpadIdType.Player1, + Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1), + State = NfcDeviceState.Initialized, + }; + + context.Device.System.NfcDevices.Add(devicePlayer1); + + return ResultCode.Success; + } + + [CommandCmif(1)] + public ResultCode Finalize(ServiceCtx context) + { + if (_state == State.Initialized) + { + _cancelTokenSource?.Cancel(); + + // NOTE: All events are destroyed here. + context.Device.System.NfcDevices.Clear(); + + _state = State.NonInitialized; + } + + return ResultCode.Success; + } + + [CommandCmif(2)] + public ResultCode GetListDevices(ServiceCtx context) + { + if (context.Request.RecvListBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + ulong outputPosition = context.Request.RecvListBuff[0].Position; + ulong outputSize = context.Request.RecvListBuff[0].Size; + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + context.Memory.Write(outputPosition + ((uint)i * sizeof(long)), (uint)context.Device.System.NfcDevices[i].Handle); + } + + context.ResponseData.Write(context.Device.System.NfcDevices.Count); + + return ResultCode.Success; + } + + [CommandCmif(3)] + public ResultCode StartDetection(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + context.Device.System.NfcDevices[i].State = NfcDeviceState.SearchingForTag; + + break; + } + } + + _cancelTokenSource = new CancellationTokenSource(); + + Task.Run(() => + { + while (true) + { + if (_cancelTokenSource.Token.IsCancellationRequested) + { + break; + } + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound) + { + context.Device.System.NfcDevices[i].SignalActivate(); + Thread.Sleep(125); // NOTE: Simulate skylander scanning delay. + + break; + } + } + } + }, _cancelTokenSource.Token); + + return ResultCode.Success; + } + + [CommandCmif(4)] + public ResultCode StopDetection(ServiceCtx context) + { + _cancelTokenSource?.Cancel(); + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + context.Device.System.NfcDevices[i].State = NfcDeviceState.Initialized; + Array.Clear(context.Device.System.NfcDevices[i].Data); + context.Device.System.NfcDevices[i].SignalDeactivate(); + + break; + } + } + + return ResultCode.Success; + } + + [CommandCmif(5)] + public ResultCode ReadMifare(ServiceCtx context) + { + if (context.Request.ReceiveBuff.Count == 0 || context.Request.SendBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + ulong outputPosition = context.Request.ReceiveBuff[0].Position; + ulong outputSize = context.Request.ReceiveBuff[0].Size; + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + ulong inputPosition = context.Request.SendBuff[0].Position; + ulong inputSize = context.Request.SendBuff[0].Size; + + byte[] readBlockParameter = new byte[inputSize]; + + context.Memory.Read(inputPosition, readBlockParameter); + + var span = MemoryMarshal.Cast(readBlockParameter); + var list = new List(span.Length); + + foreach (var item in span) + list.Add(item); + + Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay. + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + return ResultCode.TagNotFound; + } + else + { + for (int p = 0; p < list.Count; p++) + { + NfcMifareReadBlockData blockData = new() + { + SectorNumber = list[p].SectorNumber, + Reserved = new Array7(), + }; + byte[] data = new byte[16]; + + switch (list[p].SectorKey.MifareCommand) + { + case NfcMifareCommand.NfcMifareCommand_Read: + case NfcMifareCommand.NfcMifareCommand_AuthA: + if (IsCurrentBlockKeyBlock(list[p].SectorNumber)) + { + Array.Copy(context.Device.System.NfcDevices[i].Data, (16 * list[p].SectorNumber) + 6, data, 6, 4); + } + else + { + Array.Copy(context.Device.System.NfcDevices[i].Data, 16 * list[p].SectorNumber, data, 0, 16); + } + data.CopyTo(blockData.Data.AsSpan()); + context.Memory.Write(outputPosition + ((uint)(p * Unsafe.SizeOf())), blockData); + break; + } + } + } + } + } + + return ResultCode.Success; + } + + [CommandCmif(6)] + public ResultCode WriteMifare(ServiceCtx context) + { + if (context.Request.SendBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + ulong inputPosition = context.Request.SendBuff[0].Position; + ulong inputSize = context.Request.SendBuff[0].Size; + + byte[] writeBlockParameter = new byte[inputSize]; + + context.Memory.Read(inputPosition, writeBlockParameter); + + var span = MemoryMarshal.Cast(writeBlockParameter); + var list = new List(span.Length); + + foreach (var item in span) + list.Add(item); + + Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay. + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + return ResultCode.TagNotFound; + } + else + { + for (int p = 0; p < list.Count; p++) + { + switch (list[p].SectorKey.MifareCommand) + { + case NfcMifareCommand.NfcMifareCommand_Write: + case NfcMifareCommand.NfcMifareCommand_AuthA: + list[p].Data.AsSpan().CopyTo(context.Device.System.NfcDevices[i].Data.AsSpan(list[p].SectorNumber * 16, 16)); + break; + } + } + } + } + } + + return ResultCode.Success; + } + + [CommandCmif(7)] + public ResultCode GetTagInfo(ServiceCtx context) + { + ResultCode resultCode = ResultCode.Success; + + if (context.Request.RecvListBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + ulong outputPosition = context.Request.RecvListBuff[0].Position; + + context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf()); + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf()); + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + resultCode = ResultCode.TagNotFound; + } + else + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagMounted || context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound) + { + TagInfo tagInfo = new() + { + UuidLength = 4, + Reserved1 = new Array21(), + Protocol = (uint)NfcProtocol.NfcProtocol_TypeA, // Type A Protocol + TagType = (uint)NfcTagType.NfcTagType_Mifare, // Mifare Type + Reserved2 = new Array6(), + }; + + byte[] uuid = new byte[4]; + + Array.Copy(context.Device.System.NfcDevices[i].Data, 0, uuid, 0, 4); + + uuid.CopyTo(tagInfo.Uuid.AsSpan()); + + context.Memory.Write(outputPosition, tagInfo); + + resultCode = ResultCode.Success; + } + else + { + resultCode = ResultCode.WrongDeviceState; + } + } + + break; + } + } + + return resultCode; + } + + [CommandCmif(8)] + public ResultCode AttachActivateEvent(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.Device.System.NfcDevices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].ActivateEvent.ReadableEvent, out int activateEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(activateEventHandle); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(9)] + public ResultCode AttachDeactivateEvent(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.Device.System.NfcDevices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].DeactivateEvent.ReadableEvent, out int deactivateEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(deactivateEventHandle); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(10)] + public ResultCode GetState(ServiceCtx context) + { + context.ResponseData.Write((int)_state); + + return ResultCode.Success; + } + + [CommandCmif(11)] + public ResultCode GetDeviceState(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + if (context.Device.System.NfcDevices[i].State > NfcDeviceState.Finalized) + { + throw new InvalidOperationException($"{nameof(context.Device.System.NfcDevices)} contains an invalid state for device {i}: {context.Device.System.NfcDevices[i].State}"); + } + context.ResponseData.Write((uint)context.Device.System.NfcDevices[i].State); + + return ResultCode.Success; + } + } + + context.ResponseData.Write((uint)NfcDeviceState.Unavailable); + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(12)] + public ResultCode GetNpadId(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(context.Device.System.NfcDevices[i].Handle)); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(13)] + public ResultCode AttachAvailabilityChangeEvent(ServiceCtx context) + { + _availabilityChangeEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out int availabilityChangeEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(availabilityChangeEventHandle); + + return ResultCode.Success; + } + + private bool IsCurrentBlockKeyBlock(byte block) + { + return ((block + 1) % 4) == 0; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs new file mode 100644 index 000000000..5d2b97fe9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs @@ -0,0 +1,21 @@ +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Hid; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + class NfcDevice + { + public KEvent ActivateEvent; + public KEvent DeactivateEvent; + + public void SignalActivate() => ActivateEvent.ReadableEvent.Signal(); + public void SignalDeactivate() => DeactivateEvent.ReadableEvent.Signal(); + + public NfcDeviceState State = NfcDeviceState.Unavailable; + + public PlayerIndex Handle; + public NpadIdType NpadIdType; + + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs new file mode 100644 index 000000000..0ab18c183 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcMifareCommand : byte + { + NfcMifareCommand_Read = 0x30, + NfcMifareCommand_AuthA = 0x60, + NfcMifareCommand_AuthB = 0x61, + NfcMifareCommand_Write = 0xA0, + NfcMifareCommand_Transfer = 0xB0, + NfcMifareCommand_Decrement = 0xC0, + NfcMifareCommand_Increment = 0xC1, + NfcMifareCommand_Store = 0xC2, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs new file mode 100644 index 000000000..bc5f1dcf3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + struct NfcMifareReadBlockData + { + public Array16 Data; + public byte SectorNumber; + public Array7 Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs new file mode 100644 index 000000000..df4ed6fce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + struct NfcMifareReadBlockParameter + { + public byte SectorNumber; + public Array7 Reserved; + public NfcSectorKey SectorKey; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs new file mode 100644 index 000000000..fcdbfab2d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs @@ -0,0 +1,14 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct NfcMifareWriteBlockParameter + { + public Array16 Data; + public byte SectorNumber; + public Array7 Reserved; + public NfcSectorKey SectorKey; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs new file mode 100644 index 000000000..d486ddade --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcProtocol : byte + { + NfcProtocol_None = 0b_0000_0000, + NfcProtocol_TypeA = 0b_0000_0001, ///< ISO14443A + NfcProtocol_TypeB = 0b_0000_0010, ///< ISO14443B + NfcProtocol_TypeF = 0b_0000_0100, ///< Sony FeliCa + NfcProtocol_All = 0xFF, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs new file mode 100644 index 000000000..bd7d51813 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct NfcSectorKey + { + public NfcMifareCommand MifareCommand; + public byte Unknown; + public Array6 Reserved1; + public Array6 SectorKey; + public Array2 Reserved2; + + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs new file mode 100644 index 000000000..b21c4c9d0 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcTagType : byte + { + NfcTagType_None = 0b_0000_0000, + NfcTagType_Type1 = 0b_0000_0001, ///< ISO14443A RW. Topaz + NfcTagType_Type2 = 0b_0000_0010, ///< ISO14443A RW. Ultralight, NTAGX, ST25TN + NfcTagType_Type3 = 0b_0000_0100, ///< ISO14443A RW/RO. Sony FeliCa + NfcTagType_Type4A = 0b_0000_1000, ///< ISO14443A RW/RO. DESFire + NfcTagType_Type4B = 0b_0001_0000, ///< ISO14443B RW/RO. DESFire + NfcTagType_Type5 = 0b_0010_0000, ///< ISO15693 RW/RO. SLI, SLIX, ST25TV + NfcTagType_Mifare = 0b_0100_0000, ///< Mifare clasic. Skylanders + NfcTagType_All = 0xFF, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs new file mode 100644 index 000000000..9e51b8d4d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcDeviceState : byte + { + Initialized = 0, + SearchingForTag = 1, + TagFound = 2, + TagRemoved = 3, + TagMounted = 4, + Unavailable = 5, + Finalized = 6, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs new file mode 100644 index 000000000..a9ee720e9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum State + { + NonInitialized, + Initialized, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs new file mode 100644 index 000000000..5db4612c9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x58)] + struct TagInfo + { + public Array10 Uuid; + public byte UuidLength; + public Array21 Reserved1; + public uint Protocol; + public uint TagType; + public Array6 Reserved2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs new file mode 100644 index 000000000..3148e02e4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare +{ + public enum ResultCode + { + ModuleId = 161, + ErrorCodeShift = 9, + + Success = 0, + + DeviceNotFound = (64 << ErrorCodeShift) | ModuleId, // 0x80A1 + WrongArgument = (65 << ErrorCodeShift) | ModuleId, // 0x82A1 + WrongDeviceState = (73 << ErrorCodeShift) | ModuleId, // 0x92A1 + NfcDisabled = (80 << ErrorCodeShift) | ModuleId, // 0xA0A1 + TagNotFound = (97 << ErrorCodeShift) | ModuleId, // 0xC2A1 + MifareAccessError = (288 << ErrorCodeShift) | ModuleId, // 0x240a1 + } +} diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 96159a1ea..48e18a12e 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -370,6 +370,39 @@ namespace Ryujinx.Ava.UI.ViewModels public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile; + public bool IsSkylanderRequested + { + get => field && _isGameRunning; + set + { + field = value; + + OnPropertyChanged(); + } + } + + public bool HasSkylander + { + get => field && _isGameRunning; + set + { + field = value; + + OnPropertyChanged(); + } + } + + public bool ShowSkylanderActions + { + get => field && _isGameRunning; + set + { + field = value; + + OnPropertyChanged(); + } + } + public bool ShowLoadProgress { get; @@ -1864,6 +1897,46 @@ namespace Ryujinx.Ava.UI.ViewModels } } } + public async Task OpenSkylanderWindow() + { + if (AppHost.Device.System.SearchingForSkylander(out int deviceId)) + { + Optional result = await StorageProvider.OpenSingleFilePickerAsync( + new FilePickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle], + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) + { + Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"], + }, + }, + }); + if (result.HasValue) + { + // Open reading stream from the first file. + await using var stream = await result.Value.OpenReadAsync(); + using var streamReader = new BinaryReader(stream); + // Reads all the content of file as a text. + byte[] data = new byte[1024]; + var count = streamReader.Read(data, 0, 1024); + if (count < 1024) + { + return; + } + else + { + AppHost.Device.System.ScanSkylander(deviceId, data); + } + } + } + } + + public async Task RemoveSkylander() + { + AppHost.Device.System.RemoveSkylander(); + } public void ReloadRenderDocApi() { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 13a5d4a40..d5a59c181 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -184,6 +184,22 @@ IsVisible="{Binding CanScanAmiiboBinaries}" InputGesture="Ctrl + B" IsEnabled="{Binding IsAmiiboBinRequested}" /> + + Date: Sat, 21 Feb 2026 20:10:22 -0600 Subject: [PATCH 20/23] Windows ARM (win-arm64) build now launches with trimming (ryubing/ryujinx!277) See merge request ryubing/ryujinx!277 --- Directory.Packages.props | 3 +- src/Ryujinx.Common/Ryujinx.Common.csproj | 1 - src/Ryujinx/Ryujinx.csproj | 5 -- .../Utilities/SystemInfo/WindowsSystemInfo.cs | 51 +++++-------------- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fd61602a8..7f437a93a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -56,7 +56,6 @@ - - \ No newline at end of file + diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index e31d2f3bc..60a75a7b2 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 5da152501..715460274 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -28,11 +28,6 @@ true partial - - - true - false -