Skip to content

fix(shader): round v_fma_f64 once for subnormal operands and results - #1017

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
NahuelGenchi:fix/v-fma-f64-subnormals
Oct 9, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
NahuelGenchi:fix/v-fma-f64-subnormals

Conversation

@NahuelGenchi

Copy link
Copy Markdown
Contributor

What

v_fma_f64 sent any subnormal operand to a separate multiply and add, so it rounded twice. Results in the subnormal range were also rounded twice even with normal operands: once to 53 bits in the scaled domain and again by the power-of-two rescale. Both cases can differ from IEEE 754 fusedMultiplyAdd in the last bit. The second case was not in the technical debt entry.

Subnormal operands are now widened by 2^64 and go through the exact Boldo and Melquiond path with their exponent lowered by 64. That path now also keeps the sign of the residual between the 53-bit result and the exact sum. When the result lands exactly on a midpoint of the subnormal grid, it is moved one ulp towards the exact sum before the single final rounding. Normal results are rebuilt by setting the exponent, so only a subnormal result rounds. v_div_fmas_f64 shares this path, so its subnormal cases change too.

Source of the semantics is IEEE 754 fusedMultiplyAdd with f64 denormals kept, which the existing f64 path already assumes. I could not compare these cases with RDNA hardware (my GPU is GCN), so the technical debt entry now says that instead of describing the old gap.

Note: the TechnicalDebt.md hunk conflicts textually with #697 and #944, which add lines right above and below the old v_fma_f64 entry. The shader code does not overlap with any open PR. Whichever lands second only needs to keep both lines.

Tested

Linux (GCC 15), no title. New test agc_driver_f64_fma_subnormals with 32 vectors, expected values from glibc fma: 17 of them fail on main and all pass with this change. Full ctest with ANYPS5_REQUIRE_VULKAN=1 passes 370/370 on RADV (R9 380) and on lavapipe, same skips as main.

Outside the repo I modelled the emitted sequence in C++ and checked about 200M random and near-boundary inputs against std::fma, plus the scaled v_div_fmas_f64 path against exact rational arithmetic, with no mismatches. A throwaway GPU fuzz of 320k vectors per driver also had no mismatches; the old code failed 322 of 64k.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@adriadam10

Copy link
Copy Markdown
Collaborator

Heads-up: this PR is on hold, not rejected. It relies on how v_fma_f64 with subnormal operands behaves under the shader float mode (IEEE_MODE, DX10_CLAMP, the denormal bits of FLOAT_MODE in RSRC1). Real titles set that mode per shader, and the recompiler currently assumes a single one; see #981. Until that is settled, no float-mode dependent shader PR gets reviewed or merged. Nothing to do on your side for now. Once #981 lands, this will probably need tests for each mode value it relies on.

@SleglJan

SleglJan commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Hardware comparison for the subnormal cases, measured on RDNA2 (gfx1036, Ryzen 7950X3D iGPU) with the hardware oracle: v_fma_f64, v_mul_f64 and v_add_f64 over 512 rows (subnormal operands with normal and subnormal co-operands, results in the subnormal range, 117 rows constructed so that a separately rounded multiply then add differs from a single rounding, and seeded random rows), each in IEEE_MODE 0 and 1 with f16/f64 denormals kept and flushed.

  • Denormals kept: 0 mismatches against the exact result rounded once to f64 with gradual underflow, in both IEEE settings; on the 117 discriminating rows the hardware matches the single rounding in all 117 and the double rounding in none. That is the behaviour this PR implements.
  • Denormals flushed: 0 mismatches against the same model with subnormal operands flushed to signed zero before the operation and a subnormal result flushed after, in both IEEE settings.

Rows, results and the script: https://gist.github.com/SleglJan/30456e5687240f0b639a78e5477f2f91 (fma-f64-subnormal-matrix.json, fma_f64_subnormal.py).

AI-assisted: yes (Claude Code).

NahuelGenchi and others added 4 commits October 10, 2026 00:38
A subnormal operand sent v_fma_f64 to a separate multiply and add, and a
subnormal result was rounded twice: once to 53 bits in the scaled domain
and again by the power-of-two rescale. Both differ from IEEE 754
fusedMultiplyAdd in the last bit.

Subnormal operands are now widened by 2^64 and enter the exact path with
their exponent lowered by 64. The exact path keeps the sign of the residual
between the 53-bit result and the exact sum; when the result lands on a
midpoint of the subnormal grid it is moved one ulp towards the exact sum
before the single final rounding. Normal results are rebuilt by setting
the exponent, so only a subnormal result rounds. v_div_fmas_f64 shares the
path.
…results

Expected values measured on RDNA2 (gfx1035) with the HW oracle at IEEE=0
and IEEE=1, f64 denormals kept.

Co-Authored-By: Claude <noreply@anthropic.com>
With OOB_SELECT 0 and stride 0 every access is out of range now that the
buffer bounds follow OOB_SELECT, so the shader never read or wrote.

Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10

Copy link
Copy Markdown
Collaborator

Measured on RDNA2 (gfx1035) with the HW oracle (tools/hw-oracle), IEEE=0 and IEEE=1, f64 denormals kept: v_fma_f64 and v_div_fmas_f64 (VCC clear and set) over 3000 rows (subnormal operands, 772 results in the subnormal range, 316 rows where a separate multiply and add differs from one rounding), plus 16 rows aimed at v_div_fmas_f64 with VCC set. The hardware rounds once, and this PR's lowering matches it bit for bit on RADV and on CI's lavapipe (Mesa 25.2.8). This agrees with @SleglJan's gfx1036 measurement. Build and full ctest pass, and the test fails on main.

I rebased onto main and pushed these commits on top (your commit is unchanged):

  • Rebase: TechnicalDebt conflict with the neighbouring entries; kept both.
  • Test: the buffer descriptors now use 0x31016fac (OOB_SELECT 3), like the other execution tests after fa2df2e. With 0x01016fac (OOB_SELECT 0, stride 0), every access is out of range on current main, so the shader never wrote its output and the test failed.
  • Test: v_div_fmas_f64 with VCC set, 20 rows (subnormal operands, and results that land in the subnormal range after the 2^-128 scale). The expected values were measured on gfx1035, and these rows fail on main.
  • TechnicalDebt: the entry now says these cases were measured. It also records the remaining gap: the f16/f64 denormal bits of FLOAT_MODE are not read. When they flush, the hardware flushes the operands and the result of v_fma_f64 (tininess before rounding) but only the result of v_div_fmas_f64.

Merging once CI is green.

@adriadam10
adriadam10 force-pushed the fix/v-fma-f64-subnormals branch from 32a7c34 to e4a911f Compare October 9, 2026 22:47
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
@adriadam10
adriadam10 merged commit 00dd2ba into boykopovar:main Oct 9, 2026
8 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…bnormals

fix(shader): round v_fma_f64 once for subnormal operands and results
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants