diff --git a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs index 18b37515..517d1ab0 100644 --- a/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs @@ -15,16 +15,40 @@ public static class AudioOut2Exports // Clearing 0x80 bytes here overwrote the caller's stack canary immediately // following the 0x40-byte parameter block. private const int AudioOut2ContextParamSize = 0x40; - private const int AudioOut2ContextMemorySize = 0x10000; - private const int AudioOut2ContextMemoryAlignment = 0x10000; + // Keep these modest. Some Prospero titles stack-allocate QueryMemory results + // next to the frame canary: a 16-byte {size,align} write to [rbp-0x38] plants + // align at [rbp-0x30] (observed canary=0x100). Size-only (8 bytes) on stack. + private const int AudioOut2ContextMemorySize = 0x4000; + private const int AudioOut2ContextMemoryAlignment = 0x100; + // Exact object body size. Do not page-align to 64K — callers that + // stack-allocate from this size planted 0x10000 on the canary with a 64K VLA. + 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; + private const uint SpeakerArrayMaxChannels = 32; + // Field read by titles at object+0x34 (mov eax,[rbx+0x34]). + private const int SpeakerArrayDivisorFieldOffset = 0x34; + private const int SpeakerArrayResultFieldOffset = 0x3C; + private const uint SpeakerArrayDefaultDivisor = 1; + private const int SpeakerArrayCoefficientBytes = 0x400; + // OrbisAudioOutPortState is 0x20 bytes. Never grow this from r8/r9 — those + // regs arrive polluted with GetSize leftovers (0x840/0x10C/0x180) and caused + // PortGetState/GetSpeakerInfo to overwrite the speaker-array param block + // (param+0x18 == first PortGetState out) and smash the Main Thread canary + // with ContextMemoryAlignment (0x100). + private const int PortStateSize = 0x20; + private const int SpeakerInfoSize = 0x20; + private const ushort PortStateOutputConnectedPrimary = 0x01; private static long _nextContextHandle = 1; private static long _nextUserHandle = 1; private static int _nextPortId; private static long _pushTraceCount; - // Per-context audio parameters captured at ContextCreate so ContextAdvance - // can pace to the real playback cadence (grain samples at the sample rate). + private static readonly ConcurrentDictionary SpeakerArrays = new(); private static readonly ConcurrentDictionary Contexts = new(); + private static readonly ConcurrentDictionary Ports = new(); private sealed class ContextState { @@ -42,9 +66,6 @@ public static class AudioOut2Exports public uint Channels { get; } public uint GrainSamples { get; } - // Blocks the advancing thread until one grain worth of wall-clock time - // has elapsed since the previous advance, matching hardware timing so - // audio-gated titles neither spin nor drift ahead. public void PaceAdvance() { long delay; @@ -112,19 +133,36 @@ public static class AudioOut2Exports public static int AudioOut2ContextQueryMemory(CpuContext ctx) { var paramAddress = ctx[CpuRegister.Rdi]; - var memoryInfoAddress = ctx[CpuRegister.Rsi]; + var memoryInfoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); if (paramAddress == 0 || memoryInfoAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - Span memoryInfo = stackalloc byte[0x20]; + // Heap: {size, alignment} (16 bytes), matching sceAudioPropagationSystemQueryMemory. + // Stack: SIZE ONLY as a full ulong (8 bytes). Writing alignment at +8 is how + // [rbp-0x30] became 0x100 on GTA V Enhanced. Do NOT shrink this to uint32 — + // Main reads the out as a 64-bit size; a 4-byte write leaves a garbage high + // dword (observed 0x7<<32|0x4000) and the allocator aborts with int 0x41. + if (IsGuestStackAddress(memoryInfoAddress)) + { + Span sizeOnly = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(sizeOnly, AudioOut2ContextMemorySize); + TraceAudioOut2( + $"context-query-memory stack-size-only out=0x{memoryInfoAddress:X} " + + $"size=0x{AudioOut2ContextMemorySize:X}"); + return ctx.Memory.TryWrite(memoryInfoAddress, sizeOnly) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + Span memoryInfo = stackalloc byte[0x10]; memoryInfo.Clear(); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize); BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize); - BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment); - + TraceAudioOut2( + $"context-query-memory out=0x{memoryInfoAddress:X} " + + $"size=0x{AudioOut2ContextMemorySize:X} align=0x{AudioOut2ContextMemoryAlignment:X}"); return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo) ? SetReturn(ctx, 0) : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); @@ -146,8 +184,6 @@ public static class AudioOut2Exports return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - // Read channels/frequency/grain from the reset-param blob so the - // context can pace advances to the real audio cadence. uint channels = 2; uint frequency = 48000; uint grain = 256; @@ -159,8 +195,6 @@ public static class AudioOut2Exports var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]); if (pc is > 0 and <= 8) channels = pc; if (pf is >= 8000 and <= 192000) frequency = pf; - // Values below one cache line are flags/counts in observed PS5 - // callers, not audio grains. Keep the hardware-sized default. if (pg is >= 64 and <= 0x4000) grain = pg; TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}"); } @@ -199,17 +233,16 @@ public static class AudioOut2Exports public static int AudioOut2ContextPush(CpuContext ctx) { var handle = ctx[CpuRegister.Rdi]; - var traceCount = Interlocked.Increment(ref _pushTraceCount); - if (traceCount <= 16) + if (Interlocked.Increment(ref _pushTraceCount) <= 8) { - TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}"); + TraceAudioOut2($"context-push handle=0x{handle:X} data=0x{ctx[CpuRegister.Rsi]:X}"); } + // FMOD's PS5 output path uses ContextPush as the submission clock and + // does not call ContextAdvance. Pace pushes to one hardware grain so + // the feeder cannot outrun playback and starve the title. if (Contexts.TryGetValue(handle, out var context)) { - // FMOD's PS5 output path uses ContextPush as the submission clock - // and does not call ContextAdvance. Pace pushes to one hardware - // grain so the feeder cannot outrun playback and starve the game. context.PaceAdvance(); } @@ -223,11 +256,9 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2ContextAdvance(CpuContext ctx) { - // Advancing renders one grain of audio on hardware; pace it to the same - // wall-clock cadence so the guest audio thread runs at the right speed. - if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context)) + if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var state)) { - context.PaceAdvance(); + state.PaceAdvance(); } return SetReturn(ctx, 0); @@ -240,38 +271,35 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2ContextGetQueueLevel(CpuContext ctx) { - // The advance path paces synchronously, so the queue is always drained. - var levelAddress = ctx[CpuRegister.Rsi]; - if (levelAddress != 0) + // ABI out is a 32-bit queue depth (callers compare dword [out]). A + // uint64 write into a stack slot at [rbp-0x14] next to the canary at + // [rbp-0x10] zeroed the canary low half and aborted the audio thread. + var outLevelAddress = ctx[CpuRegister.Rsi]; + if (outLevelAddress == 0) { - _ = TryWriteUInt64(ctx, levelAddress, 0); + outLevelAddress = ctx[CpuRegister.Rdx]; + } + + if (outLevelAddress != 0) + { + Span level = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(level, 0); + if (!ctx.Memory.TryWrite(outLevelAddress, level)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } } return SetReturn(ctx, 0); } [SysAbiExport( - Nid = "JK2wamZPzwM", - ExportName = "sceAudioOut2PortCreate", + Nid = "Q8DZkKQ-SYc", + ExportName = "sceAudioOut2LoContextGetQueueLevel", Target = Generation.Gen5, LibraryName = "libSceAudioOut2")] - public static int AudioOut2PortCreate(CpuContext ctx) - { - var type = unchecked((int)ctx[CpuRegister.Rdi]); - var paramAddress = ctx[CpuRegister.Rsi]; - var outPortAddress = ctx[CpuRegister.Rdx]; - var contextAddress = ctx[CpuRegister.Rcx]; - if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0) - { - return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); - } - - var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF; - var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId; - return TryWriteUInt64(ctx, outPortAddress, handle) - ? SetReturn(ctx, 0) - : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); - } + public static int AudioOut2LoContextGetQueueLevel(CpuContext ctx) => + AudioOut2ContextGetQueueLevel(ctx); [SysAbiExport( Nid = "8XTArSPyWHk", @@ -280,6 +308,39 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0); + [SysAbiExport( + Nid = "JK2wamZPzwM", + ExportName = "sceAudioOut2PortCreate", + Target = Generation.Gen5, + LibraryName = "libSceAudioOut2")] + public static int AudioOut2PortCreate(CpuContext ctx) + { + // rdi=user/context, rsi=type, rdx=outPort* (fallback rcx if rdx unusable). + var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]); + if (outPortAddress == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var type = unchecked((int)ctx[CpuRegister.Rsi]); + if (type is < 0 or > 0x100) + { + type = 0; + } + + var portId = (uint)Interlocked.Increment(ref _nextPortId); + var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId; + Ports[handle] = type; + if (!TryWriteUInt64(ctx, outPortAddress, handle)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAudioOut2($"port-create handle=0x{handle:X} type={type} out=0x{outPortAddress:X}"); + return SetReturn(ctx, 0); + } + + // Fixed-size connected stereo state. Do not trust r8/r9 for byte counts. [SysAbiExport( Nid = "gatEUKG+Ea4", ExportName = "sceAudioOut2PortGetState", @@ -287,27 +348,44 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2PortGetState(CpuContext ctx) { - var handle = ctx[CpuRegister.Rdi]; - var stateAddress = ctx[CpuRegister.Rsi]; - if (handle == 0 || stateAddress == 0) + var portHandle = ctx[CpuRegister.Rdi]; + var stateAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]); + if (stateAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - var type = (int)((handle >> 16) & 0xFF); - Span state = stackalloc byte[0x20]; + // Stack out-buffers with garbage handles were writing 0x20 bytes over + // caller frames / canaries (state=0x7FFFDE1FF688 right before fail). + // Heap outs still get a real state blob even when the handle wasn't + // minted by PortCreate — some titles synthesize port ids themselves. + if (IsGuestStackAddress(stateAddress)) + { + TraceAudioOut2( + $"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}"); + return SetReturn(ctx, 0); + } + + Span state = stackalloc byte[PortStateSize]; state.Clear(); - var output = type == 2 ? 0x40 : 0x01; - var channels = type == 2 ? 1 : 2; - BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output)); - state[0x02] = unchecked((byte)channels); + // +0x00 u16 output = CONNECTED_PRIMARY (1) + // +0x02 u8 channels = 2 + // +0x04 s16 volume = -1 (N/A for main) + BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary); + state[0x02] = 2; BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1); - return ctx.Memory.TryWrite(stateAddress, state) - ? SetReturn(ctx, 0) - : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + if (!ctx.Memory.TryWrite(stateAddress, state)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAudioOut2( + $"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}"); + return SetReturn(ctx, 0); } + // rdi=out buffer, rsi=type/flag (not a pointer). Fixed-size write only. [SysAbiExport( Nid = "DImz2Ft9E2g", ExportName = "sceAudioOut2GetSpeakerInfo", @@ -315,21 +393,134 @@ public static class AudioOut2Exports LibraryName = "libSceAudioOut2")] public static int AudioOut2GetSpeakerInfo(CpuContext ctx) { - var infoAddress = ctx[CpuRegister.Rdi]; + var infoAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdi], ctx[CpuRegister.Rdx]); if (infoAddress == 0) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - Span info = stackalloc byte[0x40]; - info.Clear(); - BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1); - BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2); - BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000); + // Same rule as PortGetState — never bulk-write speaker info onto the stack. + if (IsGuestStackAddress(infoAddress)) + { + TraceAudioOut2($"get-speaker-info skip-stack out=0x{infoAddress:X}"); + return SetReturn(ctx, 0); + } - return ctx.Memory.TryWrite(infoAddress, info) - ? SetReturn(ctx, 0) - : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + Span info = stackalloc byte[SpeakerInfoSize]; + info.Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 2); + BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 48000); + BinaryPrimitives.WriteUInt16LittleEndian(info[0x08..], PortStateOutputConnectedPrimary); + + if (!ctx.Memory.TryWrite(infoAddress, info)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAudioOut2( + $"get-speaker-info out=0x{infoAddress:X} type=0x{ctx[CpuRegister.Rsi]:X} bytes=0x{SpeakerInfoSize:X}"); + return SetReturn(ctx, 0); + } + + // Matches sceAudio3dGetSpeakerArrayMemorySize(uiNumSpeakers, bIs3d): size is + // returned directly in rax. Exact channel-scaled body — never 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 rdi=0x{ctx[CpuRegister.Rdi]:X} " + + $"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} -> 0x{size:X}"); + ctx[CpuRegister.Rax] = unchecked((ulong)size); + return size; + } + + [SysAbiExport( + Nid = "4BlZurolOAo", + ExportName = "sceAudioOut2GetSpeakerArrayCoefficients", + Target = Generation.Gen5, + LibraryName = "libSceAudioOut2")] + public static int AudioOut2GetSpeakerArrayCoefficients(CpuContext ctx) => + WriteZeroSpeakerArrayCoefficients(ctx, "coefficients"); + + [SysAbiExport( + Nid = "28QqMnuuJ9Y", + ExportName = "sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients", + Target = Generation.Gen5, + LibraryName = "libSceAudioOut2")] + public static int AudioOut2GetSpeakerArrayAmbisonicsCoefficients(CpuContext ctx) => + WriteZeroSpeakerArrayCoefficients(ctx, "ambisonics-coefficients"); + + // rdi = param (may share a heap slab with PortGetState/GetSpeakerInfo outs — + // do NOT read buffer*/size* from it). rsi = &outHandle, rdx = reserved/size + // slot (leave alone), rcx = channels. Always heap-allocate a fresh object. + [SysAbiExport( + Nid = "+k91hoTuoA8", + ExportName = "sceAudioOut2SpeakerArrayCreate", + Target = Generation.Gen5, + LibraryName = "libSceAudioOut2")] + public static int AudioOut2SpeakerArrayCreate(CpuContext ctx) + { + var param = ctx[CpuRegister.Rdi]; + var outHandleAddress = ctx[CpuRegister.Rsi]; + var outReservedAddress = ctx[CpuRegister.Rdx]; + var channels = (uint)ctx[CpuRegister.Rcx]; + if (channels == 0 || channels > SpeakerArrayMaxChannels) + { + channels = SpeakerArrayDefaultChannels; + } + + if (outHandleAddress == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var bytes = ComputeSpeakerArrayBytes(channels); + if (!TryAllocateSpeakerArrayMemory(ctx, (ulong)bytes, out var memory) || + !InitializeSpeakerArrayObject(ctx, memory, channels)) + { + TraceAudioOut2( + $"speaker-array-create alloc-failed bytes=0x{bytes:X} channels={channels}"); + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + SpeakerArrays[memory] = 0; + // Publish ONLY the out-handle slot. rdx is often an adjacent size / + // reserved local on the caller stack — writing it previously fed canary + // corruption. + if (!TryWriteUInt64(ctx, outHandleAddress, memory)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAudioOut2( + $"speaker-array-create object=0x{memory:X} bytes=0x{bytes:X} " + + $"channels={channels} param=0x{param:X} out=0x{outHandleAddress:X} " + + $"reserved=0x{outReservedAddress:X} (untouched)"); + + ctx[CpuRegister.Rax] = memory; + return 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( @@ -337,7 +528,11 @@ public static class AudioOut2Exports ExportName = "sceAudioOut2PortDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioOut2")] - public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0); + public static int AudioOut2PortDestroy(CpuContext ctx) + { + Ports.TryRemove(ctx[CpuRegister.Rdi], out _); + return SetReturn(ctx, 0); + } [SysAbiExport( Nid = "IaZXJ9M79uo", @@ -367,6 +562,129 @@ public static class AudioOut2Exports : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + 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 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) && + !IsGuestStackAddress(value) && + !IsDirectMemoryWindowAddress(value); + + // BatchMap fixed dmem VAs have been 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; + + private static bool IsPlausibleGuestObjectPointer(ulong value) => + value >= 0x1000_0000UL && + value != 0x10000UL && + value < 0x0000_8000_0000_0000UL; + + // Windows user stacks sit in 0x00007FFFxxxxxxxx. Never treat those as + // heap objects we can bulk-initialize. + private static bool IsGuestStackAddress(ulong value) => + value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL; + + private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary) + { + // Accept heap or stack out-buffers (PortGetState legitimately uses both), + // but never small integers / size constants. + if (IsWritableOutBuffer(primary)) + { + return primary; + } + + if (IsWritableOutBuffer(secondary)) + { + return secondary; + } + + return 0; + } + + private static bool IsWritableOutBuffer(ulong value) => + value != 0 && + value != 0x10000UL && + value >= 0x1000UL && + (IsPlausibleGuestObjectPointer(value) || IsGuestStackAddress(value)); + + private static int WriteZeroSpeakerArrayCoefficients(CpuContext ctx, string label) + { + var destination = ctx[CpuRegister.Rsi]; + if (destination == 0) + { + destination = ctx[CpuRegister.Rdx]; + } + + // Coefficients are large — only wipe real heap objects, never stack. + if (destination != 0 && + IsPlausibleGuestObjectPointer(destination) && + !IsGuestStackAddress(destination)) + { + Span zeros = stackalloc byte[SpeakerArrayCoefficientBytes]; + zeros.Clear(); + if (!ctx.Memory.TryWrite(destination, zeros)) + { + TraceAudioOut2($"{label} write-failed dest=0x{destination:X}"); + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + } + + TraceAudioOut2($"{label} ok dest=0x{destination:X}"); + return SetReturn(ctx, 0); + } + private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value) { Span buffer = stackalloc byte[sizeof(ulong)]; diff --git a/src/SharpEmu.Libs/Audio/AudioOutExports.cs b/src/SharpEmu.Libs/Audio/AudioOutExports.cs index ceee3fb0..db9fc70a 100644 --- a/src/SharpEmu.Libs/Audio/AudioOutExports.cs +++ b/src/SharpEmu.Libs/Audio/AudioOutExports.cs @@ -175,6 +175,14 @@ public static class AudioOutExports return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } + // Same rule as AudioOut2 PortGetState: never bulk-write onto the caller + // stack. Some titles place small locals next to the canary; a full + // SceAudioOutPortState write smashes it. + if (IsGuestStackAddress(stateAddress)) + { + return ctx.SetReturn(0); + } + // SceAudioOutPortState: report a connected primary output at full volume // so pacing/mixing code sees a live port. We do no host rerouting, so // rerouteCounter and flag stay zero. @@ -192,6 +200,9 @@ public static class AudioOutExports return ctx.SetReturn(0); } + private static bool IsGuestStackAddress(ulong value) => + value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL; + [SysAbiExport( Nid = "QOQtbeDqsT4", ExportName = "sceAudioOutOutput", diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs new file mode 100644 index 00000000..a09b0658 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2PortGetStateExportsTests.cs @@ -0,0 +1,86 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2PortGetStateExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong StateAddress = MemoryBase + 0x100; + + private static CpuContext CreateContext(out FakeCpuMemory memory) + { + memory = new FakeCpuMemory(MemoryBase, 0x1000); + return new CpuContext(memory, Generation.Gen5); + } + + [Fact] + public void PortGetState_WritesFixedSizeIgnoringPollutedR9() + { + var ctx = CreateContext(out var memory); + // Paint the buffer so we can see the write footprint. + Span paint = stackalloc byte[0x100]; + paint.Fill(0xAB); + Assert.True(memory.TryWrite(StateAddress, paint)); + + ctx[CpuRegister.Rdi] = 0xDE1FF6800001UL; + ctx[CpuRegister.Rsi] = StateAddress; + ctx[CpuRegister.Rdx] = StateAddress + 0x200; + // Polluted GetSize leftover — must NOT enlarge the write. + ctx[CpuRegister.R9] = 0x180; + + var result = AudioOut2Exports.AudioOut2PortGetState(ctx); + + Assert.Equal(0, result); + Span state = stackalloc byte[0x100]; + Assert.True(memory.TryRead(StateAddress, state)); + Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(state)); + Assert.Equal(2, state[2]); + // Bytes past the fixed 0x20 header must remain untouched. + Assert.Equal(0xAB, state[0x20]); + Assert.Equal(0xAB, state[0x7F]); + } + + [Fact] + public void PortGetState_SkipsGuestStackOutBuffer() + { + var ctx = CreateContext(out _); + const ulong stackOut = 0x00007FFFDE1FF688UL; + ctx[CpuRegister.Rdi] = 0xDE1FF688004DUL; + ctx[CpuRegister.Rsi] = stackOut; + ctx[CpuRegister.Rdx] = 0; + + var result = AudioOut2Exports.AudioOut2PortGetState(ctx); + + Assert.Equal(0, result); + } + + [Fact] + public void GetSpeakerInfo_WritesFixedSizeToRdiNotRsiTypeFlag() + { + var ctx = CreateContext(out var memory); + Span paint = stackalloc byte[0x80]; + paint.Fill(0xCD); + Assert.True(memory.TryWrite(StateAddress, paint)); + + ctx[CpuRegister.Rdi] = StateAddress; + ctx[CpuRegister.Rsi] = 1; + ctx[CpuRegister.Rdx] = StateAddress + 0x200; + ctx[CpuRegister.R8] = 0x840; + ctx[CpuRegister.R9] = 0x10C; + + var result = AudioOut2Exports.AudioOut2GetSpeakerInfo(ctx); + + Assert.Equal(0, result); + Span info = stackalloc byte[0x80]; + Assert.True(memory.TryRead(StateAddress, info)); + Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(info)); + Assert.Equal(48000u, BinaryPrimitives.ReadUInt32LittleEndian(info[4..])); + Assert.Equal(0xCD, info[0x20]); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs new file mode 100644 index 00000000..8ada9051 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Audio/AudioOut2SpeakerArrayExportsTests.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Audio; +using Xunit; + +namespace SharpEmu.Libs.Tests.Audio; + +public sealed class AudioOut2SpeakerArrayExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong OutHandleAddress = MemoryBase + 0x100; + private const ulong ReservedAddress = MemoryBase + 0x120; + private const ulong ParamAddress = MemoryBase + 0x200; + private const ulong SpeakerMemoryAddress = MemoryBase + 0x400; + + private static CpuContext CreateContext(out FakeCpuMemory memory) + { + memory = new FakeCpuMemory(MemoryBase, 0x2000); + return new CpuContext(memory, Generation.Gen5); + } + + private static void WriteU64(FakeCpuMemory memory, ulong address, ulong value) + { + Span bytes = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } + + private static ulong ReadU64(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[8]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + private static uint ReadU32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[4]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + [Fact] + public void GetSpeakerArrayMemorySize_NeverReturnsTheNotFoundSentinel() + { + var ctx = CreateContext(out _); + ctx[CpuRegister.Rdi] = 8; + + var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx); + + Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + Assert.Equal(0x40 + 8 * 0x100 + 0x400, result); + Assert.Equal((ulong)result, ctx[CpuRegister.Rax]); + Assert.True(result < 0x10000); + } + + [Fact] + public void GetSpeakerArrayMemorySize_TwoChannelsIsExactChannelScaledSize() + { + var ctx = CreateContext(out _); + ctx[CpuRegister.Rdi] = 2; + + var result = AudioOut2Exports.AudioOut2GetSpeakerArrayMemorySize(ctx); + + Assert.Equal(0x40 + 2 * 0x100 + 0x400, result); + Assert.Equal(0x640UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void SpeakerArrayCreate_PublishesObjectPointerAndLeavesReservedSizeAlone() + { + var ctx = CreateContext(out var memory); + // Stage a size in the reserved slot the way callers do before Create. + WriteU64(memory, ReservedAddress, 0x100); + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = OutHandleAddress; + ctx[CpuRegister.Rdx] = ReservedAddress; + ctx[CpuRegister.Rcx] = 2; + + var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx); + + Assert.Equal(0, result); + Assert.NotEqual(0UL, ctx[CpuRegister.Rax]); + Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]); + Assert.Equal(ctx[CpuRegister.Rax], ReadU64(memory, OutHandleAddress)); + // Reserved/size slot must remain untouched — writing it corrupted canaries. + Assert.Equal(0x100UL, ReadU64(memory, ReservedAddress)); + } + + [Fact] + public void SpeakerArrayCreate_PublishesHandleForTypicalCallShape() + { + var ctx = CreateContext(out _); + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = OutHandleAddress; + ctx[CpuRegister.Rdx] = ReservedAddress; + ctx[CpuRegister.Rcx] = 2; + + var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx); + + Assert.Equal(0, result); + Assert.NotEqual(0UL, ctx[CpuRegister.Rax]); + Assert.NotEqual(0x10000UL, ctx[CpuRegister.Rax]); + Assert.NotEqual((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result); + } + + [Fact] + public void SpeakerArrayCreate_IgnoresCorruptedParamBufferFields() + { + var ctx = CreateContext(out var memory); + // Simulate PortGetState having overwritten param+0x18 (size) with a + // state blob — Create must NOT adopt that as an in-place buffer. + WriteU64(memory, ParamAddress + 0x10, SpeakerMemoryAddress); + WriteU64(memory, ParamAddress + 0x18, 0x100); + ctx[CpuRegister.Rdi] = ParamAddress; + ctx[CpuRegister.Rsi] = OutHandleAddress; + ctx[CpuRegister.Rdx] = ReservedAddress; + ctx[CpuRegister.Rcx] = 2; + + var result = AudioOut2Exports.AudioOut2SpeakerArrayCreate(ctx); + + Assert.Equal(0, result); + Assert.NotEqual(SpeakerMemoryAddress, ctx[CpuRegister.Rax]); + Assert.NotEqual(0x100UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void SpeakerArrayDestroy_UnknownHandleStillSucceeds() + { + var ctx = CreateContext(out _); + ctx[CpuRegister.Rdi] = 0xDEAD_BEEF; + + var result = AudioOut2Exports.AudioOut2SpeakerArrayDestroy(ctx); + + Assert.Equal(0, result); + } +} diff --git a/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs index defe7f78..c016a46d 100644 --- a/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs +++ b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs @@ -7,15 +7,18 @@ namespace SharpEmu.Libs.Tests; // A single contiguous guest region backed by a byte[]. Enough to hand C strings and small // structures to HLE exports under test without a live guest. -internal sealed class FakeCpuMemory : ICpuMemory +internal sealed class FakeCpuMemory : ICpuMemory, IGuestMemoryAllocator { private readonly ulong _base; private readonly byte[] _storage; + private ulong _allocBump; public FakeCpuMemory(ulong baseAddress, int size) { _base = baseAddress; _storage = new byte[size]; + // Bump from the top so test fixtures at low offsets stay intact. + _allocBump = baseAddress + (ulong)size; } public bool TryRead(ulong virtualAddress, Span destination) @@ -40,6 +43,33 @@ internal sealed class FakeCpuMemory : ICpuMemory return true; } + public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address) + { + address = 0; + if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0) + { + return false; + } + + var alignedSize = (size + alignment - 1) & ~(alignment - 1); + if (alignedSize > _allocBump - _base) + { + return false; + } + + var next = (_allocBump - alignedSize) & ~(alignment - 1); + if (next < _base) + { + return false; + } + + _allocBump = next; + address = next; + return true; + } + + public bool TryFreeGuestMemory(ulong address) => false; + public ulong WriteCString(ulong virtualAddress, string text) { var bytes = System.Text.Encoding.UTF8.GetBytes(text);