mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
e13cb28267
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>
141 lines
4.9 KiB
C#
141 lines
4.9 KiB
C#
// 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);
|
|
}
|
|
}
|