wasmcart 0.2.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.
@@ -0,0 +1,1483 @@
1
+ // webgl_imports.js - builds the `gl` WASM import module for GL-enabled carts
2
+ // using a WebGL2RenderingContext as the backend.
3
+ //
4
+ // The cart imports GL functions from a module named "gl":
5
+ // __attribute__((import_module("gl"), import_name("glClear")))
6
+ // extern void glClear(unsigned int mask);
7
+ //
8
+ // This factory maps those WASM imports to WebGL2 method calls, maintaining
9
+ // integer ID ↔ WebGL object mapping tables (like emscripten's GL.textures[]).
10
+ //
11
+ // Backend: WebGL2RenderingContext (from webgl-node or browser)
12
+
13
+ const decoder = new TextDecoder();
14
+ const encoder = new TextEncoder();
15
+
16
+ function readCString(u8, ptr) {
17
+ let end = ptr;
18
+ while (u8[end] !== 0) end++;
19
+ return decoder.decode(u8.subarray(ptr, end));
20
+ }
21
+
22
+ /**
23
+ * Create the GL import object for WASM instantiation.
24
+ *
25
+ * @param {object} options
26
+ * @param {function} options.getMemory - returns the WASM memory object
27
+ * @param {WebGL2RenderingContext} options.ctx - WebGL2 rendering context
28
+ * @returns {object} import object with GL functions
29
+ */
30
+ export function createWebGLImports({ getMemory, ctx, getMalloc, nativeGL }) {
31
+ function u8() { return new Uint8Array(getMemory().buffer); }
32
+ function u16() { return new Uint16Array(getMemory().buffer); }
33
+ function u32() { return new Uint32Array(getMemory().buffer); }
34
+ function i32() { return new Int32Array(getMemory().buffer); }
35
+ function f32() { return new Float32Array(getMemory().buffer); }
36
+
37
+ // Enable WebGL2 extensions upfront
38
+ const _extAniso = ctx.getExtension('EXT_texture_filter_anisotropic');
39
+ ctx.getExtension('OES_texture_float_linear');
40
+ ctx.getExtension('EXT_color_buffer_float');
41
+ ctx.getExtension('EXT_float_blend');
42
+ ctx.getExtension('EXT_texture_norm16');
43
+ ctx.getExtension('WEBGL_compressed_texture_s3tc');
44
+ ctx.getExtension('WEBGL_compressed_texture_s3tc_srgb');
45
+ ctx.getExtension('EXT_texture_compression_bptc');
46
+ ctx.getExtension('EXT_texture_compression_rgtc');
47
+
48
+ // Build extension list with "GL_" prefix (native GLES convention).
49
+ // Also include custom extensions for compatibility with engine feature detection.
50
+ const _extraExtensions = [
51
+ 'GL_OES_packed_depth_stencil',
52
+ 'GL_OES_texture_npot',
53
+ 'GL_EXT_texture_filter_anisotropic',
54
+ 'GL_OES_texture_float_linear',
55
+ 'GL_EXT_color_buffer_float',
56
+ 'GL_EXT_float_blend',
57
+ 'GL_EXT_texture_compression_s3tc',
58
+ 'GL_EXT_texture_compression_dxt1',
59
+ 'GL_EXT_texture_compression_bptc',
60
+ 'GL_EXT_texture_compression_rgtc',
61
+ 'WEBGL_compressed_texture_s3tc',
62
+ 'WEBGL_compressed_texture_s3tc_srgb',
63
+ ];
64
+ const _allExtensions = (() => {
65
+ const browserExts = ctx.getSupportedExtensions() || [];
66
+ const set = new Set();
67
+ for (const ext of browserExts) {
68
+ set.add(ext);
69
+ if (!ext.startsWith('GL_')) set.add('GL_' + ext);
70
+ }
71
+ for (const ext of _extraExtensions) set.add(ext);
72
+ // Remove GLES2-only extensions that are built-in in GLES3/WebGL2.
73
+ // Ganesh adds #extension directives for these which fail in GLSL 300 es.
74
+ const gles2Builtins = ['OES_standard_derivatives', 'GL_OES_standard_derivatives',
75
+ 'EXT_shader_texture_lod', 'GL_EXT_shader_texture_lod'];
76
+ for (const e of gles2Builtins) set.delete(e);
77
+ return Array.from(set);
78
+ })();
79
+
80
+ // ─── Object ID tables ─────────────────────────────────────────────────
81
+ // WebGL uses opaque objects; C code uses integer IDs.
82
+ // ID 0 = null (no object), IDs start at 1.
83
+ const _buffers = [null]; // id → WebGLBuffer
84
+ const _textures = [null]; // id → WebGLTexture
85
+ const _framebuffers = [null]; // id → WebGLFramebuffer
86
+ const _renderbuffers = [null]; // id → WebGLRenderbuffer
87
+ const _programs = [null]; // id → WebGLProgram
88
+ const _shaders = [null]; // id → WebGLShader
89
+ const _vaos = [null]; // id → WebGLVertexArrayObject
90
+ const _samplers = [null]; // id → WebGLSampler
91
+ const _queries = [null]; // id → WebGLQuery
92
+ const _syncs = [null]; // id → WebGLSync
93
+
94
+ // Uniform locations: per-program map of integer loc → WebGLUniformLocation
95
+ // Key: program ID, Value: Map<int, WebGLUniformLocation>
96
+ const _uniformLocs = new Map(); // programId → { byName: Map<string, {loc, id}>, byId: Map<int, loc> }
97
+ let _currentProgramId = 0;
98
+ let _nextUniformId = 0;
99
+
100
+ // ─── FBO redirect (same as wasmcart-native gl_imports.cpp) ──────────
101
+ // Cart's glBindFramebuffer(0) → redirect FBO with depth+stencil.
102
+ // After each frame, blit redirect FBO → real canvas (FBO null).
103
+ let _redirectFBO = null;
104
+ let _redirectTex = null;
105
+ let _redirectRBO = null;
106
+ let _redirectW = 0;
107
+ let _redirectH = 0;
108
+
109
+ function _ensureRedirectFBO(w, h) {
110
+ if (_redirectFBO && _redirectW === w && _redirectH === h) return;
111
+ if (_redirectFBO) {
112
+ ctx.deleteFramebuffer(_redirectFBO);
113
+ ctx.deleteTexture(_redirectTex);
114
+ ctx.deleteRenderbuffer(_redirectRBO);
115
+ }
116
+ _redirectFBO = ctx.createFramebuffer();
117
+ _redirectTex = ctx.createTexture();
118
+ _redirectRBO = ctx.createRenderbuffer();
119
+
120
+ ctx.bindTexture(0x0DE1, _redirectTex);
121
+ ctx.texImage2D(0x0DE1, 0, ctx.RGBA8, w, h, 0, ctx.RGBA, ctx.UNSIGNED_BYTE, null);
122
+ ctx.texParameteri(0x0DE1, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR);
123
+ ctx.texParameteri(0x0DE1, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR);
124
+
125
+ ctx.bindRenderbuffer(ctx.RENDERBUFFER, _redirectRBO);
126
+ ctx.renderbufferStorage(ctx.RENDERBUFFER, ctx.DEPTH24_STENCIL8, w, h);
127
+
128
+ ctx.bindFramebuffer(ctx.FRAMEBUFFER, _redirectFBO);
129
+ ctx.framebufferTexture2D(ctx.FRAMEBUFFER, ctx.COLOR_ATTACHMENT0, 0x0DE1, _redirectTex, 0);
130
+ ctx.framebufferRenderbuffer(ctx.FRAMEBUFFER, ctx.DEPTH_STENCIL_ATTACHMENT, ctx.RENDERBUFFER, _redirectRBO);
131
+
132
+ _redirectW = w;
133
+ _redirectH = h;
134
+ ctx.bindFramebuffer(ctx.FRAMEBUFFER, null);
135
+ }
136
+
137
+ function _blitRedirectToCanvas() {
138
+ if (!_redirectFBO) return;
139
+ const cw = ctx.drawingBufferWidth;
140
+ const ch = ctx.drawingBufferHeight;
141
+ ctx.bindFramebuffer(ctx.READ_FRAMEBUFFER, _redirectFBO);
142
+ ctx.bindFramebuffer(ctx.DRAW_FRAMEBUFFER, null);
143
+ ctx.blitFramebuffer(0, 0, _redirectW, _redirectH, 0, 0, cw, ch,
144
+ ctx.COLOR_BUFFER_BIT, ctx.LINEAR);
145
+ // Restore redirect for next frame
146
+ ctx.bindFramebuffer(ctx.FRAMEBUFFER, _redirectFBO);
147
+ }
148
+
149
+ function _allocId(table, obj) {
150
+ const id = table.length;
151
+ table.push(obj);
152
+ return id;
153
+ }
154
+
155
+ // WebGL2 requires sized internal formats for texImage2D.
156
+ // GLES2 carts often pass unsized formats (e.g. GL_RGBA instead of GL_RGBA8).
157
+ // Map unsized → sized based on format+type, and handle ARB suffixed constants.
158
+ function _fixInternalFormat(ifmt, format, type) {
159
+ const GL_UNSIGNED_BYTE = 0x1401;
160
+ const GL_FLOAT = 0x1406;
161
+ const GL_HALF_FLOAT = 0x140B;
162
+ const GL_HALF_FLOAT_OES = 0x8D61;
163
+
164
+ // ARB/EXT suffixed constants → core WebGL2
165
+ switch (ifmt) {
166
+ case 0x881A: return 0x881A; // GL_RGBA16F_ARB → GL_RGBA16F (same value, valid in WebGL2)
167
+ case 0x881B: return 0x881B; // GL_RGB16F
168
+ case 0x8814: return 0x8814; // GL_RGBA32F
169
+ case 0x8815: return 0x8815; // GL_RGB32F
170
+ case 0x822D: return 0x822D; // GL_R16F
171
+ case 0x822E: return 0x822E; // GL_RG16F
172
+ case 0x8D62: return 0x8D62; // GL_RGB16UI (not a float, but valid)
173
+ }
174
+
175
+ // Unsized → sized internal format
176
+ if (type === GL_UNSIGNED_BYTE) {
177
+ switch (ifmt) {
178
+ case 0x1906: return 0x8229; // GL_ALPHA → GL_R8 (no GL_ALPHA8 in WebGL2)
179
+ case 0x1909: return 0x8229; // GL_LUMINANCE → GL_R8
180
+ case 0x190A: return 0x8227; // GL_LUMINANCE_ALPHA → GL_RG8
181
+ case 0x1907: return 0x8051; // GL_RGB → GL_RGB8
182
+ case 0x1908: return 0x8058; // GL_RGBA → GL_RGBA8
183
+ case 0x1903: return 0x8229; // GL_RED → GL_R8
184
+ case 0x8227: return 0x8227; // GL_RG → GL_RG8
185
+ }
186
+ } else if (type === GL_FLOAT) {
187
+ switch (ifmt) {
188
+ case 0x1907: return 0x8815; // GL_RGB → GL_RGB32F
189
+ case 0x1908: return 0x8814; // GL_RGBA → GL_RGBA32F
190
+ case 0x1903: return 0x822E; // GL_RED → GL_R32F
191
+ }
192
+ } else if (type === GL_HALF_FLOAT || type === GL_HALF_FLOAT_OES) {
193
+ switch (ifmt) {
194
+ case 0x1907: return 0x881B; // GL_RGB → GL_RGB16F
195
+ case 0x1908: return 0x881A; // GL_RGBA → GL_RGBA16F
196
+ case 0x1903: return 0x822D; // GL_RED → GL_R16F
197
+ }
198
+ } else if (type === 0x1403) { // GL_UNSIGNED_SHORT
199
+ // EXT_texture_norm16 provides GL_R16, GL_RG16, GL_RGBA16 (normalized)
200
+ switch (ifmt) {
201
+ case 0x1903: return 0x822A; // GL_RED → GL_R16_EXT
202
+ case 0x8227: return 0x822C; // GL_RG → GL_RG16_EXT
203
+ case 0x1908: return 0x805B; // GL_RGBA → GL_RGBA16_EXT
204
+ }
205
+ }
206
+
207
+ // Formats WebGL2 doesn't support - map to closest equivalent
208
+ switch (ifmt) {
209
+ // GL_RGBA16: keep as-is if EXT_texture_norm16 available, else fall back to RGBA16F
210
+ case 0x805B: return (type === 0x1403) ? 0x805B : 0x881A; // GL_RGBA16 → keep or GL_RGBA16F
211
+ case 0x8F9B: return 0x8058; // GL_RGBA16_SNORM → GL_RGBA8
212
+ case 0x8054: return 0x8051; // GL_RGB12 → GL_RGB8
213
+ case 0x80E1: return 0x8058; // GL_BGRA → GL_RGBA8
214
+ case 0x80E0: return 0x8051; // GL_BGR → GL_RGB8
215
+ }
216
+
217
+ return ifmt;
218
+ }
219
+
220
+ // WebGL2 doesn't support legacy GL_LUMINANCE/GL_LUMINANCE_ALPHA/GL_ALPHA as format params.
221
+ // Map them to their WebGL2 equivalents.
222
+ function _fixFormat(fmt) {
223
+ switch (fmt) {
224
+ case 0x1906: return 0x1903; // GL_ALPHA → GL_RED
225
+ case 0x1909: return 0x1903; // GL_LUMINANCE → GL_RED
226
+ case 0x190A: return 0x8227; // GL_LUMINANCE_ALPHA → GL_RG
227
+ default: return fmt;
228
+ }
229
+ }
230
+
231
+ // Convert BGRA/BGR pixel data to RGBA/RGB (WebGL2 doesn't support BGRA/BGR formats)
232
+ const GL_BGR = 0x80E0;
233
+ const GL_BGRA = 0x80E1;
234
+
235
+ function _fixBGRA(format, type, pixelsPtr, pixelCount) {
236
+ if (format !== GL_BGRA && format !== GL_BGR) return null;
237
+ if (pixelsPtr === 0) return null;
238
+
239
+ const mem = u8();
240
+ const channels = format === GL_BGRA ? 4 : 3;
241
+ const bpp = channels; // Assumes GL_UNSIGNED_BYTE
242
+ const size = pixelCount * bpp;
243
+ const swizzled = new Uint8Array(size);
244
+ const src = pixelsPtr;
245
+
246
+ for (let i = 0; i < pixelCount; i++) {
247
+ const si = src + i * bpp;
248
+ const di = i * bpp;
249
+ swizzled[di] = mem[si + 2]; // R ← B
250
+ swizzled[di + 1] = mem[si + 1]; // G ← G
251
+ swizzled[di + 2] = mem[si]; // B ← R
252
+ if (channels === 4) {
253
+ swizzled[di + 3] = mem[si + 3]; // A ← A
254
+ }
255
+ }
256
+
257
+ return {
258
+ format: format === GL_BGRA ? 0x1908 : 0x1907, // GL_RGBA : GL_RGB
259
+ data: swizzled,
260
+ };
261
+ }
262
+
263
+ function _genObjects(table, createFn, n, ptr) {
264
+ const view = u32();
265
+ for (let i = 0; i < n; i++) {
266
+ const obj = createFn();
267
+ const id = _allocId(table, obj);
268
+ view[(ptr >> 2) + i] = id;
269
+ }
270
+ }
271
+
272
+ function _deleteObjects(table, deleteFn, n, ptr) {
273
+ const view = u32();
274
+ for (let i = 0; i < n; i++) {
275
+ const id = view[(ptr >> 2) + i];
276
+ if (id > 0 && id < table.length && table[id]) {
277
+ deleteFn(table[id]);
278
+ table[id] = null;
279
+ }
280
+ }
281
+ }
282
+
283
+ // Get or create uniform location entry for current program
284
+ function _getUniformLoc(locId) {
285
+ if (locId < 0) return null;
286
+ const progEntry = _uniformLocs.get(_currentProgramId);
287
+ if (!progEntry) return null;
288
+ return progEntry.byId.get(locId) || null;
289
+ }
290
+
291
+ // ─── Client-side vertex array support ─────────────────────────────────
292
+ const GL_ARRAY_BUFFER = 0x8892;
293
+ const GL_ELEMENT_ARRAY_BUFFER = 0x8893;
294
+
295
+ let _boundArrayBuffer = 0;
296
+ let _boundElementBuffer = 0;
297
+ const _clientAttribs = new Map();
298
+
299
+ function _bytesForType(type) {
300
+ switch (type) {
301
+ case 0x1400: return 1; // GL_BYTE
302
+ case 0x1401: return 1; // GL_UNSIGNED_BYTE
303
+ case 0x1402: return 2; // GL_SHORT
304
+ case 0x1403: return 2; // GL_UNSIGNED_SHORT
305
+ case 0x1404: return 4; // GL_INT
306
+ case 0x1405: return 4; // GL_UNSIGNED_INT
307
+ case 0x1406: return 4; // GL_FLOAT
308
+ case 0x140B: return 2; // GL_HALF_FLOAT
309
+ default: return 4;
310
+ }
311
+ }
312
+
313
+ // Temp buffers for client-side vertex data uploads
314
+ let _tempVBOs = null;
315
+ let _tempEBO = null;
316
+
317
+ function _ensureTempVBO(index) {
318
+ if (!_tempVBOs) _tempVBOs = new Map();
319
+ if (!_tempVBOs.has(index)) {
320
+ _tempVBOs.set(index, ctx.createBuffer());
321
+ }
322
+ return _tempVBOs.get(index);
323
+ }
324
+
325
+ function _ensureTempEBO() {
326
+ if (!_tempEBO) _tempEBO = ctx.createBuffer();
327
+ return _tempEBO;
328
+ }
329
+
330
+ function _uploadClientAttribs(firstVertex, vertexCount) {
331
+ if (_clientAttribs.size === 0) return;
332
+ const mem = u8();
333
+ for (const [index, attr] of _clientAttribs) {
334
+ const elemBytes = attr.size * _bytesForType(attr.type);
335
+ const effectiveStride = attr.stride || elemBytes;
336
+ const startByte = attr.wasmPtr + firstVertex * effectiveStride;
337
+ const totalBytes = vertexCount * effectiveStride;
338
+
339
+ const vbo = _ensureTempVBO(index);
340
+ ctx.bindBuffer(GL_ARRAY_BUFFER, vbo);
341
+ ctx.bufferData(GL_ARRAY_BUFFER, mem.subarray(startByte, startByte + totalBytes), 0x88E0); // GL_STREAM_DRAW
342
+ ctx.vertexAttribPointer(index, attr.size, attr.type, attr.normalized, attr.stride, 0);
343
+ }
344
+ // Restore user's binding
345
+ ctx.bindBuffer(GL_ARRAY_BUFFER, _boundArrayBuffer ? _buffers[_boundArrayBuffer] : null);
346
+ }
347
+
348
+ function _uploadClientIndices(wasmPtr, count, type) {
349
+ const elemSize = _bytesForType(type);
350
+ const totalBytes = count * elemSize;
351
+ const mem = u8();
352
+ const ebo = _ensureTempEBO();
353
+ ctx.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
354
+ ctx.bufferData(GL_ELEMENT_ARRAY_BUFFER, mem.subarray(wasmPtr, wasmPtr + totalBytes), 0x88E0);
355
+ }
356
+
357
+ let _compressedTexWarnedFormats = null;
358
+
359
+ // ─── String pool for glGetString / glGetStringi ─────────────────────
360
+ let _glStringCache = null;
361
+ let _glStringiCache = null;
362
+ let _glStringPool = null;
363
+
364
+ function _writeStringToWasm(str) {
365
+ const encoded = encoder.encode(str);
366
+ const size = encoded.length + 1; // +1 for null terminator
367
+
368
+ // Use the cart's malloc if available (safe - properly tracked by heap)
369
+ const malloc = getMalloc && getMalloc();
370
+ if (malloc) {
371
+ const ptr = malloc(size);
372
+ if (ptr) {
373
+ const mem = u8();
374
+ mem.set(encoded, ptr);
375
+ mem[ptr + encoded.length] = 0;
376
+ return ptr;
377
+ }
378
+ }
379
+
380
+ // Fallback: static pool at end of memory (only safe before heap grows there)
381
+ if (!_glStringPool) _glStringPool = { offset: 0, base: 0 };
382
+ if (!_glStringPool.base) {
383
+ _glStringPool.base = getMemory().buffer.byteLength - 16384;
384
+ _glStringPool.offset = 0;
385
+ }
386
+ const ptr = _glStringPool.base + _glStringPool.offset;
387
+ const mem = u8();
388
+ if (ptr + size < mem.length) {
389
+ mem.set(encoded, ptr);
390
+ mem[ptr + encoded.length] = 0;
391
+ _glStringPool.offset += size;
392
+ _glStringPool.offset = (_glStringPool.offset + 3) & ~3;
393
+ return ptr;
394
+ }
395
+ return 0;
396
+ }
397
+
398
+ // ─── Buffer mapping emulation ──────────────────────────────────────────
399
+ const _mappedBuffers = new Map();
400
+ const MAP_SCRATCH_SIZE = 1024 * 1024;
401
+ let _mapScratchBase = 0;
402
+ let _mapScratchUsed = 0;
403
+
404
+ function _getMapScratch(length) {
405
+ const mem = getMemory();
406
+ if (_mapScratchBase === 0) {
407
+ _mapScratchBase = mem.buffer.byteLength - 16384 - MAP_SCRATCH_SIZE;
408
+ }
409
+ if (_mapScratchUsed + length > MAP_SCRATCH_SIZE) return 0;
410
+ const ptr = _mapScratchBase + _mapScratchUsed;
411
+ _mapScratchUsed += length;
412
+ _mapScratchUsed = ((_mapScratchUsed + 7) & ~7);
413
+ return ptr;
414
+ }
415
+
416
+ let _shaderFailLogged = false;
417
+ const _shaderTypes = new Map(); // shader id → GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
418
+
419
+ // Upgrade #version 100 shaders to #version 300 es for WebGL2 compatibility.
420
+ // GLSL ES 1.00 forbids dynamic array indexing; GLSL ES 3.00 allows it.
421
+ function _patchShaderV100toV300(source, shaderType) {
422
+ if (!source.includes('#version 100')) return source;
423
+
424
+ // Replace version
425
+ let patched = source.replace('#version 100', '#version 300 es');
426
+
427
+ if (shaderType === 0x8B30) { // GL_FRAGMENT_SHADER (0x8B30)
428
+ // Add output variable declaration after precision qualifiers
429
+ // varying → in
430
+ patched = patched.replace(/\bvarying\b/g, 'in');
431
+ // gl_FragColor → FragColor, add output declaration
432
+ if (patched.includes('gl_FragColor')) {
433
+ patched = patched.replace(/\bgl_FragColor\b/g, 'FragColor');
434
+ // Insert output declaration after the last precision statement
435
+ const precisionMatch = patched.match(/(precision\s+\w+\s+\w+\s*;[^\n]*\n)/g);
436
+ if (precisionMatch) {
437
+ const lastPrecision = precisionMatch[precisionMatch.length - 1];
438
+ const insertPos = patched.lastIndexOf(lastPrecision) + lastPrecision.length;
439
+ patched = patched.slice(0, insertPos) + 'out vec4 FragColor;\n' + patched.slice(insertPos);
440
+ } else {
441
+ // Fallback: insert after #version line
442
+ patched = patched.replace(/(#version 300 es\n)/, '$1out vec4 FragColor;\n');
443
+ }
444
+ }
445
+ // gl_FragData[n] → FragData_n (rare, but handle it)
446
+ patched = patched.replace(/\bgl_FragData\s*\[\s*0\s*\]/g, 'FragColor');
447
+ } else { // GL_VERTEX_SHADER
448
+ // attribute → in
449
+ patched = patched.replace(/\battribute\b/g, 'in');
450
+ // varying → out
451
+ patched = patched.replace(/\bvarying\b/g, 'out');
452
+ }
453
+
454
+ // texture2D → texture, textureCube → texture, shadow2DEXT → texture
455
+ patched = patched.replace(/\btexture2D\b/g, 'texture');
456
+ patched = patched.replace(/\btexture2DProj\b/g, 'textureProj');
457
+ patched = patched.replace(/\btextureCube\b/g, 'texture');
458
+ patched = patched.replace(/\bshadow2DEXT\b/g, 'texture');
459
+ patched = patched.replace(/\bshadow2DProjEXT\b/g, 'textureProj');
460
+
461
+ // Strip extension declarations that are core in ES 3.00
462
+ patched = patched.replace(/^\s*#extension\s+GL_EXT_shadow_samplers\s*:.*$/gm, '// (shadow_samplers is core in ES 3.00)');
463
+
464
+ // Keep v100 code paths despite v300es syntax - we only upgraded for dynamic indexing.
465
+ // The shaders use __VERSION__ checks to enable features (bone uvec4, etc.) that
466
+ // require matching vertex attribute types not available in the v100-era VBO layout.
467
+ patched = patched.replace(/#if\s+__VERSION__\s*>=\s*300/g, '#if 0 /* v100 compat: __VERSION__ >= 300 */');
468
+ patched = patched.replace(/#if\s+__VERSION__\s*<\s*300/g, '#if 1 /* v100 compat: __VERSION__ < 300 */');
469
+
470
+ return patched;
471
+ }
472
+
473
+ // ─── Debug ─────────────────────────────────────────────────────────────
474
+ const GL_DEBUG = typeof process !== 'undefined' && process.env?.GL_DEBUG === '1';
475
+
476
+ function _checkGL(name, args) {
477
+ if (!GL_DEBUG) return;
478
+ const err = ctx.getError();
479
+ if (err !== 0) {
480
+ console.error(`GL ERROR 0x${err.toString(16)} after ${name}(${Array.from(args).map(a => typeof a === 'number' ? '0x'+a.toString(16) : a).join(', ')})`);
481
+ }
482
+ }
483
+
484
+ const funcs = {
485
+ // ─── State ──────────────────────────────────────────────────────────
486
+ glEnable: (cap) => ctx.enable(cap),
487
+ glDisable: (cap) => ctx.disable(cap),
488
+ glGetError: () => ctx.getError(),
489
+ glFinish: () => ctx.finish(),
490
+ glFlush: () => ctx.flush(),
491
+ glHint: (target, mode) => ctx.hint(target, mode),
492
+ glPixelStorei: (pname, param) => ctx.pixelStorei(pname, param),
493
+
494
+ glGetIntegerv: (pname, paramsPtr) => {
495
+ const view = i32();
496
+ // GL_NUM_EXTENSIONS: return full extension count including custom extensions
497
+ if (pname === 0x821D) {
498
+ view[paramsPtr >> 2] = _allExtensions.length;
499
+ return;
500
+ }
501
+ // Binding queries return WebGL objects - reverse-lookup to integer IDs
502
+ const bindingTable = {
503
+ 0x8069: _textures, // GL_TEXTURE_BINDING_2D
504
+ 0x806A: _textures, // GL_TEXTURE_BINDING_3D
505
+ 0x8514: _textures, // GL_TEXTURE_BINDING_CUBE_MAP
506
+ 0x8C1D: _textures, // GL_TEXTURE_BINDING_2D_ARRAY
507
+ 0x8894: _buffers, // GL_ARRAY_BUFFER_BINDING
508
+ 0x8895: _buffers, // GL_ELEMENT_ARRAY_BUFFER_BINDING
509
+ 0x8CA6: _framebuffers, // GL_FRAMEBUFFER_BINDING
510
+ 0x8CA7: _renderbuffers, // GL_RENDERBUFFER_BINDING
511
+ 0x8B8D: _programs, // GL_CURRENT_PROGRAM
512
+ 0x85B5: _vaos, // GL_VERTEX_ARRAY_BINDING
513
+ 0x8919: _samplers, // GL_SAMPLER_BINDING
514
+ };
515
+ const table = bindingTable[pname];
516
+ if (table) {
517
+ const obj = ctx.getParameter(pname);
518
+ if (!obj) { view[paramsPtr >> 2] = 0; return; }
519
+ for (let i = 1; i < table.length; i++) {
520
+ if (table[i] === obj) { view[paramsPtr >> 2] = i; return; }
521
+ }
522
+ view[paramsPtr >> 2] = 0;
523
+ return;
524
+ }
525
+ const result = ctx.getParameter(pname);
526
+ if (typeof result === 'number') {
527
+ view[paramsPtr >> 2] = result;
528
+ } else if (typeof result === 'boolean') {
529
+ view[paramsPtr >> 2] = result ? 1 : 0;
530
+ } else if (result && typeof result === 'object' && result.length) {
531
+ // Array result (e.g. GL_MAX_VIEWPORT_DIMS)
532
+ for (let i = 0; i < result.length && i < 4; i++) {
533
+ view[(paramsPtr >> 2) + i] = result[i];
534
+ }
535
+ } else {
536
+ view[paramsPtr >> 2] = 0;
537
+ }
538
+ },
539
+
540
+ glGetString: (name) => {
541
+ if (!_glStringCache) _glStringCache = {};
542
+ if (_glStringCache[name] !== undefined) return _glStringCache[name];
543
+
544
+ let str;
545
+ switch (name) {
546
+ case 0x1F00: str = 'wasmcart'; break; // GL_VENDOR
547
+ case 0x1F01: str = 'wasmcart WebGL2'; break; // GL_RENDERER
548
+ case 0x1F02: str = 'OpenGL ES 3.0 wasmcart'; break; // GL_VERSION - must start with "OpenGL ES" for GLES mode detection
549
+ case 0x1F03: {
550
+ str = _allExtensions.join(' ');
551
+ break;
552
+ }
553
+ case 0x8B8C: str = 'OpenGL ES GLSL ES 1.00'; break; // GL_SHADING_LANGUAGE_VERSION
554
+ default: str = null;
555
+ }
556
+ if (!str) { _glStringCache[name] = 0; return 0; }
557
+ const ptr = _writeStringToWasm(str);
558
+ _glStringCache[name] = ptr;
559
+ return ptr;
560
+ },
561
+
562
+ // ─── Viewport / Clear ───────────────────────────────────────────────
563
+ glViewport: (x, y, w, h) => ctx.viewport(x, y, w, h),
564
+ glScissor: (x, y, w, h) => ctx.scissor(x, y, w, h),
565
+ glClear: (mask) => ctx.clear(mask),
566
+ glClearColor: (r, g, b, a) => ctx.clearColor(r, g, b, a),
567
+ glClearDepthf: (d) => ctx.clearDepth(d),
568
+ glClearStencil: (s) => ctx.clearStencil(s),
569
+
570
+ // ─── Blending ───────────────────────────────────────────────────────
571
+ glBlendFunc: (sf, df) => ctx.blendFunc(sf, df),
572
+ glBlendFuncSeparate: (sRGB, dRGB, sA, dA) => ctx.blendFuncSeparate(sRGB, dRGB, sA, dA),
573
+ glBlendEquation: (mode) => ctx.blendEquation(mode),
574
+ glBlendEquationSeparate: (mR, mA) => ctx.blendEquationSeparate(mR, mA),
575
+ glBlendColor: (r, g, b, a) => ctx.blendColor(r, g, b, a),
576
+ glColorMask: (r, g, b, a) => ctx.colorMask(!!r, !!g, !!b, !!a),
577
+
578
+ // ─── Depth / Stencil ────────────────────────────────────────────────
579
+ glDepthFunc: (f) => ctx.depthFunc(f),
580
+ glDepthMask: (flag) => ctx.depthMask(!!flag),
581
+ glDepthRangef: (n, f) => ctx.depthRange(n, f),
582
+ glStencilFunc: (func, ref, mask) => ctx.stencilFunc(func, ref, mask),
583
+ glStencilFuncSeparate: (face, func, ref, mask) => ctx.stencilFuncSeparate(face, func, ref, mask),
584
+ glStencilOp: (fail, zfail, zpass) => ctx.stencilOp(fail, zfail, zpass),
585
+ glStencilOpSeparate: (face, sf, dpf, dpp) => ctx.stencilOpSeparate(face, sf, dpf, dpp),
586
+ glStencilMask: (mask) => ctx.stencilMask(mask),
587
+ glStencilMaskSeparate: (face, mask) => ctx.stencilMaskSeparate(face, mask),
588
+
589
+ // ─── Face culling ───────────────────────────────────────────────────
590
+ glCullFace: (mode) => ctx.cullFace(mode),
591
+ glFrontFace: (mode) => ctx.frontFace(mode),
592
+ glPolygonOffset: (factor, units) => ctx.polygonOffset(factor, units),
593
+ glLineWidth: (width) => ctx.lineWidth(width),
594
+
595
+ // ─── Buffers ────────────────────────────────────────────────────────
596
+ glGenBuffers: (n, ptr) => _genObjects(_buffers, () => ctx.createBuffer(), n, ptr),
597
+ glDeleteBuffers: (n, ptr) => _deleteObjects(_buffers, (b) => ctx.deleteBuffer(b), n, ptr),
598
+ glBindBuffer: (target, id) => {
599
+ if (target === GL_ARRAY_BUFFER) _boundArrayBuffer = id;
600
+ else if (target === GL_ELEMENT_ARRAY_BUFFER) _boundElementBuffer = id;
601
+ ctx.bindBuffer(target, id ? _buffers[id] : null);
602
+ },
603
+ glBufferData: (target, size, dataPtr, usage) => {
604
+ if (dataPtr === 0) {
605
+ ctx.bufferData(target, size, usage);
606
+ } else {
607
+ ctx.bufferData(target, u8().subarray(dataPtr, dataPtr + size), usage);
608
+ }
609
+ },
610
+ glBufferSubData: (target, offset, size, dataPtr) => {
611
+ ctx.bufferSubData(target, offset, u8().subarray(dataPtr, dataPtr + size));
612
+ },
613
+
614
+ // ─── Textures ───────────────────────────────────────────────────────
615
+ glGenTextures: (n, ptr) => _genObjects(_textures, () => ctx.createTexture(), n, ptr),
616
+ glDeleteTextures: (n, ptr) => _deleteObjects(_textures, (t) => ctx.deleteTexture(t), n, ptr),
617
+ glBindTexture: (target, id) => ctx.bindTexture(target, id ? _textures[id] : null),
618
+ glActiveTexture: (unit) => ctx.activeTexture(unit),
619
+ glTexImage2D: (target, level, internalformat, width, height, border, format, type, pixelsPtr) => {
620
+ internalformat = _fixInternalFormat(internalformat, format, type);
621
+ format = _fixFormat(format);
622
+ const converted = _fixBGRA(format, type, pixelsPtr, width * height);
623
+ const actualFormat = converted ? converted.format : format;
624
+ const byteLen = width * height * _bytesPerPixel(format, type);
625
+ const actualData = converted ? converted.data
626
+ : pixelsPtr === 0 ? null
627
+ : _getTypedPixelView(getMemory, type, pixelsPtr, byteLen);
628
+ try {
629
+ ctx.texImage2D(target, level, internalformat, width, height, border, actualFormat, type, actualData);
630
+ } catch (e) {
631
+ console.warn(`glTexImage2D fail: ifmt=0x${internalformat.toString(16)} fmt=0x${actualFormat.toString(16)} type=0x${type.toString(16)} ${width}x${height}`, e.message);
632
+ }
633
+ },
634
+ glTexSubImage2D: (target, level, xoff, yoff, width, height, format, type, pixelsPtr) => {
635
+ format = _fixFormat(format);
636
+ const converted = _fixBGRA(format, type, pixelsPtr, width * height);
637
+ const actualFormat = converted ? converted.format : format;
638
+ const byteLen = width * height * _bytesPerPixel(format, type);
639
+ const actualData = converted ? converted.data
640
+ : _getTypedPixelView(getMemory, type, pixelsPtr, byteLen);
641
+ try {
642
+ ctx.texSubImage2D(target, level, xoff, yoff, width, height, actualFormat, type, actualData);
643
+ } catch (e) {
644
+ console.warn(`glTexSubImage2D fail: fmt=0x${actualFormat.toString(16)} type=0x${type.toString(16)} ${width}x${height}`, e.message);
645
+ }
646
+ },
647
+ glTexParameteri: (target, pname, param) => {
648
+ // GL_TEXTURE_SWIZZLE_R/G/B/A/RGBA (0x8E42-0x8E46) not supported in WebGL2 - silently skip
649
+ if (pname >= 0x8E42 && pname <= 0x8E46) return;
650
+ ctx.texParameteri(target, pname, param);
651
+ },
652
+ glTexParameterf: (target, pname, param) => {
653
+ if (pname >= 0x8E42 && pname <= 0x8E46) return;
654
+ ctx.texParameterf(target, pname, param);
655
+ },
656
+ glTexParameterfv: (target, pname, paramsPtr) => {
657
+ if (pname >= 0x8E42 && pname <= 0x8E46) return;
658
+ const view = new Float32Array(getMemory().buffer, paramsPtr, 4);
659
+ ctx.texParameterf(target, pname, view[0]);
660
+ },
661
+ glTexParameteriv: (target, pname, paramsPtr) => {
662
+ if (pname >= 0x8E42 && pname <= 0x8E46) return;
663
+ const view = new Int32Array(getMemory().buffer, paramsPtr, 4);
664
+ ctx.texParameteri(target, pname, view[0]);
665
+ },
666
+ glGenerateMipmap: (target) => ctx.generateMipmap(target),
667
+ glCompressedTexImage2D: (target, level, internalformat, width, height, border, imageSize, dataPtr) => {
668
+ const data = dataPtr ? u8().subarray(dataPtr, dataPtr + imageSize) : new Uint8Array(imageSize);
669
+ // Use native GL for compressed textures - WebGL2 context gates them behind extensions
670
+ if (nativeGL?.glCompressedTexImage2D) {
671
+ nativeGL.glCompressedTexImage2D(target, level, internalformat, width, height, border, data);
672
+ } else {
673
+ try {
674
+ ctx.compressedTexImage2D(target, level, internalformat, width, height, border, data);
675
+ } catch (e) {}
676
+ }
677
+ },
678
+ glCompressedTexSubImage2D: (target, level, xoff, yoff, width, height, format, imageSize, dataPtr) => {
679
+ const data = u8().subarray(dataPtr, dataPtr + imageSize);
680
+ if (nativeGL?.glCompressedTexSubImage2D) {
681
+ nativeGL.glCompressedTexSubImage2D(target, level, xoff, yoff, width, height, format, data);
682
+ } else {
683
+ try {
684
+ ctx.compressedTexSubImage2D(target, level, xoff, yoff, width, height, format, data);
685
+ } catch (e) {}
686
+ }
687
+ },
688
+ glCopyTexSubImage2D: (target, level, xoff, yoff, x, y, w, h) => ctx.copyTexSubImage2D(target, level, xoff, yoff, x, y, w, h),
689
+
690
+ // ─── Shaders ────────────────────────────────────────────────────────
691
+ glCreateShader: (type) => {
692
+ const obj = ctx.createShader(type);
693
+ if (!obj) return 0;
694
+ const id = _allocId(_shaders, obj);
695
+ _shaderTypes.set(id, type);
696
+ return id;
697
+ },
698
+ glDeleteShader: (id) => {
699
+ if (id > 0 && id < _shaders.length && _shaders[id]) {
700
+ ctx.deleteShader(_shaders[id]);
701
+ _shaders[id] = null;
702
+ }
703
+ },
704
+ glShaderSource: (id, count, stringsPtr, lengthsPtr) => {
705
+ const mem = u8();
706
+ const ptrs = u32();
707
+ const lens = lengthsPtr ? i32() : null;
708
+ let fullSource = '';
709
+ for (let i = 0; i < count; i++) {
710
+ const strPtr = ptrs[(stringsPtr >> 2) + i];
711
+ if (lens && lens[(lengthsPtr >> 2) + i] > 0) {
712
+ const len = lens[(lengthsPtr >> 2) + i];
713
+ fullSource += decoder.decode(mem.subarray(strPtr, strPtr + len));
714
+ } else {
715
+ fullSource += readCString(mem, strPtr);
716
+ }
717
+ }
718
+ const shaderType = _shaderTypes.get(id) || 0x8B30;
719
+ fullSource = _patchShaderV100toV300(fullSource, shaderType);
720
+ // Strip GLES2 extensions that are built-in in GLSL 300 es / WebGL2
721
+ fullSource = fullSource.replace(/^\s*#extension\s+GL_OES_standard_derivatives\s*:.*$/gm, '');
722
+ ctx.shaderSource(_shaders[id], fullSource);
723
+ },
724
+ glCompileShader: (id) => {
725
+ ctx.compileShader(_shaders[id]);
726
+ if (!ctx.getShaderParameter(_shaders[id], 0x8B81)) { // GL_COMPILE_STATUS
727
+ const log = ctx.getShaderInfoLog(_shaders[id]);
728
+ if (!_shaderFailLogged) {
729
+ _shaderFailLogged = true;
730
+ const src = ctx.getShaderSource(_shaders[id]);
731
+ console.error(`[GL] shader ${id} compile FAILED:\n${log}\nFull source:\n${src}`);
732
+ } else {
733
+ console.error(`[GL] shader ${id} compile FAILED: ${log.split('\n')[0]}`);
734
+ }
735
+ }
736
+ },
737
+ glGetShaderiv: (id, pname, paramsPtr) => {
738
+ if (!paramsPtr) return;
739
+ const s = _shaders[id];
740
+ const GL_INFO_LOG_LENGTH = 0x8B84;
741
+ const GL_SHADER_SOURCE_LENGTH = 0x8B88;
742
+ if (pname === GL_INFO_LOG_LENGTH) {
743
+ const log = ctx.getShaderInfoLog(s) || '';
744
+ i32()[paramsPtr >> 2] = log.length + 1;
745
+ return;
746
+ }
747
+ if (pname === GL_SHADER_SOURCE_LENGTH) {
748
+ const src = ctx.getShaderSource(s) || '';
749
+ i32()[paramsPtr >> 2] = src.length + 1;
750
+ return;
751
+ }
752
+ const result = ctx.getShaderParameter(s, pname);
753
+ i32()[paramsPtr >> 2] = typeof result === 'boolean' ? (result ? 1 : 0) : result;
754
+ },
755
+ glGetShaderInfoLog: (id, bufSize, lengthPtr, infoLogPtr) => {
756
+ const log = ctx.getShaderInfoLog(_shaders[id]) || '';
757
+ const mem = u8();
758
+ const encoded = encoder.encode(log);
759
+ const copyLen = Math.min(encoded.length, bufSize - 1);
760
+ mem.set(encoded.subarray(0, copyLen), infoLogPtr);
761
+ mem[infoLogPtr + copyLen] = 0;
762
+ if (lengthPtr) u32()[lengthPtr >> 2] = copyLen;
763
+ },
764
+
765
+ // ─── Programs ───────────────────────────────────────────────────────
766
+ glCreateProgram: () => {
767
+ const obj = ctx.createProgram();
768
+ return obj ? _allocId(_programs, obj) : 0;
769
+ },
770
+ glDeleteProgram: (id) => {
771
+ if (id > 0 && id < _programs.length && _programs[id]) {
772
+ ctx.deleteProgram(_programs[id]);
773
+ _programs[id] = null;
774
+ _uniformLocs.delete(id);
775
+ }
776
+ },
777
+ glAttachShader: (prog, shader) => ctx.attachShader(_programs[prog], _shaders[shader]),
778
+ glDetachShader: (prog, shader) => ctx.detachShader(_programs[prog], _shaders[shader]),
779
+ glLinkProgram: (prog) => {
780
+ ctx.linkProgram(_programs[prog]);
781
+ if (!ctx.getProgramParameter(_programs[prog], 0x8B82)) { // GL_LINK_STATUS
782
+ console.error(`[GL] program ${prog} link FAILED:`, ctx.getProgramInfoLog(_programs[prog]));
783
+ }
784
+ },
785
+ glUseProgram: (prog) => {
786
+ _currentProgramId = prog;
787
+ ctx.useProgram(prog ? _programs[prog] : null);
788
+ },
789
+ glGetProgramiv: (prog, pname, paramsPtr) => {
790
+ if (!paramsPtr) return;
791
+ const p = _programs[prog];
792
+
793
+ // Emulate GLES pnames not supported by WebGL2's getProgramParameter
794
+ const GL_INFO_LOG_LENGTH = 0x8B84;
795
+ const GL_ACTIVE_UNIFORM_MAX_LENGTH = 0x8B87;
796
+ const GL_ACTIVE_ATTRIBUTE_MAX_LENGTH = 0x8B8A;
797
+ const GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH = 0x8A35;
798
+
799
+ if (pname === GL_ACTIVE_ATTRIBUTE_MAX_LENGTH) {
800
+ const count = ctx.getProgramParameter(p, ctx.ACTIVE_ATTRIBUTES) || 0;
801
+ let maxLen = 0;
802
+ for (let i = 0; i < count; i++) {
803
+ const info = ctx.getActiveAttrib(p, i);
804
+ if (info && info.name.length > maxLen) maxLen = info.name.length;
805
+ }
806
+ i32()[paramsPtr >> 2] = maxLen + 1; // +1 for null terminator
807
+ return;
808
+ }
809
+ if (pname === GL_ACTIVE_UNIFORM_MAX_LENGTH) {
810
+ const count = ctx.getProgramParameter(p, ctx.ACTIVE_UNIFORMS) || 0;
811
+ let maxLen = 0;
812
+ for (let i = 0; i < count; i++) {
813
+ const info = ctx.getActiveUniform(p, i);
814
+ if (info && info.name.length > maxLen) maxLen = info.name.length;
815
+ }
816
+ i32()[paramsPtr >> 2] = maxLen + 1;
817
+ return;
818
+ }
819
+ if (pname === GL_INFO_LOG_LENGTH) {
820
+ const log = ctx.getProgramInfoLog(p) || '';
821
+ i32()[paramsPtr >> 2] = log.length + 1;
822
+ return;
823
+ }
824
+ if (pname === GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH) {
825
+ const count = ctx.getProgramParameter(p, ctx.ACTIVE_UNIFORM_BLOCKS) || 0;
826
+ let maxLen = 0;
827
+ for (let i = 0; i < count; i++) {
828
+ const name = ctx.getActiveUniformBlockName(p, i);
829
+ if (name && name.length > maxLen) maxLen = name.length;
830
+ }
831
+ i32()[paramsPtr >> 2] = maxLen + 1;
832
+ return;
833
+ }
834
+
835
+ const result = ctx.getProgramParameter(p, pname);
836
+ i32()[paramsPtr >> 2] = typeof result === 'boolean' ? (result ? 1 : 0) : result;
837
+ },
838
+ glGetProgramInfoLog: (prog, bufSize, lengthPtr, infoLogPtr) => {
839
+ const log = ctx.getProgramInfoLog(_programs[prog]) || '';
840
+ const mem = u8();
841
+ const encoded = encoder.encode(log);
842
+ const copyLen = Math.min(encoded.length, bufSize - 1);
843
+ mem.set(encoded.subarray(0, copyLen), infoLogPtr);
844
+ mem[infoLogPtr + copyLen] = 0;
845
+ if (lengthPtr) u32()[lengthPtr >> 2] = copyLen;
846
+ },
847
+ glValidateProgram: (prog) => ctx.validateProgram(_programs[prog]),
848
+
849
+ // ─── Attributes / Uniforms ──────────────────────────────────────────
850
+ glBindAttribLocation: (prog, index, namePtr) => {
851
+ ctx.bindAttribLocation(_programs[prog], index, readCString(u8(), namePtr));
852
+ },
853
+ glGetAttribLocation: (prog, namePtr) => {
854
+ return ctx.getAttribLocation(_programs[prog], readCString(u8(), namePtr));
855
+ },
856
+ glGetUniformLocation: (prog, namePtr) => {
857
+ const name = readCString(u8(), namePtr);
858
+ const loc = ctx.getUniformLocation(_programs[prog], name);
859
+ if (!loc) return -1;
860
+
861
+ // Get or create uniform table for this program
862
+ if (!_uniformLocs.has(prog)) {
863
+ _uniformLocs.set(prog, { byName: new Map(), byId: new Map() });
864
+ }
865
+ const entry = _uniformLocs.get(prog);
866
+
867
+ // Check if we already mapped this name
868
+ if (entry.byName.has(name)) {
869
+ return entry.byName.get(name).id;
870
+ }
871
+
872
+ // Assign new integer ID
873
+ const id = _nextUniformId++;
874
+ entry.byName.set(name, { loc, id });
875
+ entry.byId.set(id, loc);
876
+ return id;
877
+ },
878
+
879
+ // ─── Uniforms ───────────────────────────────────────────────────────
880
+ glUniform1i: (loc, v0) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1i(l, v0); },
881
+ glUniform2i: (loc, v0, v1) => { const l = _getUniformLoc(loc); if (l) ctx.uniform2i(l, v0, v1); },
882
+ glUniform3i: (loc, v0, v1, v2) => { const l = _getUniformLoc(loc); if (l) ctx.uniform3i(l, v0, v1, v2); },
883
+ glUniform4i: (loc, v0, v1, v2, v3) => { const l = _getUniformLoc(loc); if (l) ctx.uniform4i(l, v0, v1, v2, v3); },
884
+ glUniform1f: (loc, v0) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1f(l, v0); },
885
+ glUniform2f: (loc, v0, v1) => { const l = _getUniformLoc(loc); if (l) ctx.uniform2f(l, v0, v1); },
886
+ glUniform3f: (loc, v0, v1, v2) => { const l = _getUniformLoc(loc); if (l) ctx.uniform3f(l, v0, v1, v2); },
887
+ glUniform4f: (loc, v0, v1, v2, v3) => { const l = _getUniformLoc(loc); if (l) ctx.uniform4f(l, v0, v1, v2, v3); },
888
+
889
+ glUniform1iv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1iv(l, new Int32Array(getMemory().buffer, ptr, count)); },
890
+ glUniform2iv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform2iv(l, new Int32Array(getMemory().buffer, ptr, count * 2)); },
891
+ glUniform3iv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform3iv(l, new Int32Array(getMemory().buffer, ptr, count * 3)); },
892
+ glUniform4iv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform4iv(l, new Int32Array(getMemory().buffer, ptr, count * 4)); },
893
+ glUniform1fv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1fv(l, new Float32Array(getMemory().buffer, ptr, count)); },
894
+ glUniform2fv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform2fv(l, new Float32Array(getMemory().buffer, ptr, count * 2)); },
895
+ glUniform3fv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform3fv(l, new Float32Array(getMemory().buffer, ptr, count * 3)); },
896
+ glUniform4fv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform4fv(l, new Float32Array(getMemory().buffer, ptr, count * 4)); },
897
+
898
+ glUniformMatrix2fv: (loc, count, transpose, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniformMatrix2fv(l, !!transpose, new Float32Array(getMemory().buffer, ptr, count * 4)); },
899
+ glUniformMatrix3fv: (loc, count, transpose, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniformMatrix3fv(l, !!transpose, new Float32Array(getMemory().buffer, ptr, count * 9)); },
900
+ glUniformMatrix4fv: (loc, count, transpose, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniformMatrix4fv(l, !!transpose, new Float32Array(getMemory().buffer, ptr, count * 16)); },
901
+
902
+ // ─── Vertex attribs ─────────────────────────────────────────────────
903
+ glEnableVertexAttribArray: (index) => ctx.enableVertexAttribArray(index),
904
+ glDisableVertexAttribArray: (index) => {
905
+ _clientAttribs.delete(index);
906
+ ctx.disableVertexAttribArray(index);
907
+ },
908
+ glVertexAttribPointer: (index, size, type, normalized, stride, offset) => {
909
+ if (_boundArrayBuffer === 0 && offset !== 0) {
910
+ _clientAttribs.set(index, { size, type, normalized, stride, wasmPtr: offset });
911
+ } else {
912
+ _clientAttribs.delete(index);
913
+ ctx.vertexAttribPointer(index, size, type, !!normalized, stride, offset);
914
+ }
915
+ },
916
+
917
+ // ─── Drawing ────────────────────────────────────────────────────────
918
+ glDrawArrays: (mode, first, count) => {
919
+ if (_clientAttribs.size > 0) {
920
+ _uploadClientAttribs(first, count);
921
+ ctx.drawArrays(mode, 0, count);
922
+ } else {
923
+ ctx.drawArrays(mode, first, count);
924
+ }
925
+ },
926
+ glDrawElements: (mode, count, type, offsetPtr) => {
927
+ const hasClientIndices = _boundElementBuffer === 0 && offsetPtr !== 0;
928
+ const hasClientAttribs = _clientAttribs.size > 0;
929
+
930
+ if (hasClientAttribs || hasClientIndices) {
931
+ let maxVertex = 0;
932
+ if (hasClientIndices) {
933
+ const mem = getMemory().buffer;
934
+ if (type === 0x1403) {
935
+ const indices = new Uint16Array(mem, offsetPtr, count);
936
+ for (let i = 0; i < count; i++) if (indices[i] > maxVertex) maxVertex = indices[i];
937
+ } else if (type === 0x1405) {
938
+ const indices = new Uint32Array(mem, offsetPtr, count);
939
+ for (let i = 0; i < count; i++) if (indices[i] > maxVertex) maxVertex = indices[i];
940
+ } else {
941
+ const indices = new Uint8Array(mem, offsetPtr, count);
942
+ for (let i = 0; i < count; i++) if (indices[i] > maxVertex) maxVertex = indices[i];
943
+ }
944
+ } else {
945
+ maxVertex = count * 2;
946
+ }
947
+
948
+ if (hasClientAttribs) _uploadClientAttribs(0, maxVertex + 1);
949
+
950
+ if (hasClientIndices) {
951
+ _uploadClientIndices(offsetPtr, count, type);
952
+ ctx.drawElements(mode, count, type, 0);
953
+ ctx.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, _boundElementBuffer ? _buffers[_boundElementBuffer] : null);
954
+ } else {
955
+ ctx.drawElements(mode, count, type, offsetPtr);
956
+ }
957
+ } else {
958
+ ctx.drawElements(mode, count, type, offsetPtr);
959
+ }
960
+ },
961
+
962
+ // ─── FBOs ───────────────────────────────────────────────────────────
963
+ glGenFramebuffers: (n, ptr) => _genObjects(_framebuffers, () => ctx.createFramebuffer(), n, ptr),
964
+ glDeleteFramebuffers: (n, ptr) => _deleteObjects(_framebuffers, (f) => ctx.deleteFramebuffer(f), n, ptr),
965
+ glBindFramebuffer: (target, id) => {
966
+ if (id === 0 && _redirectFBO) {
967
+ ctx.bindFramebuffer(target, _redirectFBO);
968
+ } else {
969
+ ctx.bindFramebuffer(target, id ? _framebuffers[id] : null);
970
+ }
971
+ _checkGL('glBindFramebuffer', [target, id]);
972
+ },
973
+ glCheckFramebufferStatus: (target) => {
974
+ const status = ctx.checkFramebufferStatus(target);
975
+ if (status !== 0x8CD5) { // GL_FRAMEBUFFER_COMPLETE
976
+ console.warn(`glCheckFramebufferStatus: 0x${status.toString(16)} (incomplete)`);
977
+ }
978
+ return status;
979
+ },
980
+ glFramebufferTexture2D: (target, attachment, textarget, tex, level) => {
981
+ ctx.framebufferTexture2D(target, attachment, textarget, tex ? _textures[tex] : null, level);
982
+ _checkGL('glFramebufferTexture2D', arguments);
983
+ },
984
+ glFramebufferRenderbuffer: (target, attachment, rbtarget, rb) => {
985
+ ctx.framebufferRenderbuffer(target, attachment, rbtarget, rb ? _renderbuffers[rb] : null);
986
+ _checkGL('glFramebufferRenderbuffer', arguments);
987
+ },
988
+
989
+ // ─── RBOs ───────────────────────────────────────────────────────────
990
+ glGenRenderbuffers: (n, ptr) => _genObjects(_renderbuffers, () => ctx.createRenderbuffer(), n, ptr),
991
+ glDeleteRenderbuffers: (n, ptr) => _deleteObjects(_renderbuffers, (r) => ctx.deleteRenderbuffer(r), n, ptr),
992
+ glBindRenderbuffer: (target, id) => ctx.bindRenderbuffer(target, id ? _renderbuffers[id] : null),
993
+ glRenderbufferStorage: (target, internalformat, width, height) => {
994
+ const origFmt = internalformat;
995
+ internalformat = _fixInternalFormat(internalformat, 0x1908, 0x1401);
996
+ try {
997
+ ctx.renderbufferStorage(target, internalformat, width, height);
998
+ } catch (e) {
999
+ console.warn(`glRenderbufferStorage fail: orig=0x${origFmt.toString(16)} fixed=0x${internalformat.toString(16)} ${width}x${height}`, e.message);
1000
+ }
1001
+ _checkGL('glRenderbufferStorage', [target, origFmt, width, height]);
1002
+ },
1003
+
1004
+ // ─── Readback ───────────────────────────────────────────────────────
1005
+ glReadPixels: (x, y, width, height, format, type, pixelsPtr) => {
1006
+ const byteLen = width * height * _bytesPerPixel(format, type);
1007
+ ctx.readPixels(x, y, width, height, format, type, _getTypedPixelView(getMemory, type, pixelsPtr, byteLen));
1008
+ },
1009
+
1010
+ // ─── VAOs ───────────────────────────────────────────────────────────
1011
+ glGenVertexArrays: (n, ptr) => _genObjects(_vaos, () => ctx.createVertexArray(), n, ptr),
1012
+ glDeleteVertexArrays: (n, ptr) => _deleteObjects(_vaos, (v) => ctx.deleteVertexArray(v), n, ptr),
1013
+ glBindVertexArray: (id) => ctx.bindVertexArray(id ? _vaos[id] : null),
1014
+
1015
+ glDrawArraysInstanced: (mode, first, count, instancecount) => ctx.drawArraysInstanced(mode, first, count, instancecount),
1016
+ glDrawElementsInstanced: (mode, count, type, offsetPtr, instancecount) => ctx.drawElementsInstanced(mode, count, type, offsetPtr, instancecount),
1017
+ glVertexAttribDivisor: (index, divisor) => ctx.vertexAttribDivisor(index, divisor),
1018
+
1019
+ glDrawBuffers: (n, bufsPtr) => {
1020
+ const view = u32();
1021
+ const arr = [];
1022
+ for (let i = 0; i < n; i++) arr.push(view[(bufsPtr >> 2) + i]);
1023
+ ctx.drawBuffers(arr);
1024
+ },
1025
+
1026
+ // ─── Tex params / state queries ─────────────────────────────────────
1027
+ glTexParameteriv: (target, pname, paramsPtr) => {
1028
+ try { ctx.texParameteri(target, pname, i32()[paramsPtr >> 2]); } catch (e) { /* unsupported */ }
1029
+ },
1030
+ glGetBooleanv: (pname, dataPtr) => {
1031
+ const val = ctx.getParameter(pname);
1032
+ u8()[dataPtr] = val ? 1 : 0;
1033
+ },
1034
+ glGetFloatv: (pname, dataPtr) => {
1035
+ const result = ctx.getParameter(pname);
1036
+ const view = new Float32Array(getMemory().buffer, dataPtr, 16);
1037
+ if (typeof result === 'number') {
1038
+ view[0] = result;
1039
+ } else if (result && result.length) {
1040
+ for (let i = 0; i < result.length; i++) view[i] = result[i];
1041
+ }
1042
+ },
1043
+ glGetStringi: (name, index) => {
1044
+ if (!_glStringiCache) _glStringiCache = {};
1045
+ const key = `${name}_${index}`;
1046
+ if (_glStringiCache[key] !== undefined) return _glStringiCache[key];
1047
+
1048
+ let str;
1049
+ if (name === 0x1F03) { // GL_EXTENSIONS
1050
+ str = index < _allExtensions.length ? _allExtensions[index] : null;
1051
+ } else {
1052
+ str = null;
1053
+ }
1054
+ if (!str) { _glStringiCache[key] = 0; return 0; }
1055
+ const ptr = _writeStringToWasm(str);
1056
+ _glStringiCache[key] = ptr;
1057
+ return ptr;
1058
+ },
1059
+
1060
+ glVertexAttrib1f: (index, x) => ctx.vertexAttrib1f(index, x),
1061
+ glVertexAttrib2f: (index, x, y) => ctx.vertexAttrib2f(index, x, y),
1062
+ glVertexAttrib3f: (index, x, y, z) => ctx.vertexAttrib3f(index, x, y, z),
1063
+ glVertexAttrib4f: (index, x, y, z, w) => ctx.vertexAttrib4f(index, x, y, z, w),
1064
+ glVertexAttrib4fv: (index, vPtr) => {
1065
+ ctx.vertexAttrib4fv(index, new Float32Array(getMemory().buffer, vPtr, 4));
1066
+ },
1067
+ glVertexAttribIPointer: (index, size, type, stride, offset) => {
1068
+ if (_boundArrayBuffer === 0 && offset !== 0) {
1069
+ _clientAttribs.set(index, { size, type, normalized: false, stride, wasmPtr: offset, integer: true });
1070
+ } else {
1071
+ _clientAttribs.delete(index);
1072
+ ctx.vertexAttribIPointer(index, size, type, stride, offset);
1073
+ }
1074
+ },
1075
+ glSampleCoverage: (value, invert) => ctx.sampleCoverage(value, !!invert),
1076
+
1077
+ // ─── Samplers ───────────────────────────────────────────────────────
1078
+ glGenSamplers: (n, ptr) => _genObjects(_samplers, () => ctx.createSampler(), n, ptr),
1079
+ glDeleteSamplers: (n, ptr) => _deleteObjects(_samplers, (s) => ctx.deleteSampler(s), n, ptr),
1080
+ glBindSampler: (unit, id) => ctx.bindSampler(unit, id ? _samplers[id] : null),
1081
+ glSamplerParameteri: (id, pname, param) => ctx.samplerParameteri(_samplers[id], pname, param),
1082
+ glSamplerParameterf: (id, pname, param) => ctx.samplerParameterf(_samplers[id], pname, param),
1083
+
1084
+ // ─── Sync ───────────────────────────────────────────────────────────
1085
+ glFenceSync: (condition, flags) => {
1086
+ const obj = ctx.fenceSync(condition, flags);
1087
+ return obj ? _allocId(_syncs, obj) : 0;
1088
+ },
1089
+ glClientWaitSync: (id, flags, timeout) => {
1090
+ return ctx.clientWaitSync(_syncs[id], flags, timeout);
1091
+ },
1092
+ glDeleteSync: (id) => {
1093
+ if (id > 0 && id < _syncs.length && _syncs[id]) {
1094
+ ctx.deleteSync(_syncs[id]);
1095
+ _syncs[id] = null;
1096
+ }
1097
+ },
1098
+
1099
+ // ─── Buffer mapping (emulated) ──────────────────────────────────────
1100
+ glMapBufferRange: (target, offset, length, access) => {
1101
+ const wasmPtr = _getMapScratch(length);
1102
+ if (wasmPtr === 0) return 0;
1103
+ _mappedBuffers.set(target, { offset, length, access, wasmPtr });
1104
+ return wasmPtr;
1105
+ },
1106
+ glFlushMappedBufferRange: () => {},
1107
+ glUnmapBuffer: (target) => {
1108
+ const mapping = _mappedBuffers.get(target);
1109
+ if (!mapping) return 0;
1110
+ if (mapping.access & 0x0002) { // GL_MAP_WRITE_BIT
1111
+ ctx.bufferSubData(target, mapping.offset, u8().subarray(mapping.wasmPtr, mapping.wasmPtr + mapping.length));
1112
+ }
1113
+ _mappedBuffers.delete(target);
1114
+ _mapScratchUsed = 0;
1115
+ return 1;
1116
+ },
1117
+
1118
+ // ─── Occlusion queries ──────────────────────────────────────────────
1119
+ glGenQueries: (n, ptr) => _genObjects(_queries, () => ctx.createQuery(), n, ptr),
1120
+ glDeleteQueries: (n, ptr) => _deleteObjects(_queries, (q) => ctx.deleteQuery(q), n, ptr),
1121
+ glBeginQuery: (target, id) => ctx.beginQuery(target, _queries[id]),
1122
+ glEndQuery: (target) => ctx.endQuery(target),
1123
+ glGetQueryObjectiv: (id, pname, paramsPtr) => {
1124
+ const result = ctx.getQueryParameter(_queries[id], pname);
1125
+ i32()[paramsPtr >> 2] = typeof result === 'boolean' ? (result ? 1 : 0) : (result || 0);
1126
+ },
1127
+ glGetQueryObjectuiv: (id, pname, paramsPtr) => {
1128
+ const result = ctx.getQueryParameter(_queries[id], pname);
1129
+ u32()[paramsPtr >> 2] = typeof result === 'boolean' ? (result ? 1 : 0) : (result || 0);
1130
+ },
1131
+
1132
+ // ─── FBO extensions ─────────────────────────────────────────────────
1133
+ glBlitFramebuffer: (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) => {
1134
+ ctx.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
1135
+ _checkGL('glBlitFramebuffer', arguments);
1136
+ },
1137
+ glRenderbufferStorageMultisample: (target, samples, internalformat, width, height) => {
1138
+ internalformat = _fixInternalFormat(internalformat, 0x1908, 0x1401);
1139
+ ctx.renderbufferStorageMultisample(target, samples, internalformat, width, height);
1140
+ _checkGL('glRenderbufferStorageMultisample', arguments);
1141
+ },
1142
+ glInvalidateFramebuffer: (target, numAttachments, attachmentsPtr) => {
1143
+ const view = new Uint32Array(getMemory().buffer, attachmentsPtr, numAttachments);
1144
+ ctx.invalidateFramebuffer(target, Array.from(view));
1145
+ },
1146
+
1147
+ // ─── Shader introspection ───────────────────────────────────────────
1148
+ glGetActiveUniform: (prog, index, bufSize, lengthPtr, sizePtr, typePtr, namePtr) => {
1149
+ const info = ctx.getActiveUniform(_programs[prog], index);
1150
+ const mem = u8();
1151
+ if (info && info.name) {
1152
+ const encoded = encoder.encode(info.name);
1153
+ const copyLen = Math.min(encoded.length, bufSize - 1);
1154
+ mem.set(encoded.subarray(0, copyLen), namePtr);
1155
+ mem[namePtr + copyLen] = 0;
1156
+ if (lengthPtr) u32()[lengthPtr >> 2] = copyLen;
1157
+ if (sizePtr) i32()[sizePtr >> 2] = info.size;
1158
+ if (typePtr) u32()[typePtr >> 2] = info.type;
1159
+ } else {
1160
+ if (lengthPtr) u32()[lengthPtr >> 2] = 0;
1161
+ if (namePtr) mem[namePtr] = 0;
1162
+ }
1163
+ },
1164
+ glGetActiveAttrib: (prog, index, bufSize, lengthPtr, sizePtr, typePtr, namePtr) => {
1165
+ const info = ctx.getActiveAttrib(_programs[prog], index);
1166
+ const mem = u8();
1167
+ if (info && info.name) {
1168
+ const encoded = encoder.encode(info.name);
1169
+ const copyLen = Math.min(encoded.length, bufSize - 1);
1170
+ mem.set(encoded.subarray(0, copyLen), namePtr);
1171
+ mem[namePtr + copyLen] = 0;
1172
+ if (lengthPtr) u32()[lengthPtr >> 2] = copyLen;
1173
+ if (sizePtr) i32()[sizePtr >> 2] = info.size;
1174
+ if (typePtr) u32()[typePtr >> 2] = info.type;
1175
+ } else {
1176
+ if (lengthPtr) u32()[lengthPtr >> 2] = 0;
1177
+ if (namePtr) mem[namePtr] = 0;
1178
+ }
1179
+ },
1180
+ glGetShaderSource: (id, bufSize, lengthPtr, sourcePtr) => {
1181
+ const source = ctx.getShaderSource(_shaders[id]) || '';
1182
+ const mem = u8();
1183
+ const encoded = encoder.encode(source);
1184
+ const copyLen = Math.min(encoded.length, bufSize - 1);
1185
+ mem.set(encoded.subarray(0, copyLen), sourcePtr);
1186
+ mem[sourcePtr + copyLen] = 0;
1187
+ if (lengthPtr) u32()[lengthPtr >> 2] = copyLen;
1188
+ },
1189
+
1190
+ // ─── State queries ──────────────────────────────────────────────────
1191
+ glGetInternalformativ: (target, internalformat, pname, count, paramsPtr) => {
1192
+ const view = new Int32Array(getMemory().buffer, paramsPtr, count);
1193
+ try {
1194
+ // WebGL2 doesn't support GL_NUM_SAMPLE_COUNTS - derive from GL_SAMPLES
1195
+ const GL_NUM_SAMPLE_COUNTS = 0x9380;
1196
+ const GL_SAMPLES = 0x80A9;
1197
+ if (pname === GL_NUM_SAMPLE_COUNTS) {
1198
+ const samples = ctx.getInternalformatParameter(target, internalformat, GL_SAMPLES);
1199
+ view[0] = (samples && samples.length) ? samples.length + 1 : 1; // +1 for non-MSAA (1 sample)
1200
+ return;
1201
+ }
1202
+ const result = ctx.getInternalformatParameter(target, internalformat, pname);
1203
+ if (result === null) {
1204
+ view[0] = 0;
1205
+ } else if (typeof result === 'number') {
1206
+ view[0] = result;
1207
+ } else if (result && result.length !== undefined) {
1208
+ // For GL_SAMPLES, prepend 1 (non-MSAA) if not present
1209
+ let vals = Array.from(result);
1210
+ if (pname === GL_SAMPLES && !vals.includes(1)) vals.push(1);
1211
+ for (let i = 0; i < Math.min(vals.length, count); i++) view[i] = vals[i];
1212
+ }
1213
+ } catch (e) {
1214
+ // If format not renderable, return 0
1215
+ view[0] = 0;
1216
+ }
1217
+ },
1218
+ glGetShaderPrecisionFormat: (shaderType, precisionType, rangePtr, precisionPtr) => {
1219
+ const result = ctx.getShaderPrecisionFormat(shaderType, precisionType);
1220
+ const view = new Int32Array(getMemory().buffer);
1221
+ if (result) {
1222
+ view[rangePtr >> 2] = result.rangeMin;
1223
+ view[(rangePtr >> 2) + 1] = result.rangeMax;
1224
+ view[precisionPtr >> 2] = result.precision;
1225
+ }
1226
+ },
1227
+ glGetBufferParameteriv: (target, pname, paramsPtr) => {
1228
+ const view = new Int32Array(getMemory().buffer, paramsPtr, 1);
1229
+ view[0] = ctx.getBufferParameter(target, pname) || 0;
1230
+ },
1231
+ glGetFramebufferAttachmentParameteriv: (target, attachment, pname, paramsPtr) => {
1232
+ const view = new Int32Array(getMemory().buffer, paramsPtr, 1);
1233
+ const result = ctx.getFramebufferAttachmentParameter(target, attachment, pname);
1234
+ view[0] = typeof result === 'number' ? result : 0;
1235
+ },
1236
+ glGetRenderbufferParameteriv: (target, pname, paramsPtr) => {
1237
+ const view = new Int32Array(getMemory().buffer, paramsPtr, 1);
1238
+ view[0] = ctx.getRenderbufferParameter(target, pname) || 0;
1239
+ },
1240
+ glIsEnabled: (cap) => ctx.isEnabled(cap) ? 1 : 0,
1241
+ glIsTexture: (id) => (id > 0 && _textures[id]) ? (ctx.isTexture(_textures[id]) ? 1 : 0) : 0,
1242
+ glIsBuffer: (id) => (id > 0 && _buffers[id]) ? (ctx.isBuffer(_buffers[id]) ? 1 : 0) : 0,
1243
+ glIsFramebuffer: (id) => (id > 0 && _framebuffers[id]) ? (ctx.isFramebuffer(_framebuffers[id]) ? 1 : 0) : 0,
1244
+ glIsRenderbuffer: (id) => (id > 0 && _renderbuffers[id]) ? (ctx.isRenderbuffer(_renderbuffers[id]) ? 1 : 0) : 0,
1245
+ glIsProgram: (id) => (id > 0 && _programs[id]) ? (ctx.isProgram(_programs[id]) ? 1 : 0) : 0,
1246
+ glIsShader: (id) => (id > 0 && _shaders[id]) ? (ctx.isShader(_shaders[id]) ? 1 : 0) : 0,
1247
+
1248
+ glGetVertexAttribiv: (index, pname, paramsPtr) => {
1249
+ const val = ctx.getVertexAttrib(index, pname);
1250
+ i32()[paramsPtr >> 2] = typeof val === 'boolean' ? (val ? 1 : 0) : (val || 0);
1251
+ },
1252
+ glGetVertexAttribfv: (index, pname, paramsPtr) => {
1253
+ const val = ctx.getVertexAttrib(index, pname);
1254
+ new Float32Array(getMemory().buffer, paramsPtr, 1)[0] = val || 0;
1255
+ },
1256
+ glGetVertexAttribPointerv: (index, pname, pointerPtr) => {
1257
+ const val = ctx.getVertexAttribOffset(index, pname);
1258
+ u32()[pointerPtr >> 2] = val || 0;
1259
+ },
1260
+ glGetRenderbufferParameteriv: (target, pname, paramsPtr) => {
1261
+ const val = ctx.getRenderbufferParameter(target, pname);
1262
+ i32()[paramsPtr >> 2] = val || 0;
1263
+ },
1264
+ glGetFramebufferAttachmentParameteriv: (target, attachment, pname, paramsPtr) => {
1265
+ const val = ctx.getFramebufferAttachmentParameter(target, attachment, pname);
1266
+ // Value might be a WebGL object (texture/renderbuffer) for OBJECT_TYPE/OBJECT_NAME queries
1267
+ if (val && typeof val === 'object' && val._id !== undefined) {
1268
+ i32()[paramsPtr >> 2] = val._id;
1269
+ } else {
1270
+ i32()[paramsPtr >> 2] = val || 0;
1271
+ }
1272
+ },
1273
+ glGetBufferParameteriv: (target, pname, paramsPtr) => {
1274
+ const val = ctx.getBufferParameter(target, pname);
1275
+ i32()[paramsPtr >> 2] = val || 0;
1276
+ },
1277
+ glGetTexParameteriv: (target, pname, paramsPtr) => {
1278
+ const val = ctx.getTexParameter(target, pname);
1279
+ i32()[paramsPtr >> 2] = val || 0;
1280
+ },
1281
+ glGetTexParameterfv: (target, pname, paramsPtr) => {
1282
+ const val = ctx.getTexParameter(target, pname);
1283
+ new Float32Array(getMemory().buffer, paramsPtr, 1)[0] = val || 0;
1284
+ },
1285
+ glGetUniformiv: (prog, loc, paramsPtr) => {
1286
+ const l = _getUniformLoc(loc);
1287
+ if (!l) { i32()[paramsPtr >> 2] = 0; return; }
1288
+ const val = ctx.getUniform(_programs[prog], l);
1289
+ i32()[paramsPtr >> 2] = val || 0;
1290
+ },
1291
+ glGetUniformfv: (prog, loc, paramsPtr) => {
1292
+ const l = _getUniformLoc(loc);
1293
+ if (!l) { new Float32Array(getMemory().buffer, paramsPtr, 1)[0] = 0; return; }
1294
+ const val = ctx.getUniform(_programs[prog], l);
1295
+ new Float32Array(getMemory().buffer, paramsPtr, 1)[0] = val || 0;
1296
+ },
1297
+
1298
+ // ─── 3D textures ────────────────────────────────────────────────────
1299
+ glTexImage3D: (target, level, internalformat, width, height, depth, border, format, type, pixelsPtr) => {
1300
+ internalformat = _fixInternalFormat(internalformat, format, type);
1301
+ format = _fixFormat(format);
1302
+ if (pixelsPtr === 0) {
1303
+ ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, null);
1304
+ } else {
1305
+ const byteLen = width * height * depth * _bytesPerPixel(format, type);
1306
+ ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, _getTypedPixelView(getMemory, type, pixelsPtr, byteLen));
1307
+ }
1308
+ },
1309
+ glTexSubImage3D: (target, level, xoff, yoff, zoff, width, height, depth, format, type, pixelsPtr) => {
1310
+ format = _fixFormat(format);
1311
+ const byteLen = width * height * depth * _bytesPerPixel(format, type);
1312
+ ctx.texSubImage3D(target, level, xoff, yoff, zoff, width, height, depth, format, type, _getTypedPixelView(getMemory, type, pixelsPtr, byteLen));
1313
+ },
1314
+ glTexStorage2D: (target, levels, internalformat, width, height) => {
1315
+ internalformat = _fixInternalFormat(internalformat, 0x1908, 0x1401);
1316
+ ctx.texStorage2D(target, levels, internalformat, width, height);
1317
+ },
1318
+ glTexStorage3D: (target, levels, internalformat, width, height, depth) => {
1319
+ internalformat = _fixInternalFormat(internalformat, 0x1908, 0x1401);
1320
+ ctx.texStorage3D(target, levels, internalformat, width, height, depth);
1321
+ },
1322
+ glCompressedTexImage3D: (target, level, internalformat, width, height, depth, border, imageSize, dataPtr) => {
1323
+ ctx.compressedTexImage3D(target, level, internalformat, width, height, depth, border, u8().subarray(dataPtr, dataPtr + imageSize));
1324
+ },
1325
+ glCompressedTexSubImage3D: (target, level, xoff, yoff, zoff, width, height, depth, format, imageSize, dataPtr) => {
1326
+ ctx.compressedTexSubImage3D(target, level, xoff, yoff, zoff, width, height, depth, format, u8().subarray(dataPtr, dataPtr + imageSize));
1327
+ },
1328
+ glFramebufferTextureLayer: (target, attachment, tex, level, layer) => {
1329
+ ctx.framebufferTextureLayer(target, attachment, tex ? _textures[tex] : null, level, layer);
1330
+ },
1331
+ glReadBuffer: (mode) => {
1332
+ ctx.readBuffer(mode);
1333
+ _checkGL('glReadBuffer', [mode]);
1334
+ },
1335
+
1336
+ // ─── UBO / buffer binding ───────────────────────────────────────────
1337
+ glBindBufferBase: (target, index, id) => ctx.bindBufferBase(target, index, id ? _buffers[id] : null),
1338
+ glBindBufferRange: (target, index, id, offset, size) => ctx.bindBufferRange(target, index, id ? _buffers[id] : null, offset, size),
1339
+ glGetUniformBlockIndex: (prog, namePtr) => {
1340
+ return ctx.getUniformBlockIndex(_programs[prog], readCString(u8(), namePtr));
1341
+ },
1342
+ glUniformBlockBinding: (prog, blockIndex, blockBinding) => {
1343
+ ctx.uniformBlockBinding(_programs[prog], blockIndex, blockBinding);
1344
+ },
1345
+ glCopyBufferSubData: (readTarget, writeTarget, readOffset, writeOffset, size) => {
1346
+ ctx.copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size);
1347
+ },
1348
+
1349
+ // ─── Unsigned int uniforms ──────────────────────────────────────────
1350
+ glUniform1ui: (loc, v0) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1ui(l, v0); },
1351
+ glUniform1uiv: (loc, count, ptr) => { const l = _getUniformLoc(loc); if (l) ctx.uniform1uiv(l, new Uint32Array(getMemory().buffer, ptr, count)); },
1352
+
1353
+ // ─── Clear buffer ───────────────────────────────────────────────────
1354
+ glClearBufferfv: (buffer, drawbuffer, valuePtr) => {
1355
+ const GL_COLOR = 0x1800;
1356
+ const numFloats = (buffer === GL_COLOR) ? 4 : 1;
1357
+ ctx.clearBufferfv(buffer, drawbuffer, new Float32Array(getMemory().buffer, valuePtr, numFloats));
1358
+ },
1359
+
1360
+ // ─── Transform feedback ─────────────────────────────────────────────
1361
+ glBeginTransformFeedback: (primitiveMode) => ctx.beginTransformFeedback(primitiveMode),
1362
+ glEndTransformFeedback: () => ctx.endTransformFeedback(),
1363
+ glTransformFeedbackVaryings: (prog, count, varyingsPtr, bufferMode) => {
1364
+ const ptrs = new Uint32Array(getMemory().buffer, varyingsPtr, count);
1365
+ const names = [];
1366
+ const mem = u8();
1367
+ for (let i = 0; i < count; i++) names.push(readCString(mem, ptrs[i]));
1368
+ ctx.transformFeedbackVaryings(_programs[prog], names, bufferMode);
1369
+ },
1370
+
1371
+ // ─── Program binary (no-op for WebGL) ───────────────────────────────
1372
+ glGetProgramBinary: () => {},
1373
+ glProgramBinary: () => {},
1374
+
1375
+ // ─── Integer attribs ────────────────────────────────────────────────
1376
+ glVertexAttribI4ui: (index, x, y, z, w) => ctx.vertexAttribI4ui(index, x, y, z, w),
1377
+ };
1378
+
1379
+ // GL call tracing
1380
+ if (typeof process !== 'undefined' && process.env?.GL_TRACE === '1') {
1381
+ process.stderr.write(`[webgl-trace] Wrapping ${Object.keys(funcs).length} GL functions\n`);
1382
+ const raw = { ...funcs };
1383
+ for (const name of Object.keys(funcs)) {
1384
+ const orig = raw[name];
1385
+ funcs[name] = (...args) => {
1386
+ const ret = orig(...args);
1387
+ process.stderr.write(`[gl] ${name}(${args.map(a => typeof a === 'number' ? '0x'+a.toString(16) : a).join(',')}) => ${ret}\n`);
1388
+ return ret;
1389
+ };
1390
+ }
1391
+ }
1392
+
1393
+ if (typeof process !== 'undefined' && process.env?.GL_CHECK_ALL === '1') {
1394
+ const skip = new Set(['glGetError']);
1395
+ const raw = { ...funcs };
1396
+ for (const name of Object.keys(funcs)) {
1397
+ if (skip.has(name)) continue;
1398
+ const orig = raw[name];
1399
+ funcs[name] = (...args) => {
1400
+ const ret = orig(...args);
1401
+ const err = ctx.getError();
1402
+ if (err !== 0) {
1403
+ console.error(`GL ERROR 0x${err.toString(16)} after ${name}(${Array.from(args).map(a => typeof a === 'number' ? '0x'+a.toString(16) : a).join(', ')})`);
1404
+ }
1405
+ return ret;
1406
+ };
1407
+ }
1408
+ process.stderr.write(`[webgl-check-all] Wrapped ${Object.keys(funcs).length} GL functions\n`);
1409
+ }
1410
+
1411
+ // Expose FBO redirect controls to the host
1412
+ funcs._setupRedirectFBO = _ensureRedirectFBO;
1413
+ funcs._blitToCanvas = _blitRedirectToCanvas;
1414
+
1415
+ return funcs;
1416
+ }
1417
+
1418
+ // ─── Helpers ──────────────────────────────────────────────────────────────
1419
+
1420
+ const GL_ALPHA = 0x1906;
1421
+ const GL_RGB = 0x1907;
1422
+ const GL_RGBA = 0x1908;
1423
+ const GL_LUMINANCE = 0x1909;
1424
+ const GL_LUMINANCE_ALPHA = 0x190A;
1425
+ const GL_RED = 0x1903;
1426
+ const GL_RG = 0x8227;
1427
+ const GL_UNSIGNED_BYTE = 0x1401;
1428
+ const GL_UNSIGNED_SHORT_5_6_5 = 0x8363;
1429
+ const GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033;
1430
+ const GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034;
1431
+ const GL_FLOAT = 0x1406;
1432
+ const GL_HALF_FLOAT = 0x140B;
1433
+ const GL_UNSIGNED_INT = 0x1405;
1434
+ const GL_UNSIGNED_SHORT = 0x1403;
1435
+ const GL_DEPTH_COMPONENT = 0x1902;
1436
+
1437
+ function _bytesPerPixel(format, type) {
1438
+ let channels;
1439
+ switch (format) {
1440
+ case GL_ALPHA: case GL_LUMINANCE: case GL_RED: case GL_DEPTH_COMPONENT: channels = 1; break;
1441
+ case GL_LUMINANCE_ALPHA: case GL_RG: channels = 2; break;
1442
+ case GL_RGB: case 0x80E0: channels = 3; break; // GL_RGB, GL_BGR
1443
+ case GL_RGBA: case 0x80E1: channels = 4; break; // GL_RGBA, GL_BGRA
1444
+ default: channels = 4; break;
1445
+ }
1446
+
1447
+ switch (type) {
1448
+ case GL_UNSIGNED_BYTE: return channels;
1449
+ case GL_UNSIGNED_SHORT_5_6_5:
1450
+ case GL_UNSIGNED_SHORT_4_4_4_4:
1451
+ case GL_UNSIGNED_SHORT_5_5_5_1: return 2;
1452
+ case GL_HALF_FLOAT:
1453
+ case GL_UNSIGNED_SHORT: return channels * 2;
1454
+ case GL_FLOAT: return channels * 4;
1455
+ case GL_UNSIGNED_INT: return channels * 4;
1456
+ default: return channels;
1457
+ }
1458
+ }
1459
+
1460
+ // Get a properly typed ArrayBufferView for texture upload.
1461
+ // WebGL2 requires the view type to match the GL type parameter.
1462
+ function _getTypedPixelView(getMemory, type, pixelsPtr, byteLen) {
1463
+ const buf = getMemory().buffer;
1464
+ switch (type) {
1465
+ case GL_FLOAT:
1466
+ if (pixelsPtr & 3) return new Float32Array(new Uint8Array(buf, pixelsPtr, byteLen).slice().buffer);
1467
+ return new Float32Array(buf, pixelsPtr, byteLen >> 2);
1468
+ case GL_HALF_FLOAT:
1469
+ case 0x8D61: // GL_HALF_FLOAT_OES
1470
+ case GL_UNSIGNED_SHORT:
1471
+ case GL_UNSIGNED_SHORT_5_6_5:
1472
+ case GL_UNSIGNED_SHORT_4_4_4_4:
1473
+ case GL_UNSIGNED_SHORT_5_5_5_1:
1474
+ if (pixelsPtr & 1) return new Uint16Array(new Uint8Array(buf, pixelsPtr, byteLen).slice().buffer);
1475
+ return new Uint16Array(buf, pixelsPtr, byteLen >> 1);
1476
+ case GL_UNSIGNED_INT:
1477
+ case 0x84FA: // GL_UNSIGNED_INT_24_8
1478
+ if (pixelsPtr & 3) return new Uint32Array(new Uint8Array(buf, pixelsPtr, byteLen).slice().buffer);
1479
+ return new Uint32Array(buf, pixelsPtr, byteLen >> 2);
1480
+ default:
1481
+ return new Uint8Array(buf, pixelsPtr, byteLen);
1482
+ }
1483
+ }