diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index d458e0d..b5f7db6 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -749,6 +749,49 @@ pub fn try_emit_native( state.retire(); Emit::Native } + // ===== Phase 2: fused multiply-add family (the 27.7% op). vfmadd/vfmsub + // give the single rounding the interpreter's `mul_add` uses; gated on host + // FMA3 (else fall back — byte-identical software FMA). Rc=1 → fallback. ===== + PpcOpcode::fmaddsx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Madd, true); + state.retire(); + Emit::Native + } + PpcOpcode::fmsubsx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Msub, true); + state.retire(); + Emit::Native + } + PpcOpcode::fnmaddsx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmadd, true); + state.retire(); + Emit::Native + } + PpcOpcode::fnmsubsx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmsub, true); + state.retire(); + Emit::Native + } + PpcOpcode::fmaddx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Madd, false); + state.retire(); + Emit::Native + } + PpcOpcode::fmsubx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Msub, false); + state.retire(); + Emit::Native + } + PpcOpcode::fnmaddx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmadd, false); + state.retire(); + Emit::Native + } + PpcOpcode::fnmsubx if !instr.rc_bit() && host_has_fma() => { + emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmsub, false); + state.retire(); + Emit::Native + } // ===== Rotate/mask (sh/mb/me are compile-time constants -> the 32/64-bit // mask folds to a constant, so these are just rol + and). ===== @@ -1239,6 +1282,17 @@ fn emit_fp_store(ops: &mut Asm, off: &Offsets, helper: i64, rs: usize) { ); } +/// Whether the host CPU has FMA3. Cached once. The `madd` family emits +/// `vfmadd`/`vfmsub` (single rounding, matching the interpreter's correctly- +/// rounded `f64::mul_add`); without FMA3 those arms must fall back to the +/// interpreter (whose `mul_add` is software FMA — still byte-identical, never a +/// `mulsd`+`addsd` that would double-round). Determinism holds on any host. +pub fn host_has_fma() -> bool { + use std::sync::OnceLock; + static FMA: OnceLock = OnceLock::new(); + *FMA.get_or_init(|| std::arch::is_x86_feature_detected!("fma")) +} + /// A native FP-arithmetic op — selects operand roles and the x64 instruction. #[derive(Clone, Copy)] pub enum FpArith { diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index 198ce72..6ae7971 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -928,6 +928,16 @@ fn fp_arith_matches() { check_fp(enc_a(63, rd, ra, rb, 0, 20, 0), g, f); check_fp(enc_a(63, rd, ra, 0, rc, 25, 0), g, f); check_fp(enc_a(63, rd, ra, rb, 0, 18, 0), g, f); + // Phase 2: fused multiply-add family (XO 29/28/31/30 = madd/msub/nmadd/ + // nmsub), single + double — only native when the host has FMA3. + if std::arch::is_x86_feature_detected!("fma") { + for &op in &[59u32, 63] { + check_fp(enc_a(op, rd, ra, rb, rc, 29, 0), g, f); // fmadd(s) + check_fp(enc_a(op, rd, ra, rb, rc, 28, 0), g, f); // fmsub(s) + check_fp(enc_a(op, rd, ra, rb, rc, 31, 0), g, f); // fnmadd(s) + check_fp(enc_a(op, rd, ra, rb, rc, 30, 0), g, f); // fnmsub(s) + } + } } }