Skip to content

Commit b0dedd2

Browse files
fuzzie360claude
andcommitted
fix: i % 2 === 0 failing to compile on some GPUs
getType() reported `%` as the left operand's type unless fixIntegerDivisionAccuracy was set, but `%` never lowers to an integer operation: both GLSL helpers behind it (modulo, integerCorrectionModulo) return float, and on cpu it stays a JS `%`. Typing `i % 2` as Integer therefore emitted a float/int comparison, which GLSL rejects: '==' : no operation '==' exists that takes a left-hand operand of type 'lowp float' and a right operand of type 'const int' Return Number for `%` regardless of the flag. `/` is untouched: with the flag off it stays an integer division in GLSL, so the operand type is right there. fixIntegerDivisionAccuracy is derived from GPU features, so this only broke on hardware whose integer division is already accurate — desktop Chrome/Firefox/Edge on Windows, but never headless-gl or Apple GPUs, which is why the suite never caught it. The new tests pin both states explicitly rather than relying on the host's features, and cover the comparison, index and fractional-operand positions. Verified on 9 BrowserStack targets, all green; mac node suite unchanged at its 12 known failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c650607 commit b0dedd2

8 files changed

Lines changed: 134 additions & 9 deletions

File tree

‎dist/gpu-browser-core.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.7
8-
* @date Sun Jul 26 2026 23:59:37 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 00:05:27 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -2635,6 +2635,7 @@ class FunctionNode {
26352635
case 'BinaryExpression':
26362636
switch (ast.operator) {
26372637
case '%':
2638+
return 'Number';
26382639
case '/':
26392640
if (this.fixIntegerDivisionAccuracy) {
26402641
return 'Number';

‎dist/gpu-browser-core.min.js‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/gpu-browser.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.7
8-
* @date Sun Jul 26 2026 23:59:37 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 00:05:27 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -8089,6 +8089,7 @@ class FunctionNode {
80898089
case 'BinaryExpression':
80908090
switch (ast.operator) {
80918091
case '%':
8092+
return 'Number';
80928093
case '/':
80938094
if (this.fixIntegerDivisionAccuracy) {
80948095
return 'Number';

‎dist/gpu-browser.min.js‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/backend/function-node.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,14 @@ class FunctionNode {
482482
case 'LogicalExpression':
483483
return 'Boolean';
484484
case 'BinaryExpression':
485-
// modulos is Number
486485
switch (ast.operator) {
487486
case '%':
487+
// `%` never yields an Integer, whatever its operands are: both GLSL
488+
// helpers it lowers to (modulo, integerCorrectionModulo) return
489+
// float, and on cpu it stays a JS `%`, which is fractional too.
490+
// Reporting the left operand's type here made `i % 2 === 0` emit a
491+
// float/int comparison that GLSL refuses to compile.
492+
return 'Number';
488493
case '/':
489494
if (this.fixIntegerDivisionAccuracy) {
490495
return 'Number';

‎test/all.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
<script type="module" src="features/legacy-encoder.js"></script>
127127
<script type="module" src="features/loops.js"></script>
128128
<script type="module" src="features/math-object.js"></script>
129+
<script type="module" src="features/modulo-comparison.js"></script>
129130
<script type="module" src="features/nested-function.js"></script>
130131
<script type="module" src="features/offscreen-canvas.js"></script>
131132
<script type="module" src="features/optimize-float-memory.js"></script>

‎test/browserstack/smoke.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,22 @@ <h1>GPU.JS smoke test</h1>
189189
assertClose(kernel()[0], 5, 'loop total');
190190
});
191191

192+
test(label('modulo in a comparison'), function (track) {
193+
const gpu = track(new GPU({ mode: mode }));
194+
// used to fail to compile wherever fixIntegerDivisionAccuracy is off,
195+
// since `%` was typed Integer but lowers to a float-returning helper
196+
const kernel = gpu.createKernel(function () {
197+
let total = 0;
198+
for (let i = 0; i < 10; i++) {
199+
if (i % 2 === 0) {
200+
total += i;
201+
}
202+
}
203+
return total;
204+
}, { output: [1] });
205+
assertClose(kernel()[0], 20, 'even sum');
206+
});
207+
192208
test(label('modulo and division'), function (track) {
193209
const gpu = track(new GPU({ mode: mode }));
194210
const kernel = gpu.createKernel(function (a) {

‎test/features/modulo-comparison.js‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const { assert, skip, test, module: describe } = require('qunit');
2+
const { GPU } = require('../../src');
3+
4+
describe('feature: modulo comparison');
5+
6+
// `%` lowers to a float-returning GLSL helper, but getType() used to report the
7+
// left operand's type when fixIntegerDivisionAccuracy was off, so `i % 2 === 0`
8+
// compiled to a float/int comparison and the shader was rejected outright:
9+
// '==' : no operation '==' exists that takes a left-hand operand of type
10+
// 'lowp float' and a right operand of type 'const int'
11+
// The flag is derived from GPU features, so the break only showed on hardware
12+
// where integer division is already accurate — desktop Chrome/Firefox on
13+
// Windows, but not headless-gl or Apple GPUs. Both states are pinned here.
14+
function testModuloEquality(mode, fixIntegerDivisionAccuracy) {
15+
const gpu = new GPU({ mode });
16+
const kernel = gpu.createKernel(function () {
17+
let total = 0;
18+
for (let i = 0; i < 10; i++) {
19+
if (i % 2 === 0) {
20+
total += i;
21+
}
22+
}
23+
return total;
24+
}, { output: [1], fixIntegerDivisionAccuracy });
25+
// 0 + 2 + 4 + 6 + 8
26+
assert.equal(kernel()[0], 20);
27+
gpu.destroy();
28+
}
29+
30+
function testModuloInequality(mode, fixIntegerDivisionAccuracy) {
31+
const gpu = new GPU({ mode });
32+
const kernel = gpu.createKernel(function () {
33+
let total = 0;
34+
for (let i = 0; i < 6; i++) {
35+
if (i % 3 !== 0) {
36+
total += 1;
37+
}
38+
}
39+
return total;
40+
}, { output: [1], fixIntegerDivisionAccuracy });
41+
// 1, 2, 4, 5
42+
assert.equal(kernel()[0], 4);
43+
gpu.destroy();
44+
}
45+
46+
function testModuloAsIndex(mode, fixIntegerDivisionAccuracy) {
47+
const gpu = new GPU({ mode });
48+
const kernel = gpu.createKernel(function (a) {
49+
return a[this.thread.x % 3];
50+
}, { output: [4], fixIntegerDivisionAccuracy });
51+
assert.deepEqual(Array.from(kernel([10, 20, 30, 40])), [10, 20, 30, 10]);
52+
gpu.destroy();
53+
}
54+
55+
function testModuloKeepsFraction(mode, fixIntegerDivisionAccuracy) {
56+
const gpu = new GPU({ mode });
57+
const kernel = gpu.createKernel(function (a) {
58+
return a[this.thread.x] % 2.5;
59+
}, { output: [4], fixIntegerDivisionAccuracy });
60+
const result = Array.from(kernel([1, 3, 6, 9]));
61+
[1, 0.5, 1, 1.5].forEach((expected, i) => {
62+
assert.ok(Math.abs(result[i] - expected) < 0.01, `index ${i}: expected ${expected}, got ${result[i]}`);
63+
});
64+
gpu.destroy();
65+
}
66+
67+
const scenarios = {
68+
'modulo equality': testModuloEquality,
69+
'modulo inequality': testModuloInequality,
70+
'modulo as index': testModuloAsIndex,
71+
'modulo keeps fraction': testModuloKeepsFraction,
72+
};
73+
74+
const modes = {
75+
auto: undefined,
76+
gpu: 'gpu',
77+
webgl: 'webgl',
78+
webgl2: 'webgl2',
79+
headlessgl: 'headlessgl',
80+
cpu: 'cpu',
81+
};
82+
83+
const supported = {
84+
auto: GPU.isGPUSupported,
85+
gpu: GPU.isGPUSupported,
86+
webgl: GPU.isWebGLSupported,
87+
webgl2: GPU.isWebGL2Supported,
88+
headlessgl: GPU.isHeadlessGLSupported,
89+
cpu: true,
90+
};
91+
92+
Object.keys(scenarios).forEach(scenario => {
93+
Object.keys(modes).forEach(modeName => {
94+
[false, true].forEach(fix => {
95+
const run = supported[modeName] ? test : skip;
96+
run(`${scenario} ${modeName} fixIntegerDivisionAccuracy=${fix}`, () => {
97+
scenarios[scenario](modes[modeName], fix);
98+
});
99+
});
100+
});
101+
});

0 commit comments

Comments
 (0)