mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
[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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// 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.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AjmExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
|
||||
private static int _nextContextId;
|
||||
public static int AjmInitialize(CpuContext ctx)
|
||||
{
|
||||
var reserved = ctx[CpuRegister.Rdi];
|
||||
var outputAddress = ctx[CpuRegister.Rsi];
|
||||
if (reserved != 0 || outputAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
|
||||
Span<byte> value = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
|
||||
if (!ctx.Memory.TryWrite(outputAddress, value))
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
Contexts[contextId] = 0;
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MHur6qCsUus",
|
||||
ExportName = "sceAjmFinalize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmFinalize(CpuContext ctx)
|
||||
{
|
||||
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Q3dyFuwGn64",
|
||||
ExportName = "sceAjmModuleRegister",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmModuleRegister(CpuContext ctx)
|
||||
{
|
||||
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var reserved = ctx[CpuRegister.Rdx];
|
||||
if (reserved != 0 || !Contexts.ContainsKey(contextId))
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wi7DtlLV+KI",
|
||||
ExportName = "sceAjmModuleUnregister",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmModuleUnregister(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MmpF1XsQiHw",
|
||||
ExportName = "sceAjmBatchInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchInitialize(CpuContext ctx)
|
||||
{
|
||||
// The caller owns and initializes the batch storage. This API resets
|
||||
// its submission cursor on hardware; FMOD does not consume a return
|
||||
// value or an additional output object here.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,70 @@
|
||||
|
||||
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
|
||||
{
|
||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||
// populate.
|
||||
private const int AudioOut2ContextParamSize = 0x30;
|
||||
// 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",
|
||||
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
@@ -52,8 +100,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
||||
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(
|
||||
@@ -88,13 +143,34 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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(
|
||||
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
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",
|
||||
@@ -117,16 +263,23 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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 ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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",
|
||||
@@ -138,7 +291,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle == 0 || stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var type = (int)((handle >> 16) & 0xFF);
|
||||
@@ -151,8 +304,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
@@ -175,8 +328,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -184,14 +337,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
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) => ctx.SetReturn(0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
|
||||
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
|
||||
outUserAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public static class AudioOutExports
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public IHostAudioStream? Backend { get; }
|
||||
public volatile float Volume = 1.0f;
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
|
||||
@@ -198,7 +199,8 @@ public static class AudioOutExports
|
||||
checked((int)port.BufferLength),
|
||||
port.Channels,
|
||||
port.BytesPerSample,
|
||||
port.IsFloat);
|
||||
port.IsFloat,
|
||||
port.Volume);
|
||||
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
|
||||
{
|
||||
port.PaceSilence();
|
||||
@@ -225,10 +227,43 @@ public static class AudioOutExports
|
||||
public static int AudioOutSetVolume(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return ctx.SetReturn(
|
||||
Ports.ContainsKey(handle)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var volumeArrayAddress = ctx[CpuRegister.Rdx];
|
||||
if (!Ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
const int unityVolume = 32768;
|
||||
var maxVolume = 0;
|
||||
var found = false;
|
||||
if (volumeArrayAddress != 0)
|
||||
{
|
||||
Span<byte> raw = stackalloc byte[sizeof(int)];
|
||||
for (var channel = 0; channel < 8; channel++)
|
||||
{
|
||||
if ((channelFlags & (1u << channel)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
|
||||
maxVolume = Math.Max(maxVolume, value);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
public static void ShutdownAllPorts()
|
||||
|
||||
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
bool isFloat,
|
||||
float volume)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
left = ApplyVolume(left, volume);
|
||||
right = ApplyVolume(right, volume);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
|
||||
}
|
||||
@@ -52,4 +55,10 @@ internal static class AudioPcmConversion
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
|
||||
private static short ApplyVolume(short sample, float volume)
|
||||
{
|
||||
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
|
||||
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class FmodCompatExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uPLTdl3psGk",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libfmod")]
|
||||
public static int FmodSystemSetOutput(CpuContext ctx)
|
||||
{
|
||||
var system = ctx[CpuRegister.Rdi];
|
||||
var output = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
|
||||
var resetPrematureInit = callCount <= 2;
|
||||
if (resetPrematureInit && system != 0)
|
||||
{
|
||||
Span<byte> zeroByte = stackalloc byte[1];
|
||||
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
|
||||
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
|
||||
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
|
||||
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
|
||||
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
|
||||
}
|
||||
|
||||
// The PS5 FMOD object arrives with its initialized byte set before Unity has
|
||||
// applied the startup output configuration. Preserve those settings while
|
||||
// clearing the premature marker so Studio can execute the real core init.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user