From 16993bb8af745f21eaf8993a610d616480d67d1c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 11:55:50 +0200 Subject: [PATCH] fix(cpu): PPCBUG-012-017/020/023-026/032/044 4c+4d latent + CR0 catch-all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 batch 6: latent writeback truncation (4c) and CR0 catch-all (4d). ~13 PPCBUGs across all remaining 32-bit ABI ALU sites. Latent writeback (4c) — the 4a/4b fixes already eliminate the upstream poisoning, but a defensive truncation here catches any future regression: - PPCBUG-012 addx, PPCBUG-013 addcx, PPCBUG-014 addex, PPCBUG-015 addzex, PPCBUG-016 addmex, PPCBUG-017 subfx — all rewritten to compute on u32 operands and write `as u64`. CA computed via 32-bit unsigned compare. Overflow now uses `true_sum != (result32 as i32) as i128` (32-bit predicate, since sum_overflow_64 is i64-bounded). - PPCBUG-032 andx/orx/xorx — CR0 catch-all only (results inherit upper bits from operands; once those are clean, no truncation needed). CR0 catch-all (4d) — fix the `update_cr_signed(0, X as i64)` pattern at every 32-bit-ABI Rc=1 path: - PPCBUG-020 catch-all: applied to mulhwx, mulhwux, divwux, mullwx (was already done in batch 4), addx/addcx/addex/addzex/addmex/subfx (now in 4c above), andx/orx/xorx, andix, andisx, slwx, srwx, cntlzwx, rlwinmx, rlwimix, rlwnmx, mullwx (already), divwx (already), srawx/srawix (already in batch 4). - PPCBUG-023 andisx: now correctly classifies bit-31 results as CR0.LT. - PPCBUG-024 rlwinmx, PPCBUG-025 rlwimix, PPCBUG-026 rlwnmx. - PPCBUG-044 slwx/srwx: bit-31 result like 0x80000000 now CR0.LT. 64-bit ABI ops (rldicl/rldicr/rldic/rldimi/rldcl/rldcr, sldx/srdx/sradx/ sradix, mulhdx/mulhdux/mulldx, divdx/divdux, cntlzdx) intentionally retain the 64-bit `as i64` form per ISA — these are 64-bit-mode instructions. Updated old tests: - addo_sets_xer_ov_on_signed_overflow_and_stickies_so: i32::MAX + 1 → INT_MIN. - addx_rc_uses_64bit_compare_not_32bit: renamed to ..._uses_32bit_compare_in_xbox_abi with assertions flipped to the correct 32-bit ABI behavior. New tests: - andisx_sign_bit_set_classifies_lt (PPCBUG-023). - slwx_high_bit_result_classifies_lt (PPCBUG-044). Co-Authored-By: Claude Sonnet 4.6 --- crates/xenia-cpu/src/interpreter.rs | 201 ++++++++++++++++++---------- 1 file changed, 131 insertions(+), 70 deletions(-) diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index c730e75..4a37ccb 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -173,89 +173,97 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== ALU: Register ===== PpcOpcode::addx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = ra.wrapping_add(rb); - ctx.gpr[instr.rd()] = result; + // PPCBUG-012+020: 32-bit ABI writeback truncation + CR0 i32 view. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = ra32.wrapping_add(rb32); + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - overflow::apply(ctx, overflow::add_ov_64(ra, rb, result)); + let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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::addcx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = ra.wrapping_add(rb); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-013+020: 32-bit truncation; CA from u32 unsigned compare. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = ra32.wrapping_add(rb32); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - overflow::apply(ctx, overflow::add_ov_64(ra, rb, result)); + let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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::addex => { - 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 result < ra || (ca != 0 && result == ra) { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-014+020: 32-bit truncation; CA from u32 unsigned compare. + 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 result32 < ra32 || (ca != 0 && result32 == ra32) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - let true_sum = (ra as i64 as i128) + (rb as i64 as i128) + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128) + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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::addzex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = ra.wrapping_add(ca); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-015+020: 32-bit truncation. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = ra32.wrapping_add(ca); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - let true_sum = (ra as i64 as i128) + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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::addmex => { - 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-016+020: 32-bit truncation. RT = RA + CA - 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) - let true_sum = (ra as i64 as i128) + (ca as i128) - 1; - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (ca as i128) - 1; + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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::subfx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = rb.wrapping_sub(ra); - ctx.gpr[instr.rd()] = result; + // PPCBUG-017+020: 32-bit truncation. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = rb32.wrapping_sub(ra32); + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - overflow::apply(ctx, overflow::sub_ov_64(ra, rb, result)); + let true_diff = (rb32 as i32 as i128) - (ra32 as i32 as i128); + overflow::apply(ctx, overflow::sum_overflow_64(true_diff, 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; } @@ -289,7 +297,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - if instr.oe() { // 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)); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } if instr.rc_bit() { ctx.update_cr_signed(0, result32 as i32 as i64); @@ -307,7 +315,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } if instr.rc_bit() { ctx.update_cr_signed(0, result32 as i32 as i64); @@ -324,7 +332,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } if instr.rc_bit() { ctx.update_cr_signed(0, result32 as i32 as i64); @@ -362,22 +370,24 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::mulhwx => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as i32 as i64; let rb = ctx.gpr[instr.rb()] as i32 as i64; let result = ra.wrapping_mul(rb); - ctx.gpr[instr.rd()] = ((result >> 32) as i32 as i64 as u64) & 0xFFFF_FFFF; + ctx.gpr[instr.rd()] = ((result >> 32) as u32) as u64; if instr.rc_bit() { - ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::mulhwux => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as u32 as u64; let rb = ctx.gpr[instr.rb()] as u32 as u64; let result = ra.wrapping_mul(rb); ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF; if instr.rc_bit() { - ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64); } ctx.pc += 4; } @@ -401,6 +411,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::divwux => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as u32; let rb = ctx.gpr[instr.rb()] as u32; let ov = overflow::divw_ov_unsigned(rb); @@ -413,7 +424,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - overflow::apply(ctx, ov); } if instr.rc_bit() { - ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64); + ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64); } ctx.pc += 4; } @@ -486,13 +497,16 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== Logical ===== PpcOpcode::andix => { + // PPCBUG-020: 32-bit ABI CR0 view. ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & (instr.uimm16() as u64); - ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); ctx.pc += 4; } PpcOpcode::andisx => { + // PPCBUG-023: 32-bit ABI CR0 view. `andis. rA, rS, 0x8000` to test + // sign bit of a 32-bit word now correctly classifies bit 31 = 1 as LT. ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ((instr.uimm16() as u64) << 16); - ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); + ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); ctx.pc += 4; } PpcOpcode::ori => { @@ -512,8 +526,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::andx => { + // PPCBUG-032+020: 32-bit ABI CR0 view (latent under clean inputs). 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); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::andcx => { @@ -525,8 +540,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::orx => { + // PPCBUG-032+020: 32-bit ABI CR0 view. 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); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::orcx => { @@ -538,8 +554,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::xorx => { + // PPCBUG-032+020: 32-bit ABI CR0 view. 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); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::norx => { @@ -589,8 +606,10 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::cntlzwx => { + // Result is 0..=32, fits in u32 with bit 31 always zero, so the + // CR0 view is benign — use the catch-all 32-bit form for consistency. ctx.gpr[instr.ra()] = (ctx.gpr[instr.rs()] as u32).leading_zeros() as u64; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::cntlzdx => { @@ -601,19 +620,23 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== Shift ===== PpcOpcode::slwx => { + // PPCBUG-044: 32-bit ABI CR0 view. A result with bit 31 set + // (e.g. 0x80000000) is negative in i32 view but positive in i64. let sh = ctx.gpr[instr.rb()] as u32; ctx.gpr[instr.ra()] = if sh < 32 { ((ctx.gpr[instr.rs()] as u32) << sh) as u64 } else { 0 }; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::srwx => { + // PPCBUG-044: 32-bit ABI CR0 view (zero-extended right shift can never + // have bit 31 set, but use the canonical form for consistency). let sh = ctx.gpr[instr.rb()] as u32; ctx.gpr[instr.ra()] = if sh < 32 { ((ctx.gpr[instr.rs()] as u32) >> sh) as u64 } else { 0 }; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::srawx => { @@ -707,7 +730,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let rotated = rs.rotate_left(sh); let mask = rlw_mask(mb, me); ctx.gpr[instr.ra()] = (rotated & mask) as u64; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-024: 32-bit ABI CR0 view. + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::rlwimix => { @@ -719,7 +743,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let mask = rlw_mask(mb, me); let ra = ctx.gpr[instr.ra()] as u32; ctx.gpr[instr.ra()] = ((rotated & mask) | (ra & !mask)) as u64; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-025: 32-bit ABI CR0 view. + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::rlwnmx => { @@ -730,7 +755,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let rotated = rs.rotate_left(sh); let mask = rlw_mask(mb, me); ctx.gpr[instr.ra()] = (rotated & mask) as u64; - if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } + // PPCBUG-026: 32-bit ABI CR0 view. + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::rldiclx => { @@ -5004,14 +5030,15 @@ mod tests { #[test] fn addo_sets_xer_ov_on_signed_overflow_and_stickies_so() { + // PPCBUG-012: 32-bit ABI. INT32_MAX + 1 overflows to INT32_MIN. let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); - ctx.gpr[3] = i64::MAX as u64; + ctx.gpr[3] = i32::MAX as u32 as u64; ctx.gpr[4] = 1; write_instr(&mut mem, 0, addx_raw(5, 3, 4, true, false)); ctx.pc = 0; step(&mut ctx, &mut mem); - assert_eq!(ctx.gpr[5], i64::MIN as u64); + assert_eq!(ctx.gpr[5], 0x8000_0000u64); assert_eq!(ctx.xer_ov, 1, "OV must be set on signed overflow"); assert_eq!(ctx.xer_so, 1, "SO must be stickied from OV"); } @@ -5046,9 +5073,10 @@ mod tests { } #[test] - fn addx_rc_uses_64bit_compare_not_32bit() { - // r3 = 0x0000_0000_FFFF_FFFF, r4 = 0 → result = 0x0000_0000_FFFF_FFFF. - // As i32 this is -1 (lt). As i64 this is positive (gt). Spec says 64-bit. + fn addx_rc_uses_32bit_compare_in_xbox_abi() { + // PPCBUG-012+020: 32-bit ABI. r3 + r4 = 0xFFFFFFFF (low 32). As i32 + // this is -1 (CR0.LT). The previous 64-bit compare wrongly classified + // this as positive (CR0.GT) for Xbox 360 binaries. let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); ctx.gpr[3] = 0x0000_0000_FFFF_FFFF; @@ -5057,8 +5085,8 @@ mod tests { ctx.pc = 0; step(&mut ctx, &mut mem); assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFF); - assert!(!ctx.cr[0].lt, "64-bit compare: value is positive, not negative"); - assert!(ctx.cr[0].gt); + assert!(ctx.cr[0].lt, "32-bit ABI: 0xFFFFFFFF as i32 is -1, CR0.LT"); + assert!(!ctx.cr[0].gt); assert!(!ctx.cr[0].eq); } @@ -5203,6 +5231,39 @@ mod tests { assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)"); } + #[test] + fn andisx_sign_bit_set_classifies_lt() { + // PPCBUG-023: andis. r4, r3, 0x8000 with r3=0xFFFFFFFF should produce + // result=0x80000000 with CR0.LT=1 (i32 view). + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFF_FFFFu64; + // andis. r4, r3, 0x8000: opcode 29, uimm16 = 0x8000 + let raw = (29u32 << 26) | (3 << 21) | (4 << 16) | 0x8000; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[4], 0x8000_0000u64); + assert!(ctx.cr[0].lt, "result=0x80000000 → i32 view negative → CR0.LT"); + } + + #[test] + fn slwx_high_bit_result_classifies_lt() { + // PPCBUG-044: slwx producing 0x80000000 must classify as CR0.LT under + // the 32-bit ABI, not CR0.GT (which 64-bit view would give). + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0x4000_0000u64; + ctx.gpr[4] = 1; + // slwx. r5, r3, r4 (XO=24, Rc=1) + let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (24 << 1) | 1; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x8000_0000u64); + assert!(ctx.cr[0].lt, "0x80000000 as i32 is negative"); + } + #[test] fn lha_negative_halfword_zero_extends_upper() { // PPCBUG-095: memory 0x8000 must yield gpr[rD] = 0x00000000_FFFF8000.