From 2a61ba0df33b89287aa081d674244c63b5feac30 Mon Sep 17 00:00:00 2001 From: Ran-j Date: Wed, 2 Sep 2026 18:06:13 -0300 Subject: [PATCH] feat: added a lot of tests --- ps2xTest/src/code_generator_tests.cpp | 29 + ps2xTest/src/elf_analyzer_tests.cpp | 2 + ps2xTest/src/ps2_gs_tests.cpp | 125 +++- ps2xTest/src/ps2_iop_tests.cpp | 119 +++- ps2xTest/src/ps2_memory_tests.cpp | 289 ++++++++- ps2xTest/src/ps2_recompiler_tests.cpp | 590 ++++++++++++++++++- ps2xTest/src/ps2_runtime_expansion_tests.cpp | 26 + ps2xTest/src/ps2_runtime_interrupt_tests.cpp | 75 +++ ps2xTest/src/ps2_runtime_io_tests.cpp | 154 ++++- ps2xTest/src/ps2_runtime_kernel_tests.cpp | 32 +- ps2xTest/src/ps2_sif_dma_tests.cpp | 76 +-- ps2xTest/src/ps2_sif_rpc_tests.cpp | 52 +- 12 files changed, 1472 insertions(+), 97 deletions(-) diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index f767849..23bef4a 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -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"; diff --git a/ps2xTest/src/elf_analyzer_tests.cpp b/ps2xTest/src/elf_analyzer_tests.cpp index 0a6dc10..8cba9b5 100644 --- a/ps2xTest/src/elf_analyzer_tests.cpp +++ b/ps2xTest/src/elf_analyzer_tests.cpp @@ -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"), diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 8343775..0458a8c 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -1885,7 +1885,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(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 vram(PS2_GS_VRAM_SIZE, 0u); GS gs; @@ -2612,7 +2613,10 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(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(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((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 vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kTextureTbp = 32u; // Physical GS page 1. + constexpr uint64_t kFrame = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kScissor = 2ull << 16; + constexpr uint64_t kTex0 = + (static_cast(kTextureTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_CT32) << 20) | + (1ull << 34) | + (1ull << 35); + constexpr uint64_t kPrim = + static_cast(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(x * 16u); + const uint64_t xy1 = static_cast((x + 1u) * 16u) | + (static_cast(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(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME @@ -2756,7 +2829,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (17ull << 56); + (17ull << 56) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | @@ -2817,7 +2891,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT16) << 51) | - (16ull << 56); + (16ull << 56) | + (1ull << 61); constexpr uint64_t kTexa = (0x80ull << 32); constexpr uint64_t kPrim = static_cast(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(kWrongClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kTex2 = (static_cast(GS_PSM_T8) << 20) | (static_cast(kExpectedClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | @@ -3016,7 +3097,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(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(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4) | @@ -4369,7 +4452,8 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbpA) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(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(kClutCbpB) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); gs.writeRegister(GS_REG_TEX0_1, kTex0HH); gs.writeRegister(GS_REG_UV, 0ull); diff --git a/ps2xTest/src/ps2_iop_tests.cpp b/ps2xTest/src/ps2_iop_tests.cpp index d75b213..dd3c846 100644 --- a/ps2xTest/src/ps2_iop_tests.cpp +++ b/ps2xTest/src/ps2_iop_tests.cpp @@ -1,5 +1,6 @@ #include "MiniTest.h" #include "ps2x/iop/iop_subsystem.h" +#include "ps2x/iop/ps2_path.h" #include #include @@ -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(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(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(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 memory; + std::vector iopMemory; uint32_t nextHandle = 0x8000u; uint32_t nextGuestAddress = 0x4000u; std::vector 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(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"); }); diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 7c3e8ea..0b22fe0 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -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(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(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(0xA0u + i); + } + + std::vector> 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(1u), + "REF tag high-half DIRECT should emit one GIF packet"); + if (!captured.empty()) + { + t.Equals(captured[0].size(), static_cast(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(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(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(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 causes = mem.consumeCompletedDmacCauses(); + t.Equals(causes.size(), static_cast(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(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(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 causes = mem.consumeCompletedDmacCauses(); + t.Equals(causes.size(), static_cast(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(::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(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(1u) << 16) | + (static_cast(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 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(0xC0u + i)); + + mem.processVIF1Data(packet.data(), static_cast(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(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; diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 9b5ec0a..86e5e3f 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -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 textWords{0x03E00008u, 0x00000000u}; + text->set_data(reinterpret_cast(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 vuWords{0x01EC48BDu, 0u, 0x01FA717Du, 0u}; + vuText->set_data(reinterpret_cast(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 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(textWords.data()), + static_cast(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 textWords{}; + std::array 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(textWords.data()), static_cast(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 tableWords{}; + std::array 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(tableWords.data()), static_cast(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 singletonCallbacks{}; + singletonCallbacks[0] = 0x00100580u; + singletonCallbacks[16] = 0x00100588u; + data->set_data(reinterpret_cast(singletonCallbacks.data()), + static_cast(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 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(abbrevBytes.data()), + static_cast(abbrevBytes.size())); + + std::vector 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(&value); + infoBytes.insert(infoBytes.end(), bytes, bytes + sizeof(value)); + }; + auto appendU32 = [&infoBytes](uint32_t value) + { + const auto *bytes = reinterpret_cast(&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(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(infoBytes.data()), + static_cast(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 &skip, - const std::vector &stubs = {}) + const std::vector &stubs = {}, + const std::vector &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(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(registrationFile), + std::istreambuf_iterator()}; + 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(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(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 §ion) + { 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(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(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"); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index ad7df46..4772f3f 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -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 rdram(PS2_RAM_SIZE, 0u); diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index 5defc89..4261bfa 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include 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 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; diff --git a/ps2xTest/src/ps2_runtime_io_tests.cpp b/ps2xTest/src/ps2_runtime_io_tests.cpp index 1e92051..4f979be 100644 --- a/ps2xTest/src/ps2_runtime_io_tests.cpp +++ b/ps2xTest/src/ps2_runtime_io_tests.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -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(value)); @@ -135,6 +150,7 @@ namespace TempPaths paths; std::vector 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(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(test.rdram[kBufferAddr + 14u]), 0xA5u, + "ROMVER reads must not append a terminator"); + + setRegU32(test.ctx, 4, static_cast(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(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(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(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(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(fd)); setRegU32(test.ctx, 5, bufAddr); setRegU32(test.ctx, 6, static_cast(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(payload.size()), @@ -212,7 +334,7 @@ void register_ps2_runtime_io_tests() // Close file setRegU32(test.ctx, 4, static_cast(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(fd)); setRegU32(test.ctx, 5, writeBufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioWrite(test.rdram.data(), &test.ctx, nullptr); + fioWrite(test.rdram.data(), &test.ctx, &test.runtime); setRegU32(test.ctx, 4, static_cast(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(fd)); setRegU32(test.ctx, 5, readBufAddr); setRegU32(test.ctx, 6, static_cast(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(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(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(fd)); setRegU32(test.ctx, 5, bufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioWrite(test.rdram.data(), &test.ctx, nullptr); + fioWrite(test.rdram.data(), &test.ctx, &test.runtime); setRegU32(test.ctx, 4, static_cast(fd)); - fioClose(test.rdram.data(), &test.ctx, nullptr); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); // Verify isolation const std::filesystem::path expectedMc = diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 2542d48..63b12d9 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -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(kUpper), 0); + + SET_GPR_ZE32(&ctx, 2, 0x80000000u); + t.Equals(static_cast(_mm_extract_epi64(ctx.r[2], 0)), + 0x0000000080000000ull, + "SET_GPR_ZE32 must zero-extend values used by LWU/LHU/LBU"); + t.Equals(static_cast(_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(_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(_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(-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) diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 46eddea..ffdeff3 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -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(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 iopReadback{}; + t.IsTrue(env.runtime.readIopMemory(kDstAddr, iopReadback.data(), iopReadback.size()) && + iopReadback == payload, + "sceSifSetDma should copy EE payload into IOP RAM"); + const std::array 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(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 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 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(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(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(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 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((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(0x1357)); - writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast(0x2468)); + writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kBank * 2u), static_cast(0x1357)); + writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast(0x2468)); writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kBank * 2u), static_cast(0x7B7B)); writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kBank * 2u), static_cast(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(0x1111)); - writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast(0x2222)); + writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast(0x1111)); + writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast(0x2222)); writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kPendingBank * 2u), static_cast(0x3333)); writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kPendingBank * 2u), static_cast(0x4444)); diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index b4fd898..88383e3 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -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(env.rdram.data(), addrTableAddr + 0u); - const uint32_t sqBaseAddr = readGuestStruct(env.rdram.data(), addrTableAddr + 4u); - const uint32_t dataBaseAddr = readGuestStruct(env.rdram.data(), addrTableAddr + 8u); + std::array 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,