[iterate-2S] GPU: implement CP SCRATCH_REG memory writeback — arms Sylpheed's swap-callback slot
Sylpheed renders the splash (draws=78, iterate-2O) then plateaus: the
title's per-frame manager (sub_821741C8) only re-fires when "clock B"
([gfx+15160], swap count) changes, which only the CP swap-complete
callback sub_824CE2B8 increments. The graphics ISR sub_824BE9A0
indirect-calls that callback via [[gfx+10772]+16] on CP (source=1)
interrupts, but the slot stayed NULL so the callback never ran.
Root (runtime-verified, ours-side GPU): the guest arms the slot through
the Xenos CP scratch-register writeback path, which ours never
implemented. The arming IB (drained by ours at 0x4adf5180) contains a
Type-0 register write of the callback PC 0x824ce2b8 into SCRATCH_REG4
(0x057C). On hardware/canary, writing a SCRATCH_REG{n} mirrors the value
to SCRATCH_ADDR + n*4 in memory when the matching SCRATCH_UMSK bit is
set. Runtime values: SCRATCH_ADDR=0x0b1d5000 (the [gfx+10772]
descriptor), SCRATCH_UMSK=0x20033 (bit 4 set), so SCRATCH_REG4 ->
0x0b1d5010 = descriptor+16 = the callback slot (0x4b1d5010). Ours
decoded the Type-0 write into the register file but performed no
writeback (case a: drained-but-mishandled), so the slot stayed NULL.
Fix mirrors canary's CommandProcessor::HandleSpecialRegisterWrite
(command_processor.cc:545-552): a scratch_register_writeback() helper
called from handle_type0/handle_type1 after every register write; for
SCRATCH_REG0..7 with the UMSK bit set, it writes the value (big-endian,
as mem.write_u32 already stores) to SCRATCH_ADDR + n*4 (projected via
physical_to_backing). Deterministic given identical register state;
proven by unit test.
Cascade (verified by runtime probe): slot 0x4b1d5010 now armed with
0x824ce2b8; on the 2-3 CP interrupts that fire, the ISR reads the slot
and bcctrl's into sub_824CE2B8 (runs 2x; 0x cascade on master);
sub_824CE2B8 increments clock B ([gfx+15160]). The cascade does NOT yet
reach draws>78: there are only ~3 CP interrupts (from the initial 9825-
packet batch), and the title render loop stalls upstream (the iterate-2Q
title-respawn gate) before it submits more PM4_INTERRUPT work, so the
callback can't bootstrap a self-sustaining loop. This is the remaining
update-17/18 arming gap closed; the upstream stall is the next gate.
The default threaded GPU backend drains the ring on a separate host
thread, so with the callback now doing work the exact CP-interrupt
delivery instruction varies run to run (pre-existing GPU-thread race).
Pin the n50m oracle test to --gpu-inline (instruction-count
deterministic) and re-baseline its golden; bit-exact across repeated
runs. New unit test scratch_reg_write_mirrors_to_memory_when_umsk_enabled.
Tests: 675 pass (was 674). Golden re-baselined + determinism verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"instructions": 50000001,
|
"instructions": 50000013,
|
||||||
"imports": 451499,
|
"imports": 451497,
|
||||||
"unimpl": 0,
|
"unimpl": 0,
|
||||||
"draws": 78,
|
"draws": 78,
|
||||||
"swaps": 3,
|
"swaps": 3,
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ fn run_oracle(label: &str, max_instr: u64, golden_rel: &str) {
|
|||||||
&iso,
|
&iso,
|
||||||
"-n",
|
"-n",
|
||||||
&max_instr_str,
|
&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",
|
"--stable-digest",
|
||||||
"--expect",
|
"--expect",
|
||||||
&golden_str,
|
&golden_str,
|
||||||
|
|||||||
@@ -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) {
|
fn writeback_read_ptr(&mut self, mem: &dyn MemoryAccess) {
|
||||||
if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() {
|
if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() {
|
||||||
mem.write_u32_fence(
|
mem.write_u32_fence(
|
||||||
@@ -868,6 +900,7 @@ impl GpuSystem {
|
|||||||
let value = mem.read_u32(dword_addr);
|
let value = mem.read_u32(dword_addr);
|
||||||
let target = if write_one { base_index } else { base_index + i };
|
let target = if write_one { base_index } else { base_index + i };
|
||||||
self.register_file.write(target, value);
|
self.register_file.write(target, value);
|
||||||
|
self.scratch_register_writeback(mem, target, value);
|
||||||
}
|
}
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
base = format_args!("{base_index:#x}"),
|
base = format_args!("{base_index:#x}"),
|
||||||
@@ -890,6 +923,8 @@ impl GpuSystem {
|
|||||||
let b = mem.read_u32(b_addr);
|
let b = mem.read_u32(b_addr);
|
||||||
self.register_file.write(reg_index_1, a);
|
self.register_file.write(reg_index_1, a);
|
||||||
self.register_file.write(reg_index_2, b);
|
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!(
|
tracing::trace!(
|
||||||
r1 = format_args!("{reg_index_1:#x}"),
|
r1 = format_args!("{reg_index_1:#x}"),
|
||||||
r2 = format_args!("{reg_index_2:#x}"),
|
r2 = format_args!("{reg_index_2:#x}"),
|
||||||
@@ -1511,6 +1546,17 @@ pub mod reg {
|
|||||||
/// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits
|
/// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits
|
||||||
/// (Canary `register_table.inc:530`).
|
/// (Canary `register_table.inc:530`).
|
||||||
pub const COHER_STATUS_HOST: u32 = 0x0A31;
|
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
|
/// 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);
|
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]
|
#[test]
|
||||||
fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() {
|
fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() {
|
||||||
let mut gpu = GpuSystem::new();
|
let mut gpu = GpuSystem::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user