Skip to content

fix(shader): translate v_mullit_f32 in f32 denormal modes 1 and 2 - #5941

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
zmeizy:claude/mullit-f32-denormal-modes
Oct 24, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
zmeizy:claude/mullit-f32-denormal-modes

Conversation

@zmeizy

@zmeizy zmeizy commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

A shader whose FLOAT_MODE f32 denormal field is 1 or 2 failed to translate on v_mullit_f32 ("f32 denormal mode N is not implemented"). Measured on hardware, the two modes do what their names say:

  • Mode 2 flushes the three sources and keeps the result.
  • Mode 1 keeps the sources and flushes the result: a product below 2^-126 after rounding becomes a zero of its sign. That includes products of preserved denormal sources, so 0x00000001 * 1.0 gives +0.

f32DenormalFlushFor now returns these flags for v_mullit_f32: sources only for mode 2, result only for mode 1. The existing input flush and tiny-product check then produce the measured results. Mode 1 is only accepted with f32 round to nearest even, the rounding that was measured; with another f32 rounding it still throws. Every other instruction still throws in modes 1 and 2.

Source: measured on RDNA2, gfx1030 (RX 6800, Windows 11, AMD driver HIP runtime), with the hardware oracle's kernel template.

  • Inputs: 24 values per source (zeros, denormals of both signs, the smallest normals, 2^-64, 2^-63, ±1, ±max, ±infinity, quiet and signalling NaNs), in all 13,824 combinations.
  • Modes: denorm32 0..3 at IEEE 0 and 1, f32 round to nearest, f16 denormals kept, DX10_CLAMP 1.
  • Result: the model also matches modes 0 and 3, which were already translated.

Tested

Ubuntu 24.04, GCC 13.3, lavapipe, on main a453108c.

  • agc_driver_mullit_f32_denormal_modes (new) pins 23 measured rows, with an expected value for each of the 8 mode and IEEE combinations.
    • On main it fails (mode 1 does not translate); with the change it passes in 0.3 s.
  • All 110,592 measured rows (13,824 in each of 8 modes) match the recompiler output with the change.
  • agc_driver_f32_denormal_flush and agc_driver_f32_constant_factor_tininess pass.

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 (n/a)
  • 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 (n/a)
  • Generic behaviour, not specific to one title
  • Depends on: fix(shader): translate f32 add and multiply in f32 denormal modes 1 a… #6010
  • AI-assisted: yes

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA

@zmeizy

zmeizy commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@jotitadev jotitadev 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.

Run on an Apple GPU, since the PR was tested only on lavapipe. agc_driver_mullit_f32_denormal_modes fails here, because the test needs f32 denormals to be preserved and this device does not preserve them.

GPUs tested: Apple M2 Max, MoltenVK 1.4.2 (Vulkan 1.4.357), macOS 27.2, x86-64 under Rosetta; also MoltenVK main built with KhronosGroup/SPIRV-Cross#2704 (keeps the sign of -0)

