diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 00a6108..b5510b2 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -6545,6 +6545,196 @@ mod tests { assert_eq!(ctx.ctr, 0x8000_0001); } + // ─────────────────────────────────────────────────────────────────────── + // P8 — test gap closure (PPCBUG-055/067/070/081-085/089) + // ─────────────────────────────────────────────────────────────────────── + + // PPCBUG-055: branch test gaps. Cover blr, bdnz forward+backward, bcl LK. + + #[test] + fn blr_branches_to_lr_aligned() { + // bclr 20, 0 = blr — XO=16. lr lower 2 bits ignored. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.lr = 0x82001003; + ctx.pc = 0x100; + let raw = (19u32 << 26) | (20 << 21) | (0 << 16) | (16 << 1); + write_instr(&mut mem, 0x100, raw); + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x82001000, "blr aligns LR target to 4 bytes"); + } + + #[test] + fn bctr_branches_to_ctr_aligned() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.ctr = 0x82002007; + ctx.pc = 0x200; + // bcctr 20, 0 XO=528 + let raw = (19u32 << 26) | (20 << 21) | (0 << 16) | (528 << 1); + write_instr(&mut mem, 0x200, raw); + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x82002004); + } + + #[test] + fn bcl_lk_writes_lr_even_when_not_taken() { + // bcl with cond not satisfied still writes LR per ISA. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.cr[0] = crate::context::CrField { lt: false, gt: true, eq: false, so: false }; + ctx.pc = 0x100; + // bc 12, 0, +8, LK=1 — branch if CR0.LT=1 (false here) + let raw = (16u32 << 26) | (12 << 21) | (0 << 16) | (2 << 2) | 1; + write_instr(&mut mem, 0x100, raw); + step(&mut ctx, &mut mem); + assert_eq!(ctx.pc, 0x104, "not taken — pc advances"); + assert_eq!(ctx.lr, 0x104, "lk=1 writes LR even on not-taken"); + } + + // PPCBUG-070: CR logical test gaps. + + #[test] + fn cror_combines_cr_bits() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // CR bit 4 (cr1.lt=true), bit 8 (cr2.lt=false), result to bit 0 (cr0.lt) + ctx.cr[1] = crate::context::CrField { lt: true, gt: false, eq: false, so: false }; + ctx.cr[2] = crate::context::CrField { lt: false, gt: false, eq: false, so: false }; + // cror crbD=0, crbA=4, crbB=8: (19<<26)|(0<<21)|(4<<16)|(8<<11)|(449<<1) + let raw = (19u32 << 26) | (0 << 21) | (4 << 16) | (8 << 11) | (449 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.cr[0].lt, "cror 0,4,8 → cr0.lt = cr1.lt | cr2.lt = true"); + } + + #[test] + fn crand_combines_cr_bits() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.cr[1] = crate::context::CrField { lt: true, gt: false, eq: false, so: false }; + ctx.cr[2] = crate::context::CrField { lt: false, gt: false, eq: false, so: false }; + // crand crbD=0, crbA=4, crbB=8: XO=257 + let raw = (19u32 << 26) | (0 << 21) | (4 << 16) | (8 << 11) | (257 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].lt, "crand 0,4,8 → cr0.lt = cr1.lt & cr2.lt = false"); + } + + #[test] + fn crxor_self_self_clears_bit() { + // `crclr crbD` is encoded as `crxor crbD, crbD, crbD`. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.cr[0] = crate::context::CrField { lt: true, gt: false, eq: false, so: false }; + // crxor 0, 0, 0: XO=193 + let raw = (19u32 << 26) | (0 << 21) | (0 << 16) | (0 << 11) | (193 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].lt, "crxor self self → 0"); + } + + // PPCBUG-067: trap+sc test gaps. + + #[test] + fn sc_returns_systemcall_and_advances_pc() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // sc 0 + let raw = (17u32 << 26) | (1 << 1); + write_instr(&mut mem, 0x100, raw); + ctx.pc = 0x100; + let r = step(&mut ctx, &mut mem); + assert_eq!(r, StepResult::SystemCall); + assert_eq!(ctx.pc, 0x104, "sc leaves pc at NIA (return address)"); + } + + #[test] + fn tw_to_zero_never_traps() { + // TO=0 — every condition mask is 0, so no trap can fire. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + ctx.gpr[4] = 5; + // tw 0, r3, r4 XO=4 + let raw = (31u32 << 26) | (0 << 21) | (3 << 16) | (4 << 11) | (4 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + let r = step(&mut ctx, &mut mem); + assert_eq!(r, StepResult::Continue); + assert_eq!(ctx.pc, 4); + } + + // PPCBUG-081-085: SPR/MSR/TB/FPSCR/VSCR move test gaps. + + #[test] + fn mfcr_assembles_8_fields_into_u32() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // CR0=0b1010 (LT, EQ), CR7=0b0001 (SO), others zero. + ctx.cr[0] = crate::context::CrField { lt: true, gt: false, eq: true, so: false }; + ctx.cr[7] = crate::context::CrField { lt: false, gt: false, eq: false, so: true }; + // mfcr r3: XO=19 + let raw = (31u32 << 26) | (3 << 21) | (19 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + // CR0 nibble (high nibble) = 0b1010 = 0xA → byte 0xA0000000 + // CR7 nibble (low nibble) = 0b0001 = 0x1 → byte 0x00000001 + assert_eq!(ctx.gpr[3], 0xA000_0001); + } + + #[test] + fn mtfsb1_sets_fpscr_bit() { + // mtfsb1 sets a single bit in FPSCR. crbD=0 (bit 0 from MSB) sets FX (1<<31 in our u32 view). + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpscr = 0; + // mtfsb1 0: XO=38 + let raw = (63u32 << 26) | (0 << 21) | (38 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_ne!(ctx.fpscr & fpscr::FX, 0, "mtfsb1 0 sets FPSCR.FX"); + } + + #[test] + fn mtfsb0_clears_fpscr_bit() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpscr = fpscr::FX; + // mtfsb0 0: XO=70 + let raw = (63u32 << 26) | (0 << 21) | (70 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpscr & fpscr::FX, 0, "mtfsb0 0 clears FPSCR.FX"); + } + + // PPCBUG-089: cache + sync test gaps. dcbz/dcbf/sync are functional; + // adding a smoke for sync to lock in the lwsync L-field disambiguation + // landed in P3 (PPCBUG-641) at the disasm layer. + + #[test] + fn sync_advances_pc_no_state_change() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + let pre_xer = ctx.xer(); + let pre_fpscr = ctx.fpscr; + // sync L=0: XO=598 + let raw = (31u32 << 26) | (598 << 1); + write_instr(&mut mem, 0x100, raw); + ctx.pc = 0x100; + let r = step(&mut ctx, &mut mem); + assert_eq!(r, StepResult::Continue); + assert_eq!(ctx.pc, 0x104); + assert_eq!(ctx.xer(), pre_xer); + assert_eq!(ctx.fpscr, pre_fpscr); + } + // ---------- Block-cache parity tests ---------- // // These confirm that running a program through the basic-block