From 9ab986ec09fb6e38073d29ee30f3b76854931588 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 3 May 2026 13:37:51 +0200 Subject: [PATCH] =?UTF-8?q?fix(cpu):=20SWAPBUG-001=20=E2=80=94=20revert=20?= =?UTF-8?q?addi=2032-bit=20truncation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The addi opcode was truncating its result to 32 bits per the post-P4-batch3 "32-bit ABI" rationale (commit bf8208e). Hunk-level bisection during the 2026-05 audit (M11) isolated this single cast as the cause of the post-P8 swap regression: swaps dropped 2 → 1 and the renderer lost a frame. PowerISA mandates sign-extension to 64 bits; canary does not truncate addi. The truncation was a canary-divergent over-extension of the addis fix (which IS canary-divergent by design, see addis at interpreter.rs:121-134). The addi_li_neg_one_zero_extends_upper test encoded the wrong invariant. Replaced with a sign-extension test asserting canonical PowerISA behavior (gpr[3] == 0xFFFF_FFFF_FFFF_FFFF for `li r3, -1`). Verification at -n 100M lockstep: swaps: 1 → 2 (gate met) draws: 0 → 0 (unchanged — gated by Phase C+D+E) instructions: ~100M (unchanged) imports: 11.4M → 987k (game escapes retry loop) packets: 281M → 57M (same) interrupts_delivered: 629 → 630 Tests: 551 passing (unchanged). Lockstep determinism: byte-identical across two 100M runs except packets (±5%, GPU-thread-race noise floor). Closes SWAPBUG-001 / PPCBUG-001. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/xenia-cpu/src/interpreter.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 29d1ebb..0e150e8 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -112,10 +112,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - match instr.opcode { // ===== ALU: Immediate ===== PpcOpcode::addi => { - // PPCBUG-001: 32-bit ABI. `li rT, -1` (= addi rT, r0, -1) must produce - // 0x00000000_FFFFFFFF, not 0xFFFFFFFF_FFFFFFFF (sign-extended simm16). let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; - ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64) as u32 as u64; + ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64); ctx.pc += 4; } PpcOpcode::addis => { @@ -5570,9 +5568,11 @@ mod tests { } #[test] - fn addi_li_neg_one_zero_extends_upper() { - // PPCBUG-001: `li r3, -1` (= addi r3, r0, -1) must produce - // 0x00000000_FFFFFFFF, not 0xFFFFFFFF_FFFFFFFF. + fn addi_li_neg_one_sign_extends_per_powerisa() { + // SWAPBUG-001 / PPCBUG-001 revert: `li r3, -1` (= addi r3, r0, -1) + // must sign-extend simm16 to 64 bits per PowerISA, producing + // 0xFFFFFFFF_FFFFFFFF. The pre-revert form truncated to 32 bits, + // which broke the swap path (canary-divergent and load-bearing). let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); // addi r3, r0, -1: opcode 14, simm16 = 0xFFFF @@ -5580,7 +5580,7 @@ mod tests { write_instr(&mut mem, 0, raw); ctx.pc = 0; step(&mut ctx, &mut mem); - assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFFu64); + assert_eq!(ctx.gpr[3], 0xFFFF_FFFF_FFFF_FFFFu64); } #[test]