mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
fix(audio): harden AudioOut2 stack out-buffer writes against canary smash (#532)
Titles that stack-allocate AudioOut2 outs next to the frame canary were corrupted by oversized or mistyped HLE writes; keep ContextPush pacing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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<ulong, byte> SpeakerArrays = new();
|
||||
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
|
||||
private static readonly ConcurrentDictionary<ulong, int> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<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) &&
|
||||
!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<byte> 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<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<byte> 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<byte> 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<byte> 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<byte> 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]);
|
||||
}
|
||||
}
|
||||
@@ -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<byte> bytes = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
private static ulong ReadU64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private static uint ReadU32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<byte> 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);
|
||||
|
||||
Reference in New Issue
Block a user