@origingame/engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/web/origin-engine.d.ts +44 -5
  2. package/dist/web/origin-engine.js +279 -19
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
  5. package/dist/web-publish/origin-engine-lanes.js +289 -48
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +44 -5
  8. package/dist/web-publish/origin-engine.js +279 -38
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
  11. package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +44 -5
  14. package/dist/web-publish-2d/origin-engine.js +279 -38
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
  17. package/knowledge/README.md +2 -1
  18. package/knowledge/guides/three-to-og.md +393 -0
  19. package/knowledge/knowledge.json +2 -1
  20. package/knowledge/looks/catalogue.json +123 -2
  21. package/knowledge/package.json +3 -2
  22. package/knowledge/scaffold/origin-engine.json +2 -2
  23. package/knowledge/scaffold/template/package.json +1 -1
  24. package/knowledge/scaffold/template/project.ogt +1 -1
  25. package/knowledge/scripts/validate.ts +209 -27
  26. package/knowledge/skills/og-audio/SKILL.md +33 -7
  27. package/knowledge/skills/og-build/SKILL.md +66 -7
  28. package/knowledge/skills/og-core/SKILL.md +24 -8
  29. package/knowledge/skills/og-feel/SKILL.md +11 -3
  30. package/knowledge/skills/og-game/SKILL.md +680 -51
  31. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  32. package/knowledge/skills/og-model/SKILL.md +374 -0
  33. package/knowledge/skills/og-platform-sdk/SKILL.md +7 -1
  34. package/knowledge/skills/og-taste/SKILL.md +7 -0
  35. package/knowledge/skills/og-ui/SKILL.md +39 -8
  36. package/knowledge/wiki/ai.md +20 -20
  37. package/knowledge/wiki/anim.md +537 -63
  38. package/knowledge/wiki/atmosphere.md +13 -13
  39. package/knowledge/wiki/audio.md +160 -20
  40. package/knowledge/wiki/camera.md +13 -13
  41. package/knowledge/wiki/commands.md +11 -11
  42. package/knowledge/wiki/debug.md +13 -13
  43. package/knowledge/wiki/game.md +8 -8
  44. package/knowledge/wiki/i18n.md +7 -7
  45. package/knowledge/wiki/index.md +81 -3
  46. package/knowledge/wiki/input.md +47 -47
  47. package/knowledge/wiki/load.md +83 -57
  48. package/knowledge/wiki/math.md +102 -102
  49. package/knowledge/wiki/nav.md +34 -34
  50. package/knowledge/wiki/net.md +10 -10
  51. package/knowledge/wiki/path.md +14 -14
  52. package/knowledge/wiki/physics.md +111 -100
  53. package/knowledge/wiki/platform.md +44 -44
  54. package/knowledge/wiki/probe.md +86 -86
  55. package/knowledge/wiki/reflect.md +30 -18
  56. package/knowledge/wiki/render.md +1243 -176
  57. package/knowledge/wiki/rng.md +14 -14
  58. package/knowledge/wiki/shapes.md +727 -0
  59. package/knowledge/wiki/stream.md +15 -15
  60. package/knowledge/wiki/system.md +8 -8
  61. package/knowledge/wiki/systems.md +2 -2
  62. package/knowledge/wiki/terrain.md +24 -20
  63. package/knowledge/wiki/tilemap.md +13 -13
  64. package/knowledge/wiki/time.md +4 -4
  65. package/knowledge/wiki/timeline.md +14 -14
  66. package/knowledge/wiki/transform.md +8 -8
  67. package/knowledge/wiki/tween.md +17 -17
  68. package/knowledge/wiki/ui.md +245 -269
  69. package/knowledge/wiki/video.md +5 -5
  70. package/knowledge/wiki/world.md +36 -36
  71. package/package.json +4 -4
  72. package/schema/api.schema.json +2238 -81
  73. package/shell/page.ts +4 -0
  74. package/shell/platform.ts +5 -0
  75. package/shell/player.ts +8 -2
  76. package/shell/transport.ts +6 -0
  77. package/src/generated/abi.ts +40 -1
  78. package/src/geom.ts +260 -0
  79. package/src/index.ts +19 -1
  80. package/src/node/device.ts +5 -2
  81. package/src/node/playtest-run.ts +1 -0
  82. package/src/node/polish.ts +34 -15
  83. package/src/node/run.ts +23 -6
  84. package/src/platform-local.ts +2 -0
  85. package/src/playtest.ts +8 -0
  86. package/src/runtime.ts +88 -6
  87. package/src/ui-kit/settings.ts +9 -0
  88. package/src/web/gl-state.ts +907 -0
  89. package/src/web/shell.ts +1 -1
  90. package/src/web/worker.ts +8 -2
  91. package/types/bindings.json +1222 -453
  92. package/types/index.d.ts +1868 -315
