From f6a444b9d14a040db9421bb2860bcedb343d9498 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 12:13:08 +0200 Subject: [PATCH] fix(cpu): PPCBUG-221+227 round_to_i64 + PPCBUG-432 vrfin round-to-even MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 batch 1 (5a): round-to-int correctness. PPCBUG-221+227 (coupled): round_to_i64 NearestEven tie-breaking used `(diff - 0.5).abs() < f64::EPSILON` to detect half-integers, but for |v| > 2^52 every f64 value is an exact integer (v.trunc() == v), giving diff == 0. The buggy check fell through to v.round() (round-half-away- from-zero), giving wrong results for large odd half-integers. Replaced with a fractional-part-only check that's exact for |v| <= 2^52 and degenerates to truncation above. PPCBUG-432: vrfin/vrfin128 used Rust's `f32::round()` which is round- half-away-from-zero. ISA requires round-to-nearest-even (banker's rounding). Implemented inline. PPCBUG-201 (FPSCR.RN for double arithmetic) deferred — requires MXCSR-set/restore wrappers around 10+ FPU arms; will land in a focused sub-batch after the remaining 5a-5f fixes. Tests: - round_to_i64_nearest_even_on_tie: extended with 0.5, 1.5, -0.5, -1.5. - round_to_i64_non_tie_cases: 0.4/0.6 (non-tie sanity). - round_to_i32_nearest_even_on_tie: PPCBUG-227 coverage. Co-Authored-By: Claude Sonnet 4.6 --- crates/xenia-cpu/src/fpscr.rs | 47 ++++++++++++++++++++++++----- crates/xenia-cpu/src/interpreter.rs | 16 +++++++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/crates/xenia-cpu/src/fpscr.rs b/crates/xenia-cpu/src/fpscr.rs index 1d05e67..366e4f7 100644 --- a/crates/xenia-cpu/src/fpscr.rs +++ b/crates/xenia-cpu/src/fpscr.rs @@ -220,15 +220,22 @@ pub fn round_to_single(ctx: &PpcContext, v: f64) -> f64 { pub fn round_to_i64(ctx: &PpcContext, v: f64) -> i64 { match rounding_mode(ctx) { RoundingMode::NearestEven => { - // Round-half-to-even (banker's rounding). - let r = v.round(); - // Rust's f64::round is round-half-away-from-zero. Correct ties to even: - let diff = (v - v.trunc()).abs(); - if (diff - 0.5).abs() < f64::EPSILON { - let floor = v.floor(); - if (floor as i64) & 1 == 0 { floor as i64 } else { v.ceil() as i64 } + // PPCBUG-221: round-half-to-even (banker's rounding). The previous + // tie-detection used `(diff - 0.5).abs() < f64::EPSILON` which + // breaks for |v| > 2^52 (where v.trunc() == v exactly, giving diff + // == 0). Use a fractional-part-only check that's exact for + // |v| <= 2^52 and degenerates correctly above. + let t = v.trunc(); + let frac = v - t; + let fa = frac.abs(); + if fa > 0.5 { + t as i64 + if v >= 0.0 { 1 } else { -1 } + } else if fa < 0.5 { + t as i64 } else { - r as i64 + // Exact 0.5 tie — round to even. + let fi = t as i64; + if fi & 1 == 0 { fi } else { fi + if v >= 0.0 { 1 } else { -1 } } } } RoundingMode::TowardZero => v.trunc() as i64, @@ -355,11 +362,35 @@ mod tests { #[test] fn round_to_i64_nearest_even_on_tie() { let c = ctx(); + assert_eq!(round_to_i64(&c, 0.5_f64), 0); + assert_eq!(round_to_i64(&c, 1.5_f64), 2); assert_eq!(round_to_i64(&c, 2.5_f64), 2); assert_eq!(round_to_i64(&c, 3.5_f64), 4); + assert_eq!(round_to_i64(&c, -0.5_f64), 0); + assert_eq!(round_to_i64(&c, -1.5_f64), -2); assert_eq!(round_to_i64(&c, -2.5_f64), -2); } + #[test] + fn round_to_i64_non_tie_cases() { + // PPCBUG-221 regression: non-tie fractions must round to nearest. + let c = ctx(); + assert_eq!(round_to_i64(&c, 0.4_f64), 0); + assert_eq!(round_to_i64(&c, 0.6_f64), 1); + assert_eq!(round_to_i64(&c, -0.4_f64), 0); + assert_eq!(round_to_i64(&c, -0.6_f64), -1); + } + + #[test] + fn round_to_i32_nearest_even_on_tie() { + // PPCBUG-227: round_to_i32 inherits round_to_i64's tie semantics. + let c = ctx(); + assert_eq!(round_to_i32(&c, 0.5_f64), 0); + assert_eq!(round_to_i32(&c, 1.5_f64), 2); + assert_eq!(round_to_i32(&c, 2.5_f64), 2); + assert_eq!(round_to_i32(&c, -1.5_f64), -2); + } + #[test] fn check_invalid_add_detects_inf_minus_inf() { let mut c = ctx(); diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index aad2485..9ff2b6b 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -2398,11 +2398,25 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::vrfin | PpcOpcode::vrfin128 => { + // PPCBUG-432: ISA round-to-nearest-even, NOT Rust's round-half-away-from-zero. let vb = if matches!(instr.opcode, PpcOpcode::vrfin128) { instr.vb128() } else { instr.rb() }; let vd = if matches!(instr.opcode, PpcOpcode::vrfin128) { instr.vd128() } else { instr.rd() }; let b = ctx.vr[vb].as_f32x4(); let mut r = [0f32; 4]; - for i in 0..4 { r[i] = b[i].round(); } + for i in 0..4 { + let x = b[i]; + let t = x.trunc(); + let frac = (x - t).abs(); + r[i] = if frac > 0.5 { + t + if x >= 0.0 { 1.0 } else { -1.0 } + } else if frac < 0.5 { + t + } else { + // Tie — round to even. + let ti = t as i64; + if ti & 1 == 0 { t } else { t + if x >= 0.0 { 1.0 } else { -1.0 } } + }; + } ctx.vr[vd] = xenia_types::Vec128::from_f32x4_array(r); ctx.pc += 4; }