mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
feat(audio): implement sceAudioOutOutputs (#605)
* feat(audio): implement batched output submission * test(audio): cover batched output semantics * test(audio): cover multi-port output batches --------- Co-authored-by: diego <diego@DIGOTE-PC>
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.HLE.Host;
|
using SharpEmu.HLE.Host;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
@@ -11,8 +12,17 @@ namespace SharpEmu.Libs.Audio;
|
|||||||
|
|
||||||
public static class AudioOutExports
|
public static class AudioOutExports
|
||||||
{
|
{
|
||||||
|
private const int AudioOutOutputParamSize = 16;
|
||||||
|
private const int AudioOutMaximumOutputCount = 25;
|
||||||
|
|
||||||
|
internal const int AudioOutErrorInvalidPort = unchecked((int)0x80260003);
|
||||||
|
internal const int AudioOutErrorInvalidPointer = unchecked((int)0x80260004);
|
||||||
|
internal const int AudioOutErrorPortFull = unchecked((int)0x80260005);
|
||||||
|
internal const int AudioOutErrorInvalidSize = unchecked((int)0x80260006);
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||||
private static int _nextPortHandle;
|
private static int _nextPortHandle;
|
||||||
|
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||||
|
|
||||||
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
||||||
// guest submits real samples or silence. Gated so it costs nothing when off.
|
// guest submits real samples or silence. Gated so it costs nothing when off.
|
||||||
@@ -56,6 +66,7 @@ public static class AudioOutExports
|
|||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
public IHostAudioStream? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
|
public object SubmissionGate { get; } = new();
|
||||||
public volatile float Volume = 1.0f;
|
public volatile float Volume = 1.0f;
|
||||||
public int BufferByteLength =>
|
public int BufferByteLength =>
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
@@ -83,7 +94,24 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => Backend?.Dispose();
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (SubmissionGate)
|
||||||
|
{
|
||||||
|
Backend?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct OutputDescriptor(int Handle, ulong SourceAddress);
|
||||||
|
|
||||||
|
private struct ResolvedOutput
|
||||||
|
{
|
||||||
|
public int Handle;
|
||||||
|
public ulong SourceAddress;
|
||||||
|
public PortState Port;
|
||||||
|
public byte[]? HostBuffer;
|
||||||
|
public int HostBufferLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -114,11 +142,20 @@ public static class AudioOutExports
|
|||||||
IHostAudioStream? backend = null;
|
IHostAudioStream? backend = null;
|
||||||
string backendName;
|
string backendName;
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||||
|
if (streamFactory is not null)
|
||||||
|
{
|
||||||
|
backend = streamFactory(frequency);
|
||||||
|
backendName = "test";
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var audio = HostPlatform.Current.Audio;
|
var audio = HostPlatform.Current.Audio;
|
||||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||||
backendName = audio.BackendName;
|
backendName = audio.BackendName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
backendName = "silent";
|
backendName = "silent";
|
||||||
@@ -192,6 +229,46 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w3PdaSTSwGE",
|
||||||
|
ExportName = "sceAudioOutOutputs",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut")]
|
||||||
|
public static int AudioOutOutputs(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outputCount = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
if (outputCount == 0 || outputCount > AudioOutMaximumOutputCount)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(AudioOutErrorPortFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = checked((int)outputCount);
|
||||||
|
Span<byte> parameterBytes =
|
||||||
|
stackalloc byte[AudioOutMaximumOutputCount * AudioOutOutputParamSize];
|
||||||
|
parameterBytes = parameterBytes[..checked(count * AudioOutOutputParamSize)];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, parameterBytes))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<OutputDescriptor> descriptors = stackalloc OutputDescriptor[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var entry = parameterBytes.Slice(i * AudioOutOutputParamSize, AudioOutOutputParamSize);
|
||||||
|
descriptors[i] = new OutputDescriptor(
|
||||||
|
BinaryPrimitives.ReadInt32LittleEndian(entry),
|
||||||
|
BinaryPrimitives.ReadUInt64LittleEndian(entry[8..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.SetReturn(SubmitOutputs(ctx, descriptors));
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QOQtbeDqsT4",
|
Nid = "QOQtbeDqsT4",
|
||||||
ExportName = "sceAudioOutOutput",
|
ExportName = "sceAudioOutOutput",
|
||||||
@@ -225,16 +302,7 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceOutput)
|
TraceOutput(handle, port, source);
|
||||||
{
|
|
||||||
var n = Interlocked.Increment(ref _outputCount);
|
|
||||||
if (n <= 8 || n % 200 == 0)
|
|
||||||
{
|
|
||||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port.Backend is null)
|
if (port.Backend is null)
|
||||||
{
|
{
|
||||||
@@ -272,6 +340,184 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SubmitOutputs(CpuContext ctx, ReadOnlySpan<OutputDescriptor> descriptors)
|
||||||
|
{
|
||||||
|
var resolvedArray = ArrayPool<ResolvedOutput>.Shared.Rent(descriptors.Length);
|
||||||
|
var resolved = resolvedArray.AsSpan(0, descriptors.Length);
|
||||||
|
resolved.Clear();
|
||||||
|
|
||||||
|
Span<int> lockOrder = stackalloc int[descriptors.Length];
|
||||||
|
var acquiredLocks = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint bufferLength = 0;
|
||||||
|
for (var i = 0; i < descriptors.Length; i++)
|
||||||
|
{
|
||||||
|
var descriptor = descriptors[i];
|
||||||
|
for (var previous = 0; previous < i; previous++)
|
||||||
|
{
|
||||||
|
if (resolved[previous].Handle == descriptor.Handle)
|
||||||
|
{
|
||||||
|
return AudioOutErrorInvalidPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Ports.TryGetValue(descriptor.Handle, out var port))
|
||||||
|
{
|
||||||
|
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
bufferLength = port.BufferLength;
|
||||||
|
}
|
||||||
|
else if (port.BufferLength != bufferLength)
|
||||||
|
{
|
||||||
|
return AudioOutErrorInvalidSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved[i].Handle = descriptor.Handle;
|
||||||
|
resolved[i].SourceAddress = descriptor.SourceAddress;
|
||||||
|
resolved[i].Port = port;
|
||||||
|
lockOrder[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every batch takes port locks in handle order. Two guest threads can
|
||||||
|
// submit overlapping batches in a different descriptor order without
|
||||||
|
// deadlocking each other.
|
||||||
|
for (var i = 1; i < lockOrder.Length; i++)
|
||||||
|
{
|
||||||
|
var index = lockOrder[i];
|
||||||
|
var position = i;
|
||||||
|
while (position > 0 &&
|
||||||
|
resolved[lockOrder[position - 1]].Handle > resolved[index].Handle)
|
||||||
|
{
|
||||||
|
lockOrder[position] = lockOrder[position - 1];
|
||||||
|
position--;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockOrder[position] = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; acquiredLocks < lockOrder.Length; acquiredLocks++)
|
||||||
|
{
|
||||||
|
Monitor.Enter(resolved[lockOrder[acquiredLocks]].Port.SubmissionGate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioOutClose removes the handle before waiting for SubmissionGate.
|
||||||
|
// Recheck after acquiring all gates so a close racing this batch cannot
|
||||||
|
// turn a validated submission into a write to a disposed backend.
|
||||||
|
for (var i = 0; i < resolved.Length; i++)
|
||||||
|
{
|
||||||
|
if (!Ports.TryGetValue(resolved[i].Handle, out var current) ||
|
||||||
|
!ReferenceEquals(current, resolved[i].Port))
|
||||||
|
{
|
||||||
|
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage every guest buffer before the first host submission. A bad
|
||||||
|
// pointer in a later descriptor therefore cannot partially enqueue the
|
||||||
|
// earlier ports.
|
||||||
|
for (var i = 0; i < resolved.Length; i++)
|
||||||
|
{
|
||||||
|
ref var output = ref resolved[i];
|
||||||
|
if (output.SourceAddress == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceBuffer = ArrayPool<byte>.Shared.Rent(output.Port.BufferByteLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var source = sourceBuffer.AsSpan(0, output.Port.BufferByteLength);
|
||||||
|
if (!ctx.Memory.TryRead(output.SourceAddress, source))
|
||||||
|
{
|
||||||
|
return AudioOutErrorInvalidPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceOutput(output.Handle, output.Port, source);
|
||||||
|
|
||||||
|
output.HostBufferLength = checked(
|
||||||
|
(int)output.Port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||||
|
output.HostBuffer = ArrayPool<byte>.Shared.Rent(output.HostBufferLength);
|
||||||
|
AudioPcmConversion.ConvertToStereoPcm16(
|
||||||
|
source,
|
||||||
|
output.HostBuffer.AsSpan(0, output.HostBufferLength),
|
||||||
|
checked((int)output.Port.BufferLength),
|
||||||
|
output.Port.Channels,
|
||||||
|
output.Port.BytesPerSample,
|
||||||
|
output.Port.IsFloat,
|
||||||
|
output.Port.Volume);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(sourceBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PortState? pacingPort = null;
|
||||||
|
for (var i = 0; i < resolved.Length; i++)
|
||||||
|
{
|
||||||
|
ref var output = ref resolved[i];
|
||||||
|
if (output.HostBuffer is null ||
|
||||||
|
output.Port.Backend is null ||
|
||||||
|
!output.Port.Backend.Submit(
|
||||||
|
output.HostBuffer.AsSpan(0, output.HostBufferLength)))
|
||||||
|
{
|
||||||
|
if (pacingPort is null ||
|
||||||
|
HasLongerBufferDuration(output.Port, pacingPort))
|
||||||
|
{
|
||||||
|
pacingPort = output.Port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A batch is one guest scheduling point. When one or more ports have
|
||||||
|
// no usable backend, pace once using the longest affected buffer rather
|
||||||
|
// than sleeping once per port.
|
||||||
|
pacingPort?.PaceSilence();
|
||||||
|
return checked((int)resolved[0].Port.BufferLength);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
for (var i = acquiredLocks - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Monitor.Exit(resolved[lockOrder[i]].Port.SubmissionGate);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < resolved.Length; i++)
|
||||||
|
{
|
||||||
|
if (resolved[i].HostBuffer is { } hostBuffer)
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(hostBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayPool<ResolvedOutput>.Shared.Return(resolvedArray, clearArray: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasLongerBufferDuration(PortState candidate, PortState current) =>
|
||||||
|
(ulong)candidate.BufferLength * current.Frequency >
|
||||||
|
(ulong)current.BufferLength * candidate.Frequency;
|
||||||
|
|
||||||
|
private static void TraceOutput(int handle, PortState port, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
if (!_traceOutput)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var n = Interlocked.Increment(ref _outputCount);
|
||||||
|
if (n <= 8 || n % 200 == 0)
|
||||||
|
{
|
||||||
|
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "b+uAV89IlxE",
|
Nid = "b+uAV89IlxE",
|
||||||
ExportName = "sceAudioOutSetVolume",
|
ExportName = "sceAudioOutSetVolume",
|
||||||
@@ -362,6 +608,25 @@ public static class AudioOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||||
|
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||||
|
|
||||||
|
internal static void ResetForTests()
|
||||||
|
{
|
||||||
|
foreach (var handle in Ports.Keys)
|
||||||
|
{
|
||||||
|
if (Ports.TryRemove(handle, out var port))
|
||||||
|
{
|
||||||
|
port.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextPortHandle = 0;
|
||||||
|
_outputCount = 0;
|
||||||
|
Volatile.Write(ref _shutdown, false);
|
||||||
|
Volatile.Write(ref _streamFactoryForTests, null);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool _shutdown;
|
private static bool _shutdown;
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
private static bool TryGetFormat(
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.Libs.Audio;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Audio;
|
||||||
|
|
||||||
|
[CollectionDefinition("AudioOutState", DisableParallelization = true)]
|
||||||
|
public sealed class AudioOutStateCollection
|
||||||
|
{
|
||||||
|
public const string Name = "AudioOutState";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection(AudioOutStateCollection.Name)]
|
||||||
|
public sealed class AudioOutExportsTests : IDisposable
|
||||||
|
{
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private const int MemorySize = 0x4000;
|
||||||
|
private const ulong ParameterAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong FirstSourceAddress = MemoryBase + 0x1000;
|
||||||
|
private const ulong SecondSourceAddress = MemoryBase + 0x2000;
|
||||||
|
|
||||||
|
private readonly FakeCpuMemory _memory = new(MemoryBase, MemorySize);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
private readonly List<RecordingAudioStream> _streams = [];
|
||||||
|
|
||||||
|
public AudioOutExportsTests()
|
||||||
|
{
|
||||||
|
AudioOutExports.ResetForTests();
|
||||||
|
AudioOutExports.SetStreamFactoryForTests(_ =>
|
||||||
|
{
|
||||||
|
var stream = new RecordingAudioStream();
|
||||||
|
_streams.Add(stream);
|
||||||
|
return stream;
|
||||||
|
});
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_StagesAndSubmitsSingleStereoPort()
|
||||||
|
{
|
||||||
|
var handle = OpenPort(bufferLength: 2);
|
||||||
|
Span<byte> source = stackalloc byte[8];
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(source, -32768);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(source[2..], 32767);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(source[4..], -1234);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(source[6..], 5678);
|
||||||
|
Assert.True(_memory.TryWrite(FirstSourceAddress, source));
|
||||||
|
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 1);
|
||||||
|
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
Assert.Equal(2UL, _ctx[CpuRegister.Rax]);
|
||||||
|
Assert.Equal(source.ToArray(), Assert.Single(_streams[0].Submissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_SubmitsEveryPortInTheBatch()
|
||||||
|
{
|
||||||
|
var firstHandle = OpenPort(bufferLength: 2);
|
||||||
|
var secondHandle = OpenPort(bufferLength: 2);
|
||||||
|
byte[] firstSource = [1, 0, 2, 0, 3, 0, 4, 0];
|
||||||
|
byte[] secondSource = [5, 0, 6, 0, 7, 0, 8, 0];
|
||||||
|
Assert.True(_memory.TryWrite(FirstSourceAddress, firstSource));
|
||||||
|
Assert.True(_memory.TryWrite(SecondSourceAddress, secondSource));
|
||||||
|
|
||||||
|
// Reverse handle order to exercise canonical lock ordering without
|
||||||
|
// changing which guest buffer belongs to each port.
|
||||||
|
WriteDescriptor(0, secondHandle, FirstSourceAddress);
|
||||||
|
WriteDescriptor(1, firstHandle, SecondSourceAddress);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 2);
|
||||||
|
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
Assert.Equal(secondSource, Assert.Single(_streams[0].Submissions));
|
||||||
|
Assert.Equal(firstSource, Assert.Single(_streams[1].Submissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_AcceptsNullBufferAsSynchronizationOnly()
|
||||||
|
{
|
||||||
|
var handle = OpenPort(bufferLength: 2);
|
||||||
|
WriteDescriptor(0, handle, sourceAddress: 0);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 1);
|
||||||
|
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
Assert.Empty(_streams[0].Submissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_FaultInLaterBufferDoesNotPartiallySubmit()
|
||||||
|
{
|
||||||
|
var firstHandle = OpenPort(bufferLength: 2);
|
||||||
|
var secondHandle = OpenPort(bufferLength: 2);
|
||||||
|
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||||
|
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||||
|
WriteDescriptor(1, secondHandle, MemoryBase + MemorySize);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 2);
|
||||||
|
|
||||||
|
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPointer, result);
|
||||||
|
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_RejectsDuplicateHandlesBeforeSubmission()
|
||||||
|
{
|
||||||
|
var handle = OpenPort(bufferLength: 2);
|
||||||
|
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||||
|
Assert.True(_memory.TryWrite(SecondSourceAddress, new byte[8]));
|
||||||
|
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||||
|
WriteDescriptor(1, handle, SecondSourceAddress);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 2);
|
||||||
|
|
||||||
|
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPort, result);
|
||||||
|
Assert.Empty(_streams[0].Submissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_RejectsPortsWithDifferentBufferLengths()
|
||||||
|
{
|
||||||
|
var firstHandle = OpenPort(bufferLength: 2);
|
||||||
|
var secondHandle = OpenPort(bufferLength: 4);
|
||||||
|
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||||
|
WriteDescriptor(1, secondHandle, SecondSourceAddress);
|
||||||
|
|
||||||
|
var result = Submit(outputCount: 2);
|
||||||
|
|
||||||
|
Assert.Equal(AudioOutExports.AudioOutErrorInvalidSize, result);
|
||||||
|
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_RejectsUnknownHandle()
|
||||||
|
{
|
||||||
|
WriteDescriptor(0, 99, FirstSourceAddress);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
AudioOutExports.AudioOutErrorInvalidPort,
|
||||||
|
Submit(outputCount: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0u)]
|
||||||
|
[InlineData(26u)]
|
||||||
|
public void Outputs_RejectsInvalidOutputCount(uint outputCount)
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
AudioOutExports.AudioOutErrorPortFull,
|
||||||
|
Submit(outputCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Outputs_RejectsNullOrUnreadableParameterArray()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||||
|
Submit(outputCount: 1, parameterAddress: 0));
|
||||||
|
Assert.Equal(
|
||||||
|
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||||
|
Submit(outputCount: 1, parameterAddress: MemoryBase + MemorySize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OutputsExportRegistersForBothGenerations()
|
||||||
|
{
|
||||||
|
foreach (var generation in new[] { Generation.Gen4, Generation.Gen5 })
|
||||||
|
{
|
||||||
|
var manager = new ModuleManager();
|
||||||
|
manager.RegisterExports(
|
||||||
|
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation));
|
||||||
|
|
||||||
|
Assert.True(manager.TryGetExport("w3PdaSTSwGE", out var export));
|
||||||
|
Assert.Equal("sceAudioOutOutputs", export.Name);
|
||||||
|
Assert.Equal("libSceAudioOut", export.LibraryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => AudioOutExports.ResetForTests();
|
||||||
|
|
||||||
|
private int OpenPort(uint bufferLength)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = 1;
|
||||||
|
_ctx[CpuRegister.Rsi] = 0;
|
||||||
|
_ctx[CpuRegister.Rdx] = 0;
|
||||||
|
_ctx[CpuRegister.Rcx] = bufferLength;
|
||||||
|
_ctx[CpuRegister.R8] = 48000;
|
||||||
|
_ctx[CpuRegister.R9] = 1;
|
||||||
|
return AudioOutExports.AudioOutOpen(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Submit(uint outputCount, ulong parameterAddress = ParameterAddress)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = parameterAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = outputCount;
|
||||||
|
return AudioOutExports.AudioOutOutputs(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteDescriptor(int index, int handle, ulong sourceAddress)
|
||||||
|
{
|
||||||
|
Span<byte> descriptor = stackalloc byte[16];
|
||||||
|
descriptor.Clear();
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(descriptor, handle);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(descriptor[8..], sourceAddress);
|
||||||
|
Assert.True(_memory.TryWrite(
|
||||||
|
ParameterAddress + unchecked((ulong)(index * descriptor.Length)),
|
||||||
|
descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingAudioStream : IHostAudioStream
|
||||||
|
{
|
||||||
|
public List<byte[]> Submissions { get; } = [];
|
||||||
|
|
||||||
|
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||||
|
{
|
||||||
|
Submissions.Add(stereoPcm16.ToArray());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user