GXRuntime: inline the hot paths of the FP gate and paired-single access - #9
Conversation
Profiling a recompiled Mario Kart: Double Dash on Apple Silicon (`sample`, race
scene) put these near the top of self time inside StaticRecompCore::Run:
ppc_fp_available 869 samples ~7.1%
psq_load_value + psq_store_value 1101 samples ~9.0%
Generated code calls them once per instruction site -- 121,874 FP sites and
19,642 paired-single sites on this title.
ppc_fp_available_inline() puts the MSR[FP] test in the header and leaves only
the exception raise out of line. A running game always has MSR[FP] set, so the
cost was the call, not the test.
ppc_psq_load_inline()/ppc_psq_store_inline() handle GQR type 0 -- plain IEEE
singles, no quantisation, no scale, which is what games leave GQR0 at -- as two
32-bit accesses and a conversion. Quantised types (4..7), invalid types and the
LSQE illegal-instruction check all fall through to the existing out-of-line
functions, so behaviour is unchanged. Nothing new had to be exposed:
f64_value, convert_to_double, convert_to_single_ftz and f64_bits were already
static inline in core/types.h.
ppc_fp_available(), ppc_psq_load() and ppc_psq_store() all remain real symbols.
The LLVM backend emits calls to them by name.
Measured on an idle Apple M5, alternating A/B with a reversed-order block:
inline FP gate +11.8% ranges do not overlap
inline paired-single +4.0% ranges do not overlap
Both beat their profiled share, because an out-of-line call is also an
optimisation barrier: at every generated site w, gqr_index, indexed and cia are
literals, so inlining lets the compiler fold the LSQE test and the w branch away
per site. The paired-single module came out slightly SMALLER despite 19,642
inlined sites, which is that folding showing up.
Together with PGO on top (+7.5%), MKDD under static recompilation on Metal went
51.1 -> 62.5 fps single-player and 40.6 -> 50.8 fps split-screen.
Pairs with the DolRecomp change that emits the _inline forms. This one should
land first: with the emitter updated and these absent, a port fails to compile.
|
Correction to the absolute fps figures in this description. The relative results are unaffected; the absolutes are roughly half what the hardware does. Every macOS number I recorded before 2026-08-06 was measured with the laptop in Low Power Mode — a 2.02× CPU throttle I only found today, by toggling the setting and watching a previously recorded figure reproduce to four decimal places. So this line:
should read, unthrottled and on AC: The +11.8% and +4.0% in this PR stand unchanged. They were alternating A/B arms measured inside a single session, so a constant throttle scales both arms and cancels — it cannot manufacture a difference between them. That is precisely why the measurements were interleaved rather than compared against a previous day's number. What the throttle invalidates is the absolute fps, not the deltas. Nothing in the diff changes. Flagging it only so the numbers in the description aren't read as this patch's ceiling — static recomp on this scene is comfortably past 60 fps, not scraping it. |
Profiling a recompiled Mario Kart: Double Dash on Apple Silicon (
sample, race scene) put two runtime helpers near the top of self time insideStaticRecompCore::Run:ppc_fp_availablepsq_load_value+psq_store_valueGenerated code calls them once per instruction site — 121,874 FP sites and 19,642 paired-single sites on this title.
What changed
ppc_fp_available_inline()puts the MSR[FP] test in the header, leaving only the exception raise out of line. A running game always has MSR[FP] set, so the cost was the call, not the test.ppc_psq_load_inline()/ppc_psq_store_inline()handle GQR type 0 — plain IEEE singles, no quantisation, no scale, which is what games leave GQR0 at — as two 32-bit accesses and a conversion. Quantised types (4..7), invalid types, and the LSQE illegal-instruction check all fall through to the existing out-of-line functions, so behaviour is unchanged.Nothing new had to be exposed:
f64_value,convert_to_double,convert_to_single_ftzandf64_bitswere alreadystatic inlineincore/types.h, and the memory accessors andPPC_HID2_LSQEwere already header-visible.ppc_fp_available(),ppc_psq_load()andppc_psq_store()all remain real symbols — the LLVM backend emits calls to them by name.Measured
Idle Apple M5, alternating A/B with a reversed-order block, largest competing process under 70%:
Both beat their profiled share. An out-of-line call is also an optimisation barrier: at every generated site
w,gqr_index,indexedandciaare literals, so inlining lets the compiler fold the LSQE test and thewbranch away per site. The paired-single module came out slightly smaller despite 19,642 inlined sites, which is that folding showing up.With PGO on top (+7.5%), MKDD under static recompilation on Metal went 51.1 → 62.5 fps single-player and 40.6 → 50.8 fps split-screen.
Ordering
Pairs with a DolRecomp change that emits the
_inlineforms. This one should land first — with the emitter updated and these absent, a port fails to compile.