Skip to content

perf(lift): fast float regions, plain SSE checked once by MXCSR with the exact copy as fallback - #2433

Open
jordanella wants to merge 4 commits into
NotRexed:mainfrom
jordanella:perf/hot-paths
Open

jordanella wants to merge 4 commits into
NotRexed:mainfrom
jordanella:perf/hot-paths

Conversation

@jordanella

@jordanella jordanella commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

The frame is bound by the game's own job graph, and its biggest serial steps (Havok AI's character step, Link's
movement and collision) are mostly float math the lifter runs through an exactness check on every single op (about 25
instructions around each SSE op). This lifts straight runs of that math twice: a plain-SSE copy checked once, and the
exact copy kept as the fallback.

It's on by default. Reports from Intel, Linux and CPUs without AVX2 are still welcome (the SSE path has only run in
the unit test so far): HOS_NO_FASTFP turns it off on the same exe to compare.

How it works

a64_lift.py (ff_region, FASTFP_HEADER): a straight run of instructions with no label, store, call, branch or
system-register access, holding at least 8 single-precision adds, subtracts, multiplies, fused multiply-adds, divides
or square roots, is emitted twice:

  • fast copy: the ops as plain SSE (asm volatile, in Arm's operand order; multiply-adds as vfmadd231ss on FMA3
    builds), between one MXCSR flag clear and one read;
  • exact copy: the lifted code as before.

