webgl2 1.2.0 → 1.2.1

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/coverage.md CHANGED
@@ -8,44 +8,48 @@
8
8
  | src/decompiler/emitter.rs | 17 | 8 | 25 | 68.00% 🟡 |
9
9
  | src/decompiler/lifter.rs | 17 | 21 | 38 | 44.74% 🟠 |
10
10
  | src/decompiler/mod.rs | 8 | 6 | 14 | 57.14% 🟡 |
11
- | src/decompiler/module.rs | 1 | 1 | 2 | 50.00% 🟡 |
12
- | src/decompiler/parser.rs | 17 | 2 | 19 | 89.47% 🟢 |
13
- | src/decompiler/simplifier.rs | 53 | 19 | 72 | 73.61% 🟡 |
14
- | src/error.rs | 7 | 15 | 22 | 31.82% 🟠 |
15
- | src/lib.rs | 315 | 210 | 525 | 60.00% 🟡 |
11
+ | src/decompiler/module.rs | 3 | 1 | 4 | 75.00% 🟡 |
12
+ | src/decompiler/parser.rs | 15 | 2 | 17 | 88.24% 🟢 |
13
+ | src/decompiler/simplifier.rs | 54 | 19 | 73 | 73.97% 🟡 |
14
+ | src/error.rs | 8 | 15 | 23 | 34.78% 🟠 |
15
+ | src/lib.rs | 314 | 224 | 538 | 58.36% 🟡 |
16
16
  | src/naga_wasm_backend/backend.rs | 62 | 2 | 64 | 96.88% 🟢 |
17
- | src/naga_wasm_backend/call_lowering.rs | 18 | 0 | 18 | 100.00% 🟢 |
18
- | src/naga_wasm_backend/control_flow.rs | 54 | 16 | 70 | 77.14% 🟡 |
17
+ | src/naga_wasm_backend/call_lowering.rs | 19 | 0 | 19 | 100.00% 🟢 |
18
+ | src/naga_wasm_backend/control_flow.rs | 30 | 40 | 70 | 42.86% 🟠 |
19
19
  | src/naga_wasm_backend/debug/stub.rs | 20 | 0 | 20 | 100.00% 🟢 |
20
- | src/naga_wasm_backend/expressions.rs | 65 | 105 | 170 | 38.24% 🟠 |
21
- | src/naga_wasm_backend/function_abi.rs | 22 | 0 | 22 | 100.00% 🟢 |
20
+ | src/naga_wasm_backend/expressions.rs | 65 | 109 | 174 | 37.36% 🟠 |
21
+ | src/naga_wasm_backend/function_abi.rs | 25 | 0 | 25 | 100.00% 🟢 |
22
22
  | src/naga_wasm_backend/functions/prep.rs | 4 | 0 | 4 | 100.00% 🟢 |
23
- | src/naga_wasm_backend/functions/registry.rs | 4 | 1 | 5 | 80.00% 🟢 |
23
+ | src/naga_wasm_backend/functions/registry.rs | 4 | 0 | 4 | 100.00% 🟢 |
24
24
  | src/naga_wasm_backend/memory_layout.rs | 9 | 0 | 9 | 100.00% 🟢 |
25
- | src/naga_wasm_backend/output_layout.rs | 2 | 2 | 4 | 50.00% 🟡 |
25
+ | src/naga_wasm_backend/mod.rs | 1 | 0 | 1 | 100.00% 🟢 |
26
+ | src/naga_wasm_backend/output_layout.rs | 4 | 2 | 6 | 66.67% 🟡 |
26
27
  | src/naga_wasm_backend/types.rs | 11 | 0 | 11 | 100.00% 🟢 |
27
- | src/wasm_gl_emu/framebuffer.rs | 1 | 0 | 1 | 100.00% 🟢 |
28
- | src/wasm_gl_emu/rasterizer.rs | 26 | 18 | 44 | 59.09% 🟡 |
29
- | src/webgl2_context/blend.rs | 2 | 1 | 3 | 66.67% 🟡 |
30
- | src/webgl2_context/buffers.rs | 18 | 1 | 19 | 94.74% 🟢 |
31
- | src/webgl2_context/drawing.rs | 26 | 3 | 29 | 89.66% 🟢 |
32
- | src/webgl2_context/framebuffers.rs | 10 | 0 | 10 | 100.00% 🟢 |
28
+ | src/wasm_gl_emu/device.rs | 20 | 4 | 24 | 83.33% 🟢 |
29
+ | src/wasm_gl_emu/framebuffer.rs | 3 | 1 | 4 | 75.00% 🟡 |
30
+ | src/wasm_gl_emu/rasterizer.rs | 32 | 18 | 50 | 64.00% 🟡 |
31
+ | src/wasm_gl_emu/transfer.rs | 19 | 7 | 26 | 73.08% 🟡 |
32
+ | src/webgl2_context/blend.rs | 0 | 3 | 3 | 0.00% 🟡 |
33
+ | src/webgl2_context/buffers.rs | 20 | 4 | 24 | 83.33% 🟢 |
34
+ | src/webgl2_context/drawing.rs | 13 | 0 | 13 | 100.00% 🟢 |
35
+ | src/webgl2_context/framebuffers.rs | 11 | 1 | 12 | 91.67% 🟢 |
33
36
  | src/webgl2_context/registry.rs | 7 | 0 | 7 | 100.00% 🟢 |
34
- | src/webgl2_context/renderbuffers.rs | 11 | 0 | 11 | 100.00% 🟢 |
35
- | src/webgl2_context/shaders.rs | 113 | 12 | 125 | 90.40% 🟢 |
36
- | src/webgl2_context/state.rs | 23 | 1 | 24 | 95.83% 🟢 |
37
- | src/webgl2_context/textures.rs | 28 | 17 | 45 | 62.22% 🟡 |
38
- | src/webgl2_context/types.rs | 15 | 1 | 16 | 93.75% 🟢 |
37
+ | src/webgl2_context/renderbuffers.rs | 10 | 0 | 10 | 100.00% 🟢 |
38
+ | src/webgl2_context/shaders.rs | 112 | 14 | 126 | 88.89% 🟢 |
39
+ | src/webgl2_context/state.rs | 16 | 4 | 20 | 80.00% 🟢 |
40
+ | src/webgl2_context/textures.rs | 25 | 9 | 34 | 73.53% 🟡 |
41
+ | src/webgl2_context/types.rs | 16 | 0 | 16 | 100.00% 🟢 |
39
42
  | src/webgl2_context/vaos.rs | 35 | 0 | 35 | 100.00% 🟢 |
40
- | src/webgpu/adapter.rs | 3 | 0 | 3 | 100.00% 🟢 |
41
- | src/webgpu/backend.rs | 25 | 27 | 52 | 48.08% 🟠 |
42
- | **Total** | **1049** | **490** | **1539** | **68.16% 🟡** |
43
+ | src/webgpu/adapter.rs | 2 | 0 | 2 | 100.00% 🟢 |
44
+ | src/webgpu/backend.rs | 70 | 26 | 96 | 72.92% 🟡 |
45
+ | src/webgpu/command.rs | 3 | 1 | 4 | 75.00% 🟡 |
46
+ | **Total** | **1104** | **542** | **1646** | **67.07% 🟡** |
43
47
 
44
48
  ## Top Missed Files
45
49
 
46
50
  | File | Lines Missed | Illustrative Line | Coverage |
47
51
  |---|---|---|---:|
