1679 lines
65 KiB
C#
1679 lines
65 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.HLE.Host;
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
|
|
namespace SharpEmu.Libs.Audio;
|
|
|
|
public static class AudioOut2Exports
|
|
{
|
|
// Every size below is the x86-64 sizeof() of the matching libSceAudioOut2
|
|
// struct. Out-parameter sizes are never inferred from scratch registers or
|
|
// from the host address range of the pointer: the ABI names exactly one out
|
|
// pointer per argument slot, and a title is free to place it on its stack
|
|
// right next to a canary or another local.
|
|
|
|
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
|
|
private const int AudioOut2ContextParamSize = 0x40;
|
|
private const int AudioOut2ContextMemorySize = 0x4000;
|
|
private const int AudioOut2ContextMemoryAlignment = 0x100;
|
|
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
|
|
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
|
|
private const int SpeakerArrayHeaderSize = 0x40;
|
|
private const int SpeakerArrayEntrySize = 0x100;
|
|
// Extra scratch the title writes after the per-channel entries (coefficients).
|
|
private const int SpeakerArrayScratchBytes = 0x400;
|
|
private const uint SpeakerArrayDefaultChannels = 8;
|
|
// The ABI caps a speaker array at 32 speakers.
|
|
private const uint SpeakerArrayMaxChannels = 32;
|
|
// Field read by GTA at object+0x34 (see AV at eboot+0xB07D: mov eax,[rbx+0x34]).
|
|
private const int SpeakerArrayDivisorFieldOffset = 0x34;
|
|
private const int SpeakerArrayResultFieldOffset = 0x3C;
|
|
private const uint SpeakerArrayDefaultDivisor = 1;
|
|
private const int SpeakerArrayCoefficientBytes = 0x400;
|
|
|
|
private static readonly string _stackOutBufferModes =
|
|
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_OUT2_STACK_WRITES") ?? "1";
|
|
|
|
private static bool AllowStackOut(string which) =>
|
|
string.Equals(_stackOutBufferModes, "1", StringComparison.Ordinal) ||
|
|
_stackOutBufferModes
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Contains(which, StringComparer.OrdinalIgnoreCase);
|
|
|
|
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
|
|
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
|
|
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
|
|
private const int SpeakerArrayParamSize = 0x40;
|
|
private const int SpeakerArrayParamNumSpeakersOffset = 0x08;
|
|
private const int SpeakerArrayParamBufferOffset = 0x10;
|
|
private const int SpeakerArrayParamSizeOffset = 0x18;
|
|
// SceAudioOut2PortState: u16 output, u8 numChannels, u8 pad1, s16 volume,
|
|
// u16 rerouteCounter, u32 flags, u32 pad2, u64 reserved[6].
|
|
private const int PortStateSize = 0x40;
|
|
// SceAudioOut2SystemState: float loudness, u32 pad, u64 reserved[7].
|
|
private const int SystemStateSize = 0x40;
|
|
// SceAudioOut2SpeakerInfo: u8 type + 3 pad, u32 availableBits, u32 flags,
|
|
// u32 pad, SceAudioOut2SpeakerAngle aSpeakerAngle[SCE_AUDIO_OUT2_SPEAKER_MAX].
|
|
private const int SpeakerInfoSize = 0x50;
|
|
private const int SpeakerInfoAngleOffset = 0x10;
|
|
private const int SpeakerMax = 16;
|
|
// SceAudioOut2PortParam: u16 portType, u16 pad, u32 dataFormat,
|
|
// u32 samplingFreq, u32 flags, SceAudioOut2UserHandle userHandle (0x10),
|
|
// u32 reserved[10].
|
|
private const int PortParamSize = 0x40;
|
|
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
|
|
private const int AttributeEntrySize = 0x18;
|
|
private const uint PortAttributeIdPcm = 0;
|
|
private const uint PortAttributeIdGain = 1;
|
|
private const uint PortAttributeIdPassthrough = 5;
|
|
private const uint PortAttributeIdAmbisonics = 8;
|
|
internal const uint AmbisonicsNone = uint.MaxValue;
|
|
internal const uint AmbisonicsAcnBase = 64;
|
|
internal const uint PassthroughLeft = 1;
|
|
internal const uint PassthroughRight = 2;
|
|
private const float HalfPower = 0.70710677f;
|
|
private const ushort PortStateOutputConnectedPrimary = 0x01;
|
|
|
|
// A host device that already holds this much audio is pacing the context; anything beyond the
|
|
// cushion counts as queued grains. The cushion stays below the emulated queue so host
|
|
// scheduling jitter does not turn into a dropout.
|
|
private const int DeviceCushionMilliseconds = 40;
|
|
private const byte SpeakerTypeTv = 0;
|
|
|
|
// libSceAudioOut2 has its own error space (0x80268xxx);
|
|
// the generic ORBIS_GEN2_* codes are not what a title compares against.
|
|
private const int AudioOut2ErrorInvalidParam = unchecked((int)0x8026_8001);
|
|
private const int AudioOut2ErrorOutOfMemory = unchecked((int)0x8026_8003);
|
|
private const int AudioOut2ErrorInvalidPointer = unchecked((int)0x8026_800C);
|
|
private const int AudioOut2ErrorInvalidUser = unchecked((int)0x8026_8010);
|
|
private static long _nextContextHandle = 1;
|
|
private static long _nextUserHandle = 1;
|
|
private static int _nextPortId;
|
|
private static long _pushTraceCount;
|
|
private static long _submitTraceCount;
|
|
private static long _submitSkipTraceCount;
|
|
private static long _attributePcmTraceCount;
|
|
|
|
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
|
|
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
|
|
private static readonly ConcurrentDictionary<ulong, PortState> Ports = new();
|
|
|
|
private sealed class ContextState
|
|
{
|
|
private readonly object _paceGate = new();
|
|
private long _nextAdvanceTimestamp;
|
|
|
|
public ContextState(ulong handle, uint frequency, uint grainSamples, uint queueDepth, IHostAudioStream? backend)
|
|
{
|
|
Handle = handle;
|
|
Frequency = frequency == 0 ? 48000 : frequency;
|
|
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
|
|
QueueDepth = queueDepth == 0 ? 4 : queueDepth;
|
|
Backend = backend;
|
|
}
|
|
|
|
public ulong Handle { get; }
|
|
public uint Frequency { get; }
|
|
public uint GrainSamples { get; }
|
|
public uint QueueDepth { get; }
|
|
public IHostAudioStream? Backend { get; }
|
|
|
|
public long GrainTicks => (long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency);
|
|
|
|
// Set once a grain reaches a host device: the device queue, not the grain clock, is then
|
|
// what holds the context's queued audio.
|
|
public volatile bool DevicePaced;
|
|
|
|
// The output holds QueueDepth grains: a grain can be queued as soon as the grain
|
|
// QueueDepth-1 ahead of it starts playing, so a title may render that far ahead of real
|
|
// time before a push blocks. Blocking every push for a whole grain instead holds a title's
|
|
// audio lock across each grain it renders in a burst (Wwise renders several under one lock).
|
|
public long QueueSlotFree(long grainStart) =>
|
|
grainStart - (GrainTicks * Math.Max((long)QueueDepth - 1, 0));
|
|
|
|
// Grains reserved on the clock that have not started playing, at most QueueDepth.
|
|
public uint ClockQueuedGrains()
|
|
{
|
|
long ahead;
|
|
lock (_paceGate)
|
|
{
|
|
ahead = _nextAdvanceTimestamp - Stopwatch.GetTimestamp();
|
|
}
|
|
|
|
return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth);
|
|
}
|
|
|
|
public double DeviceSlotFreeMilliseconds =>
|
|
DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth);
|
|
|
|
// Grains the device holds beyond the cushion, at most QueueDepth.
|
|
public uint DeviceQueuedGrains(int queuedMilliseconds)
|
|
{
|
|
var grainMilliseconds = GrainSamples * 1000.0 / Frequency;
|
|
var beyondCushion = queuedMilliseconds - DeviceCushionMilliseconds;
|
|
return beyondCushion <= 0
|
|
? 0
|
|
: (uint)Math.Min(Math.Ceiling(beyondCushion / grainMilliseconds), QueueDepth);
|
|
}
|
|
|
|
// Claims the next grain on the context's clock and returns the timestamp it starts playing.
|
|
public long ReserveGrain()
|
|
{
|
|
lock (_paceGate)
|
|
{
|
|
var now = Stopwatch.GetTimestamp();
|
|
if (_nextAdvanceTimestamp < now)
|
|
{
|
|
_nextAdvanceTimestamp = now;
|
|
}
|
|
|
|
var start = _nextAdvanceTimestamp;
|
|
_nextAdvanceTimestamp = checked(_nextAdvanceTimestamp + GrainTicks);
|
|
return start;
|
|
}
|
|
}
|
|
|
|
public static void SleepUntil(long timestamp)
|
|
{
|
|
var delay = timestamp - Stopwatch.GetTimestamp();
|
|
if (delay > 0)
|
|
{
|
|
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
|
}
|
|
}
|
|
|
|
// The start of the grain an Advance reserved and no Push has waited for yet; 0 when none.
|
|
public long AdvancedGrainStart;
|
|
}
|
|
|
|
private sealed class PortState
|
|
{
|
|
public PortState(
|
|
ulong handle,
|
|
ulong contextHandle,
|
|
ushort portType,
|
|
uint dataFormat,
|
|
uint samplingFrequency,
|
|
uint grainSamples)
|
|
{
|
|
Handle = handle;
|
|
ContextHandle = contextHandle;
|
|
PortType = portType;
|
|
DataFormat = dataFormat;
|
|
SamplingFrequency = samplingFrequency == 0 ? 48000 : samplingFrequency;
|
|
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
|
|
}
|
|
|
|
public ulong Handle { get; }
|
|
public ulong ContextHandle { get; }
|
|
/// <summary>Full Prospero port type (low byte = MAIN/BGM/…, 0x0100 = object).</summary>
|
|
public ushort PortType { get; }
|
|
public uint DataFormat { get; }
|
|
public uint SamplingFrequency { get; }
|
|
public uint GrainSamples { get; }
|
|
public ulong PcmAddress;
|
|
public float Gain = 1f;
|
|
public uint Ambisonics = AmbisonicsNone;
|
|
public uint Passthrough;
|
|
|
|
public byte[]? PcmData;
|
|
|
|
}
|
|
|
|
// Two host streams: primary FMOD context (menus) and everything else
|
|
// (Bink/intro). Mixing those into one waveOut re-crunched audio; the OS
|
|
// mixer keeps separate devices clean.
|
|
private static readonly object HostBackendGate = new();
|
|
private static IHostAudioStream? PrimaryBackend;
|
|
private static IHostAudioStream? SecondaryBackend;
|
|
private static string PrimaryBackendName = "none";
|
|
private static string SecondaryBackendName = "none";
|
|
private static ulong PrimaryContextHandle;
|
|
private static readonly object HostSubmitGate = new();
|
|
|
|
[SysAbiExport(
|
|
Nid = "g2tViFIohHE",
|
|
ExportName = "sceAudioOut2Initialize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2Initialize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
// Ghost of Yotei calls this with flags=0 during Scream startup and never
|
|
// checks the result before continuing into its mastering path; the actual
|
|
// mastering chain lives in the host mixer, so accepting the request is
|
|
// sufficient.
|
|
[SysAbiExport(
|
|
Nid = "XHl38ZNknbs",
|
|
ExportName = "sceAudioOut2MasteringInit",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2MasteringInit(CpuContext ctx)
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "v8iOE+j8a5o",
|
|
ExportName = "sceAudioOut2MasteringSetParam",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2MasteringSetParam(CpuContext ctx)
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
// 3D-audio object latency hint; the host mixer has no object pipeline to
|
|
// tune, but failure here makes Yotei tear down its whole ACM context and
|
|
// abort audio arena bring-up.
|
|
[SysAbiExport(
|
|
Nid = "TViD1EZXkNI",
|
|
ExportName = "sceAudioOut2Set3DLatency",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2Set3DLatency(CpuContext ctx)
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "t5YrizufpQc",
|
|
ExportName = "sceAudioOut2ContextResetParam",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextResetParam(CpuContext ctx)
|
|
{
|
|
// ABI: int32_t sceAudioOut2ContextResetParam(SceAudioOut2ContextParam *pParams).
|
|
var paramAddress = ctx[CpuRegister.Rdi];
|
|
if (paramAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
// SceAudioOut2ContextParam: maxPorts, maxObjectPorts, guaranteeObjectPorts,
|
|
// queueDepth, numGrains, flags, uint32_t reserved[10].
|
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
|
param.Clear();
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], 256);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 256);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 4);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x10..], 512);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x14..], 1);
|
|
|
|
return ctx.Memory.TryWrite(paramAddress, param)
|
|
? SetReturn(ctx, 0)
|
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "pDmme7Bgm6E",
|
|
ExportName = "sceAudioOut2ContextQueryMemory",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
|
{
|
|
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
|
|
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
|
|
var paramAddress = ctx[CpuRegister.Rdi];
|
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
|
if (paramAddress == 0 || memoryInfoAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
var contextMemorySize = (ulong)AudioOut2ContextMemorySize;
|
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
|
if (ctx.Memory.TryRead(paramAddress, param))
|
|
{
|
|
var queueDepth = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
|
|
if (queueDepth == 0)
|
|
{
|
|
queueDepth = 4;
|
|
}
|
|
|
|
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
|
|
}
|
|
|
|
// The output is a single size_t wherever it lives. Titles keep other locals right after it
|
|
// (Octopath Traveler II divides by the dword at +12), so nothing else may be written.
|
|
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
|
|
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
|
|
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
|
|
? SetReturn(ctx, 0)
|
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "0x6o1VVAYSY",
|
|
ExportName = "sceAudioOut2ContextCreate",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextCreate(CpuContext ctx)
|
|
{
|
|
var paramAddress = ctx[CpuRegister.Rdi];
|
|
var memoryAddress = ctx[CpuRegister.Rsi];
|
|
var memorySize = ctx[CpuRegister.Rdx];
|
|
// ABI: sceAudioOut2ContextCreate(const SceAudioOut2ContextParam *pParams,
|
|
// void *pBuffer, size_t szBufferSize, SceAudioOut2ContextHandle *phCtx).
|
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
|
if (paramAddress == 0 || memoryAddress == 0 || outContextAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
if (memorySize == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidParam);
|
|
}
|
|
|
|
// Prospero AudioOut2 context params are port/queue config, not an AudioOut
|
|
// open-style frequency/channel block. Sample rate is fixed at 48 kHz.
|
|
uint frequency = 48000;
|
|
uint grain = 256;
|
|
uint queueDepth = 4;
|
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
|
if (ctx.Memory.TryRead(paramAddress, param))
|
|
{
|
|
var qd = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
|
|
var ng = BinaryPrimitives.ReadUInt32LittleEndian(param[0x10..]);
|
|
if (qd is >= 1 and <= 32) queueDepth = qd;
|
|
if (ng is >= 64 and <= 0x4000) grain = ng;
|
|
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
|
|
}
|
|
|
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
|
// Backend is bound lazily on first real Push (primary vs secondary device).
|
|
Contexts[handle] = new ContextState(handle, frequency, grain, queueDepth, backend: null);
|
|
TraceAudioOut2(
|
|
$"context-create handle=0x{handle:X} frequency={frequency} grain={grain} " +
|
|
$"queue={queueDepth} memory=0x{memoryAddress:X} size=0x{memorySize:X} backend=pending");
|
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
|
? SetReturn(ctx, 0)
|
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "on6ZH7Abo10",
|
|
ExportName = "sceAudioOut2ContextDestroy",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextDestroy(CpuContext ctx)
|
|
{
|
|
var contextHandle = ctx[CpuRegister.Rdi];
|
|
Contexts.TryRemove(contextHandle, out _);
|
|
foreach (var (portHandle, port) in Ports)
|
|
{
|
|
if (port.ContextHandle != contextHandle ||
|
|
!Ports.TryRemove(portHandle, out var removedPort))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Interlocked.Exchange(ref removedPort.PcmData, null);
|
|
removedPort.PcmAddress = 0;
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "DxGyV8dtOR8",
|
|
ExportName = "sceAudioOut2ContextBedWrite",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "aII9h5nli9U",
|
|
ExportName = "sceAudioOut2ContextPush",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextPush(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2ContextPush(ctx, blocking). RSI is a blocking flag
|
|
// (observed 1), not a PCM pointer. PCM is attached earlier via
|
|
// PortSetAttributes(attribute_id=PCM) and flushed here.
|
|
var handle = ctx[CpuRegister.Rdi];
|
|
var blocking = unchecked((uint)ctx[CpuRegister.Rsi]);
|
|
if (Interlocked.Increment(ref _pushTraceCount) <= 8)
|
|
{
|
|
TraceAudioOut2($"context-push handle=0x{handle:X} blocking={blocking}");
|
|
}
|
|
|
|
if (!Contexts.TryGetValue(handle, out var context))
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
// Host Submit already blocks on the output queue; software pacing covers grains with nothing
|
|
// queued, once per grain whichever of Advance and Push comes first.
|
|
var submitted = TrySubmitContextAudio(ctx, context);
|
|
var advancedGrain = Interlocked.Exchange(ref context.AdvancedGrainStart, 0);
|
|
if (submitted)
|
|
{
|
|
context.DevicePaced = true;
|
|
}
|
|
|
|
if (submitted || advancedGrain == 1)
|
|
{
|
|
if (blocking != 0)
|
|
{
|
|
WaitForDeviceSlot(context);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Push is the blocking point of a grain: it waits for the grain an Advance claimed, or
|
|
// claims one itself when the title does not advance separately.
|
|
ContextState.SleepUntil(context.QueueSlotFree(advancedGrain != 0 ? advancedGrain : context.ReserveGrain()));
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "PE2zHMqLSHs",
|
|
ExportName = "sceAudioOut2ContextAdvance",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextAdvance(CpuContext ctx)
|
|
{
|
|
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var state))
|
|
{
|
|
var submitted = TrySubmitContextAudio(ctx, state);
|
|
if (submitted)
|
|
{
|
|
state.DevicePaced = true;
|
|
// The host backend already blocked for this grain; the Push that follows must not.
|
|
Volatile.Write(ref state.AdvancedGrainStart, 1);
|
|
}
|
|
else
|
|
{
|
|
// Advance moves the context one grain on without blocking; the Push that follows waits.
|
|
var previous = Interlocked.Exchange(ref state.AdvancedGrainStart, state.ReserveGrain());
|
|
if (previous != 0)
|
|
{
|
|
// Two Advances without a Push: the earlier grain is waited for here.
|
|
ContextState.SleepUntil(state.QueueSlotFree(previous));
|
|
}
|
|
}
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "R7d0F1g2qsU",
|
|
ExportName = "sceAudioOut2ContextGetQueueLevel",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
|
|
{
|
|
// ABI out is a 32-bit queue depth (GTA compares dword [out] to 4). A
|
|
// uint64 write into a stack slot at [rbp-0x14] next to the canary at
|
|
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
|
|
var outLevelAddress = ctx[CpuRegister.Rsi];
|
|
var outAvailableAddress = ctx[CpuRegister.Rdx];
|
|
if (outLevelAddress == 0)
|
|
{
|
|
outLevelAddress = outAvailableAddress;
|
|
outAvailableAddress = 0;
|
|
}
|
|
|
|
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
|
|
// update and pushes them non-blocking, so a queue always reported empty made it render a
|
|
// full queue every update and block in Push while holding its audio lock.
|
|
var hasContext = Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context);
|
|
var queued = hasContext ? QueuedGrains(context!) : 0u;
|
|
Span<byte> level = stackalloc byte[sizeof(uint)];
|
|
if (outLevelAddress != 0)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(level, queued);
|
|
if (!ctx.Memory.TryWrite(outLevelAddress, level))
|
|
{
|
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
}
|
|
|
|
if (outAvailableAddress != 0 &&
|
|
outAvailableAddress != outLevelAddress &&
|
|
IsWritableOutBuffer(outAvailableAddress))
|
|
{
|
|
var available = hasContext ? context!.QueueDepth - queued : 4u;
|
|
BinaryPrimitives.WriteUInt32LittleEndian(level, available);
|
|
if (!ctx.Memory.TryWrite(outAvailableAddress, level))
|
|
{
|
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Q8DZkKQ-SYc",
|
|
ExportName = "sceAudioOut2LoContextGetQueueLevel",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2LoContextGetQueueLevel(CpuContext ctx) =>
|
|
AudioOut2ContextGetQueueLevel(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "8XTArSPyWHk",
|
|
ExportName = "sceAudioOut2PortSetAttributes",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2PortSetAttributes(CpuContext ctx)
|
|
{
|
|
// sceAudioOut2PortSetAttributes(port, attributes*, num).
|
|
// Attribute id 0 = PCM; value points at { const void* data }.
|
|
var portHandle = ctx[CpuRegister.Rdi];
|
|
var attributesAddress = ctx[CpuRegister.Rsi];
|
|
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
|
if (!Ports.TryGetValue(portHandle, out var port))
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
if (attributeCount == 0 || attributesAddress == 0)
|
|
{
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
if (attributeCount > 32)
|
|
{
|
|
attributeCount = 32;
|
|
}
|
|
|
|
Span<byte> entry = stackalloc byte[AttributeEntrySize];
|
|
Span<byte> pcm = stackalloc byte[8];
|
|
Span<byte> word = stackalloc byte[sizeof(uint)];
|
|
for (uint i = 0; i < attributeCount; i++)
|
|
{
|
|
if (!ctx.Memory.TryRead(attributesAddress + (i * AttributeEntrySize), entry))
|
|
{
|
|
break;
|
|
}
|
|
|
|
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
|
|
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
|
|
var valueSize = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x10..]);
|
|
if (valueAddress == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (attributeId is PortAttributeIdGain or PortAttributeIdPassthrough or PortAttributeIdAmbisonics)
|
|
{
|
|
if (valueSize < sizeof(uint) || !ctx.Memory.TryRead(valueAddress, word))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var value = BinaryPrimitives.ReadUInt32LittleEndian(word);
|
|
switch (attributeId)
|
|
{
|
|
case PortAttributeIdGain:
|
|
var gain = BitConverter.UInt32BitsToSingle(value);
|
|
if (float.IsFinite(gain))
|
|
{
|
|
port.Gain = gain;
|
|
}
|
|
|
|
break;
|
|
case PortAttributeIdPassthrough:
|
|
port.Passthrough = value;
|
|
break;
|
|
default:
|
|
port.Ambisonics = value;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (attributeId != PortAttributeIdPcm || valueSize < 8)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ctx.Memory.TryRead(valueAddress, pcm))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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} " +
|
|
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
|
|
$"grains={port.GrainSamples}");
|
|
}
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "JK2wamZPzwM",
|
|
ExportName = "sceAudioOut2PortCreate",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2PortCreate(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
|
|
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
|
|
var contextHandle = ctx[CpuRegister.Rdi];
|
|
var paramAddress = ctx[CpuRegister.Rsi];
|
|
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
|
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
|
|
if (outPortAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
ushort portType = 0;
|
|
uint dataFormat = 0x0000_0200; // float stereo default
|
|
uint samplingFrequency = 48000;
|
|
uint grainSamples = 256;
|
|
if (Contexts.TryGetValue(contextHandle, out var context))
|
|
{
|
|
grainSamples = context.GrainSamples;
|
|
samplingFrequency = context.Frequency;
|
|
}
|
|
|
|
if (paramAddress != 0)
|
|
{
|
|
Span<byte> param = stackalloc byte[PortParamSize];
|
|
if (ctx.Memory.TryRead(paramAddress, param))
|
|
{
|
|
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
|
|
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
|
|
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
|
|
if (freq is >= 8000 and <= 192000)
|
|
{
|
|
samplingFrequency = freq;
|
|
}
|
|
}
|
|
}
|
|
|
|
var portId = (uint)Interlocked.Increment(ref _nextPortId);
|
|
// Handle encodes only the low type byte; PortState keeps the full type
|
|
// so object ports (0x01xx) can still be filtered at submit time.
|
|
var handle = 0x2000_0000UL | ((ulong)(portType & 0xFF) << 16) | portId;
|
|
var portState = new PortState(
|
|
handle,
|
|
contextHandle,
|
|
portType,
|
|
dataFormat,
|
|
samplingFrequency,
|
|
grainSamples);
|
|
Ports[handle] = portState;
|
|
if (!TryWriteUInt64(ctx, outPortAddress, handle))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
TraceAudioOut2(
|
|
$"port-create handle=0x{handle:X} ctx=0x{contextHandle:X} type=0x{portType:X} " +
|
|
$"format=0x{dataFormat:X} freq={samplingFrequency} out=0x{outPortAddress:X}");
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "gatEUKG+Ea4",
|
|
ExportName = "sceAudioOut2PortGetState",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2PortGetState(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2PortGetState(SceAudioOut2PortHandle hVPort,
|
|
// SceAudioOut2PortState *pOutState) — rsi is the only out pointer and the
|
|
// write is exactly sizeof(SceAudioOut2PortState) == 0x40, stack or heap.
|
|
var portHandle = ctx[CpuRegister.Rdi];
|
|
var stateAddress = ctx[CpuRegister.Rsi];
|
|
if (stateAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
Span<byte> state = stackalloc byte[PortStateSize];
|
|
state.Clear();
|
|
// +0x00 u16 output = OUTPUT_PRIMARY (1)
|
|
// +0x02 u8 numChannels = from port format when known, else 2
|
|
// +0x04 s16 volume = -1 (not applicable outside PADSPK)
|
|
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
|
|
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
|
|
byte channels = 2;
|
|
if (Ports.TryGetValue(portHandle, out var port) &&
|
|
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
|
|
{
|
|
channels = (byte)Math.Clamp(decodedChannels, 1, SpeakerMax);
|
|
}
|
|
|
|
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
|
|
state[0x02] = channels;
|
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
|
|
|
if (!ctx.Memory.TryWrite(stateAddress, state))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
TraceAudioOut2(
|
|
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "4dq2rblWlg0",
|
|
ExportName = "sceAudioOut2ContextSetAttributes",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2ContextSetAttributes(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2ContextSetAttributes(SceAudioOut2ContextHandle hCtx,
|
|
// const SceAudioOut2Attribute *pAttribute, uint32_t uiNumAttributes).
|
|
var attributeAddress = ctx[CpuRegister.Rsi];
|
|
var count = unchecked((uint)ctx[CpuRegister.Rdx]);
|
|
return SetReturn(
|
|
ctx,
|
|
count != 0 && attributeAddress == 0 ? AudioOut2ErrorInvalidPointer : 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "bkBN+CMLwRc",
|
|
ExportName = "sceAudioOut2GetSystemState",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2GetSystemState(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2GetSystemState(SceAudioOut2SystemState *outStates) —
|
|
// one out pointer, exactly sizeof(SceAudioOut2SystemState) == 0x40. loudness
|
|
// stays 0.0f because the host mixer runs no loudness meter.
|
|
var stateAddress = ctx[CpuRegister.Rdi];
|
|
if (stateAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
Span<byte> state = stackalloc byte[SystemStateSize];
|
|
state.Clear();
|
|
return ctx.Memory.TryWrite(stateAddress, state)
|
|
? SetReturn(ctx, 0)
|
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "DImz2Ft9E2g",
|
|
ExportName = "sceAudioOut2GetSpeakerInfo",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2GetSpeakerInfo(SceAudioOut2SpeakerInfo *outInfo,
|
|
// uint32_t flags) — rdi is the out struct, esi is a flag word (never a
|
|
// pointer). The write is exactly sizeof(SceAudioOut2SpeakerInfo) == 0x50.
|
|
var infoAddress = ctx[CpuRegister.Rdi];
|
|
if (infoAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
Span<byte> info = stackalloc byte[SpeakerInfoSize];
|
|
info.Clear();
|
|
// +0x00 u8 type = SPEAKER_TYPE_TV
|
|
// +0x04 u32 availableBits = FRONT_LEFT | FRONT_RIGHT
|
|
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
|
|
// +0x10 SceAudioOut2SpeakerAngle aSpeakerAngle[16] { s16 azimuth, s16 elevation }
|
|
info[0x00] = SpeakerTypeTv;
|
|
const uint stereoBits =
|
|
(1u << 0 /* SCE_AUDIO_OUT2_SPEAKER_FRONT_LEFT */) |
|
|
(1u << 1 /* SCE_AUDIO_OUT2_SPEAKER_FRONT_RIGHT */);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], stereoBits);
|
|
BinaryPrimitives.WriteInt16LittleEndian(info[SpeakerInfoAngleOffset..], -30);
|
|
BinaryPrimitives.WriteInt16LittleEndian(info[(SpeakerInfoAngleOffset + 4)..], 30);
|
|
|
|
if (!ctx.Memory.TryWrite(infoAddress, info))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
TraceAudioOut2(
|
|
$"get-speaker-info out=0x{infoAddress:X} flags=0x{ctx[CpuRegister.Rsi]:X} " +
|
|
$"bytes=0x{SpeakerInfoSize:X}");
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
// ABI: size_t sceAudioOut2GetSpeakerArrayMemorySize(uint32_t uiNumSpeakers,
|
|
// uint8_t ucIs3d, uint8_t uiIsAmbisonics) — the size is the return value in rax.
|
|
// The header does not document the object body, so the exact channel-scaled
|
|
// size below is ours; never page-align it to a 64K slab.
|
|
[SysAbiExport(
|
|
Nid = "G1YOKDJYX2Y",
|
|
ExportName = "sceAudioOut2GetSpeakerArrayMemorySize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2GetSpeakerArrayMemorySize(CpuContext ctx)
|
|
{
|
|
var numChannels = (uint)ctx[CpuRegister.Rdi];
|
|
if (numChannels == 0 || numChannels > SpeakerArrayMaxChannels)
|
|
{
|
|
numChannels = SpeakerArrayDefaultChannels;
|
|
}
|
|
|
|
var size = ComputeSpeakerArrayBytes(numChannels);
|
|
TraceAudioOut2(
|
|
$"speaker-array-get-size numSpeakers={numChannels} is3d={ctx[CpuRegister.Rsi] & 0xFF} " +
|
|
$"isAmbisonics={ctx[CpuRegister.Rdx] & 0xFF} -> 0x{size:X}");
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)size);
|
|
return size;
|
|
}
|
|
|
|
// ABI: sceAudioOut2GetSpeakerArrayCoefficients(handle,
|
|
// SceAudioOut2Position pos, float fSpread, float *pCoefficients,
|
|
// uint32_t uiNumCoefficients, uint8_t bHeightAware, float fDownmixSpreadRadius).
|
|
// pos/fSpread/fDownmixSpreadRadius are SSE-class, so the integer slots are
|
|
// rdi=handle, rsi=pCoefficients, edx=uiNumCoefficients, cl=bHeightAware.
|
|
[SysAbiExport(
|
|
Nid = "4BlZurolOAo",
|
|
ExportName = "sceAudioOut2GetSpeakerArrayCoefficients",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2GetSpeakerArrayCoefficients(CpuContext ctx) =>
|
|
WriteZeroSpeakerArrayCoefficients(
|
|
ctx,
|
|
ctx[CpuRegister.Rsi],
|
|
unchecked((uint)ctx[CpuRegister.Rdx]),
|
|
"coefficients");
|
|
|
|
// ABI: sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(handle,
|
|
// SceAudioOut2Ambisonics eAmbiChannel, float *pCoefficients,
|
|
// uint32_t uiNumCoefficients) — all integer-class, so the buffer is rdx.
|
|
[SysAbiExport(
|
|
Nid = "28QqMnuuJ9Y",
|
|
ExportName = "sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2GetSpeakerArrayAmbisonicsCoefficients(CpuContext ctx) =>
|
|
WriteZeroSpeakerArrayCoefficients(
|
|
ctx,
|
|
ctx[CpuRegister.Rdx],
|
|
unchecked((uint)ctx[CpuRegister.Rcx]),
|
|
"ambisonics-coefficients");
|
|
|
|
[SysAbiExport(
|
|
Nid = "+k91hoTuoA8",
|
|
ExportName = "sceAudioOut2SpeakerArrayCreate",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2SpeakerArrayCreate(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2SpeakerArrayCreate(SceAudioOut2SpeakerArrayHandle *pHandle,
|
|
// const SceAudioOut2SpeakerArrayParam *pVbapParams,
|
|
// const SceAudioOut2AmbisonicsDecodeParam *pAmbiParams). rdi is the OUT handle
|
|
// and rsi carries the speaker count plus the caller-owned pBuffer/szSize the
|
|
// handle is meant to live in; there is no fourth "channels" argument.
|
|
var outHandleAddress = ctx[CpuRegister.Rdi];
|
|
var vbapParamAddress = ctx[CpuRegister.Rsi];
|
|
var ambiParamAddress = ctx[CpuRegister.Rdx];
|
|
if (outHandleAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
var channels = SpeakerArrayDefaultChannels;
|
|
ulong guestBuffer = 0;
|
|
ulong guestBufferSize = 0;
|
|
if (vbapParamAddress != 0)
|
|
{
|
|
Span<byte> param = stackalloc byte[SpeakerArrayParamSize];
|
|
if (ctx.Memory.TryRead(vbapParamAddress, param))
|
|
{
|
|
var numSpeakers =
|
|
BinaryPrimitives.ReadUInt32LittleEndian(param[SpeakerArrayParamNumSpeakersOffset..]);
|
|
if (numSpeakers is >= 1 and <= SpeakerArrayMaxChannels)
|
|
{
|
|
channels = numSpeakers;
|
|
}
|
|
|
|
guestBuffer = BinaryPrimitives.ReadUInt64LittleEndian(param[SpeakerArrayParamBufferOffset..]);
|
|
guestBufferSize = BinaryPrimitives.ReadUInt64LittleEndian(param[SpeakerArrayParamSizeOffset..]);
|
|
}
|
|
}
|
|
|
|
// The real library builds the array inside the caller's pBuffer and hands
|
|
// that back as the handle. Fall back to an HLE allocation only when the
|
|
// title did not provide (or under-sized) the buffer.
|
|
var bytes = (ulong)ComputeSpeakerArrayBytes(channels);
|
|
var memory = guestBuffer != 0 && guestBufferSize >= bytes ? guestBuffer : 0;
|
|
if (memory == 0 && !TryAllocateSpeakerArrayMemory(ctx, bytes, out memory))
|
|
{
|
|
TraceAudioOut2($"speaker-array-create alloc-failed bytes=0x{bytes:X} channels={channels}");
|
|
return SetReturn(ctx, AudioOut2ErrorOutOfMemory);
|
|
}
|
|
|
|
if (!InitializeSpeakerArrayObject(ctx, memory, channels))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
SpeakerArrays[memory] = 0;
|
|
if (!TryWriteUInt64(ctx, outHandleAddress, memory))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
TraceAudioOut2(
|
|
$"speaker-array-create handle=0x{memory:X} bytes=0x{bytes:X} channels={channels} " +
|
|
$"vbap=0x{vbapParamAddress:X} ambi=0x{ambiParamAddress:X} out=0x{outHandleAddress:X} " +
|
|
$"guestBuffer=0x{guestBuffer:X}");
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "erCWQR5eKiQ",
|
|
ExportName = "sceAudioOut2SpeakerArrayDestroy",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2SpeakerArrayDestroy(CpuContext ctx)
|
|
{
|
|
SpeakerArrays.TryRemove(ctx[CpuRegister.Rdi], out _);
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "cd+Rtw+D1x8",
|
|
ExportName = "sceAudioOut2PortDestroy",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2PortDestroy(CpuContext ctx)
|
|
{
|
|
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "IaZXJ9M79uo",
|
|
ExportName = "sceAudioOut2UserDestroy",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "xywYcRB7nbQ",
|
|
ExportName = "sceAudioOut2UserCreate",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2UserCreate(CpuContext ctx)
|
|
{
|
|
// ABI: sceAudioOut2UserCreate(uint32_t userId,
|
|
// SceAudioOut2UserHandle *pHandle) — pHandle is a uintptr_t out slot (8 bytes).
|
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
|
if (outUserAddress == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
// SCE_USER_SERVICE_USER_ID_INVALID is the only value the library rejects;
|
|
// every other id maps onto the single local user this emulator exposes.
|
|
if (userId == -1)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidUser);
|
|
}
|
|
|
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
|
return TryWriteUInt64(ctx, outUserAddress, handle)
|
|
? SetReturn(ctx, 0)
|
|
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
// ABI: sceAudioOut2UserGetSupportedAttributes(SceAudioOut2UserHandle handle,
|
|
// uint32_t *contextSupportedAttributes, uint32_t *portSupportedAttributes) — two
|
|
// independent uint32 outs.
|
|
[SysAbiExport(
|
|
Nid = "iE8trxPKnAg",
|
|
ExportName = "sceAudioOut2UserGetSupportedAttributes",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAudioOut2")]
|
|
public static int AudioOut2UserGetSupportedAttributes(CpuContext ctx)
|
|
{
|
|
var contextAttributesAddress = ctx[CpuRegister.Rsi];
|
|
var portAttributesAddress = ctx[CpuRegister.Rdx];
|
|
Span<byte> mask = stackalloc byte[sizeof(uint)];
|
|
|
|
// Context attribute ids 0..4 and port attribute ids 0..13 exist in the
|
|
// header; report them all as supported since nothing here rejects one.
|
|
if (contextAttributesAddress != 0)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(mask, (1u << 5) - 1u);
|
|
if (!ctx.Memory.TryWrite(contextAttributesAddress, mask))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
}
|
|
|
|
if (portAttributesAddress != 0)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(mask, (1u << 14) - 1u);
|
|
if (!ctx.Memory.TryWrite(portAttributesAddress, mask))
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
}
|
|
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
private static IHostAudioStream? ResolveContextBackend(ContextState context, out string backendName)
|
|
{
|
|
lock (HostBackendGate)
|
|
{
|
|
if (PrimaryContextHandle == 0)
|
|
{
|
|
PrimaryContextHandle = context.Handle;
|
|
}
|
|
|
|
if (context.Handle == PrimaryContextHandle)
|
|
{
|
|
if (PrimaryBackend is null)
|
|
{
|
|
try
|
|
{
|
|
var audio = HostPlatform.Current.Audio;
|
|
// Deeper host queue than classic AudioOut: FMOD's bursty
|
|
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
|
|
PrimaryBackend = audio.OpenStereoPcm16Stream(
|
|
context.Frequency,
|
|
maxQueuedPcmBytes: 128 * 1024);
|
|
PrimaryBackendName = audio.BackendName + "-primary";
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
PrimaryBackendName = "silent";
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] AudioOut2 primary backend unavailable: {exception.Message}");
|
|
}
|
|
}
|
|
|
|
backendName = PrimaryBackendName;
|
|
return PrimaryBackend;
|
|
}
|
|
|
|
if (SecondaryBackend is null)
|
|
{
|
|
try
|
|
{
|
|
var audio = HostPlatform.Current.Audio;
|
|
SecondaryBackend = audio.OpenStereoPcm16Stream(
|
|
context.Frequency,
|
|
maxQueuedPcmBytes: 128 * 1024);
|
|
SecondaryBackendName = audio.BackendName + "-secondary";
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
SecondaryBackendName = "silent";
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] AudioOut2 secondary backend unavailable: {exception.Message}");
|
|
}
|
|
}
|
|
|
|
backendName = SecondaryBackendName;
|
|
return SecondaryBackend;
|
|
}
|
|
}
|
|
|
|
internal static double DeviceSlotFreeThreshold(uint grainSamples, uint frequency, uint queueDepth) =>
|
|
DeviceCushionMilliseconds + (grainSamples * 1000.0 / frequency * Math.Max((long)queueDepth - 1, 0));
|
|
|
|
private static void WaitForDeviceSlot(ContextState context)
|
|
{
|
|
var backend = ResolveContextBackend(context, out _);
|
|
if (backend is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var limit = context.DeviceSlotFreeMilliseconds;
|
|
var deadline = Stopwatch.GetTimestamp() + (Stopwatch.Frequency / 4);
|
|
int queued;
|
|
while ((queued = backend.QueuedMilliseconds) > limit &&
|
|
Stopwatch.GetTimestamp() < deadline &&
|
|
!HostSessionControl.IsShutdownRequested)
|
|
{
|
|
Thread.Sleep((int)Math.Clamp(Math.Ceiling(queued - limit), 1, 50));
|
|
}
|
|
}
|
|
|
|
private static uint QueuedGrains(ContextState context)
|
|
{
|
|
if (context.DevicePaced)
|
|
{
|
|
var backend = ResolveContextBackend(context, out _);
|
|
var queuedMilliseconds = backend?.QueuedMilliseconds ?? -1;
|
|
if (queuedMilliseconds >= 0)
|
|
{
|
|
return context.DeviceQueuedGrains(queuedMilliseconds);
|
|
}
|
|
}
|
|
|
|
return context.ClockQueuedGrains();
|
|
}
|
|
|
|
private static bool TrySubmitContextAudio(CpuContext ctx, ContextState context)
|
|
{
|
|
var frames = checked((int)context.GrainSamples);
|
|
if (frames <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lock (HostSubmitGate)
|
|
{
|
|
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
|
|
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
|
|
try
|
|
{
|
|
mix.AsSpan(0, frames * 2).Clear();
|
|
var mixedPorts = 0;
|
|
foreach (var port in Ports.Values)
|
|
{
|
|
if (port.ContextHandle != context.Handle ||
|
|
!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 > checked(frames * 16 * sizeof(float)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (pcmData.Length < byteLength)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MixPortIntoStereo(
|
|
pcmData.AsSpan(0, byteLength),
|
|
mix.AsSpan(0, frames * 2),
|
|
frames,
|
|
ch,
|
|
bps,
|
|
isFloat,
|
|
leftGain,
|
|
rightGain);
|
|
mixedPorts++;
|
|
}
|
|
|
|
if (mixedPorts == 0)
|
|
{
|
|
TraceSubmitSkipped(context, frames, "no-ports");
|
|
return false;
|
|
}
|
|
|
|
var outputSpan = output.AsSpan(0, frames * AudioPcmConversion.OutputFrameSize);
|
|
var peak = 0f;
|
|
for (var frame = 0; frame < frames; frame++)
|
|
{
|
|
var left = Math.Clamp(mix[frame * 2], -1f, 1f);
|
|
var right = Math.Clamp(mix[(frame * 2) + 1], -1f, 1f);
|
|
peak = Math.Max(peak, Math.Max(Math.Abs(left), Math.Abs(right)));
|
|
BinaryPrimitives.WriteInt16LittleEndian(
|
|
outputSpan[(frame * AudioPcmConversion.OutputFrameSize)..],
|
|
FloatToPcm16(left));
|
|
BinaryPrimitives.WriteInt16LittleEndian(
|
|
outputSpan[((frame * AudioPcmConversion.OutputFrameSize) + 2)..],
|
|
FloatToPcm16(right));
|
|
}
|
|
|
|
var backend = ResolveContextBackend(context, out var backendName);
|
|
if (backend is null)
|
|
{
|
|
TraceSubmitSkipped(context, frames, "no-backend");
|
|
return false;
|
|
}
|
|
|
|
var n = Interlocked.Increment(ref _submitTraceCount);
|
|
if (n <= 8 || n % 500 == 0)
|
|
{
|
|
TraceAudioOut2(
|
|
$"context-submit#{n} handle=0x{context.Handle:X} frames={frames} " +
|
|
$"ports={mixedPorts} peak={peak:F4} backend={backendName}");
|
|
}
|
|
|
|
return backend.Submit(outputSpan);
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<float>.Shared.Return(mix);
|
|
ArrayPool<byte>.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<uint, IHostAudioStream?>? 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) =>
|
|
Ports.TryGetValue(portHandle, out var port) ? (port.Gain, port.Ambisonics, port.Passthrough) : null;
|
|
|
|
internal static bool TryGetStereoGains(
|
|
ushort portType,
|
|
int channels,
|
|
float gain,
|
|
uint ambisonics,
|
|
uint passthrough,
|
|
out float left,
|
|
out float right)
|
|
{
|
|
left = 0f;
|
|
right = 0f;
|
|
if (!RoutesToSpeakers(portType) || gain == 0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!IsObjectPort(portType) || channels != 1)
|
|
{
|
|
left = gain;
|
|
right = gain;
|
|
return true;
|
|
}
|
|
|
|
if (ambisonics == AmbisonicsNone)
|
|
{
|
|
left = passthrough == PassthroughRight ? 0f : passthrough == PassthroughLeft ? gain : gain * HalfPower;
|
|
right = passthrough == PassthroughLeft ? 0f : passthrough == PassthroughRight ? gain : gain * HalfPower;
|
|
return true;
|
|
}
|
|
|
|
var acn = ambisonics >= AmbisonicsAcnBase;
|
|
var component = acn ? ambisonics - AmbisonicsAcnBase : ambisonics;
|
|
if (component == 0)
|
|
{
|
|
left = acn ? gain * HalfPower : gain;
|
|
right = left;
|
|
return true;
|
|
}
|
|
|
|
if (component == (acn ? 1u : 2u))
|
|
{
|
|
left = gain * HalfPower;
|
|
right = -left;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
|
|
RoutesToSpeakers(portType) &&
|
|
(portType & 0xFF) != AudioOut2PortTypeVibration &&
|
|
(!IsObjectPort(portType) || channels == 1);
|
|
|
|
internal static void MixPortIntoStereo(
|
|
ReadOnlySpan<byte> source,
|
|
Span<float> mix,
|
|
int frames,
|
|
int channels,
|
|
int bytesPerSample,
|
|
bool isFloat,
|
|
float leftGain,
|
|
float rightGain)
|
|
{
|
|
var frameSize = channels * bytesPerSample;
|
|
for (var frame = 0; frame < frames; frame++)
|
|
{
|
|
var frameBytes = source.Slice(frame * frameSize, frameSize);
|
|
float left;
|
|
float right;
|
|
if (channels >= 8)
|
|
{
|
|
var fl = ReadNormalizedSample(frameBytes, 0, bytesPerSample, isFloat);
|
|
var fr = ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
|
|
var c = ReadNormalizedSample(frameBytes, 2, bytesPerSample, isFloat);
|
|
var bl = ReadNormalizedSample(frameBytes, 4, bytesPerSample, isFloat);
|
|
var br = ReadNormalizedSample(frameBytes, 5, bytesPerSample, isFloat);
|
|
var sl = ReadNormalizedSample(frameBytes, 6, bytesPerSample, isFloat);
|
|
var sr = ReadNormalizedSample(frameBytes, 7, bytesPerSample, isFloat);
|
|
const float side = 0.70710678f;
|
|
left = fl + (c * side) + (bl * side) + (sl * side);
|
|
right = fr + (c * side) + (br * side) + (sr * side);
|
|
}
|
|
else
|
|
{
|
|
left = ReadNormalizedSample(frameBytes, 0, bytesPerSample, isFloat);
|
|
right = channels == 1
|
|
? left
|
|
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
|
|
}
|
|
|
|
mix[frame * 2] += left * leftGain;
|
|
mix[(frame * 2) + 1] += right * rightGain;
|
|
}
|
|
}
|
|
|
|
private static float ReadNormalizedSample(
|
|
ReadOnlySpan<byte> frame,
|
|
int channel,
|
|
int bytesPerSample,
|
|
bool isFloat)
|
|
{
|
|
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
|
if (isFloat)
|
|
{
|
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
|
var value = BitConverter.Int32BitsToSingle(bits);
|
|
return float.IsFinite(value) ? value : 0f;
|
|
}
|
|
|
|
return BinaryPrimitives.ReadInt16LittleEndian(sample) / 32768f;
|
|
}
|
|
|
|
private static short FloatToPcm16(float value)
|
|
{
|
|
var scale = value < 0f ? 32768f : short.MaxValue;
|
|
return (short)Math.Clamp(MathF.Round(value * scale), short.MinValue, short.MaxValue);
|
|
}
|
|
|
|
private static bool IsObjectPort(ushort portType) => (portType & 0xFF00) == 0x0100;
|
|
|
|
private static bool TryDecodeDataFormat(
|
|
uint dataFormat,
|
|
out int channels,
|
|
out int bytesPerSample,
|
|
out bool isFloat)
|
|
{
|
|
channels = (int)((dataFormat >> 8) & 0xFF);
|
|
if (channels == 0)
|
|
{
|
|
channels = 2;
|
|
}
|
|
|
|
if (channels is < 1 or > 16)
|
|
{
|
|
bytesPerSample = 0;
|
|
isFloat = false;
|
|
return false;
|
|
}
|
|
|
|
var dataType = dataFormat & 0x7Fu;
|
|
isFloat = dataType == 0;
|
|
bytesPerSample = isFloat ? 4 : dataType == 1 ? 2 : 0;
|
|
return bytesPerSample != 0;
|
|
}
|
|
|
|
private static int ComputeSpeakerArrayBytes(uint channels) =>
|
|
SpeakerArrayHeaderSize + (int)(channels * SpeakerArrayEntrySize) + SpeakerArrayScratchBytes;
|
|
|
|
private static bool InitializeSpeakerArrayObject(CpuContext ctx, ulong memory, uint channels)
|
|
{
|
|
// Header only — never wipe the full GetSize slab (and never touch stack).
|
|
Span<byte> body = stackalloc byte[SpeakerArrayHeaderSize];
|
|
body.Clear();
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body[0x00..], (uint)SpeakerArrayHeaderSize);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body[0x04..], channels);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body[SpeakerArrayDivisorFieldOffset..], SpeakerArrayDefaultDivisor);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body[SpeakerArrayResultFieldOffset..], 0);
|
|
return ctx.Memory.TryWrite(memory, body);
|
|
}
|
|
|
|
// Prefer the high guest arena (0x6000_xxxx). TryAllocateHleData advances
|
|
// _nextVirtualAddress into the title's direct-memory window (~0x1559_xxxx);
|
|
// publishing an object there made sceKernelBatchMap(fixed, 0x1559C80000,
|
|
// 0x20000) return NOT_FOUND and abort RenderThread with int 0x41.
|
|
// Never mint the old 0x1559C0xxxx "cookie" pointers — they are unmapped and
|
|
// collide with dmem VAs.
|
|
private static bool TryAllocateSpeakerArrayMemory(CpuContext ctx, ulong bytes, out ulong memory)
|
|
{
|
|
memory = 0;
|
|
var length = Math.Max(bytes, 0x1000UL);
|
|
|
|
if (TryAllocateViaGuestAllocator(ctx, length, 0x1000, out memory) &&
|
|
IsSafeSpeakerArrayAddress(memory))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Kernel.KernelMemoryCompatExports.TryAllocateHleData(ctx, length, 0x1000, out memory) &&
|
|
IsSafeSpeakerArrayAddress(memory))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
memory = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryAllocateViaGuestAllocator(CpuContext ctx, ulong length, ulong alignment, out ulong memory)
|
|
{
|
|
memory = 0;
|
|
var allocator = ctx.Memory as IGuestMemoryAllocator;
|
|
if (allocator is null && ctx.Memory is ICpuMemoryWrapper { Inner: IGuestMemoryAllocator inner })
|
|
{
|
|
allocator = inner;
|
|
}
|
|
|
|
return allocator is not null && allocator.TryAllocateGuestMemory(length, alignment, out memory);
|
|
}
|
|
|
|
private static bool IsSafeSpeakerArrayAddress(ulong value) =>
|
|
IsPlausibleGuestObjectPointer(value) &&
|
|
!IsDirectMemoryWindowAddress(value);
|
|
|
|
// GTA V Enhanced BatchMap fixed dmem VAs observed around 0x1559_xxxx_xxxx.
|
|
// Keep HLE speaker-array objects out of that window.
|
|
private static bool IsDirectMemoryWindowAddress(ulong value) =>
|
|
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
|
|
|
|
// Only sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel still
|
|
// pick an out buffer by host address range; both are left untouched here so they do
|
|
// not collide with the changes already in flight for them, and these two predicates
|
|
// go away with the last of their callers.
|
|
private static bool IsGuestStackAddress(ulong value) =>
|
|
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
|
|
|
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
|
|
{
|
|
if (IsWritableOutBuffer(primary))
|
|
{
|
|
return primary;
|
|
}
|
|
|
|
return IsWritableOutBuffer(secondary) ? secondary : 0;
|
|
}
|
|
|
|
private static bool IsWritableOutBuffer(ulong value) =>
|
|
value != 0 &&
|
|
value != 0x10000UL &&
|
|
value >= 0x1000UL &&
|
|
(IsPlausibleGuestObjectPointer(value) || IsGuestStackAddress(value));
|
|
|
|
private static bool IsPlausibleGuestObjectPointer(ulong value) =>
|
|
value >= 0x1000_0000UL &&
|
|
value != 0x10000UL &&
|
|
value < 0x0000_8000_0000_0000UL;
|
|
|
|
// The caller states how many coefficients its buffer holds, so write exactly
|
|
// uiNumCoefficients floats and never a fixed slab: the buffer is routinely a
|
|
// small stack array with other locals behind it.
|
|
private static int WriteZeroSpeakerArrayCoefficients(
|
|
CpuContext ctx,
|
|
ulong destination,
|
|
uint numCoefficients,
|
|
string label)
|
|
{
|
|
if (destination == 0)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
if (numCoefficients == 0 || numCoefficients > SpeakerArrayMaxChannels)
|
|
{
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidParam);
|
|
}
|
|
|
|
Span<byte> zeros = stackalloc byte[(int)(SpeakerArrayMaxChannels * sizeof(float))];
|
|
zeros = zeros[..(int)(numCoefficients * sizeof(float))];
|
|
zeros.Clear();
|
|
if (!ctx.Memory.TryWrite(destination, zeros))
|
|
{
|
|
TraceAudioOut2($"{label} write-failed dest=0x{destination:X}");
|
|
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
|
|
}
|
|
|
|
TraceAudioOut2($"{label} ok dest=0x{destination:X} count={numCoefficients}");
|
|
return SetReturn(ctx, 0);
|
|
}
|
|
|
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
|
return ctx.Memory.TryWrite(address, buffer);
|
|
}
|
|
|
|
private static int SetReturn(CpuContext ctx, int result)
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
return result;
|
|
}
|
|
|
|
private static void TraceAudioOut2(string message)
|
|
{
|
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
|
|
}
|
|
}
|
|
|
|
private static void TraceSubmitSkipped(ContextState context, int frames, string reason)
|
|
{
|
|
var n = Interlocked.Increment(ref _submitSkipTraceCount);
|
|
if (n <= 8 || n % 500 == 0)
|
|
{
|
|
TraceAudioOut2(
|
|
$"context-submit-skip#{n} handle=0x{context.Handle:X} frames={frames} reason={reason}");
|
|
}
|
|
}
|
|
}
|