From e9e1680d02635df4b70898e88cff704db8125014 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:43:08 +0300 Subject: [PATCH] [Audio] Snapshot AudioOut2 PCM when the guest submits it Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --- src/SharpEmu.Libs/Audio/AudioOut2Exports.cs | 129 +++++++++++++++++--- 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index de77a4b..f7b4599 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -212,7 +212,7 @@ public static class AudioOut2Exports public uint Ambisonics = AmbisonicsNone; public uint Passthrough; - public int PcmPending; + public byte[]? PcmData; } @@ -622,14 +622,38 @@ public static class AudioOut2Exports continue; } - port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm); - Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0); + var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm); + byte[]? pcmData = null; + if (pcmAddress != 0 && + TryDecodeDataFormat(port.DataFormat, out var channels, out var bytesPerSample, out _) && + IsSnapshotPcmPort(port.PortType, channels)) + { + int byteLength; + try + { + byteLength = checked((int)port.GrainSamples * channels * bytesPerSample); + } + catch (OverflowException) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + pcmData = new byte[byteLength]; + if (!ctx.Memory.TryRead(pcmAddress, pcmData)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + } + + port.PcmAddress = pcmAddress; + Volatile.Write(ref port.PcmData, pcmData); var n = Interlocked.Increment(ref _attributePcmTraceCount); if (n <= 8 || n % 500 == 0) { TraceAudioOut2( $"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " + - $"format=0x{port.DataFormat:X} grains={port.GrainSamples}"); + $"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " + + $"grains={port.GrainSamples}"); } } @@ -1155,7 +1179,6 @@ public static class AudioOut2Exports lock (HostSubmitGate) { var mix = ArrayPool.Shared.Rent(frames * 2); - var source = ArrayPool.Shared.Rent(frames * 16 * sizeof(float)); var output = ArrayPool.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize); try { @@ -1164,29 +1187,42 @@ public static class AudioOut2Exports foreach (var port in Ports.Values) { if (port.ContextHandle != context.Handle || - port.PcmAddress == 0 || - Interlocked.Exchange(ref port.PcmPending, 0) == 0 || - !TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat) || - !TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough, - out var leftGain, out var rightGain)) + !TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat)) + { + continue; + } + + var pcmData = TakePortPcmSnapshot(port); + if (pcmData is null) + { + continue; + } + + if (!TryGetStereoGains( + port.PortType, + ch, + port.Gain, + port.Ambisonics, + port.Passthrough, + out var leftGain, + out var rightGain)) { continue; } var byteLength = checked(frames * ch * bps); - if (byteLength <= 0 || byteLength > source.Length) + if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float))) { continue; } - var sourceSpan = source.AsSpan(0, byteLength); - if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan)) + if (pcmData.Length < byteLength) { continue; } MixPortIntoStereo( - sourceSpan, + pcmData.AsSpan(0, byteLength), mix.AsSpan(0, frames * 2), frames, ch, @@ -1238,12 +1274,70 @@ public static class AudioOut2Exports finally { ArrayPool.Shared.Return(mix); - ArrayPool.Shared.Return(source); ArrayPool.Shared.Return(output); } } } + internal static byte[]? GetPortPcmSnapshotForTests(ulong portHandle) + { + if (!Ports.TryGetValue(portHandle, out var port)) + { + return null; + } + + var snapshot = Volatile.Read(ref port.PcmData); + return snapshot?.ToArray(); + } + + internal static bool HasPortForTests(ulong portHandle) => Ports.ContainsKey(portHandle); + + internal static byte[]? TakePortPcmSnapshotForTests(ulong portHandle) + { + return Ports.TryGetValue(portHandle, out var port) + ? TakePortPcmSnapshot(port) + : null; + } + + private static byte[]? TakePortPcmSnapshot(PortState port) => + Interlocked.Exchange(ref port.PcmData, null); + + internal static void SetStreamFactoryForTests(Func? streamFactory) => + Volatile.Write(ref _streamFactoryForTests, streamFactory); + + internal static void ResetForTests() + { + Contexts.Clear(); + Ports.Clear(); + SpeakerArrays.Clear(); + Interlocked.Exchange(ref _nextContextHandle, 1); + Interlocked.Exchange(ref _nextUserHandle, 1); + Interlocked.Exchange(ref _nextPortId, 0); + Interlocked.Exchange(ref _pushTraceCount, 0); + Interlocked.Exchange(ref _submitTraceCount, 0); + Interlocked.Exchange(ref _submitSkipTraceCount, 0); + Interlocked.Exchange(ref _attributePcmTraceCount, 0); + + lock (HostBackendGate) + { + PrimaryBackend?.Dispose(); + SecondaryBackend?.Dispose(); + PrimaryBackend = null; + SecondaryBackend = null; + PrimaryBackendName = "none"; + SecondaryBackendName = "none"; + PrimaryContextHandle = 0; + } + + Volatile.Write(ref _streamFactoryForTests, null); + } + + private static bool IsMainOrBgmPort(ushort portType) + { + var kind = portType & 0xFF; + return kind is 0 or 1; + } + internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2; internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) => @@ -1298,6 +1392,11 @@ public static class AudioOut2Exports return false; } + private static bool IsSnapshotPcmPort(ushort portType, int channels) => + RoutesToSpeakers(portType) && + (portType & 0xFF) != AudioOut2PortTypeVibration && + (!IsObjectPort(portType) || channels == 1); + internal static void MixPortIntoStereo( ReadOnlySpan source, Span mix,