software-mansion / software-mansion/TypeGPU
std.intdiv with two runtime f32 operands emits unsigned division, so negative inputs silently return 0 on GPU
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.2k
- Forks
- 122
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 34
Description
What's wrong
When std.intdiv is called with two runtime floating-point values, the generated shader casts both operands to u32 (an unsigned integer) before dividing. Negative inputs cannot be represented as unsigned, so they saturate to 0 and the division quietly returns the wrong answer. The CPU implementation handles the same inputs correctly, so the two backends disagree and nothing warns about it: std.intdiv(-5.9, 2.1) is -2 on the CPU and 0 on the GPU.
Where
At HEAD (186a026):
packages/typegpu/src/std/numeric.ts lines 1215-1239. The signature unifies the operands against [u32, i32] and the CPU path is sign-preserving:
https://github.com/software-mansion/TypeGPU/blob/186a026c36ac55268377dac3bb233c19c0410f06/packages/typegpu/src/std/numeric.ts#L1215-L1239
function cpuIntdiv(lhs: number, rhs: number): number {
...
return Math.trunc(Math.trunc(lhs) / Math.trunc(rhs));
}
...
signature: (lhs, rhs) => {
const unified = unify([lhs, rhs], [u32, i32]);
packages/typegpu/src/tgsl/conversion.ts lines 103-200 is why u32 wins. For an f32 source, primitivePreference plus the rank formula at line 123 gives i32 and u32 identical rank (20 each, sum 40 vs 40). The tie-break at line 199 uses strict <, so the first candidate in the list wins, and the list is [u32, i32] with u32 first:
https://github.com/software-mansion/TypeGPU/blob/186a026c36ac55268377dac3bb233c19c0410f06/packages/typegpu/src/tgsl/conversion.ts#L103-L200
How it manifests
- Define a GPU function with two runtime
f32parameters that returnsstd.intdiv(a, b). - Resolve it. The emitted WGSL is:
fn foo(a: f32, b: f32) -> u32 {
return (u32(a) / u32(b));
}
- With
a = -5.9, b = 2.1: CPU returns-2, but in WGSLu32(-5.9)saturates to0, so the GPU returns0.
Reproduced against typegpu 0.12.1 with unplugin-typegpu 0.12.2 in a production build; no warning is emitted (the implicit-conversion log category is dev-only, and even in dev it doesn't say a signed value became unsigned).
Expected behavior
Float operands should unify to i32, matching the sign-preserving CPU semantics and the doc comment ("Coerces both arguments to integers"), which says nothing about unsignedness. The repo's own float test (intdiv coerces float arguments to integers in packages/typegpu/tests/std/numeric/intdiv.test.ts) resolves intdiv(d.f32(5.5), 2) to i32, because the mixed f32 + abstractInt case isn't a tie. The all-float case appears to be an unintended tie-break artifact. Reordering the candidates to [i32, u32], or preferring signed targets for float sources, would fix it; a regression test with two runtime f32 operands and a negative value would lock it in.
For context, this seems adjacent to #2293 (integer division generating f32 division), which std.intdiv was added to address.
Found while running Ito (AI code review, free for open source) against recently merged PRs. Full analysis: https://app.ito.ai/share/ad79650a-6683-4f6b-9241-e1d9ccb0070e.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read packages/typegpu/src/std/numeric.ts and packages/typegpu/src/tgsl/conversion.ts, focusing on std.intdiv's operand unification and the signed-versus-unsigned tie-break. Run packages/typegpu/tests/std/numeric/intdiv.test.ts, then add coverage for two runtime f32 operands with a negative value; done means the generated shader and CPU path preserve the expected signed result.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers, computer-graphics
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100