main does not run the GPU tests on macOS yet, so I tested on macos-port at b7eb6b6b (mugurc/AnyPS5) plus the ImportWatched change from #1, with this PR's own diff (merge-base..head 5279465e1) applied. Not main. Release build, LunarG Vulkan SDK 1.4.363.0. Suite: ctest -R "^agc" -E "agc_display_buffer_dcc|agc_driver_flip|agc_driver_lds_atomics64" -j1 --timeout 30. Without the PR, 13 tests fail on this setup (7 with the #2704 build), all known macOS failures listed in #1. The diff applies cleanly.

  • With only the new test added to the unchanged branch, it fails with f32 denormal mode 1 at pc 24 is not implemented. So the test runs on this device, and the translation now accepts modes 1 and 2.
  • With the PR, it fails with both builds:
    stock:  v_mullit_f32 denorm32 1 no ieee inputs 0x00000001 0x80000001 0x00800000 is 0x00000000, expected 0x80000000
    #2704:  v_mullit_f32 denorm32 1 no ieee inputs 0x00000001 0x7f7fffff 0x00800000 is 0x00000000, expected 0x34ffffff
    
  • To see every failing row, I changed the Require in Check locally to print each mismatch. The test has 23 rows × 8 modes.
    • Stock MoltenVK: 34 mismatches. 12 of them are -0 returned as +0 (the SPIRV-Cross issue in #1), and they go away with the #2704 build.
    • #2704 build: 22 mismatches. Every one returns a zero where the expected value depends on keeping an f32 denormal:
      • Mode 0 passes every row.
      • Mode 1 fails 1 row, at both IEEE settings: 0x00000001 * 0x7f7fffff, where the denormal source is flushed before the multiply.
      • Mode 2 fails 1 row, at both IEEE settings: 0x00800000 * 0.5, where the 0x00400000 result is flushed.
      • Mode 3 fails 9 rows, at both IEEE settings: denormal sources or results, for example 0x00000001 * 1.0 gives 0, not 0x00000001.
  • This device reports shaderDenormPreserveFloat32 = false (and shaderDenormFlushToZeroFloat32 = false). agc_driver_f32_denormal_flush already fails on this branch without the PR. The mode 3 rows exercise translation that is already on main.
  • Suite: the base failures plus agc_driver_mullit_f32_denormal_modes, with both builds. No other test changes.

So the new modes 1 and 2 look right on Apple for every row that does not need a preserved f32 denormal. The test fails here only because of a device capability. If these tests are to run on hosts without f32 denormal preservation, the rows that need it could be skipped when that feature is missing, as #5492 does for ds_add_f32.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 docs/dev/debt/shader.md
PR 5371 docs/dev/debt/shader.md
PR 6235 docs/dev/debt/shader.md
PR 6280 docs/dev/debt/shader.md
PR 6412 docs/dev/debt/shader.md
PR 6488 docs/dev/debt/shader.md
PR 6924 docs/dev/debt/shader.md
PR 6959 docs/dev/debt/shader.md
PR 7223 docs/dev/debt/shader.md
PR 7410 docs/dev/debt/shader.md
PR 7484 docs/dev/debt/shader.md
PR 7684 docs/dev/debt/shader.md
PR 8004 docs/dev/debt/shader.md
PR 8077 docs/dev/debt/shader.md
PR 9193 docs/dev/debt/shader.md
PR 9335 docs/dev/debt/shader.md
PR 9387 docs/dev/debt/shader.md
PR 9889 core/shader/recompiler/Translation/src/VectorInstructions.cpp
docs/dev/debt/shader.md
PR 9969 docs/dev/debt/shader.md
PR 10233 docs/dev/debt/shader.md

@adriadam10 adriadam10 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.

I measured v_mullit_f32 on RDNA2 (gfx1035) with the hardware oracle: 13,824 operand sets in denorm32 modes 0..3 at IEEE 0 and 1, round to nearest even. The register-only form matches your gfx1030 numbers, and with this PR the recompiler matches every row on CI's lavapipe. Two problems are left:

  • Mode 1 with a constant factor skips the result flush, on every driver. flushTinyProduct (Translation/src/TranslationContext/OperandAccess.cpp:159, called from FloatInstructions.cpp:443) returns the plain product when a factor is a constant with exponent 0 or ≥ 127 (keepsProductOutOfTinyRange). That shortcut is only valid when the sources are flushed. In mode 1 they are kept, so a tiny product goes through. Oracle vs this PR, on both lavapipe and RADV, with src2 = 1.0:

    • v_mullit_f32 v, 1.0, v5, v6 with v5 = 0x00000001: hardware 0x00000000, PR 0x00000001
    • v_mullit_f32 v, v4, 2.0, v6 with v4 = 0x00000001: hardware 0x00000000, PR 0x00000002
    • v_mullit_f32 v, 0x1 (literal), v5, v6 with v5 = 1.0: hardware 0x00000000, PR 0x00000001

    Over the sweep that is 1,560 rows each for the inline 1.0 forms, 624 for inline 2.0 and 936 for the literal. Applying the shortcut only when f32DenormalFlush & 1u is set fixes it. Please also add test rows with an inline constant operand.

  • The new test fails on RADV (Mesa, gfx1035): v_mullit_f32 denorm32 1 no ieee inputs 0x00000001 0x7f7fffff 0x00800000 is 0x00000000, expected 0x34ffffff. jotitadev hit the same row on MoltenVK. Nothing asks the driver to keep f32 denormals: only DenormPreserve 16 and 64 are ever requested (VulkanDevice.cpp:1284, :1495, SpirvModuleSetup.cpp:337). RADV flushes f32 denormals by default, and lavapipe keeps them. So on RADV, mode 1 gives 0 for 0x00000001 * 0x7f7fffff and mode 2 gives 0 for 0x00800000 * 0.5 (hardware 0x00400000). Before this PR those modes threw. One possible fix:

    • Request DenormPreserve 32 (gated on shaderDenormPreserveFloat32, like the f16/f64 flags) for shaders whose f32 denormal field keeps sources or results.
    • Keep throwing for modes 1 and 2 on devices without that feature, and skip those rows in the test there.

    The mode 3 rows have the same gap on main today. Either cover them the same way, or leave them out of this test so it does not fail on drivers that flush.

@zmeizy

zmeizy commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for measuring on gfx1035 and for both findings. Updated in eae67fc, rebased onto main cecb540.

Constant factors. The keepsProductOutOfTinyRange shortcut in flushTinyProduct now applies only when the sources are flushed (f32DenormalFlush & 1u), as you suggested. The same shortcut also hit v_mul_f32 and v_mul_legacy_f32 in mode 1 (#6010), so the fix lives in #6010's commit. With #6010's result-only flush fix, a denormal result in mode 1 is flushed as well.
Measured on gfx1030 with the forms you listed: v_mullit_f32 v, 1.0, v4, v6, v4, 2.0, v6, 0x1 (literal), v5, v6 and 0x3fc00000 (literal), v4, v6, plus the register form. 1,632 rows (kept denormals and normals around 2^-126, src2 = ±1.0, 2^-149, 0) in all four denormal modes at IEEE 0/1. The recompiler now matches every row.
The test pins 40 of those rows for the four constant forms, next to the 23 register rows.
RADV and devices that flush. Modes 1 and 2 now translate only when the module keeps f32 denormals (target.float32DenormPreserve set, hardware f32 flush not in use). Otherwise they throw ... needs DenormPreserve 32, which the device does not offer for this shader. Mode 1 with a directed round mode still throws as before.
On RADV, main currently declares the hardware f32 flush in every module, so these modes throw there. claude/f32-hardware-flush-float-mode (a separate PR) limits that flush to shaders that flush f32 denormals, after which RADV gets DenormPreserve 32 here.
Test. agc_driver_mullit_f32_denormal_modes accepts the DenormPreserve 32 throw in modes 1 and 2 only on a device that doesn't keep f32 denormals. It checks mode 3 only where the device keeps them, and always checks mode 0. On CI's lavapipe 25.2.8 (no shaderDenormPreserveFloat32) that is mode 0 for both programs at IEEE 0/1 plus the throw in modes 1 and 2. main fails it (f32 denormal mode 1 at pc 24 is not implemented).
Stacked on #6010. The branch sits on #6010's commit, which adds the translator flag, the result flush and the shortcut fix. So Depends on: #6010. I'll rebase to a single commit when #6010 is in.
Checked on Ubuntu 24.04 (WSL2), GCC 13.3, lavapipe (Mesa 25.2.8):

Values, with preserve forced in the test and the harness: the test checks all 16 runs, and all 110,592 register rows (13,824 × 8 modes) and all 1,632 × 5 constant-form rows per mode match.
Focused tests: agc_driver_mullit_f32_denormal_modes, f32_add_mul_denormal_modes, f32_constant_factor_tininess, f32_denormal_flush, f32_arithmetic_nan and nan_quieting pass with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ANYPS5_REQUIRE_VULKAN=1 and ANYPS5_NO_SHADER_CACHE=1, with no validation messages beyond main's.
Measurement data for the constant forms: anyps5-gpu-probes-v4-gfx1030.json.gz (probe constant_factor_denormal_modes), attached.

@zmeizy

zmeizy commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 22, 2026
zmeizy and others added 2 commits October 22, 2026 22:45
…nd 2

v_add_f32, v_sub_f32, v_subrev_f32, v_mul_f32 and v_mul_legacy_f32
follow the FLOAT_MODE f32 denormal field with f32 round to nearest
even, as measured on RDNA2 (gfx1030): mode 1 keeps denormal inputs and
flushes a denormal result, and mode 2 flushes denormal inputs and keeps
a denormal result. Both need the device to keep the f32 denormals the
shader keeps, so they translate only when the module declares
DenormPreserve 32 and throw otherwise. The result flush now applies
when only the result flush bit is set; it was skipped unless the input
flush bit was set too. A constant factor no longer skips the tiny
product check while denormal inputs are kept, as a kept denormal times
such a constant can still be tiny. Directed rounding in these modes
still throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Denormal mode 2 flushes only the sources and mode 1 only the result, as
measured on RDNA2 (gfx1030), also with an inline or literal constant
factor. Both need the device to keep the f32 denormals the shader keeps,
so they translate only when the module declares DenormPreserve 32 and
throw otherwise; mode 1 with another f32 rounding throws as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zmeizy
zmeizy force-pushed the claude/mullit-f32-denormal-modes branch from eae67fc to c33e289 Compare October 22, 2026 20:02
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 22, 2026
@adriadam10
adriadam10 merged commit d0e7fe7 into boykopovar:main Oct 24, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Re-checked against current main: #6010 has merged, so this PR is now only its own commit. Modes 1 and 2 of v_mullit_f32 translate only with DenormPreserve 32 (mode 1 also only with f32 round to nearest even) and throw otherwise, as v_add_f32/v_mul_f32 do on main. The constant-factor shortcut in flushTinyProduct needs flushed sources, so the mode 1 constant-factor rows still match our RDNA2 (gfx1035) capture at IEEE 0 and 1 in modes 0..3; merged.

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.

4 participants