test(cpu): PPCBUG-187/208/228/438/439/440 P8 batch 3 — FPU + VMX float

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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 14:14:10 +02:00
parent 2d223eee69
commit ebfd18a64e

View File

@@ -6976,6 +6976,228 @@ mod tests {
assert_eq!(mem.read_u32(0x100), 1.5_f32.to_bits()); 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 ---------- // ---------- Block-cache parity tests ----------
// //
// These confirm that running a program through the basic-block // These confirm that running a program through the basic-block