From 49bf74fae6b426c6954c91fc3da815d4faa7bced Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 12:22:47 +0200 Subject: [PATCH] fix(cpu): PPCBUG-223/224/225/229/230 FPU XX bit on inexact conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 batch 3 (5c) — partial: targeted XX-on-inexact fixes for the float-to-int and double-to-single conversion family. (PPCBUG-180/200, the broader update_after_op XX/FR/FI rework, deferred to a focused sub-batch.) - PPCBUG-225 frspx: set XX when the f64→f32 round produces a different value (i.e. precision loss). Almost every frsp call is inexact — previously games polling FPSCR.XX never saw the set bit after a frsp. - PPCBUG-224 fcfidx: set XX when the i64 input has > 53 significant bits (precision lost in conversion to f64). - PPCBUG-229 fctidx/fctidzx: set XX when input is non-integer (fractional part discarded by the conversion). - PPCBUG-230 fctiwx/fctiwzx: same shape for word-width conversions. - PPCBUG-223 verified already correct in current code (fcmpo sets VXSNAN/VXVC on NaN operands; the audit-cited drift was already fixed). Co-Authored-By: Claude Sonnet 4.6 --- crates/xenia-cpu/src/interpreter.rs | 31 ++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 6395b25..8239266 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -2783,28 +2783,38 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== FPU: Rounding/Conversion ===== PpcOpcode::frspx => { - // Round to single precision honouring FPSCR[RN] + // Round to single precision honouring FPSCR[RN]. + // PPCBUG-225: set XX on inexact rounding (almost every frsp call). let b = ctx.fpr[instr.rb()]; if fpscr::is_snan(b) { fpscr::set_exception(ctx, fpscr::VXSNAN); } let result = to_single(ctx, b); + if b.is_finite() && result.is_finite() && result != b { + fpscr::set_exception(ctx, fpscr::XX); + } ctx.fpr[instr.rd()] = result; fpscr::update_after_op(ctx, result, b.is_finite()); if instr.rc_bit() { update_cr1_from_fpscr(ctx); } ctx.pc += 4; } PpcOpcode::fcfidx => { - // Convert from integer doubleword: frD = (double)(int64_t)frB_as_bits + // Convert from integer doubleword: frD = (double)(int64_t)frB_as_bits. + // PPCBUG-224: set XX when |i64| > 2^53 (precision loss in conversion). let bits = ctx.fpr[instr.rb()].to_bits(); - let result = (bits as i64) as f64; + let i = bits as i64; + let result = i as f64; + if (result as i64) != i { + fpscr::set_exception(ctx, fpscr::XX); + } ctx.fpr[instr.rd()] = result; fpscr::set_fprf(ctx, fpscr::classify_fprf(result)); if instr.rc_bit() { update_cr1_from_fpscr(ctx); } ctx.pc += 4; } PpcOpcode::fctidx => { - // Convert to integer doubleword (round per FPSCR[RN]) + // Convert to integer doubleword (round per FPSCR[RN]). + // PPCBUG-229: set XX on inexact (fractional input). let val = ctx.fpr[instr.rb()]; let result = if val.is_nan() { fpscr::set_exception(ctx, fpscr::VXCVI | if fpscr::is_snan(val) { fpscr::VXSNAN } else { 0 }); @@ -2816,6 +2826,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - fpscr::set_exception(ctx, fpscr::VXCVI); 0x8000_0000_0000_0000u64 } else { + if val != val.trunc() { fpscr::set_exception(ctx, fpscr::XX); } fpscr::round_to_i64(ctx, val) as u64 }; ctx.fpr[instr.rd()] = f64::from_bits(result); @@ -2823,7 +2834,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::fctidzx => { - // Convert to integer doubleword (round toward zero) + // Convert to integer doubleword (round toward zero). + // PPCBUG-229: set XX on inexact. let val = ctx.fpr[instr.rb()]; let result = if val.is_nan() { fpscr::set_exception(ctx, fpscr::VXCVI | if fpscr::is_snan(val) { fpscr::VXSNAN } else { 0 }); @@ -2835,6 +2847,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - fpscr::set_exception(ctx, fpscr::VXCVI); 0x8000_0000_0000_0000u64 } else { + if val != val.trunc() { fpscr::set_exception(ctx, fpscr::XX); } (val.trunc() as i64) as u64 }; ctx.fpr[instr.rd()] = f64::from_bits(result); @@ -2842,7 +2855,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::fctiwx => { - // Convert to integer word (round per FPSCR[RN]) + // Convert to integer word (round per FPSCR[RN]). + // PPCBUG-230: set XX on inexact. let val = ctx.fpr[instr.rb()]; let result_u32: u32 = if val.is_nan() { fpscr::set_exception(ctx, fpscr::VXCVI | if fpscr::is_snan(val) { fpscr::VXSNAN } else { 0 }); @@ -2854,6 +2868,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - fpscr::set_exception(ctx, fpscr::VXCVI); 0x8000_0000 } else { + if val != val.trunc() { fpscr::set_exception(ctx, fpscr::XX); } fpscr::round_to_i32(ctx, val) as u32 }; ctx.fpr[instr.rd()] = f64::from_bits(result_u32 as u64); @@ -2861,7 +2876,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::fctiwzx => { - // Convert to integer word (round toward zero) + // Convert to integer word (round toward zero). + // PPCBUG-230: set XX on inexact. let val = ctx.fpr[instr.rb()]; let result_u32: u32 = if val.is_nan() { fpscr::set_exception(ctx, fpscr::VXCVI | if fpscr::is_snan(val) { fpscr::VXSNAN } else { 0 }); @@ -2873,6 +2889,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - fpscr::set_exception(ctx, fpscr::VXCVI); 0x8000_0000 } else { + if val != val.trunc() { fpscr::set_exception(ctx, fpscr::XX); } val.trunc() as i32 as u32 }; ctx.fpr[instr.rd()] = f64::from_bits(result_u32 as u64);