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;