From eb7c6f98cd979496d20b1c9c7ff2925314dbeefe Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 5 Jul 2026 15:14:24 +0200 Subject: [PATCH] [iterate-4C] JIT native FP-arith Phase 2: fused madd family (the 27.7% op) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native fmadds/fmsubs/fnmadds/fnmsubs + double siblings via hardware FMA (vfmadd213sd/vfmsub213sd — single rounding, matching the interpreter's correctly-rounded f64::mul_add; negated forms flip the result sign with xorpd, and NaN — which the interpreter preserves unnegated — is caught by the finite guard → deopt, so unconditional negate is exact on the fast path). Gated on a runtime cpuid FMA3 check (emit::host_has_fma, cached OnceLock); on a non-FMA host these arms fall back to the interpreter's software FMA — byte-identical, so goldens hold on any host (NEVER emit mulsd+addsd, which double-rounds). fp_arith_matches extended to the madd family (guarded on host FMA3). All 6-config GOLDEN n200m BYTE-IDENTICAL; 24 xenia-jit tests green. MEASURED (2B-instr run through the video, --gpu-inline, best-of-3) — CLEAN 3-way: interp 72.6s (27.5 MIPS) region JIT (pre-FP) 27.3s (73.2 MIPS) = 2.66x over interp region JIT + native FP 23.3s (85.8 MIPS) = 3.12x over interp So native FP's OWN contribution is ~1.17x (+17%) on top of the region JIT — it removed 62% of all interpreter fallbacks (306.6M→117.3M, the FP-arith share) but that translates to only ~17% wall-time here because the region JIT already made non-FP code fast and the --gpu-inline drain + plumbing dominate the remainder. This matches the conservative ~1.3-1.5x plan estimate (region JIT is the bigger CPU lever on the video; FP is a solid, deterministic increment and a multiplier for a future multi-core mode). Video still ~7x from real-time → multi-core next. --- crates/xenia-jit/src/emit.rs | 54 +++++++++++++++++++++++++++++++++++ crates/xenia-jit/src/tests.rs | 10 +++++++ 2 files changed, 64 insertions(+) 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) + } + } } }