Skip to content

Commit 73e93c0

Browse files
fuzzie360claude
andcommitted
fix: WebGL contexts and kernels leaking from GPU.destroy()
kernel.destroy() splices itself out of gpu.kernels, and GPU.destroy() was indexing that same array as it shrank, so: * every other kernel was skipped — 4 kernels left 2 alive, with their textures, framebuffers and programs * this.kernels[0] was read after the loop had emptied the list, so a single-kernel GPU never reached destroyContext and leaked its WebGL context entirely The second is the one that bites in a browser. 40 awaited create/destroy cycles produced 25 "Too many active WebGL contexts" warnings, at which point the browser starts evicting contexts that are still in use. Walk a copy of the list instead, and keep the reference needed to release the context. Fixing that exposed a latent crash: a Texture holds a reference to the kernel that made it, so it can outlive that kernel and call deleteTexture() after the context is gone. Previously masked, because those kernels were never destroyed. deleteTexture now returns early when the context has been released, matching destroy()'s own guard — the GL texture went with the context. Also from the same device logs: * enableVertexAttribArray/vertexAttribPointer were handed -1 whenever the GLSL compiler removed aTexCoord (any shader that samples no texture), raising INVALID_VALUE on every draw. Software GL keeps the attribute, so this only appeared on real drivers. * acorn.parse was called without ecmaVersion, which acorn 8 warns about and will eventually reject. Pinned to 2020, the version it already defaults to, so nothing changes but the warning. * the test pages requested /favicon.ico and 404ed. The browser console is now silent across the smoke suite, down from 26 warnings; the only remaining entries are the one-off INVALID_ENUM pair from probing float texture support, which is how that probe works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dc39458 commit 73e93c0

15 files changed

Lines changed: 202 additions & 67 deletions

‎dist/gpu-browser-core.js‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.8
8-
* @date Mon Jul 27 2026 01:14:09 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 10:18:00 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -2434,7 +2434,8 @@ class FunctionNode {
24342434
}
24352435

24362436
const ast = Object.freeze(inParser.parse(`const parser_${ this.name } = ${ this.source };`, {
2437-
locations: true
2437+
locations: true,
2438+
ecmaVersion: 2020
24382439
}));
24392440
const functionAST = ast.body[0].declarations[0].init;
24402441
this.traceFunctionAST(functionAST);
@@ -10097,6 +10098,7 @@ class WebGLKernel extends GLKernel {
1009710098
if (index !== -1) {
1009810099
this.textureCache.splice(index, 1);
1009910100
}
10101+
if (!this.context) return;
1010010102
this.context.deleteTexture(texture);
1010110103
}
1010210104

