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.
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/SPEC.md +477 -0
- package/bin/wasmcart-pack.js +257 -0
- package/docs/bind_framebuffer.md +275 -0
- package/docs/fetch.md +105 -0
- package/docs/gl-surface.md +111 -0
- package/docs/input.md +102 -0
- package/docs/networking.md +78 -0
- package/docs/porting.md +88 -0
- package/include/wc_cart.h +144 -0
- package/include/wc_fb.h +275 -0
- package/include/wc_gl.h +224 -0
- package/include/wc_gl_blit.h +129 -0
- package/include/wc_mat4.h +210 -0
- package/include/wc_math.h +116 -0
- package/include/wc_pcm_mixer.h +487 -0
- package/include/wc_vec3.h +80 -0
- package/index.js +3 -0
- package/package.json +55 -0
- package/src/CartHost.js +1713 -0
- package/src/CartHostWeb.js +1381 -0
- package/src/abi.js +94 -0
- package/src/cartWorker.js +201 -0
- package/src/cartWorkerWeb.js +170 -0
- package/src/webgl_imports.js +1483 -0
- package/web.js +3 -0
|
@@ -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
|
+
}
|