Repository navigation
perf(fpx): fuse single-precision FMA, FRECPS and FRSQRTS on x86-64-v3 - #2383
Merged
Merged
Conversation
suyu's header takes the hardware FMA for recomp_fpx_fma32, _recps32 and _rsqrts32 only on an AArch64 host. On x86-64-v3, which already has RECOMP_FPX_FMA64, they computed in binary64 and fell back on every binary32 midpoint. nx2/suyu_rt_fixes.py patches the generated a64_suyu.h, as lift_fixes does for the lifter, so third_party is left alone. tests/test_fpx_sticky.py checks the fast paths against the exact bodies. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101
approved these changes
Nov 11, 2026
hckrman101
left a comment
Collaborator
There was a problem hiding this comment.
Looks right to me. I went through it against the Arm semantics:
- The three
#ifguards now take the__builtin_fmafbranch that an AArch64 host already uses, so x86-64-v3 runs exactly the code path FPX1 was designed and proven for there. The keep conditions after it are unchanged: only a normal result (or a finite zero-product for FMADD) under an open gate (IXC already set, FPCR clean of RMode/DN/AHP/FZ16 and the inhibit bit) is kept, and FZ inputs andzare still screened. NaNs, infinities, tiny results and the FRECPS/FRSQRTS inf*0 case (NaN from the fma, never kept, exact body gives 2.0/1.5) all still fall back, so rounding mode, default-NaN, NaN propagation and flush-to-zero are all still the exact body's job. - FRSQRTS:
fmaf(-a, b, 3) * 0.5fmatches Arm's single rounding of(3 - a*b)/2whenever the halved value is normal, which is the only case kept; overflow before halving gives inf, so it falls back. - Gating:
RECOMP_FPX_FMA64is only 1 for HOST 1, or HOST 2 with__FMA__on GCC/clang, so x86-64-v2/baseline (host_march()on a CPU without FMA), MSVC and wasm keep the binary64 form. Since the game is built with the march of the PC it runs on, no FMA instruction can reach a CPU without one. - Same results on both platforms: CI ran
test_fpx_sticky.py(with the newfastmode) and it passed on ubuntu-24.04 and windows-2022 (llvm-mingw). The midpoint cases I checked by hand (1+2^-12 squared, the FRECPS and FRSQRTS ones) really are binary32 midpoints, so the test does show the fused path is the one in use. - Cache:
make_game.lift_keyhashes every .py under nx2/, so the new file and the changed lift.py trigger a relift, and build.py's object key hashes the generated headers in gen/, soa64_suyu.hchanging recompiles the chunks. No stale lifted code. - Patching
a64_rtgen.runtime_hworks becausewrite()looks upruntime_has a module global at call time, and the exactly-once substitution check is a good guard for a64-kit refreshes.
CI: the failing checks are test_vcore_fs_open / test_vcore_video_acquire (main fails them too right now) and the AppImage job timing out. None of that is from this PR.
What's missing is evidence from the game: one relift and run, to check it boots and plays normally, plus a before/after frame time from somewhere FMA-heavy, would back up the perf claim. That's not blocking given how much the test covers, but it would be good to have before merge. Nit: expect a small import-block conflict with #2324, as you noted.
RufflezAU
pushed a commit
to RufflezAU/Hookshot
that referenced
this pull request
Nov 12, 2026
…dedupe, residual-ratio interpolation - vk_replay: NotRexed#1993 (batch out at an idle GPU) with RX_VK_BATCH_IDLE_MIN (default 2) so the GPU is fed during the frame instead of in one burst at its end: 20.4 -> 24.5 fps measured. Guest barriers no longer end the render pass (no pass-count change: harmless). - vk_draw: the storage-pointer handle guard (upstream 758e563, fragment stages only) - the fairy and NPC boxes, the water flicker, objects vanishing. - vk_draw: push-constant dedupe, index-buffer dedupe, and NOP_SET_RT only bumps the serial when the target set actually changed. - fg_synth: the residual-ratio interpolation (SAFE=3, default): per pixel, the warp's error against a held frame's error, both measured on the same pixel; never a mix of two frames. SAFE=1 plain blend, SAFE=2 hold, SAFE=0 the old warp. - REJECTED after measurement: NotRexed#2383 fused FMA (the game never leaves the title) and a sampler cache by id (same). Both reverted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On x86-64-v3 builds the single-precision FMADD/FMLA, FRECPS and FRSQRTS fast paths still compute in binary64 and round twice, although the CPU has a hardware FMA. One fused instruction gives the same results and makes FMA-bound guest code 22 to 45% faster.
What changed
suyu's runtime header uses
__builtin_fmafinrecomp_fpx_fma32,recomp_fpx_recps32andrecomp_fpx_rsqrts32only whenRECOMP_FPX_HOST == 1(an AArch64 host). On x86-64-v3, whereRECOMP_FPX_FMA64is already set from__FMA__, they took the binary64 form with a midpoint test, and fell back to the exact body on every binary32 midpoint.New
nx2/suyu_rt_fixes.pywrapsa64_rtgen.runtime_h, aslift_fixesdoes for the lifter, and changes those three guards toRECOMP_FPX_HOST == 1 || RECOMP_FPX_FMA64. Nothing underthird_party/is edited. Each substitution must match exactly once, so a refreshed a64-kit that moves these lines fails loudly.nx2/lift.pyandnx2/test_leaf.pyinstall it next tolift_fixes.Builds at x86-64-v2, baseline x86-64, MSVC and wasm are unchanged:
RECOMP_FPX_FMA64is 0 there. An existing game picks the change up on its next lift (a new file undernx2/changes the lift key).Measurements
llvm-mingw clang 23 from the v2.0.14 builder, at the build's lifted-code flags (
-march=x86-64-v3 -ffp-contract=off -fno-strict-aliasing -O2), headers from the real generators. Synthetic kernels shaped like the lifted call forms; thread CPU cycles per element (QueryThreadCycleTime), median of 31 samples, 7 interleaved before/after rounds:The new header emits
vfmadd/vfnmaddand nofmafcall.The game-wide gain depends on how much of a frame runs these instructions, which needs an in-game frame-time comparison. I have not run one.
How it was tested
tests/fpx_sticky_test.cgets afastmode: the inline fast paths againstrecomp_fpe_*, value and FPSR, under random FPCR/FPSR witha*bnear 1, 2 or 3 as in the Newton steps, plus three binary32 midpoints that must be kept when the build has FMA.tests/test_fpx_sticky.pyruns it with 8M cases in both the v3 and v2 builds: 0 mismatches. Withoutsuyu_rt_fixesinstalled the v3 run fails (the midpoints fall back).zgave 85k mismatches in 2M.python -I -B tests/test_fpx_sticky.pywithHOOKSHOT_MINGWat the builder's llvm-mingw: passes.python -I -B tests/run_all.pyon Windows: the same failing set as untouchedmainat 32693f0, none new (the failures here come from tools missing on this PC).nx2/test_leaf.pyneeds the dump and was not run.Notes
The only behaviour change is that binary32 midpoints now stay on the fast path, with the same result. #2324 moves the
a64_rtgen.writecall innx2/lift.py; this change patches at import time, so it still applies, with at most a small conflict in the import block.