48
- | src/lib.rs | 210/525 | [1040] `/// Get active attribute info.` | 60.00% 🟡 |
49
- | src/naga_wasm_backend/expressions.rs | 105/170 | [1504] `for j in 0..count {` | 38.24% 🟠 |
50
- | src/webgpu/backend.rs | 27/52 | [965] `}` | 48.08% 🟠 |
51
- | src/decompiler/lifter.rs | 21/38 | [455] `fn unary_op(&mut self, op: UnaryOp) {` | 44.74% 🟠 |
52
+ | src/lib.rs | 224/538 | [1033] `/// Use a program.` | 58.36% 🟡 |
53
+ | src/naga_wasm_backend/expressions.rs | 109/174 | [1504] `for j in 0..count {` | 37.36% 🟠 |
54
+ | src/naga_wasm_backend/control_flow.rs | 40/70 | [218] `for (s, s_span) in body.span_iter() {` | 42.86% 🟠 |
55
+ | src/webgpu/backend.rs | 26/96 | [1404] `}` | 72.92% 🟡 |
package/demo.js CHANGED
@@ -616,8 +616,8 @@ async function runTerminalAnimation(width, height, duration = 20000) {
616
616
  firstFrame = false;
617
617
  } else {
618
618
  // Move cursor up to overwrite previous frame
619
- process.stdout.write(`\x1b[${numLines}A`);
620
- process.stdout.write(output);
619
+ process.stdout.write(
620
+ `\x1b[${numLines}A` + output);
621
621
  }
622
622
 
623
623
  lastFrameTime = now;
