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

Draft
dyst wants to merge 4 commits from dyst/Longshot:perf/hot-paths into main
Member

Why

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 that the lifted code 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.

What changed

  • lift: ff_eligible and ff_region in third_party/a64-kit/lift/a64_lift.py: a straight run of instructions with no label, store, call, branch or system-register access, holding at least 8 (A64_FASTFP_MIN) single-precision adds, subtracts, multiplies, fused multiply-adds, divides or square roots, is emitted twice: a 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 (a64_ff_begin, a64_ff_end); and the exact copy, the lifted code as before. On an invalid, denormal or underflow flag the registers the run names are put back and the exact copy runs. Divide-by-zero, overflow and inexact are or-ed into FPSR.
  • lift: Lifter's function emitter gathers joinable instructions into a run (flush), counts ff_regions and ff_ops, and prints a fast float regions (A64_FASTFP=...) line; FASTFP_HEADER is written beside the fpx header when the mode is on.
  • lift: FASTFP_HEADER: A64_FF_ON gates on the existing RECOMP_FPX_OPEN (FPCR at its defaults apart from FZ, FPSR.IXC set) and on a64_ff_enabled_ (HOS_NO_FASTFP). a64_ff_guard_ raises the invalid flag on purpose for results x86 gets right in value but not in Arm's flags, or not in value under FZ: any NaN result; a product, quotient or multiply-add of exactly 2^-126; a subnormal divided by a zero. The 2-lane vector ops load their upper lanes as zeros. Without FMA3 a multiply-add calls the exact helper inside the fast copy; non-x86 builds compile the exact copy only.
  • lift: fastfp_mode: A64_FASTFP=0 off; hot the A64_HOT_FUNCS list only (ff_only); check both copies every time, compared (a64_ff_check, fastfp check: lines in the log with counts per chunk and any mismatch's guest address).
  • tests: tests/test_fastfp_fma.py with tests/fastfp_fma_test.c (new, @Jiro131's, extended) and tests/test_fastfp_regions.py (new).

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_NO_FASTFP Any value: the exact copies only, to compare on the same exe unset (fast copies on) off by default (a developer switch) - - -
A64_FASTFP 0 off; hot the hot functions only; check both copies every time, compared, with fastfp check: lines in the log on (1, every function) on by default (this PR turns it on) Ryzen 9 3900X, Windows: Temple of Time 41.5 → 45.7 fps; Havok AI's character step 26% less CPU there, 16% less in Hyrule Town Hyrule Castle, Ryzen 9 3900X: no change in fps (30.9 → 30.8) Intel, Linux, CPUs without AVX2 (the SSE path has run only in the unit test)
A64_FASTFP_MIN The fewest float ops a run needs to be made a region 8 off by default (a developer switch) - - -

Measurements

Measured on the GitHub version (https://github.com/NotRexed/Hookshot/pull/2433), not yet on Longshot: this PR stays WIP until it is.

Measured on ebbba2ea, before the rebase and before the result checks of 1e6b7a2f, which add a compare per op.

Hardware: Ryzen 9 3900X, Windows
Scene: Temple of Time; Hyrule Town; Hyrule Castle
Method: alternating runs in the same session (this PC's run-to-run noise is about ±8%); Hyrule Town and Hyrule Castle two alternating pairs each, base against this PR

GitHub main this PR (GitHub) @ ebbba2ea
Temple of Time, fps (regions without multiply-adds) 41.5 45.7
Temple of Time, Havok AI's character step, ms of CPU a frame 5.6 4.1
Hyrule Castle, fps 30.9 30.8
Hyrule Town, Havok AI's character step, ms of CPU a frame 9.4 7.9
Hyrule Town, the BaseProc executors, ms (two pairs) 118, 121 114, 88
the exe 470 MB 512 MB
  • Temple of Time with multiply-adds in the regions, against the build without them: 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 Castle: the pairs differ by 3 fps between themselves. Hyrule 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, and the BaseProc executors don't rise.
  • 17,068 regions, 315,994 float ops across the game (47,371 of them multiply-adds; 13,114 regions without them).
  • The fast copy is kept in 97–99% of runs in each scene, at least 95% in every file.
  • 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.

Testing

Tested: the runs under Measurements (on the GitHub version) and the game-free checks this description names. Checked but not measured: check mode in eight saves (above), for correctness rather than speed.

Not tested: Intel, Linux and CPUs without AVX2 (the SSE path has run only in the unit test so far). Reports from them are welcome; HOS_NO_FASTFP turns it off on the same exe to compare.

Needs other testers:

  • Intel CPU (Windows): Kokiri Forest with and without HOS_NO_FASTFP, expecting the same picture, no crash, and its fps (request to open)
  • A CPU without AVX2: the game starts and plays in Kokiri Forest (request to open)
  • Linux: the same as on Windows, with and without HOS_NO_FASTFP (request to open)

Screenshots

Nothing on screen.

Risk and rollback

  • A float result the fast copy gets wrong where the guards don't catch it would change game logic (physics, AI, Link's movement and collision); the result checks and check mode are there to find one.
  • The exe grows 470 → 512 MB, since both copies are kept.
  • HOS_NO_FASTFP turns the fast copies off at run time on the same exe; A64_FASTFP=0 lifts without them; reverting the commit is enough.

Reviewer notes

The switch reference (docs/development.md, the README) isn't updated here: this PR carries code and tests, and the flags are noted under Flags and settings.

Carried over from https://github.com/NotRexed/Hookshot/pull/2433 with the move to Longshot: the same change on Longshot's main, with its rename applied. Its review and discussion are there (reviewed by Joey-Magnus).

  • The checks and the game runs above were done on the GitHub version; this branch is the same change with the rename applied (the same files and line counts). Forgejo runs no checks, and they were not run again here.
  • The change is in third_party/a64-kit/lift/a64_lift.py (the lifter), so the third_party/ box is not ticked.
  • Since the draft: fused multiply-adds join regions (9f060614, @Jiro131's): they are about half the float ops in Link's movement and collision code, and in Town and Castle one hot file had fallen 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 (1e6b7a2f): the three cases above, found by @Jiro131's tests/test_fastfp_fma.py and by the same test once the divide was added to it; check mode never met them in game. Rebased onto GitHub's main, including https://github.com/NotRexed/Hookshot/pull/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.
  • CI on GitHub: Windows part 1 fails on tests/test_psychov35.py, from https://github.com/NotRexed/Hookshot/pull/2481 on main (scanf is deprecated under -Werror with the runner's MSVC-target clang), not from this PR.
  • tests/test_fastfp_fma.py: 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, with 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. It links libm only off Windows.
  • How to help test: build this branch and play (anything that behaves differently from main is worth a report); or build with A64_FASTFP=check, play anywhere, and look in longshot_log.txt for fastfp check: MISMATCH (there should be none); or compare HOS_THREAD_TIMES=5 with and without HOS_NO_FASTFP on the same exe.

third_party/: this diff changes files under third_party/, which Longshot's rules keep as unmodified upstream copies (a new folder with a PROVENANCE.md instead). That has to be resolved before this can be ready.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.

🤖 Generated with Claude Code

## Why 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 that the lifted code 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. ## What changed - lift: `ff_eligible` and `ff_region` in `third_party/a64-kit/lift/a64_lift.py`: a straight run of instructions with no label, store, call, branch or system-register access, holding at least 8 (`A64_FASTFP_MIN`) single-precision adds, subtracts, multiplies, fused multiply-adds, divides or square roots, is emitted twice: a 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 (`a64_ff_begin`, `a64_ff_end`); and the exact copy, the lifted code as before. On an invalid, denormal or underflow flag the registers the run names are put back and the exact copy runs. Divide-by-zero, overflow and inexact are or-ed into FPSR. - lift: `Lifter`'s function emitter gathers joinable instructions into a run (`flush`), counts `ff_regions` and `ff_ops`, and prints a `fast float regions (A64_FASTFP=...)` line; `FASTFP_HEADER` is written beside the fpx header when the mode is on. - lift: `FASTFP_HEADER`: `A64_FF_ON` gates on the existing `RECOMP_FPX_OPEN` (FPCR at its defaults apart from FZ, FPSR.IXC set) and on `a64_ff_enabled_` (`HOS_NO_FASTFP`). `a64_ff_guard_` raises the invalid flag on purpose for results x86 gets right in value but not in Arm's flags, or not in value under FZ: any NaN result; a product, quotient or multiply-add of exactly 2^-126; a subnormal divided by a zero. The 2-lane vector ops load their upper lanes as zeros. Without FMA3 a multiply-add calls the exact helper inside the fast copy; non-x86 builds compile the exact copy only. - lift: `fastfp_mode`: `A64_FASTFP=0` off; `hot` the `A64_HOT_FUNCS` list only (`ff_only`); `check` both copies every time, compared (`a64_ff_check`, `fastfp check:` lines in the log with counts per chunk and any mismatch's guest address). - tests: `tests/test_fastfp_fma.py` with `tests/fastfp_fma_test.c` (new, @Jiro131's, extended) and `tests/test_fastfp_regions.py` (new). ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_NO_FASTFP` | Any value: the exact copies only, to compare on the same exe | unset (fast copies on) | off by default (a developer switch) | - | - | - | | `A64_FASTFP` | `0` off; `hot` the hot functions only; `check` both copies every time, compared, with `fastfp check:` lines in the log | on (`1`, every function) | on by default (this PR turns it on) | Ryzen 9 3900X, Windows: Temple of Time 41.5 → 45.7 fps; Havok AI's character step 26% less CPU there, 16% less in Hyrule Town | Hyrule Castle, Ryzen 9 3900X: no change in fps (30.9 → 30.8) | Intel, Linux, CPUs without AVX2 (the SSE path has run only in the unit test) | | `A64_FASTFP_MIN` | The fewest float ops a run needs to be made a region | 8 | off by default (a developer switch) | - | - | - | ## Measurements Measured on the GitHub version (https://github.com/NotRexed/Hookshot/pull/2433), not yet on Longshot: this PR stays WIP until it is. Measured on `ebbba2ea`, before the rebase and before the result checks of 1e6b7a2f, which add a compare per op. Hardware: Ryzen 9 3900X, Windows Scene: Temple of Time; Hyrule Town; Hyrule Castle Method: alternating runs in the same session (this PC's run-to-run noise is about ±8%); Hyrule Town and Hyrule Castle two alternating pairs each, base against this PR | | GitHub main | this PR (GitHub) @ ebbba2ea | |---|---|---| | Temple of Time, fps (regions without multiply-adds) | 41.5 | 45.7 | | Temple of Time, Havok AI's character step, ms of CPU a frame | 5.6 | 4.1 | | Hyrule Castle, fps | 30.9 | 30.8 | | Hyrule Town, Havok AI's character step, ms of CPU a frame | 9.4 | 7.9 | | Hyrule Town, the BaseProc executors, ms (two pairs) | 118, 121 | 114, 88 | | the exe | 470 MB | 512 MB | - Temple of Time with multiply-adds in the regions, against the build without them: 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 Castle: the pairs differ by 3 fps between themselves. Hyrule 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, and the BaseProc executors don't rise. - 17,068 regions, 315,994 float ops across the game (47,371 of them multiply-adds; 13,114 regions without them). - The fast copy is kept in 97–99% of runs in each scene, at least 95% in every file. - 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. ## Testing **Tested:** the runs under Measurements (on the GitHub version) and the game-free checks this description names. Checked but not measured: check mode in eight saves (above), for correctness rather than speed. **Not tested:** Intel, Linux and CPUs without AVX2 (the SSE path has run only in the unit test so far). Reports from them are welcome; `HOS_NO_FASTFP` turns it off on the same exe to compare. **Needs other testers:** - Intel CPU (Windows): Kokiri Forest with and without `HOS_NO_FASTFP`, expecting the same picture, no crash, and its fps (request to open) - A CPU without AVX2: the game starts and plays in Kokiri Forest (request to open) - Linux: the same as on Windows, with and without `HOS_NO_FASTFP` (request to open) ## Screenshots Nothing on screen. ## Risk and rollback - A float result the fast copy gets wrong where the guards don't catch it would change game logic (physics, AI, Link's movement and collision); the result checks and check mode are there to find one. - The exe grows 470 → 512 MB, since both copies are kept. - `HOS_NO_FASTFP` turns the fast copies off at run time on the same exe; `A64_FASTFP=0` lifts without them; reverting the commit is enough. ## Reviewer notes The switch reference (docs/development.md, the README) isn't updated here: this PR carries code and tests, and the flags are noted under Flags and settings. Carried over from https://github.com/NotRexed/Hookshot/pull/2433 with the move to Longshot: the same change on Longshot's `main`, with its rename applied. Its review and discussion are there (reviewed by Joey-Magnus). - The checks and the game runs above were done on the GitHub version; this branch is the same change with the rename applied (the same files and line counts). Forgejo runs no checks, and they were not run again here. - The change is in `third_party/a64-kit/lift/a64_lift.py` (the lifter), so the `third_party/` box is not ticked. - Since the draft: fused multiply-adds join regions (9f060614, @Jiro131's): they are about half the float ops in Link's movement and collision code, and in Town and Castle one hot file had fallen 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 (1e6b7a2f): the three cases above, found by @Jiro131's `tests/test_fastfp_fma.py` and by the same test once the divide was added to it; check mode never met them in game. Rebased onto GitHub's main, including https://github.com/NotRexed/Hookshot/pull/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`. - CI on GitHub: Windows part 1 fails on `tests/test_psychov35.py`, from https://github.com/NotRexed/Hookshot/pull/2481 on main (`scanf` is deprecated under `-Werror` with the runner's MSVC-target clang), not from this PR. - `tests/test_fastfp_fma.py`: 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, with 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. It links libm only off Windows. - How to help test: build this branch and play (anything that behaves differently from `main` is worth a report); or build with `A64_FASTFP=check`, play anywhere, and look in `longshot_log.txt` for `fastfp check: MISMATCH` (there should be none); or compare `HOS_THREAD_TIMES=5` with and without `HOS_NO_FASTFP` on the same exe. **`third_party/`:** this diff changes files under `third_party/`, which Longshot's rules keep as unmodified upstream copies (a new folder with a `PROVENANCE.md` instead). That has to be resolved before this can be ready. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [ ] Rebased on current `main`. - [ ] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [ ] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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) <[email protected]>
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) <[email protected]>
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 <[email protected]>
fix(lift): fast float ops keep only results Arm gives the same
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
d3590ecd58
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) <[email protected]>
dyst changed title from perf(lift): fast float regions, plain SSE checked once by MXCSR with the exact copy as fallback to WIP: perf(lift): fast float regions, plain SSE checked once by MXCSR with the exact copy as fallback 2026-11-13 01:14:02 +00:00
Author
Member

Back to WIP under #48's process: this was carried over before the new template. Its description will be brought to the template and measured on Longshot (main against this PR, at named commits) when it's wanted for review; ask here if you'd like it sooner.

Back to WIP under #48's process: this was carried over before the new template. Its description will be brought to the template and measured on Longshot (main against this PR, at named commits) when it's wanted for review; ask here if you'd like it sooner.
Author
Member

Description brought to Longshot's PR template (#48). Still WIP: its numbers are from the GitHub version, and it isn't rebased on current main; it gets measured on Longshot (main against this PR, at named commits) before it is marked ready. Note: this diff changes files under third_party/, which Longshot keeps as unmodified upstream copies; that needs resolving before it can be ready.

Description brought to Longshot's PR template (#48). Still WIP: its numbers are from the GitHub version, and it isn't rebased on current main; it gets measured on Longshot (main against this PR, at named commits) before it is marked ready. Note: this diff changes files under third_party/, which Longshot keeps as unmodified upstream copies; that needs resolving before it can be ready.
Author
Member

Description moved to the template's Testing section and whole checklist (#72): what was tested, what wasn't, and what needs other testers (their test requests to open while this is WIP).

Description moved to the template's Testing section and whole checklist (#72): what was tested, what wasn't, and what needs other testers (their test requests to open while this is WIP).
Author
Member

The description follows the template after #76 and #106: If applicable keeps only the boxes that apply (one that applies and isn't done yet stays unticked while this is WIP), the Flags table has the template's seven columns (Goal is now Recommendation), and GitHub references are full URLs. Nothing else changed.

The description follows the template after #76 and #106: If applicable keeps only the boxes that apply (one that applies and isn't done yet stays unticked while this is WIP), the Flags table has the template's seven columns (Goal is now Recommendation), and GitHub references are full URLs. Nothing else changed.
mooki left a comment

Read head d3590ecd against main cf605c85: all four changed files, the exact arithmetic and emission callers, all four Forgejo comments and the empty review list, plus the retained GitHub discussion; ran both new tests with GCC and Clang, and the existing lift-cache, mark-group, stale-hot-piece and spawned-emitter checks. The new tests and default checks pass; the spawned-emitter check fails with A64_FASTFP=hot A64_FASTFP_MIN=1 under both compilers. No game, full build or full suite was run.

What's good

  • The cost exists in main: each outlined arithmetic call checks FPCR, FPSR and the result before retaining native arithmetic. Sharing those checks across a run is a concrete optimization, not merely a different compiler setting.
  • The added guards address the earlier reported NaN, smallest-normal and subnormal-divided-by-zero cases. The isolated arithmetic tests compare values and FPSR against the real integer implementations, not another native approximation.
  • Ordinary region exits restore the old MXCSR. Failed runs restore the named integer registers, vector registers, NZCV and FPSR before retrying. Labels, branches, stores and the existing after patches end regions.

Must change

  1. Do not publish exceptions from an abandoned native probe. In third_party/a64-kit/lift/a64_lift.py, lines 730-733, every a64_fpx_* wrapper may join a region, including operations that are not replaced. Those wrappers can try native arithmetic, reject it, and return a correct software result while leaving host exception flags behind. A concrete case is FRSQRTS on an FMA3 build, with FPCR 0, FPSR 0x10, and operands 0x5f800000 and 0xdf800000, or positive and negative 2^64. Its existing native probe computes fmaf(-a, b, 3) * 0.5, overflowing before the halve; the integer fallback computes (3 - a*b)/2 and returns the finite 0x7f000000 without OFC. Inside a region with otherwise ordinary operations, lines 791-792 accept that run and add the probe's overflow to FPSR. This is a source-traced counterexample, not a game reproduction. End regions before these unconverted wrappers, or keep their discarded host exceptions out of the region's result. A mixed-operation test using the real FRSQRTS wrapper and comparing FPSR would settle it. Check mode must compare FPSR too: lines 756-762 currently omit it.

  2. Keep the frame-time hooks on every executed copy. The duplication at third_party/a64-kit/lift/a64_lift.py, lines 739-743, does not account for the later nx2/oot_patches.py edits. With the supported A64_FASTFP_MIN=1, each frame-step divide can become a region. patch_frame_seconds, lines 379-402, still finds exactly one a64_fpx_div32_div32, so it patches only the fallback; the accepted a64_ff_div32_div32 copy skips the measured frame time and the frame-hook probe. This bypasses the real-time stepping correction above a 60 fps cap. The two divide runs in the retained baseline each contain only one qualifying operation, so this is not evidence of that failure with the default minimum of 8. Fence these sites from regions or apply their edits before duplication, and test the composed emission and patching at both minima. This makes the older review's patch-site concern concrete for the exposed minimum setting.

  3. Send the hot-function selection to spawned workers. third_party/a64-kit/lift/a64_lift.py, lines 1856-1857, treats a missing ff_only as permission to optimize every function, but the new value set at line 2024 is absent from nx2/chunks.py's EMIT_TABLES. Windows-style workers therefore ignore A64_FASTFP=hot, while serial and forked emission honor it. The existing check demonstrates different code_000.c output with A64_FASTFP=hot A64_FASTFP_MIN=1 python -I -B tests/test_spawn_emit.py --cxx clang++; the same check fails with g++. The lower minimum lets its one-add fixture exercise a region; the missing selection also affects longer regions at the default minimum. Carry ff_only into the workers and add a hot-list case to this test.

  4. Provide the new header to direct emitter callers. third_party/a64-kit/lift/a64_lift.py, lines 2003-2004, appends FASTFP_HEADER only in Lifter.write(). nx2/test_leaf.py, lines 147 and 319-327, calls emit_function() directly and writes only fpx_header(). Once a selected function contains a qualifying region, its C references undefined A64_FF_ON, a64_ff_begin, a64_ff_end and arithmetic helpers, so the whole-function comparison cannot compile. Share header generation with that caller rather than switching the optimization off in the correctness test. A game-free emitted function compiled through the same header path would settle it.

Exact arithmetic and modes

The exact add/subtract uses integer significands with guard and sticky bits; multiply/FMA uses an integer product-and-addend lattice; divide uses integer quotient and remainder; square root uses a restoring integer root and remainder. They perform guest rounding and set guest flags independently of MXCSR.

For the replaced single-precision operations, the intended differences are handled as follows:

  • Add/subtract: native nearest-even arithmetic, with NaN results, denormal operands and flushed tiny results sent back to the exact copy.
  • Multiply, divide and FMA: the same, plus the smallest-normal result guard for Arm's before-rounding tininess rule. Divide also checks subnormal divided by zero. The emitter applies the FMA variants' sign changes before the fused operation, and FNMUL's negation after multiplication.
  • Square root: signed zero and positive infinity agree; a quiet NaN has the same single-operand payload, while a signalling NaN, a negative nonzero input or a denormal input triggers fallback. A positive finite square root cannot underflow in this format.
  • Nondefault guest rounding, default-NaN mode, other excluded FPCR controls, the native-FP inhibit bit, or clear FPSR.IXC close the gate. FZ alone remains supported. Without FMA3, FMA retains the existing checked wrapper; non-x86 builds retain the original path.

The passing isolated tests do not establish the mixed-wrapper flag behavior in point 1, nor the downstream integration in points 2-4.

Smaller points and measurements

  • This head is 117 main commits behind, with four commits of its own, but merges cleanly with main cf605c85. Main still uses the same relevant arithmetic and worker-state transfer. The newer entry-point fix is separate. A missing rebase is not the reason for requesting changes.
  • The displayed performance results predate the final per-operation guards and do not establish the gain at this head. The main commit, complete per-run table, medians and repeat spreads are missing. The archived Intel report also explicitly uses one pair on a different fork with background load. Keep the current gain unmeasured until a named-main/current-head comparison has at least two interleaved runs each on the same PC and settings.
  • There are no shared changed files with #357, #358, #314, #360, #22, #24, #28 or #35. The worker change in 357 shares the pipeline involved in point 3; it does not repair the missing selection. The key changes in 22 and 28 still include A64_* controls. None replaces this arithmetic change.
  • The third-party provenance issue and missing switch documentation are already acknowledged in the description. The older switch-parsing comment also remains: HOS_NO_FASTFP=0 still disables the optimization.

Not checked

No Windows execution, non-x86 compilation, actual older CPU, fresh game translation, gameplay above 60 fps or current-head performance measurement here. The arithmetic counterexample, minimum-setting hook bypass and direct-caller header failure are source proofs, not claimed executed regressions. The original GitHub endpoints now return 404; its discussion was read from the retained snapshot through November 12, including the two reviews and inline comment. The present Forgejo thread was read directly.

Read head `d3590ecd` against main `cf605c85`: all four changed files, the exact arithmetic and emission callers, all four Forgejo comments and the empty review list, plus the retained GitHub discussion; ran both new tests with GCC and Clang, and the existing lift-cache, mark-group, stale-hot-piece and spawned-emitter checks. The new tests and default checks pass; the spawned-emitter check fails with `A64_FASTFP=hot A64_FASTFP_MIN=1` under both compilers. No game, full build or full suite was run. ### What's good - The cost exists in main: each outlined arithmetic call checks FPCR, FPSR and the result before retaining native arithmetic. Sharing those checks across a run is a concrete optimization, not merely a different compiler setting. - The added guards address the earlier reported NaN, smallest-normal and subnormal-divided-by-zero cases. The isolated arithmetic tests compare values and FPSR against the real integer implementations, not another native approximation. - Ordinary region exits restore the old MXCSR. Failed runs restore the named integer registers, vector registers, NZCV and FPSR before retrying. Labels, branches, stores and the existing `after` patches end regions. ### Must change 1. **Do not publish exceptions from an abandoned native probe.** In `third_party/a64-kit/lift/a64_lift.py`, lines 730-733, every `a64_fpx_*` wrapper may join a region, including operations that are not replaced. Those wrappers can try native arithmetic, reject it, and return a correct software result while leaving host exception flags behind. A concrete case is FRSQRTS on an FMA3 build, with FPCR 0, FPSR `0x10`, and operands `0x5f800000` and `0xdf800000`, or positive and negative 2^64. Its existing native probe computes `fmaf(-a, b, 3) * 0.5`, overflowing before the halve; the integer fallback computes `(3 - a*b)/2` and returns the finite `0x7f000000` without OFC. Inside a region with otherwise ordinary operations, lines 791-792 accept that run and add the probe's overflow to FPSR. This is a source-traced counterexample, not a game reproduction. End regions before these unconverted wrappers, or keep their discarded host exceptions out of the region's result. A mixed-operation test using the real FRSQRTS wrapper and comparing FPSR would settle it. Check mode must compare FPSR too: lines 756-762 currently omit it. 2. **Keep the frame-time hooks on every executed copy.** The duplication at `third_party/a64-kit/lift/a64_lift.py`, lines 739-743, does not account for the later `nx2/oot_patches.py` edits. With the supported `A64_FASTFP_MIN=1`, each frame-step divide can become a region. `patch_frame_seconds`, lines 379-402, still finds exactly one `a64_fpx_div32_div32`, so it patches only the fallback; the accepted `a64_ff_div32_div32` copy skips the measured frame time and the frame-hook probe. This bypasses the real-time stepping correction above a 60 fps cap. The two divide runs in the retained baseline each contain only one qualifying operation, so this is not evidence of that failure with the default minimum of 8. Fence these sites from regions or apply their edits before duplication, and test the composed emission and patching at both minima. This makes the older review's patch-site concern concrete for the exposed minimum setting. 3. **Send the hot-function selection to spawned workers.** `third_party/a64-kit/lift/a64_lift.py`, lines 1856-1857, treats a missing `ff_only` as permission to optimize every function, but the new value set at line 2024 is absent from `nx2/chunks.py`'s `EMIT_TABLES`. Windows-style workers therefore ignore `A64_FASTFP=hot`, while serial and forked emission honor it. The existing check demonstrates different `code_000.c` output with `A64_FASTFP=hot A64_FASTFP_MIN=1 python -I -B tests/test_spawn_emit.py --cxx clang++`; the same check fails with `g++`. The lower minimum lets its one-add fixture exercise a region; the missing selection also affects longer regions at the default minimum. Carry `ff_only` into the workers and add a hot-list case to this test. 4. **Provide the new header to direct emitter callers.** `third_party/a64-kit/lift/a64_lift.py`, lines 2003-2004, appends `FASTFP_HEADER` only in `Lifter.write()`. `nx2/test_leaf.py`, lines 147 and 319-327, calls `emit_function()` directly and writes only `fpx_header()`. Once a selected function contains a qualifying region, its C references undefined `A64_FF_ON`, `a64_ff_begin`, `a64_ff_end` and arithmetic helpers, so the whole-function comparison cannot compile. Share header generation with that caller rather than switching the optimization off in the correctness test. A game-free emitted function compiled through the same header path would settle it. ### Exact arithmetic and modes The exact add/subtract uses integer significands with guard and sticky bits; multiply/FMA uses an integer product-and-addend lattice; divide uses integer quotient and remainder; square root uses a restoring integer root and remainder. They perform guest rounding and set guest flags independently of MXCSR. For the replaced single-precision operations, the intended differences are handled as follows: - Add/subtract: native nearest-even arithmetic, with NaN results, denormal operands and flushed tiny results sent back to the exact copy. - Multiply, divide and FMA: the same, plus the smallest-normal result guard for Arm's before-rounding tininess rule. Divide also checks subnormal divided by zero. The emitter applies the FMA variants' sign changes before the fused operation, and FNMUL's negation after multiplication. - Square root: signed zero and positive infinity agree; a quiet NaN has the same single-operand payload, while a signalling NaN, a negative nonzero input or a denormal input triggers fallback. A positive finite square root cannot underflow in this format. - Nondefault guest rounding, default-NaN mode, other excluded FPCR controls, the native-FP inhibit bit, or clear FPSR.IXC close the gate. FZ alone remains supported. Without FMA3, FMA retains the existing checked wrapper; non-x86 builds retain the original path. The passing isolated tests do not establish the mixed-wrapper flag behavior in point 1, nor the downstream integration in points 2-4. ### Smaller points and measurements - This head is 117 main commits behind, with four commits of its own, but merges cleanly with main `cf605c85`. Main still uses the same relevant arithmetic and worker-state transfer. The newer entry-point fix is separate. A missing rebase is not the reason for requesting changes. - The displayed performance results predate the final per-operation guards and do not establish the gain at this head. The main commit, complete per-run table, medians and repeat spreads are missing. The archived Intel report also explicitly uses one pair on a different fork with background load. Keep the current gain unmeasured until a named-main/current-head comparison has at least two interleaved runs each on the same PC and settings. - There are no shared changed files with https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/357, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/358, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/314, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/360, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/22, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/24, https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/28 or https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/35. The worker change in 357 shares the pipeline involved in point 3; it does not repair the missing selection. The key changes in 22 and 28 still include `A64_*` controls. None replaces this arithmetic change. - The third-party provenance issue and missing switch documentation are already acknowledged in the description. The older switch-parsing comment also remains: `HOS_NO_FASTFP=0` still disables the optimization. ### Not checked No Windows execution, non-x86 compilation, actual older CPU, fresh game translation, gameplay above 60 fps or current-head performance measurement here. The arithmetic counterexample, minimum-setting hook bypass and direct-caller header failure are source proofs, not claimed executed regressions. The original GitHub endpoints now return 404; its discussion was read from the retained snapshot through November 12, including the two reviews and inline comment. The present Forgejo thread was read directly.
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/hot-paths:dyst-perf/hot-paths'
git switch 'dyst-perf/hot-paths'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!38
No description provided.