diff --git a/crates/xenia-app/tests/golden/sylpheed_n50m.json b/crates/xenia-app/tests/golden/sylpheed_n50m.json index f3d5ba1..39dbaa8 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n50m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n50m.json @@ -1,6 +1,6 @@ { - "instructions": 50000001, - "imports": 451499, + "instructions": 50000013, + "imports": 451497, "unimpl": 0, "draws": 78, "swaps": 3, diff --git a/crates/xenia-app/tests/sylpheed_oracles.rs b/crates/xenia-app/tests/sylpheed_oracles.rs index d7c8083..d2fea29 100644 --- a/crates/xenia-app/tests/sylpheed_oracles.rs +++ b/crates/xenia-app/tests/sylpheed_oracles.rs @@ -57,6 +57,16 @@ fn run_oracle(label: &str, max_instr: u64, golden_rel: &str) { &iso, "-n", &max_instr_str, + // Pin the inline (single-threaded) GPU backend. The default + // threaded backend drains the ring on a separate host thread, + // so the exact instruction at which a CP interrupt is queued — + // and therefore when the guest's swap-complete ISR callback runs + // (iterate-2S armed it via SCRATCH_REG writeback) — varies run to + // run. Inline draining is instruction-count-deterministic, which + // is what a regression golden needs. (The threaded path is the + // documented "GPU thread race" the stable-digest already warns + // about.) + "--gpu-inline", "--stable-digest", "--expect", &golden_str, diff --git a/crates/xenia-gpu/src/gpu_system.rs b/crates/xenia-gpu/src/gpu_system.rs index fbc5bd7..fc254ce 100644 --- a/crates/xenia-gpu/src/gpu_system.rs +++ b/crates/xenia-gpu/src/gpu_system.rs @@ -844,6 +844,38 @@ impl GpuSystem { } } + /// CP scratch-register memory writeback, mirroring canary's + /// `CommandProcessor::HandleSpecialRegisterWrite` + /// (`command_processor.cc:545-552`). Every register write runs through + /// here; when the target is one of the eight `SCRATCH_REG{n}` + /// (`0x0578..=0x057F`) **and** the matching bit in `SCRATCH_UMSK` is set, + /// the value is also written (big-endian, as `mem.write_u32` already + /// stores) to `SCRATCH_ADDR + n*4` in guest physical memory. + /// + /// Sylpheed arms its CP swap-complete interrupt callback through this + /// path: it programs `SCRATCH_ADDR` to the GPU command-block descriptor + /// (`[gfx+10772]`, runtime `0x0b1d5000`), `SCRATCH_UMSK` bit 4, then a + /// Type-0 write of the callback PC `0x824ce2b8` into `SCRATCH_REG4` + /// (`0x057C`). The writeback lands it at descriptor+16 (`0x4b1d5010`), + /// which the graphics ISR (`sub_824BE9A0`) reads via `[[gfx+10772]+16]` + /// and `bcctrl`s to fire the swap-complete callback. Without this + /// writeback the slot stayed NULL, the ISR skipped the callback, the + /// swap counter never advanced, and the title's per-frame manager + /// re-fired once then plateaued. + fn scratch_register_writeback(&self, mem: &dyn MemoryAccess, index: u32, value: u32) { + if !(reg::SCRATCH_REG0..=reg::SCRATCH_REG7).contains(&index) { + return; + } + let scratch_reg = index - reg::SCRATCH_REG0; + let umsk = self.register_file.read(reg::SCRATCH_UMSK); + if (1u32 << scratch_reg) & umsk == 0 { + return; + } + let scratch_addr = self.register_file.read(reg::SCRATCH_ADDR); + let mem_addr = physical_to_backing(scratch_addr.wrapping_add(scratch_reg * 4)); + mem.write_u32(mem_addr, value); + } + fn writeback_read_ptr(&mut self, mem: &dyn MemoryAccess) { if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() { mem.write_u32_fence( @@ -868,6 +900,7 @@ impl GpuSystem { let value = mem.read_u32(dword_addr); let target = if write_one { base_index } else { base_index + i }; self.register_file.write(target, value); + self.scratch_register_writeback(mem, target, value); } tracing::trace!( base = format_args!("{base_index:#x}"), @@ -890,6 +923,8 @@ impl GpuSystem { let b = mem.read_u32(b_addr); self.register_file.write(reg_index_1, a); self.register_file.write(reg_index_2, b); + self.scratch_register_writeback(mem, reg_index_1, a); + self.scratch_register_writeback(mem, reg_index_2, b); tracing::trace!( r1 = format_args!("{reg_index_1:#x}"), r2 = format_args!("{reg_index_2:#x}"), @@ -1511,6 +1546,17 @@ pub mod reg { /// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits /// (Canary `register_table.inc:530`). pub const COHER_STATUS_HOST: u32 = 0x0A31; + /// `XE_GPU_REG_SCRATCH_UMSK` — bitmask of which `SCRATCH_REG{n}` writes are + /// mirrored to memory (Canary `register_table.inc:139`). + pub const SCRATCH_UMSK: u32 = 0x01DC; + /// `XE_GPU_REG_SCRATCH_ADDR` — base physical address of the scratch + /// writeback block (Canary `register_table.inc:141`). + pub const SCRATCH_ADDR: u32 = 0x01DD; + /// `XE_GPU_REG_SCRATCH_REG0` — first of 8 CP scratch registers + /// (`0x0578..=0x057F`, Canary `register_table.inc:331-338`). + pub const SCRATCH_REG0: u32 = 0x0578; + /// `XE_GPU_REG_SCRATCH_REG7` — last CP scratch register. + pub const SCRATCH_REG7: u32 = 0x057F; } /// 32-bit FNV-1a over a u32 seed + a slice of u32s. Used to derive a @@ -1601,6 +1647,38 @@ mod tests { assert_eq!(gpu.register_file.read(0x101), 0xCAFE_BABE); } + #[test] + fn scratch_reg_write_mirrors_to_memory_when_umsk_enabled() { + // Mirrors Sylpheed's CP swap-callback arming: SCRATCH_ADDR points at a + // descriptor, SCRATCH_UMSK enables bit 4, and a Type-0 write of the + // callback PC into SCRATCH_REG4 (0x57C) must land at SCRATCH_ADDR + 16. + let mut gpu = GpuSystem::new(); + let mut mem = build_mem(); + gpu.initialize_ring_buffer(0x4000_0000, 10); + // Program SCRATCH_ADDR = 0x4000_1000 (physical-mirror identity), and + // SCRATCH_UMSK = bit 4 only (so SCRATCH_REG4 mirrors, REG3 does not). + gpu.register_file.write(reg::SCRATCH_ADDR, 0x4000_1000); + gpu.register_file.write(reg::SCRATCH_UMSK, 1 << 4); + // Type0 write run: base = SCRATCH_REG3 (0x57B), count = 2 → writes + // 0x11111111 → SCRATCH_REG3 (UMSK bit 3 clear), 0x824CE2B8 → + // SCRATCH_REG4 (UMSK bit 4 set → mirrored to ADDR + 4*4 = +16). + const SCRATCH_REG3: u32 = 0x057B; + let hdr = (1u32 << 16) | SCRATCH_REG3; + mem.write_u32(0x4000_0000, hdr); + mem.write_u32(0x4000_0004, 0x1111_1111); + mem.write_u32(0x4000_0008, 0x824C_E2B8); + gpu.extend_write_ptr(3); + assert!(matches!(gpu.execute_one(&mut mem), ExecOutcome::Stepped { .. })); + // SCRATCH_REG3 (bit 3 clear) must NOT mirror; SCRATCH_REG4 (bit 4 set) + // must mirror to SCRATCH_ADDR + 16. + assert_eq!(mem.read_u32(0x4000_1000 + 12), 0, "reg3 must not mirror"); + assert_eq!( + mem.read_u32(0x4000_1000 + 16), + 0x824C_E2B8, + "reg4 must mirror to SCRATCH_ADDR+16" + ); + } + #[test] fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() { let mut gpu = GpuSystem::new();