package/index.js CHANGED
@@ -1,368 +1,404 @@
1
- // @ts-check
2
-
3
- import {
4
- WasmWebGL2RenderingContext,
5
- ERR_OK,
6
- ERR_INVALID_HANDLE,
7
- readErrorMessage,
8
- getShaderModule,
9
- getShaderWat,
10
- getShaderGlsl,
11
- decompileWasmToGlsl
12
- } from './src/webgl2_context.js';
13
- import { GPU, GPUBufferUsage, GPUMapMode, GPUTextureUsage } from './src/webgpu_context.js';
14
-
15
- export const debug = {
16
- getLcovReport,
17
- resetLcovReport
18
- };
19
-
20
- export { ERR_OK, ERR_INVALID_HANDLE, GPUBufferUsage, GPUMapMode, GPUTextureUsage, getShaderModule, getShaderWat, getShaderGlsl, decompileWasmToGlsl };
21
-
22
- /**
23
- * Simple allocator for function table indices.
24
- * Tracks which slots are in use to enable reuse.
25
- */
26
- class TableAllocator {
27
- constructor() {
28
- // Rust uses the first ~1900 slots for its indirect function table (dyn calls, etc).
29
- // We must valid collision by starting allocations after that region.
30
- this.nextIndex = 2000;
31
- this.freeList = [];
32
- }
33
-
34
- allocate() {
35
- if (this.freeList.length > 0) {
36
- return this.freeList.pop();
37
- }
38
- return this.nextIndex++;
39
- }
40
-
41
- free(index) {
42
- this.freeList.push(index);
43
- }
44
- }
45
-
46
- /**
47
- * WebGL2 Prototype: Rust-owned Context, JS thin-forwarder
48
- * Implements docs/1.1.1-webgl2-prototype.md
49
- *
50
- * This module provides:
51
- * - WasmWebGL2RenderingContext: JS class that forwards all calls to WASM
52
- * - webGL2(): factory function to create a new context
53
- *
54
- * WASM owns all runtime state (textures, framebuffers, contexts).
55
- * JS is a thin forwarder with no emulation of WebGL behavior.
56
- *
57
- * Explicit lifecycle: caller must call destroy() to free resources.
58
- * All operations return errno (0 = OK). Non-zero errno causes JS to throw
59
- * with the message from wasm_last_error_ptr/len.
60
- */
61
-
62
-
63
- const isNode =
64
- typeof process !== 'undefined' &&
65
- process.versions != null &&
66
- process.versions.node != null;
67
-
68
- /** @typedef {number} u32 */
69
-
70
- /**
71
- * Factory function: create a new WebGL2 context.
72
- *
73
- * This function:
74
- * 1. Auto-loads webgl2.wasm (expects it next to index2.js)
75
- * 2. Instantiates the WASM module with memory
76
- * 3. Creates a Rust-owned context via wasm_create_context_with_flags(flags)
77
- * 4. Returns a WasmWebGL2RenderingContext JS wrapper
78
- *
79
- * @param {{
80
- * debug?: boolean | 'shaders' | 'rust' | 'all',
81
- * size?: { width: number, height: number },
82
- * }} [opts] - options
83
- * @returns {Promise<WasmWebGL2RenderingContext>}
84
- * @throws {Error} if WASM loading or instantiation fails
85
- */
86
- export async function webGL2({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true', size } = {}) {
87
- // Determine if we need the debug WASM binary (Rust symbols)
88
- const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
89
-
90
- // Load WASM binary
91
- let promise = wasmCache.get(useDebugWasm);
92
- if (!promise) {
93
- promise = initWASM({ debug: useDebugWasm });
94
- wasmCache.set(useDebugWasm, promise);
95
- // ensure success is cached but not failure
96
- promise.catch(() => {
97
- if (wasmCache.get(useDebugWasm) === promise) {
98
- wasmCache.delete(useDebugWasm);
99
- }
100
- });
101
- }
102
- const { ex, instance, sharedTable, tableAllocator } = await promise;
103
-
104
- // Initialize coverage if available
105
- if (ex.wasm_init_coverage && ex.COV_MAP_PTR) {
106
- const mapPtr = ex.COV_MAP_PTR.value;
107
- // Read num_entries from the start of the map data
108
- // mapPtr is aligned to 16 bytes, so we can use Uint32Array
109
- const mem = new Uint32Array(ex.memory.buffer);
110
- const numEntries = mem[mapPtr >>> 2];
111
- ex.wasm_init_coverage(numEntries);
112
- }
113
-
114
- // Determine debug flags for creation
115
- const debugShaders = debug === true || debug === 'shaders' || debug === 'all';
116
- const debugRust = debug === true || debug === 'rust' || debug === 'all';
117
- const flags = (debugShaders ? 1 : 0); // only shader debug encoded in flags
118
-
119
- // Default size to 640x480 if not provided
120
- const width = size?.width ?? 640;
121
- const height = size?.height ?? 480;
122
-
123
- // Create a context in WASM using the flags-aware API (mandatory)
124
- const ctxHandle = ex.wasm_create_context_with_flags(flags, width, height);
125
-
126
- if (ctxHandle === 0) {
127
- const msg = readErrorMessage(instance);
128
- throw new Error(`Failed to create context: ${msg}`);
129
- }
130
-
131
- // Wrap and return, pass debug booleans to the JS wrapper
132
- const gl = new WasmWebGL2RenderingContext({
133
- instance,
134
- ctxHandle,
135
- width,
136
- height,
137
- debugShaders: !!debugShaders,
138
- sharedTable,
139
- tableAllocator
140
- });
141
-
142
- if (size && typeof size.width === 'number' && typeof size.height === 'number') {
143
- gl.resize(size.width, size.height);
144
- gl.viewport(0, 0, size.width, size.height);
145
- }
146
-
147
- return gl;
148
- }
149
-
150
- /**
151
- * Factory function: create a new WebGPU instance.
152
- *
153
- * @param {{
154
- * debug?: boolean | 'shaders' | 'rust' | 'all',
155
- * }} [opts] - options
156
- * @returns {Promise<GPU>}
157
- */
158
- export async function webGPU({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true' } = {}) {
159
- const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
160
- let promise = wasmCache.get(useDebugWasm);
161
- if (!promise) {
162
- promise = initWASM({ debug: useDebugWasm });
163
- wasmCache.set(useDebugWasm, promise);
164
- promise.catch(() => {
165
- if (wasmCache.get(useDebugWasm) === promise) {
166
- wasmCache.delete(useDebugWasm);
167
- }
168
- });
169
- }
170
- const { ex, instance } = await promise;
171
- return new GPU(ex, ex.memory);
172
- }
173
-
174
- /**
175
- * @type {Map<boolean, Promise<{ ex: WebAssembly.Exports, instance: WebAssembly.Instance, module: WebAssembly.Module }>>}
176
- */
177
- const wasmCache = new Map();
178
-
179
- async function initWASM({ debug } = {}) {
180
- const wasmFile = debug ? 'webgl2.debug.wasm' : 'webgl2.wasm';
181
- let wasmBuffer;
182
- if (isNode) {
183
- // Use dynamic imports so this module can be loaded in the browser too.
184
- const path = await import('path');
185
- const fs = await import('fs');
186
- const { fileURLToPath } = await import('url');
187
- const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), wasmFile);
188
- if (!fs.existsSync(wasmPath)) {
189
- throw new Error(`WASM not found at ${wasmPath}. Run: npm run build:wasm`);
190
- }
191
- // readFileSync is available on the imported namespace
192
- wasmBuffer = fs.readFileSync(wasmPath);
193
- } else {
194
- // Browser: fetch the wasm relative to this module
195
- const resp = await fetch(new URL('./' + wasmFile, import.meta.url));
196
- if (!resp.ok) {
197
- throw new Error(`Failed to fetch ${wasmFile}: ${resp.status}`);
198
- }
199
- wasmBuffer = await resp.arrayBuffer();
200
- }
201
-
202
- // Compile WASM module
203
- const wasmModule = await WebAssembly.compile(wasmBuffer);
204
-
205
- // Instantiate WASM (no imports needed, memory is exported)
206
- let instance;
207
-
208
- // Create shared function table for direct shader calls
209
- const sharedTable = new WebAssembly.Table({
210
- initial: 4096, // WASM module requires at least 1982
211
- maximum: 4096, // Prevent unbounded growth
212
- element: "anyfunc"
213
- });
214
- const tableAllocator = new TableAllocator();
215
-
216
- const importObject = {
217
- env: {
218
- __indirect_function_table: sharedTable, // Exact name LLVM expects
219
- print: (ptr, len) => {
220
- const mem = new Uint8Array(instance.exports.memory.buffer);
221
- const bytes = mem.subarray(ptr, ptr + len);
222
- console.log(new TextDecoder('utf-8').decode(bytes));
223
- },
224
- wasm_execute_shader: (ctx, type, tableIdx, attrPtr, uniformPtr, varyingPtr, privatePtr, texturePtr) => {
225
- const gl = WasmWebGL2RenderingContext._contexts.get(ctx);
226
- if (gl) {
227
- gl._executeShader(type, tableIdx, attrPtr, uniformPtr, varyingPtr, privatePtr, texturePtr);
228
- } else {
229
- // console.log(`DEBUG: wasm_execute_shader: ctx ${ctx} not found in _contexts`);
230
- }
231
- },
232
- dispatch_uncaptured_error: (ptr, len) => {
233
- const mem = new Uint8Array(instance.exports.memory.buffer);
234
- const bytes = mem.subarray(ptr, ptr + len);
235
- const msg = new TextDecoder('utf-8').decode(bytes);
236
- if (typeof GPU !== 'undefined' && typeof GPU.dispatchUncapturedError === 'function') {
237
- GPU.dispatchUncapturedError(msg);
238
- } else {
239
- console.error("GPU.dispatchUncapturedError not available", msg);
240
- }
241
- },
242
- // Required by egg crate for timing measurements
243
- now: () => {
244
- return performance.now();
245
- }
246
- },
247
- math: {
248
- sin: Math.sin,
249
- cos: Math.cos,
250
- tan: Math.tan,
251
- asin: Math.asin,
252
- acos: Math.acos,
253
- atan: Math.atan,
254
- atan2: Math.atan2,
255
- exp: Math.exp,
256
- exp2: (x) => Math.pow(2, x),
257
- log: Math.log,
258
- log2: Math.log2,
259
- pow: Math.pow
260
- }
261
- };
262
- instance = await WebAssembly.instantiate(wasmModule, importObject);
263
-
264
- // Verify required exports
265
- const ex = instance.exports;
266
- if (typeof ex.wasm_create_context_with_flags !== 'function') {
267
- throw new Error('WASM module missing wasm_create_context_with_flags export');
268
- }
269
- if (!(ex.memory instanceof WebAssembly.Memory)) {
270
- throw new Error('WASM module missing memory export');
271
- }
272
- return { ex, instance, module: wasmModule, sharedTable, tableAllocator };
273
- }
274
-
275
- /**
276
- * Reads an error message from WASM memory and returns it.
277
- * @param {WebAssembly.Instance} instance
278
- * @returns {string}
279
- */
280
- function _readErrorMessage(instance) {
281
- const ex = instance.exports;
282
- if (!ex || typeof ex.wasm_last_error_ptr !== 'function' || typeof ex.wasm_last_error_len !== 'function') {
283
- return '(no error message available)';
284
- }
285
- const ptr = ex.wasm_last_error_ptr();
286
- const len = ex.wasm_last_error_len();
287
- if (ptr === 0 || len === 0) {
288
- return '';
289
- }
290
- const mem = new Uint8Array(ex.memory.buffer);
291
- const bytes = mem.subarray(ptr, ptr + len);
292
- return new TextDecoder('utf-8').decode(bytes);
293
- }
294
-
295
- /**
296
- * Get LCOV coverage report from a context or device.
297
- * @param {any} glOrGpu
298
- * @returns {string}
299
- */
300
- function getLcovReport(glOrGpu) {
301
- if (!glOrGpu) return '';
302
-
303
- let ex;
304
- if (glOrGpu._instance && glOrGpu._instance.exports) {
305
- ex = glOrGpu._instance.exports;
306
- } else if (glOrGpu.wasm) {
307
- ex = glOrGpu.wasm;
308
- } else if (glOrGpu._instance) {
309
- ex = glOrGpu._instance;
310
- }
311
-
312
- if (ex && typeof ex.wasm_get_lcov_report_ptr === 'function' && typeof ex.wasm_get_lcov_report_len === 'function') {
313
- const ptr = ex.wasm_get_lcov_report_ptr();
314
- const len = ex.wasm_get_lcov_report_len();
315
- if (ptr === 0 || len === 0) return '';
316
- const mem = new Uint8Array(ex.memory.buffer);
317
- const bytes = mem.subarray(ptr, ptr + len);
318
- return new TextDecoder('utf-8').decode(bytes);
319
- }
320
- return '';
321
- }
322
-
323
- /**
324
- * Reset LCOV coverage counters.
325
- * @param {any} glOrGpu
326
- */
327
- export function resetLcovReport(glOrGpu) {
328
- if (!glOrGpu) return;
329
-
330
- let ex;
331
- if (glOrGpu._instance && glOrGpu._instance.exports) {
332
- ex = glOrGpu._instance.exports;
333
- } else if (glOrGpu.wasm) {
334
- ex = glOrGpu.wasm;
335
- } else if (glOrGpu._instance) {
336
- ex = glOrGpu._instance;
337
- }
338
-
339
- if (ex && typeof ex.wasm_reset_coverage === 'function') {
340
- ex.wasm_reset_coverage();
341
- }
342
- }
343
-
344
- /**
345
- * Checks a WASM return code (errno).
346
- * If non-zero, reads the error message and throws.
347
- * @param {number} code
348
- * @param {WebAssembly.Instance} instance
349
- * @throws {Error} if code !== 0
350
- */
351
- function _checkErr(code, instance) {
352
- if (code === ERR_OK) return;
353
- const msg = _readErrorMessage(instance);
354
- throw new Error(`WASM error ${code}: ${msg}`);
355
- }
356
-
357
- if (typeof window !== 'undefined' && window) {
358
- // also populate globals when running in a browser environment
359
- try {
360
- window.webGL2 = webGL2;
361
- window.webGPU = webGPU;
362
- window.getLcovReport = getLcovReport;
363
- window.resetLcovReport = resetLcovReport;
364
- window.WasmWebGL2RenderingContext = WasmWebGL2RenderingContext;
365
- } catch (e) {
366
- // ignore if window is not writable
367
- }
368
- }
1
+ // @ts-check
2
+
3
+ import {
4
+ WasmWebGL2RenderingContext,
5
+ ERR_OK,
6
+ ERR_INVALID_HANDLE,
7
+ readErrorMessage,
8
+ getShaderModule,
9
+ getShaderWat,
10
+ getShaderGlsl,
11
+ decompileWasmToGlsl
12
+ } from './src/webgl2_context.js';
13
+ import { GPU, GPUBufferUsage, GPUMapMode, GPUTextureUsage } from './src/webgpu_context.js';
14
+
15
+ export const debug = {
16
+ getLcovReport,
17
+ resetLcovReport
18
+ };
19
+
20
+ export { ERR_OK, ERR_INVALID_HANDLE, GPUBufferUsage, GPUMapMode, GPUTextureUsage, getShaderModule, getShaderWat, getShaderGlsl, decompileWasmToGlsl };
21
+
22
+ /**
23
+ * Simple allocator for function table indices.
24
+ * Tracks which slots are in use to enable reuse.
25
+ */
26
+ class TableAllocator {
27
+ constructor() {
28
+ // Rust uses the first ~1900 slots for its indirect function table (dyn calls, etc).
29
+ // We must valid collision by starting allocations after that region.
30
+ this.nextIndex = 2000;
31
+ this.freeList = [];
32
+ }
33
+
34
+ allocate() {
35
+ if (this.freeList.length > 0) {
36
+ return this.freeList.pop();
37
+ }
38
+ return this.nextIndex++;
39
+ }
40
+
41
+ free(index) {
42
+ this.freeList.push(index);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * WebGL2 Prototype: Rust-owned Context, JS thin-forwarder
48
+ * Implements docs/1.1.1-webgl2-prototype.md
49
+ *
50
+ * This module provides:
51
+ * - WasmWebGL2RenderingContext: JS class that forwards all calls to WASM
52
+ * - webGL2(): factory function to create a new context
53
+ *
54
+ * WASM owns all runtime state (textures, framebuffers, contexts).
55
+ * JS is a thin forwarder with no emulation of WebGL behavior.
56
+ *
57
+ * Explicit lifecycle: caller must call destroy() to free resources.
58
+ * All operations return errno (0 = OK). Non-zero errno causes JS to throw
59
+ * with the message from wasm_last_error_ptr/len.
60
+ */
61
+
62
+
63
+ const isNode =
64
+ typeof process !== 'undefined' &&
65
+ process.versions != null &&
66
+ process.versions.node != null;
67
+
68
+ /** @typedef {number} u32 */
69
+
70
+ /**
71
+ * Factory function: create a new WebGL2 context.
72
+ *
73
+ * This function:
74
+ * 1. Auto-loads webgl2.wasm (expects it next to index2.js)
75
+ * 2. Instantiates the WASM module with memory
76
+ * 3. Creates a Rust-owned context via wasm_create_context_with_flags(flags)
77
+ * 4. Returns a WasmWebGL2RenderingContext JS wrapper
78
+ *
79
+ * @param {{
80
+ * debug?: boolean | 'shaders' | 'rust' | 'all',
81
+ * size?: { width: number, height: number },
82
+ * }} [opts] - options
83
+ * @returns {Promise<WasmWebGL2RenderingContext>}
84
+ * @throws {Error} if WASM loading or instantiation fails
85
+ */
86
+ export async function webGL2({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true', size } = {}) {
87
+ // Determine if we need the debug WASM binary (Rust symbols)
88
+ const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
89
+
90
+ // Load WASM binary
91
+ let promise = wasmCache.get(useDebugWasm);
92
+ if (!promise) {
93
+ promise = initWASM({ debug: useDebugWasm });
94
+ wasmCache.set(useDebugWasm, promise);
95
+ // ensure success is cached but not failure
96
+ promise.catch(() => {
97
+ if (wasmCache.get(useDebugWasm) === promise) {
98
+ wasmCache.delete(useDebugWasm);
99
+ }
100
+ });
101
+ }
102
+ const { ex, instance, sharedTable, tableAllocator } = await promise;
103
+
104
+ // Initialize coverage if available
105
+ if (ex.wasm_init_coverage && ex.COV_MAP_PTR) {
106
+ const mapPtr = ex.COV_MAP_PTR.value;
107
+ // Read num_entries from the start of the map data
108
+ // mapPtr is aligned to 16 bytes, so we can use Uint32Array
109
+ const mem = new Uint32Array(ex.memory.buffer);
110
+ const numEntries = mem[mapPtr >>> 2];
111
+ ex.wasm_init_coverage(numEntries);
112
+ }
113
+
114
+ // Determine debug flags for creation
115
+ const debugShaders = debug === true || debug === 'shaders' || debug === 'all';
116
+ const debugRust = debug === true || debug === 'rust' || debug === 'all';
117
+ const flags = (debugShaders ? 1 : 0); // only shader debug encoded in flags
118
+
119
+ // Default size to 640x480 if not provided
120
+ const width = size?.width ?? 640;
121
+ const height = size?.height ?? 480;
122
+
123
+ // Create a context in WASM using the flags-aware API (mandatory)
124
+ const ctxHandle = ex.wasm_create_context_with_flags(flags, width, height);
125
+
126
+ if (ctxHandle === 0) {
127
+ const msg = readErrorMessage(instance);
128
+ throw new Error(`Failed to create context: ${msg}`);
129
+ }
130
+
131
+ // Wrap and return, pass debug booleans to the JS wrapper
132
+ const gl = new WasmWebGL2RenderingContext({
133
+ instance,
134
+ ctxHandle,
135
+ width,
136
+ height,
137
+ debugShaders: !!debugShaders,
138
+ sharedTable,
139
+ tableAllocator
140
+ });
141
+
142
+ if (size && typeof size.width === 'number' && typeof size.height === 'number') {
143
+ gl.resize(size.width, size.height);
144
+ gl.viewport(0, 0, size.width, size.height);
145
+ }
146
+
147
+ return gl;
148
+ }
149
+
150
+ /**
151
+ * Factory function: create a new WebGPU instance.
152
+ *
153
+ * @param {{
154
+ * debug?: boolean | 'shaders' | 'rust' | 'all',
155
+ * }} [opts] - options
156
+ * @returns {Promise<GPU>}
157
+ */
158
+ export async function webGPU({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true' } = {}) {
159
+ const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
160
+ let promise = wasmCache.get(useDebugWasm);
161
+ if (!promise) {
162
+ promise = initWASM({ debug: useDebugWasm });
163
+ wasmCache.set(useDebugWasm, promise);
164
+ promise.catch(() => {
165
+ if (wasmCache.get(useDebugWasm) === promise) {
166
+ wasmCache.delete(useDebugWasm);
167
+ }
168
+ });
169
+ }
170
+ const { ex, instance } = await promise;
171
+ return new GPU(ex, ex.memory);
172
+ }
173
+
174
+ /**
175
+ * @type {Map<boolean, Promise<{ ex: WebAssembly.Exports, instance: WebAssembly.Instance, module: WebAssembly.Module }>>}
176
+ */
177
+ const wasmCache = new Map();
178
+
179
+ async function initWASM({ debug } = {}) {
180
+ const wasmFile = debug ? 'webgl2.debug.wasm' : 'webgl2.wasm';
181
+ let wasmBuffer;
182
+ if (isNode) {
183
+ // Use dynamic imports so this module can be loaded in the browser too.
184
+ const path = await import('path');
185
+ const fs = await import('fs');
186
+ const { fileURLToPath } = await import('url');
187
+ const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), wasmFile);
188
+ if (!fs.existsSync(wasmPath)) {
189
+ throw new Error(`WASM not found at ${wasmPath}. Run: npm run build:wasm`);
190
+ }
191
+ // readFileSync is available on the imported namespace
192
+ wasmBuffer = fs.readFileSync(wasmPath);
193
+ } else {
194
+ // Browser: fetch the wasm relative to this module
195
+ const resp = await fetch(new URL('./' + wasmFile, import.meta.url));
196
+ if (!resp.ok) {
197
+ throw new Error(`Failed to fetch ${wasmFile}: ${resp.status}`);
198
+ }
199
+ wasmBuffer = await resp.arrayBuffer();
200
+ }
201
+
202
+ // Compile WASM module
203
+ const wasmModule = await WebAssembly.compile(wasmBuffer);
204
+
205
+ // Instantiate WASM (no imports needed, memory is exported)
206
+ let instance;
207
+
208
+ // Create shared function table for direct shader calls
209
+ const sharedTable = new WebAssembly.Table({
210
+ initial: 4096, // Increased from 4096 to handle larger modules
211
+ maximum: 8192, // Prevent unbounded growth
212
+ element: "anyfunc"
213
+ });
214
+ const tableAllocator = new TableAllocator();
215
+
216
+ const importObject = {
217
+ env: {
218
+ __indirect_function_table: sharedTable, // Exact name LLVM expects
219
+ print: (ptr, len) => {
220
+ const mem = new Uint8Array(instance.exports.memory.buffer);
221
+ const bytes = mem.subarray(ptr, ptr + len);
222
+ console.log(new TextDecoder('utf-8').decode(bytes));
223
+ },
224
+ wasm_execute_shader: (ctx, type, tableIdx, attrPtr, uniformPtr, varyingPtr, privatePtr, texturePtr) => {
225
+ const gl = WasmWebGL2RenderingContext._contexts.get(ctx);
226
+ if (gl) {
227
+ gl._executeShader(type, tableIdx, attrPtr, uniformPtr, varyingPtr, privatePtr, texturePtr);
228
+ } else {
229
+ // General device execution (WebGPU)
230
+ if (tableIdx > 0 && sharedTable) {
231
+ const func = sharedTable.get(tableIdx);
232
+ if (func) {
233
+ func(ctx, type, tableIdx, attrPtr, uniformPtr, varyingPtr, privatePtr, texturePtr);
234
+ }
235
+ }
236
+ }
237
+ },
238
+ wasm_register_shader: (ptr, len) => {
239
+ const mem = new Uint8Array(instance.exports.memory.buffer);
240
+ const bytes = mem.slice(ptr, ptr + len);
241
+ const shaderModule = new WebAssembly.Module(bytes);
242
+ const index = tableAllocator.allocate();
243
+
244
+ const env = {
245
+ memory: instance.exports.memory,
246
+ __indirect_function_table: sharedTable,
247
+ };
248
+
249
+ // Copy math functions
250
+ const mathFuncs = [
251
+ 'gl_cos', 'gl_sin', 'gl_tan', 'gl_acos', 'gl_asin', 'gl_atan', 'gl_atan2',
252
+ 'gl_exp', 'gl_exp2', 'gl_log', 'gl_log2', 'gl_pow', 'gl_floor', 'gl_ceil',
253
+ 'gl_fract', 'gl_mod', 'gl_min', 'gl_max', 'gl_abs', 'gl_sign', 'gl_sqrt',
254
+ 'gl_inversesqrt', 'gl_sinh', 'gl_cosh', 'gl_tanh', 'gl_asinh', 'gl_acosh', 'gl_atanh'
255
+ ];
256
+ for (const name of mathFuncs) {
257
+ if (instance.exports[name]) {
258
+ env[name] = instance.exports[name];
259
+ }
260
+ }
261
+
262
+ const shaderInstance = new WebAssembly.Instance(shaderModule, { env });
263
+ if (shaderInstance.exports.main) {
264
+ sharedTable.set(index, shaderInstance.exports.main);
265
+ }
266
+ return index;
267
+ },
268
+ dispatch_uncaptured_error: (ptr, len) => {
269
+ const mem = new Uint8Array(instance.exports.memory.buffer);
270
+ const bytes = mem.subarray(ptr, ptr + len);
271
+ const msg = new TextDecoder('utf-8').decode(bytes);
272
+ if (typeof GPU !== 'undefined' && typeof GPU.dispatchUncapturedError === 'function') {
273
+ GPU.dispatchUncapturedError(msg);
274
+ } else {
275
+ console.error("GPU.dispatchUncapturedError not available", msg);
276
+ }
277
+ },
278
+ // Required by egg crate for timing measurements
279
+ now: () => {
280
+ return performance.now();
281
+ }
282
+ },
283
+ math: {
284
+ sin: Math.sin,
285
+ cos: Math.cos,
286
+ tan: Math.tan,
287
+ asin: Math.asin,
288
+ acos: Math.acos,
289
+ atan: Math.atan,
290
+ atan2: Math.atan2,
291
+ exp: Math.exp,
292
+ exp2: (x) => Math.pow(2, x),
293
+ log: Math.log,
294
+ log2: Math.log2,
295
+ pow: Math.pow
296
+ }
297
+ };
298
+ instance = await WebAssembly.instantiate(wasmModule, importObject);
299
+
300
+ // Verify required exports
301
+ const ex = instance.exports;
302
+ if (typeof ex.wasm_create_context_with_flags !== 'function') {
303
+ throw new Error('WASM module missing wasm_create_context_with_flags export');
304
+ }
305
+ if (!(ex.memory instanceof WebAssembly.Memory)) {
306
+ throw new Error('WASM module missing memory export');
307
+ }
308
+ return { ex, instance, module: wasmModule, sharedTable, tableAllocator };
309
+ }
310
+
311
+ /**
312
+ * Reads an error message from WASM memory and returns it.
313
+ * @param {WebAssembly.Instance} instance
314
+ * @returns {string}
315
+ */
316
+ function _readErrorMessage(instance) {
317
+ const ex = instance.exports;
318
+ if (!ex || typeof ex.wasm_last_error_ptr !== 'function' || typeof ex.wasm_last_error_len !== 'function') {
319
+ return '(no error message available)';
320
+ }
321
+ const ptr = ex.wasm_last_error_ptr();
322
+ const len = ex.wasm_last_error_len();
323
+ if (ptr === 0 || len === 0) {
324
+ return '';
325
+ }
326
+ const mem = new Uint8Array(ex.memory.buffer);
327
+ const bytes = mem.subarray(ptr, ptr + len);
328
+ return new TextDecoder('utf-8').decode(bytes);
329
+ }
330
+
331
+ /**
332
+ * Get LCOV coverage report from a context or device.
333
+ * @param {any} glOrGpu
334
+ * @returns {string}
335
+ */
336
+ function getLcovReport(glOrGpu) {
337
+ if (!glOrGpu) return '';
338
+
339
+ let ex;
340
+ if (glOrGpu._instance && glOrGpu._instance.exports) {
341
+ ex = glOrGpu._instance.exports;
342
+ } else if (glOrGpu.wasm) {
343
+ ex = glOrGpu.wasm;
344
+ } else if (glOrGpu._instance) {
345
+ ex = glOrGpu._instance;
346
+ }
347
+
348
+ if (ex && typeof ex.wasm_get_lcov_report_ptr === 'function' && typeof ex.wasm_get_lcov_report_len === 'function') {
349
+ const ptr = ex.wasm_get_lcov_report_ptr();
350
+ const len = ex.wasm_get_lcov_report_len();
351
+ if (ptr === 0 || len === 0) return '';
352
+ const mem = new Uint8Array(ex.memory.buffer);
353
+ const bytes = mem.subarray(ptr, ptr + len);
354
+ return new TextDecoder('utf-8').decode(bytes);
355
+ }
356
+ return '';
357
+ }
358
+
359
+ /**
360
+ * Reset LCOV coverage counters.
361
+ * @param {any} glOrGpu
362
+ */
363
+ export function resetLcovReport(glOrGpu) {
364
+ if (!glOrGpu) return;
365
+
366
+ let ex;
367
+ if (glOrGpu._instance && glOrGpu._instance.exports) {
368
+ ex = glOrGpu._instance.exports;
369
+ } else if (glOrGpu.wasm) {
370
+ ex = glOrGpu.wasm;
371
+ } else if (glOrGpu._instance) {
372
+ ex = glOrGpu._instance;
373
+ }
374
+
375
+ if (ex && typeof ex.wasm_reset_coverage === 'function') {
376
+ ex.wasm_reset_coverage();
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Checks a WASM return code (errno).
382
+ * If non-zero, reads the error message and throws.
383
+ * @param {number} code
384
+ * @param {WebAssembly.Instance} instance
385
+ * @throws {Error} if code !== 0
386
+ */
387
+ function _checkErr(code, instance) {
388
+ if (code === ERR_OK) return;
389
+ const msg = _readErrorMessage(instance);
390
+ throw new Error(`WASM error ${code}: ${msg}`);
391
+ }
392
+
393
+ if (typeof window !== 'undefined' && window) {
394
+ // also populate globals when running in a browser environment
395
+ try {
396
+ window.webGL2 = webGL2;
397
+ window.webGPU = webGPU;
398
+ window.getLcovReport = getLcovReport;
399
+ window.resetLcovReport = resetLcovReport;
400
+ window.WasmWebGL2RenderingContext = WasmWebGL2RenderingContext;
401
+ } catch (e) {
402
+ // ignore if window is not writable
403
+ }
404
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webgl2",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "WebGL2 tools to derisk large GPU projects on the web beyond toys and demos.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -30,6 +30,7 @@ export class WasmWebGL2RenderingContext {
30
30
  DEPTH_BUFFER_BIT = 0x00000100;
31
31
  DEPTH_TEST = 0x0B71;
32
32
  STENCIL_TEST = 0x0B90;
33
+ SCISSOR_TEST = 0x0C11;
33
34
  STENCIL_BUFFER_BIT = 0x00000400;
34
35
  COMPILE_STATUS = 0x8B81;
35
36
  LINK_STATUS = 0x8B82;
@@ -37,6 +38,12 @@ export class WasmWebGL2RenderingContext {
37
38
  VALIDATE_STATUS = 0x8B83;
38
39
  ARRAY_BUFFER = 0x8892;
39
40
  ELEMENT_ARRAY_BUFFER = 0x8893;
41
+ COPY_READ_BUFFER = 0x8F36;
42
+ COPY_WRITE_BUFFER = 0x8F37;
43
+ PIXEL_PACK_BUFFER = 0x88EB;
44
+ PIXEL_UNPACK_BUFFER = 0x88EC;
45
+ UNIFORM_BUFFER = 0x8A11;
46
+ TRANSFORM_FEEDBACK_BUFFER = 0x8C8E;
40
47
  STATIC_DRAW = 0x88E4;
41
48
  BYTE = 0x1400;
42
49
  UNSIGNED_BYTE = 0x1401;
@@ -108,6 +115,8 @@ export class WasmWebGL2RenderingContext {
108
115
 
109
116
  RENDERBUFFER = 0x8D41;
110
117
  FRAMEBUFFER = 0x8D40;
118
+ READ_FRAMEBUFFER = 0x8CA8;
119
+ DRAW_FRAMEBUFFER = 0x8CA9;
111
120
  DEPTH_COMPONENT16 = 0x81A5;
112
121
  DEPTH_STENCIL = 0x84F9;
113
122
  RGBA4 = 0x8056;
@@ -1432,7 +1441,22 @@ export class WasmWebGL2RenderingContext {
1432
1441
  ex.wasm_free(ptr);
1433
1442
  }
1434
1443
  }
1435
- copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size) { this._assertNotDestroyed(); throw new Error('not implemented'); }
1444
+ copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size) {
1445
+ this._assertNotDestroyed();
1446
+ const ex = this._instance.exports;
1447
+ if (!ex || typeof ex.wasm_ctx_copy_buffer_sub_data !== 'function') {
1448
+ throw new Error('wasm_ctx_copy_buffer_sub_data not found');
1449
+ }
1450
+ const code = ex.wasm_ctx_copy_buffer_sub_data(
1451
+ this._ctxHandle,
1452
+ readTarget >>> 0,
1453
+ writeTarget >>> 0,
1454
+ readOffset >>> 0,
1455
+ writeOffset >>> 0,
1456
+ size >>> 0
1457
+ );
1458
+ _checkErr(code, this._instance);
1459
+ }
1436
1460
  getBufferParameter(target, pname) {
1437
1461
  this._assertNotDestroyed();
1438
1462
  const ex = this._instance.exports;
@@ -1664,7 +1688,20 @@ export class WasmWebGL2RenderingContext {
1664
1688
  }
1665
1689
 
1666
1690
  checkFramebufferStatus(target) { this._assertNotDestroyed(); throw new Error('not implemented'); }
1667
- blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) { this._assertNotDestroyed(); throw new Error('not implemented'); }
1691
+ blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) {
1692
+ this._assertNotDestroyed();
1693
+ const ex = this._instance.exports;
1694
+ if (!ex || typeof ex.wasm_ctx_blit_framebuffer !== 'function') {
1695
+ throw new Error('wasm_ctx_blit_framebuffer not found');
1696
+ }
1697
+ const code = ex.wasm_ctx_blit_framebuffer(
1698
+ this._ctxHandle,
1699
+ srcX0 | 0, srcY0 | 0, srcX1 | 0, srcY1 | 0,
1700
+ dstX0 | 0, dstY0 | 0, dstX1 | 0, dstY1 | 0,
1701
+ mask >>> 0, filter >>> 0
1702
+ );
1703
+ _checkErr(code, this._instance);
1704
+ }
1668
1705
  readBuffer(src) { this._assertNotDestroyed(); throw new Error('not implemented'); }
1669
1706
 
1670
1707
  pixelStorei(pname, param) { this._assertNotDestroyed(); throw new Error('not implemented'); }
@@ -91,17 +91,17 @@ export class GPU {
91
91
  // In Node.js, Event might not be available globally or might behave differently.
92
92
  // But we are using 'node:test' which implies Node environment.
93
93
  // If Event is not defined, we might need a polyfill or just call the handler directly.
94
-
94
+
95
95
  const error = new GPUError(msg);
96
-
96
+
97
97
  for (const device of activeDevices) {
98
98
  if (typeof device.onuncapturederror === 'function') {
99
99
  device.onuncapturederror({ error });
100
100
  }
101
101
  // Also dispatch as event if supported
102
102
  if (typeof Event !== 'undefined') {
103
- const event = new GPUUncapturedErrorEvent('uncapturederror', { error });
104
- device.dispatchEvent(event);
103
+ const event = new GPUUncapturedErrorEvent('uncapturederror', { error });
104
+ device.dispatchEvent(event);
105
105
  }
106
106
  }
107
107
  }
@@ -200,7 +200,7 @@ export class GPUDevice extends (typeof EventTarget !== 'undefined' ? EventTarget
200
200
  if (filter === 'validation') filterCode = 0;
201
201
  else if (filter === 'out-of-memory') filterCode = 1;
202
202
  else if (filter === 'internal') filterCode = 2;
203
-
203
+
204
204
  this.wasm.wasm_webgpu_push_error_scope(filterCode);
205
205
  }
206
206
 
@@ -379,6 +379,45 @@ export class GPUDevice extends (typeof EventTarget !== 'undefined' ? EventTarget
379
379
  layoutHandle = descriptor.layout.layoutHandle;
380
380
  }
381
381
 
382
+ const primitiveTopology = {
383
+ 'point-list': 1,
384
+ 'line-list': 2,
385
+ 'line-strip': 3,
386
+ 'triangle-list': 4,
387
+ 'triangle-strip': 5,
388
+ }[descriptor.primitive?.topology || 'triangle-list'] || 4;
389
+
390
+ const depthStencil = descriptor.depthStencil;
391
+ const depthFormat = {
392
+ 'depth32float': 1,
393
+ 'depth24plus': 2,
394
+ 'depth24plus-stencil8': 3,
395
+ }[depthStencil?.format] || 0;
396
+
397
+ const depthCompare = {
398
+ 'never': 1,
399
+ 'less': 2,
400
+ 'equal': 3,
401
+ 'less-equal': 4,
402
+ 'greater': 5,
403
+ 'not-equal': 6,
404
+ 'greater-equal': 7,
405
+ 'always': 8,
406
+ }[depthStencil?.depthCompare || 'less'] || 2;
407
+
408
+ const blendFactorMap = {
409
+ 'zero': 0, 'one': 1, 'src': 2, 'one-minus-src': 3,
410
+ 'src-alpha': 4, 'one-minus-src-alpha': 5,
411
+ 'dst': 6, 'one-minus-dst': 7, 'dst-alpha': 8, 'one-minus-dst-alpha': 9,
412
+ };
413
+
414
+ const blendOpMap = {
415
+ 'add': 0, 'subtract': 1, 'reverse-subtract': 2, 'min': 3, 'max': 4,
416
+ };
417
+
418
+ const fragmentTarget = descriptor.fragment.targets?.[0];
419
+ const blend = fragmentTarget?.blend;
420
+
382
421
  const pipelineHandle = this.wasm.wasm_webgpu_create_render_pipeline(
383
422
  this.ctxHandle,
384
423
  this.deviceHandle,
@@ -390,7 +429,18 @@ export class GPUDevice extends (typeof EventTarget !== 'undefined' ? EventTarget
390
429
  fBytes.length,
391
430
  lPtr,
392
431
  layoutData.length,
393
- layoutHandle
432
+ layoutHandle,
433
+ primitiveTopology,
434
+ depthFormat,
435
+ depthStencil?.depthWriteEnabled ? 1 : 0,
436
+ depthCompare,
437
+ blend ? 1 : 0,
438
+ blendFactorMap[blend?.color?.srcFactor] || 0,
439
+ blendFactorMap[blend?.color?.dstFactor] || 0,
440
+ blendOpMap[blend?.color?.operation] || 0,
441
+ blendFactorMap[blend?.alpha?.srcFactor] || 0,
442
+ blendFactorMap[blend?.alpha?.dstFactor] || 0,
443
+ blendOpMap[blend?.alpha?.operation] || 0
394
444
  );
395
445
 
396
446
  this.wasm.wasm_free(vPtr, vBytes.length);
@@ -576,7 +626,66 @@ export class GPUQueue {
576
626
  * @param {number} size
577
627
  */
578
628
  writeBuffer(buffer, bufferOffset, data, dataOffset = 0, size) {
579
- // TODO: Call wasm function to write buffer
629
+ const srcData = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
630
+ const actualSize = size !== undefined ? size : srcData.byteLength - dataOffset;
631
+ const subData = srcData.subarray(dataOffset, dataOffset + actualSize);
632
+
633
+ const ptr = this.wasm.wasm_alloc(subData.byteLength);
634
+ const heap = new Uint8Array(this.memory.buffer, ptr, subData.byteLength);
635
+ heap.set(subData);
636
+
637
+ this.wasm.wasm_webgpu_queue_write_buffer(
638
+ this.ctxHandle,
639
+ this.queueHandle,
640
+ buffer.bufferHandle,
641
+ BigInt(bufferOffset),
642
+ ptr,
643
+ subData.byteLength
644
+ );
645
+
646
+ this.wasm.wasm_free(ptr, subData.byteLength);
647
+ }
648
+
649
+ /**
650
+ * Write data to a texture
651
+ * @param {Object} destination
652
+ * @param {ArrayBuffer|TypedArray} data
653
+ * @param {Object} dataLayout
654
+ * @param {Object} size
655
+ */
656
+ writeTexture(destination, data, dataLayout, size) {
657
+ const srcData = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
658
+ const subData = dataLayout.offset ? srcData.subarray(dataLayout.offset) : srcData;
659
+
660
+ const ptr = this.wasm.wasm_alloc(subData.byteLength);
661
+ const heap = new Uint8Array(this.memory.buffer, ptr, subData.byteLength);
662
+ heap.set(subData);
663
+
664
+ let width, height, depthOrArrayLayers;
665
+ if (Array.isArray(size)) {
666
+ width = size[0];
667
+ height = size[1] || 1;
668
+ depthOrArrayLayers = size[2] || 1;
669
+ } else {
670
+ width = size.width;
671
+ height = size.height || 1;
672
+ depthOrArrayLayers = size.depthOrArrayLayers || 1;
673
+ }
674
+
675
+ this.wasm.wasm_webgpu_queue_write_texture(
676
+ this.ctxHandle,
677
+ this.queueHandle,
678
+ destination.texture.textureHandle,
679
+ ptr,
680
+ subData.byteLength,
681
+ dataLayout.bytesPerRow || 0,
682
+ dataLayout.rowsPerImage || 0,
683
+ width,
684
+ height,
685
+ depthOrArrayLayers
686
+ );
687
+
688
+ this.wasm.wasm_free(ptr, subData.byteLength);
580
689
  }
581
690
  }
582
691
 
@@ -818,6 +927,18 @@ export class GPURenderPassEncoder {
818
927
  this.commands.push(4, index, bindGroup.bindGroupHandle);
819
928
  }
820
929
 
930
+ /**
931
+ * Set index buffer
932
+ * @param {GPUBuffer} buffer
933
+ * @param {string} indexFormat
934
+ * @param {number} offset
935
+ * @param {number} size
936
+ */
937
+ setIndexBuffer(buffer, indexFormat, offset = 0, size) {
938
+ const formatId = indexFormat === 'uint32' ? 2 : 1;
939
+ this.commands.push(5, buffer.bufferHandle, formatId, offset, size || (buffer.size - offset));
940
+ }
941
+
821
942
  /**
822
943
  * Draw vertices
823
944
  * @param {number} vertexCount
@@ -829,6 +950,51 @@ export class GPURenderPassEncoder {
829
950
  this.commands.push(3, vertexCount, instanceCount, firstVertex, firstInstance);
830
951
  }
831
952
 
953
+ /**
954
+ * Draw indexed vertices
955
+ * @param {number} indexCount
956
+ * @param {number} instanceCount
957
+ * @param {number} firstIndex
958
+ * @param {number} baseVertex
959
+ * @param {number} firstInstance
960
+ */
961
+ drawIndexed(indexCount, instanceCount = 1, firstIndex = 0, baseVertex = 0, firstInstance = 0) {
962
+ this.commands.push(6, indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
963
+ }
964
+
965
+ /**
966
+ * Set viewport
967
+ * @param {number} x
968
+ * @param {number} y
969
+ * @param {number} width
970
+ * @param {number} height
971
+ * @param {number} minDepth
972
+ * @param {number} maxDepth
973
+ */
974
+ setViewport(x, y, width, height, minDepth, maxDepth) {
975
+ // Use Float32Array to get bit representation of floats
976
+ const f32 = new Float32Array(6);
977
+ f32[0] = x;
978
+ f32[1] = y;
979
+ f32[2] = width;
980
+ f32[3] = height;
981
+ f32[4] = minDepth;
982
+ f32[5] = maxDepth;
983
+ const u32 = new Uint32Array(f32.buffer);
984
+ this.commands.push(7, u32[0], u32[1], u32[2], u32[3], u32[4], u32[5]);
985
+ }
986
+
987
+ /**
988
+ * Set scissor rectangle
989
+ * @param {number} x
990
+ * @param {number} y
991
+ * @param {number} width
992
+ * @param {number} height
993
+ */
994
+ setScissorRect(x, y, width, height) {
995
+ this.commands.push(8, x, y, width, height);
996
+ }
997
+
832
998
  /**
833
999
  * End the render pass
834
1000
  */
@@ -973,6 +1139,105 @@ export function createWebGPU(wasmModule, wasmMemory) {
973
1139
  return new GPU(wasmModule, wasmMemory);
974
1140
  }
975
1141
 
1142
+ export class GPUCanvasContext {
1143
+ /**
1144
+ * @param {*} wasmModule
1145
+ * @param {WebAssembly.Memory} wasmMemory
1146
+ * @param {HTMLCanvasElement} canvas
1147
+ */
1148
+ constructor(wasmModule, wasmMemory, canvas) {
1149
+ this.wasm = wasmModule;
1150
+ this.memory = wasmMemory;
1151
+ this.canvas = canvas;
1152
+ this.device = null;
1153
+ this.format = 'rgba8unorm';
1154
+ this.usage = GPUTextureUsage.RENDER_ATTACHMENT;
1155
+ this.width = canvas.width;
1156
+ this.height = canvas.height;
1157
+ }
1158
+
1159
+ /**
1160
+ * Configure the context
1161
+ * @param {Object} descriptor
1162
+ */
1163
+ configure(descriptor) {
1164
+ this.device = descriptor.device;
1165
+ this.format = descriptor.format || 'rgba8unorm';
1166
+ this.usage = descriptor.usage || GPUTextureUsage.RENDER_ATTACHMENT;
1167
+ this.alphaMode = descriptor.alphaMode || 'opaque';
1168
+
1169
+ // Resize canvas internal buffer if needed
1170
+ if (this.canvas.width !== this.width || this.canvas.height !== this.height) {
1171
+ this.width = this.canvas.width;
1172
+ this.height = this.canvas.height;
1173
+ }
1174
+ }
1175
+
1176
+ unconfigure() {
1177
+ this.device = null;
1178
+ }
1179
+
1180
+ /**
1181
+ * Get the current texture to render into
1182
+ * @returns {GPUTexture}
1183
+ */
1184
+ getCurrentTexture() {
1185
+ if (!this.device) {
1186
+ throw new Error("Context not configured");
1187
+ }
1188
+
1189
+ // Create a temporary texture that represents the canvas surface
1190
+ // In a real implementation, this would be a managed swapchain texture.
1191
+ // For SoftApi, we just create a regular texture that we will present later.
1192
+ return this.device.createTexture({
1193
+ size: { width: this.width, height: this.height, depthOrArrayLayers: 1 },
1194
+ format: this.format,
1195
+ usage: this.usage | GPUTextureUsage.COPY_SRC
1196
+ });
1197
+ }
1198
+
1199
+ /**
1200
+ * Present the current texture to the canvas
1201
+ * This is a non-standard method for our Soft-GPU to bridge to the browser display.
1202
+ * @param {GPUTexture} texture
1203
+ */
1204
+ present(texture) {
1205
+ const ctx2d = this.canvas.getContext('2d');
1206
+ if (!ctx2d) return;
1207
+
1208
+ const width = texture.width;
1209
+ const height = texture.height;
1210
+
1211
+ // Use readPixels-like logic to get data from WASM
1212
+ const len = width * height * 4;
1213
+
1214
+ // We need a buffer to copy the texture to
1215
+ const buffer = this.device.createBuffer({
1216
+ size: len,
1217
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
1218
+ });
1219
+
1220
+ const encoder = this.device.createCommandEncoder();
1221
+ encoder.copyTextureToBuffer(
1222
+ { texture: texture },
1223
+ { buffer: buffer, bytesPerRow: width * 4 },
1224
+ { width, height, depthOrArrayLayers: 1 }
1225
+ );
1226
+ this.device.queue.submit([encoder.finish()]);
1227
+
1228
+ // Map and copy to canvas
1229
+ buffer.mapAsync(GPUMapMode.READ).then(() => {
1230
+ const data = buffer.getMappedRange();
1231
+ const clamped = new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);
1232
+ const imageData = new ImageData(clamped, width, height);
1233
+ ctx2d.putImageData(imageData, 0, 0);
1234
+ buffer.unmap();
1235
+ buffer.destroy();
1236
+ texture.destroy();
1237
+ });
1238
+ }
1239
+ }
1240
+
976
1241
  export class GPUPipelineLayout {
977
1242
  constructor(wasmModule, wasmMemory, ctxHandle, layoutHandle) {
978
1243
  this.wasm = wasmModule;
package/webgl2.debug.wasm CHANGED
Binary file
package/webgl2.wasm CHANGED
Binary file