From c8be06d3d57e39598f7c76e9f5e6729a4f87f339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Braganc=CC=A7a?= Date: Sat, 10 Oct 2026 14:04:40 +0100 Subject: [PATCH] fix(runtime): mirror D_ENABLEW into D_ENABLER and reset it to 0x1201 D_ENABLEW (0x1000F590) and D_ENABLER (0x1000F520) are the write and read ports of the single DMAC enable register, but PS2Memory kept them as two unrelated I/O slots, so a write to one was never visible at the other. Any guest that suspends the DMAC and polls for the CPND bit therefore spun forever; Ridge Racer V does this around its DMAC interrupt handler and frame kick and froze after loading its IOP modules. writeIORegister now stores a D_ENABLEW write into both slots, which covers every access width (write8/16 merge into the 32-bit slot, write64/128 split into write32, recompiled stores arrive through PS2Runtime::Store32). initialize seeds both slots with 0x1201, the value the hardware reports at boot, with CPND clear. Matches PCSX2's dmacWrite32 and hwReset. Channel kicks are still executed immediately while CPND is set: the runtime has no queued-DMA model, and a kick with D_CTRL.DMAE clear is dropped today, so gating on suspend would lose transfers. Adds three PS2Memory cases: the reset value, sw/sh writes to D_ENABLEW reading back at D_ENABLER (including the byte read PCSX2 polls), and the suspend/resume spin loops terminating. They fail on main. ps2x_tests: 440 passed, 0 failed (437 before). Fixes #257 --- ps2xRuntime/src/lib/ps2_memory.cpp | 18 ++++++++ ps2xTest/src/ps2_memory_tests.cpp | 67 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 37b6a4dec..3a2e629c7 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -99,6 +99,13 @@ namespace constexpr uint32_t kGsCsrRegOffset = 0x1000u; + // D_ENABLER (read port) and D_ENABLEW (write port) are the one DMAC enable + // register; bit 16 (CPND) suspends every channel. The hardware reads 0x1201 + // at boot (PCSX2 hwReset, ps2tek). + constexpr uint32_t kDEnableR = 0x1000F520u; + constexpr uint32_t kDEnableW = 0x1000F590u; + constexpr uint32_t kDEnableResetValue = 0x1201u; + // Atomically apply a 32-bit write to one half (off=0 low dword, off=4 high // dword) of the GS CSR register. Bits 0..1 of the low dword (SIGNAL/FINISH) are // write-one-to-clear; everything else is a plain merge. Uses compare_exchange @@ -355,6 +362,8 @@ bool PS2Memory::initialize(size_t ramSize) // Initialize I/O registers m_ioRegisters.clear(); + m_ioRegisters[kDEnableR] = kDEnableResetValue; + m_ioRegisters[kDEnableW] = kDEnableResetValue; // Initialize GS registers memset(&gs_regs, 0, sizeof(gs_regs)); @@ -1216,6 +1225,15 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) return true; } + if (address == kDEnableW) + { + // Writes to D_ENABLEW must read back at D_ENABLER: guests suspend the DMAC + // with `sw D_ENABLEW` and spin on `lw D_ENABLER` until CPND is set (#257). + m_ioRegisters[kDEnableW] = value; + m_ioRegisters[kDEnableR] = value; + return true; + } + m_ioRegisters[address] = value; if (address >= 0x10003C00u && address < 0x10003E00u) diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 0b22fe03c..6cd1fdd40 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -1912,6 +1912,73 @@ void register_ps2_memory_tests() } }); + tc.Run("DMAC D_ENABLER reads 0x1201 after reset with CPND clear", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kDEnableR = 0x1000F520u; + constexpr uint32_t kCpnd = 1u << 16; // DMAC suspended + + // The hardware reports 0x1201 at boot (PCSX2 hwReset, ps2tek); bit 16 is + // clear, so the DMAC starts enabled. + t.Equals(mem.read32(kDEnableR), 0x1201u, "D_ENABLER should read 0x1201 after reset"); + t.IsTrue((mem.read32(kDEnableR) & kCpnd) == 0u, "CPND should be clear after reset"); + }); + + tc.Run("DMAC D_ENABLEW writes are visible at D_ENABLER", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kDEnableR = 0x1000F520u; + constexpr uint32_t kDEnableW = 0x1000F590u; + constexpr uint32_t kCpnd = 1u << 16; + + mem.write32(kDEnableW, kCpnd); + t.Equals(mem.read32(kDEnableR), kCpnd, "sw D_ENABLEW should be readable at D_ENABLER"); + + mem.write32(kDEnableW, 0u); + t.Equals(mem.read32(kDEnableR), 0u, "clearing D_ENABLEW should clear D_ENABLER"); + + // Narrow accesses merge into the same 32-bit register: PCSX2 polls the + // suspend state with a byte read of D_ENABLER+2. + mem.write16(kDEnableW + 2u, 0x1u); + t.Equals(mem.read32(kDEnableR), kCpnd, "sh to the upper half of D_ENABLEW should set CPND at D_ENABLER"); + t.Equals(mem.read8(kDEnableR + 2u), static_cast(1u), "lb of D_ENABLER byte 2 should expose CPND"); + }); + + tc.Run("DMAC suspend and resume polls through D_ENABLEW and D_ENABLER terminate", [](TestCase &t) + { + // Ridge Racer V brackets its DMAC interrupt handler and frame kick with + // while ((D_ENABLER & 0x10000) == 0) D_ENABLEW = 0x10000; + // while ((D_ENABLER & 0x10000) != 0) D_ENABLEW = 0; + // which spins forever unless the two ports are one register. + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kDEnableR = 0x1000F520u; + constexpr uint32_t kDEnableW = 0x1000F590u; + constexpr uint32_t kCpnd = 1u << 16; + constexpr uint32_t kMaxPolls = 8u; + + uint32_t suspendPolls = 0u; + while ((mem.read32(kDEnableR) & kCpnd) == 0u && suspendPolls < kMaxPolls) + { + mem.write32(kDEnableW, kCpnd); + ++suspendPolls; + } + t.Equals(suspendPolls, 1u, "the suspend request should be visible on the first poll"); + + uint32_t resumePolls = 0u; + while ((mem.read32(kDEnableR) & kCpnd) != 0u && resumePolls < kMaxPolls) + { + mem.write32(kDEnableW, 0u); + ++resumePolls; + } + t.Equals(resumePolls, 1u, "the resume request should be visible on the first poll"); + }); + tc.Run("DMAC SPR_FROM copies scratchpad to RDRAM and completes channel 8", [](TestCase &t) { PS2Memory mem;