Fix signed % wrong for negative operands on NVIDIA (OpSRem poison without maintenance8)#9674
Fix signed % wrong for negative operands on NVIDIA (OpSRem poison without maintenance8)#9674mstampfli wants to merge 1 commit into
Conversation
02d3cc4 to
37bd4f0
Compare
|
For reviewers: a standalone reproducer is here: https://github.com/mstampfli/wgsl-mod-repro — it has a cross-driver differential (NVIDIA vs Intel ANV vs llvmpipe on the same naga SPIR-V), a SPIR-V opcode dump showing naga emits OpSRem, and a raw-Vulkan A/B that toggles VK_KHR_maintenance8 to confirm NVIDIA is conformant and the result is poison without it. |
|
You will need to add some snapshot that demonstrates this change I think, so that we notice if it ever goes back. Frankly I'm surprised none of our current snapshots are affected. |
Add a SPIRV snapshot that exercises the avoid_signed_int_remainder option so a regression is caught if signed % ever goes back to OpSRem. SpirvOutParameters now exposes the option through the [spv] toml section (default false), and int-signed-remainder.wgsl enables it: the generated SPIR-V reconstructs signed % as a - b * (a / b) (no OpSRem) while unsigned % stays OpUMod. Requested in review of gfx-rs#9674.
|
Good call, added in b65ab07. On why nothing was affected: the option defaults to The new snapshot is targets = "SPIRV"
[spv]
avoid_signed_int_remainder = trueIt covers scalar and vector signed |
|
IMO this should not be an obscure option because the result of not using it is undefined behavior when correctly calling functions the way they are described in the WGSL spec. I think this fix should apply globally. It is likely that the actual GPU compilers will be able to optimize it away in many cases anyway. |
|
Agreed, and that lines up with the sibling GLSL PR (#9687), which already applies the reconstruction unconditionally for the same reason: Happy to make this the default in the SPIR-V backend too and drop both the opt-in default and the NVIDIA-only gating in When the device supports (a) always reconstruct I lean (a), which matches your "compilers can optimize it away" point, unless you would rather have (b). Either way this changes every existing signed- |
69e0653 to
b4bc2e7
Compare
WGSL defines signed `%` for all non-degenerate operands, but naga lowered it to `OpSRem`, which produces a poison (undefined) result for negative operands in the Vulkan SPIR-V environment unless `VK_KHR_maintenance8` is enabled. NVIDIA exercises that latitude (e.g. `-1 % 768` yields `255` instead of `-1`); other drivers happen to define it. Always lower signed `%` as `a - b * (a / b)` instead. `OpSDiv` is not poisoned for negative operands, and the existing wrapped-divisor guard keeps the degenerate (zero and `INT_MIN / -1`) cases matching the WGSL spec. This is unconditional rather than gated to one vendor, since the poison is a property of the SPIR-V environment, not the driver. Fixes gfx-rs#8191.
b4bc2e7 to
fe0560f
Compare
|
Decided on (a): always reconstruct, no I benchmarked first to make sure a So enabling Side effect for the earlier snapshot request: several existing snapshots ( |
|
I would've preferred option b but thats fine. We tend to lean on these clarifying vulkan extensions where possible, for example for out of bounds writes and zero initialization. But since you demonstrated it doesn't matter this should be fine. You can probably drop the new snapshot inputs by the way, unless you see a reason to keep them. Sorry for directing you to do that because I failed to understand the state of the PR. I'll give a proper review now that my initial questions were answered. Thank you for helping me understand |
Fixes #8191.
Problem
WGSL defines signed integer
%for all non-degenerate operands --1 % 768must be-1(truncated remainder, sign of the dividend; WGSL §8.7). But on NVIDIA it returns255, i.e.(-1 as u32) % 768.naga lowers signed
%to SPIR-VOpSRem. In the Vulkan SPIR-V environment,OpSRem/OpSModwith a negative operand produce apoison(undefined) result unlessVK_KHR_maintenance8is enabled (Vulkan spec, SPIR-V environment appendix). wgpu does not enable that extension, so the result is undefined for negative operands. NVIDIA exercises that latitude; Mesa (ANV, llvmpipe) happen to define it. So this is a wgpu/naga conformance gap, not an NVIDIA driver bug.Evidence
Same naga-generated SPIR-V, run on every Vulkan adapter (no maintenance8):
Enabling
VK_KHR_maintenance8on the same NVIDIA device makesOpSRemcorrect (-1), confirming NVIDIA is conformant and the SPIR-V is the issue.Fix
OpSDivis not poisoned for negative operands, so signed%can be lowered withoutOpSRem:naga::back::spv::Options::avoid_signed_int_remainder(defaultsfalse→ existing output byte-for-byte unchanged). When set, the existingnaga_modwrapper emitsa - b * (a / b)instead ofOpSRem, reusing the wrapper's zero/INT_MIN-guarded divisor so degenerate cases still match WGSL's required0.Verified end-to-end: with the flag, the patched naga's SPIR-V returns the WGSL-correct values on NVIDIA without maintenance8.
Alternative / follow-up
A perf-optimal complement is to enable
VK_KHR_maintenance8inwgpu-halwhen the adapter supports it (no per-op cost), keeping this polyfill as the fallback for drivers/targets that lack it. Happy to do that here or as a follow-up - whichever maintainers prefer. (Also open to gating the polyfill differently, e.g. always-on for signed%, or via aWorkaroundsbit.)