@@ -0,0 +1,907 @@
1
+ // The WebGL2 tier's state filter: a mirror of the context's state in front
2
+ // of the WebGL2RenderingContext wgpu's GL backend draws through, which drops
3
+ // every call that sets a value the context already holds and defers the
4
+ // enables (capabilities, vertex attribute arrays), the active texture unit,
5
+ // the program, the framebuffer bindings and the generic buffer bindings to
6
+ // the next call that reads them.
7
+ //
8
+ // Why: wgpu-hal's GLES backend re-states everything it may need on every
9
+ // command buffer, pass and draw (seven capabilities reset per command
10
+ // buffer, every attachment point detached per pass, every vertex attribute
11
+ // re-specified and every bound texture's base and max level set again per
12
+ // draw, every attribute array disabled after each pass). On the reference
13
+ // scene that is about 3,800 WebGL calls a frame for 41 draws; each one crosses
14
+ // into the browser's WebGL implementation and its GPU process, which the
15
+ // WebGPU tier never pays. tests/evidence/r4/perf-webgl2/NOTES.md has the
16
+ // accounting.
17
+ //
18
+ // Exact by construction: a call is dropped only when the mirror holds the
19
+ // value it would set, and a deferred value is set before any call that could
20
+ // read it (a draw, a clear, a blit, an upload, a query), so every such call
21
+ // acts on the state it would have acted on unfiltered: frames are identical.
22
+ // What the mirror does not know (at the start, after a call it does not
23
+ // model, after an extension's call) is unknown, and a call on unknown state
24
+ // is forwarded. The hot paths take fixed arguments and allocate nothing (a
25
+ // dropped call costs less than the WebGL call it replaces); a call is
26
+ // forwarded with as many arguments as it was given, as WebGL picks an
27
+ // overload by their count.
28
+
29
+ /** Calls the filter received, and those it passed to the context. */
30
+ export interface GlStateCounts {
31
+ calls: number;
32
+ forwarded: number;
33
+ }
34
+
35
+ type Gl = WebGL2RenderingContext;
36
+ type Fn = (...a: unknown[]) => unknown;
37
+ type Obj = object | string | null;
38
+
39
+ const FRAMEBUFFER = 0x8d40;
40
+ const READ_FRAMEBUFFER = 0x8ca8;
41
+ const DRAW_FRAMEBUFFER = 0x8ca9;
42
+ const DEPTH_ATTACHMENT = 0x8d00;
43
+ const STENCIL_ATTACHMENT = 0x8d20;
44
+ const DEPTH_STENCIL_ATTACHMENT = 0x821a;
45
+ const ELEMENT_ARRAY_BUFFER = 0x8893;
46
+ const ARRAY_BUFFER = 0x8892;
47
+ const TRANSFORM_FEEDBACK_BUFFER = 0x8c8e;
48
+ const TEXTURE0 = 0x84c0;
49
+ const FRONT_AND_BACK = 0x0408;
50
+ const FRONT = 0x0404;
51
+ const BACK = 0x0405;
52
+ /** SCISSOR_TEST, RASTERIZER_DISCARD, DITHER: the capabilities a clear reads. */
53
+ const CLEAR_CAPS = [0x0c11, 0x8c89, 0x0bd0];
54
+
55
+ /** Unknown: a value no argument can equal. */
56
+ const UNKNOWN: unique symbol = Symbol("unknown");
57
+ /** Nothing asked: a deferred binding with no call waiting to set it. */
58
+ const PENDING: unique symbol = Symbol("nothing asked");
59
+ type Known<T> = T | typeof UNKNOWN;
60
+
61
+ /** Calls that change no state the mirror holds and read none it defers. */
62
+ const INDEPENDENT = [
63
+ "uniformBlockBinding", "fenceSync", "deleteSync", "getSyncParameter", "clientWaitSync", "flush", "createBuffer",
64
+ "createFramebuffer", "createRenderbuffer", "createSampler", "createTexture", "createVertexArray", "createQuery",
65
+ "createShader", "createProgram", "shaderSource", "compileShader", "getShaderParameter", "getShaderInfoLog",
66
+ "attachShader", "detachShader", "deleteShader", "linkProgram", "getProgramParameter", "getProgramInfoLog",
67
+ "getUniformLocation", "getUniformBlockIndex", "getAttribLocation", "bindAttribLocation", "samplerParameteri",
68
+ "samplerParameterf", "bindRenderbuffer", "renderbufferStorage", "renderbufferStorageMultisample",
69
+ "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f", "vertexAttrib1fv", "vertexAttrib2fv",
70
+ "vertexAttrib3fv", "vertexAttrib4fv", "vertexAttribI4i", "vertexAttribI4ui", "vertexAttribI4iv", "vertexAttribI4uiv",
71
+ ];
72
+ /** Clears: they read no deferred state but CLEAR_CAPS and the draw framebuffer. */
73
+ const CLEARS = ["clear", "clearBufferfv", "clearBufferiv", "clearBufferuiv", "clearBufferfi"];
74
+ /**
75
+ * Calls that change no state the mirror holds but may read any it defers:
76
+ * they run after it is set. A call in none of these lists and not modelled
77
+ * below forgets the whole mirror after it runs.
78
+ */
79
+ const OBSERVING = [
80
+ "blitFramebuffer", "readPixels", "getBufferSubData", "texImage2D", "texImage3D", "texSubImage2D", "texSubImage3D",
81
+ "texStorage2D", "texStorage3D", "compressedTexImage2D", "compressedTexImage3D", "compressedTexSubImage2D",
82
+ "compressedTexSubImage3D", "copyTexImage2D", "copyTexSubImage2D", "copyTexSubImage3D", "generateMipmap",
83
+ "getTexParameter", "getParameter", "getIndexedParameter", "getError", "isEnabled", "getVertexAttrib",
84
+ "getVertexAttribOffset", "getFramebufferAttachmentParameter", "getRenderbufferParameter", "getBufferParameter",
85
+ "checkFramebufferStatus", "getSupportedExtensions", "getContextAttributes", "isContextLost", "finish", "beginQuery",
86
+ "endQuery", "getQuery", "getQueryParameter", "getActiveUniform", "getActiveAttrib", "getActiveUniformBlockParameter",
87
+ "getActiveUniformBlockName", "getActiveUniforms", "getUniformIndices", "getUniform", "getShaderSource",
88
+ "getShaderPrecisionFormat", "getAttachedShaders", "getInternalformatParameter", "getSamplerParameter", "isBuffer",
89
+ "isFramebuffer", "isProgram", "isRenderbuffer", "isShader", "isTexture", "isVertexArray", "isSampler", "isSync",
90
+ "isQuery", "validateProgram", "deleteQuery", "hint", "lineWidth", "sampleCoverage", "waitSync", "deleteProgram",
91
+ ];
92
+ /** Draws: they read the capabilities, the attribute arrays, the program and the draw framebuffer. */
93
+ const DRAWS = ["drawArrays", "drawElements", "drawArraysInstanced", "drawElementsInstanced", "drawRangeElements"];
94
+
95
+ /** What one attachment point holds: kind 0 none, 1 a 2D image, 2 a layer, 3 a renderbuffer. */
96
+ interface Attachment {
97
+ kind: number;
98
+ obj: Obj;
99
+ a: unknown;
100
+ b: unknown;
101
+ }
102
+ const NONE: Attachment = { kind: 0, obj: null, a: 0, b: 0 };
103
+
104
+ /** The state one vertex array object holds, per attribute location. */
105
+ class VaoState {
106
+ elements: Known<Obj> = UNKNOWN;
107
+ /** Arrays enabled as the context has them (undefined: unknown), and as asked. */
108
+ readonly enabled: (boolean | undefined)[] = [];
109
+ readonly wanted: (boolean | undefined)[] = [];
110
+ /** Locations with a wanted value. */
111
+ readonly dirty: number[] = [];
112
+ /** Pointers: buffer (UNKNOWN: unknown), integer, size, type, normalised, stride, offset. */
113
+ readonly buffer: Known<Obj>[] = [];
114
+ readonly integer: boolean[] = [];
115
+ readonly size: unknown[] = [];
116
+ readonly type: unknown[] = [];
117
+ readonly norm: unknown[] = [];
118
+ readonly stride: unknown[] = [];
119
+ readonly offset: unknown[] = [];
120
+ readonly divisor: unknown[] = [];
121
+ /** Forgets every pointer that reads `obj` (it was deleted). */
122
+ drop(obj: unknown): void {
123
+ if (this.elements === obj) this.elements = UNKNOWN;
124
+ for (let i = 0; i < this.buffer.length; i++) if (this.buffer[i] === obj) this.buffer[i] = UNKNOWN;
125
+ }
126
+ }
127
+
128
+ /** A fixed-function value: the last arguments set (UNKNOWN: unknown). */
129
+ class Value {
130
+ a: unknown = UNKNOWN;
131
+ b: unknown = UNKNOWN;
132
+ c: unknown = UNKNOWN;
133
+ d: unknown = UNKNOWN;
134
+ /** Whether it holds (a, b, c, d); if not, it does from now on. */
135
+ holds(a: unknown, b: unknown, c: unknown, d: unknown): boolean {
136
+ if (this.a === a && this.b === b && this.c === c && this.d === d) return true;
137
+ this.a = a;
138
+ this.b = b;
139
+ this.c = c;
140
+ this.d = d;
141
+ return false;
142
+ }
143
+ forget(): void {
144
+ this.a = UNKNOWN;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Puts the filter on `gl`'s own methods (shadowing its prototype's) and
150
+ * returns its counts. Install it before anything else draws through `gl`.
151
+ */
152
+ export function filterGlState(gl: Gl): GlStateCounts {
153
+ const counts: GlStateCounts = { calls: 0, forwarded: 0 };
154
+ const raw = new Map<string, Fn>();
155
+ for (let p = Object.getPrototypeOf(gl) as object | null; p && p !== Object.prototype; p = Object.getPrototypeOf(p) as object | null) {
156
+ for (const k of Object.getOwnPropertyNames(p)) {
157
+ const d = Object.getOwnPropertyDescriptor(p, k);
158
+ if (k !== "constructor" && !raw.has(k) && d && typeof d.value === "function") raw.set(k, d.value as Fn);
159
+ }
160
+ }
161
+ const noop: Fn = () => undefined;
162
+ const fn = (name: string): Fn => raw.get(name) ?? noop;
163
+ // The filter's methods live on a prototype put between the context and
164
+ // its own: the context keeps no own properties, so its method lookups
165
+ // stay as fast as the unfiltered ones.
166
+ const methods = Object.create(Object.getPrototypeOf(gl) as object) as Record<string, unknown>;
167
+ const define = (name: string, f: Fn) => {
168
+ if (raw.has(name)) Object.defineProperty(methods, name, { value: f, configurable: true, writable: true });
169
+ };
170
+
171
+ // ── the mirror ────────────────────────────────────────────────────────
172
+ // Capabilities by a small index (capIndex): as the context has them
173
+ // (undefined: unknown), as asked (undefined: nothing asked), and those asked.
174
+ const capIndex = new Map<number, number>();
175
+ const capOf: number[] = [];
176
+ const caps: (boolean | undefined)[] = [];
177
+ const wantedCaps: (boolean | undefined)[] = [];
178
+ const asked: number[] = [];
179
+ const indexOf = (cap: number): number => {
180
+ let i = capIndex.get(cap);
181
+ if (i === undefined) {
182
+ capIndex.set(cap, (i = capOf.length));
183
+ capOf.push(cap);
184
+ }
185
+ return i;
186
+ };
187
+ const clearCaps = CLEAR_CAPS.map(indexOf);
188
+ /** Active unit index (TEXTURE0 = 0) as the context has it, and as asked. */
189
+ let unit: Known<number> = UNKNOWN;
190
+ let wantedUnit: Known<number> = UNKNOWN;
191
+ /** Texture bound per target, by unit index (undefined: unknown). */
192
+ const textures = new Map<number, (Obj | undefined)[]>();
193
+ const texParams = new Map<Obj, Map<number, unknown>>();
194
+ const samplers: (Obj | undefined)[] = [];
195
+ /** Buffer bound per generic target (not ELEMENT_ARRAY_BUFFER: the VAO's). */
196
+ const buffers = new Map<number, Obj>();
197
+ /** Indexed bindings per target, by index: buffer, offset, size (base: UNKNOWN offset). */
198
+ const ranges = new Map<number, Value[]>();
199
+ // Bindings as the context has them, and as asked (PENDING: nothing asked):
200
+ // the program, the framebuffers and the generic buffer targets are bound
201
+ // when a call reads them.
202
+ let program: Known<Obj> = UNKNOWN;
203
+ let wantedProgram: Obj | typeof PENDING = PENDING;
204
+ let drawFb: Known<Obj> = UNKNOWN;
205
+ let readFb: Known<Obj> = UNKNOWN;
206
+ let wantedDraw: Obj | typeof PENDING = PENDING;
207
+ let wantedRead: Obj | typeof PENDING = PENDING;
208
+ const wantedBuffers = new Map<number, Obj>();
209
+ const attachments = new Map<Obj, Map<number, Attachment>>();
210
+ const drawBuffersOf = new Map<Obj, number[]>();
211
+ const values = new Map<string, Value>();
212
+ const pixelStore = new Map<number, unknown>();
213
+ let vao: Known<Obj> = UNKNOWN;
214
+ const vaos = new Map<Obj, VaoState>();
215
+ /** The bound VAO's state (which one it is may be unknown: `vao`). */
216
+ let current = new VaoState();
217
+ const value = (key: string): Value => {
218
+ let v = values.get(key);
219
+ if (!v) values.set(key, (v = new Value()));
220
+ return v;
221
+ };
222
+
223
+ const rEnable = fn("enable");
224
+ const rDisable = fn("disable");
225
+ const rActiveTexture = fn("activeTexture");
226
+ const rEnableAttrib = fn("enableVertexAttribArray");
227
+ const rDisableAttrib = fn("disableVertexAttribArray");
228
+ /** Sets the capability `i` asked for, if one was. */
229
+ const flushCap = (i: number) => {
230
+ const on = wantedCaps[i];
231
+ if (on === undefined) return;
232
+ wantedCaps[i] = undefined;
233
+ if (caps[i] !== on) {
234
+ counts.forwarded++;
235
+ (on ? rEnable : rDisable).call(gl, capOf[i]);
236
+ caps[i] = on;
237
+ }
238
+ };
239
+ /** Sets the capabilities asked for. */
240
+ const flushCaps = () => {
241
+ for (let k = 0; k < asked.length; k++) flushCap(asked[k]!);
242
+ asked.length = 0;
243
+ };
244
+ /** Sets those a clear reads. */
245
+ const flushClearCaps = () => {
246
+ for (let k = 0; k < clearCaps.length; k++) flushCap(clearCaps[k]!);
247
+ };
248
+ const flushUnit = () => {
249
+ if (wantedUnit !== unit && wantedUnit !== UNKNOWN) {
250
+ counts.forwarded++;
251
+ rActiveTexture.call(gl, TEXTURE0 + wantedUnit);
252
+ unit = wantedUnit;
253
+ }
254
+ };
255
+ const flushAttribs = () => {
256
+ const v = current;
257
+ const dirty = v.dirty;
258
+ if (dirty.length === 0) return;
259
+ for (let i = 0; i < dirty.length; i++) {
260
+ const loc = dirty[i]!;
261
+ const on = v.wanted[loc];
262
+ if (on === undefined) continue;
263
+ v.wanted[loc] = undefined;
264
+ if (v.enabled[loc] !== on) {
265
+ counts.forwarded++;
266
+ (on ? rEnableAttrib : rDisableAttrib).call(gl, loc);
267
+ v.enabled[loc] = on;
268
+ }
269
+ }
270
+ dirty.length = 0;
271
+ };
272
+ const rUseProgram = fn("useProgram");
273
+ const flushProgram = () => {
274
+ if (wantedProgram === PENDING) return;
275
+ const p = wantedProgram;
276
+ wantedProgram = PENDING;
277
+ if (program !== p) {
278
+ counts.forwarded++;
279
+ rUseProgram.call(gl, p);
280
+ program = p;
281
+ }
282
+ };
283
+ const rBindFramebuffer = fn("bindFramebuffer");
284
+ /** Binds the framebuffers asked for (`draw`, `read`: which of them a call reads). */
285
+ const flushFramebuffers = (draw: boolean, read: boolean) => {
286
+ const d = draw && wantedDraw !== PENDING && wantedDraw !== drawFb ? wantedDraw : PENDING;
287
+ const r = read && wantedRead !== PENDING && wantedRead !== readFb ? wantedRead : PENDING;
288
+ if (draw) wantedDraw = PENDING;
289
+ if (read) wantedRead = PENDING;
290
+ if (d !== PENDING && r !== PENDING && d === r) {
291
+ counts.forwarded++;
292
+ rBindFramebuffer.call(gl, FRAMEBUFFER, d);
293
+ drawFb = readFb = d;
294
+ return;
295
+ }
296
+ if (d !== PENDING) {
297
+ counts.forwarded++;
298
+ rBindFramebuffer.call(gl, DRAW_FRAMEBUFFER, d);
299
+ drawFb = d;
300
+ }
301
+ if (r !== PENDING) {
302
+ counts.forwarded++;
303
+ rBindFramebuffer.call(gl, READ_FRAMEBUFFER, r);
304
+ readFb = r;
305
+ }
306
+ };
307
+ const rBindBuffer = fn("bindBuffer");
308
+ /** Binds the buffer asked for at `target`. */
309
+ const flushBuffer = (target: number) => {
310
+ if (wantedBuffers.size === 0 || !wantedBuffers.has(target)) return;
311
+ const buf = wantedBuffers.get(target)!;
312
+ wantedBuffers.delete(target);
313
+ if (!buffers.has(target) || buffers.get(target) !== buf) {
314
+ counts.forwarded++;
315
+ rBindBuffer.call(gl, target, buf);
316
+ buffers.set(target, buf);
317
+ }
318
+ };
319
+ const flushBuffers = () => {
320
+ for (const target of wantedBuffers.keys()) flushBuffer(target);
321
+ };
322
+ /** What a draw reads. */
323
+ const flushDraw = () => {
324
+ flushCaps();
325
+ flushAttribs();
326
+ flushProgram();
327
+ flushFramebuffers(true, false);
328
+ };
329
+ const flushAll = () => {
330
+ flushCaps();
331
+ flushUnit();
332
+ flushAttribs();
333
+ flushProgram();
334
+ flushFramebuffers(true, true);
335
+ flushBuffers();
336
+ };
337
+ /** The framebuffer bound to `target` once the calls asked so far are made. */
338
+ const framebufferAt = (target: unknown): Known<Obj> =>
339
+ target === READ_FRAMEBUFFER ? (wantedRead !== PENDING ? wantedRead : readFb) : wantedDraw !== PENDING ? wantedDraw : drawFb;
340
+ /** The buffer bound to `target` once the calls asked so far are made. */
341
+ const bufferAt = (target: number): Known<Obj> => (wantedBuffers.has(target) ? wantedBuffers.get(target)! : buffers.has(target) ? buffers.get(target)! : UNKNOWN);
342
+
343
+ /** Forget everything: the context may hold anything. */
344
+ const forget = () => {
345
+ flushAll();
346
+ caps.length = 0;
347
+ unit = UNKNOWN;
348
+ textures.clear();
349
+ texParams.clear();
350
+ samplers.length = 0;
351
+ buffers.clear();
352
+ ranges.clear();
353
+ program = UNKNOWN;
354
+ drawFb = UNKNOWN;
355
+ readFb = UNKNOWN;
356
+ attachments.clear();
357
+ drawBuffersOf.clear();
358
+ for (const v of values.values()) v.forget();
359
+ pixelStore.clear();
360
+ vao = UNKNOWN;
361
+ vaos.clear();
362
+ current = new VaoState();
363
+ };
364
+ // A restored context starts from its defaults with new objects: nothing
365
+ // asked of the lost one is set on it.
366
+ const lost = () => {
367
+ wantedCaps.length = 0;
368
+ asked.length = 0;
369
+ wantedProgram = wantedDraw = wantedRead = PENDING;
370
+ wantedBuffers.clear();
371
+ wantedUnit = UNKNOWN;
372
+ current.dirty.length = 0;
373
+ forget();
374
+ };
375
+ const canvas = gl.canvas as ((OffscreenCanvas | HTMLCanvasElement) & EventTarget) | undefined;
376
+ canvas?.addEventListener?.("webglcontextlost", lost);
377
+ canvas?.addEventListener?.("webglcontextrestored", lost);
378
+
379
+ /** Forgets every binding to `obj` the context drops when it is deleted, and every value naming it. */
380
+ const deleted = (obj: unknown) => {
381
+ for (const list of textures.values()) for (let i = 0; i < list.length; i++) if (list[i] === obj) list[i] = null;
382
+ for (let i = 0; i < samplers.length; i++) if (samplers[i] === obj) samplers[i] = null;
383
+ for (const [t, b] of buffers) if (b === obj) buffers.set(t, null);
384
+ for (const list of ranges.values()) for (const r of list) if (r && r.a === obj) r.forget();
385
+ for (const map of attachments.values()) for (const [k, v] of map) if (v.obj === obj) map.delete(k);
386
+ for (const s of vaos.values()) s.drop(obj);
387
+ current.drop(obj);
388
+ texParams.delete(obj as Obj);
389
+ if (drawFb === obj) drawFb = null;
390
+ if (readFb === obj) readFb = null;
391
+ if (obj !== null) {
392
+ attachments.delete(obj as Obj);
393
+ drawBuffersOf.delete(obj as Obj);
394
+ }
395
+ };
396
+
397
+ // ── deferred ──────────────────────────────────────────────────────────
398
+ const want = (cap: number, on: boolean) => {
399
+ const i = indexOf(cap);
400
+ if (wantedCaps[i] === undefined) asked.push(i);
401
+ wantedCaps[i] = on;
402
+ };
403
+ define("enable", (cap) => {
404
+ counts.calls++;
405
+ want(cap as number, true);
406
+ });
407
+ define("disable", (cap) => {
408
+ counts.calls++;
409
+ want(cap as number, false);
410
+ });
411
+ define("activeTexture", (u) => {
412
+ counts.calls++;
413
+ wantedUnit = (u as number) - TEXTURE0;
414
+ });
415
+ const wantAttrib = (loc: number, on: boolean) => {
416
+ const v = current;
417
+ if (v.wanted[loc] === undefined) v.dirty.push(loc);
418
+ v.wanted[loc] = on;
419
+ };
420
+ define("enableVertexAttribArray", (loc) => {
421
+ counts.calls++;
422
+ wantAttrib(loc as number, true);
423
+ });
424
+ define("disableVertexAttribArray", (loc) => {
425
+ counts.calls++;
426
+ wantAttrib(loc as number, false);
427
+ });
428
+
429
+ // ── textures and samplers ─────────────────────────────────────────────
430
+ const rBindTexture = fn("bindTexture");
431
+ define("bindTexture", (target, tex) => {
432
+ counts.calls++;
433
+ let list = textures.get(target as number);
434
+ if (wantedUnit !== UNKNOWN) {
435
+ if (list && list[wantedUnit] === tex) return;
436
+ if (!list) textures.set(target as number, (list = []));
437
+ }
438
+ flushUnit();
439
+ if (wantedUnit !== UNKNOWN) list![wantedUnit] = tex as Obj;
440
+ counts.forwarded++;
441
+ return rBindTexture.call(gl, target, tex);
442
+ });
443
+ const rTexParameteri = fn("texParameteri");
444
+ define("texParameteri", (target, pname, v) => {
445
+ counts.calls++;
446
+ const tex = wantedUnit === UNKNOWN ? undefined : textures.get(target as number)?.[wantedUnit];
447
+ const params = tex ? texParams.get(tex) : undefined;
448
+ if (params && params.get(pname as number) === v) return;
449
+ flushUnit();
450
+ if (tex) {
451
+ if (params) params.set(pname as number, v);
452
+ else texParams.set(tex, new Map([[pname as number, v]]));
453
+ } else if (tex === undefined) {
454
+ // Bound to an unknown texture: any one's parameters may change.
455
+ texParams.clear();
456
+ }
457
+ counts.forwarded++;
458
+ return rTexParameteri.call(gl, target, pname, v);
459
+ });
460
+ // Not mirrored: it forgets the parameter it sets (every texture's
461
+ // parameters, when which one is bound is unknown).
462
+ const rTexParameterf = fn("texParameterf");
463
+ define("texParameterf", (target, pname, v) => {
464
+ counts.calls++;
465
+ const tex = wantedUnit === UNKNOWN ? undefined : textures.get(target as number)?.[wantedUnit];
466
+ flushUnit();
467
+ if (tex) texParams.get(tex)?.delete(pname as number);
468
+ else if (tex === undefined) texParams.clear();
469
+ counts.forwarded++;
470
+ return rTexParameterf.call(gl, target, pname, v);
471
+ });
472
+ const rBindSampler = fn("bindSampler");
473
+ define("bindSampler", (u, s) => {
474
+ counts.calls++;
475
+ if (samplers[u as number] === s) return;
476
+ samplers[u as number] = s as Obj;
477
+ counts.forwarded++;
478
+ return rBindSampler.call(gl, u, s);
479
+ });
480
+
481
+ // ── buffers ───────────────────────────────────────────────────────────
482
+ define("bindBuffer", (target, buf) => {
483
+ counts.calls++;
484
+ if (target !== ELEMENT_ARRAY_BUFFER) {
485
+ // Deferred: bound when a call reads the target.
486
+ wantedBuffers.set(target as number, buf as Obj);
487
+ return;
488
+ }
489
+ if (current.elements === buf) return;
490
+ current.elements = buf as Obj;
491
+ counts.forwarded++;
492
+ return rBindBuffer.call(gl, target, buf);
493
+ });
494
+ // The calls that read a generic target's buffer.
495
+ // (Overloaded: forwarded with the arguments as given.)
496
+ for (const name of ["bufferData", "bufferSubData"]) {
497
+ const r = fn(name);
498
+ define(name, (...a) => {
499
+ counts.calls++;
500
+ flushBuffer(a[0] as number);
501
+ counts.forwarded++;
502
+ return r.apply(gl, a);
503
+ });
504
+ }
505
+ const rCopyBufferSubData = fn("copyBufferSubData");
506
+ define("copyBufferSubData", (read, write, a, b, c) => {
507
+ counts.calls++;
508
+ flushBuffer(read as number);
509
+ flushBuffer(write as number);
510
+ counts.forwarded++;
511
+ return rCopyBufferSubData.call(gl, read, write, a, b, c);
512
+ });
513
+ /** An indexed binding; it also binds the generic target. */
514
+ const range = (target: number, index: number, buf: unknown, offset: unknown, size: unknown): boolean => {
515
+ let list = ranges.get(target);
516
+ if (!list) ranges.set(target, (list = []));
517
+ let r = list[index];
518
+ if (!r) list[index] = r = new Value();
519
+ // A binding asked of the generic target before is overridden.
520
+ wantedBuffers.delete(target);
521
+ const generic = buffers.get(target) === buf && buffers.has(target);
522
+ const held = r.holds(buf, offset, size, 0);
523
+ buffers.set(target, buf as Obj);
524
+ return held && generic;
525
+ };
526
+ const rBindBufferRange = fn("bindBufferRange");
527
+ define("bindBufferRange", (target, index, buf, offset, size) => {
528
+ counts.calls++;
529
+ if (range(target as number, index as number, buf, offset, size)) return;
530
+ counts.forwarded++;
531
+ return rBindBufferRange.call(gl, target, index, buf, offset, size);
532
+ });
533
+ const rBindBufferBase = fn("bindBufferBase");
534
+ define("bindBufferBase", (target, index, buf) => {
535
+ counts.calls++;
536
+ if (range(target as number, index as number, buf, "base", "base")) return;
537
+ counts.forwarded++;
538
+ return rBindBufferBase.call(gl, target, index, buf);
539
+ });
540
+ const rBindTransformFeedback = fn("bindTransformFeedback");
541
+ define("bindTransformFeedback", (target, tf) => {
542
+ counts.calls++;
543
+ flushAll();
544
+ // The indexed transform feedback bindings are the object's.
545
+ ranges.delete(TRANSFORM_FEEDBACK_BUFFER);
546
+ buffers.delete(TRANSFORM_FEEDBACK_BUFFER);
547
+ wantedBuffers.delete(TRANSFORM_FEEDBACK_BUFFER);
548
+ counts.forwarded++;
549
+ return rBindTransformFeedback.call(gl, target, tf);
550
+ });
551
+ for (const name of ["deleteTexture", "deleteBuffer", "deleteFramebuffer", "deleteRenderbuffer", "deleteSampler"]) {
552
+ const r = fn(name);
553
+ define(name, (obj) => {
554
+ counts.calls++;
555
+ flushAll();
556
+ deleted(obj);
557
+ counts.forwarded++;
558
+ return r.call(gl, obj);
559
+ });
560
+ }
561
+
562
+ // ── vertex arrays and attributes ──────────────────────────────────────
563
+ const rBindVertexArray = fn("bindVertexArray");
564
+ define("bindVertexArray", (v) => {
565
+ counts.calls++;
566
+ if (vao === v) return;
567
+ flushAttribs();
568
+ vao = v as Obj;
569
+ let s = vaos.get(vao);
570
+ if (!s) vaos.set(vao, (s = new VaoState()));
571
+ current = s;
572
+ counts.forwarded++;
573
+ return rBindVertexArray.call(gl, v);
574
+ });
575
+ const rDeleteVertexArray = fn("deleteVertexArray");
576
+ define("deleteVertexArray", (v) => {
577
+ counts.calls++;
578
+ flushAttribs();
579
+ if (v !== null) vaos.delete(v as Obj);
580
+ // Deleting the bound one binds the default; which one is bound may be unknown.
581
+ if (vao === v) {
582
+ vao = null;
583
+ let s = vaos.get(null);
584
+ if (!s) vaos.set(null, (s = new VaoState()));
585
+ current = s;
586
+ } else if (vao === UNKNOWN) {
587
+ current = new VaoState();
588
+ }
589
+ counts.forwarded++;
590
+ return rDeleteVertexArray.call(gl, v);
591
+ });
592
+ /** Whether location `loc` points as asked (it does from now on); a pointer takes ARRAY_BUFFER's buffer. */
593
+ const points = (loc: number, integer: boolean, size: unknown, type: unknown, norm: unknown, stride: unknown, offset: unknown): boolean => {
594
+ const v = current;
595
+ const buf = bufferAt(ARRAY_BUFFER);
596
+ if (buf !== UNKNOWN && v.buffer[loc] === buf && v.integer[loc] === integer && v.size[loc] === size && v.type[loc] === type && v.norm[loc] === norm && v.stride[loc] === stride && v.offset[loc] === offset) return true;
597
+ v.buffer[loc] = buf;
598
+ v.integer[loc] = integer;
599
+ v.size[loc] = size;
600
+ v.type[loc] = type;
601
+ v.norm[loc] = norm;
602
+ v.stride[loc] = stride;
603
+ v.offset[loc] = offset;
604
+ return false;
605
+ };
606
+ const rPointer = fn("vertexAttribPointer");
607
+ define("vertexAttribPointer", (loc, size, type, norm, stride, offset) => {
608
+ counts.calls++;
609
+ if (points(loc as number, false, size, type, norm, stride, offset)) return;
610
+ flushBuffer(ARRAY_BUFFER);
611
+ counts.forwarded++;
612
+ return rPointer.call(gl, loc, size, type, norm, stride, offset);
613
+ });
614
+ const rIPointer = fn("vertexAttribIPointer");
615
+ define("vertexAttribIPointer", (loc, size, type, stride, offset) => {
616
+ counts.calls++;
617
+ if (points(loc as number, true, size, type, undefined, stride, offset)) return;
618
+ flushBuffer(ARRAY_BUFFER);
619
+ counts.forwarded++;
620
+ return rIPointer.call(gl, loc, size, type, stride, offset);
621
+ });
622
+ const rDivisor = fn("vertexAttribDivisor");
623
+ define("vertexAttribDivisor", (loc, d) => {
624
+ counts.calls++;
625
+ if (current.divisor[loc as number] === d) return;
626
+ current.divisor[loc as number] = d;
627
+ counts.forwarded++;
628
+ return rDivisor.call(gl, loc, d);
629
+ });
630
+
631
+ // ── programs and uniforms ─────────────────────────────────────────────
632
+ // Deferred: bound when a draw or a uniform reads it.
633
+ define("useProgram", (p) => {
634
+ counts.calls++;
635
+ wantedProgram = p as Obj;
636
+ });
637
+ // A null location is ignored by WebGL (wgpu sets its first-instance
638
+ // uniform on every draw whether the program has one or not).
639
+ for (const name of raw.keys()) {
640
+ if (!/^uniform(Matrix)?[1-4]/.test(name)) continue;
641
+ const r = fn(name);
642
+ define(name, (...a) => {
643
+ counts.calls++;
644
+ if (a[0] === null || a[0] === undefined) return;
645
+ flushProgram();
646
+ counts.forwarded++;
647
+ return r.apply(gl, a);
648
+ });
649
+ }
650
+
651
+ // ── framebuffers ──────────────────────────────────────────────────────
652
+ // Deferred: bound when a call reads the target.
653
+ define("bindFramebuffer", (target, fb) => {
654
+ counts.calls++;
655
+ if (target !== FRAMEBUFFER && target !== DRAW_FRAMEBUFFER && target !== READ_FRAMEBUFFER) {
656
+ flushAll();
657
+ counts.forwarded++;
658
+ return rBindFramebuffer.call(gl, target, fb);
659
+ }
660
+ if (target !== READ_FRAMEBUFFER) wantedDraw = fb as Obj;
661
+ if (target !== DRAW_FRAMEBUFFER) wantedRead = fb as Obj;
662
+ });
663
+ /** Binds the framebuffer a call on `target` reads. */
664
+ const flushTarget = (target: unknown) => flushFramebuffers(target !== READ_FRAMEBUFFER, target === READ_FRAMEBUFFER || target === FRAMEBUFFER);
665
+ /** Whether attachment point `at` of the framebuffer bound to `target` holds what is asked (it does from now on). */
666
+ const attached = (target: unknown, at: number, kind: number, obj: Obj, a: unknown, b: unknown): boolean => {
667
+ const fb = framebufferAt(target);
668
+ // Bound to an unknown framebuffer: any one's attachments may change.
669
+ if (fb === UNKNOWN) {
670
+ attachments.clear();
671
+ return false;
672
+ }
673
+ if (fb === null) return false;
674
+ let map = attachments.get(fb);
675
+ if (!map) attachments.set(fb, (map = new Map()));
676
+ const holds = (p: number) => {
677
+ const v = map!.get(p);
678
+ return v !== undefined && v.kind === kind && v.obj === obj && v.a === a && v.b === b;
679
+ };
680
+ const value = kind === 0 ? NONE : { kind, obj, a, b };
681
+ // DEPTH_STENCIL_ATTACHMENT is the depth and the stencil points at once.
682
+ if (at === DEPTH_STENCIL_ATTACHMENT) {
683
+ if (holds(DEPTH_ATTACHMENT) && holds(STENCIL_ATTACHMENT)) return true;
684
+ map.set(DEPTH_ATTACHMENT, value);
685
+ map.set(STENCIL_ATTACHMENT, value);
686
+ return false;
687
+ }
688
+ if (holds(at)) return true;
689
+ map.set(at, value);
690
+ return false;
691
+ };
692
+ const rTexture2D = fn("framebufferTexture2D");
693
+ define("framebufferTexture2D", (target, at, textarget, tex, level) => {
694
+ counts.calls++;
695
+ if (tex === null ? attached(target, at as number, 0, null, 0, 0) : attached(target, at as number, 1, tex as Obj, textarget, level)) return;
696
+ flushTarget(target);
697
+ counts.forwarded++;
698
+ return rTexture2D.call(gl, target, at, textarget, tex, level);
699
+ });
700
+ const rTextureLayer = fn("framebufferTextureLayer");
701
+ define("framebufferTextureLayer", (target, at, tex, level, layer) => {
702
+ counts.calls++;
703
+ if (tex === null ? attached(target, at as number, 0, null, 0, 0) : attached(target, at as number, 2, tex as Obj, level, layer)) return;
704
+ flushTarget(target);
705
+ counts.forwarded++;
706
+ return rTextureLayer.call(gl, target, at, tex, level, layer);
707
+ });
708
+ const rRenderbuffer = fn("framebufferRenderbuffer");
709
+ define("framebufferRenderbuffer", (target, at, rbTarget, rb) => {
710
+ counts.calls++;
711
+ if (rb === null ? attached(target, at as number, 0, null, 0, 0) : attached(target, at as number, 3, rb as Obj, 0, 0)) return;
712
+ flushTarget(target);
713
+ counts.forwarded++;
714
+ return rRenderbuffer.call(gl, target, at, rbTarget, rb);
715
+ });
716
+ const rDrawBuffers = fn("drawBuffers");
717
+ define("drawBuffers", (list) => {
718
+ counts.calls++;
719
+ const l = list as ArrayLike<number>;
720
+ const fb = framebufferAt(DRAW_FRAMEBUFFER);
721
+ if (fb === UNKNOWN) drawBuffersOf.clear();
722
+ else {
723
+ const held = drawBuffersOf.get(fb);
724
+ let same = held !== undefined && held.length === l.length;
725
+ for (let i = 0; same && i < l.length; i++) same = held![i] === l[i];
726
+ if (same) return;
727
+ drawBuffersOf.set(fb, Array.from(l));
728
+ }
729
+ flushFramebuffers(true, false);
730
+ counts.forwarded++;
731
+ return rDrawBuffers.call(gl, list);
732
+ });
733
+
734
+ // ── fixed-function values ─────────────────────────────────────────────
735
+ /**
736
+ * A setter of `arity` (up to four) values, dropped when the context holds
737
+ * them (`held`: whether it does, by default the arguments' own value `key`).
738
+ */
739
+ const setter = (name: string, arity: number, key: string, held?: (a: unknown, b: unknown, c: unknown, d: unknown) => boolean) => {
740
+ const own = value(key);
741
+ const spread = held ?? ((a: unknown, b: unknown, c: unknown, d: unknown) => own.holds(a, b, c, d));
742
+ const r = fn(name);
743
+ define(name, (a, b, c, d) => {
744
+ counts.calls++;
745
+ if (spread(a, b, c, d)) return;
746
+ counts.forwarded++;
747
+ return arity === 1 ? r.call(gl, a) : arity === 2 ? r.call(gl, a, b) : arity === 3 ? r.call(gl, a, b, c) : r.call(gl, a, b, c, d);
748
+ });
749
+ };
750
+ for (const name of ["depthMask", "frontFace", "cullFace", "depthFunc", "clearDepth", "clearStencil"]) setter(name, 1, name);
751
+ for (const name of ["depthRange", "polygonOffset"]) setter(name, 2, name);
752
+ for (const name of ["colorMask", "viewport", "scissor", "blendColor", "clearColor"]) setter(name, 4, name);
753
+ const equation = value("blendEquation");
754
+ setter("blendEquation", 1, "", (a) => equation.holds(a, a, 0, 0));
755
+ setter("blendEquationSeparate", 2, "", (a, b) => equation.holds(a, b, 0, 0));
756
+ const func = value("blendFunc");
757
+ setter("blendFunc", 2, "", (a, b) => func.holds(a, b, a, b));
758
+ setter("blendFuncSeparate", 4, "blendFunc");
759
+ // Stencil state per face: the plain forms and FRONT_AND_BACK set both.
760
+ const faced = (key: string) => {
761
+ const f = value(`${key}:front`);
762
+ const k = value(`${key}:back`);
763
+ return (face: unknown, a: unknown, b: unknown, c: unknown) => {
764
+ const front = face !== BACK;
765
+ const back = face === BACK || face === FRONT_AND_BACK || face === undefined;
766
+ // Both checked before either is set: holds() takes the new value.
767
+ const held = (!front || (f.a === a && f.b === b && f.c === c && f.d === 0)) && (!back || (k.a === a && k.b === b && k.c === c && k.d === 0));
768
+ if (front) f.holds(a, b, c, 0);
769
+ if (back) k.holds(a, b, c, 0);
770
+ return held;
771
+ };
772
+ };
773
+ const isFace = (face: unknown) => face === FRONT || face === BACK || face === FRONT_AND_BACK;
774
+ for (const key of ["stencilMask", "stencilFunc", "stencilOp"]) {
775
+ const of = faced(key);
776
+ if (key === "stencilMask") setter(key, 1, "", (a) => of(undefined, a, 0, 0));
777
+ else setter(key, 3, "", (a, b, c) => of(undefined, a, b, c));
778
+ if (key === "stencilMask") setter(`${key}Separate`, 2, "", (face, a) => isFace(face) && of(face, a, 0, 0));
779
+ else setter(`${key}Separate`, 4, "", (face, a, b, c) => isFace(face) && of(face, a, b, c));
780
+ }
781
+ const rPixelStorei = fn("pixelStorei");
782
+ define("pixelStorei", (pname, v) => {
783
+ counts.calls++;
784
+ if (pixelStore.get(pname as number) === v && pixelStore.has(pname as number)) return;
785
+ pixelStore.set(pname as number, v);
786
+ counts.forwarded++;
787
+ return rPixelStorei.call(gl, pname, v);
788
+ });
789
+
790
+ // ── extensions: their calls change state the mirror cannot see ────────
791
+ const wrapped = new WeakSet<object>();
792
+ const rGetExtension = fn("getExtension");
793
+ define("getExtension", (name) => {
794
+ counts.calls++;
795
+ counts.forwarded++;
796
+ const ext = rGetExtension.call(gl, name) as Record<string, unknown> | null;
797
+ if (!ext || wrapped.has(ext)) return ext;
798
+ wrapped.add(ext);
799
+ for (let p = Object.getPrototypeOf(ext) as object | null; p && p !== Object.prototype; p = Object.getPrototypeOf(p) as object | null) {
800
+ for (const k of Object.getOwnPropertyNames(p)) {
801
+ const d = Object.getOwnPropertyDescriptor(p, k);
802
+ if (k === "constructor" || !d || typeof d.value !== "function" || Object.prototype.hasOwnProperty.call(ext, k)) continue;
803
+ const f = d.value as Fn;
804
+ Object.defineProperty(ext, k, {
805
+ value: (...a: unknown[]) => {
806
+ flushAll();
807
+ const r = f.apply(ext, a);
808
+ forget();
809
+ return r;
810
+ },
811
+ configurable: true,
812
+ writable: true,
813
+ });
814
+ }
815
+ }
816
+ return ext;
817
+ });
818
+
819
+ // ── everything else ───────────────────────────────────────────────────
820
+ // The calls on a framebuffer target: they read its binding.
821
+ for (const name of ["invalidateFramebuffer", "invalidateSubFramebuffer"]) {
822
+ const r = fn(name);
823
+ define(name, (...a) => {
824
+ counts.calls++;
825
+ flushTarget(a[0]);
826
+ counts.forwarded++;
827
+ return r.apply(gl, a);
828
+ });
829
+ }
830
+ const rReadBuffer = fn("readBuffer");
831
+ define("readBuffer", (src) => {
832
+ counts.calls++;
833
+ flushFramebuffers(false, true);
834
+ counts.forwarded++;
835
+ return rReadBuffer.call(gl, src);
836
+ });
837
+ const independent = new Set(INDEPENDENT);
838
+ const clears = new Set(CLEARS);
839
+ const draws = new Set(DRAWS);
840
+ const observing = new Set(OBSERVING);
841
+ for (const [name, r] of raw) {
842
+ if (Object.prototype.hasOwnProperty.call(methods, name)) continue;
843
+ if (independent.has(name)) {
844
+ define(name, (...a) => {
845
+ counts.calls++;
846
+ counts.forwarded++;
847
+ return r.apply(gl, a);
848
+ });
849
+ } else if (draws.has(name)) {
850
+ define(name, (...a) => {
851
+ counts.calls++;
852
+ flushDraw();
853
+ counts.forwarded++;
854
+ return r.apply(gl, a);
855
+ });
856
+ } else if (clears.has(name)) {
857
+ define(name, (...a) => {
858
+ counts.calls++;
859
+ flushClearCaps();
860
+ flushFramebuffers(true, false);
861
+ counts.forwarded++;
862
+ return r.apply(gl, a);
863
+ });
864
+ } else if (observing.has(name)) {
865
+ define(name, (...a) => {
866
+ counts.calls++;
867
+ flushAll();
868
+ counts.forwarded++;
869
+ return r.apply(gl, a);
870
+ });
871
+ } else {
872
+ // Not modelled: it may change anything.
873
+ define(name, (...a) => {
874
+ counts.calls++;
875
+ flushAll();
876
+ counts.forwarded++;
877
+ const result = r.apply(gl, a);
878
+ forget();
879
+ return result;
880
+ });
881
+ }
882
+ }
883
+ Object.setPrototypeOf(gl, methods);
884
+ return counts;
885
+ }
886
+
887
+ /**
888
+ * Filters the WebGL2 context `canvas` hands out (wgpu's GL backend asks
889
+ * for it while the renderer attaches): installed on the canvas's
890
+ * getContext before then.
891
+ */
892
+ export function filterGlStateOf(canvas: OffscreenCanvas): void {
893
+ const get = canvas.getContext.bind(canvas) as (id: string, options?: unknown) => unknown;
894
+ const filtered = new WeakSet<object>();
895
+ Object.defineProperty(canvas, "getContext", {
896
+ value: (id: string, options?: unknown) => {
897
+ const ctx = get(id, options);
898
+ if (id === "webgl2" && ctx && !filtered.has(ctx as object)) {
899
+ filtered.add(ctx as object);
900
+ filterGlState(ctx as Gl);
901
+ }
902
+ return ctx;
903
+ },
904
+ configurable: true,
905
+ writable: true,
906
+ });
907
+ }