Files
sharpemu/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs
T
Miguel Cruz 864cbb0fa0 [AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)
* [Core] Add POSIX native execution and PS5 SELF support

Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases.

* [HLE] Expand PS5 service and media compatibility

Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT.

* [AGC/Vulkan] Extend Gen5 shader and presentation support

Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders.

* [Core] Align static TLS reservation across hosts

* [Pad] Align primary user ID with UserService

* [Gpu] Preserve runtime scalar buffers across renderer seam

* [AGC] Restore omitted command helper exports

* [Vulkan] Reuse primary views for promoted MRT targets

* [Vulkan] Preserve scratch storage bindings in compute dispatches
2026-07-16 02:02:34 +03:00

391 lines
15 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
// 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;
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<ulong, ContextState> Contexts = new();
private sealed class ContextState
{
private readonly object _paceGate = new();
private long _nextAdvanceTimestamp;
public ContextState(uint frequency, uint channels, uint grainSamples)
{
Frequency = frequency == 0 ? 48000 : frequency;
Channels = channels == 0 ? 2 : channels;
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
}
public uint Frequency { get; }
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;
lock (_paceGate)
{
var now = Stopwatch.GetTimestamp();
if (_nextAdvanceTimestamp < now)
{
_nextAdvanceTimestamp = now;
}
delay = _nextAdvanceTimestamp - now;
_nextAdvanceTimestamp += checked(
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
}
if (delay > 0)
{
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
}
}
}
[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;
}
[SysAbiExport(
Nid = "t5YrizufpQc",
ExportName = "sceAudioOut2ContextResetParam",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextResetParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
param.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "pDmme7Bgm6E",
ExportName = "sceAudioOut2ContextQueryMemory",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[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];
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
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;
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
if (ctx.Memory.TryRead(paramAddress, param))
{
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
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)}");
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
Contexts[handle] = new ContextState(frequency, channels, grain);
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
return TryWriteUInt64(ctx, outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "on6ZH7Abo10",
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx)
{
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
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)
{
var handle = ctx[CpuRegister.Rdi];
var traceCount = Interlocked.Increment(ref _pushTraceCount);
if (traceCount <= 16)
{
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}");
}
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();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "PE2zHMqLSHs",
ExportName = "sceAudioOut2ContextAdvance",
Target = Generation.Gen5,
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))
{
context.PaceAdvance();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "R7d0F1g2qsU",
ExportName = "sceAudioOut2ContextGetQueueLevel",
Target = Generation.Gen5,
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)
{
_ = TryWriteUInt64(ctx, levelAddress, 0);
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2PortCreate",
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);
}
[SysAbiExport(
Nid = "8XTArSPyWHk",
ExportName = "sceAudioOut2PortSetAttributes",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortGetState(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
Span<byte> state = stackalloc byte[0x20];
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);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "DImz2Ft9E2g",
ExportName = "sceAudioOut2GetSpeakerInfo",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cd+Rtw+D1x8",
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => 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)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
outUserAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return TryWriteUInt64(ctx, outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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}");
}
}
}