Repository navigation
fix(shader): round normalized storage image writes from the exact product - #7666
tibzejoker wants to merge 2 commits into
Conversation
Vulkan lets a device return either neighbour when it converts a float to a normalized texel, and devices differ: for a product of 123.50003, NVIDIA and Intel both store 123, and on GTA V's FSR1 RCAS compute pass the two vendors stored different bytes for the same inputs. Writes to unorm and snorm storage images whose components share one width now clamp the value (NaN to 0), round value * scale to nearest even in the shader and store level * c, where c is the smallest float not below 1 / scale. The exact product of that float with the scale lies in [level, level + 0.01) for 8-, 10- and 16-bit unorm and snorm, so a device that rounds to nearest and one that truncates both store the level. The format comes from the image descriptor through specialization constants (StoreQuantizeBase, four per written storage image), so other formats keep the plain write. Test: agc_driver_image_store_unorm_rounding stores values next to the midpoints between levels, edge values and NaN/infinities to 8_UNORM, 16_16_UNORM, 8_8_8_8_UNORM, 8_8_8_8_SNORM and 16_16_16_16_UNORM images; it fails on main on an RTX 4090 and on an Intel UHD 770 and passes on both with this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
|
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.
|
|
The overlap with #7085 is textual only: both add a constant next to AI-assisted (Claude Code). |
|
A question for anyone with an RDNA hardware oracle (as used on #7671). This PR picks round-to-nearest-even of An AI-assisted (Claude Code). |
|
AMD run, since the description covers an RTX 4090. Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), Windows 11 26300, WinLibs GCC 15.2.0 r7, Release,
AI-assisted |
jotitadev
left a comment
There was a problem hiding this comment.
Apple GPU / MoltenVK run, with a data point for the vendor question. Without this PR, Apple stores the level nearest to the exact product x * scale, not to fl32(x * scale). That is a third behaviour next to NVIDIA and Intel. With the PR, agc_driver_image_store_unorm_rounding passes, and the full ^agc suite is unchanged.
Machine: Apple M2 Max, macOS 27.2, x86-64 build under Rosetta. Head 3e54993, PR diff applied cleanly. Drivers: MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0) and MoltenVK main + SPIRV-Cross #2704.
Tested: the tree is main a373e85 + mugurc's macOS series (ci/macos-series-2) + the PRs needed to build on macOS (#6667, #6668, #7524, #6969, and two files of #7332), not plain main (it doesn't build on macOS yet). Also in the same build: #6606, #7683, #7696, #7692 and #7415 (they applied cleanly together), plus a test-only patch that lets nine AGC fixtures load the Vulkan loader on macOS (branch jotitadev:fix/agc-tests-vulkan-loader-macos). Base on that tree: 40 ^agc failures with MoltenVK 1.4.2 and 26 with MoltenVK main + #2704.
-
With the PR:
agc_driver_image_store_unorm_roundingpasses 3 of 3 on both drivers. It still passes 3 of 3 withMVK_CONFIG_FAST_MATH_ENABLED=1, which makes MoltenVK compile with Metal fast math. -
With this PR's
DescriptorBindingBuilder.cpp,PipelineSpecialization.hppandSpirvImageEmitter.cppreverted, the test fails on both drivers; the first mismatch is 8_UNORM texel (10, 0), 0xcd where 0xce is expected. I changed the check locally to log every mismatch instead of stopping at the first. 59 of the 736 written components differ, and the results are identical on both drivers:format mismatches example input exact x * scaleApple stored test expects 8_UNORM 4 0x1.9c9c9cp-1205.4999954 205 206 16_16_UNORM 7 0x1.159116p-52220.5000529 2221 2220 8_8_8_8_UNORM 17 0x1.010102p-90.5000000296 1 0 8_8_8_8_SNORM 16 -0x1.7cf9f4p-1-94.5000004 -95 -94 16_16_16_16_UNORM 15 0x1.cec1cep-7925.4999769 925 926 In each of these inputs,
fl32(x * scale)is exactly .5, so round-to-nearest-even of the float product and rounding of the exact product give different levels. I recomputed the test's inputs offline: exactly 59 components fall in that case (4/7/17/16/15 per format), and Apple stored the level nearest to the exact product in all 59. The NaN, infinity, clamp and edge values match the test's expectations without the PR. -
Since both drivers give the same result, the conversion happens in Metal or the hardware, not in SPIRV-Cross. Apple seems to convert float to unorm/snorm with correct rounding. NVIDIA and Intel, as the description says, store 123 for 123.50003.
-
On the RDNA question: if the console also rounds the exact product, the inputs above are where this PR's rule (RTNE of
fl32(x * scale)) and the console would differ. The test already exercises them, so a hardware oracle run of it would settle the question. -
Full
^agcsuite with the stack: the base failures on each driver, plusagc_driver_pooled_buffer_rangeandagc_performance_overlay, which only run because of the loader patch. Nothing from this PR.
GPUs tested: Apple M2 Max (MoltenVK 1.4.2; MoltenVK main + SPIRV-Cross #2704)
Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
|
Thanks @jotitadev, this is a real gap in the PR. I picked "round to nearest even of Rounding the exact product costs little in the shader. What I don't know is what RDNA does, and the console is the reference. @simonbbby, could your RDNA 2 oracle run the stores of
Until there is data, I'll leave the PR as it is, so that it doesn't change twice. Written with AI assistance (Claude Code). |
|
@tibzejoker RDNA 2 answer: the hardware rounds the exact product, like Apple, not RX 6700 XT (gfx1031), native
In every row where the rules differ, the hardware stored the level nearest to the exact product, for example My counts per format differ from @jotitadev's because I stored all four values of every slot into every format, where the test's own layout stores fewer components for the one- and two-channel formats. Desktop RDNA 2, not a PS5. |
RDNA 2 (RX 6700 XT, native image_store) and Apple M2 store the level nearest to the exact product x * scale, not the nearest even level of its f32 rounding. The two differ when fl32(x * scale) lands exactly on a .5: the fma residual of the product now breaks that tie by its sign. The residual is decorated NoContraction, without it the Intel UHD 770 does not evaluate the fma exactly. The test expects the level nearest to the product computed in double, exact for these inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
|
Thank you @thuanquang, that settles it: RDNA 2 and Apple M2 both store the level nearest to the exact product. Pushed 61340e2 on top of the first commit:
Passes 3/3 on an RTX 4090 and an Intel UHD 770 with @LeandroLP23 your 860M run was on the first version; if you have a moment, a rerun of AI-assisted: yes (Claude Code) |
|
Rerun on 61340e2, AMD Radeon 860M (RDNA 3.5,
AI-assisted: yes (Claude Code) |
|
Thank you @LeandroLP23, that is the check I hoped for: on RDNA 3.5 the new head passes and the first version's shader fails on the same texel. I added the 860M rerun to the GPU line. AI-assisted: yes (Claude Code) |
|
One more detail for the fma rule's |
|
Thanks @thuanquang, good to have the exact ties pinned down too. ±0.5 giving ±64 / ±16384 in SNORM and 128 / 32768 in UNORM is round-to-even, and -1.0 giving -127 / -32767 matches the clamp to [-1, 1]. So the head's rule agrees with RDNA 2 on every case measured. AI-assisted: yes (Claude Code) |
jotitadev
left a comment
There was a problem hiding this comment.
Apple GPU / MoltenVK rerun on 61340e2. The new rule matches what Apple stores, and the fma residual survives Metal fast math. agc_driver_image_store_unorm_rounding passes, the first commit's shader fails it on the texel you quoted, and the full ^agc suite is unchanged.
Machine: Apple M2 Max, macOS 27.2, x86-64 build under Rosetta. Drivers: MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0) and MoltenVK main + SPIRV-Cross #2704. Head 61340e2, PR diff applied cleanly.
Tested: the tree is main a373e85 + mugurc's macOS series (ci/macos-series-2) + the PRs needed to build on macOS (#6667, #6668, #7524, #6969, and two files of #7332), not plain main (it doesn't build on macOS yet). Applied together in the same build: #7666, #7811, #7799 and #7803, plus a test-only patch that lets nine AGC fixtures load the Vulkan loader on macOS (branch jotitadev:fix/agc-tests-vulkan-loader-macos). Base on that tree: 40 ^agc failures with MoltenVK 1.4.2 and 26 with MoltenVK main + #2704, plus agc_driver_pooled_buffer_range and agc_performance_overlay, which only run because of the loader patch.
- With the PR:
agc_driver_image_store_unorm_roundingpasses 3 of 3 on both drivers, withMVK_CONFIG_FAST_MATH_ENABLEDset to 0, 1 (always fast math) and 2. - With
SpirvImageEmitter.cppfrom the first commit (3e54993) and the new test, it fails 3 of 3 on both drivers, and also with fast math forced:image_store of 8_UNORM: texel (10, 0) component 0 is 0x000000ce, expected 0x000000cd (level nearest to the exact product). That is the same texel as on the 860M. - With all of this PR's non-test changes reverted, the new test passes on Apple. Without the shader rounding, Apple's own float-to-unorm/snorm conversion already stores the level nearest to the exact product (my earlier review). So on Apple the test catches a regression to the first commit's rule, not the removal of the rounding.
- Why fast math does not break the residual: in the MSL that MoltenVK generates (
MVK_CONFIG_DEBUG=1, fast math forced), SPIRV-Cross turns theNoContractionmultiply and subtract into its[[clang::optnone]]helpersspvFMul(fma(l, r, 0)) andspvFSub(fma(-1, r, l)). The residual is a plainfma(x, scale, -p). The compiler can't foldpback into the residual. - Full
^agcsuite with the stack: the base failures on each driver, nothing new and nothing gone.
GPUs tested: Apple M2 Max (MoltenVK 1.4.2; MoltenVK main + SPIRV-Cross #2704)
Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
|
Thank you @jotitadev. Good to know the residual survives forced fast math through SPIRV-Cross's AI-assisted: yes (Claude Code) |
|
First AMD-on-Linux (RADV) run of this PR. Merged onto AMD Radeon RX 7900 XT (RADV NAVI31, gfx1100), Mesa 25.2.8, Linux Mint 22.3 (Ubuntu 24.04 base), kernel 7.0.0-38, GCC 13.3, Release. Full suite ( GPUs tested: AMD Radeon RX 7900 XT (RADV NAVI31, gfx1100), Mesa 25.2.8, Linux Mint 22.3 / kernel 7.0.0-38 AI-assisted: yes |
|
Thank you @DavidAmsellem, the first Linux/RADV run is a useful one to have: the new test ran and passed on gfx1100, and the full suite shows only the two failures AI-assisted: yes (Claude Code) |
What
Vulkan lets a device return either neighbour when it converts a float to a normalized texel ("implementations should round to nearest"), and devices differ in practice. For a stored value whose product with 255 is 123.50003, an RTX 4090 and an Intel UHD 770 both store 123 instead of 124. On GTA V's FSR1 RCAS compute pass (PPSA04264, compute 0x25b393000, RGBA8 output), the two vendors stored 112 different bytes for the same inputs.
Writes to unorm and snorm storage images whose components share one width (8_UNORM, 16_16_UNORM, 8_8_8_8_UNORM/SNORM, 16_16_16_16_UNORM, ...) now:
value * scalein the shader to the level nearest the EXACT product, ties to even.fl32(value * scale)can land exactly on a .5 when the exact product does not; the fma residualfma(value, scale, -p)breaks that tie by its sign. RDNA 2 (RX 6700 XT, nativeimage_store, measured by @thuanquang) and Apple M2 (@jotitadev) both round this way. The residual is decoratedNoContraction: without it the Intel UHD 770 does not evaluate the fma exactly;level * c, wherecis the smallest float not below1 / scale.The exact product of that stored float with the scale lies in
[level, level + 0.01)for 8-, 10- and 16-bit unorm and snorm. I checked every level with exact rational arithmetic. A device that rounds to nearest and a device that truncates therefore both storelevel.The format comes from the image descriptor through specialization constants (
StoreQuantizeBase, four per written storage image), so float formats, packed formats and format codes outside the format table keep the plain write. Registration modules, tables and atomics are unchanged.Tested
Windows 11, MinGW GCC 15.2:
agc_driver_image_store_unorm_rounding. It stores values next to the midpoints between levels, edge values and NaN/infinities to 8_UNORM, 16_16_UNORM, 8_8_8_8_UNORM, 8_8_8_8_SNORM and 16_16_16_16_UNORM images.mainit fails on both GPUs:image_store of 8_UNORM: texel (6, 0) component 0 is 0x7b, expected 0x7c.ANYPS5_NO_SHADER_CACHE=1). The expected level isnearbyintof the product computed in double, exact for these inputs.fl32(value * scale)) fails it:texel (10, 0) component 0 is 0xce, expected 0xcd(exact product 205.49999..., RDNA 2 stores 205).ctest -R ^agc_on the RTX 4090: 727/728. The failure isagc_driver_bda_device, which fails onmaintoo.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code
https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P