From 20a730d69e29200a7cf0c1fea1e7711997d9f583 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 11:47:24 +0200 Subject: [PATCH] fix(cpu): PPCBUG-095/096/097/098/105 halfword + lwa load truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 batch 5: 5 PPCBUGs in the load family. lha/lhax/lhau/lhaux sign-extended halfword results to u64 (active poisoning for negative halfwords); lwa/lwax/lwaux sign-extended u32 results. - PPCBUG-095/096/097/098 lha[ux]: `as i16 as i64 as u64` → `as i16 as i32 as u32 as u64`. Sign-extend to i32 then zero-extend. Common trigger: int16_t struct fields, PCM samples, packed vertex deltas. Memory 0x8000 was producing 0xFFFFFFFF_FFFF8000. - PPCBUG-105 lwa/lwax/lwaux: `as i32 as i64 as u64` → `as u64`. Per-canary the 64-bit-mode form sign-extends, but in 32-bit ABI we must zero-extend (canary's behavior is rescued by x86 register zeroing in JIT; pure interpreter has no escape). Memory 0x80000000 was producing 0xFFFFFFFF_80000000. Tests: - lha_negative_halfword_zero_extends_upper (PPCBUG-095). - lhaux_negative_halfword_clean_writeback (PPCBUG-098 + EA update). - lwa_high_bit_set_zero_extends_upper (PPCBUG-105). Co-Authored-By: Claude Sonnet 4.6 --- crates/xenia-cpu/src/interpreter.rs | 63 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 652d566..c730e75 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -1030,13 +1030,13 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - PpcOpcode::lha => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.pc += 4; } PpcOpcode::lhax => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.pc += 4; } PpcOpcode::lhzux => { @@ -1047,13 +1047,13 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } PpcOpcode::lhau => { let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } PpcOpcode::lhaux => { let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } @@ -1072,18 +1072,18 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - PpcOpcode::lwa => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.pc += 4; } PpcOpcode::lwax => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.pc += 4; } PpcOpcode::lwaux => { let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } @@ -5203,6 +5203,55 @@ mod tests { assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)"); } + #[test] + fn lha_negative_halfword_zero_extends_upper() { + // PPCBUG-095: memory 0x8000 must yield gpr[rD] = 0x00000000_FFFF8000. + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u16(0x100, 0x8000); + ctx.gpr[3] = 0x100; + // lha r5, 0(r3): opcode 42 + let raw = (42u32 << 26) | (5 << 21) | (3 << 16) | 0; + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_8000u64); + } + + #[test] + fn lhaux_negative_halfword_clean_writeback() { + // PPCBUG-098: indexed update form. Memory 0xFFFF → rD = 0x00000000_FFFFFFFF; + // rA must update to the EA. + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u16(0x200, 0xFFFF); + ctx.gpr[3] = 0x100; // ra + ctx.gpr[4] = 0x100; // rb + // lhaux r5, r3, r4 (XO=375) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (375 << 1); + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFFu64); + assert_eq!(ctx.gpr[3], 0x200, "rA updated to EA"); + } + + #[test] + fn lwa_high_bit_set_zero_extends_upper() { + // PPCBUG-105: memory 0x80000000 must yield rD = 0x00000000_80000000 + // under 32-bit ABI (no sign extension to bits 32-63). + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u32(0x100, 0x8000_0000); + ctx.gpr[3] = 0x100; + // lwa r5, 0(r3): opcode 58, XO=2 (DS-form, ds=0) + let raw = (58u32 << 26) | (5 << 21) | (3 << 16) | 2; + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_8000_0000u64); + } + #[test] fn mullwx_overflow_truncates_to_32() { // PPCBUG-009: mullwo r5, r3, r4 with ra=0x10000, rb=0x10000 → product