feat: added a lot of tests

This commit is contained in:
Ran-j
2026-09-02 18:06:13 -03:00
parent c8c86661fa
commit 2a61ba0df3
12 changed files with 1472 additions and 97 deletions
+29
View File
@@ -180,6 +180,20 @@ void register_code_generator_tests()
"the registration source must use the same unambiguous stub header");
});
tc.Run("unsigned integer loads use explicit zero extension", [](TestCase &t) {
CodeGenerator gen({}, {});
const std::string lbu = gen.translateInstruction(makeIType(0x8F10, OPCODE_LBU, 1, 2, 0x10));
const std::string lhu = gen.translateInstruction(makeIType(0x8F14, OPCODE_LHU, 1, 3, 0x12));
const std::string lwu = gen.translateInstruction(makeIType(0x8F18, OPCODE_LWU, 1, 4, 0x14));
t.IsTrue(lbu.find("SET_GPR_ZE32(ctx, 2") != std::string::npos,
"LBU must zero-extend into the low 64-bit scalar lane");
t.IsTrue(lhu.find("SET_GPR_ZE32(ctx, 3") != std::string::npos,
"LHU must zero-extend into the low 64-bit scalar lane");
t.IsTrue(lwu.find("SET_GPR_ZE32(ctx, 4") != std::string::npos,
"LWU must not sign-extend bit 31 into the allocator bitmap value");
});
tc.Run("SYSCALL publishes its continuation before entering the runtime", [](TestCase &t) {
Function func;
func.name = "syscall_resume";
@@ -306,6 +320,21 @@ void register_code_generator_tests()
"constant MMIO SW should not go through WRITE32 address classification");
});
tc.Run("stale MMIO annotation does not replace the guest effective address", [](TestCase &t) {
Instruction store = makeSw(0x1100, 2, 1, 0);
store.isMmio = true;
store.mmioAddress = 0x10000000u; // A stale analyzer hint; $at still owns the real address.
CodeGenerator gen({}, {});
const std::string generated = gen.translateInstruction(store);
printGeneratedCode("stale MMIO annotation does not replace the guest effective address", generated);
t.IsTrue(generated.find("runtime->Store32(rdram, ctx, ADD32(GPR_U32(ctx, 1), 0), GPR_U32(ctx, 2))") != std::string::npos,
"MMIO annotations should select runtime access without hard-coding a possibly stale address");
t.IsTrue(generated.find("0x10000000u") == std::string::npos,
"stale MMIO address should not replace the address calculated by guest registers");
});
tc.Run("constant RDRAM load and store emit fast memory access", [](TestCase &t) {
Function func;
func.name = "rdram_access";
+2
View File
@@ -72,6 +72,8 @@ void register_elf_analyzer_tests()
"libdma memclr should resolve to a runtime stub");
t.IsTrue(FunctionClassifier::hasRuntimeHandler("__divdi3"),
"libgcc 64-bit division should resolve to a runtime stub");
t.IsFalse(FunctionClassifier::hasRuntimeHandler("GetRomName"),
"ABI-incompatible GetRomName variants must be recompiled instead of name-stubbed");
t.IsFalse(FunctionClassifier::hasRuntimeHandler("__sbprintf"),
"optional stdio internals should not resolve as automatic runtime stubs");
t.IsFalse(FunctionClassifier::hasRuntimeHandler("__sprint"),
+105 -20
View File
@@ -1885,7 +1885,8 @@ void register_ps2_gs_tests()
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 55);
(1ull << 55) |
(1ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_TRIANGLE) |
(1ull << 4);
@@ -2590,7 +2591,7 @@ void register_ps2_gs_tests()
}
});
tc.Run("GS T4 CSM1 lookup matches Veronica ClutCopy layout", [](TestCase &t)
tc.Run("GS T4 CSM1 CLUT cache survives source VRAM reuse", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
@@ -2612,7 +2613,10 @@ void register_ps2_gs_tests()
(1ull << 34) |
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51);
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(2ull << 61); // CLD=2: load and remember CBP0
constexpr uint64_t kTex0LoadIfCbp0Changed =
(kTex0 & ~(7ull << 61)) | (4ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
(1ull << 4) | // TME
@@ -2624,8 +2628,7 @@ void register_ps2_gs_tests()
const uint32_t texByteOff = texNibbleAddr >> 1;
vram[texByteOff] = static_cast<uint8_t>((vram[texByteOff] & 0xF0u) | 0x08u);
// Veronica uploads CSM1 CLUT rows with a 64-pixel GS stride, so logical entry 8
// resolves to row 1, column 0 after the CSM1 swizzle.
// CSM1 stores logical entry 8 at row 1, column 0 after the CLUT swizzle.
const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 8u, 0u);
const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 1u);
std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor));
@@ -2638,6 +2641,13 @@ void register_ps2_gs_tests()
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
gs.writeRegister(GS_REG_ALPHA_1, 0ull);
gs.writeRegister(GS_REG_TEX0_1, kTex0);
// TEX0 loads the palette into the GS CLUT temporary buffer. The
// source VRAM can subsequently be reused without changing the
// palette seen by this draw.
std::memcpy(vram.data() + expectedClutOff, &kWrongColor, sizeof(kWrongColor));
gs.writeRegister(GS_REG_TEX0_1, kTex0LoadIfCbp0Changed);
gs.writeRegister(GS_REG_PRIM, kPrim);
gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull);
gs.writeRegister(GS_REG_UV, 0ull);
@@ -2648,7 +2658,69 @@ void register_ps2_gs_tests()
uint32_t pixel = 0u;
std::memcpy(&pixel, vram.data(), sizeof(pixel));
t.Equals(pixel, kExpectedColor,
"T4 CSM1 lookup should follow Veronica's swizzled CLUT row layout for logical index 8");
"T4 CSM1 lookup should keep the cached palette after its source VRAM is reused");
});
tc.Run("GS texture page buffer hides local-memory writes until TEXFLUSH", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
constexpr uint32_t kTextureTbp = 32u; // Physical GS page 1.
constexpr uint64_t kFrame =
(0ull << 0) |
(1ull << 16) |
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
constexpr uint64_t kZbuf = (1ull << 32);
constexpr uint64_t kScissor = 2ull << 16;
constexpr uint64_t kTex0 =
(static_cast<uint64_t>(kTextureTbp) << 0) |
(1ull << 14) |
(static_cast<uint64_t>(GS_PSM_CT32) << 20) |
(1ull << 34) |
(1ull << 35);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
(1ull << 4) | // TME
(1ull << 8); // FST
constexpr uint32_t kInitialColor = 0x80112233u;
constexpr uint32_t kUpdatedColor = 0x80445566u;
gs.WriteVram(GS_PSM_CT32, kTextureTbp, 1u, 0u, 0u, kInitialColor);
gs.writeRegister(GS_REG_FRAME_1, kFrame);
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
gs.writeRegister(GS_REG_SCISSOR_1, kScissor);
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
gs.writeRegister(GS_REG_ALPHA_1, 0ull);
gs.writeRegister(GS_REG_TEX0_1, kTex0);
gs.writeRegister(GS_REG_PRIM, kPrim);
gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull);
const auto drawPixel = [&gs](uint32_t x)
{
const uint64_t xy0 = static_cast<uint64_t>(x * 16u);
const uint64_t xy1 = static_cast<uint64_t>((x + 1u) * 16u) |
(static_cast<uint64_t>(16u) << 16u);
gs.writeRegister(GS_REG_UV, 0ull);
gs.writeRegister(GS_REG_XYZ2, xy0);
gs.writeRegister(GS_REG_UV, 0ull);
gs.writeRegister(GS_REG_XYZ2, xy1);
};
drawPixel(0u); // Fills the hardware texture page buffer.
gs.WriteVram(GS_PSM_CT32, kTextureTbp, 1u, 0u, 0u, kUpdatedColor);
drawPixel(1u);
gs.writeRegister(GS_REG_TEXFLUSH, 0ull);
drawPixel(2u);
t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 0u, 0u), kInitialColor,
"the first sample should read the original texture page");
t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 1u, 0u), kInitialColor,
"writes to local memory must remain hidden by the cached texture page");
t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 2u, 0u), kUpdatedColor,
"TEXFLUSH must make the updated local-memory page visible to texture reads");
});
tc.Run("GS T8 CT32-uploaded CSM1 CLUT follows swizzled palette layout", [](TestCase &t)
@@ -2673,7 +2745,8 @@ void register_ps2_gs_tests()
(1ull << 34) |
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51);
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
(1ull << 4) | // TME
@@ -2756,7 +2829,8 @@ void register_ps2_gs_tests()
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(17ull << 56);
(17ull << 56) |
(1ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
(1ull << 4) |
@@ -2817,7 +2891,8 @@ void register_ps2_gs_tests()
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT16) << 51) |
(16ull << 56);
(16ull << 56) |
(1ull << 61);
constexpr uint64_t kTexa = (0x80ull << 32);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
@@ -2829,10 +2904,14 @@ void register_ps2_gs_tests()
writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 1u);
// CSA=16 selects the upper half of a CT16 CLUT. CSM1 swaps bits
// 3 and 4 but must preserve address bit 8.
// CSA selects the destination in the temporary buffer, not a
// different source coordinate. Seed the lower half first, then
// replace the same source palette before loading CSA=16.
gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kWrongGreen);
gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 16u, kExpectedRed);
const uint64_t kTex0Lower = kTex0 & ~(0x1Full << 56);
gs.writeRegister(GS_REG_TEX0_1, kTex0Lower);
gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kExpectedRed);
gs.writeRegister(GS_REG_TEXFLUSH, 0ull);
gs.writeRegister(GS_REG_FRAME_1, kFrameReg);
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
@@ -2951,12 +3030,14 @@ void register_ps2_gs_tests()
(1ull << 35) |
(static_cast<uint64_t>(kWrongClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 55);
(1ull << 55) |
(1ull << 61);
constexpr uint64_t kTex2 =
(static_cast<uint64_t>(GS_PSM_T8) << 20) |
(static_cast<uint64_t>(kExpectedClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 55);
(1ull << 55) |
(1ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_SPRITE) |
(1ull << 4) |
@@ -3016,7 +3097,8 @@ void register_ps2_gs_tests()
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 55);
(1ull << 55) |
(1ull << 61);
constexpr uint64_t kTexClut =
(1ull << 0) |
(3ull << 6) |
@@ -3032,7 +3114,7 @@ void register_ps2_gs_tests()
vram[texOff] = 0u;
const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 0u);
const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 3u, 2u);
const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 48u, 2u);
std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor));
std::memcpy(vram.data() + expectedClutOff, &kExpectedColor, sizeof(kExpectedColor));
@@ -3042,8 +3124,8 @@ void register_ps2_gs_tests()
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
gs.writeRegister(GS_REG_ALPHA_1, 0ull);
gs.writeRegister(GS_REG_TEX0_1, kTex0);
gs.writeRegister(GS_REG_TEXCLUT, kTexClut);
gs.writeRegister(GS_REG_TEX0_1, kTex0);
gs.writeRegister(GS_REG_PRIM, kPrim);
gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull);
gs.writeRegister(GS_REG_UV, 0ull);
@@ -3349,7 +3431,8 @@ void register_ps2_gs_tests()
(1ull << 34) |
(1ull << 35) |
(static_cast<uint64_t>(kClutCbp) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51);
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 61);
constexpr uint64_t kPrim =
static_cast<uint64_t>(GS_PRIM_TRIANGLE) |
(1ull << 4) |
@@ -4369,7 +4452,8 @@ void register_ps2_gs_tests()
(1ull << 34) |
(1ull << 35) |
(static_cast<uint64_t>(kClutCbpA) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51);
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 61);
gs.writeRegister(GS_REG_FRAME_1, kFrameReg);
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
@@ -4399,7 +4483,8 @@ void register_ps2_gs_tests()
(1ull << 34) |
(1ull << 35) |
(static_cast<uint64_t>(kClutCbpB) << 37) |
(static_cast<uint64_t>(GS_PSM_CT32) << 51);
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
(1ull << 61);
gs.writeRegister(GS_REG_TEX0_1, kTex0HH);
gs.writeRegister(GS_REG_UV, 0ull);
+109 -10
View File
@@ -1,5 +1,6 @@
#include "MiniTest.h"
#include "ps2x/iop/iop_subsystem.h"
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
#include <array>
@@ -39,7 +40,7 @@ namespace
{
public:
explicit FakeIopHost(size_t memorySize = 0x10000u)
: memory(memorySize, 0u)
: memory(memorySize, 0u), iopMemory(0x00200000u, 0u)
{
}
@@ -85,6 +86,52 @@ namespace
return normalized < memory.size();
}
bool readIopMemory(uint32_t address, void *destination, size_t size) const override
{
uint32_t normalized = 0u;
if ((!destination && size != 0u) || !normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
if (size != 0u)
std::memcpy(destination, iopMemory.data() + normalized, size);
return true;
}
bool writeIopMemory(uint32_t address, const void *source, size_t size) override
{
uint32_t normalized = 0u;
if ((!source && size != 0u) || !normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
if (size != 0u)
std::memcpy(iopMemory.data() + normalized, source, size);
return true;
}
bool zeroIopMemory(uint32_t address, size_t size) override
{
uint32_t normalized = 0u;
if (!normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
std::fill(iopMemory.begin() + normalized, iopMemory.begin() + normalized + size, 0u);
return true;
}
bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const override
{
const bool physical = address < 0x00200000u;
const bool cached = address >= 0x80000000u && address < 0x80200000u;
const bool uncached = address >= 0xA0000000u && address < 0xA0200000u;
if (!physical && !cached && !uncached)
{
normalized = 0u;
return false;
}
normalized = address & 0x1FFFFFFFu;
return normalized < iopMemory.size();
}
uint32_t allocateIopHandle(IopHandleKind kind) override
{
const uint32_t value = nextHandle;
@@ -275,6 +322,7 @@ namespace
}
std::vector<uint8_t> memory;
std::vector<uint8_t> iopMemory;
uint32_t nextHandle = 0x8000u;
uint32_t nextGuestAddress = 0x4000u;
std::vector<uint32_t> guestAllocations;
@@ -347,6 +395,57 @@ void register_ps2_iop_tests()
{
MiniTest::Case("PS2IopSubsystem", [](TestCase &tc)
{
tc.Run("PS2 path parsing is shared and normalizes ISO/module names", [](TestCase &t)
{
const ps2x::iop::ParsedPs2Path cd = ps2x::iop::parsePs2Path("CDROM0:\\MODULES\\LIBSD.IRX;1");
t.Equals(cd.device, ps2x::iop::Ps2PathDevice::Cdrom,
"device names should be case-insensitive");
t.Equals(cd.path, std::string("MODULES/LIBSD.IRX"),
"separators and ISO version suffixes should normalize once");
t.Equals(ps2x::iop::ps2PathLeafKey(cd), std::string("libsd"),
"module lookup should use a normalized IRX leaf key");
const ps2x::iop::ParsedPs2Path rom = ps2x::iop::parsePs2Path("rom0:ROMVER");
t.Equals(rom.device, ps2x::iop::Ps2PathDevice::Rom0,
"ROM0 should remain a distinct virtual device");
t.IsFalse(static_cast<bool>(ps2x::iop::parsePs2Path("unknown0:file.irx")),
"unsupported devices must not fall through to cdrom0");
});
tc.Run("HLE services activate only after a recognized module load", [](TestCase &t)
{
FakeIopHost host;
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"unmatched.elf", 0x100000u, 0u}, &error),
"core-only IOP configuration should succeed");
t.IsFalse(subsystem.canBindRpc(0x80000701u),
"LIBSD RPC must not exist before LIBSD is loaded");
const ps2x::iop::ModuleLoadResult unknown = subsystem.loadModule("rom0:NOT_A_REAL_MODULE");
t.IsTrue(unknown.handled, "the module manager should return a real load result");
t.IsTrue(unknown.moduleId < 0, "unknown ROM modules must fail instead of receiving fake IDs");
const ps2x::iop::ModuleLoadResult loaded = subsystem.loadModule("rom0:LIBSD");
t.IsTrue(loaded.moduleId > 0, "a registered no-BIOS HLE module should load");
t.IsTrue(subsystem.canBindRpc(0x80000701u),
"loading LIBSD should activate its HLE RPC endpoint");
ps2x::iop::RpcRequest request{};
request.sid = 0x80000701u;
request.function = 3u;
t.IsTrue(subsystem.handleRpc(request).handled,
"the activated LIBSD service should handle its RPC");
t.Equals(host.audioCalls, 1u, "the RPC should reach the HLE audio contract");
int32_t stopResult = -1;
t.IsTrue(subsystem.stopModule(loaded.moduleId, &stopResult),
"an HLE module should have a real stoppable lifecycle");
t.Equals(stopResult, 0, "stopping an HLE module should report success");
t.IsFalse(subsystem.canBindRpc(0x80000701u),
"stopping LIBSD should deactivate its RPC endpoint");
});
tc.Run("unknown SID remains unhandled without a matching profile", [](TestCase &t)
{
FakeIopHost host;
@@ -540,27 +639,27 @@ void register_ps2_iop_tests()
"TSNDDRV should handle the characterized command queue");
int16_t writtenChecksum = 0;
t.IsTrue(host.readGuest(statusAddress + 0x26u,
&writtenChecksum,
sizeof(writtenChecksum)),
t.IsTrue(host.readIopMemory(statusAddress + 0x26u,
&writtenChecksum,
sizeof(writtenChecksum)),
"TSNDDRV SE checksum slot should be readable");
t.Equals(writtenChecksum, kChecksum,
"valid port should mirror the profile-bound checksum table");
constexpr uint32_t kPastStatusAddress = 0x44u;
constexpr uint16_t kSentinel = 0xBEEFu;
t.IsTrue(host.writeGuest(statusAddress + kPastStatusAddress,
&kSentinel,
sizeof(kSentinel)),
t.IsTrue(host.writeIopMemory(statusAddress + kPastStatusAddress,
&kSentinel,
sizeof(kSentinel)),
"sentinel after the status structure should be writable");
command[1] = 0x0Fu;
(void)host.writeGuest(kCommandAddress, command.data(), command.size());
(void)subsystem.handleRpc(commandRequest);
uint16_t sentinelAfter = 0u;
(void)host.readGuest(statusAddress + kPastStatusAddress,
&sentinelAfter,
sizeof(sentinelAfter));
(void)host.readIopMemory(statusAddress + kPastStatusAddress,
&sentinelAfter,
sizeof(sentinelAfter));
t.Equals(sentinelAfter, kSentinel,
"invalid port must not overwrite memory past the 0x42-byte status structure");
});
+286 -3
View File
@@ -861,6 +861,57 @@ void register_ps2_memory_tests()
t.IsFalse(mem.isPath3Masked(), "MSKPATH3 with imm bit15 clear should disable PATH3 mask");
});
tc.Run("VIF1 FIFO MSKPATH3 is visible through GIF_STAT", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
const uint32_t setMask = makeVifCmd(0x06u, 0u, 0x8000u);
const __m128i setPacket = _mm_set_epi32(0, 0, 0, static_cast<int32_t>(setMask));
mem.write128(0x10005000u, setPacket);
t.IsTrue(mem.isPath3Masked(), "a direct VIF1 FIFO command should execute MSKPATH3");
t.IsTrue((mem.readIORegister(0x10003020u) & 0x2u) != 0u,
"GIF_STAT.M3P should report the VIF1 PATH3 mask");
const uint32_t clearMask = makeVifCmd(0x06u, 0u, 0x0000u);
const __m128i clearPacket = _mm_set_epi32(0, 0, 0, static_cast<int32_t>(clearMask));
mem.write128(0x10005000u, clearPacket);
t.IsFalse(mem.isPath3Masked(), "a direct VIF1 FIFO command should clear MSKPATH3");
t.IsTrue((mem.readIORegister(0x10003020u) & 0x2u) == 0u,
"GIF_STAT.M3P should clear with the VIF1 PATH3 mask");
mem.writeIORegister(0x10003010u, 0x5u); // GIF_MODE: M3R | IMT
mem.writeIORegister(0x10003000u, 0x8u); // GIF_CTRL: PSE
t.Equals(mem.readIORegister(0x10003020u) & 0xDu, 0xDu,
"GIF_STAT should mirror GIF_MODE and GIF_CTRL status bits");
});
tc.Run("GIF_STAT exposes synchronously drained DMA occupancy for one EE quantum", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kGifChannel = 0x1000A000u;
constexpr uint32_t kSource = 0x00020000u;
mem.setGifPacketCallback([](const uint8_t *, uint32_t) {});
t.IsTrue(mem.writeIORegister(kGifChannel + 0x10u, kSource),
"write GIF MADR should succeed");
t.IsTrue(mem.writeIORegister(kGifChannel + 0x20u, 1u),
"write GIF QWC should succeed");
t.IsTrue(mem.writeIORegister(kGifChannel + 0x00u, 0x100u),
"start GIF normal DMA should succeed");
const uint32_t visibleFqc = (mem.readIORegister(0x10003020u) >> 24u) & 0x1Fu;
t.Equals(visibleFqc, 1u,
"a synchronously consumed qword should remain observable through GIF_STAT.FQC");
mem.advanceEeTimers(1u);
const uint32_t drainedFqc = (mem.readIORegister(0x10003020u) >> 24u) & 0x1Fu;
t.Equals(drainedFqc, 0u,
"the synthetic FIFO observation should expire at the next EE scheduling boundary");
});
tc.Run("PATH3 mask queues packets until unmask", [](TestCase &t)
{
PS2Memory mem;
@@ -1396,7 +1447,8 @@ void register_ps2_memory_tests()
});
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u),
"write VIF1 CHCR STR|CHAIN|TTE should succeed");
mem.processPendingTransfers();
@@ -1435,7 +1487,8 @@ void register_ps2_memory_tests()
std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd));
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u),
"write VIF1 CHCR STR|CHAIN|TTE should succeed");
mem.processPendingTransfers();
@@ -1445,6 +1498,84 @@ void register_ps2_memory_tests()
"qwc-zero compact VIF1 chain should clear the STR bit after drain");
});
tc.Run("VIF1 DMA chain transfers REF tag high bytes when TTE is enabled", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kVif1Ch = 0x10009000u;
constexpr uint32_t kTag0 = 0x00025200u;
constexpr uint32_t kTag1 = kTag0 + 0x10u;
constexpr uint32_t kRefPayload = 0x00025300u;
uint8_t *rdram = mem.getRDRAM();
writeDmaTag(rdram, kTag0, makeDmaTag(1u, 3u, kRefPayload, false)); // REF
writeDmaTag(rdram, kTag1, makeDmaTag(0u, 7u, 0u, false)); // END
// With CHCR.TTE set, both VIFcodes stored in every DMAtag's upper half
// precede that tag's payload, including tags whose payload is referenced.
const uint32_t directCmd = makeVifCmd(0x50u, 0u, 1u);
std::memcpy(rdram + kTag0 + 12u, &directCmd, sizeof(directCmd));
for (uint32_t i = 0; i < 16u; ++i)
{
rdram[kRefPayload + i] = static_cast<uint8_t>(0xA0u + i);
}
std::vector<std::vector<uint8_t>> captured;
mem.setGifPacketCallback([&](const uint8_t *data, uint32_t sizeBytes)
{
captured.emplace_back(data, data + sizeBytes);
});
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag0), "write VIF1 TADR should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u),
"write VIF1 CHCR STR|CHAIN|TTE should succeed");
mem.processPendingTransfers();
t.Equals(captured.size(), static_cast<size_t>(1u),
"REF tag high-half DIRECT should emit one GIF packet");
if (!captured.empty())
{
t.Equals(captured[0].size(), static_cast<size_t>(16u),
"REF tag high-half DIRECT packet should be 1 QW");
bool payloadOk = true;
for (uint32_t i = 0; i < 16u; ++i)
{
if (captured[0][i] != static_cast<uint8_t>(0xA0u + i))
{
payloadOk = false;
break;
}
}
t.IsTrue(payloadOk, "REF payload should reach the GIF callback without VIF desynchronization");
}
});
tc.Run("VIF1 DMA chain ignores tag high bytes when TTE is disabled", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kVif1Ch = 0x10009000u;
constexpr uint32_t kTag = 0x00025400u;
uint8_t *rdram = mem.getRDRAM();
writeDmaTag(rdram, kTag, makeDmaTag(0u, 7u, 0u, false)); // END
const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x55u);
std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd));
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u),
"write VIF1 CHCR STR|CHAIN without TTE should succeed");
mem.processPendingTransfers();
t.Equals(mem.vif1_regs.itops, 0u,
"tag high-half VIFcodes must stay hidden when CHCR.TTE is clear");
});
tc.Run("VIF1 packet builders keep chain qwc live before terminate", [](TestCase &t)
{
PS2Memory mem;
@@ -1497,7 +1628,8 @@ void register_ps2_memory_tests()
});
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kBaseAddr), "write VIF1 TADR should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed");
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u),
"write VIF1 CHCR STR|CHAIN|TTE should succeed");
mem.processPendingTransfers();
@@ -1780,6 +1912,74 @@ void register_ps2_memory_tests()
}
});
tc.Run("DMAC SPR_FROM copies scratchpad to RDRAM and completes channel 8", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kChannel = 0x1000D000u;
constexpr uint32_t kMadr = 0x00028000u;
constexpr uint32_t kSadr = 0x00000120u;
constexpr uint32_t kQwc = 2u;
constexpr uint32_t kBytes = kQwc * 16u;
for (uint32_t i = 0; i < kBytes; ++i)
mem.getScratchpad()[kSadr + i] = static_cast<uint8_t>(0x30u + i);
t.IsTrue(mem.writeIORegister(kChannel + 0x10u, kMadr), "write SPR_FROM MADR should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x20u, kQwc), "write SPR_FROM QWC should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x80u, kSadr), "write SPR_FROM SADR should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x00u, 0x100u), "start SPR_FROM should succeed");
bool copied = true;
for (uint32_t i = 0; i < kBytes; ++i)
copied = copied && mem.getRDRAM()[kMadr + i] == static_cast<uint8_t>(0x30u + i);
t.IsTrue(copied, "SPR_FROM should copy every qword from scratchpad to RDRAM");
t.IsTrue((mem.readIORegister(kChannel + 0x00u) & 0x100u) == 0u, "SPR_FROM completion should clear CHCR.STR");
t.Equals(mem.readIORegister(kChannel + 0x20u), 0u, "SPR_FROM completion should consume QWC");
t.Equals(mem.readIORegister(kChannel + 0x10u), kMadr + kBytes, "SPR_FROM should advance MADR");
t.Equals(mem.readIORegister(kChannel + 0x80u), (kSadr + kBytes) & 0x3FFFu, "SPR_FROM should advance SADR");
t.IsTrue((mem.readIORegister(0x1000E010u) & (1u << 8u)) != 0u, "SPR_FROM should raise D_STAT channel 8");
const std::vector<uint32_t> causes = mem.consumeCompletedDmacCauses();
t.Equals(causes.size(), static_cast<size_t>(1u), "SPR_FROM should queue one DMAC completion");
if (!causes.empty())
t.Equals(causes[0], 8u, "SPR_FROM completion should use DMAC cause 8");
});
tc.Run("DMAC SPR_TO copies RDRAM to scratchpad and completes channel 9", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kChannel = 0x1000D400u;
constexpr uint32_t kMadr = 0x00028400u;
constexpr uint32_t kSadr = 0x00000240u;
constexpr uint32_t kQwc = 2u;
constexpr uint32_t kBytes = kQwc * 16u;
for (uint32_t i = 0; i < kBytes; ++i)
mem.getRDRAM()[kMadr + i] = static_cast<uint8_t>(0x70u + i);
t.IsTrue(mem.writeIORegister(kChannel + 0x10u, kMadr), "write SPR_TO MADR should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x20u, kQwc), "write SPR_TO QWC should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x80u, kSadr), "write SPR_TO SADR should succeed");
t.IsTrue(mem.writeIORegister(kChannel + 0x00u, 0x100u), "start SPR_TO should succeed");
bool copied = true;
for (uint32_t i = 0; i < kBytes; ++i)
copied = copied && mem.getScratchpad()[kSadr + i] == static_cast<uint8_t>(0x70u + i);
t.IsTrue(copied, "SPR_TO should copy every qword from RDRAM to scratchpad");
t.IsTrue((mem.readIORegister(kChannel + 0x00u) & 0x100u) == 0u, "SPR_TO completion should clear CHCR.STR");
t.Equals(mem.readIORegister(kChannel + 0x20u), 0u, "SPR_TO completion should consume QWC");
t.IsTrue((mem.readIORegister(0x1000E010u) & (1u << 9u)) != 0u, "SPR_TO should raise D_STAT channel 9");
const std::vector<uint32_t> causes = mem.consumeCompletedDmacCauses();
t.Equals(causes.size(), static_cast<size_t>(1u), "SPR_TO should queue one DMAC completion");
if (!causes.empty())
t.Equals(causes[0], 9u, "SPR_TO completion should use DMAC cause 9");
});
tc.Run("sceDmaReset re-enables DMAC DMAE", [](TestCase &t)
{
PS2Runtime runtime;
@@ -1812,6 +2012,38 @@ void register_ps2_memory_tests()
t.Equals(mem.readIORegister(kDstadr), 0u, "sceDmaReset should clear D_STADR");
});
tc.Run("sceDmaSend preserves guest-configured VIF1 TTE", [](TestCase &t)
{
PS2Runtime runtime;
t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kVif1Ch = 0x10009000u;
constexpr uint32_t kTag = 0x00028500u;
PS2Memory &mem = runtime.memory();
uint8_t *rdram = mem.getRDRAM();
writeDmaTag(rdram, kTag, makeDmaTag(0u, 7u, 0u, false)); // END
const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x66u);
std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd));
// Fatal Frame follows this exact sequence: get channel, set CHCR.TTE,
// then submit the chain through sceDmaSend.
t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x40u),
"guest should be able to configure VIF1 CHCR.TTE before submission");
R5900Context ctx{};
setRegU32(ctx, 4, 1u); // sceDmaGetChan(1) / VIF1
setRegU32(ctx, 5, kTag);
ps2_stubs::sceDmaSend(rdram, &ctx, &runtime);
t.Equals(static_cast<int32_t>(::getRegU32(&ctx, 2)), 0,
"sceDmaSend should accept the VIF1 chain");
t.IsTrue((mem.readIORegister(kVif1Ch + 0x00u) & 0x40u) != 0u,
"sceDmaSend must preserve guest-configured CHCR.TTE");
t.Equals(mem.vif1_regs.itops, 0x66u,
"preserved TTE should deliver the tag high-half VIFcode");
});
tc.Run("VIF1 DMA DIRECT image packet reaches GS through arbiter", [](TestCase &t)
{
PS2Memory mem;
@@ -1933,6 +2165,57 @@ void register_ps2_memory_tests()
t.IsTrue(imageOk, "raw qwords after a DIRECT image tag should continue the PATH2 image upload");
});
tc.Run("VIF1 DIRECT finds an image continuation after packed setup", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
GS gs;
gs.init(mem.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &mem.gs());
GifArbiter arbiter([&](const uint8_t *data, uint32_t sizeBytes)
{
gs.processGIFPacket(data, sizeBytes);
});
mem.setGifArbiter(&arbiter);
const uint64_t bitblt =
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(1u) << 48);
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32));
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU32(packet, makeVifCmd(0x50u, 0u, 3u)); // PACKED tag + A+D + IMAGE tag.
appendU64(packet, makeGifTag(1u, GIF_FMT_PACKED, 1u, false));
appendU64(packet, 0x0Eull);
appendU64(packet, 0x8000008000ull); // TEXA, harmless setup preceding the IMAGE tag.
appendU64(packet, GS_REG_TEXA);
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
for (uint32_t i = 0; i < 16u; ++i)
packet.push_back(static_cast<uint8_t>(0xC0u + i));
mem.processVIF1Data(packet.data(), static_cast<uint32_t>(packet.size()));
const uint8_t *vramOut = mem.getGSVRAM();
bool imageOk = true;
for (uint32_t x = 0; x < 4u && imageOk; ++x)
{
const uint32_t off = GSPSMCT32::addrPSMCT32(0u, 1u, x, 0u);
for (uint32_t c = 0; c < 4u; ++c)
{
if (vramOut[off + c] != static_cast<uint8_t>(0xC0u + x * 4u + c))
{
imageOk = false;
break;
}
}
}
t.IsTrue(imageOk, "raw image continuation after packed setup should not be decoded as VIF/GIF registers");
});
tc.Run("unaligned accesses throw", [](TestCase &t)
{
PS2Memory mem;
+580 -10
View File
@@ -11,6 +11,7 @@
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <unordered_map>
#include <vector>
@@ -155,7 +156,122 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath)
static bool writeMinimalMipsElfWithVuMicroprogramSection(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const std::array<uint32_t, 2> textWords{0x03E00008u, 0x00000000u};
text->set_data(reinterpret_cast<const char *>(textWords.data()), sizeof(textWords));
ELFIO::section *vuText = writer.sections.add(".vutext");
vuText->set_type(ELFIO::SHT_PROGBITS);
vuText->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
vuText->set_addr_align(16);
vuText->set_address(0x00250000u);
const std::array<uint32_t, 4> vuWords{0x01EC48BDu, 0u, 0x01FA717Du, 0u};
vuText->set_data(reinterpret_cast<const char *>(vuWords.data()), sizeof(vuWords));
ELFIO::section *strtab = writer.sections.add(".strtab");
strtab->set_type(ELFIO::SHT_STRTAB);
strtab->set_addr_align(1);
ELFIO::section *symtab = writer.sections.add(".symtab");
symtab->set_type(ELFIO::SHT_SYMTAB);
symtab->set_info(1);
symtab->set_link(strtab->get_index());
symtab->set_addr_align(4);
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
ELFIO::symbol_section_accessor symbols(writer, symtab);
ELFIO::string_section_accessor strings(strtab);
symbols.add_symbol(strings, "", 0, 0, ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
symbols.add_symbol(strings, "ee_entry", text->get_address(), text->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
symbols.add_symbol(strings, "vu_program", vuText->get_address(), vuText->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, vuText->get_index());
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *vuSegment = writer.segments.add();
vuSegment->set_type(ELFIO::PT_LOAD);
vuSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
vuSegment->set_align(0x1000);
vuSegment->add_section_index(vuText->get_index(), vuText->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithUnmappedEntryHint(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const std::array<uint32_t, 10> textWords = {
0x03E00008u, 0x00000000u, // known function at 0x00100000
0x00000000u, 0x00000000u,
0x03E00008u, 0x00000000u, // omitted entry at 0x00100010
0x00000000u, 0x00000000u,
0x03E00008u, 0x00000000u, // next known function at 0x00100020
};
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
ELFIO::section *strtab = writer.sections.add(".strtab");
strtab->set_type(ELFIO::SHT_STRTAB);
strtab->set_addr_align(1);
ELFIO::section *symtab = writer.sections.add(".symtab");
symtab->set_type(ELFIO::SHT_SYMTAB);
symtab->set_info(1);
symtab->set_link(strtab->get_index());
symtab->set_addr_align(4);
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
ELFIO::symbol_section_accessor symbols(writer, symtab);
ELFIO::string_section_accessor strings(strtab);
symbols.add_symbol(strings, "", 0, 0,
ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
symbols.add_symbol(strings, "known_before", 0x00100000u, 8u,
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
symbols.add_symbol(strings, "stubbed_owner", 0x00100008u, 0x18u,
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
symbols.add_symbol(strings, "known_after", 0x00100020u, 8u,
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath,
bool includePartialDwarf = false)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
@@ -170,7 +286,7 @@ static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::
text->set_addr_align(4);
text->set_address(0x00100000u);
std::array<uint32_t, 30> textWords{};
std::array<uint32_t, 384> textWords{};
textWords[0] = 0x3C040010u; // lui a0,0x10
textWords[1] = 0xAC800000u; // sw zero,0(a0)
textWords[2] = 0x0C040008u; // jal 0x00100020 (callback registrar)
@@ -178,7 +294,7 @@ static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::
textWords[4] = 0x03E00008u; // jr ra
textWords[5] = 0x00000000u; // nop
textWords[6] = 0x3C080010u; // lui t0,0x10
textWords[7] = 0x25080070u; // addiu t0,t0,0x70 (code label, not a callback argument)
textWords[7] = 0x25080300u; // addiu t0,t0,0x300 (code label, not a callback argument)
textWords[8] = 0x03E00008u; // registrar at 0x00100020
textWords[9] = 0x00000000u;
@@ -189,13 +305,220 @@ static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::
textWords[19] = 0x03E00008u; // jr ra
textWords[20] = 0x27BD0010u; // addiu sp,sp,0x10
textWords[24] = 0x03E00008u; // table leaf at 0x00100060
textWords[25] = 0x00000000u;
textWords[24] = 0x08040008u; // table leaf thunk at 0x00100060: j 0x00100020
textWords[25] = 0x00000000u; // nop (delay slot)
textWords[26] = 0x03E00008u; // table leaf at 0x00100068
textWords[27] = 0x00000000u;
textWords[28] = 0x03E00008u; // isolated pointer target at 0x00100070
textWords[28] = 0x03E00008u; // adjacent leaf thunk at 0x00100070
textWords[29] = 0x00000000u;
// Address-taken initializer at 0x00100080 with a long constant-setup
// preamble before its stack frame, matching retail constructor tables.
textWords[32] = 0x3C020010u; // lui v0,0x10
textWords[33] = 0x3C030010u; // lui v1,0x10
textWords[34] = 0x3C050010u; // lui a1,0x10
textWords[35] = 0x3C060010u; // lui a2,0x10
textWords[36] = 0x3C070010u; // lui a3,0x10
textWords[37] = 0x3C080010u; // lui t0,0x10
textWords[38] = 0x3C090010u; // lui t1,0x10
textWords[39] = 0x3C0A0010u; // lui t2,0x10
textWords[40] = 0x3C0B0010u; // lui t3,0x10
textWords[41] = 0x27BDFFF0u; // addiu sp,sp,-0x10
textWords[42] = 0x03E00008u; // jr ra
textWords[43] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot)
// A callback address completed three instructions before the registrar call.
// Its leaf body is deliberately longer than a small thunk and begins after a
// preceding function's return, matching stripped retail ELF callback ranges.
textWords[44] = 0x3C060010u; // lui a2,0x10
textWords[45] = 0x7FB00010u; // sq s0,0x10(sp)
textWords[46] = 0xFFBF0000u; // sd ra,0(sp)
textWords[47] = 0x24C600E0u; // addiu a2,a2,0xE0 (callback at 0x001000E0)
textWords[48] = 0x24040008u; // addiu a0,zero,8
textWords[49] = 0x2405040Fu; // addiu a1,zero,0x40F
textWords[50] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[51] = 0x00000000u; // nop (delay slot)
textWords[56] = 0x3C020010u; // long leaf callback at 0x001000E0
textWords[57] = 0x8C420200u;
textWords[58] = 0x3C030020u;
textWords[59] = 0x24630100u;
textWords[60] = 0x3C068000u;
textWords[61] = 0x24420001u;
textWords[62] = 0x00A31821u;
textWords[63] = 0x3C010010u;
textWords[64] = 0xAC420200u;
textWords[65] = 0xAC660004u;
textWords[66] = 0x0080102Du;
textWords[67] = 0x3C010010u;
textWords[68] = 0xAC450204u;
textWords[69] = 0x03E00008u; // jr ra, beyond the old eight-word leaf window
textWords[70] = 0xAC600000u; // sw zero,0(v1) (delay slot)
// A long leaf method referenced only by a clustered descriptor table. Its
// return is deliberately beyond the materialized-callback probe distance.
textWords[72] = 0x8C850014u; // lw a1,0x14(a0), method at 0x00100120
for (size_t index = 73; index < 121; ++index)
{
textWords[index] = 0x24420001u; // addiu v0,v0,1
}
textWords[121] = 0x03E00008u; // jr ra at method instruction 49
textWords[122] = 0x00000000u; // nop (delay slot)
// Some retail callback registrars keep an address in a saved register while
// assembling the remaining arguments, then copy it into a2 immediately before
// the JAL. The call is deliberately well beyond any small lookahead window.
textWords[128] = 0x3C140010u; // lui s4,0x10
textWords[129] = 0x26940280u; // addiu s4,s4,0x280 (callback at 0x00100280)
textWords[130] = 0x7FB00060u; // sq s0,0x60(sp)
textWords[131] = 0x7FB10050u; // sq s1,0x50(sp)
textWords[132] = 0x24070001u; // addiu a3,zero,1
textWords[133] = 0x7FB20040u; // sq s2,0x40(sp)
textWords[134] = 0x0000202Du; // daddu a0,zero,zero
textWords[135] = 0x7FB30030u; // sq s3,0x30(sp)
textWords[136] = 0xFFBF0000u; // sd ra,0(sp)
textWords[137] = 0x2405011Fu; // addiu a1,zero,0x11F
textWords[138] = 0x0C040008u; // setup call; s4 must preserve the incomplete address
textWords[139] = 0x00000000u; // nop (delay slot)
textWords[144] = 0x0280302Du; // daddu a2,s4,zero
textWords[146] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[147] = 0x00000000u; // nop (delay slot)
// A retail-style conditional initializes a callback register in its delay
// slot, then completes the address only in the taken successor block. A
// linear lookahead cannot connect these two halves; CFG traversal must.
textWords[148] = 0x04410004u; // bgez v0,0x00100264
textWords[149] = 0x3C060010u; // lui a2,0x10 (delay slot)
textWords[150] = 0x10000008u; // b 0x0010027C (not-taken path)
textWords[151] = 0x00000000u; // nop (delay slot)
textWords[153] = 0x24C602C0u; // addiu a2,a2,0x2C0
textWords[154] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[155] = 0x24040008u; // addiu a0,zero,8 (delay slot)
textWords[160] = 0x3C030010u; // callback at 0x00100280
textWords[161] = 0x8C630200u;
textWords[162] = 0x24630001u;
textWords[163] = 0xAC630200u;
textWords[164] = 0x03E00008u; // jr ra
textWords[165] = 0x0080102Du; // daddu v0,a0,zero (delay slot)
textWords[176] = 0x8F830000u; // callback at 0x001002C0: lw v1,0(gp)
textWords[177] = 0x0080102Du; // daddu v0,a0,zero
textWords[178] = 0x2405FFFFu; // addiu a1,zero,-1
textWords[179] = 0x00832021u; // addu a0,a0,v1
textWords[180] = 0x03E00008u; // jr ra
textWords[181] = 0xAC850000u; // sw a1,0(a0) (delay slot)
// Retail class constructors often build their method tables in writable
// memory instead of shipping literal function pointers in .rodata. The
// materialized code address is never passed to a registrar; storing it in
// the descriptor is the only address-taken evidence.
textWords[184] = 0x3C040010u; // lui a0,0x10
textWords[185] = 0x24840340u; // addiu a0,a0,0x340 (method at 0x00100340)
textWords[186] = 0xAE44001Cu; // sw a0,0x1c(s2)
textWords[187] = 0x0000202Du; // daddu a0,zero,zero (clobber)
textWords[208] = 0x3C020020u; // stored leaf method at 0x00100340
textWords[209] = 0x03E00008u; // jr ra
textWords[210] = 0x24420100u; // addiu v0,v0,0x100 (delay slot)
// Long leaf in an alternating (function pointer, numeric id) table. This
// is a common stripped retail dispatch-table layout and provides strong
// address evidence through the adjacent ordinary function pointer.
textWords[224] = 0x3C010020u; // long leaf at 0x00100380
for (size_t index = 225; index < 235; ++index)
{
textWords[index] = 0x24420001u;
}
textWords[235] = 0x03E00008u;
textWords[236] = 0x00000000u;
// A stripped function map may merge the middle member of a run of trivial
// leaf accessors into its predecessor. Only the first accessor is reached by
// a direct call; the second still needs its own callable entry.
textWords[188] = 0x0C0400F0u; // jal 0x001003C0
textWords[189] = 0x00000000u; // nop (delay slot)
textWords[192] = 0x03E00008u; // isolated pointer target at 0x00100300
textWords[193] = 0x00000000u; // nop (delay slot)
textWords[240] = 0x03E00008u; // known leaf at 0x001003C0: jr ra
textWords[241] = 0x0080102Du; // daddu v0,a0,zero (delay slot)
textWords[242] = 0x03E00008u; // merged leaf at 0x001003C8: jr ra
textWords[243] = 0x0080102Du; // daddu v0,a0,zero (delay slot)
// Some retail registrars take more than four register arguments. The fifth
// callback is passed in physical t0, followed by unrelated argument setup
// before the call. It must remain distinguishable from the dead t0 code
// materialization at 0x00100018 above.
textWords[196] = 0x3C080010u; // lui t0,0x10
textWords[197] = 0x250803E0u; // addiu t0,t0,0x3E0 (callback at 0x001003E0)
textWords[198] = 0x24040014u; // addiu a0,zero,0x14
textWords[199] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[200] = 0x24050A0Bu; // addiu a1,zero,0xA0B (delay slot)
textWords[248] = 0x03E00008u; // extended-argument leaf at 0x001003E0: jr ra
textWords[249] = 0x0080102Du; // daddu v0,a0,zero (delay slot)
// The fifth register argument can also reference a substantial leaf body.
// Its return deliberately lies beyond the old fixed 64-instruction scan
// window, so discovery must follow the candidate's reachable control flow.
textWords[201] = 0x3C080010u; // lui t0,0x10
textWords[202] = 0x25080400u; // addiu t0,t0,0x400 (callback at 0x00100400)
textWords[203] = 0x24040030u; // addiu a0,zero,0x30
textWords[204] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[205] = 0x24050A06u; // addiu a1,zero,0xA06 (delay slot)
textWords[256] = 0x3C080048u; // long extended-argument leaf at 0x00100400
for (size_t index = 257; index < 336; ++index)
{
textWords[index] = 0x24420001u; // addiu v0,v0,1
}
textWords[336] = 0x03E00008u; // jr ra at instruction 80
textWords[337] = 0x00000000u; // nop (delay slot)
// Stripped function maps can merge a normal non-leaf function into the
// preceding function even though the boundary is unambiguous in the bytes:
// `jr ra`, its delay slot, then a fresh stack allocation.
textWords[338] = 0x27BDFFF0u; // post-return function at 0x00100548
textWords[339] = 0xFFBF0000u; // sd ra,0(sp)
textWords[340] = 0x03E00008u; // jr ra
textWords[341] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot)
// The target at 0x00100580 has only a singleton initialized-data pointer.
// A known function loads that slot and invokes it through JALR, matching
// retail callback slots that are not large enough to look like a table.
textWords[30] = 0x0C0400D4u; // jal 0x00100350
textWords[31] = 0x00000000u; // nop (delay slot)
textWords[212] = 0x27BDFFF0u; // indirect caller at 0x00100350
textWords[213] = 0xFFBF0000u; // sd ra,0(sp)
textWords[214] = 0x3C100020u; // lui s0,0x20
textWords[215] = 0x8E021000u; // lw v0,0x1000(s0) -> [0x00201000]
textWords[216] = 0x00000000u; // nop
textWords[217] = 0x0040F809u; // jalr v0
textWords[218] = 0x00000000u; // nop (delay slot)
textWords[219] = 0xDFBF0000u; // ld ra,0(sp)
textWords[220] = 0x03E00008u; // jr ra
textWords[221] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot)
textWords[352] = 0x27BDFFF0u; // singleton data target at 0x00100580
textWords[353] = 0x0320F809u; // jalr t9
textWords[354] = 0x0200202Du; // daddu a0,s0,zero (delay slot at 0x00100588)
textWords[355] = 0x24420001u; // addiu v0,v0,1
textWords[356] = 0x24420001u; // addiu v0,v0,1
textWords[357] = 0x03E00008u; // jr ra
textWords[358] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot)
// A second initialized-data word deliberately points at 0x00100588, the
// delay slot of the JALR above. Its following body can look callable to a
// reachability probe, but splitting there would truncate the real owner.
textWords[12] = 0x0C040170u; // jal 0x001005C0
textWords[13] = 0x00000000u; // nop (delay slot)
textWords[368] = 0x27BDFFF0u; // delay-slot pointer caller at 0x001005C0
textWords[369] = 0x3C100020u; // lui s0,0x20
textWords[370] = 0x8E021040u; // lw v0,0x1040(s0) -> [0x00201040]
textWords[371] = 0x0040F809u; // jalr v0
textWords[372] = 0x00000000u; // nop (delay slot)
textWords[373] = 0x03E00008u; // jr ra
textWords[374] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot)
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
@@ -205,13 +528,101 @@ static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::
rodata->set_addr_align(4);
rodata->set_address(0x00200000u);
std::array<uint32_t, 20> tableWords{};
std::array<uint32_t, 32> tableWords{};
tableWords[1] = 0x00100060u;
tableWords[3] = 0x00100068u;
tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster
// Retail class descriptor: name pointer, ordinary method, two reserved
// words, then a long leaf method.
tableWords[5] = 0x0020004Cu;
tableWords[6] = 0x00100080u;
tableWords[7] = 0;
tableWords[8] = 0;
tableWords[9] = 0x00100120u;
tableWords[16] = 0x00100300u; // plausible entry, but not part of a pointer cluster
tableWords[28] = 0x00100080u; // ordinary function, followed by a numeric id
tableWords[29] = 0x0000000Du;
tableWords[30] = 0x00100380u; // long leaf, followed by a numeric id
tableWords[31] = 0x0000000Bu;
rodata->set_data(reinterpret_cast<const char *>(tableWords.data()),
static_cast<ELFIO::Elf_Word>(tableWords.size() * sizeof(uint32_t)));
ELFIO::section *data = writer.sections.add(".data");
data->set_type(ELFIO::SHT_PROGBITS);
data->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE);
data->set_addr_align(4);
data->set_address(0x00201000u);
std::array<uint32_t, 17> singletonCallbacks{};
singletonCallbacks[0] = 0x00100580u;
singletonCallbacks[16] = 0x00100588u;
data->set_data(reinterpret_cast<const char *>(singletonCallbacks.data()),
static_cast<ELFIO::Elf_Word>(singletonCallbacks.size() * sizeof(uint32_t)));
if (includePartialDwarf)
{
// A retail ELF can retain debug information for only part of its code.
// The parser must still supplement that incomplete map with static
// address-taken discovery instead of treating any DWARF as exhaustive.
const std::array<uint8_t, 19> abbrevBytes = {
0x01, 0x11, 0x01, // abbrev 1: compile_unit, has children
0x03, 0x08, // DW_AT_name, DW_FORM_string
0x00, 0x00,
0x02, 0x2E, 0x00, // abbrev 2: subprogram, no children
0x03, 0x08, // DW_AT_name, DW_FORM_string
0x11, 0x01, // DW_AT_low_pc, DW_FORM_addr
0x12, 0x06, // DW_AT_high_pc, DW_FORM_data4
0x00, 0x00, // end of attribute list
0x00}; // end of abbreviation table
ELFIO::section *debugAbbrev = writer.sections.add(".debug_abbrev");
debugAbbrev->set_type(ELFIO::SHT_PROGBITS);
debugAbbrev->set_addr_align(1);
debugAbbrev->set_data(reinterpret_cast<const char *>(abbrevBytes.data()),
static_cast<ELFIO::Elf_Word>(abbrevBytes.size()));
std::vector<uint8_t> infoBytes(sizeof(uint32_t), 0);
auto appendU8 = [&infoBytes](uint8_t value)
{ infoBytes.push_back(value); };
auto appendU16 = [&infoBytes](uint16_t value)
{
const auto *bytes = reinterpret_cast<const uint8_t *>(&value);
infoBytes.insert(infoBytes.end(), bytes, bytes + sizeof(value));
};
auto appendU32 = [&infoBytes](uint32_t value)
{
const auto *bytes = reinterpret_cast<const uint8_t *>(&value);
infoBytes.insert(infoBytes.end(), bytes, bytes + sizeof(value));
};
auto appendString = [&infoBytes](std::string_view value)
{
infoBytes.insert(infoBytes.end(), value.begin(), value.end());
infoBytes.push_back(0);
};
appendU16(4); // DWARF version
appendU32(0); // abbreviation table offset
appendU8(4); // address size
appendU8(1); // compile-unit DIE
appendString("partial-unit");
appendU8(2); // subprogram DIE
appendString("known_partial_function");
appendU32(0x00100000u);
appendU32(0x20u); // DWARF 4 high_pc offset
appendU8(2); // a later known subprogram bounds fallback ranges
appendString("known_tail_function");
appendU32(0x001003F0u);
appendU32(0x10u);
appendU8(0); // end compile-unit children
const uint32_t unitLength = static_cast<uint32_t>(infoBytes.size() - sizeof(uint32_t));
std::memcpy(infoBytes.data(), &unitLength, sizeof(unitLength));
ELFIO::section *debugInfo = writer.sections.add(".debug_info");
debugInfo->set_type(ELFIO::SHT_PROGBITS);
debugInfo->set_addr_align(1);
debugInfo->set_data(reinterpret_cast<const char *>(infoBytes.data()),
static_cast<ELFIO::Elf_Word>(infoBytes.size()));
}
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
@@ -223,6 +634,7 @@ static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::
dataSegment->set_flags(ELFIO::PF_R);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(rodata->get_index(), rodata->get_addr_align());
dataSegment->add_section_index(data->get_index(), data->get_addr_align());
return writer.save(elfPath.string());
}
@@ -295,7 +707,8 @@ static bool writeRecompilerTestConfig(const std::filesystem::path &configPath,
const std::filesystem::path &elfPath,
const std::filesystem::path &outputPath,
const std::vector<std::string> &skip,
const std::vector<std::string> &stubs = {})
const std::vector<std::string> &stubs = {},
const std::vector<std::string> &entryPoints = {})
{
std::ofstream config(configPath);
if (!config)
@@ -320,6 +733,14 @@ static bool writeRecompilerTestConfig(const std::filesystem::path &configPath,
config << '"' << stubs[i] << '"';
}
config << "]\n";
config << "entry_points = [";
for (size_t i = 0; i < entryPoints.size(); ++i)
{
if (i != 0u)
config << ", ";
config << '"' << entryPoints[i] << '"';
}
config << "]\n";
return static_cast<bool>(config);
}
@@ -946,6 +1367,51 @@ void register_ps2_recompiler_tests()
std::filesystem::remove(configPath, removeError);
});
tc.Run("configured entry hint synthesizes an omitted standalone function", [](TestCase &t) {
const std::string uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const std::filesystem::path tempRoot =
std::filesystem::temp_directory_path() / ("ps2recomp-entry-synthesis-" + uniqueSuffix);
const std::filesystem::path elfPath = tempRoot / "entry-hint.elf";
const std::filesystem::path configPath = tempRoot / "entry-hint.toml";
const std::filesystem::path outputPath = tempRoot / "output";
std::filesystem::create_directories(tempRoot);
const bool elfWritten = writeMinimalMipsElfWithUnmappedEntryHint(elfPath);
const bool configWritten = writeRecompilerTestConfig(
configPath,
elfPath,
outputPath,
{},
{"InitAlarm@0x00100008"},
{"omitted_callback@0x00100010"});
t.IsTrue(elfWritten && configWritten,
"standalone entry regression inputs should be generated");
if (elfWritten && configWritten)
{
PS2Recompiler recompiler(configPath.string());
t.IsTrue(recompiler.initialize(),
"standalone entry regression config should initialize");
t.IsTrue(recompiler.recompile(),
"configured executable entry should be decoded even without a symbol");
recompiler.generateOutput();
const std::filesystem::path registrationPath = outputPath / "register_functions.cpp";
std::ifstream registrationFile(registrationPath);
const std::string registration{
std::istreambuf_iterator<char>(registrationFile),
std::istreambuf_iterator<char>()};
t.IsTrue(registration.find("// 0x100010") != std::string::npos,
"synthesized entry address should be registered for guest dispatch");
t.IsTrue(recompiler.reportCounters().additionalEntryPoints >= 1u,
"synthesized entry should be visible in the report");
}
std::error_code removeError;
std::filesystem::remove_all(tempRoot, removeError);
});
tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
@@ -984,6 +1450,41 @@ void register_ps2_recompiler_tests()
std::filesystem::remove(elfPath, removeError);
});
tc.Run("elf parser keeps VU microprograms out of EE code discovery", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-vutext-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithVuMicroprogramSection(elfPath);
t.IsTrue(writeOk, "temporary ELF with .vutext should be generated");
if (!writeOk)
return;
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "ELF with .vutext should parse");
if (parseOk)
{
const auto sections = parser.getSections();
const auto vuSection = std::find_if(sections.begin(), sections.end(),
[](const Section &section)
{ return section.name == ".vutext"; });
t.IsTrue(vuSection != sections.end(), ".vutext bytes should remain available");
if (vuSection != sections.end())
t.IsFalse(vuSection->isCode, ".vutext must not be decoded as R5900 code");
const auto functions = parser.extractFunctions();
const bool hasVuFunction = std::any_of(functions.begin(), functions.end(),
[](const Function &function)
{ return function.start == 0x00250000u; });
t.IsFalse(hasVuFunction, "VU symbol must not become an EE function");
}
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("ghidra map replaces JAL fallback-only auto starts", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
@@ -1092,13 +1593,82 @@ void register_ps2_recompiler_tests()
"clustered rodata pointers should discover the first leaf callback");
t.IsTrue(hasStart(0x00100068u),
"clustered rodata pointers should discover the second leaf callback");
t.IsFalse(hasStart(0x00100070u),
t.IsTrue(hasStart(0x00100080u),
"clustered pointers should discover an initializer with a delayed stack prologue");
t.IsTrue(hasStart(0x001000E0u),
"a nearby registrar call should discover a longer leaf callback");
t.IsTrue(hasStart(0x00100120u),
"a clustered descriptor should discover a long leaf method");
t.IsTrue(hasStart(0x00100280u),
"a callback should flow through a saved register into a call argument");
t.IsTrue(hasStart(0x001002C0u),
"a delay-slot LUI should flow into the taken branch successor");
t.IsTrue(hasStart(0x00100340u),
"a materialized method stored into a runtime descriptor should be discovered");
t.IsTrue(hasStart(0x00100380u),
"an alternating pointer/id table should discover a neighboring long leaf");
t.IsTrue(hasStart(0x001003C8u),
"an adjacent two-instruction leaf thunk should be split from a known thunk");
t.IsTrue(hasStart(0x001003E0u),
"a callback passed as the fifth register argument should be discovered");
t.IsTrue(hasStart(0x00100400u),
"a long callback passed as the fifth register argument should be discovered");
t.IsFalse(hasStart(0x00100548u),
"a post-return prologue without a cross-reference must remain only a hint");
t.IsTrue(hasStart(0x00100580u),
"a singleton data pointer loaded and consumed by JALR should be discovered");
t.IsFalse(hasStart(0x00100588u),
"a data pointer must not split a function at a control-transfer delay slot");
t.IsFalse(hasStart(0x00100300u),
"an isolated data pointer or non-callback code materialization must not become a function");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("elf parser supplements partial DWARF with address-taken callbacks", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-partial-dwarf-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath, true);
t.IsTrue(writeOk, "temporary ELF with partial DWARF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF with partial DWARF should parse");
if (parseOk)
{
const auto functions = parser.extractFunctions();
const auto callbackIt = std::find_if(
functions.begin(), functions.end(),
[](const Function &function)
{ return function.start == 0x00100040u; });
t.IsTrue(callbackIt != functions.end(),
"partial DWARF must not suppress static callback discovery");
const auto lastFallbackIt = std::find_if(
functions.begin(), functions.end(),
[](const Function &function)
{ return function.start == 0x001003E0u; });
t.IsTrue(lastFallbackIt != functions.end(),
"the last inferred callback should still be discovered");
if (lastFallbackIt != functions.end())
{
t.Equals(0x001003F0u, lastFallbackIt->end,
"later partial DWARF should bound an inferred callback");
}
}
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("runtime call resolution includes Veronica compatibility aliases", [](TestCase &t) {
t.Equals(ps2_runtime_calls::resolveSyscallName("ReleaseAlarm"), std::string_view{"ReleaseAlarm"},
"ReleaseAlarm should resolve as a syscall name");
@@ -481,6 +481,32 @@ void register_ps2_runtime_expansion_tests()
"missing target should remain visible in ctx->pc for diagnostics");
});
tc.Run("ContinueToTarget unwinds a missing call without skipping it", [](TestCase &t)
{
PS2Runtime runtime;
runtime.setMissingFunctionPolicy(
PS2Runtime::MissingFunctionPolicy::ContinueToTarget);
R5900Context ctx{};
ctx.pc = 0x2000u;
const bool continuedInCaller = runtime.dispatchGuestBranch(
nullptr,
&ctx,
0x3210u,
0x2000u,
0x2008u,
PS2Runtime::GuestBranchKind::IndirectCall,
"test-missing-unwind");
t.IsFalse(continuedInCaller,
"ContinueToTarget must unwind the generated caller");
t.Equals(ctx.pc, 0x3210u,
"the unresolved target should remain visible to the dispatcher");
t.IsFalse(runtime.isStopRequested(),
"ContinueToTarget should remain a non-stopping debug policy");
});
tc.Run("MPEG init and callback stubs return success instead of TODO errors", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
@@ -8,7 +8,9 @@
#include <cstdint>
#include <cstring>
#include <exception>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
using namespace ps2_syscalls;
@@ -67,6 +69,9 @@ namespace
constexpr uint32_t kTimer2WaitPc = 0x00160500u;
constexpr uint32_t kTimer2ResumePc = 0x00160510u;
constexpr uint32_t kTimer2HandlerPc = 0x00160520u;
constexpr uint32_t kInvocationQueuePc = 0x00160530u;
constexpr uint32_t kInvocationQueueResumePc = 0x00160540u;
constexpr uint32_t kInvocationQueueHandlerPc = 0x00160550u;
constexpr uint32_t kTimer2Count = 0x10001000u;
constexpr uint32_t kTimer2Mode = 0x10001010u;
@@ -89,6 +94,9 @@ namespace
uint64_t g_vsyncCsr = 0;
std::atomic<bool> g_timer2Resumed{false};
uint32_t g_irqObservedSp = 0u;
uint32_t g_invocationQueueRuns = 0u;
uint32_t g_invocationQueueSp = 0u;
bool g_invocationQueueSpChanged = false;
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
@@ -328,6 +336,43 @@ namespace
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerInvocationQueueHandler(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
const uint32_t sp = getRegU32(ctx, 29);
if (g_invocationQueueSp == 0u)
{
g_invocationQueueSp = sp;
}
else if (g_invocationQueueSp != sp)
{
g_invocationQueueSpChanged = true;
}
++g_invocationQueueRuns;
ctx->pc = 0u;
}
void schedulerQueueManyInvocations(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
constexpr uint32_t kInvocationCount = 96u;
EeScheduler &scheduler = runtime->eeScheduler();
for (uint32_t i = 0u; i < kInvocationCount; ++i)
{
GuestInvocation invocation{};
invocation.kind = GuestInvocationKind::Interrupt;
invocation.tag = i;
invocation.context.pc = kInvocationQueueHandlerPc;
setRegU32(invocation.context, 31, 0u);
scheduler.queueInvocation(std::move(invocation));
}
ctx->pc = kInvocationQueueResumePc;
}
void schedulerInvocationQueueResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
ctx->pc = 0u;
runtime->requestStop();
}
}
void register_ps2_runtime_interrupt_tests()
@@ -527,6 +572,36 @@ void register_ps2_runtime_interrupt_tests()
"IRQ handler stack writes must not clobber the registering thread's live frame");
});
tc.Run("pending async callbacks execute sequentially on a reusable invocation stack", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kInvocationQueuePc, schedulerQueueManyInvocations);
env.runtime.registerFunction(kInvocationQueueResumePc, schedulerInvocationQueueResume);
env.runtime.registerFunction(kInvocationQueueHandlerPc, schedulerInvocationQueueHandler);
g_invocationQueueRuns = 0u;
g_invocationQueueSp = 0u;
g_invocationQueueSpChanged = false;
R5900Context mainContext{};
mainContext.pc = kInvocationQueuePc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
bool exhausted = false;
try
{
env.runtime.eeScheduler().run();
}
catch (const std::runtime_error &error)
{
exhausted = std::string_view(error.what()) == "EE invocation stack space exhausted";
}
t.IsFalse(exhausted, "queued callbacks must not consume one invocation stack per pending item");
t.Equals(g_invocationQueueRuns, 96u, "every queued callback should execute exactly once");
t.IsFalse(g_invocationQueueSpChanged, "sequential callbacks should reuse the same stack depth");
});
tc.Run("iSignalSema defers selection until IRQ return", [](TestCase &t)
{
TestEnv env;
+138 -16
View File
@@ -5,6 +5,8 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <utility>
#include <vector>
#include <cstring>
#include <chrono>
@@ -50,6 +52,19 @@ namespace
static_assert(sizeof(SceMcTblGetDir) == 64, "sceMcTblGetDir size mismatch");
struct GuestIoStat
{
uint32_t mode;
uint32_t attr;
uint32_t size;
uint8_t ctime[8];
uint8_t atime[8];
uint8_t mtime[8];
uint32_t hisize;
};
static_assert(sizeof(GuestIoStat) == 40u);
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
@@ -135,6 +150,7 @@ namespace
TempPaths paths;
std::vector<uint8_t> rdram;
R5900Context ctx;
PS2Runtime runtime;
TestContext() : paths(makeTempPaths()), rdram(PS2_RAM_SIZE, 0)
{
@@ -152,6 +168,112 @@ void register_ps2_runtime_io_tests()
{
MiniTest::Case("PS2RuntimeIO", [](TestCase &tc)
{
tc.Run("ROM0 ROMVER is exposed as the 14-byte firmware pseudo-file", [](TestCase &t)
{
TestContext test;
constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START;
constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START;
constexpr char kExpectedRomVersion[] = "0200AC20040614";
static_assert(sizeof(kExpectedRomVersion) - 1u == 14u);
writeGuestString(test.rdram.data(), kPathAddr, "rom0:ROMVER");
std::memset(test.rdram.data() + kBufferAddr, 0xA5, 16u);
setRegU32(test.ctx, 4, kPathAddr);
setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t fd = getRegS32(&test.ctx, 2);
t.IsTrue(fd >= 0, "fioOpen should recognize rom0:ROMVER without a host file");
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, kBufferAddr);
setRegU32(test.ctx, 6, 14u);
fioRead(test.rdram.data(), &test.ctx, &test.runtime);
t.Equals(getRegS32(&test.ctx, 2), 14, "fioRead should return the complete ROMVER payload");
t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr,
kExpectedRomVersion,
sizeof(kExpectedRomVersion) - 1u) == 0,
"ROMVER should use the normal consumer-console format");
t.Equals(static_cast<uint32_t>(test.rdram[kBufferAddr + 14u]), 0xA5u,
"ROMVER reads must not append a terminator");
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
t.Equals(getRegS32(&test.ctx, 2), 0, "fioClose should release the ROMVER descriptor");
});
tc.Run("ROM0 profiles can extend and override files without a BIOS", [](TestCase &t)
{
PS2RomProfile profile;
profile.id = "runtime-io-test";
profile.provider = "test-extension";
profile.matcher.elfName = "rom_profile_test.elf";
profile.files["CONFIG"] = {'p', 'r', 'o', 'f', 'i', 'l', 'e'};
profile.files["ROMVER"] = {'9', '9', '9', '9', 'T', '2', '0', '2', '6', '0', '8', '2', '4', 'X'};
PS2RomDevice::registerProfile(std::move(profile));
TestContext test;
std::string error;
t.IsTrue(test.runtime.romDevice().configure({"rom_profile_test.elf", 0u, 0u}, &error),
"a uniquely matched ROM0 profile should configure");
t.Equals(std::string(test.runtime.romDevice().activeProvider()), std::string("test-extension"),
"the selected ROM0 profile should expose its provider");
constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START;
constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START;
writeGuestString(test.rdram.data(), kPathAddr, "rom0:CONFIG");
setRegU32(test.ctx, 4, kPathAddr);
setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t fd = getRegS32(&test.ctx, 2);
t.IsTrue(fd >= 0, "a profile-provided ROM0 file should open through normal FileIO");
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, kBufferAddr);
setRegU32(test.ctx, 6, 7u);
fioRead(test.rdram.data(), &test.ctx, &test.runtime);
t.Equals(getRegS32(&test.ctx, 2), 7, "profile-provided ROM0 bytes should be readable");
t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr, "profile", 7u) == 0,
"ROM0 profile contents should reach the guest unchanged");
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
});
tc.Run("ROM0 uses VFS stat and per-runtime descriptors", [](TestCase &t)
{
TestContext owner;
TestContext other;
constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START;
constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START;
constexpr uint32_t kStatAddr = GUEST_BUFFER_AREA_START + 0x100u;
writeGuestString(owner.rdram.data(), kPathAddr, "rom0:ROMVER");
setRegU32(owner.ctx, 4, kPathAddr);
setRegU32(owner.ctx, 5, PS2_FIO_O_RDONLY);
fioOpen(owner.rdram.data(), &owner.ctx, &owner.runtime);
const int32_t fd = getRegS32(&owner.ctx, 2);
t.IsTrue(fd >= 3, "ROM0 should return a normal VFS descriptor");
setRegU32(owner.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(owner.ctx, 5, kBufferAddr);
setRegU32(owner.ctx, 6, 4u);
fioRead(owner.rdram.data(), &owner.ctx, &other.runtime);
t.Equals(getRegS32(&owner.ctx, 2), -1,
"a descriptor must not leak into a different runtime instance");
setRegU32(owner.ctx, 4, kPathAddr);
setRegU32(owner.ctx, 5, kStatAddr);
fioGetstat(owner.rdram.data(), &owner.ctx, &owner.runtime);
t.Equals(getRegS32(&owner.ctx, 2), 0, "fioGetstat should see ROM0 virtual files");
GuestIoStat stat{};
std::memcpy(&stat, owner.rdram.data() + kStatAddr, sizeof(stat));
t.Equals(stat.size, 14u, "ROMVER stat should report its exact payload size");
t.Equals(stat.mode & 0x38u, 0x10u, "ROMVER should be reported as an ioman regular file");
setRegU32(owner.ctx, 4, static_cast<uint32_t>(fd));
fioClose(owner.rdram.data(), &owner.ctx, &owner.runtime);
});
tc.Run("mc0 directory creation", [](TestCase &t)
{
TestContext test;
@@ -161,7 +283,7 @@ void register_ps2_runtime_io_tests()
writeGuestString(test.rdram.data(), dirAddr, dirPath);
setRegU32(test.ctx, 4, dirAddr);
fioMkdir(test.rdram.data(), &test.ctx, nullptr);
fioMkdir(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t result = getRegS32(&test.ctx, 2);
t.IsTrue(result >= 0, "fioMkdir should succeed for mc0: directory");
@@ -182,7 +304,7 @@ void register_ps2_runtime_io_tests()
const uint32_t dirAddr = GUEST_STRING_AREA_START;
writeGuestString(test.rdram.data(), dirAddr, dirPath);
setRegU32(test.ctx, 4, dirAddr);
fioMkdir(test.rdram.data(), &test.ctx, nullptr);
fioMkdir(test.rdram.data(), &test.ctx, &test.runtime);
// Test: open file for writing
const std::string filePath = "mc0:/SAVEDATA/test.txt";
@@ -191,7 +313,7 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 4, fileAddr);
setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC);
fioOpen(test.rdram.data(), &test.ctx, nullptr);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t fd = getRegS32(&test.ctx, 2);
t.IsTrue(fd >= 0, "fioOpen should return valid file descriptor");
@@ -204,7 +326,7 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, bufAddr);
setRegU32(test.ctx, 6, static_cast<uint32_t>(payload.size()));
fioWrite(test.rdram.data(), &test.ctx, nullptr);
fioWrite(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t bytesWritten = getRegS32(&test.ctx, 2);
t.Equals(bytesWritten, static_cast<int32_t>(payload.size()),
@@ -212,7 +334,7 @@ void register_ps2_runtime_io_tests()
// Close file
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, nullptr);
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t closeResult = getRegS32(&test.ctx, 2);
t.IsTrue(closeResult >= 0, "fioClose should succeed");
@@ -239,7 +361,7 @@ void register_ps2_runtime_io_tests()
const uint32_t dirAddr = GUEST_STRING_AREA_START;
writeGuestString(test.rdram.data(), dirAddr, dirPath);
setRegU32(test.ctx, 4, dirAddr);
fioMkdir(test.rdram.data(), &test.ctx, nullptr);
fioMkdir(test.rdram.data(), &test.ctx, &test.runtime);
const std::string filePath = "mc0:/SAVEDATA/test.txt";
const uint32_t fileAddr = GUEST_STRING_AREA_START + 0x100;
@@ -252,21 +374,21 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 4, fileAddr);
setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC);
fioOpen(test.rdram.data(), &test.ctx, nullptr);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
int32_t fd = getRegS32(&test.ctx, 2);
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, writeBufAddr);
setRegU32(test.ctx, 6, static_cast<uint32_t>(payload.size()));
fioWrite(test.rdram.data(), &test.ctx, nullptr);
fioWrite(test.rdram.data(), &test.ctx, &test.runtime);
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, nullptr);
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
// Test: read back via fioRead
setRegU32(test.ctx, 4, fileAddr);
setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY);
fioOpen(test.rdram.data(), &test.ctx, nullptr);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
fd = getRegS32(&test.ctx, 2);
t.IsTrue(fd >= 0, "fioOpen for reading should succeed");
@@ -277,7 +399,7 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, readBufAddr);
setRegU32(test.ctx, 6, static_cast<uint32_t>(payload.size()));
fioRead(test.rdram.data(), &test.ctx, nullptr);
fioRead(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t bytesRead = getRegS32(&test.ctx, 2);
t.Equals(bytesRead, static_cast<int32_t>(payload.size()),
@@ -290,7 +412,7 @@ void register_ps2_runtime_io_tests()
t.Equals(readback, payload, "fioRead content should match original");
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, nullptr);
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
});
tc.Run("mc0 paths isolated from cdRoot", [](TestCase &t)
@@ -307,11 +429,11 @@ void register_ps2_runtime_io_tests()
// Create directory and file on mc0:
setRegU32(test.ctx, 4, dirAddr);
fioMkdir(test.rdram.data(), &test.ctx, nullptr);
fioMkdir(test.rdram.data(), &test.ctx, &test.runtime);
setRegU32(test.ctx, 4, fileAddr);
setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC);
fioOpen(test.rdram.data(), &test.ctx, nullptr);
fioOpen(test.rdram.data(), &test.ctx, &test.runtime);
const int32_t fd = getRegS32(&test.ctx, 2);
const std::string payload = "isolation test";
@@ -321,10 +443,10 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
setRegU32(test.ctx, 5, bufAddr);
setRegU32(test.ctx, 6, static_cast<uint32_t>(payload.size()));
fioWrite(test.rdram.data(), &test.ctx, nullptr);
fioWrite(test.rdram.data(), &test.ctx, &test.runtime);
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, nullptr);
fioClose(test.rdram.data(), &test.ctx, &test.runtime);
// Verify isolation
const std::filesystem::path expectedMc =
+31 -1
View File
@@ -549,6 +549,29 @@ void register_ps2_runtime_kernel_tests()
{
MiniTest::Case("PS2RuntimeKernel", [](TestCase &tc)
{
tc.Run("unsigned loads and ABI word writes extend independently", [](TestCase &t)
{
constexpr uint64_t kUpper = 0x1122334455667788ull;
R5900Context ctx{};
ctx.r[2] = _mm_set_epi64x(static_cast<int64_t>(kUpper), 0);
SET_GPR_ZE32(&ctx, 2, 0x80000000u);
t.Equals(static_cast<uint64_t>(_mm_extract_epi64(ctx.r[2], 0)),
0x0000000080000000ull,
"SET_GPR_ZE32 must zero-extend values used by LWU/LHU/LBU");
t.Equals(static_cast<uint64_t>(_mm_extract_epi64(ctx.r[2], 1)),
kUpper,
"SET_GPR_ZE32 must preserve the upper 64 bits of the 128-bit GPR");
SET_GPR_U32(&ctx, 2, 0x80000000u);
t.Equals(static_cast<uint64_t>(_mm_extract_epi64(ctx.r[2], 0)),
0xFFFFFFFF80000000ull,
"SET_GPR_U32 must retain the existing EE 32-bit ABI extension semantics");
t.Equals(static_cast<uint64_t>(_mm_extract_epi64(ctx.r[2], 1)),
kUpper,
"SET_GPR_U32 must preserve the upper 64 bits of the 128-bit GPR");
});
tc.Run("CreateThread and CreateSema decode the exact PS2SDK EE layouts", [](TestCase &t)
{
TestEnv env;
@@ -977,7 +1000,7 @@ void register_ps2_runtime_kernel_tests()
t.Equals(getRegS32(env.ctx, 2), -4, "__divdi3 should divide signed 64-bit values");
});
tc.Run("ReleaseAlarm aliases CancelAlarm and cache toggles succeed", [](TestCase &t)
tc.Run("ReleaseAlarm aliases CancelAlarm and cache syscalls succeed", [](TestCase &t)
{
TestEnv env;
@@ -1005,6 +1028,13 @@ void register_ps2_runtime_kernel_tests()
DisableCache(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DisableCache should succeed as a no-op");
setRegU32(env.ctx, 2, 0xDEADBEEFu);
setRegU32(env.ctx, 4, 0u); // PS2SDK WRITEBACK_DCACHE
t.IsTrue(callSyscall(static_cast<uint32_t>(-0x68), env.rdram.data(), &env.ctx, &env.runtime),
"-0x68 should dispatch iFlushCache");
t.Equals(getRegS32(env.ctx, 2), KE_OK,
"iFlushCache should succeed when guest and host memory are coherent");
});
tc.Run("setup heap and thread invalid ids use documented kernel errors", [](TestCase &t)
+42 -34
View File
@@ -111,6 +111,12 @@ namespace
return value;
}
void writeIopS16(PS2Runtime &runtime, uint32_t addr, int16_t value)
{
if (!runtime.writeIopMemory(addr, &value, sizeof(value)))
throw std::runtime_error("failed to write IOP test memory");
}
uint32_t g_dmacHandlerWriteAddr = 0u;
uint32_t g_dmacHandlerValue = 0u;
uint32_t g_dmacHandlerLastCause = 0u;
@@ -176,7 +182,7 @@ void register_ps2_sif_dma_tests()
payload[i] = static_cast<uint8_t>(0x30u + i);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kDstAddr, 0, payload.size());
std::memset(env.rdram.data() + kDstAddr, 0x5A, payload.size());
const Ps2SifDmaTransfer desc{
kSrcAddr,
@@ -191,8 +197,15 @@ void register_ps2_sif_dma_tests()
const int32_t dmaId = getRegS32(env.ctx, 2);
t.IsTrue(dmaId > 0, "sceSifSetDma should return a positive transfer id on success");
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0,
"sceSifSetDma should copy transfer payload to destination");
std::array<uint8_t, 16> iopReadback{};
t.IsTrue(env.runtime.readIopMemory(kDstAddr, iopReadback.data(), iopReadback.size()) &&
iopReadback == payload,
"sceSifSetDma should copy EE payload into IOP RAM");
const std::array<uint8_t, 16> eeSentinel = {
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A};
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, eeSentinel.data(), eeSentinel.size()) == 0,
"sceSifSetDma must not alias an equal-numbered EE address");
setRegU32(env.ctx, 4, static_cast<uint32_t>(dmaId));
ps2_stubs::sceSifDmaStat(env.rdram.data(), &env.ctx, &env.runtime);
@@ -206,7 +219,6 @@ void register_ps2_sif_dma_tests()
constexpr uint32_t kDescAddr = 0x00020040u;
constexpr uint32_t kSrcAddr = 0x00020140u;
constexpr uint32_t kRoundTripAddr = 0x00020240u;
constexpr uint32_t kFormerAliasAddr = 0x01A53880u;
constexpr uint32_t kIopBlockSize = 0x880u;
std::array<uint8_t, 32> payload{};
@@ -216,13 +228,13 @@ void register_ps2_sif_dma_tests()
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kRoundTripAddr, 0, payload.size());
std::memset(env.rdram.data() + kFormerAliasAddr, 0x5Au, payload.size());
setRegU32(env.ctx, 4, kIopBlockSize);
ps2_stubs::sceSifAllocIopHeap(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t iopAddress = ::getRegU32(&env.ctx, 2);
t.IsTrue(iopAddress >= PS2_RAM_SIZE,
"sceSifAllocIopHeap should return an address outside EE RDRAM");
t.IsTrue(iopAddress >= 0x00120000u && iopAddress < 0x00200000u,
"sceSifAllocIopHeap should return an address in physical IOP RAM");
std::memset(env.rdram.data() + iopAddress, 0x5Au, payload.size());
Ps2SifDmaTransfer desc{
kSrcAddr,
@@ -241,32 +253,25 @@ void register_ps2_sif_dma_tests()
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A};
t.IsTrue(std::memcmp(env.rdram.data() + kFormerAliasAddr,
t.IsTrue(std::memcmp(env.rdram.data() + iopAddress,
aliasSentinel.data(), aliasSentinel.size()) == 0,
"IOP DMA must not overwrite the old 0x01A00000 EE alias range");
"IOP DMA must not overwrite the equal-numbered EE range");
PS2IopHostAdapter host(env.runtime);
auto scope = host.enterCall(&env.ctx, env.rdram.data());
uint32_t normalized = 0u;
std::array<uint8_t, 32> hostReadback{};
t.IsTrue(host.normalizeGuestAddress(iopAddress, normalized) &&
normalized == iopAddress,
"IOP modules should preserve private IOP heap addresses");
t.IsTrue(host.readGuest(iopAddress, hostReadback.data(), hostReadback.size()) &&
t.IsTrue(host.readIopMemory(iopAddress, hostReadback.data(), hostReadback.size()) &&
hostReadback == payload,
"IOP modules should read the private heap backing");
"IOP modules should read the shared physical IOP RAM");
desc = {
iopAddress,
kRoundTripAddr,
static_cast<int32_t>(payload.size()),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0,
"IOP-to-EE DMA should accept a private IOP heap source");
constexpr uint32_t kRdAddr = 0x00020340u;
setRegU32(env.ctx, 4, kRdAddr);
setRegU32(env.ctx, 5, iopAddress);
setRegU32(env.ctx, 6, kRoundTripAddr);
setRegU32(env.ctx, 7, static_cast<uint32_t>(payload.size()));
ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0,
"IOP-to-EE transfer should accept a physical IOP source");
t.IsTrue(std::memcmp(env.rdram.data() + kRoundTripAddr,
payload.data(), payload.size()) == 0,
"IOP-to-EE DMA should round-trip the payload");
@@ -286,7 +291,7 @@ void register_ps2_sif_dma_tests()
payload[i] = static_cast<uint8_t>(0x50u + i);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kDstAddr, 0, payload.size());
std::memset(env.rdram.data() + kDstAddr, 0x5A, payload.size());
const Ps2SifDmaTransfer desc{
kSrcAddr,
@@ -299,8 +304,10 @@ void register_ps2_sif_dma_tests()
setRegU32(env.ctx, 5, 1u);
ps2_stubs::isceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "isceSifSetDma should report a successful transfer id");
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0,
"isceSifSetDma should copy transfer payload like sceSifSetDma");
std::array<uint8_t, 12> iopReadback{};
t.IsTrue(env.runtime.readIopMemory(kDstAddr, iopReadback.data(), iopReadback.size()) &&
iopReadback == payload,
"isceSifSetDma should copy EE payload into IOP RAM");
ps2_stubs::isceSifSetDChain(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "isceSifSetDChain should mirror sceSifSetDChain");
@@ -958,7 +965,8 @@ void register_ps2_sif_dma_tests()
{
payload[i] = static_cast<uint8_t>((i * 7u) & 0xFFu);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
t.IsTrue(env.runtime.writeIopMemory(kSrcAddr, payload.data(), payload.size()),
"test setup should populate physical IOP RAM");
std::memset(env.rdram.data() + kDstAddr, 0, payload.size());
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
@@ -1017,8 +1025,8 @@ void register_ps2_sif_dma_tests()
std::memset(env.rdram.data() + kDstAddr, 0, kSize);
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
writeGuestS16(env.rdram.data(), kSrcAddr + kSeSumOffset + (kBank * 2u), static_cast<int16_t>(0x1357));
writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast<int16_t>(0x2468));
writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kBank * 2u), static_cast<int16_t>(0x1357));
writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast<int16_t>(0x2468));
writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kBank * 2u), static_cast<int16_t>(0x7B7B));
writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kBank * 2u), static_cast<int16_t>(0x6A6A));
@@ -1078,8 +1086,8 @@ void register_ps2_sif_dma_tests()
std::memset(env.rdram.data() + kDstAddr, 0, kSize);
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
writeGuestS16(env.rdram.data(), kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x1111));
writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x2222));
writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x1111));
writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x2222));
writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kPendingBank * 2u), static_cast<int16_t>(0x3333));
writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kPendingBank * 2u), static_cast<int16_t>(0x4444));
+49 -3
View File
@@ -333,6 +333,41 @@ void register_ps2_sif_rpc_tests()
"SifInitRpc must not reboot or reset the IOP");
});
tc.Run("SifLoadModule validates ROM modules and activates their HLE service", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kPathAddress = 0x00021000u;
const auto load = [&](std::string_view path)
{
std::memcpy(env.rdram.data() + kPathAddress, path.data(), path.size());
env.rdram[kPathAddress + path.size()] = 0u;
setRegU32(env.ctx, 4, kPathAddress);
setRegU32(env.ctx, 5, 0u);
setRegU32(env.ctx, 6, 0u);
SifLoadModule(env.rdram.data(), &env.ctx, &env.runtime);
return getRegS32(env.ctx, 2);
};
t.Equals(load("rom0:NOT_A_REAL_MODULE"), -1,
"SifLoadModule must reject unknown ROM modules instead of fabricating success");
const int32_t libsdId = load("rom0:LIBSD");
t.IsTrue(libsdId > 0, "registered no-BIOS ROM module should receive a real managed ID");
const auto snapshot = env.runtime.iopDebugSnapshot();
bool libsdActive = false;
for (const auto &service : snapshot.services)
{
if (service.name == "libsd")
{
libsdActive = service.active;
break;
}
}
t.IsTrue(libsdActive, "loading LIBSD should activate its HLE RPC route");
});
tc.Run("emulated RPC bind waits for a registered IOP server", [](TestCase &t)
{
TestEnv env;
@@ -511,6 +546,8 @@ void register_ps2_sif_rpc_tests()
tc.Run("MCSERV RPC init and get info report a formatted PS2 card", [](TestCase &t)
{
TestEnv env;
const auto mcservModule = env.runtime.loadIopModule("rom0:MCSERV");
t.IsTrue(mcservModule.moduleId > 0, "MCSERV test should load its IOP module first");
ScopedTempDir temp("mcserv_rpc");
const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths();
@@ -569,6 +606,8 @@ void register_ps2_sif_rpc_tests()
tc.Run("DBCMAN version RPC returns the 3.20 compatibility response", [](TestCase &t)
{
TestEnv env;
const auto dbcmanModule = env.runtime.loadIopModule("rom0:DBCMAN");
t.IsTrue(dbcmanModule.moduleId > 0, "DBCMAN test should load its IOP module first");
constexpr uint32_t kDbcManSid = 0x80001300u;
constexpr uint32_t kCheckVersionRpc = 0x80001363u;
@@ -594,6 +633,8 @@ void register_ps2_sif_rpc_tests()
tc.Run("LIBSD RPC routes through the IOP audio service", [](TestCase &t)
{
TestEnv env;
const auto libsdModule = env.runtime.loadIopModule("rom0:LIBSD");
t.IsTrue(libsdModule.moduleId > 0, "LIBSD test should load its IOP module first");
constexpr uint32_t kSetVoiceRpc = 0x8010u;
constexpr uint32_t kSendAddr = 0x00035B00u;
@@ -1092,9 +1133,14 @@ void register_ps2_sif_rpc_tests()
t.IsTrue(addrTableAddr > 0u && addrTableAddr < 0x00200000u,
"rpc 0x13 should return a low guest address like an IOP pointer");
const uint32_t hdBaseAddr = readGuestStruct<uint32_t>(env.rdram.data(), addrTableAddr + 0u);
const uint32_t sqBaseAddr = readGuestStruct<uint32_t>(env.rdram.data(), addrTableAddr + 4u);
const uint32_t dataBaseAddr = readGuestStruct<uint32_t>(env.rdram.data(), addrTableAddr + 8u);
std::array<uint32_t, 3> addressTable{};
t.IsTrue(env.runtime.readIopMemory(addrTableAddr,
addressTable.data(),
sizeof(addressTable)),
"the sound-driver address table should live in physical IOP RAM");
const uint32_t hdBaseAddr = addressTable[0];
const uint32_t sqBaseAddr = addressTable[1];
const uint32_t dataBaseAddr = addressTable[2];
t.IsTrue(hdBaseAddr > 0u && hdBaseAddr < 0x00200000u,
"sound-driver hd base should stay in low guest address space");
t.IsTrue(sqBaseAddr > hdBaseAddr && sqBaseAddr < 0x00200000u,