fix(cpu): SWAPBUG-001 — revert addi 32-bit truncation

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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-03 13:37:51 +02:00
parent caa37fc595
commit 9ab986ec09

View File

@@ -112,10 +112,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
match instr.opcode { match instr.opcode {
// ===== ALU: Immediate ===== // ===== ALU: Immediate =====
PpcOpcode::addi => { 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()] }; 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; ctx.pc += 4;
} }
PpcOpcode::addis => { PpcOpcode::addis => {
@@ -5570,9 +5568,11 @@ mod tests {
} }
#[test] #[test]
fn addi_li_neg_one_zero_extends_upper() { fn addi_li_neg_one_sign_extends_per_powerisa() {
// PPCBUG-001: `li r3, -1` (= addi r3, r0, -1) must produce // SWAPBUG-001 / PPCBUG-001 revert: `li r3, -1` (= addi r3, r0, -1)
// 0x00000000_FFFFFFFF, not 0xFFFFFFFF_FFFFFFFF. // 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 ctx = PpcContext::new();
let mut mem = TestMem::new(); let mut mem = TestMem::new();
// addi r3, r0, -1: opcode 14, simm16 = 0xFFFF // addi r3, r0, -1: opcode 14, simm16 = 0xFFFF
@@ -5580,7 +5580,7 @@ mod tests {
write_instr(&mut mem, 0, raw); write_instr(&mut mem, 0, raw);
ctx.pc = 0; ctx.pc = 0;
step(&mut ctx, &mut mem); step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFFu64); assert_eq!(ctx.gpr[3], 0xFFFF_FFFF_FFFF_FFFFu64);
} }
#[test] #[test]