diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index f8d3e4437a..6a17f0dd1e 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -62,6 +62,11 @@ if(PS2X_IOP_BUILD_TESTS) target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests) + add_executable(ps2_iop_memory_tests tests/iop_memory_tests.cpp) + target_link_libraries(ps2_iop_memory_tests PRIVATE ps2_iop) + target_include_directories(ps2_iop_memory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME ps2_iop_memory_tests COMMAND ps2_iop_memory_tests) + endif() install(TARGETS ps2_iop diff --git a/ps2xIOP/src/emulator/core/iop_memory.cpp b/ps2xIOP/src/emulator/core/iop_memory.cpp index 25e9b36d19..7c872fe20a 100644 --- a/ps2xIOP/src/emulator/core/iop_memory.cpp +++ b/ps2xIOP/src/emulator/core/iop_memory.cpp @@ -269,9 +269,23 @@ namespace ps2x::iop::detail const uint32_t wordsPerBlock = std::max(blockControl & 0xFFFFu, 1u); const uint32_t blockCount = std::max(blockControl >> 16u, 1u); const uint64_t transferWords = static_cast(wordsPerBlock) * blockCount; + + // With auto-DMA (ADMAS at 0x1F9001B0 / 0x1F9005B0) the SPU2 pulls PCM + // only as fast as it plays it: 48 kHz stereo 16-bit = 192000 bytes/s, + // i.e. 192 IOP cycles per byte. Completing at bus speed instead makes + // sceSdBlockTrans streams (software mixers) refill back-to-back and + // keeps the IOP 100% busy. + const uint32_t admasAddress = secondCore ? 0x1F9005B0u : 0x1F9001B0u; + uint32_t admas = 0u; + if (const auto current = m_hardware.find(admasAddress); current != m_hardware.end()) + admas = current->second & 0xFFFFu; + constexpr uint64_t kAutoDmaCyclesPerByte = 192u; + const uint64_t delayCycles = (admas & 3u) != 0u + ? transferWords * 4u * kAutoDmaCyclesPerByte + : std::max(transferWords * 2u, 64u); m_dmaStart = DmaStart{ secondCore ? kDmaSpu1Irq : kDmaSpu0Irq, - std::max(transferWords * 2u, 64u), + delayCycles, }; } diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp index e5f63a9fe2..ffb8693067 100644 --- a/ps2xIOP/src/emulator/iop_emulator.cpp +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -654,8 +654,11 @@ namespace ps2x::iop::detail ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) { + // Real sceSifLoadModuleBuffer addresses IOP RAM; keep accepting EE + // addresses for callers that hand over an image still in EE RAM. std::vector image; - if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image)) + if (!IopModuleLoader::readElfFromIop(memory, guestAddress, image) && + !IopModuleLoader::readElfFromGuest(host, guestAddress, image)) return {true, -1, -1}; std::ostringstream tag; tag << "buffer@0x" << std::hex << guestAddress; diff --git a/ps2xIOP/src/emulator/services/iop_module_loader.cpp b/ps2xIOP/src/emulator/services/iop_module_loader.cpp index 80e59d6bc3..306bf0702a 100644 --- a/ps2xIOP/src/emulator/services/iop_module_loader.cpp +++ b/ps2xIOP/src/emulator/services/iop_module_loader.cpp @@ -298,6 +298,57 @@ namespace ps2x::iop::detail } } + namespace + { + template + bool readElfImage(ReadFn &&read, uint32_t guestAddress, std::vector &bytes) + { + Elf32Ehdr header{}; + if (!read(guestAddress, &header, sizeof(header)) || !validElfHeader(header)) + return false; + + uint64_t required = sizeof(header); + required = std::max(required, static_cast(header.phoff) + static_cast(header.phentsize) * header.phnum); + required = std::max(required, static_cast(header.shoff) + static_cast(header.shentsize) * header.shnum); + + if (required > kMaxImageSize) + return false; // Should we log an error here? TODO check later + + bytes.resize(static_cast(required)); + if (!read(guestAddress, bytes.data(), bytes.size())) + return false; + + if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr)) + { + for (uint16_t i = 0; i < header.shnum; ++i) + { + Elf32Shdr section{}; + const size_t offset = static_cast(header.shoff) + static_cast(i) * header.shentsize; + std::memcpy(§ion, bytes.data() + offset, sizeof(section)); + if (section.type != SHT_NOBITS) + { + required = std::max(required, static_cast(section.offset) + section.size); + } + } + } + if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr)) + { + for (uint16_t i = 0; i < header.phnum; ++i) + { + Elf32Phdr program{}; + const size_t offset = static_cast(header.phoff) + static_cast(i) * header.phentsize; + std::memcpy(&program, bytes.data() + offset, sizeof(program)); + required = std::max(required, static_cast(program.offset) + program.filesz); + } + } + if (required > kMaxImageSize) + return false; + + bytes.resize(static_cast(required)); + return read(guestAddress, bytes.data(), bytes.size()); + } + } + bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector &bytes) { const std::string translated = host.translateGuestPath(guestPath); @@ -322,49 +373,16 @@ namespace ps2x::iop::detail bool IopModuleLoader::readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector &bytes) { - Elf32Ehdr header{}; - if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header)) - return false; - - uint64_t required = sizeof(header); - required = std::max(required, static_cast(header.phoff) + static_cast(header.phentsize) * header.phnum); - required = std::max(required, static_cast(header.shoff) + static_cast(header.shentsize) * header.shnum); - - if (required > kMaxImageSize) - return false; // Should we log an error here? TODO check later - - bytes.resize(static_cast(required)); - if (!host.readGuest(guestAddress, bytes.data(), bytes.size())) - return false; - - if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr)) - { - for (uint16_t i = 0; i < header.shnum; ++i) - { - Elf32Shdr section{}; - const size_t offset = static_cast(header.shoff) + static_cast(i) * header.shentsize; - std::memcpy(§ion, bytes.data() + offset, sizeof(section)); - if (section.type != SHT_NOBITS) - { - required = std::max(required, static_cast(section.offset) + section.size); - } - } - } - if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr)) - { - for (uint16_t i = 0; i < header.phnum; ++i) - { - Elf32Phdr program{}; - const size_t offset = static_cast(header.phoff) + static_cast(i) * header.phentsize; - std::memcpy(&program, bytes.data() + offset, sizeof(program)); - required = std::max(required, static_cast(program.offset) + program.filesz); - } - } - if (required > kMaxImageSize) - return false; + return readElfImage([&host](uint32_t address, void *destination, size_t size) + { return host.readGuest(address, destination, size); }, + guestAddress, bytes); + } - bytes.resize(static_cast(required)); - return host.readGuest(guestAddress, bytes.data(), bytes.size()); + bool IopModuleLoader::readElfFromIop(const IopMemory &memory, uint32_t iopAddress, std::vector &bytes) + { + return readElfImage([&memory](uint32_t address, void *destination, size_t size) + { return memory.readRam(address, destination, size); }, + iopAddress, bytes); } IopImageLoadResult IopModuleLoader::load(std::span image, IopMemory &memory, uint32_t moduleCursor) diff --git a/ps2xIOP/src/emulator/services/iop_module_loader.h b/ps2xIOP/src/emulator/services/iop_module_loader.h index 6cd9ef6c18..753e02dd67 100644 --- a/ps2xIOP/src/emulator/services/iop_module_loader.h +++ b/ps2xIOP/src/emulator/services/iop_module_loader.h @@ -43,6 +43,9 @@ namespace ps2x::iop::detail public: [[nodiscard]] static bool readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector &bytes); [[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector &bytes); + // sceSifLoadModuleBuffer takes an IOP address: the EE first copies the + // image into IOP RAM with sceSifSetDma. + [[nodiscard]] static bool readElfFromIop(const IopMemory &memory, uint32_t iopAddress, std::vector &bytes); [[nodiscard]] static IopImageLoadResult load(std::span image, IopMemory &memory, uint32_t moduleCursor); }; } diff --git a/ps2xIOP/tests/iop_emulator_tests.cpp b/ps2xIOP/tests/iop_emulator_tests.cpp index 44c2494993..b8b6fc3761 100644 --- a/ps2xIOP/tests/iop_emulator_tests.cpp +++ b/ps2xIOP/tests/iop_emulator_tests.cpp @@ -911,6 +911,23 @@ int main() if (!expect(stopResult == 0, "Emulated module stop result mismatch")) return 1; if (!expect(iop.debugSnapshot().emulatorLoadedModules == 0u, "Module was not released")) return 1; + // sceSifLoadModuleBuffer takes an IOP address: the EE DMAs the image into + // IOP RAM first, so the EE copy may be gone by the time the load runs. + { + constexpr uint32_t imageBytes = 0x118u; // header, program header, code + writeMinimalIrx(host, 0x100u); + const std::vector image(host.guest.begin() + 0x100, host.guest.begin() + 0x100 + imageBytes); + std::fill(host.guest.begin() + 0x100, host.guest.begin() + 0x100 + imageBytes, uint8_t{0}); + const uint32_t iopImage = iop.allocateMemory(imageBytes, 64u); + if (!expect(iopImage != 0u && iop.writeMemory(iopImage, image.data(), image.size()), + "Could not stage the IRX image in IOP RAM")) return 1; + const ModuleLoadResult fromIop = iop.loadModuleBuffer(iopImage); + if (!expect(fromIop.handled && fromIop.moduleId > 0 && fromIop.startResult == 7, + "sceSifLoadModuleBuffer must load an image that lives in IOP RAM")) return 1; + if (!expect(iop.stopModule(fromIop.moduleId, &stopResult), "IOP-RAM module stop failed")) return 1; + if (!expect(iop.freeMemory(iopImage), "IOP image buffer free failed")) return 1; + } + constexpr uint32_t rpcSid = 0xF00DCAFEu; writeRpcServerIrx(host, 0x100u); const ModuleLoadResult rpcModule = iop.loadModuleBuffer(0x100u); diff --git a/ps2xIOP/tests/iop_memory_tests.cpp b/ps2xIOP/tests/iop_memory_tests.cpp new file mode 100644 index 0000000000..e906d4ac93 --- /dev/null +++ b/ps2xIOP/tests/iop_memory_tests.cpp @@ -0,0 +1,46 @@ +#include "iop_compat_test_support.h" + +#include "emulator/core/iop_memory.h" + +namespace +{ + using namespace iop_test; + using namespace ps2x::iop::detail; + + constexpr uint32_t kSpu0Bcr = 0x1F8010C4u; + constexpr uint32_t kSpu0Chcr = 0x1F8010C8u; + constexpr uint32_t kSpu0Admas = 0x1F9001B0u; + constexpr uint32_t kChcrStartToSpu = 0x01000201u; + + void spu2DmaCompletesAtBusSpeed() + { + IopMemory memory; + memory.write32(kSpu0Bcr, (1u << 16u) | 16u); // one block of 16 words + memory.write32(kSpu0Chcr, kChcrStartToSpu); + const auto start = memory.takeDmaStart(); + require(start.has_value(), "SPU2 DMA did not start"); + require(start->delayCycles == 64u, "a plain SPU2 DMA should complete at bus speed"); + } + + void spu2AutoDmaCompletesAtPlaybackSpeed() + { + // Auto-DMA feeds the SPU2 at its playback rate: 48 kHz stereo 16-bit + // is 192000 bytes/s, 192 IOP cycles per byte. + IopMemory memory; + memory.write16(kSpu0Admas, 1u); + memory.write32(kSpu0Bcr, (1u << 16u) | 16u); + memory.write32(kSpu0Chcr, kChcrStartToSpu); + const auto start = memory.takeDmaStart(); + require(start.has_value(), "SPU2 auto-DMA did not start"); + require(start->delayCycles == 16u * 4u * 192u, "SPU2 auto-DMA should complete at playback speed"); + } +} + +int main() +{ + const Test tests[] = { + {"SPU2 DMA completes at bus speed", spu2DmaCompletesAtBusSpeed}, + {"SPU2 auto-DMA completes at playback speed", spu2AutoDmaCompletesAtPlaybackSpeed}, + }; + return run(tests); +} diff --git a/ps2xRecomp/src/lib/control_flow_emitter.cpp b/ps2xRecomp/src/lib/control_flow_emitter.cpp index 4bf8dc550c..95f69d7a3f 100644 --- a/ps2xRecomp/src/lib/control_flow_emitter.cpp +++ b/ps2xRecomp/src/lib/control_flow_emitter.cpp @@ -406,10 +406,10 @@ namespace ps2recomp return fmt::format("GPR_U64(ctx, {}) != GPR_U64(ctx, {})", rsReg, rtReg); case OPCODE_BLEZ: case OPCODE_BLEZL: - return fmt::format("GPR_S32(ctx, {}) <= 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) <= 0", rsReg); case OPCODE_BGTZ: case OPCODE_BGTZL: - return fmt::format("GPR_S32(ctx, {}) > 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) > 0", rsReg); case OPCODE_REGIMM: switch (m_branchInst.rt) { @@ -417,12 +417,12 @@ namespace ps2recomp case REGIMM_BLTZL: case REGIMM_BLTZAL: case REGIMM_BLTZALL: - return fmt::format("GPR_S32(ctx, {}) < 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) < 0", rsReg); case REGIMM_BGEZ: case REGIMM_BGEZL: case REGIMM_BGEZAL: case REGIMM_BGEZALL: - return fmt::format("GPR_S32(ctx, {}) >= 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) >= 0", rsReg); default: return "false"; } diff --git a/ps2xRecomp/src/lib/cop0_translator.cpp b/ps2xRecomp/src/lib/cop0_translator.cpp index 1823a39462..9f375bbb75 100644 --- a/ps2xRecomp/src/lib/cop0_translator.cpp +++ b/ps2xRecomp/src/lib/cop0_translator.cpp @@ -44,7 +44,7 @@ namespace ps2recomp case COP0_REG_BADVADDR: return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)ctx->cop0_badvaddr);", rt); case COP0_REG_COUNT: - return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)ctx->cop0_count);", rt); + return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)runtime->readCop0Count(ctx));", rt); case COP0_REG_ENTRYHI: return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)ctx->cop0_entryhi);", rt); case COP0_REG_COMPARE: @@ -94,7 +94,7 @@ namespace ps2recomp case COP0_REG_BADVADDR: return "// MTC0 to BADVADDR register ignored (read-only)"; case COP0_REG_COUNT: - return fmt::format("ctx->cop0_count = GPR_U32(ctx, {});", rt); + return fmt::format("runtime->writeCop0Count(ctx, GPR_U32(ctx, {}));", rt); case COP0_REG_ENTRYHI: return fmt::format("ctx->cop0_entryhi = GPR_U32(ctx, {}) & 0xC00000FF;", rt); case COP0_REG_COMPARE: diff --git a/ps2xRecomp/src/lib/fpu_translator.cpp b/ps2xRecomp/src/lib/fpu_translator.cpp index 435db3aa93..e5a2e7acb1 100644 --- a/ps2xRecomp/src/lib/fpu_translator.cpp +++ b/ps2xRecomp/src/lib/fpu_translator.cpp @@ -58,7 +58,8 @@ namespace ps2recomp "ctx->f[{}] = FPU_DIV_S(ctx->f[{}], ctx->f[{}]); }}", ft, fd, fs, ft); case COP1_S_SQRT: - return fmt::format("ctx->f[{}] = FPU_SQRT_S(ctx->f[{}]);", fd, fs); + // SQRT.S fd, ft: the source is in the ft field (fs is 0). + return fmt::format("ctx->f[{}] = FPU_SQRT_S(ctx->f[{}]);", fd, ft); case COP1_S_ABS: return fmt::format("ctx->f[{}] = FPU_ABS_S(ctx->f[{}]);", fd, fs); case COP1_S_MOV: diff --git a/ps2xRecomp/src/lib/function_table_emitter.cpp b/ps2xRecomp/src/lib/function_table_emitter.cpp index b82b259747..4c4dcb6f44 100644 --- a/ps2xRecomp/src/lib/function_table_emitter.cpp +++ b/ps2xRecomp/src/lib/function_table_emitter.cpp @@ -105,6 +105,16 @@ namespace ps2recomp addEntry(address, name); } + // A resume point inside one function must never replace another + // function's entry (e.g. a final branch whose delay slot is the first + // instruction of the next function). Stubs and library functions are + // registered below, after the resume points. + std::unordered_set functionStarts; + for (const auto &function : functions) + { + functionStarts.insert(function.start); + } + for (const auto &[ownerStart, targets] : cg.m_resumeEntryTargetsByOwner) { const std::string ownerName = cg.getFunctionName(ownerStart); @@ -115,6 +125,10 @@ namespace ps2recomp for (uint32_t target : targets) { + if (target != ownerStart && functionStarts.contains(target)) + { + continue; + } addEntry(target, ownerName); } } diff --git a/ps2xRecomp/src/lib/instruction_translator.cpp b/ps2xRecomp/src/lib/instruction_translator.cpp index 7684097593..3494451220 100644 --- a/ps2xRecomp/src/lib/instruction_translator.cpp +++ b/ps2xRecomp/src/lib/instruction_translator.cpp @@ -80,7 +80,7 @@ namespace ps2recomp { if (memoryHint.hasAddress) { - const uint32_t resolvedAddress = memoryHint.address; + const uint32_t resolvedAddress = (width == 128) ? (memoryHint.address & ~0xFu) : memoryHint.address; const std::string resolvedAddressExpr = addressLiteral(resolvedAddress); if (inst.isMmio || Ps2IsSpecialAddress(resolvedAddress)) { @@ -104,7 +104,7 @@ namespace ps2recomp { if (memoryHint.hasAddress) { - const uint32_t resolvedAddress = memoryHint.address; + const uint32_t resolvedAddress = (width == 128) ? (memoryHint.address & ~0xFu) : memoryHint.address; const std::string resolvedAddressExpr = addressLiteral(resolvedAddress); if (inst.isMmio || Ps2IsSpecialAddress(resolvedAddress)) { @@ -195,10 +195,12 @@ namespace ps2recomp return genWrite(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("(uint16_t)GPR_U32(ctx, {})", inst.rt)) + ";"; case OPCODE_SW: return genWrite(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("GPR_U32(ctx, {})", inst.rt)) + ";"; + // The R5900 ignores the low four address bits of quadword accesses + // (LQ/SQ/LQC2/SQC2); memcpy-style code relies on this with QFSRV. case OPCODE_LQ: - return fmt::format("SET_GPR_VEC(ctx, {}, {});", inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("SET_GPR_VEC(ctx, {}, {});", inst.rt, genRead(128, fmt::format("(ADD32(GPR_U32(ctx, {}), {}) & ~0xFu)", inst.rs, inst.simmediate))); case OPCODE_SQ: - return genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("GPR_VEC(ctx, {})", inst.rt)) + ";"; + return genWrite(128, fmt::format("(ADD32(GPR_U32(ctx, {}), {}) & ~0xFu)", inst.rs, inst.simmediate), fmt::format("GPR_VEC(ctx, {})", inst.rt)) + ";"; case OPCODE_LD: return fmt::format("SET_GPR_U64(ctx, {}, {});", inst.rt, genRead(64, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_SD: @@ -211,9 +213,9 @@ namespace ps2recomp inst.rt, genWrite(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), "bits")); case OPCODE_LDC2: - return fmt::format("ctx->vu0_vf[{}] = _mm_castsi128_ps({});", inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("ctx->vu0_vf[{}] = _mm_castsi128_ps({});", inst.rt, genRead(128, fmt::format("(ADD32(GPR_U32(ctx, {}), {}) & ~0xFu)", inst.rs, inst.simmediate))); case OPCODE_SDC2: - return genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("_mm_castps_si128(ctx->vu0_vf[{}])", inst.rt)) + ";"; + return genWrite(128, fmt::format("(ADD32(GPR_U32(ctx, {}), {}) & ~0xFu)", inst.rs, inst.simmediate), fmt::format("_mm_castps_si128(ctx->vu0_vf[{}])", inst.rt)) + ";"; case OPCODE_DADDI: return fmt::format( "{{ int64_t src = (int64_t)GPR_S64(ctx, {}); " diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 589d76850a..633d501b1a 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -385,6 +385,7 @@ add_library(ps2_runtime STATIC src/lib/gs/gs_cpu_backend.cpp src/lib/ps2_iop_host.cpp src/lib/ps2_memory.cpp + src/lib/ps2_ee_interpreter.cpp src/lib/ps2_pad.cpp src/lib/ps2_rom_device.cpp src/lib/ps2_vfs.cpp diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 4b9242cb4c..97a6dcf9e3 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -337,7 +337,14 @@ class PS2Runtime // TODO remove this later need to update all tests bool registerFunction(uint32_t address, RecompiledFunction func); RecompiledFunction lookupFunction(uint32_t address); + // True if a recompiled function starts at the address. bool hasFunction(uint32_t address) const; + // True if the address can be dispatched: a recompiled function, or RAM + // the EE interpreter runs (code the game builds at run time). + bool canRunGuestCode(uint32_t address) const; + // The scheduler calls this before dispatching guest code (see + // dispatchGuestBranch). + void clearGuestUnwind() noexcept { m_guestUnwinding = false; } bool dispatchGuestBranch(uint8_t *rdram, R5900Context *ctx, uint32_t targetPc, @@ -386,12 +393,20 @@ class PS2Runtime uint32_t reserveAsyncCallbackStack(uint32_t size, uint32_t alignment = 16u); void drainCompletedDmacHandlers(uint8_t *rdram); + // Runs the handlers for one cause from PS2Memory::consumeCompletedDmacCauses + // (INTC handlers for causes flagged with kQueuedIntcCauseFlag). + void dispatchCompletedDmacCause(uint8_t *rdram, uint32_t cause); void requestStop(); bool isStopRequested() const; EeScheduler &eeScheduler(); const EeScheduler &eeScheduler() const; + // COP0 Count increments every EE cycle. It is derived from the + // scheduler's cycle counter so guest code that times itself with Count + // sees time pass; a write rebases it. + uint32_t readCop0Count(R5900Context *ctx) noexcept; + void writeCop0Count(R5900Context *ctx, uint32_t value) noexcept; void postEeEvent(EeEvent event); bool eeCheckpointDue(uint32_t cycles = 32u) noexcept; [[noreturn]] void eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc); @@ -543,6 +558,8 @@ class PS2Runtime std::vector m_loadedModules; uint8_t *m_boundRdram = nullptr; uint8_t *m_boundGSVram = nullptr; + uint32_t m_cop0CountBias = 0u; + bool m_guestUnwinding = false; }; // Generated by ps2xRecomp in ps2xRuntime/src/runner/register_functions.cpp. diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index 0485d50060..0016942856 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -853,7 +853,17 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value) #define FPU_FLOOR_W_S(a) ((int32_t)floorf((float)(a))) #define FPU_CVT_S_W(a) ((float)(int32_t)(a)) #define FPU_CVT_S_L(a) ((float)(int64_t)(a)) -#define FPU_CVT_W_S(a) ((int32_t)nearbyintf((float)(a))) +// CVT.W.S on the EE always truncates toward zero; magnitudes of 2^31 and +// above saturate to INT32_MAX / INT32_MIN by sign. +static inline int32_t ps2_cvt_w_s(float value) +{ + uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + if ((bits & 0x7F800000u) <= 0x4E800000u) + return static_cast(value); + return (bits & 0x80000000u) ? INT32_MIN : INT32_MAX; +} +#define FPU_CVT_W_S(a) ps2_cvt_w_s((float)(a)) #define FPU_CVT_L_S(a) ((int64_t)(float)(a)) #define FPU_C_F_S(a, b) (0) #define FPU_C_UN_S(a, b) (isnan((float)(a)) || isnan((float)(b))) diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index f247ff998d..d37df54340 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -334,6 +334,11 @@ class EeScheduler void dispatchIrq(bool dmac, uint32_t cause); void setVSyncFlag(uint32_t flagAddress, uint32_t tickAddress); [[nodiscard]] uint64_t currentVSyncTick() const noexcept; + // Takes effect from the next scheduled VBlank (SetGsCrt picks the mode). + void setPalVBlankRate(bool pal) noexcept; + [[nodiscard]] std::chrono::microseconds vblankPeriod() const noexcept { return m_vblankPeriod; } + // EE cycles of guest time elapsed so far (advances at every checkpoint). + [[nodiscard]] uint64_t eeCycle() const noexcept { return m_eeCycle; } uint32_t setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_t sp); [[noreturn]] void waitVSync(uint64_t afterTick, int fixedResult = -1, std::function completion = {}); @@ -442,6 +447,8 @@ class EeScheduler uint64_t m_eventSequence = 0; uint64_t m_invocationSequence = 0; uint64_t m_vsyncTick = 0; + std::chrono::microseconds m_vblankPeriod{16667}; + uint64_t m_vblankPeriodCycles = (16667ull * kEeClockHz + 999999ull) / 1000000ull; uint32_t m_vsyncFlagAddress = 0; uint32_t m_vsyncTickAddress = 0; uint32_t m_gsVSyncCallback = 0; diff --git a/ps2xRuntime/include/runtime/ps2_ee_interpreter.h b/ps2xRuntime/include/runtime/ps2_ee_interpreter.h new file mode 100644 index 0000000000..67c2db1635 --- /dev/null +++ b/ps2xRuntime/include/runtime/ps2_ee_interpreter.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct R5900Context; +class PS2Runtime; + +// Executes EE code that has no recompiled function: code a game loads or +// builds in RAM at run time (overlays, code modules linked from data files). +// Runs from ctx->pc and returns once control reaches recompiled code (with +// ctx->pc set to it), when the scheduler needs a checkpoint, or when a call +// into recompiled code unwinds. Same signature as a recompiled function. +void ps2InterpretGuestCode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); + +// True if the function table has recompiled code for this exact address. +bool ps2HasRecompiledFunction(uint32_t address); diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index ea86a9cb25..78ba1597f4 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -345,6 +345,8 @@ class PS2Memory void processVIF1Data(const uint8_t *data, uint32_t sizeBytes); void processPendingTransfers(); std::vector consumeCompletedDmacCauses(); + // Set on queued causes that are INTC interrupts (e.g. VIF1) rather than DMAC. + static constexpr uint32_t kQueuedIntcCauseFlag = 0x100u; int pollDmaRegisters(); @@ -409,6 +411,16 @@ class PS2Memory bool m_path3Masked = false; uint32_t m_vif1PendingPath2ImageQwc = 0u; bool m_vif1PendingPath2DirectHl = false; + // A VIFcode with the interrupt (I) bit stalls VIF1 after it executes: the + // rest of the DMA data (from m_vif1StalledOffset on) waits here until + // VIF1_FBRST.STC, and the channel's completion is deferred until that data + // has been consumed. Resuming processes the buffer in place, so stalling + // again inside it (m_vif1ResumeData) only moves the offset. + std::vector m_vif1StalledData; + size_t m_vif1StalledOffset = 0; + std::vector *m_vif1ResumeData = nullptr; + bool m_vif1Stalled = false; + bool m_vif1DmaCompletionDeferred = false; std::vector> m_path3MaskedFifo; struct PendingTransfer @@ -419,9 +431,13 @@ class PS2Memory std::vector chainData; }; std::vector m_pendingGifTransfers; + // GIF (PATH3) chains started while VIF1 is stalled on a VIFcode interrupt + // with data left; see runDeferredGifTransfers(). + std::vector m_deferredGifTransfers; std::vector m_pendingVif0Transfers; std::vector m_pendingVif1Transfers; std::mutex m_completedDmacMutex; + // DMAC channel causes; INTC causes are queued with kQueuedIntcCauseFlag set. std::vector m_completedDmacCauses; struct CodeRegion @@ -452,6 +468,8 @@ class PS2Memory bool tryProcessScratchpadDma(uint32_t channelBase, uint32_t chcr); void completeDmacChannel(uint32_t channelBase, uint32_t cause); void queueCompletedDmacCause(uint32_t cause); + void resumeStalledVif1(); + void runDeferredGifTransfers(); }; #endif // PS2_MEMORY_H diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index c5e31c48d8..a089d7fbd5 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -31,7 +31,8 @@ namespace constexpr uint32_t WEF_OR = 0x01u; constexpr uint32_t WEF_CLEAR = 0x10u; constexpr uint32_t WEF_CLEAR_ALL = 0x20u; - constexpr auto kVBlankPeriod = std::chrono::microseconds(16667); + constexpr uint32_t kNtscVBlankMicroseconds = 16667u; // 60 Hz + constexpr uint32_t kPalVBlankMicroseconds = 20000u; // 50 Hz constexpr auto kVBlankDuration = std::chrono::microseconds(500); constexpr uint64_t kAlarmTickMicroseconds = 64u; constexpr uint32_t kDebugPublishDispatchInterval = 4096u; @@ -50,7 +51,6 @@ namespace return std::chrono::seconds(wholeSeconds) + std::chrono::nanoseconds(remainingNanoseconds); } - constexpr uint64_t kVBlankPeriodCycles = microsecondsToEeCycles(16667u); constexpr uint64_t kVBlankDurationCycles = microsecondsToEeCycles(500u); constexpr uint64_t kAlarmTickCycles = microsecondsToEeCycles(kAlarmTickMicroseconds); @@ -146,8 +146,8 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) main.status = EeThreadStatus::Ready; m_threads.emplace(main.id, std::move(main)); m_readyQueues[0].push_back(kMainThreadId); - scheduleEvent(m_eeCycle + kVBlankPeriodCycles, - std::chrono::steady_clock::now() + kVBlankPeriod, + scheduleEvent(m_eeCycle + m_vblankPeriodCycles, + std::chrono::steady_clock::now() + m_vblankPeriod, EeEvent{EeEventType::VBlankStart, 0, 0}); publishSnapshot(); } @@ -266,7 +266,7 @@ void EeScheduler::run() continue; } - if (!m_runtime.hasFunction(context.pc)) + if (!m_runtime.canRunGuestCode(context.pc)) { if (!running->invocations.empty()) { @@ -285,6 +285,7 @@ void EeScheduler::run() } continue; } + m_runtime.clearGuestUnwind(); PS2Runtime::RecompiledFunction function = m_runtime.lookupFunction(context.pc); if (checkpointDue(kGuestDispatchCycles)) @@ -1304,6 +1305,13 @@ void EeScheduler::setVSyncFlag(uint32_t flagAddress, uint32_t tickAddress) } } +void EeScheduler::setPalVBlankRate(bool pal) noexcept +{ + const uint32_t microseconds = pal ? kPalVBlankMicroseconds : kNtscVBlankMicroseconds; + m_vblankPeriod = std::chrono::microseconds(microseconds); + m_vblankPeriodCycles = microsecondsToEeCycles(microseconds); +} + uint64_t EeScheduler::currentVSyncTick() const noexcept { return m_vsyncTick; @@ -1850,8 +1858,8 @@ void EeScheduler::processDueDeadlines() scheduleEvent(scheduled.deadlineCycle + kVBlankDurationCycles, scheduled.hostDeadline + kVBlankDuration, EeEvent{EeEventType::VBlankEnd, 0, m_vsyncTick + 1u}); - scheduleEvent(scheduled.deadlineCycle + kVBlankPeriodCycles, - scheduled.hostDeadline + kVBlankPeriod, + scheduleEvent(scheduled.deadlineCycle + m_vblankPeriodCycles, + scheduled.hostDeadline + m_vblankPeriod, EeEvent{EeEventType::VBlankStart, 0, 0}); } processEvent(scheduled.event); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index aed4cb5437..06e2a29feb 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -802,6 +802,10 @@ namespace ps2_stubs g_gparam.omode = static_cast(omode & 0xFF); g_gparam.ffmode = static_cast(ffmode & 0x1); writeGsGParamToScratch(runtime); + // The real sceGsResetGraph ends with SetGsCrt(interlace, omode, + // ffmode), which selects the CRT timing: omode 3 is PAL (50 Hz). + if (runtime) + runtime->eeScheduler().setPalVBlankRate((omode & 0xFFu) == 3u); uint64_t pmode = makePmode(1, 0, 0, 0, 0, 0x80); uint64_t smode2 = (interlace & 0x1) | ((ffmode & 0x1) << 1); uint64_t dispfb = makeDispFb(0, 10, 0, 0, 0); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index b1bb6d8515..2d4c6f6eb3 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -1431,7 +1431,7 @@ namespace for (const uint32_t completedCause : completedCauses) { - ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, completedCause); + runtime->dispatchCompletedDmacCause(rdram, completedCause); } return 0; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp index b50a4cf6aa..21672666f2 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp @@ -7,6 +7,7 @@ namespace ps2_stubs { constexpr uint8_t kPadModeDigital = 0x41; constexpr uint8_t kPadModeDualShock = 0x73; + constexpr uint8_t kPadModeDualShock2Pressure = 0x79; constexpr uint8_t kPadAnalogCenter = 0x80; constexpr int32_t kPadTypeDigital = 4; constexpr int32_t kPadTypeDualShock = 7; @@ -245,7 +246,11 @@ namespace ps2_stubs void fillPadStatus(uint8_t *data, const PadInputState &state, const PadPortState &portState) { std::memset(data, 0, 32); - data[1] = portState.analogMode ? kPadModeDualShock : kPadModeDigital; + // Terminal ID (high nibble) and payload length in halfwords: in + // pressure mode a DualShock 2 sends 9 halfwords (0x79), so games + // that enabled it re-run pad setup if they still see 0x73. + data[1] = !portState.analogMode ? kPadModeDigital + : (portState.pressureEnabled ? kPadModeDualShock2Pressure : kPadModeDualShock); data[2] = static_cast(state.buttons & 0xFFu); data[3] = static_cast((state.buttons >> 8) & 0xFFu); data[4] = state.rx; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index 961401614d..df31011b76 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -6,11 +6,12 @@ namespace ps2_syscalls void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { int interlaced = getRegU32(ctx, 4); // $a0 - 0=non-interlaced, 1=interlaced - int videoMode = getRegU32(ctx, 5); // $a1 - 0=NTSC, 1=PAL, 2=VESA, 3=HiVision + int videoMode = getRegU32(ctx, 5); // $a1 - 2=NTSC, 3=PAL, 0x1A+=VESA, 0x50+=HDTV int frameMode = getRegU32(ctx, 6); // $a2 - 0=field, 1=frame if (runtime) { + runtime->eeScheduler().setPalVBlankRate(videoMode == 3); auto &gs = runtime->memory().gs(); const uint64_t smode2 = (static_cast(interlaced) & 0x1ull) | diff --git a/ps2xRuntime/src/lib/ps2_ee_interpreter.cpp b/ps2xRuntime/src/lib/ps2_ee_interpreter.cpp new file mode 100644 index 0000000000..9bbaeb2f00 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_ee_interpreter.cpp @@ -0,0 +1,662 @@ +#include "runtime/ps2_ee_interpreter.h" + +#include "ps2_runtime.h" +#include "ps2_runtime_macros.h" + +#include +#include +#include +#include + +// Instruction semantics mirror what ps2xRecomp emits for the same opcode, so +// interpreted and recompiled code behave identically (register extension +// rules, HI/LO handling, PS2 FPU helpers, MMIO-aware memory access). + +namespace +{ + // How often (in instructions) the interpreter offers the scheduler a + // checkpoint; also the cycle count charged for it. + constexpr uint32_t kCheckpointInterval = 32u; + + struct Fields + { + uint32_t word; + uint32_t rs, rt, rd, sa, funct; + int32_t simm; + uint32_t uimm; + }; + + Fields decode(uint32_t word) + { + return {word, (word >> 21) & 31u, (word >> 16) & 31u, (word >> 11) & 31u, (word >> 6) & 31u, word & 63u, + static_cast(static_cast(word & 0xFFFFu)), word & 0xFFFFu}; + } + + void reportUnimplemented(uint32_t pc, uint32_t word, const char *what) + { + static std::unordered_set reported; + if (!reported.insert(word & 0xFC1F07FFu).second) + return; + std::cerr << "[ee-interp] unimplemented " << what << " 0x" << std::hex << word << " at pc=0x" << pc + << std::dec << " (treated as NOP)" << std::endl; + } + + enum class Flow + { + Next, // fall through to pc + 4 + Branch, // control transfer with a delay slot (handled by caller) + Stop, // leave the interpreter (ctx->pc already set) + }; + + bool isControlTransfer(uint32_t word) + { + const uint32_t op = word >> 26; + if (op == 0x00) + { + const uint32_t funct = word & 63u; + return funct == 0x08 || funct == 0x09; // JR, JALR + } + if (op == 0x01) + { + const uint32_t rt = (word >> 16) & 31u; + return rt <= 0x03 || (rt >= 0x10 && rt <= 0x13); // BLTZ/BGEZ(L)(AL) + } + if (op == 0x02 || op == 0x03) + return true; // J, JAL + if ((op >= 0x04 && op <= 0x07) || (op >= 0x14 && op <= 0x17)) + return true; // BEQ..BGTZ(L) + if (op == 0x11 && ((word >> 21) & 31u) == 0x08) + return true; // BC1x + return false; + } + + // Executes one instruction that is not a control transfer. + Flow executeSimple(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t pc, const Fields &f) + { + const uint32_t op = f.word >> 26; + switch (op) + { + case 0x00: // SPECIAL + switch (f.funct) + { + case 0x00: SET_GPR_S32(ctx, f.rd, (int32_t)SLL32(GPR_U32(ctx, f.rt), f.sa)); return Flow::Next; + case 0x02: SET_GPR_S32(ctx, f.rd, (int32_t)SRL32(GPR_U32(ctx, f.rt), f.sa)); return Flow::Next; + case 0x03: SET_GPR_S32(ctx, f.rd, SRA32(GPR_S32(ctx, f.rt), f.sa)); return Flow::Next; + case 0x04: SET_GPR_S32(ctx, f.rd, (int32_t)SLL32(GPR_U32(ctx, f.rt), GPR_U32(ctx, f.rs) & 0x1F)); return Flow::Next; + case 0x06: SET_GPR_S32(ctx, f.rd, (int32_t)SRL32(GPR_U32(ctx, f.rt), GPR_U32(ctx, f.rs) & 0x1F)); return Flow::Next; + case 0x07: SET_GPR_S32(ctx, f.rd, SRA32(GPR_S32(ctx, f.rt), GPR_U32(ctx, f.rs) & 0x1F)); return Flow::Next; + case 0x0A: if (GPR_U64(ctx, f.rt) == 0) SET_GPR_VEC(ctx, f.rd, GPR_VEC(ctx, f.rs)); return Flow::Next; // MOVZ + case 0x0B: if (GPR_U64(ctx, f.rt) != 0) SET_GPR_VEC(ctx, f.rd, GPR_VEC(ctx, f.rs)); return Flow::Next; // MOVN + case 0x0C: // SYSCALL (generated code continues with the next instruction) + ctx->pc = pc + 4u; + runtime->handleSyscall(rdram, ctx, (f.word >> 6) & 0xFFFFFu); + return Flow::Next; + case 0x0D: runtime->handleBreak(rdram, ctx); return Flow::Next; + case 0x0F: return Flow::Next; // SYNC + case 0x10: SET_GPR_U64(ctx, f.rd, ctx->hi); return Flow::Next; + case 0x11: ctx->hi = GPR_U64(ctx, f.rs); return Flow::Next; + case 0x12: SET_GPR_U64(ctx, f.rd, ctx->lo); return Flow::Next; + case 0x13: ctx->lo = GPR_U64(ctx, f.rs); return Flow::Next; + case 0x14: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) << (GPR_U32(ctx, f.rs) & 0x3F)); return Flow::Next; + case 0x16: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) >> (GPR_U32(ctx, f.rs) & 0x3F)); return Flow::Next; + case 0x17: SET_GPR_S64(ctx, f.rd, GPR_S64(ctx, f.rt) >> (GPR_U32(ctx, f.rs) & 0x3F)); return Flow::Next; + case 0x18: // MULT + { + const int64_t result = (int64_t)GPR_S32(ctx, f.rs) * (int64_t)GPR_S32(ctx, f.rt); + ctx->lo = (uint64_t)(int64_t)(int32_t)result; + ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x19: // MULTU + { + const uint64_t result = (uint64_t)GPR_U32(ctx, f.rs) * (uint64_t)GPR_U32(ctx, f.rt); + ctx->lo = (uint64_t)(int64_t)(int32_t)result; + ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x1A: // DIV + { + const int32_t divisor = GPR_S32(ctx, f.rt); + const int32_t dividend = GPR_S32(ctx, f.rs); + if (divisor != 0) + { + if (divisor == -1 && dividend == INT32_MIN) + { + ctx->lo = (uint64_t)(int64_t)INT32_MIN; + ctx->hi = 0; + } + else + { + ctx->lo = (uint64_t)(int64_t)(dividend / divisor); + ctx->hi = (uint64_t)(int64_t)(dividend % divisor); + } + } + else + { + ctx->lo = (dividend < 0) ? 1ull : 0xFFFFFFFFFFFFFFFFull; + ctx->hi = (uint64_t)(int64_t)dividend; + } + return Flow::Next; + } + case 0x1B: // DIVU + { + const uint32_t divisor = GPR_U32(ctx, f.rt); + if (divisor != 0) + { + ctx->lo = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, f.rs) / divisor); + ctx->hi = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, f.rs) % divisor); + } + else + { + ctx->lo = 0xFFFFFFFFFFFFFFFFull; + ctx->hi = (uint64_t)(int64_t)(int32_t)GPR_U32(ctx, f.rs); + } + return Flow::Next; + } + case 0x20: // ADD + { + const int64_t result = (int64_t)GPR_S32(ctx, f.rs) + (int64_t)GPR_S32(ctx, f.rt); + if (result > INT32_MAX || result < INT32_MIN) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x21: SET_GPR_S32(ctx, f.rd, (int32_t)ADD32(GPR_U32(ctx, f.rs), GPR_U32(ctx, f.rt))); return Flow::Next; + case 0x22: // SUB + { + uint32_t tmp; + bool ov; + SUB32_OV(GPR_U32(ctx, f.rs), GPR_U32(ctx, f.rt), tmp, ov); + if (ov) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S32(ctx, f.rd, (int32_t)tmp); + return Flow::Next; + } + case 0x23: SET_GPR_S32(ctx, f.rd, (int32_t)SUB32(GPR_U32(ctx, f.rs), GPR_U32(ctx, f.rt))); return Flow::Next; + case 0x24: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rs) & GPR_U64(ctx, f.rt)); return Flow::Next; + case 0x25: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rs) | GPR_U64(ctx, f.rt)); return Flow::Next; + case 0x26: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rs) ^ GPR_U64(ctx, f.rt)); return Flow::Next; + case 0x27: SET_GPR_U64(ctx, f.rd, ~(GPR_U64(ctx, f.rs) | GPR_U64(ctx, f.rt))); return Flow::Next; + case 0x28: SET_GPR_U32(ctx, f.rd, ctx->sa); return Flow::Next; // MFSA + case 0x29: ctx->sa = GPR_U32(ctx, f.rs) & 0x7F; return Flow::Next; // MTSA + case 0x2A: SET_GPR_U64(ctx, f.rd, ((int64_t)GPR_S64(ctx, f.rs) < (int64_t)GPR_S64(ctx, f.rt)) ? 1 : 0); return Flow::Next; + case 0x2B: SET_GPR_U64(ctx, f.rd, ((uint64_t)GPR_U64(ctx, f.rs) < (uint64_t)GPR_U64(ctx, f.rt)) ? 1 : 0); return Flow::Next; + case 0x2C: // DADD + { + const int64_t a = GPR_S64(ctx, f.rs), b = GPR_S64(ctx, f.rt), r = (int64_t)((uint64_t)a + (uint64_t)b); + if (((a ^ b) >= 0) && ((a ^ r) < 0)) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S64(ctx, f.rd, r); + return Flow::Next; + } + case 0x2D: SET_GPR_U64(ctx, f.rd, (uint64_t)GPR_U64(ctx, f.rs) + (uint64_t)GPR_U64(ctx, f.rt)); return Flow::Next; + case 0x2E: // DSUB + { + const int64_t a = GPR_S64(ctx, f.rs), b = GPR_S64(ctx, f.rt), r = (int64_t)((uint64_t)a - (uint64_t)b); + if (((a ^ b) < 0) && ((a ^ r) < 0)) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S64(ctx, f.rd, r); + return Flow::Next; + } + case 0x2F: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rs) - GPR_U64(ctx, f.rt)); return Flow::Next; + case 0x30: if (GPR_S64(ctx, f.rs) >= GPR_S64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x31: if (GPR_U64(ctx, f.rs) >= GPR_U64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x32: if (GPR_S64(ctx, f.rs) < GPR_S64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x33: if (GPR_U64(ctx, f.rs) < GPR_U64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x34: if (GPR_U64(ctx, f.rs) == GPR_U64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x36: if (GPR_U64(ctx, f.rs) != GPR_U64(ctx, f.rt)) runtime->handleTrap(rdram, ctx); return Flow::Next; + case 0x38: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) << f.sa); return Flow::Next; + case 0x3A: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) >> f.sa); return Flow::Next; + case 0x3B: SET_GPR_S64(ctx, f.rd, GPR_S64(ctx, f.rt) >> f.sa); return Flow::Next; + case 0x3C: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) << (32 + f.sa)); return Flow::Next; + case 0x3E: SET_GPR_U64(ctx, f.rd, GPR_U64(ctx, f.rt) >> (32 + f.sa)); return Flow::Next; + case 0x3F: SET_GPR_S64(ctx, f.rd, GPR_S64(ctx, f.rt) >> (32 + f.sa)); return Flow::Next; + default: + reportUnimplemented(pc, f.word, "SPECIAL"); + return Flow::Next; + } + + case 0x08: // ADDI + { + const int64_t result = (int64_t)GPR_S32(ctx, f.rs) + (int64_t)f.simm; + if (result > INT32_MAX || result < INT32_MIN) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S32(ctx, f.rt, (int32_t)result); + return Flow::Next; + } + case 0x09: SET_GPR_S32(ctx, f.rt, (int32_t)ADD32(GPR_U32(ctx, f.rs), f.simm)); return Flow::Next; + case 0x0A: SET_GPR_U64(ctx, f.rt, ((int64_t)GPR_S64(ctx, f.rs) < (int64_t)(int32_t)f.simm) ? 1 : 0); return Flow::Next; + case 0x0B: SET_GPR_U64(ctx, f.rt, ((uint64_t)GPR_U64(ctx, f.rs) < (uint64_t)(int64_t)(int32_t)f.simm) ? 1 : 0); return Flow::Next; + case 0x0C: SET_GPR_U64(ctx, f.rt, GPR_U64(ctx, f.rs) & (uint64_t)f.uimm); return Flow::Next; + case 0x0D: SET_GPR_U64(ctx, f.rt, GPR_U64(ctx, f.rs) | (uint64_t)f.uimm); return Flow::Next; + case 0x0E: SET_GPR_U64(ctx, f.rt, GPR_U64(ctx, f.rs) ^ (uint64_t)f.uimm); return Flow::Next; + case 0x0F: SET_GPR_S32(ctx, f.rt, (int32_t)(f.uimm << 16)); return Flow::Next; + + case 0x10: // COP0 + if (f.rs == 0x00) // MFC0 + { + if (f.rd == 9) + SET_GPR_S32(ctx, f.rt, (int32_t)runtime->readCop0Count(ctx)); + else if (f.rd == 12) + SET_GPR_S32(ctx, f.rt, (int32_t)ctx->cop0_status); + else if (f.rd == 13) + SET_GPR_S32(ctx, f.rt, (int32_t)ctx->cop0_cause); + else if (f.rd == 14) + SET_GPR_S32(ctx, f.rt, (int32_t)ctx->cop0_epc); + else + reportUnimplemented(pc, f.word, "MFC0"); + return Flow::Next; + } + if (f.rs == 0x04) // MTC0 + { + if (f.rd == 9) + runtime->writeCop0Count(ctx, GPR_U32(ctx, f.rt)); + else if (f.rd == 12) + ctx->cop0_status = GPR_U32(ctx, f.rt) & 0xFF57FFFF; + else + reportUnimplemented(pc, f.word, "MTC0"); + return Flow::Next; + } + if (f.rs == 0x10 && f.funct == 0x38) // EI + { + ctx->cop0_status |= 0x10000; + return Flow::Next; + } + if (f.rs == 0x10 && f.funct == 0x39) // DI + { + ctx->cop0_status &= ~0x10000u; + return Flow::Next; + } + reportUnimplemented(pc, f.word, "COP0"); + return Flow::Next; + + case 0x11: // COP1 + switch (f.rs) + { + case 0x00: { uint32_t bits; std::memcpy(&bits, &ctx->f[f.rd], sizeof(bits)); SET_GPR_U32(ctx, f.rt, bits); return Flow::Next; } + case 0x02: SET_GPR_U32(ctx, f.rt, f.rd == 31 ? ctx->fcr31 : 0u); return Flow::Next; // CFC1 + case 0x04: { uint32_t bits = GPR_U32(ctx, f.rt); std::memcpy(&ctx->f[f.rd], &bits, sizeof(bits)); return Flow::Next; } + case 0x06: if (f.rd == 31) ctx->fcr31 = GPR_U32(ctx, f.rt) & 0x0183FFFF; return Flow::Next; // CTC1 + case 0x10: // S format: fs = rd field, ft = rt field, fd = sa field + { + const uint32_t fs = f.rd, ft = f.rt, fd = f.sa; + auto compare = [&](bool result) { ctx->fcr31 = result ? (ctx->fcr31 | 0x800000) : (ctx->fcr31 & ~0x800000u); }; + switch (f.funct) + { + case 0x00: ctx->f[fd] = FPU_ADD_S(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x01: ctx->f[fd] = FPU_SUB_S(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x02: ctx->f[fd] = FPU_MUL_S(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x03: + if (ctx->f[ft] == 0.0f) + ctx->fcr31 |= 0x100000; + ctx->f[fd] = FPU_DIV_S(ctx->f[fs], ctx->f[ft]); + return Flow::Next; + case 0x04: ctx->f[fd] = FPU_SQRT_S(ctx->f[ft]); return Flow::Next; + case 0x05: ctx->f[fd] = FPU_ABS_S(ctx->f[fs]); return Flow::Next; + case 0x06: ctx->f[fd] = FPU_MOV_S(ctx->f[fs]); return Flow::Next; + case 0x07: ctx->f[fd] = FPU_NEG_S(ctx->f[fs]); return Flow::Next; + case 0x16: ctx->f[fd] = FPU_RSQRT_S(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x18: FPU_SET_ACC(ctx, FPU_ADD_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x19: FPU_SET_ACC(ctx, FPU_SUB_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x1A: FPU_SET_ACC(ctx, FPU_MUL_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x1C: ctx->f[fd] = FPU_ADD_S(ctx->f_acc, FPU_MUL_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x1D: ctx->f[fd] = FPU_SUB_S(ctx->f_acc, FPU_MUL_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x1E: FPU_SET_ACC(ctx, FPU_ADD_S(ctx->f_acc, FPU_MUL_S(ctx->f[fs], ctx->f[ft]))); return Flow::Next; + case 0x1F: FPU_SET_ACC(ctx, FPU_SUB_S(ctx->f_acc, FPU_MUL_S(ctx->f[fs], ctx->f[ft]))); return Flow::Next; + case 0x24: { int32_t tmp = FPU_CVT_W_S(ctx->f[fs]); std::memcpy(&ctx->f[fd], &tmp, sizeof(tmp)); return Flow::Next; } + case 0x28: ctx->f[fd] = std::max(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x29: ctx->f[fd] = std::min(ctx->f[fs], ctx->f[ft]); return Flow::Next; + case 0x30: ctx->fcr31 &= ~0x800000u; return Flow::Next; + case 0x32: compare(FPU_C_EQ_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x34: compare(FPU_C_OLT_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x36: compare(FPU_C_OLE_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x3C: compare(FPU_C_LT_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + case 0x3E: compare(FPU_C_LE_S(ctx->f[fs], ctx->f[ft])); return Flow::Next; + default: + reportUnimplemented(pc, f.word, "COP1.S"); + return Flow::Next; + } + } + case 0x14: // W format + if (f.funct == 0x20) + { + int32_t tmp; + std::memcpy(&tmp, &ctx->f[f.rd], sizeof(tmp)); + ctx->f[f.sa] = FPU_CVT_S_W(tmp); + return Flow::Next; + } + reportUnimplemented(pc, f.word, "COP1.W"); + return Flow::Next; + default: + reportUnimplemented(pc, f.word, "COP1"); + return Flow::Next; + } + + case 0x18: // DADDI + { + const int64_t a = GPR_S64(ctx, f.rs), b = f.simm, r = (int64_t)((uint64_t)a + (uint64_t)b); + if (((a ^ b) >= 0) && ((a ^ r) < 0)) + runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); + else + SET_GPR_S64(ctx, f.rt, r); + return Flow::Next; + } + case 0x19: SET_GPR_S64(ctx, f.rt, GPR_S64(ctx, f.rs) + (int64_t)f.simm); return Flow::Next; // DADDIU + + case 0x1C: // MMI (the subset compilers emit for scalar code) + switch (f.funct) + { + case 0x10: SET_GPR_U64(ctx, f.rd, ctx->hi1); return Flow::Next; + case 0x11: ctx->hi1 = GPR_U64(ctx, f.rs); return Flow::Next; + case 0x12: SET_GPR_U64(ctx, f.rd, ctx->lo1); return Flow::Next; + case 0x13: ctx->lo1 = GPR_U64(ctx, f.rs); return Flow::Next; + case 0x18: // MULT1 + { + const int64_t result = (int64_t)GPR_S32(ctx, f.rs) * (int64_t)GPR_S32(ctx, f.rt); + ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; + ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x19: // MULTU1 + { + const uint64_t result = (uint64_t)GPR_U32(ctx, f.rs) * (uint64_t)GPR_U32(ctx, f.rt); + ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; + ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x00: // MADD + { + const uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); + const int64_t result = (int64_t)acc + (int64_t)GPR_S32(ctx, f.rs) * (int64_t)GPR_S32(ctx, f.rt); + ctx->lo = Ps2SignExt32ToU64((uint32_t)result); + ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x01: // MADDU + { + const uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); + const uint64_t result = acc + (uint64_t)GPR_U32(ctx, f.rs) * (uint64_t)GPR_U32(ctx, f.rt); + ctx->lo = Ps2SignExt32ToU64((uint32_t)result); + ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); + SET_GPR_S32(ctx, f.rd, (int32_t)result); + return Flow::Next; + } + case 0x09: // MMI2 + if (f.sa == 0x0E) { SET_GPR_VEC(ctx, f.rd, PS2_PCPYLD(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } + if (f.sa == 0x12) { SET_GPR_VEC(ctx, f.rd, PS2_PAND(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } + if (f.sa == 0x13) { SET_GPR_VEC(ctx, f.rd, PS2_PXOR(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } + break; + case 0x29: // MMI3 + if (f.sa == 0x0E) { SET_GPR_VEC(ctx, f.rd, _mm_unpackhi_epi64(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } // PCPYUD + if (f.sa == 0x12) { SET_GPR_VEC(ctx, f.rd, PS2_POR(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } + if (f.sa == 0x13) { SET_GPR_VEC(ctx, f.rd, PS2_PNOR(GPR_VEC(ctx, f.rs), GPR_VEC(ctx, f.rt))); return Flow::Next; } + break; + default: + break; + } + reportUnimplemented(pc, f.word, "MMI"); + return Flow::Next; + + case 0x1A: // LDL + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~7u, offset = addr & 7u; + const uint64_t mem = READ64(aligned); + const uint32_t shift = (7u - offset) << 3; + const uint64_t keepMask = (shift == 0) ? 0ull : ((1ull << shift) - 1ull); + SET_GPR_U64(ctx, f.rt, (GPR_U64(ctx, f.rt) & keepMask) | (mem << shift)); + return Flow::Next; + } + case 0x1B: // LDR + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~7u, offset = addr & 7u; + const uint64_t mem = READ64(aligned); + const uint32_t shift = offset << 3; + const uint64_t keepMask = (offset == 0) ? 0ull : (0xFFFFFFFFFFFFFFFFull << ((8u - offset) << 3)); + SET_GPR_U64(ctx, f.rt, (GPR_U64(ctx, f.rt) & keepMask) | (mem >> shift)); + return Flow::Next; + } + case 0x1E: SET_GPR_VEC(ctx, f.rt, READ128(ADD32(GPR_U32(ctx, f.rs), f.simm) & ~0xFu)); return Flow::Next; // LQ + case 0x1F: WRITE128(ADD32(GPR_U32(ctx, f.rs), f.simm) & ~0xFu, GPR_VEC(ctx, f.rt)); return Flow::Next; // SQ + case 0x20: SET_GPR_S32(ctx, f.rt, (int32_t)(int8_t)READ8(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; + case 0x21: SET_GPR_S32(ctx, f.rt, (int32_t)(int16_t)READ16(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; + case 0x22: // LWL + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~3u, offset = addr & 3u; + const uint32_t mem = READ32(aligned); + const uint32_t shift = (3u - offset) << 3; + const uint32_t keepMask = (shift == 0) ? 0u : ((1u << shift) - 1u); + SET_GPR_S32(ctx, f.rt, (int32_t)((GPR_U32(ctx, f.rt) & keepMask) | (mem << shift))); + return Flow::Next; + } + case 0x23: SET_GPR_S32(ctx, f.rt, (int32_t)READ32(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; + case 0x24: SET_GPR_U32(ctx, f.rt, (uint32_t)READ8(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; + case 0x25: SET_GPR_U32(ctx, f.rt, (uint32_t)READ16(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; + case 0x26: // LWR + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~3u, offset = addr & 3u; + const uint32_t mem = READ32(aligned); + const uint32_t shift = offset << 3; + const uint32_t keepMask = (offset == 0) ? 0u : (0xFFFFFFFFu << ((4u - offset) << 3)); + const uint32_t merged32 = (GPR_U32(ctx, f.rt) & keepMask) | (mem >> shift); + uint64_t merged64 = (GPR_U64(ctx, f.rt) & 0xFFFFFFFF00000000ull) | (uint64_t)merged32; + if (offset == 0) + merged64 = (uint64_t)(int64_t)(int32_t)merged32; + SET_GPR_U64(ctx, f.rt, merged64); + return Flow::Next; + } + case 0x27: SET_GPR_ZE32(ctx, f.rt, READ32(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; // LWU + case 0x28: WRITE8(ADD32(GPR_U32(ctx, f.rs), f.simm), (uint8_t)GPR_U32(ctx, f.rt)); return Flow::Next; + case 0x29: WRITE16(ADD32(GPR_U32(ctx, f.rs), f.simm), (uint16_t)GPR_U32(ctx, f.rt)); return Flow::Next; + case 0x2A: // SWL + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~3u, offset = addr & 3u; + const uint32_t shift = (3u - offset) << 3; + const uint32_t mask = 0xFFFFFFFFu >> shift; + const uint32_t oldData = READ32(aligned); + const uint32_t newData = (oldData & ~mask) | ((GPR_U32(ctx, f.rt) >> shift) & mask); + WRITE32(aligned, newData); + return Flow::Next; + } + case 0x2B: WRITE32(ADD32(GPR_U32(ctx, f.rs), f.simm), GPR_U32(ctx, f.rt)); return Flow::Next; + case 0x2C: // SDL + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~7u, offset = addr & 7u; + const uint32_t shift = (7u - offset) << 3; + const uint64_t mask = 0xFFFFFFFFFFFFFFFFull >> shift; + const uint64_t oldData = READ64(aligned); + const uint64_t newData = (oldData & ~mask) | ((GPR_U64(ctx, f.rt) >> shift) & mask); + WRITE64(aligned, newData); + return Flow::Next; + } + case 0x2D: // SDR + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~7u, offset = addr & 7u; + const uint32_t shift = offset << 3; + const uint64_t mask = 0xFFFFFFFFFFFFFFFFull << shift; + const uint64_t oldData = READ64(aligned); + const uint64_t newData = (oldData & ~mask) | ((GPR_U64(ctx, f.rt) << shift) & mask); + WRITE64(aligned, newData); + return Flow::Next; + } + case 0x2E: // SWR + { + const uint32_t addr = ADD32(GPR_U32(ctx, f.rs), f.simm), aligned = addr & ~3u, offset = addr & 3u; + const uint32_t shift = offset << 3; + const uint32_t mask = 0xFFFFFFFFu << shift; + const uint32_t oldData = READ32(aligned); + const uint32_t newData = (oldData & ~mask) | ((GPR_U32(ctx, f.rt) << shift) & mask); + WRITE32(aligned, newData); + return Flow::Next; + } + case 0x2F: return Flow::Next; // CACHE + case 0x31: // LWC1 + { + const uint32_t bits = READ32(ADD32(GPR_U32(ctx, f.rs), f.simm)); + std::memcpy(&ctx->f[f.rt], &bits, sizeof(bits)); + return Flow::Next; + } + case 0x33: return Flow::Next; // PREF + case 0x37: SET_GPR_U64(ctx, f.rt, READ64(ADD32(GPR_U32(ctx, f.rs), f.simm))); return Flow::Next; // LD + case 0x39: // SWC1 + { + uint32_t bits; + std::memcpy(&bits, &ctx->f[f.rt], sizeof(bits)); + WRITE32(ADD32(GPR_U32(ctx, f.rs), f.simm), bits); + return Flow::Next; + } + case 0x3F: WRITE64(ADD32(GPR_U32(ctx, f.rs), f.simm), GPR_U64(ctx, f.rt)); return Flow::Next; // SD + default: + reportUnimplemented(pc, f.word, "opcode"); + return Flow::Next; + } + reportUnimplemented(pc, f.word, "instruction"); + return Flow::Next; + } + + struct BranchInfo + { + bool taken = false; + bool likely = false; // delay slot only runs when taken + bool link = false; + uint32_t linkReg = 31; + bool call = false; + uint32_t target = 0; + }; + + // Decodes a control transfer; registers are read before the delay slot. + BranchInfo evaluateBranch(R5900Context *ctx, uint32_t pc, const Fields &f) + { + BranchInfo b; + const uint32_t op = f.word >> 26; + const uint32_t branchTarget = pc + 4u + (static_cast(f.simm) << 2); + switch (op) + { + case 0x00: + b.taken = true; + b.target = GPR_U32(ctx, f.rs); + if (f.funct == 0x09) // JALR + { + b.link = true; + b.linkReg = f.rd; + b.call = true; + } + return b; + case 0x01: + { + const int64_t value = GPR_S64(ctx, f.rs); + const bool lessThanZero = value < 0; + b.target = branchTarget; + switch (f.rt) + { + case 0x00: b.taken = lessThanZero; break; + case 0x01: b.taken = !lessThanZero; break; + case 0x02: b.taken = lessThanZero; b.likely = true; break; + case 0x03: b.taken = !lessThanZero; b.likely = true; break; + case 0x10: b.taken = lessThanZero; b.link = true; b.call = true; break; + case 0x11: b.taken = !lessThanZero; b.link = true; b.call = true; break; + case 0x12: b.taken = lessThanZero; b.link = true; b.call = true; b.likely = true; break; + case 0x13: b.taken = !lessThanZero; b.link = true; b.call = true; b.likely = true; break; + } + return b; + } + case 0x02: + case 0x03: + b.taken = true; + b.target = ((pc + 4u) & 0xF0000000u) | ((f.word & 0x03FFFFFFu) << 2); + b.link = b.call = (op == 0x03); + return b; + case 0x04: case 0x14: b.taken = GPR_U64(ctx, f.rs) == GPR_U64(ctx, f.rt); break; + case 0x05: case 0x15: b.taken = GPR_U64(ctx, f.rs) != GPR_U64(ctx, f.rt); break; + case 0x06: case 0x16: b.taken = GPR_S64(ctx, f.rs) <= 0; break; + case 0x07: case 0x17: b.taken = GPR_S64(ctx, f.rs) > 0; break; + case 0x11: // BC1F/BC1T/BC1FL/BC1TL + { + const bool condition = (ctx->fcr31 & 0x800000) != 0; + b.taken = (f.rt & 1u) ? condition : !condition; + b.likely = (f.rt & 2u) != 0; + b.target = branchTarget; + return b; + } + } + b.target = branchTarget; + b.likely = op >= 0x14; + return b; + } +} + +void ps2InterpretGuestCode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) +{ + uint32_t pc = ctx->pc; + uint32_t sinceCheckpoint = 0; + for (;;) + { + // Control reached code that has a recompiled function, or left the + // RAM this interpreter runs: let the caller (or the scheduler) + // continue there. + if (ps2HasRecompiledFunction(pc) || !runtime->canRunGuestCode(pc)) + { + ctx->pc = pc; + return; + } + if (++sinceCheckpoint >= kCheckpointInterval) + { + sinceCheckpoint = 0; + ctx->pc = pc; + if (runtime->eeCheckpointDue(kCheckpointInterval)) + return; + } + + ctx->pc = pc; + const Fields f = decode(READ32(pc)); + if (!isControlTransfer(f.word)) + { + if (executeSimple(rdram, ctx, runtime, pc, f) == Flow::Stop) + return; + pc += 4u; + continue; + } + + const BranchInfo b = evaluateBranch(ctx, pc, f); + if (b.link) + SET_GPR_U32(ctx, b.linkReg, pc + 8u); + if (b.taken || !b.likely) + { + const uint32_t delaySlotPc = pc + 4u; + const Fields slot = decode(READ32(delaySlotPc)); + ctx->pc = delaySlotPc; + ctx->in_delay_slot = true; + ctx->branch_pc = pc; + if (isControlTransfer(slot.word)) + reportUnimplemented(delaySlotPc, slot.word, "branch in delay slot"); + else + executeSimple(rdram, ctx, runtime, delaySlotPc, slot); + ctx->in_delay_slot = false; + } + if (!b.taken) + { + pc += 8u; + continue; + } + + if (b.call && ps2HasRecompiledFunction(b.target)) + { + const PS2Runtime::GuestBranchKind kind = (f.word >> 26) == 0x00 ? PS2Runtime::GuestBranchKind::IndirectCall + : PS2Runtime::GuestBranchKind::DirectCall; + if (!runtime->dispatchGuestBranch(rdram, ctx, b.target, pc, pc + 8u, kind, "interpreter")) + return; + pc = pc + 8u; + continue; + } + pc = b.target; + } +} diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 37b6a4decc..dbbbffce4a 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -3,6 +3,7 @@ #include "runtime/gs/gs_frontend.h" #include "ps2_log.h" #include +#include #include #include #include @@ -329,6 +330,7 @@ bool PS2Memory::initialize(size_t ramSize) m_codeRegions.clear(); m_path3Masked = false; m_path3MaskedFifo.clear(); + m_deferredGifTransfers.clear(); m_vif1PendingPath2ImageQwc = 0u; m_vif1PendingPath2DirectHl = false; resetEeTimers(); @@ -1124,6 +1126,11 @@ void PS2Memory::write128(uint32_t address, __m128i value) bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) { + // A deferred GIF chain waits only for the I/O write that follows its start: + // when that write resumes VIF1 (FBRST.STC), VIF1 goes first. + if (!m_deferredGifTransfers.empty() && !(address == 0x10003C10u && (value & 0x8u) != 0u)) + runDeferredGifTransfers(); + size_t timerIndex = 0u; uint32_t timerOffset = 0u; if (decodeEeTimerRegister(address, timerIndex, timerOffset)) @@ -1231,6 +1238,10 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) std::memset(&vif1_regs, 0, sizeof(vif1_regs)); m_vif1PendingPath2ImageQwc = 0u; m_vif1PendingPath2DirectHl = false; + m_vif1Stalled = false; + m_vif1StalledData.clear(); + m_vif1StalledOffset = 0; + m_vif1DmaCompletionDeferred = false; m_path3Masked = false; if (wasPath3Masked) flushMaskedPath3Packets(); @@ -1238,6 +1249,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) if (value & 0x8u) // STC { vif1_regs.stat &= ~((1u << 8) | (1u << 9) | (1u << 10) | (1u << 11) | (1u << 12) | (1u << 13)); + resumeStalledVif1(); } break; case 0x10003C30u: @@ -1560,7 +1572,21 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) const bool autoProcessTransfers = (channelBase == 0x1000A000u) ? (m_gifPacketCallback || m_gifArbiter != nullptr) : true; - if (autoProcessTransfers) + if (channelBase == 0x1000A000u && autoProcessTransfers && + m_vif1Stalled && m_vif1StalledData.size() > m_vif1StalledOffset) + { + // PATH3 has the lowest GIF priority. Games that sync PATH2 and + // PATH3 with a VIF1 interrupt resume both from the handler (GIF + // first) and rely on the PATH2 draws that follow the stall + // winning the arbitration: the uploads are for the next batch + // and may reuse VRAM those draws still read. The chain runs at + // the next I/O write (see writeIORegister), after the resumed + // VIF1 data if that write is VIF1_FBRST.STC. + for (auto &transfer : m_pendingGifTransfers) + m_deferredGifTransfers.push_back(std::move(transfer)); + m_pendingGifTransfers.clear(); + } + else if (autoProcessTransfers) { processPendingTransfers(); } @@ -1896,7 +1922,13 @@ void PS2Memory::processPendingTransfers() m_ioRegisters[VIF0_CHANNEL + 0x00] &= ~0x100u; m_ioRegisters[VIF0_CHANNEL + 0x20] = 0; } - if (hadVif1) + if (hadVif1 && m_vif1Stalled) + { + // VIF1 stopped on a VIFcode interrupt; the channel finishes once the + // stalled data has been consumed after VIF1_FBRST.STC. + m_vif1DmaCompletionDeferred = true; + } + else if (hadVif1) { raiseDStatChannel(1u); // VIF1 channel queueCompletedDmacCause(1u); @@ -1905,6 +1937,40 @@ void PS2Memory::processPendingTransfers() } } +void PS2Memory::resumeStalledVif1() +{ + if (!m_vif1Stalled) + return; + m_vif1Stalled = false; + std::vector pending; + pending.swap(m_vif1StalledData); + const size_t offset = std::exchange(m_vif1StalledOffset, size_t{0}); + if (offset < pending.size()) + { + m_vif1ResumeData = &pending; + processVIF1Data(pending.data() + offset, static_cast(pending.size() - offset)); + m_vif1ResumeData = nullptr; + } + runDeferredGifTransfers(); + if (!m_vif1Stalled && m_vif1DmaCompletionDeferred) + { + static constexpr uint32_t VIF1_CHANNEL = 0x10009000u; + m_vif1DmaCompletionDeferred = false; + m_ioRegisters[VIF1_CHANNEL + 0x20] = 0; + completeDmacChannel(VIF1_CHANNEL, 1u); + } +} + +void PS2Memory::runDeferredGifTransfers() +{ + if (m_deferredGifTransfers.empty()) + return; + for (auto &transfer : m_deferredGifTransfers) + m_pendingGifTransfers.push_back(std::move(transfer)); + m_deferredGifTransfers.clear(); + processPendingTransfers(); +} + void PS2Memory::queueCompletedDmacCause(uint32_t cause) { std::lock_guard lock(m_completedDmacMutex); @@ -2339,6 +2405,9 @@ uint32_t PS2Memory::readIORegister(uint32_t address) return val; } + if (address == 0x10003020u || address == 0x1000A000u || address == 0x1000E010u) // GIF_STAT, GIF_CHCR, D_STAT + runDeferredGifTransfers(); + if (address == 0x10003020u) // GIF_STAT { uint32_t stat = m_ioRegisters.count(address) ? m_ioRegisters[address] : 0u; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 45cb9d267e..0b53bdc082 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -1,4 +1,5 @@ #include "ps2_runtime.h" +#include "runtime/ps2_ee_interpreter.h" #include "ps2_log.h" #include "ps2_stubs.h" #include "ps2_syscalls.h" @@ -26,6 +27,7 @@ #include #include #include +#include namespace ps2_stubs { @@ -605,6 +607,20 @@ void PS2Runtime::notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransf m_iopSubsystem->onSifTransfer(transfer); } +uint32_t PS2Runtime::readCop0Count(R5900Context *ctx) noexcept +{ + if (m_eeScheduler) + ctx->cop0_count = static_cast(m_eeScheduler->eeCycle()) + m_cop0CountBias; + return ctx->cop0_count; +} + +void PS2Runtime::writeCop0Count(R5900Context *ctx, uint32_t value) noexcept +{ + ctx->cop0_count = value; + if (m_eeScheduler) + m_cop0CountBias = value - static_cast(m_eeScheduler->eeCycle()); +} + void PS2Runtime::advanceIopEeCycles(uint64_t eeCycles) noexcept { m_iopSubsystem->runEeCycles(eeCycles); @@ -1065,6 +1081,13 @@ void PS2Runtime::configureIoPathsFromElf(const std::string &elfPath) paths.mcRoot = paths.elfDirectory / "mc0"; } + // Games with their own ISO9660 reader (e.g. EA titles) issue raw sector + // reads, which need the original disc image rather than extracted files. + if (const char *image = std::getenv("PS2X_CD_IMAGE"); image && *image) + { + paths.cdImage = std::filesystem::path(image); + } + setIoPaths(paths); } @@ -1109,12 +1132,35 @@ bool PS2Runtime::registerFunction(uint32_t address, RecompiledFunction func) return replaceFunction(address, func); } -bool PS2Runtime::hasFunction(uint32_t address) const +bool ps2HasRecompiledFunction(uint32_t address) { uint32_t slot = 0u; return generatedFunctionTableSlot(address, slot) && g_ps2RecompiledFunctionTable[slot] != nullptr; } +// Code the game places in RAM at run time (e.g. modules linked from data +// files) has no recompiled function; such addresses run in the interpreter. +// PS2X_NO_INTERPRETER=1 disables this (missing-function reports instead). +static bool isInterpretableGuestCode(const PS2Memory &memory, uint32_t address) +{ + // User RAM only: below 1 MB is the kernel's, and kernel-segment + // addresses are where interrupt handlers return (the scheduler ends an + // invocation when it reaches an address without a function). + static const bool disabled = std::getenv("PS2X_NO_INTERPRETER") != nullptr; + return !disabled && (address & 3u) == 0u && address >= 0x00100000u && address < PS2_RAM_SIZE && + !memory.isCodeAddress(address); +} + +bool PS2Runtime::hasFunction(uint32_t address) const +{ + return ps2HasRecompiledFunction(address); +} + +bool PS2Runtime::canRunGuestCode(uint32_t address) const +{ + return ps2HasRecompiledFunction(address) || isInterpretableGuestCode(m_memory, address); +} + const char *describeGuestBranchKind(PS2Runtime::GuestBranchKind kind) { switch (kind) @@ -1148,6 +1194,18 @@ PS2Runtime::RecompiledFunction PS2Runtime::lookupFunction(uint32_t address) } } + if (isInterpretableGuestCode(m_memory, address)) + { + static bool announced = false; + if (!announced) + { + announced = true; + std::cerr << "[ee-interp] running guest code at 0x" << std::hex << address << std::dec + << " in the interpreter (no recompiled function)" << std::endl; + } + return &ps2InterpretGuestCode; + } + std::cerr << "Error: No exact recompiled function for guest PC 0x" << std::hex << address << " tableBase=0x" << g_ps2RecompiledFunctionTableBase << " tableEnd=0x" << g_ps2RecompiledFunctionTableEnd @@ -1353,26 +1411,37 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, ctx->pc = targetPc; const bool isCall = (kind == GuestBranchKind::DirectCall || kind == GuestBranchKind::IndirectCall); + // Returning false makes the generated caller return at once, and so on up + // to the scheduler, which then dispatches ctx->pc. Remember that an unwind + // is in progress: an enclosing call must keep unwinding even if ctx->pc + // happens to equal its own callee's entry (recursion), instead of taking + // that for "callee returned without setting pc". + auto unwindToScheduler = [this]() + { + m_guestUnwinding = true; + return false; + }; + // Every inter-function transfer is also a deterministic EE safe point. // Backward edges inside generated functions use eeCheckpointDue(), while // this charge bounds straight-line call chains that have no local loop. if (m_eeScheduler && m_eeScheduler->checkpointDue(EeScheduler::kGuestDispatchCycles)) { - return false; + return unwindToScheduler(); } if (!isCall) { - if (!hasFunction(targetPc)) + if (!canRunGuestCode(targetPc)) { reportMissingFunction(rdram, ctx, targetPc, sourcePc, kind, debugName); } ctx->pc = targetPc; - return false; + return unwindToScheduler(); } - if (!hasFunction(targetPc)) + if (!canRunGuestCode(targetPc)) { reportMissingFunction(rdram, ctx, targetPc, sourcePc, kind, debugName); @@ -1388,27 +1457,32 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, { ctx->pc = targetPc; // if you need the app to keep open to open debug pannel change this to false - return false; + return unwindToScheduler(); } - return false; + return unwindToScheduler(); } RecompiledFunction targetFn = lookupFunction(targetPc); const uint32_t entryPc = ctx->pc; targetFn(rdram, ctx, this); - if (isStopRequested() || ctx->pc == 0u) + if (m_guestUnwinding || isStopRequested() || ctx->pc == 0u) { - return false; + return unwindToScheduler(); } + // HLE handlers may return without setting ctx->pc. if (ctx->pc == entryPc) { ctx->pc = fallthroughPc; } - return ctx->pc == fallthroughPc; + if (ctx->pc != fallthroughPc) + { + return unwindToScheduler(); + } + return true; } void PS2Runtime::SignalException(R5900Context *ctx, PS2Exception exception) @@ -1487,10 +1561,20 @@ void PS2Runtime::drainCompletedDmacHandlers(uint8_t *rdram) { for (uint32_t cause : m_memory.consumeCompletedDmacCauses()) { - ps2_syscalls::dispatchDmacHandlersForCause(rdram, this, cause); + dispatchCompletedDmacCause(rdram, cause); } } +void PS2Runtime::dispatchCompletedDmacCause(uint8_t *rdram, uint32_t cause) +{ + if ((cause & PS2Memory::kQueuedIntcCauseFlag) != 0u) + { + m_eeScheduler->dispatchIrq(false, cause & ~PS2Memory::kQueuedIntcCauseFlag); + return; + } + ps2_syscalls::dispatchDmacHandlersForCause(rdram, this, cause); +} + void PS2Runtime::handleTrap(uint8_t *rdram, R5900Context *ctx) { raiseCop0Exception(ctx, EXCEPTION_TRAP); @@ -2211,7 +2295,11 @@ void PS2Runtime::postEeEvent(EeEvent event) bool PS2Runtime::eeCheckpointDue(uint32_t cycles) noexcept { - return m_eeScheduler->checkpointDue(cycles); + // A due checkpoint makes the generated function return to the scheduler. + if (!m_eeScheduler->checkpointDue(cycles)) + return false; + m_guestUnwinding = true; + return true; } [[noreturn]] void PS2Runtime::eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc) diff --git a/ps2xRuntime/src/lib/ps2_vfs.cpp b/ps2xRuntime/src/lib/ps2_vfs.cpp index aff25e6607..0385e895b1 100644 --- a/ps2xRuntime/src/lib/ps2_vfs.cpp +++ b/ps2xRuntime/src/lib/ps2_vfs.cpp @@ -4,6 +4,7 @@ #include "runtime/ps2_rom_device.h" #include +#include #include #include #include @@ -150,6 +151,49 @@ namespace const auto systemValue = std::chrono::time_point_cast(value - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); return std::chrono::system_clock::to_time_t(systemValue); } + + // ISO9660 names are case-insensitive but games spell them freely; on hosts + // with case-sensitive filesystems match each component ignoring ASCII case. + void resolveCaseInsensitive(const std::filesystem::path &base, + const std::filesystem::path &relative, + std::filesystem::path &result) + { + std::error_code error; + if (std::filesystem::exists(result, error)) + return; + + auto lower = [](std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) + { return static_cast(std::tolower(ch)); }); + return text; + }; + + std::filesystem::path current = base; + for (const auto &component : relative) + { + const std::filesystem::path direct = current / component; + if (std::filesystem::exists(direct, error)) + { + current = direct; + continue; + } + const std::string wanted = lower(component.string()); + bool matched = false; + for (const auto &entry : std::filesystem::directory_iterator(current, error)) + { + if (lower(entry.path().filename().string()) == wanted) + { + current = entry.path(); + matched = true; + break; + } + } + if (!matched) + return; + } + result = current.lexically_normal(); + } } PS2Vfs::~PS2Vfs() = default; @@ -298,6 +342,8 @@ bool PS2Vfs::resolveHostPath(std::string_view path, const PS2VfsMounts &mounts, if (base.empty() || !safeRelativePath(parsed.path, relative)) return false; result = (base / relative).lexically_normal(); + if (parsed.device == ps2x::iop::Ps2PathDevice::Cdrom) + resolveCaseInsensitive(base, relative, result); return true; } diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index f872edd156..a4b04324fb 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -1,5 +1,6 @@ // Based on Blackline Interactive implementation #include "runtime/ps2_memory.h" +#include #include enum VIFCmd : uint8_t @@ -300,10 +301,40 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) if (sizeBytes == 0u) return; + if (m_vif1Stalled) + { + // The FIFO is blocked behind an unacknowledged VIFcode interrupt. + m_vif1StalledData.insert(m_vif1StalledData.end(), data, data + sizeBytes); + return; + } + uint32_t pos = 0; + bool stallAfterCommand = false; + auto stall = [&]() + { + if (m_vif1ResumeData && data >= m_vif1ResumeData->data() && + data < m_vif1ResumeData->data() + m_vif1ResumeData->size()) + { + // Stalled again inside the data being resumed: keep that buffer. + m_vif1StalledOffset = static_cast(data - m_vif1ResumeData->data()) + pos; + m_vif1StalledData.swap(*m_vif1ResumeData); + } + else + { + m_vif1StalledData.assign(data + pos, data + sizeBytes); + m_vif1StalledOffset = 0; + } + m_vif1Stalled = true; + queueCompletedDmacCause(kQueuedIntcCauseFlag | 5u); // INTC VIF1 + }; while (pos + 4 <= sizeBytes) { + if (stallAfterCommand) + { + stall(); + return; + } if (m_vif1PendingPath2ImageQwc != 0u) { const uint32_t availableQw = (sizeBytes - pos) / 16u; @@ -344,7 +375,12 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) vif1_regs.code = cmd; vif1_regs.num = num; if (irq) + { vif1_regs.stat |= (1u << 11); // INT + // Unless ERR.MII masks it, the VIF stalls once this command is done. + if ((vif1_regs.err & 0x1u) == 0u) + stallAfterCommand = true; + } if (opcode == VIF_NOP) { @@ -398,6 +434,9 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_FLUSHE || opcode == VIF_FLUSH || opcode == VIF_FLUSHA) { + // FLUSHA waits for PATH3 to finish. + if (opcode == VIF_FLUSHA) + runDeferredGifTransfers(); continue; } else if (opcode == VIF_MSCAL || opcode == VIF_MSCALF) @@ -780,4 +819,10 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) continue; } } + + if (stallAfterCommand) + { + pos = std::min(pos, sizeBytes); + stall(); + } } diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 463b6cff1a..9229a5d9ba 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -205,6 +205,26 @@ void register_code_generator_tests() "LWU must not sign-extend bit 31 into the allocator bitmap value"); }); + tc.Run("quadword loads and stores ignore the low four address bits", [](TestCase &t) { + CodeGenerator gen({}, {}); + const std::string lq = gen.translateInstruction(makeIType(0x8F20, OPCODE_LQ, 5, 8, 0x8)); + const std::string sq = gen.translateInstruction(makeIType(0x8F24, OPCODE_SQ, 4, 8, 0x4)); + const std::string lqc2 = gen.translateInstruction(makeIType(0x8F28, OPCODE_LQC2, 5, 1, 0x8)); + const std::string sqc2 = gen.translateInstruction(makeIType(0x8F2C, OPCODE_SQC2, 4, 1, 0x4)); + const std::string lw = gen.translateInstruction(makeLw(0x8F30, 2, 5, 0x8)); + + t.IsTrue(lq.find("(ADD32(GPR_U32(ctx, 5), 8) & ~0xFu)") != std::string::npos, + "LQ must align its effective address down to 16 bytes like the R5900"); + t.IsTrue(sq.find("(ADD32(GPR_U32(ctx, 4), 4) & ~0xFu)") != std::string::npos, + "SQ must align its effective address down to 16 bytes like the R5900"); + t.IsTrue(lqc2.find("(ADD32(GPR_U32(ctx, 5), 8) & ~0xFu)") != std::string::npos, + "LQC2 must align its effective address down to 16 bytes like the R5900"); + t.IsTrue(sqc2.find("(ADD32(GPR_U32(ctx, 4), 4) & ~0xFu)") != std::string::npos, + "SQC2 must align its effective address down to 16 bytes like the R5900"); + t.IsTrue(lw.find("& ~0xFu") == std::string::npos, + "narrower accesses must keep their exact effective address"); + }); + tc.Run("SYSCALL publishes its continuation before entering the runtime", [](TestCase &t) { Function func; func.name = "syscall_resume"; @@ -269,6 +289,34 @@ void register_code_generator_tests() "the syscall continuation must register to the owner wrapper"); }); + tc.Run("a resume entry never replaces another function's entry", [](TestCase &t) { + // A resume point can land on the first instruction of the next + // function (e.g. a final branch whose delay slot is that + // instruction). Stubs and library functions are registered after + // resume points, so without care the owner would take that address. + Function owner; + owner.name = "resume_owner"; + owner.start = 0x9100; + owner.end = 0x9108; + owner.isRecompiled = true; + Function next; + next.name = "next_function"; + next.start = 0x9108; + next.end = 0x9110; + next.isStub = true; + + CodeGenerator gen({}, {}); + gen.setRenamedFunctions({{0x9100u, "resume_owner_0x9100"}, {0x9108u, "next_function_0x9108"}}); + gen.setResumeEntryTargets({{0x9100u, std::vector{0x9104u, 0x9108u}}}); + const std::string registration = gen.generateFunctionRegistration({owner, next}, {}); + t.IsTrue(registration.find("g_ps2RecompiledFunctionTable[1] = resume_owner_0x9100; // 0x9104") != std::string::npos, + "a resume point inside the owner should still register to the owner"); + t.IsTrue(registration.find("g_ps2RecompiledFunctionTable[2] = next_function_0x9108; // 0x9108") != std::string::npos, + "the next function's entry should stay registered to that function"); + t.IsTrue(registration.find("resume_owner_0x9100; // 0x9108") == std::string::npos, + "the owner must not take over the next function's entry"); + }); + tc.Run("R5900 MULT writes rd when rd is non-zero", [](TestCase &t) { CodeGenerator gen({}, {}); @@ -478,6 +526,45 @@ void register_code_generator_tests() t.IsTrue(generated.find("label_1008:") == std::string::npos, "delay slot without incoming branch should not get a label"); }); + tc.Run("sign-testing branches compare the full 64-bit register", [](TestCase &t) { + // EE GPRs are 64 bits wide and BLEZ/BGTZ/BLTZ/BGEZ test bit 63, so a + // value like 0x0000000080000000 is positive even though its low word + // is negative. + Function func; + func.name = "test_func"; + func.start = 0x1000; + func.end = 0x1040; + func.isRecompiled = true; + func.isStub = false; + + const auto signBranch = [](uint32_t address, uint32_t opcode, uint32_t rt) + { + Instruction inst = makeBranch(address, 1); + inst.opcode = opcode; + inst.rs = 4; + inst.rt = rt; + return inst; + }; + std::vector instructions; + instructions.push_back(signBranch(0x1000, OPCODE_BLEZ, 0)); + instructions.push_back(makeNop(0x1004)); + instructions.push_back(signBranch(0x1008, OPCODE_BGTZ, 0)); + instructions.push_back(makeNop(0x100c)); + instructions.push_back(signBranch(0x1010, OPCODE_REGIMM, REGIMM_BLTZ)); + instructions.push_back(makeNop(0x1014)); + instructions.push_back(signBranch(0x1018, OPCODE_REGIMM, REGIMM_BGEZ)); + instructions.push_back(makeNop(0x101c)); + instructions.push_back(makeNop(0x1020)); + + CodeGenerator gen({}, {}); + const std::string generated = gen.generateFunction(func, instructions, false); + printGeneratedCode("sign-testing branches compare the full 64-bit register", generated); + + for (const char *condition : {"GPR_S64(ctx, 4) <= 0", "GPR_S64(ctx, 4) > 0", "GPR_S64(ctx, 4) < 0", "GPR_S64(ctx, 4) >= 0"}) + t.IsTrue(generated.find(condition) != std::string::npos, std::string("missing branch condition ") + condition); + t.IsTrue(generated.find("GPR_S32(ctx, 4)") == std::string::npos, "branch conditions should not truncate to 32 bits"); + }); + tc.Run("labels delay slot when it is a branch target", [](TestCase &t) { Function func; func.name = "delay_slot_label"; @@ -990,6 +1077,16 @@ void register_code_generator_tests() t.IsTrue(mtc0Code.find("GPR_U32(ctx, 7)") != std::string::npos, "MTC0 should read from rt"); t.IsTrue(mtc0Code.find("Unimplemented MTC0") == std::string::npos, "MTC0 should not hit unimplemented path"); t.IsTrue(mtc0Code.find("Unhandled COP0") == std::string::npos, "MTC0 should not hit unhandled COP0 path"); + + // Count runs with the EE clock, so it goes through the runtime. + mfc0.rd = COP0_REG_COUNT; + const std::string countRead = gen.translateInstruction(mfc0); + t.IsTrue(countRead.find("SET_GPR_S32(ctx, 5, (int32_t)runtime->readCop0Count(ctx));") != std::string::npos, + "MFC0 Count should read the runtime's cycle-based Count but got: " + countRead); + mtc0.rd = COP0_REG_COUNT; + const std::string countWrite = gen.translateInstruction(mtc0); + t.IsTrue(countWrite.find("runtime->writeCop0Count(ctx, GPR_U32(ctx, 7));") != std::string::npos, + "MTC0 Count should rebase the runtime's Count but got: " + countWrite); }); tc.Run("FCR access uses CFC1/CTC1", [](TestCase &t) { diff --git a/ps2xTest/src/pad_input_tests.cpp b/ps2xTest/src/pad_input_tests.cpp index 07f9b7a29d..d742ed2841 100644 --- a/ps2xTest/src/pad_input_tests.cpp +++ b/ps2xTest/src/pad_input_tests.cpp @@ -351,6 +351,18 @@ void register_pad_input_tests() runPadRead(ctx, rdram); t.Equals(data[1], static_cast(0x73), "mode byte should be 0x73 (analog) after SetMainMode"); + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadEnterPressMode(rdram.data(), &ctx, nullptr); + runPadRead(ctx, rdram); + t.Equals(data[1], static_cast(0x79), "mode byte should be 0x79 (9 halfwords) in pressure mode"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadExitPressMode(rdram.data(), &ctx, nullptr); + runPadRead(ctx, rdram); + t.Equals(data[1], static_cast(0x73), "mode byte should return to 0x73 after leaving pressure mode"); + closePadPort(ctx, rdram); }); diff --git a/ps2xTest/src/ps2_float_semantics_tests.cpp b/ps2xTest/src/ps2_float_semantics_tests.cpp index 2866d2bff3..87a60ef3bd 100644 --- a/ps2xTest/src/ps2_float_semantics_tests.cpp +++ b/ps2xTest/src/ps2_float_semantics_tests.cpp @@ -210,6 +210,26 @@ void register_ps2_float_semantics_tests() t.Equals(laneBits(flushed, 3), bitsOf(0.5f), "lane 3: 1 * 0.5 should be 0.5"); }); + tc.Run("CVT.W.S truncates toward zero and saturates", [](TestCase &t) + { + t.Equals(FPU_CVT_W_S(6.807f), 6, "6.807 should truncate to 6 (games compute log2 this way)"); + t.Equals(FPU_CVT_W_S(8.58f), 8, "8.58 should truncate to 8"); + t.Equals(FPU_CVT_W_S(-2.5f), -2, "-2.5 should truncate toward zero to -2"); + t.Equals(FPU_CVT_W_S(0.999f), 0, "0.999 should truncate to 0"); + t.Equals(FPU_CVT_W_S(floatOf(0x4F000000u)), INT32_MAX, "2^31 should saturate to INT32_MAX"); + t.Equals(FPU_CVT_W_S(floatOf(0xCF000000u)), INT32_MIN, "-2^31 should saturate to INT32_MIN"); + t.Equals(FPU_CVT_W_S(floatOf(kTwoPow128)), INT32_MAX, "2^128 should saturate to INT32_MAX"); + }); + + tc.Run("SQRT.S reads its operand from ft", [](TestCase &t) + { + CodeGenerator gen({}, {}); + // SQRT.S fd, ft is encoded with fs = 0. + const std::string sqrt = gen.translateInstruction(makeCop1S(COP1_S_SQRT, 2, 0, 4)); + t.IsTrue(sqrt.find("ctx->f[2] = FPU_SQRT_S(ctx->f[4])") != std::string::npos, + "SQRT.S should write sqrt(ft) to fd but got: " + sqrt); + }); + tc.Run("DIV.S and RSQRT.S translate to the PS2 semantics helpers", [](TestCase &t) { CodeGenerator gen({}, {}); diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 113cc88813..e9c68507dd 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -4135,6 +4135,28 @@ void register_ps2_gs_tests() "sceGsResetGraph should free its temporary GIF packet"); }); + tc.Run("sceGsResetGraph selects the VBlank rate of its video mode", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); + std::vector rdram(PS2_RAM_SIZE, 0u); + + R5900Context ctx{}; + setRegU32(ctx, 4, 0u); // reset + setRegU32(ctx, 5, 1u); // interlaced + setRegU32(ctx, 6, 3u); // PAL + setRegU32(ctx, 7, 1u); + ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime); + t.Equals(runtime.eeScheduler().vblankPeriod().count(), static_cast(20000), + "a PAL reset should produce 50 Hz VBlanks"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 6, 2u); // NTSC + ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime); + t.Equals(runtime.eeScheduler().vblankPeriod().count(), static_cast(16667), + "an NTSC reset should restore 60 Hz VBlanks"); + }); + tc.Run("sceGsSyncV resumes through the scheduler with deterministic field parity", [](TestCase &t) { PS2Runtime runtime; diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 0b22fe03c6..43efb17b27 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -769,6 +769,26 @@ void register_ps2_memory_tests() t.IsTrue((mem.vif1_regs.stat & (1u << 11)) == 0u, "FBRST.STC should clear VIF1 STAT.INT"); }); + tc.Run("VIF1 stalls after an interrupt command until FBRST.STC", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + const uint32_t words[] = { + 0x80000000u | makeVifCmd(0x00u, 0x00u, 0x0000u), // NOP with the I bit + makeVifCmd(0x07u, 0x00u, 0x1234u), // MARK 0x1234 + }; + mem.processVIF1Data(reinterpret_cast(words), sizeof(words)); + t.Equals(mem.vif1_regs.mark, 0u, "commands after the interrupt should wait for the CPU"); + + const std::vector causes = mem.consumeCompletedDmacCauses(); + t.IsTrue(std::find(causes.begin(), causes.end(), PS2Memory::kQueuedIntcCauseFlag | 5u) != causes.end(), + "the interrupt command should raise the INTC VIF1 interrupt"); + + t.IsTrue(mem.writeIORegister(0x10003C10u, 0x8u), "FBRST STC write should succeed"); + t.Equals(mem.vif1_regs.mark, 0x1234u, "FBRST.STC should resume the stalled commands"); + }); + tc.Run("VIF FBRST RST clears VIF1 command state", [](TestCase &t) { PS2Memory mem; @@ -956,6 +976,42 @@ void register_ps2_memory_tests() t.IsTrue(secondOk, "second queued PATH3 packet should flush in-order"); }); + tc.Run("PATH3 started during a VIF1 interrupt stall waits for the resumed PATH2 data", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + std::vector firstBytes; + mem.setGifPacketCallback([&](const uint8_t *data, uint32_t) + { + firstBytes.push_back(data[0]); + }); + + // VIF1: NOP with the I bit, then a DIRECT (PATH2) packet. + uint32_t vif1[2u + 4u] = { + 0x80000000u | makeVifCmd(0x00u, 0x00u, 0x0000u), + makeVifCmd(0x50u, 0x00u, 0x0001u), + }; + vif1[2] = 0xA2u; + mem.processVIF1Data(reinterpret_cast(vif1), sizeof(vif1)); + + // The interrupt handler restarts GIF (PATH3) before VIF1. + constexpr uint32_t kGifCh = 0x1000A000u; + constexpr uint32_t kSrc = 0x00022000u; + uint8_t *rdram = mem.getRDRAM(); + std::memset(rdram + kSrc, 0, 16u); + rdram[kSrc] = 0xA3u; + t.IsTrue(mem.writeIORegister(kGifCh + 0x10u, kSrc), "write MADR should succeed"); + t.IsTrue(mem.writeIORegister(kGifCh + 0x20u, 1u), "write QWC should succeed"); + t.IsTrue(mem.writeIORegister(kGifCh + 0x00u, 0x100u), "write CHCR STR should succeed"); + t.IsTrue(firstBytes.empty(), "PATH3 should wait while VIF1 holds PATH2 data behind the interrupt"); + + t.IsTrue(mem.writeIORegister(0x10003C10u, 0x8u), "FBRST STC write should succeed"); + t.Equals(firstBytes.size(), static_cast(2u), "both paths should run after FBRST.STC"); + t.IsTrue(firstBytes.size() == 2u && firstBytes[0] == 0xA2u && firstBytes[1] == 0xA3u, + "the resumed PATH2 packet should reach the GS before the PATH3 upload"); + }); + tc.Run("GIF arbiter prioritizes PATH1 then PATH2 then PATH3", [](TestCase &t) { std::vector order; diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index 4772f3fb77..31369c8d7c 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -157,6 +157,26 @@ namespace } } + // A function at 0x3000 that calls itself; the inner call loops back to + // the function's first instruction, which unwinds to the scheduler. + int gRecursionDepth = 0; + bool gRecursiveCallReturned = false; + + void testRecursiveUnwindHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + if (gRecursionDepth++ == 0) + { + gRecursiveCallReturned = runtime->dispatchGuestBranch( + rdram, ctx, 0x3000u, 0x3004u, 0x3008u, PS2Runtime::GuestBranchKind::DirectCall, "test-recursive-jal"); + } + else + { + runtime->dispatchGuestBranch( + rdram, ctx, 0x3000u, 0x3010u, 0x3014u, PS2Runtime::GuestBranchKind::DirectJump, "test-loop"); + } + --gRecursionDepth; + } + void testGuestBranchTransferHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) { if (ctx) @@ -406,6 +426,48 @@ void register_ps2_runtime_expansion_tests() "callee should still execute normally"); }); + tc.Run("dispatchGuestBranch keeps unwinding when the resume PC is the callee's entry", [](TestCase &t) + { + PS2Runtime runtime; + runtime.registerFunction(0x3000u, &testRecursiveUnwindHandler); + gRecursionDepth = 0; + gRecursiveCallReturned = true; + + R5900Context ctx{}; + ctx.pc = 0x3000u; + testRecursiveUnwindHandler(nullptr, &ctx, &runtime); + + t.IsTrue(!gRecursiveCallReturned, + "the outer call must unwind too, not treat pc == entry as an implicit return"); + t.Equals(ctx.pc, 0x3000u, "the scheduler should resume at the inner loop's target"); + }); + + tc.Run("calls into RAM code without a recompiled function run in the EE interpreter", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); + uint8_t *rdram = runtime.memory().getRDRAM(); + + // A routine the game would have loaded or built in RAM at run time. + constexpr uint32_t kCode = 0x00200000u; + const uint32_t code[] = { + 0x24020005u, // addiu v0, zero, 5 + 0x24420007u, // addiu v0, v0, 7 + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + std::memcpy(rdram + kCode, code, sizeof(code)); + + R5900Context ctx{}; + setRegU32(ctx, 31, 0x2008u); + t.IsTrue(runtime.canRunGuestCode(kCode), "user RAM outside the code regions should be runnable"); + t.IsFalse(runtime.hasFunction(kCode), "hasFunction should still mean a recompiled function"); + const bool returned = runtime.dispatchGuestBranch( + rdram, &ctx, kCode, 0x2000u, 0x2008u, PS2Runtime::GuestBranchKind::DirectCall, "test-jal"); + t.IsTrue(returned, "the interpreted routine should return to its caller"); + t.Equals(::getRegU32(&ctx, 2), 12u, "the interpreter should run the routine"); + }); + tc.Run("dispatchGuestBranch jump returns to central dispatcher without nesting", [](TestCase &t) { PS2Runtime runtime; diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index 4261bface1..eb7b1ffff7 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -379,6 +379,29 @@ void register_ps2_runtime_interrupt_tests() { MiniTest::Case("PS2RuntimeInterrupt", [](TestCase &tc) { + tc.Run("SetGsCrt selects the VBlank rate of the video mode", [](TestCase &t) + { + TestEnv env; + constexpr uint32_t kModeNtsc = 2u; + constexpr uint32_t kModePal = 3u; + EeScheduler &scheduler = env.runtime.eeScheduler(); + t.Equals(scheduler.vblankPeriod().count(), static_cast(16667), + "the default VBlank rate should stay NTSC"); + + R5900Context ctx{}; + setRegU32(ctx, 4, 1u); + setRegU32(ctx, 5, kModePal); + setRegU32(ctx, 6, 0u); + GsSetCrt(env.rdram.data(), &ctx, &env.runtime); + t.Equals(scheduler.vblankPeriod().count(), static_cast(20000), + "PAL mode should produce 50 Hz VBlanks"); + + setRegU32(ctx, 5, kModeNtsc); + GsSetCrt(env.rdram.data(), &ctx, &env.runtime); + t.Equals(scheduler.vblankPeriod().count(), static_cast(16667), + "NTSC mode should restore 60 Hz VBlanks"); + }); + tc.Run("negative interrupt-safe EE syscall ids dispatch", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_runtime_io_tests.cpp b/ps2xTest/src/ps2_runtime_io_tests.cpp index 4f979be0d6..4afbaea4b4 100644 --- a/ps2xTest/src/ps2_runtime_io_tests.cpp +++ b/ps2xTest/src/ps2_runtime_io_tests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include using namespace ps2_syscalls; @@ -104,6 +105,18 @@ namespace std::memset(&ctx, 0, sizeof(ctx)); } + void setEnvVar(const char *name, const char *value) + { +#ifdef _WIN32 + _putenv_s(name, value ? value : ""); +#else + if (value) + setenv(name, value, 1); + else + unsetenv(name); +#endif + } + int32_t syncMc(std::vector &rdram, int32_t *cmdOut = nullptr) { R5900Context syncCtx{}; @@ -274,6 +287,71 @@ void register_ps2_runtime_io_tests() fioClose(owner.rdram.data(), &owner.ctx, &owner.runtime); }); + tc.Run("cdrom0 paths match disc files regardless of letter case", [](TestCase &t) + { + TestContext test; + std::filesystem::create_directories(test.paths.cdRoot / "DATA"); + { + std::ofstream file(test.paths.cdRoot / "DATA" / "IOP.BIG", std::ios::binary); + file << "BIGF"; + } + + constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START; + constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START; + writeGuestString(test.rdram.data(), kPathAddr, "cdrom0:\\data\\iop.big;1"); + 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 lowercase cdrom0 path should open an uppercase ISO9660 file"); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, kBufferAddr); + setRegU32(test.ctx, 6, 4u); + fioRead(test.rdram.data(), &test.ctx, &test.runtime); + t.Equals(getRegS32(&test.ctx, 2), 4, "the case-insensitively matched file should be readable"); + t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr, "BIGF", 4u) == 0, + "the matched file contents should reach the guest"); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); + + writeGuestString(test.rdram.data(), kPathAddr, "cdrom0:\\DATA\\MISSING.BIG;1"); + setRegU32(test.ctx, 4, kPathAddr); + setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); + t.IsTrue(getRegS32(&test.ctx, 2) < 0, "a file that does not exist in any case should still fail"); + }); + + tc.Run("PS2X_CD_IMAGE supplies the image for raw sector reads", [](TestCase &t) + { + TestContext test; + constexpr uint32_t kSectorSize = 2048u; + const std::filesystem::path image = test.paths.base / "disc.iso"; + { + std::string bytes(kSectorSize * 4u, '\0'); + std::memcpy(bytes.data() + kSectorSize * 2u, "SECTOR02", 8u); + std::ofstream file(image, std::ios::binary); + file.write(bytes.data(), static_cast(bytes.size())); + } + + setEnvVar("PS2X_CD_IMAGE", image.string().c_str()); + PS2Runtime::configureIoPathsFromElf((test.paths.cdRoot / "SLES_000.00").string()); + setEnvVar("PS2X_CD_IMAGE", nullptr); + t.Equals(PS2Runtime::getIoPaths().cdImage.string(), + std::filesystem::absolute(image).lexically_normal().string(), + "PS2X_CD_IMAGE should become the runtime CD image"); + + constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START; + setRegU32(test.ctx, 4, 2u); + setRegU32(test.ctx, 5, 1u); + setRegU32(test.ctx, 6, kBufferAddr); + ps2_stubs::sceCdRead(test.rdram.data(), &test.ctx, &test.runtime); + t.Equals(getRegS32(&test.ctx, 2), 1, "sceCdRead should accept an LBN inside the image"); + t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr, "SECTOR02", 8u) == 0, + "sceCdRead should copy the requested sector from the image"); + }); + tc.Run("mc0 directory creation", [](TestCase &t) { TestContext test;