@@ -10240,11 +10242,15 @@ class WebGLKernel extends GLKernel {
1024010242
gl.bufferSubData(gl.ARRAY_BUFFER, texCoordOffset, texCoords);
1024110243

1024210244
const aPosLoc = gl.getAttribLocation(this.program, 'aPos');
10243-
gl.enableVertexAttribArray(aPosLoc);
10244-
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
10245+
if (aPosLoc !== -1) {
10246+
gl.enableVertexAttribArray(aPosLoc);
10247+
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
10248+
}
1024510249
const aTexCoordLoc = gl.getAttribLocation(this.program, 'aTexCoord');
10246-
gl.enableVertexAttribArray(aTexCoordLoc);
10247-
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
10250+
if (aTexCoordLoc !== -1) {
10251+
gl.enableVertexAttribArray(aTexCoordLoc);
10252+
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
10253+
}
1024810254
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
1024910255

1025010256
let i = 0;
@@ -13578,10 +13584,11 @@ class GPU {
1357813584
}
1357913585
setTimeout(() => {
1358013586
try {
13581-
for (let i = 0; i < this.kernels.length; i++) {
13582-
this.kernels[i].destroy(true);
13587+
const kernels = this.kernels.slice();
13588+
for (let i = 0; i < kernels.length; i++) {
13589+
kernels[i].destroy(true);
1358313590
}
13584-
let firstKernel = this.kernels[0];
13591+
let firstKernel = kernels[0];
1358513592
if (firstKernel) {
1358613593
if (firstKernel.kernel) {
1358713594
firstKernel = firstKernel.kernel;
@@ -14451,7 +14458,7 @@ const utils = {
1445114458
if (!flattened) {
1445214459
flattened = settings.flattened = {};
1445314460
}
14454-
const ast = acorn.parse(source);
14461+
const ast = acorn.parse(source, { ecmaVersion: 2020 });
1445514462
const functionDependencies = [];
1445614463
let indent = 0;
1445714464

@@ -14790,7 +14797,7 @@ const utils = {
1479014797

1479114798
getMinifySafeName: (fn) => {
1479214799
try {
14793-
const ast = acorn.parse(`const value = ${fn.toString()}`);
14800+
const ast = acorn.parse(`const value = ${fn.toString()}`, { ecmaVersion: 2020 });
1479414801
const { init } = ast.body[0].declarations[0];
1479514802
return init.body.name || init.body.body[0].argument.name;
1479614803
} catch (e) {

‎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: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* GPU Accelerated JavaScript
66
*
77
* @version 2.19.8
8-
* @date Mon Jul 27 2026 01:14:09 GMT+0800 (Singapore Standard Time)
8+
* @date Mon Jul 27 2026 10:18:00 GMT+0800 (Singapore Standard Time)
99
*
1010
* @license MIT
1111
* The MIT License
@@ -7888,7 +7888,8 @@ class FunctionNode {
78887888
}
78897889

78907890
const ast = Object.freeze(inParser.parse(`const parser_${ this.name } = ${ this.source };`, {
7891-
locations: true
7891+
locations: true,
7892+
ecmaVersion: 2020
78927893
}));
78937894
const functionAST = ast.body[0].declarations[0].init;
78947895
this.traceFunctionAST(functionAST);
@@ -15551,6 +15552,7 @@ class WebGLKernel extends GLKernel {
1555115552
if (index !== -1) {
1555215553
this.textureCache.splice(index, 1);
1555315554
}
15555+
if (!this.context) return;
1555415556
this.context.deleteTexture(texture);
1555515557
}
1555615558

@@ -15694,11 +15696,15 @@ class WebGLKernel extends GLKernel {
1569415696
gl.bufferSubData(gl.ARRAY_BUFFER, texCoordOffset, texCoords);
1569515697

1569615698
const aPosLoc = gl.getAttribLocation(this.program, 'aPos');
15697-
gl.enableVertexAttribArray(aPosLoc);
15698-
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
15699+
if (aPosLoc !== -1) {
15700+
gl.enableVertexAttribArray(aPosLoc);
15701+
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
15702+
}
1569915703
const aTexCoordLoc = gl.getAttribLocation(this.program, 'aTexCoord');
15700-
gl.enableVertexAttribArray(aTexCoordLoc);
15701-
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
15704+
if (aTexCoordLoc !== -1) {
15705+
gl.enableVertexAttribArray(aTexCoordLoc);
15706+
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
15707+
}
1570215708
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
1570315709

1570415710
let i = 0;
@@ -19032,10 +19038,11 @@ class GPU {
1903219038
}
1903319039
setTimeout(() => {
1903419040
try {
19035-
for (let i = 0; i < this.kernels.length; i++) {
19036-
this.kernels[i].destroy(true);
19041+
const kernels = this.kernels.slice();
19042+
for (let i = 0; i < kernels.length; i++) {
19043+
kernels[i].destroy(true);
1903719044
}
19038-
let firstKernel = this.kernels[0];
19045+
let firstKernel = kernels[0];
1903919046
if (firstKernel) {
1904019047
if (firstKernel.kernel) {
1904119048
firstKernel = firstKernel.kernel;
@@ -19905,7 +19912,7 @@ const utils = {
1990519912
if (!flattened) {
1990619913
flattened = settings.flattened = {};
1990719914
}
19908-
const ast = acorn.parse(source);
19915+
const ast = acorn.parse(source, { ecmaVersion: 2020 });
1990919916
const functionDependencies = [];
1991019917
let indent = 0;
1991119918

@@ -20244,7 +20251,7 @@ const utils = {
2024420251

2024520252
getMinifySafeName: (fn) => {
2024620253
try {
20247-
const ast = acorn.parse(`const value = ${fn.toString()}`);
20254+
const ast = acorn.parse(`const value = ${fn.toString()}`, { ecmaVersion: 2020 });
2024820255
const { init } = ast.body[0].declarations[0];
2024920256
return init.body.name || init.body.body[0].argument.name;
2025020257
} catch (e) {

‎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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ class FunctionNode {
267267
}
268268

269269
const ast = Object.freeze(inParser.parse(`const parser_${ this.name } = ${ this.source };`, {
270-
locations: true
270+
locations: true,
271+
ecmaVersion: 2020
271272
}));
272273
// take out the function object, outside the var declarations
273274
const functionAST = ast.body[0].declarations[0].init;

‎src/backend/web-gl/kernel.js‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,10 @@ class WebGLKernel extends GLKernel {
438438
if (index !== -1) {
439439
this.textureCache.splice(index, 1);
440440
}
441+
// a Texture keeps a reference to the kernel that made it, so it can outlive
442+
// that kernel and call back here after destroy() has released the context.
443+
// The GL texture went with the context, so there is nothing left to free.
444+
if (!this.context) return;
441445
this.context.deleteTexture(texture);
442446
}
443447

@@ -580,12 +584,20 @@ class WebGLKernel extends GLKernel {
580584
gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertices);
581585
gl.bufferSubData(gl.ARRAY_BUFFER, texCoordOffset, texCoords);
582586

587+
// getAttribLocation returns -1 for an attribute the GLSL compiler removed —
588+
// aTexCoord goes when the fragment shader samples no texture — and passing
589+
// that on raises INVALID_VALUE on every draw. Software GL keeps the unused
590+
// attribute, so this only shows on real drivers.
583591
const aPosLoc = gl.getAttribLocation(this.program, 'aPos');
584-
gl.enableVertexAttribArray(aPosLoc);
585-
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
592+
if (aPosLoc !== -1) {
593+
gl.enableVertexAttribArray(aPosLoc);
594+
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);
595+
}
586596
const aTexCoordLoc = gl.getAttribLocation(this.program, 'aTexCoord');
587-
gl.enableVertexAttribArray(aTexCoordLoc);
588-
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
597+
if (aTexCoordLoc !== -1) {
598+
gl.enableVertexAttribArray(aTexCoordLoc);
599+
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 0, texCoordOffset);
600+
}
589601
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
590602

591603
let i = 0;

‎src/gpu.js‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,16 @@ class GPU {
551551
// if webGl is created and destroyed in the same run loop.
552552
setTimeout(() => {
553553
try {
554-
for (let i = 0; i < this.kernels.length; i++) {
555-
this.kernels[i].destroy(true); // remove canvas if exists
554+
// kernel.destroy() splices itself out of this.kernels, so walk a copy:
555+
// mutating the list being indexed skipped every other kernel, and left
556+
// this.kernels[0] undefined below, which meant a single-kernel GPU
557+
// never released its WebGL context at all
558+
const kernels = this.kernels.slice();
559+
for (let i = 0; i < kernels.length; i++) {
560+
kernels[i].destroy(true); // remove canvas if exists
556561
}
557562
// all kernels are associated with one context, go ahead and take care of it here
558-
let firstKernel = this.kernels[0];
563+
let firstKernel = kernels[0];
559564
if (firstKernel) {
560565
// if it is shortcut
561566
if (firstKernel.kernel) {

‎src/utils.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,9 @@ const utils = {
630630
if (!flattened) {
631631
flattened = settings.flattened = {};
632632
}
633-
const ast = acorn.parse(source);
633+
// acorn 8 requires ecmaVersion and warns when it is absent; 2020 is the
634+
// version it defaults to, so this pins current behaviour rather than changing it
635+
const ast = acorn.parse(source, { ecmaVersion: 2020 });
634636
const functionDependencies = [];
635637
let indent = 0;
636638

@@ -985,7 +987,7 @@ const utils = {
985987

986988
getMinifySafeName: (fn) => {
987989
try {
988-
const ast = acorn.parse(`const value = ${fn.toString()}`);
990+
const ast = acorn.parse(`const value = ${fn.toString()}`, { ecmaVersion: 2020 });
989991
const { init } = ast.body[0].declarations[0];
990992
return init.body.name || init.body.body[0].argument.name;
991993
} catch (e) {

‎test/all-template.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html>
33
<head>
44
<meta charset="utf-8">
5+
<link rel="icon" href="data:,">
56
<title>GPU.JS : Test All</title>
67
<link rel="stylesheet" href="../node_modules/qunit/qunit/qunit.css">
78

‎test/all.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html>
33
<head>
44
<meta charset="utf-8">
5+
<link rel="icon" href="data:,">
56
<title>GPU.JS : Test All</title>
67
<link rel="stylesheet" href="../node_modules/qunit/qunit/qunit.css">
78

@@ -158,6 +159,7 @@
158159
<script type="module" src="internal/context-inheritance.js"></script>
159160
<script type="module" src="internal/deep-types.js"></script>
160161
<script type="module" src="internal/deprecated.js"></script>
162+
<script type="module" src="internal/destroy-releases-resources.js"></script>
161163
<script type="module" src="internal/different-texture-cloning.js"></script>
162164
<script type="module" src="internal/dynamic-output-texture-cache.js"></script>
163165
<script type="module" src="internal/function-builder.js"></script>

0 commit comments

Comments
 (0)