With no invalid, denormal or underflow flag (FTZ on under FPCR.FZ, so a tiny result is flushed and flagged), IEEE single
precision round-to-nearest gives Arm's result; a multiply-add rounds once, as on Arm. A few results x86 gets right in
value but not in Arm's flags, or not in value under FZ, raise the invalid flag on purpose (a64_ff_guard_):

  • any NaN result (x86 raises no denormal flag beside a NaN operand; a multiply-add's 0 * inf + a quiet NaN);
  • a product, quotient or multiply-add of exactly 2^-126 (x86 detects tininess after rounding, Arm before);
  • a subnormal divided by a zero (Arm under FZ divides 0 by 0).

Divide-by-zero and overflow give the same infinities; they and inexact are or-ed into FPSR. On an invalid, denormal or
underflow flag, the registers the run names are put back and the exact copy runs. The 2-lane vector ops load their
upper lanes as zeros, so those raise no flags.
The gate is the existing RECOMP_FPX_OPEN (FPCR at its defaults apart from FZ, FPSR.IXC set). Without FMA3 a
multiply-add calls the exact helper inside the fast copy; non-x86 builds compile the exact copy only.

Switches

  • A64_FASTFP=0 (lift time): off; hot: the hot functions only; check: both copies every time, the results compared,
    fastfp check: lines in the log (counts per chunk, and any mismatch with the run's guest address).
  • HOS_NO_FASTFP (run time): the exact copies only, to compare on the same exe.

Results (Ryzen 9 3900X, Windows; measured on ebbba2ea, before the rebase and before the result checks of 1e6b7a2, which add a compare per op)

  • 17,068 regions, 315,994 float ops across the game (47,371 of them multiply-adds; 13,114 regions without them).
  • Check mode, eight saves (Kokiri Forest, Hyrule Field, Hyrule Town, Hyrule Castle, the Deku Tree, Dodongo's
    Cavern, Jabu-Jabu's Belly, the Water Temple; 42–105 million region runs each): no wrong result. The Water Temple
    reports 16 mismatches, all in one run (0x831e6808). Rerunning its fast copy from the same inputs gave the exact
    copy's result every time; the one register that differed holds a float the run loads, which another thread writes
    between the two copies. Check mode runs each region twice, so it can see that; a normal build loads it once.
  • The fast copy is kept in 97–99% of runs in each scene, at least 95% in every file.
  • Temple of Time (alternating runs, same session; this PC's run-to-run noise is about ±8%):
    • without multiply-adds: 41.5 → 45.7 fps; Havok AI's character step 5.6 → 4.1 ms of CPU a frame (−26%);
    • with them, against that: 47.1 → 49.1 fps. Sampled, Link's update job takes 19% less time a frame than on main, its
      collision functions 44–56% less.
  • Hyrule Town and Hyrule Castle: no regression (two alternating pairs each, base against this PR).
    • Castle: 30.9 → 30.8 fps (the pairs differ by 3 fps between themselves).
    • Town: its fps moves between 18 and 25 from run to run of the same exe (the town's own load), so fps can't tell.
      Havok AI's character step drops in each pair, 9.4 → 7.9 ms of CPU a frame (−16%), and the BaseProc executors
      don't rise (118 → 114 and 121 → 88 ms).
  • The exe grows 470 → 512 MB (both copies kept).

Since the draft

  • Fused multiply-adds join regions (9f06061, @Jiro131's). They are about half the float ops in Link's movement and
    collision code, so without them most of its runs stayed under 8 ops; in Town and Castle one hot file also fell back to
    the exact copy in about 55% of runs, which it no longer does.
  • Results Arm doesn't give the same now take the exact copy (1e6b7a2): the three cases above, found by @Jiro131's
    tests/test_fastfp_fma.py (a product rounded up to 2^-126, a subnormal beside a NaN) and by the same test once the
    divide was added to it (a subnormal over a zero). They need inputs at the very bottom of the float range or NaNs, so
    check mode never met them in game, but the fast copy has to be exact.
  • Rebased onto main, including perf(fpx): fuse single-precision FMA, FRECPS and FRSQRTS on x86-64-v3 #2383 (the exact path's single-precision FMA fused on x86-64-v3): the exact copies get
    it, the fast copies use their own vfmadd231ss.
  • tests/test_fastfp_regions.py links libm only off Windows (the Windows runner's MSVC-target clang has no m.lib).

CI: Windows part 1 fails on tests/test_psychov35.py, from #2481 on main (scanf is deprecated under -Werror
with the runner's MSVC-target clang), not from this PR.

Tests

Both game-free.

  • tests/test_fastfp_fma.py (@Jiro131's, extended): each fast op alone in the MXCSR window against the exact body
    the lifted code falls back to (recomp_fpe_fma32, _div32, _add32, _sub32; the vector ops lane by lane, 4 lanes
    and 2), value and FPSR, FZ off and on: random operands weighted to the underflow and overflow edges, NaNs, subnormals
    and cancellations, plus an edge list. 0 mismatches in 2–8M checks per op. It fails if any of the result checks is
    removed, or the 2-lane ops load four lanes.
  • tests/test_fastfp_regions.py: what may join a region, and a whole region from the real header against plain
    IEEE results at -march=x86-64-v3 and x86-64-v2 (200,000 random runs; a multiply-add rounded once; NaN results,
    0 * inf, a denormal operand and a tiny result under FZ take the exact copy with the inputs put back; overflow and
    divide-by-zero stay fast with FPSR set; the gate closed or HOS_NO_FASTFP take the exact copy). It fails if the AVX
    operands are swapped or the invalid flag stops sending a region to its exact copy.

How to help test

  1. Build this branch as usual and play; anything that moves or behaves differently from main is worth a report.
  2. Or build with A64_FASTFP=check (slower to build and run), play anywhere, and look in hookshot_log.txt for
    fastfp check: MISMATCH: there should be none.
  3. On the same exe, compare HOS_THREAD_TIMES=5 with and without HOS_NO_FASTFP (the NavMeshCAStepDepWorker1 share).

🤖 Generated with Claude Code

@Jiro131

Jiro131 commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Tested on Intel as asked.

  • Nothing broke, but there is no measurable fps gain in Kokiri Forest.
  • I added fused multiply-add to the fast regions, since that is where the remaining exact-float time goes. It is checked bit-exact, but it doesn't move the frame rate either.
  • One correctness issue in the fast FMUL path: details and repro inputs below.

Setup:

  • i9-13900K (Raptor Lake, AVX2 + FMA3), RTX 4090, Windows 11;
  • this PR's commit cherry-picked onto upstream main at b744b1e, against the same tree without it;
  • same hookshot_vk.dll, settings and caches in every game folder.

1. This PR as it is

  • tests/test_fastfp_regions.py passes at x86-64-v3 and x86-64-v2.
  • The lift reports fast float regions (A64_FASTFP=1): 13114, 250040 float ops in them.

Where the exact float time goes: samples with an exact helper as the leaf, from HOS_SAMPLE_ALL (every game thread every 2 ms, about 7,000 instants per build, walking around Kokiri):

helper baseline this PR
a64_fpx_fma32_fma32 809 780
a64_fpx_mul32_fma32 719 362
a64_fpx_add32_add32 203 108
a64_fpx_sub32_sub32 113 75
a64_fpx_div32_div32 73 44
a64_fpx_sqrt32_sqrt32 46 26

The PR removes about 30% of these samples. fma32_fma32 is untouched and is now more than half of what is left.

2. Fused multiply-add on top: Jiro131/Hookshot@perf/fastfp-fma

The branch is one commit on top of aaf991d, so you can cherry-pick it if you want it.

What the commit does:

  • a64_ff_fma32_fma32 is a single vfmadd231ss under __FMA__. Without FMA3 (x86-64-v2) it stays the exact helper.
  • fma32_fma32 is added to FF_CONV.
  • FMADD, FMSUB, FNMADD and FNMSUB, and the FMLA and FMLS S lanes, all reach the fast path. The lifted code applies their negations before the call.

What x86 FMA gets right: it rounds once, as Arm does, and returns NaNs in Arm's order (addend, then a, then b).

Where it differs from Arm, probed on this CPU:

  • 0 × inf + quiet NaN. x86 returns the addend's NaN with no flag. Arm returns the default NaN and sets IOC.
  • Tininess. x86 detects it after rounding, Arm before. A result that rounds up to exactly 2^-126 raises no underflow on x86. Arm sets UFC, and under FZ flushes it to zero.
  • Example: 0x3f000001 * 0x00fffffe, whose exact product is (1 - 2^-46) · 2^-126. x86 gives 0x00800000 and only the inexact flag. Arm gives UFC, and 0 under FZ.

How the commit handles those two cases:

  • Any NaN result, or a result of magnitude 2^-126, runs a vcomiss on a NaN.
  • That raises invalid inside the window, so the run goes to its exact copy.
  • The check costs one compare and a branch that is almost never taken.

How it was checked:

  • tests/test_fastfp_fma.py (new). It builds the real recomp_fpe_fma32 the way test_fpx_sticky.py does, and runs the op alone inside the a64_ff_begin/a64_ff_end window.
    • Over 8M checks, every result the window keeps equals Arm's in value and FPSR, with FZ off and on: 0 mismatches.
    • The inputs are random operands weighted toward the underflow and overflow edges, NaNs, subnormals and cancellations, plus an edge list.
    • It fails if either guard is removed, or if the addend changes place (vfmadd213ss).
  • tests/test_fastfp_regions.py.
    • It now has an FMSUB in its region.
    • At v3 it covers the cases that must fall back: 0 × inf + qNaN, a product rounded up to 2^-126 with FZ off and on, and a subnormal addend under FZ.
    • It fails if fma32_fma32 is dropped from FF_CONV.
  • An A64_FASTFP=check build played in Kokiri.
    • About 222M region runs: 0 mismatches.
    • 94% of the runs were clean, though four files (code_022_1, code_023_1, code_024_3, code_024_7) are only 57–69% clean.
  • The lift now reports 17068, 315994 float ops in them: runs with FMAs now reach A64_FASTFP_MIN.

3. A correctness issue in the fast FMUL (and probably FDIV)

The same test runs a64_ff_mul32_fma32 against recomp_fpe_fma32 with a signed-zero addend, the way FMUL reaches the exact body. It finds 15,348 kept results out of 8M that differ from Arm's. They come from two cases.

A product that rounds up to 2^-126. This is the tininess difference above. FZ on gives a wrong value; FZ off gives only a missing UFC.

FMUL a=3f000001 b=00fffffe fpcr=0       fpsr=10: exact 00800000/18  fast 00800000/10
FMUL a=3f000001 b=00fffffe fpcr=1000000 fpsr=10: exact 00000000/18  fast 00800000/10

Under FZ, a subnormal times a quiet NaN. Arm flushes the subnormal and sets IDC. x86 raises no denormal flag when the other operand is a NaN.

FMUL a=003ed3cd b=7ffb5d55 fpcr=1000000 fpsr=10: exact 7ffb5d55/90  fast 7ffb5d55/10

These inputs are rare in game code, but the fast copy is meant to be exact.

  • Scalar ops: the FMA's guard (any NaN result, or |r| == 2^-126, sends the run to the exact copy) would close both cases for mulss.
  • mulps: it would need a per-lane check.
  • divss: I expect the 2^-126 case there too, but haven't tested it.

4. Frame times

Kokiri Forest, Link standing still where the save puts him, no input, uncapped (HOS_FPS_MAX=1000), 30 s per run, HOS_PRESENT_TRACE.

baseline this PR this PR + FMA
session 1, fps 58.94 58.75 –
session 2, pair 1, fps 60.77 – 61.46
session 2, pair 2, fps 60.44 – 60.26

The baseline itself moved 3% between sessions, so all of these differences are noise.

Why the frame barely moves:

Measured and written with Claude Code (Claude Opus 5.5).

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Its own new test fails to link on Windows (LNK1181: cannot open m.lib).
  • The other Windows failure is main's old ASTC test (#2397).
  • It's on by default "so a build tests it", which changes generated code for every player and grows the exe from 470 to 507 MB. That should be opt-in before it's ready.

@jordanella
jordanella marked this pull request as ready for review November 11, 2026 22:47
jordanella and others added 2 commits November 11, 2026 14:55
…the exact copy as fallback

A straight run of single-precision float ops (no label, store, call, branch or system register access; at least 8
adds, subtracts, multiplies, divides or square roots) is lifted twice: a plain SSE copy between one MXCSR flag clear
and one read, and the exact copy. With no invalid, denormal or underflow flag the fast result is Arm's (FTZ on under
FPCR.FZ, ops in Arm's operand order); divide-by-zero, overflow and inexact are or-ed into FPSR; otherwise the registers
the run names are put back and the exact copy runs. On by default; A64_FASTFP=0 off, =hot the hot functions only,
=check both copies every time with mismatches logged; HOS_NO_FASTFP at run time takes the exact copies only.

OoT, Temple of Time (Ryzen 9 3900X): 13,114 regions, 250,040 ops; check mode 0 mismatches in tens of millions of runs;
Havok AI's character step 5.5 -> 4.1 ms of CPU a frame. tests/test_fastfp_regions.py checks the regions game-free.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Windows runner's clang targets MSVC, which has no m.lib; Windows keeps the math functions in the C runtime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jordanella
jordanella marked this pull request as draft November 11, 2026 23:03
@jordanella

Copy link
Copy Markdown
Contributor Author

@Jiro131 thank you, both cases reproduce here (Ryzen 9 3900X): 0x3f000001 * 0x00fffffe gives 0x00800000 with only the inexact flag, FZ or not, and a subnormal times a quiet NaN raises no denormal flag.

  • Your multiply-add commit is in this PR as is (9f06061, you as author). It replaces the one I had pushed the same day, which was the same design without your 2^-126 guard.
  • 1e6b7a2 closes the rest:
    • every fast op checks its result: any NaN, and exactly 2^-126 for the multiply, divide and multiply-add;
    • a subnormal divided by a zero also takes the exact copy. Your test found this one once I added the divide to it: x86 gives an infinity with divide-by-zero only, while Arm under FZ divides 0 by 0 (the default NaN, IOC);
    • the 2-lane vector ops load their upper lanes as zeros, since an overflow in those lanes set OFC.
  • tests/test_fastfp_fma.py now holds all eight fast ops to the exact bodies (--strict-mul is gone): 0 mismatches in 2–8M checks per op, and removing any of the checks fails it.
  • On Kokiri: that matches what I see. The frame waits on the job chain, so the gain shows only where float-heavy jobs are on it (Temple of Time 41.5 → 45.7 fps; in Hyrule Town, Havok AI's character step −16% of CPU a frame).

@Joey-Magnus

  • CI: m.lib is fixed (0840188), and the branch is rebased past the ASTC fix. Windows part 1 now fails only on main's tests/test_psychov35.py (feat(hdr): PsychoV35 scene tone mapper for Native HDR (RenoDX) #2481).
  • Opt-in: I'd keep it on by default, for three reasons:
    • check mode across eight saves (42–105M region runs each) found no wrong result, apart from a race between the two copies that only check mode can have;
    • every op is now held bit-exact to the exact bodies, value and FPSR, by test_fastfp_fma.py;
    • opt-in would mean almost no one runs it, since the Builder doesn't expose A64_FASTFP.
  • Turning it off: HOS_NO_FASTFP at run time, A64_FASTFP=0 at lift time.
  • The cost: 42 MB of exe (470 → 512 MB).
  • If you'd still rather it ship off, I can flip the default.

@jordanella
jordanella marked this pull request as ready for review November 11, 2026 23:13
Jiro131 and others added 2 commits November 11, 2026 16:03
The fast float regions left a64_fpx_fma32_fma32 (FMADD/FMSUB/FNMADD/FNMSUB,
FMLA/FMLS S lanes) as an exact helper call; it is the hottest exact float
helper left in a Kokiri Forest profile. On x86-64-v3, vfmadd231ss rounds once
like Arm and takes its NaN in Arm's order. Two cases differ and now send the
run to its exact copy (an invalid flag raised in the window): any NaN result
(x86 returns 0 * inf + qNaN's NaN with no flag, Arm the default NaN with IOC),
and a result of magnitude 2^-126 (x86 detects tininess after rounding, Arm
before: UFC, and a zero under FZ). Without FMA3 the op stays the exact helper.

tests/test_fastfp_fma.py checks the op in the MXCSR window against
recomp_fpe_fma32 (8M checks, 0 mismatches); it also reports the fast FMUL's
15348 mismatches (the same 2^-126 case, and subnormal x qNaN under FZ).

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three cases kept a fast result that differs from Arm's, found by tests/test_fastfp_fma.py (Jiro131's):
- a product, quotient or multiply-add rounded up to exactly 2^-126: x86 detects tininess after rounding, Arm before,
  so Arm sets UFC and, under FZ, gives a zero;
- a NaN beside a subnormal operand: x86 raises no denormal flag, Arm under FZ flushes the subnormal and sets IDC;
- a subnormal over a zero: x86 gives an infinity with divide-by-zero only, Arm under FZ divides 0 by 0 (the default
  NaN, IOC).
Each op now checks its result (any NaN; exactly 2^-126 for the multiply, divide and multiply-add; the divide's
operands for the third case) and raises the invalid flag, so the run takes its exact copy. The 2-lane vector ops load
their upper lanes as zeros, which no longer raise flags of their own (an overflow there set OFC).

tests/test_fastfp_fma.py now holds every fast op, vector ones lane by lane, to the exact bodies: 0 mismatches in 2-8M
checks each; removing any of the checks fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@DFN-master

Copy link
Copy Markdown
Contributor

Test report — fast float regions

Machine

  • Windows 11, 64-bit; Intel Core i7-11700, 8C/16T; 32 GB RAM; NVIDIA GeForce RTX 3080 10 GB; 1920x1080 @ 60 Hz.
  • Test build: 4a53c897 + zstd/BC7 (my playable fork), with the current PR head 1e6b7a2f cherry-picked onto it. The exe is 526,973,440 bytes (503 MB) against 470,541,312 for the same build without the PR; the DLL and shader caches are the 4a53 ones.
  • Lift: A64_FASTFP=1 (default), hot chunks, PGO: 17,068 regions / 315,994 float ops, matching the author's report. Split into 8 hot pieces.
  • A/B on the same exe and save: default fast path vs HOS_NO_FASTFP=1 (exact copies only), headless, 240 s, static Market (!cheat 1003) after the save loads; the same fixed save is restored before each run.

Result (static Market, CPU-heavy)

fast (default) exact (HOS_NO_FASTFP=1)
fps, samples after 6000 presents 56.66 avg, 48.3–60.1, n=50 54.11 avg, 46.0–59.1, n=48
last samples 52.1–58.9 50.0–52.4
HOS_PROFILE all ~480% of a core ~470%

The fast path is +2.55 fps on average (~4.7%) in this scene, and the last part of the run is consistently higher (52–59 vs 50–52). This is the same direction as the author's Temple of Time result (41.5 -> 45.7 fps) and the Havok AI character step. The Kakariko route with two teleports was noisy on this machine and is not used for the A/B; the author already notes that Kokiri/Kakariko frame time is mostly job-chain waits, so no claim there.

Checks

  • tests/test_fastfp_regions.py: pass, including the real-header x86-64-v3 and x86-64-v2 runs.
  • tests/test_fastfp_fma.py could not be built locally: a64emit failed with 'algorithm' file not found in this PC's MSVC-target clang setup, the same local toolchain issue as test_fpx_sticky.py. That is not the PR's code; the author reports the FMA test with 0 mismatches and the 2^-126/NaN/subnormal fixes in 1e6b7a2f.
  • The latest head includes the FMA correctness guard and the test; my test build uses that head, not the earlier e8cd68b8.

Caveats

  • My base is 4a53c897 + zstd/BC7, not the PR's latest main; the PR's own base is fce53453. The fast path's effect can differ with the newer lift/main.
  • One static scene, one pair, fast run first; the machine has background load. The +2.55 fps is a small but consistent difference in the measured window, not a general claim for every scene.
  • The exe grows by ~56 MB here (the author reports 470 -> 512 MB on their build); the fast path is on by default and HOS_NO_FASTFP/A64_FASTFP=0 turn it off.
  • No game screenshots or machine-specific files were uploaded; the report is text only.

DevL0rd added a commit to DevL0rd/Hookshot that referenced this pull request Nov 12, 2026

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is careful work, and the fix-up after @Jiro131's report is solid. I went through the fast path's semantics. One thing still needs checking before I'd approve: how the regions interact with the lifted-text edits in nx2/oot_patches.py.

What I checked and agree with

  • The MXCSR window looks right: exceptions masked, round to nearest, DAZ off, FTZ only under FPCR.FZ. The gate is the existing RECOMP_FPX_OPEN, and an MSR (or an MRS of FPSR) ends a run, so FPCR can't change inside a region.
  • The exact helpers that stay inside a fast copy (fp64 ops, compares, conversions) never read or write MXCSR. The window's only difference from the host contract is FTZ, and FTZ only changes a result when it raises UE. So anything the window changes sends the run back to its exact copy. At worst that costs a fallback; it never gives a wrong result.
  • Jiro's cases are closed in the code: the NaN and 2^-126 guards on mul, div and fma (per lane for mulps), the subnormal over zero divide pre-check, and the 2-lane ops loading zeros into the upper lanes. comiss is the right choice for the spoil, since it signals on a quiet NaN. Sums can't round to 2^-126, so add and sub without the tiny check is fine.
  • The restore is complete. SP is c->x[31], so REG_REF covers it. NZCV and FPSR are saved and restored, and A64_LOCAL_REGS/A64_LOCAL_FLAGS rename the save/restore along with the body. Labels, stores, calls, goto/return and sync marks all end a run, so nothing can jump into the middle of one.
  • CPU coverage: the header uses only SSE2 intrinsics. The VEX forms sit under __AVX__ and vfmadd231ss under __FMA__, and both follow build.py's host_march()/RX_MARCH. So a no-AVX2 PC built at x86-64-v2 or x86-64 gets the legacy encodings and the exact FMA helper. I found no AVX2-only path.
  • Generated code: with A64_FASTFP=0 the emitter's output matches main (no joins, no header). With it on, a run under A64_FASTFP_MIN is joined back exactly as before, so functions without a region are byte-identical.
  • @Joey-Magnus's m.lib point is fixed: both new tests pass on the Linux and Windows runners. The remaining red checks come from main, not this PR: test_env_on.py flags HOS_FPS_LOG in host/oot_high_fps.c, plus test_psychov35.py and test_pcache_header_read.py.

Needs a check: regions vs. nx2/oot_patches.py
The patches edit the finished C by matching the lift's exact text, and a region changes that text in two ways. It duplicates the text (fast copy first, exact copy second), and it wraps the run in the region block, so text that spans a run's edge no longer matches. Most patches then miss and only warn, but patch_frame_seconds would succeed silently on the wrong copy. It requires exactly one a64_fpx_div32_div32 in the function. If the step divide sat inside a region, the fast copy would hold a64_ff_div32_div32, the count would still be 1, and the hook would go into the exact copy only. The fast copy (97–99% of runs) would skip it, and above a 60 fps cap the game would run fast again. Every timing in the description is below 60 fps, where the hook does nothing, so none of them would show this.
I can't tell from here whether a region forms at those sites. Could you check your lift: no new oot_patches warnings, and none of the patched places inside an L_ff_ block? A cheap guard would also help: end a run at those functions, or have the frame-seconds patch refuse a body that contains a64_ff_div32_div32. That keeps a future lift change from breaking it quietly.

Default on: given the check-mode coverage across eight saves, the per-op bit-exact tests and HOS_NO_FASTFP to turn it off at run time, I'm fine with on by default once the patch-site check is answered.

One nit inline.

#if RECOMP_FPX_HOST == 2 && defined(__x86_64__) && !defined(__EMSCRIPTEN__)
#include <stdlib.h>
#include <stdio.h>
static int a64_ff_enabled_(void) { static int v = -1; if (v < 0) v = !getenv("HOS_NO_FASTFP"); return v; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this reads the switch the old way, so HOS_NO_FASTFP=0 turns the fast path off. #2042 moved the project to env_on semantics (unset, empty, 0, false, off and no all mean off). test_env_on.py only scans host/ and gpu/vk, so it won't catch this one. A small inline equivalent here would keep it consistent. A64_FASTFP at lift time likewise only treats the literal 0 as off.

sheepbun added a commit to sheepbun/Bunny-Hood that referenced this pull request Nov 12, 2026
…the state files

The ledger records the sync at upstream's 3e32949 (pull request 3), upstream's
own CPU profile (NotRexed#2424) and the open pull requests found since: NotRexed#2470 (sound and
the physics step above 60), NotRexed#2325 (the frame's chain of waits), NotRexed#2433 (fast float
regions), NotRexed#2487 (wake-ups on Linux). correctness.json takes the water ripples as
fixed upstream (NotRexed#2409), the Deku Tree platform's cause as reported in NotRexed#2470, and a
new entry for the sound loss.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
sheepbun added a commit to sheepbun/Bunny-Hood that referenced this pull request Nov 12, 2026
…the state files

The ledger records the sync at upstream's 3e32949 (pull request 3), upstream's
own CPU profile (NotRexed#2424) and the open pull requests found since: NotRexed#2470 (sound and
the physics step above 60), NotRexed#2325 (the frame's chain of waits), NotRexed#2433 (fast float
regions), NotRexed#2487 (wake-ups on Linux). correctness.json takes the water ripples as
fixed upstream (NotRexed#2409), the Deku Tree platform's cause as reported in NotRexed#2470, and a
new entry for the sound loss.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Jiro131

Jiro131 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

A correction to the frame-time table in my comment above (section 4, "Frame times").

In session 2, the "this PR + FMA" game folder had lost its nvngx_dlss.dll when I installed that build. Its two runs went without DLSS: the log says NVIDIA DLSS: not available: no nvngx_dlss.dll. The baseline in the same session started NGX. Session 1 is not affected: both sides started NGX.

Both sides of session 2 show a GPU-bound wait of 0.0 ms at about 2,500 draws, so the game was CPU-bound either way, and the missing DLSS probably changed little. Still, those two pairs are not like for like. Please read only session 1 as an A/B. The conclusion does not change: no measurable fps gain while standing still in Kokiri Forest, which matches what you see there.

Sections 1 to 3 (the profile, the multiply-add commit and its tests, the FMUL cases) are not affected.

Written with Claude Code (Claude Opus 5.5).

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My earlier points are settled: libm links only off Windows, the ASTC failure was main's, and the check-mode runs plus tests/test_fastfp_fma.py persuade me on default-on. I merged it into current main (including #2748's parallel emission) and ran test_fastfp_fma.py, test_fastfp_regions.py, test_fpx_sticky.py, test_mark_groups.py, test_spawn_emit.py and test_stale_hot_pieces.py: all pass.

Still open is hckrman101's question on nx2/oot_patches.py. patch_frame_seconds (:352) only checks that a64_fpx_div32_div32 appears once in f_80706be0 or f_83b6ea90. If that divide is inside a region, the fast copy calls a64_ff_div32_div32, the count is still 1, and the hook goes into the exact copy only. Above a 60 fps cap the game would run fast again in the 97-99% of runs that keep the fast copy, and none of your timings are above 60. Please check those two functions in your lift and add a guard: end a run there, or have the patch refuse a body with a64_ff_div32_div32.

The nit stands too: HOS_NO_FASTFP is read with !getenv (a64_lift.py:776), so HOS_NO_FASTFP=0 turns the fast path off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants