Skip to content

Commit 5624a7f

Browse files
fuzzie360claude
andcommitted
fix: Signed right shift losing a bit on exact operands
bitwiseSignedRightShift divided by pow(2.0, float(shifts)), but pow is approximate on most GPUs — it is usually exp2(y * log2(x)) — so the divisor could land ~1 ulp high. The quotient then fell just short of a whole number and floor() discarded it: 2 >> 1 returned 0, 8 >> 1 returned 3, 8 >> 3 returned 0. Only even left operands were affected. Odd ones carry a fractional part large enough to absorb the error, which is why 3 >> 1 and 5 >> 1 were always right and the breakage looked arbitrary. Use _pow2 instead, the exact constant-multiply helper already added for the same class of bug in the float packing (#659). Applied to both the webgl and webgl2 shaders, with a forward declaration since _pow2 is defined further down. The other shift and bitwise operators are integer loops that never touched pow, which matches the failure set exactly. macOS suite goes 9 -> 3 failures; only the Infinity encoding family is left. Verified exhaustively against JS semantics for `>>` over a wide value range including negatives (0/484 mismatches, was 7/100 on the 0-9 grid alone), the other five bitwise operators unchanged, and 9 BrowserStack targets green — the webgl2 path has no node coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ec8e295 commit 5624a7f

7 files changed

Lines changed: 65 additions & 14 deletions

File tree

‎dist/gpu-browser-core.js‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.7
8-
* @date Mon Jul 27 2026 00:05:27 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 00:22:08 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -6617,8 +6617,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
66176617
return result;
66186618
}
66196619
6620+
// _pow2 is defined further down, alongside encode32/decode32
6621+
float _pow2(float e);
66206622
int bitwiseSignedRightShift(int num, int shifts) {
6621-
return int(floor(float(num) / pow(2.0, float(shifts))));
6623+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
6624+
// division fall just under a whole number, which floor() then rounds away:
6625+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
6626+
// affected, odd ones having enough slack to survive. _pow2 is exact.
6627+
return int(floor(float(num) / _pow2(float(shifts))));
66226628
}
66236629
66246630
int bitwiseZeroFillRightShift(int n, int shift) {
@@ -11358,8 +11364,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
1135811364
return result;
1135911365
}
1136011366
11367+
// _pow2 is defined further down, alongside encode32/decode32
11368+
float _pow2(float e);
1136111369
int bitwiseSignedRightShift(int num, int shifts) {
11362-
return int(floor(float(num) / pow(2.0, float(shifts))));
11370+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
11371+
// division fall just under a whole number, which floor() then rounds away:
11372+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
11373+
// affected, odd ones having enough slack to survive. _pow2 is exact.
11374+
return int(floor(float(num) / _pow2(float(shifts))));
1136311375
}
1136411376
1136511377
int bitwiseZeroFillRightShift(int n, int shift) {

‎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: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.7
8-
* @date Mon Jul 27 2026 00:05:27 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 00:22:08 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -12071,8 +12071,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
1207112071
return result;
1207212072
}
1207312073

12074+
// _pow2 is defined further down, alongside encode32/decode32
12075+
float _pow2(float e);
1207412076
int bitwiseSignedRightShift(int num, int shifts) {
12075-
return int(floor(float(num) / pow(2.0, float(shifts))));
12077+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
12078+
// division fall just under a whole number, which floor() then rounds away:
12079+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
12080+
// affected, odd ones having enough slack to survive. _pow2 is exact.
12081+
return int(floor(float(num) / _pow2(float(shifts))));
1207612082
}
1207712083

1207812084
int bitwiseZeroFillRightShift(int n, int shift) {
@@ -16812,8 +16818,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
1681216818
return result;
1681316819
}
1681416820

16821+
// _pow2 is defined further down, alongside encode32/decode32
16822+
float _pow2(float e);
1681516823
int bitwiseSignedRightShift(int num, int shifts) {
16816-
return int(floor(float(num) / pow(2.0, float(shifts))));
16824+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
16825+
// division fall just under a whole number, which floor() then rounds away:
16826+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
16827+
// affected, odd ones having enough slack to survive. _pow2 is exact.
16828+
return int(floor(float(num) / _pow2(float(shifts))));
1681716829
}
1681816830

1681916831
int bitwiseZeroFillRightShift(int n, int shift) {

‎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/web-gl/fragment-shader.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
192192
return result;
193193
}
194194
195+
// _pow2 is defined further down, alongside encode32/decode32
196+
float _pow2(float e);
195197
int bitwiseSignedRightShift(int num, int shifts) {
196-
return int(floor(float(num) / pow(2.0, float(shifts))));
198+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
199+
// division fall just under a whole number, which floor() then rounds away:
200+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
201+
// affected, odd ones having enough slack to survive. _pow2 is exact.
202+
return int(floor(float(num) / _pow2(float(shifts))));
197203
}
198204
199205
int bitwiseZeroFillRightShift(int n, int shift) {

‎src/backend/web-gl2/fragment-shader.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,14 @@ int bitwiseZeroFillLeftShift(int n, int shift) {
154154
return result;
155155
}
156156
157+
// _pow2 is defined further down, alongside encode32/decode32
158+
float _pow2(float e);
157159
int bitwiseSignedRightShift(int num, int shifts) {
158-
return int(floor(float(num) / pow(2.0, float(shifts))));
160+
// pow(2.0, n) is approximate on many GPUs, and landing 1 ulp high makes the
161+
// division fall just under a whole number, which floor() then rounds away:
162+
// 2 >> 1 came out 0, 8 >> 1 came out 3. Only exact left operands were
163+
// affected, odd ones having enough slack to survive. _pow2 is exact.
164+
return int(floor(float(num) / _pow2(float(shifts))));
159165
}
160166
161167
int bitwiseZeroFillRightShift(int n, int shift) {

‎test/browserstack/smoke.html‎

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

192+
test(label('signed right shift'), function (track) {
193+
const gpu = track(new GPU({ mode: mode }));
194+
// emulated in GLSL for both webgl and webgl2; used to use an
195+
// approximate pow(2.0, n), so exact left operands shifted one too far
196+
const kernel = gpu.createKernel(function (a, b) {
197+
return a[this.thread.x] >> b[this.thread.x];
198+
}, { output: [6] });
199+
const left = [2, 4, 8, 8, 1024, -8];
200+
const right = [1, 1, 3, 2, 5, 1];
201+
const out = kernel(left, right);
202+
left.forEach(function (value, i) {
203+
assertClose(out[i], value >> right[i], value + ' >> ' + right[i], 0.001);
204+
});
205+
});
206+
192207
test(label('modulo in a comparison'), function (track) {
193208
const gpu = track(new GPU({ mode: mode }));
194209
// used to fail to compile wherever fixIntegerDivisionAccuracy is off,

0 commit comments

Comments
 (0)