diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 19fa865..2abc218 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -266,65 +266,71 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::subfex => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(rb).wrapping_add(ca); - ctx.xer_ca = if rb > ra || (rb == ra && ca != 0) { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-008: 32-bit ABI. Compute in u32 space — `!ra` on u64 always + // pollutes the upper 32 bits, making this an active poisoner. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(rb32).wrapping_add(ca); + ctx.xer_ca = if rb32 > ra32 || (rb32 == ra32 && ca != 0) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + RB + CA == RB - RA - 1 + CA - let true_sum = (rb as i64 as i128) - (ra as i64 as i128) - 1 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + // RT <- !RA + RB + CA == RB - RA - 1 + CA (32-bit semantics). + let true_sum = (rb32 as i32 as i128) - (ra32 as i32 as i128) - 1 + (ca as i128); + overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); } if instr.rc_bit() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::subfzex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(ca); - // RT <- !RA + CA (no -1 term). 64-bit carry-out only when - // !RA = u64::MAX (i.e. RA = 0) AND CA = 1. - ctx.xer_ca = if ra == 0 && ca != 0 { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-018: same active-poisoning shape as subfex; operate in u32. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(ca); + // RT <- !RA + CA (no -1 term). 32-bit carry-out only when + // !ra32 = u32::MAX (i.e. ra32 = 0) AND ca = 1. + ctx.xer_ca = if ra32 == 0 && ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + CA == -RA - 1 + CA - let true_sum = -(ra as i64 as i128) - 1 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128); + overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); } if instr.rc_bit() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::subfmex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(ca).wrapping_sub(1); - ctx.xer_ca = if (!ra) != 0 || ca != 0 { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-019: also fixes the always-true CA edge — `!ra` on u64 + // is non-zero when ra32==0xFFFFFFFF and ca==0, so CA was stuck at 1. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if (!ra32) != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + CA + (-1) == -RA - 2 + CA - let true_sum = -(ra as i64 as i128) - 2 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128); + overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); } if instr.rc_bit() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::negx => { - let ra = ctx.gpr[instr.ra()]; - let result = (!ra).wrapping_add(1); - ctx.gpr[instr.rd()] = result; + // PPCBUG-006: 32-bit ABI. `(!ra).wrapping_add(1)` on u64 always + // sets upper 32 bits — every neg poisoned the GPR. neg_ov also + // checks at 64-bit INT_MIN; should be 32-bit INT_MIN. + let ra32 = ctx.gpr[instr.ra()] as u32; + let result32 = (!ra32).wrapping_add(1); + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - overflow::apply(ctx, overflow::neg_ov_64(ra)); + overflow::apply(ctx, ra32 == 0x8000_0000); } if instr.rc_bit() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } @@ -497,8 +503,11 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::andcx => { - ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & !ctx.gpr[instr.rb()]; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-033: !rb on u64 flips upper 32 bits — active poisoning. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (rs32 & !rb32) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::orx => { @@ -507,8 +516,11 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::orcx => { - ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | !ctx.gpr[instr.rb()]; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-028: same shape as andcx — operate in u32. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (rs32 | !rb32) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::xorx => { @@ -517,18 +529,28 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::norx => { - ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]); - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-029: `not` simplified mnemonic — every `not` poisoned the GPR. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 | rb32)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::nandx => { - ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]); - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-030: same shape — operate in u32. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 & rb32)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::eqvx => { - ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]); - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-031: `eqv rA, rA, rA` is a common "set to all-ones" idiom; + // 64-bit form gave 0xFFFFFFFFFFFFFFFF but 32-bit ABI expects 0x00000000FFFFFFFF. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 ^ rb32)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } @@ -5067,17 +5089,115 @@ mod tests { #[test] fn nego_sets_ov_only_on_int_min() { + // PPCBUG-006: 32-bit ABI. INT_MIN is 0x80000000 (low 32), not 0x8000000000000000. let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); // nego r5, r3 (XO=104, OE=1) - ctx.gpr[3] = i64::MIN as u64; + ctx.gpr[3] = 0x8000_0000; let raw = (31 << 26) | (5 << 21) | (3 << 16) | (1 << 10) | (104 << 1); write_instr(&mut mem, 0, raw); ctx.pc = 0; step(&mut ctx, &mut mem); assert_eq!(ctx.xer_ov, 1); - // -INT_MIN wraps to INT_MIN - assert_eq!(ctx.gpr[5], i64::MIN as u64); + // -INT_MIN wraps to INT_MIN (low 32 bits) with upper 32 bits zero. + assert_eq!(ctx.gpr[5], 0x0000_0000_8000_0000); + } + + #[test] + fn neg_clean_input_no_upper_bits() { + // PPCBUG-006 regression: neg r3=5 must produce 0x00000000_FFFFFFFB, + // not 0xFFFFFFFF_FFFFFFFB (the 64-bit !ra-then-add-1 result). + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (104 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFB); + } + + #[test] + fn norx_not_simplified_keeps_upper_bits_clean() { + // PPCBUG-029: `not rA, rB` (norx with rs==rb) is the canonical not + // simplified mnemonic. 64-bit !val poisons upper 32 bits of every + // execution; under the 32-bit ABI we must truncate. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0x0000_0000_0000_00FF; + // norx r5, r3, r3 (XO=124) + let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (3 << 11) | (124 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FF00, "upper 32 bits must be zero"); + } + + #[test] + fn eqvx_self_self_self_sets_low32_to_all_ones() { + // PPCBUG-031: `eqv rA, rA, rA` is a common "set-to-all-ones" idiom. + // 64-bit !(0^0) gives u64::MAX (0xFFFFFFFF_FFFFFFFF); 32-bit ABI + // expects 0x00000000_FFFFFFFF. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0; + // eqvx r3, r3, r3 (XO=284) + let raw = (31u32 << 26) | (3 << 21) | (3 << 16) | (3 << 11) | (284 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFF); + } + + #[test] + fn andcx_bit_clear_keeps_upper_clean() { + // PPCBUG-033: `andc rA, rS, rB` = rS & !rB. 64-bit !rB poisons. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFF_FFFF; // rS + ctx.gpr[4] = 0x000F; // rB (low bits to clear) + // andcx r5, r3, r4 (XO=60) + let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (60 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFF0); + } + + #[test] + fn subfex_clean_inputs_no_upper_bits() { + // PPCBUG-008: 32-bit ABI. RT = !RA + RB + CA. RA=5, RB=10, CA=1 + // → !5u32 = 0xFFFFFFFA, +10 = 0x100000004, +1 = 0x100000005, low32 = 5. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + ctx.gpr[4] = 10; + ctx.xer_ca = 1; + // subfex r5, r3, r4 (XO=136) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (136 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 5); + assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)"); + } + + #[test] + fn subfmex_ra_max_ca_zero_clears_ca() { + // PPCBUG-019: `subfme` with RA=u32::MAX and CA=0 should set CA=0 + // (because !u32::MAX = 0). The buggy code's `!ra != 0` predicate + // on u64 was always true (because !u64-cast-of-u32::MAX has high + // bits flipped non-zero), wrongly setting CA=1. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFF_FFFFu64; + ctx.xer_ca = 0; + // subfmex r5, r3 (XO=232) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (232 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.xer_ca, 0, "RA=u32::MAX, CA=0 → !RA32==0, CA=0"); } // ---------- Phase 2 fixes: trap TO-field ---------- @@ -6086,7 +6206,8 @@ mod tests { ctx.xer_ca = 0; step(&mut ctx, &mem); assert_eq!(ctx.xer_ca, 0, "ra=0, ca=0 should produce CA=0"); - assert_eq!(ctx.gpr[3], u64::MAX, "result = !0 + 0 = u64::MAX"); + // PPCBUG-018: 32-bit ABI. !0u32 + 0 = u32::MAX, with upper 32 bits zero. + assert_eq!(ctx.gpr[3], 0xFFFF_FFFFu64, "result = !0u32 + 0 = u32::MAX"); } // Case 3: ra=1, ca=0 → CA=0 (old buggy code reported CA=1) { @@ -6098,21 +6219,20 @@ mod tests { ctx.xer_ca = 0; step(&mut ctx, &mem); assert_eq!(ctx.xer_ca, 0, "ra=1, ca=0 should produce CA=0"); - assert_eq!(ctx.gpr[3], u64::MAX - 1, "result = !1 + 0 = u64::MAX - 1"); + // PPCBUG-018: 32-bit ABI. !1u32 + 0 = u32::MAX - 1, with upper 32 bits zero. + assert_eq!(ctx.gpr[3], 0xFFFF_FFFEu64, "result = !1u32 + 0 = u32::MAX - 1"); } - // Case 4: ra=u64::MAX, ca=0 → CA=0 (old buggy code reported CA=1 - // because !ra == 0 only here, which the buggy `!ra != 0` predicate - // happened to handle right; flip ca=1 to exercise the other arm) + // Case 4: ra=u32::MAX, ca=1 → CA=0; result = !u32::MAX + 1 = 1. { let mut ctx = PpcContext::new(); let mem = TestMem::new(); write_instr(&mem, 0, raw); ctx.pc = 0; - ctx.gpr[4] = u64::MAX; + ctx.gpr[4] = 0xFFFF_FFFFu64; ctx.xer_ca = 1; step(&mut ctx, &mem); - assert_eq!(ctx.xer_ca, 0, "ra=u64::MAX, ca=1 should produce CA=0"); - assert_eq!(ctx.gpr[3], 1, "result = !u64::MAX + 1 = 1"); + assert_eq!(ctx.xer_ca, 0, "ra=u32::MAX, ca=1 should produce CA=0"); + assert_eq!(ctx.gpr[3], 1, "result = !u32::MAX + 1 = 1"); } }