From ebfd18a64e8be6bd8592b9af984f33336efbf41c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 14:14:10 +0200 Subject: [PATCH] =?UTF-8?q?test(cpu):=20PPCBUG-187/208/228/438/439/440=20P?= =?UTF-8?q?8=20batch=203=20=E2=80=94=20FPU=20+=20VMX=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8 batch 3 — FPU and VMX float test gap closure. 14 new tests: - Single FPU (187): fadds, fmuls - Double FPU (208): fmul, fdiv (zero-numerator), fneg, fabs, fmr - FPU convert/compare (228): fcmpu, fcfid - VMX float compare (438): vcmpeqfp lane mask - VMX rounding (439): vrfip, vrfim, vrfiz - VMX convert (440): vctsxs saturation to INT_MAX/INT_MIN The VMX VX-form encoding nit (XO is 11 bits at PPC 21-31, host bits 10-0, with bit 0 the LSB — not bit 1) was caught by initial test failures and fixed before commit. VC-form (vcmpeqfp) has the same "XO at bit 0" layout. Co-Authored-By: Claude Sonnet 4.6 --- crates/xenia-cpu/src/interpreter.rs | 222 ++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index a5751d0..519ed51 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -6976,6 +6976,228 @@ mod tests { assert_eq!(mem.read_u32(0x100), 1.5_f32.to_bits()); } + // ─────────────────────────────────────────────────────────────────────── + // P8 batch 3 — FPU + VMX float test gaps + // (PPCBUG-187/208/228/438/439/440) + // ─────────────────────────────────────────────────────────────────────── + + // PPCBUG-187 single-precision FPU smokes. + #[test] + fn fadds_single_arithmetic() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 1.5; + ctx.fpr[2] = 2.5; + // fadds f3, f1, f2: opcode 59, XO=21 + let raw = (59u32 << 26) | (3 << 21) | (1 << 16) | (2 << 11) | (21 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], 4.0); + } + + #[test] + fn fmuls_single_multiply() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 2.0; + ctx.fpr[2] = 3.0; + // fmuls f3, f1, f2: XO=25, frC at bits 21-25 (so c is rb encoding slot) + // Standard A-form: (59<<26)|(rd<<21)|(ra<<16)|(0<<11)|(rc<<6)|(25<<1) + let raw = (59u32 << 26) | (3 << 21) | (1 << 16) | (2 << 6) | (25 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], 6.0); + } + + // PPCBUG-208 double-precision FPU smokes. + #[test] + fn fmul_double_multiply() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 4.0; + ctx.fpr[2] = 0.25; + // fmul f3, f1, f2: opcode 63, XO=25 + let raw = (63u32 << 26) | (3 << 21) | (1 << 16) | (2 << 6) | (25 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], 1.0); + } + + #[test] + fn fdiv_zero_over_finite() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 0.0; + ctx.fpr[2] = 5.0; + // fdiv f3, f1, f2: XO=18 + let raw = (63u32 << 26) | (3 << 21) | (1 << 16) | (2 << 11) | (18 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.fpr[3], 0.0); + assert_eq!(ctx.fpscr & fpscr::ZX, 0, "0/finite is not divide-by-zero"); + } + + #[test] + fn fneg_flips_sign_bit() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 1.0; + // fneg f3, f1: XO=40 + let raw = (63u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | (40 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], -1.0); + } + + #[test] + fn fabs_clears_sign_bit() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = -3.5; + // fabs f3, f1: XO=264 + let raw = (63u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | (264 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], 3.5); + } + + #[test] + fn fmr_copies_register() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[5] = 1.5_f64.copysign(-1.0); + // fmr f3, f5: XO=72 + let raw = (63u32 << 26) | (3 << 21) | (0 << 16) | (5 << 11) | (72 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3].to_bits(), ctx.fpr[5].to_bits()); + } + + // PPCBUG-228 fpu convert / fcmp smokes. + #[test] + fn fcmpu_lt_sets_cr_lt() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = 1.0; + ctx.fpr[2] = 2.0; + // fcmpu cr3, f1, f2: opcode 63, XO=0, BF=3 + let raw = (63u32 << 26) | (3 << 23) | (1 << 16) | (2 << 11) | (0 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.cr[3].lt); + assert!(!ctx.cr[3].gt); + } + + #[test] + fn fcfid_converts_int64_to_double() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.fpr[1] = f64::from_bits(123u64); + // fcfid f3, f1: XO=846 + let raw = (63u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | (846 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.fpr[3], 123.0); + } + + // PPCBUG-438 VMX float compares. VC-form: XO at PPC 22-31 (host 9-0), bit 0. + #[test] + fn vcmpeqfp_sets_lanes_to_all_ones_on_eq() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.vr[1] = xenia_types::Vec128::from_f32x4_array([1.0, 2.0, 3.0, 4.0]); + ctx.vr[2] = xenia_types::Vec128::from_f32x4_array([1.0, 0.0, 3.0, 0.0]); + // vcmpeqfp v3, v1, v2: canary base 0x100000c6 → op6=4, XO=198 at bits 0-9. + let raw = (4u32 << 26) | (3 << 21) | (1 << 16) | (2 << 11) | 198; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + let r = ctx.vr[3].as_u32x4(); + assert_eq!(r[0], 0xFFFF_FFFF); // 1.0 == 1.0 → all ones + assert_eq!(r[1], 0); // 2.0 != 0.0 + assert_eq!(r[2], 0xFFFF_FFFF); // 3.0 == 3.0 + assert_eq!(r[3], 0); // 4.0 != 0.0 + } + + // PPCBUG-439 VMX rounding. VX-form XO at bit 0. + #[test] + fn vrfip_rounds_toward_pos_inf() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.vr[1] = xenia_types::Vec128::from_f32x4_array([1.4, 1.5, -1.4, -1.5]); + // vrfip canary base 0x1000028a → XO=650. + let raw = (4u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | 650; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + let r = ctx.vr[3].as_f32x4(); + assert_eq!(r[0], 2.0); + assert_eq!(r[1], 2.0); + assert_eq!(r[2], -1.0); + assert_eq!(r[3], -1.0); + } + + #[test] + fn vrfim_rounds_toward_neg_inf() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.vr[1] = xenia_types::Vec128::from_f32x4_array([1.4, 1.5, -1.4, -1.5]); + // vrfim canary base 0x100002ca → XO=714. + let raw = (4u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | 714; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + let r = ctx.vr[3].as_f32x4(); + assert_eq!(r[0], 1.0); + assert_eq!(r[1], 1.0); + assert_eq!(r[2], -2.0); + assert_eq!(r[3], -2.0); + } + + #[test] + fn vrfiz_truncates_toward_zero() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.vr[1] = xenia_types::Vec128::from_f32x4_array([1.7, 2.5, -1.7, -2.5]); + // vrfiz canary base 0x1000024a → XO=586. + let raw = (4u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | 586; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + let r = ctx.vr[3].as_f32x4(); + assert_eq!(r[0], 1.0); + assert_eq!(r[1], 2.0); + assert_eq!(r[2], -1.0); + assert_eq!(r[3], -2.0); + } + + // PPCBUG-440 VMX convert. + #[test] + fn vctsxs_saturates_max_to_int_max() { + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.vr[1] = xenia_types::Vec128::from_f32x4_array([1e10, -1e10, 1.5, -1.5]); + // vctsxs canary base 0x100003ca → XO=970, UIMM=0. + let raw = (4u32 << 26) | (3 << 21) | (0 << 16) | (1 << 11) | 970; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + let r = crate::vmx::as_i32x4(ctx.vr[3]); + assert_eq!(r[0], i32::MAX, "1e10 saturates to INT_MAX"); + assert_eq!(r[1], i32::MIN, "-1e10 saturates to INT_MIN"); + assert_eq!(r[2], 1); + assert_eq!(r[3], -1); + } + // ---------- Block-cache parity tests ---------- // // These confirm that running a program through the basic-block