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 +33 -29
- package/demo.js +2 -2
- package/index.js +404 -368
- package/package.json +1 -1
- package/src/webgl2_context.js +39 -2
- package/src/webgpu_context.js +272 -7
- package/webgl2.debug.wasm +0 -0
- package/webgl2.wasm +0 -0
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 |
|
|
12
|
-
| src/decompiler/parser.rs |
|
|
13
|
-
| src/decompiler/simplifier.rs |
|
|
14
|
-
| src/error.rs |
|
|
15
|
-
| src/lib.rs |
|
|
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
|
-
| src/naga_wasm_backend/control_flow.rs |
|
|
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 |
|
|
21
|
-
| src/naga_wasm_backend/function_abi.rs |
|
|
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 |
|
|
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/
|
|
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/
|
|
28
|
-
| src/wasm_gl_emu/
|
|
29
|
-
| src/
|
|
30
|
-
| src/
|
|
31
|
-
| src/webgl2_context/
|
|
32
|
-
| src/webgl2_context/
|
|
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 |
|
|
35
|
-
| src/webgl2_context/shaders.rs |
|
|
36
|
-
| src/webgl2_context/state.rs |
|
|
37
|
-
| src/webgl2_context/textures.rs |
|
|
38
|
-
| src/webgl2_context/types.rs |
|
|
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 |
|
|
41
|
-
| src/webgpu/backend.rs |
|
|
42
|
-
|
|
|
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 |
|
|
49
|
-
| src/naga_wasm_backend/expressions.rs |
|
|
50
|
-
| src/
|
|
51
|
-
| src/
|
|
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(
|
|
620
|
-
|
|
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, //
|
|
211
|
-
maximum:
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
package/src/webgl2_context.js
CHANGED
|
@@ -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) {
|
|
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) {
|
|
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'); }
|
package/src/webgpu_context.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|