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/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": "", 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/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/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..2e3b97517 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs @@ -0,0 +1,228 @@ +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 sealed 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 + { + AudioStreamBasicDescription format = + GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6); + + int result = AudioQueueNewOutput( + ref format, + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero, + 0, + out nint 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() + { + 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 ManualResetEvent GetUpdateRequiredEvent() + => _updateRequiredEvent; + + public ManualResetEvent GetPauseEvent() + => _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) + => _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); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + foreach (AppleHardwareDeviceSession session in _sessions.Keys) + { + session.Dispose(); + } + + _pauseEvent.Dispose(); + } + } + + public bool SupportsDirection(Direction direction) + => direction != Direction.Input; + + public bool SupportsSampleRate(uint sampleRate) => true; + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + => sampleFormat != SampleFormat.PcmInt24; + + public bool SupportsChannelCount(uint channelCount) + => channelCount != 6 || _supportSurroundConfiguration; + } +} diff --git a/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs new file mode 100644 index 000000000..1606e9954 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs @@ -0,0 +1,285 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.Versioning; +using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox; + +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 nint _audioQueue; + private readonly nint[] _audioQueueBuffers = new nint[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( + nint userData, + nint audioQueue, + nint 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) + { + AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat( + RequestedSampleFormat, + RequestedSampleRate, + RequestedChannelCount); + + nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate); + nint userData = GCHandle.ToIntPtr(_gcHandle); + + int result = AudioQueueNewOutput( + ref format, + callbackPtr, + userData, + nint.Zero, + nint.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(nint 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, nint.Zero); + } + + private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr) + { + if (!_started || bufferPtr == nint.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(nint 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.DataPointer, GetSampleCount(buffer))); + } + + public override void Start() + { + lock (_lock) + { + if (_started) + return; + + _started = true; + AudioQueueStart(_audioQueue, nint.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.DataPointer; + } + + public override void PrepareToClose() { } + public override void UnregisterBuffer(AudioBuffer buffer) { } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Stop(); + + if (_audioQueue != nint.Zero) + { + AudioQueueStop(_audioQueue, true); + AudioQueueDispose(_audioQueue, true); + _audioQueue = nint.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..ea2a7867a --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs @@ -0,0 +1,102 @@ +using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming + +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; + } +} 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..c27fdee5b --- /dev/null +++ b/src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + true + + + + + + + + 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/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.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)) { 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); 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/Systems/Updater/Updater.GitLab.cs b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs index deb515797..73fe9f66b 100644 --- a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs +++ b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs @@ -7,6 +7,7 @@ using Ryujinx.Common.Logging; using Ryujinx.Systems.Update.Client; using Ryujinx.Systems.Update.Common; using System; +using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -46,6 +47,12 @@ namespace Ryujinx.Ava.Systems return Return.Failure( new MessageError("DNS resolution error occurred. Is your internet down?")); } + catch (HttpRequestException hre) + when (hre.StatusCode is HttpStatusCode.BadGateway) + { + return Return.Failure( + new MessageError("Could not connect to the update server, but it appears like you have internet. It seems like the update server is offline, try again later.")); + } } public static async Task> CheckVersionAsync(bool showVersionUpToDate = false) 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 @@ +