webgl2 1.0.4 ā 1.0.6
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/README.md +2 -0
- package/index.js +217 -11
- package/package.json +5 -4
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -1,15 +1,221 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Re-export the new implementation in `index2.js` so existing consumers who
|
|
3
|
-
// import `index.js` keep working. The legacy implementation was removed and
|
|
4
|
-
// moved to `index2.js` which contains the canonical, tested API.
|
|
1
|
+
// @ts-check
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
WasmWebGL2RenderingContext,
|
|
5
|
+
ERR_OK,
|
|
6
|
+
ERR_INVALID_HANDLE,
|
|
7
|
+
readErrorMessage
|
|
8
|
+
} from './src/webgl2_context.js';
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* WebGL2 Prototype: Rust-owned Context, JS thin-forwarder
|
|
12
|
+
* Implements docs/1.1.1-webgl2-prototype.md
|
|
13
|
+
*
|
|
14
|
+
* This module provides:
|
|
15
|
+
* - WasmWebGL2RenderingContext: JS class that forwards all calls to WASM
|
|
16
|
+
* - webGL2(): factory function to create a new context
|
|
17
|
+
*
|
|
18
|
+
* WASM owns all runtime state (textures, framebuffers, contexts).
|
|
19
|
+
* JS is a thin forwarder with no emulation of WebGL behavior.
|
|
20
|
+
*
|
|
21
|
+
* Explicit lifecycle: caller must call destroy() to free resources.
|
|
22
|
+
* All operations return errno (0 = OK). Non-zero errno causes JS to throw
|
|
23
|
+
* with the message from wasm_last_error_ptr/len.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const isNode =
|
|
28
|
+
typeof process !== 'undefined' &&
|
|
29
|
+
process.versions != null &&
|
|
30
|
+
process.versions.node != null;
|
|
31
|
+
|
|
32
|
+
/** @typedef {number} u32 */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Factory function: create a new WebGL2 context.
|
|
36
|
+
*
|
|
37
|
+
* This function:
|
|
38
|
+
* 1. Auto-loads webgl2.wasm (expects it next to index2.js)
|
|
39
|
+
* 2. Instantiates the WASM module with memory
|
|
40
|
+
* 3. Creates a Rust-owned context via wasm_create_context()
|
|
41
|
+
* 4. Returns a WasmWebGL2RenderingContext JS wrapper
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} opts - options (unused for now)
|
|
44
|
+
* @returns {Promise<WasmWebGL2RenderingContext>}
|
|
45
|
+
* @throws {Error} if WASM loading or instantiation fails
|
|
46
|
+
*/
|
|
47
|
+
async function webGL2(opts = {}) {
|
|
48
|
+
// Load WASM binary
|
|
49
|
+
const { ex, instance } = await (
|
|
50
|
+
wasmInitPromise ||
|
|
51
|
+
(async () => {
|
|
52
|
+
// ensure success is cached but not failure
|
|
53
|
+
let succeeded = false;
|
|
54
|
+
try {
|
|
55
|
+
const wasm = await initWASM();
|
|
56
|
+
succeeded = true;
|
|
57
|
+
return wasm;
|
|
58
|
+
} finally {
|
|
59
|
+
if (!succeeded)
|
|
60
|
+
wasmInitPromise = undefined;
|
|
61
|
+
}
|
|
62
|
+
})());
|
|
63
|
+
|
|
64
|
+
// Create a context in WASM
|
|
65
|
+
const ctxHandle = ex.wasm_create_context();
|
|
66
|
+
if (ctxHandle === 0) {
|
|
67
|
+
const msg = readErrorMessage(instance);
|
|
68
|
+
throw new Error(`Failed to create context: ${msg}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Wrap and return
|
|
72
|
+
const gl = new WasmWebGL2RenderingContext(instance, ctxHandle);
|
|
73
|
+
return gl;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @type {(
|
|
78
|
+
* Promise<{ ex: WebAssembly.Exports, instance: WebAssembly.Instance }> |
|
|
79
|
+
* { ex: WebAssembly.Exports, instance: WebAssembly.Instance } |
|
|
80
|
+
* undefined
|
|
81
|
+
* )}
|
|
82
|
+
*/
|
|
83
|
+
var wasmInitPromise;
|
|
84
|
+
|
|
85
|
+
async function initWASM() {
|
|
86
|
+
let wasmBuffer;
|
|
87
|
+
if (isNode) {
|
|
88
|
+
// Use dynamic imports so this module can be loaded in the browser too.
|
|
89
|
+
const path = await import('path');
|
|
90
|
+
const fs = await import('fs');
|
|
91
|
+
const { fileURLToPath } = await import('url');
|
|
92
|
+
const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'webgl2.wasm');
|
|
93
|
+
if (!fs.existsSync(wasmPath)) {
|
|
94
|
+
throw new Error(`WASM not found at ${wasmPath}. Run: npm run build:wasm`);
|
|
95
|
+
}
|
|
96
|
+
// readFileSync is available on the imported namespace
|
|
97
|
+
wasmBuffer = fs.readFileSync(wasmPath);
|
|
98
|
+
} else {
|
|
99
|
+
// Browser: fetch the wasm relative to this module
|
|
100
|
+
const resp = await fetch(new URL('./webgl2.wasm', import.meta.url));
|
|
101
|
+
if (!resp.ok) {
|
|
102
|
+
throw new Error(`Failed to fetch webgl2.wasm: ${resp.status}`);
|
|
103
|
+
}
|
|
104
|
+
wasmBuffer = await resp.arrayBuffer();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compile WASM module
|
|
108
|
+
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
|
109
|
+
|
|
110
|
+
// Create memory (WASM will import it)
|
|
111
|
+
const memory = new WebAssembly.Memory({ initial: 16, maximum: 256 });
|
|
112
|
+
|
|
113
|
+
// Instantiate WASM
|
|
114
|
+
const importObj = { env: { memory } };
|
|
115
|
+
const instance = await WebAssembly.instantiate(wasmModule, importObj);
|
|
116
|
+
|
|
117
|
+
// Verify required exports
|
|
118
|
+
const ex = instance.exports;
|
|
119
|
+
if (typeof ex.wasm_create_context !== 'function') {
|
|
120
|
+
throw new Error('WASM module missing wasm_create_context export');
|
|
121
|
+
}
|
|
122
|
+
return wasmInitPromise = { ex, instance };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reads an error message from WASM memory and returns it.
|
|
127
|
+
* @param {WebAssembly.Instance} instance
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function _readErrorMessage(instance) {
|
|
131
|
+
const ex = instance.exports;
|
|
132
|
+
if (!ex || typeof ex.wasm_last_error_ptr !== 'function' || typeof ex.wasm_last_error_len !== 'function') {
|
|
133
|
+
return '(no error message available)';
|
|
134
|
+
}
|
|
135
|
+
const ptr = ex.wasm_last_error_ptr();
|
|
136
|
+
const len = ex.wasm_last_error_len();
|
|
137
|
+
if (ptr === 0 || len === 0) {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
const mem = new Uint8Array(ex.memory.buffer);
|
|
141
|
+
const bytes = mem.subarray(ptr, ptr + len);
|
|
142
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks a WASM return code (errno).
|
|
147
|
+
* If non-zero, reads the error message and throws.
|
|
148
|
+
* @param {number} code
|
|
149
|
+
* @param {WebAssembly.Instance} instance
|
|
150
|
+
* @throws {Error} if code !== 0
|
|
151
|
+
*/
|
|
152
|
+
function _checkErr(code, instance) {
|
|
153
|
+
if (code === ERR_OK) return;
|
|
154
|
+
const msg = _readErrorMessage(instance);
|
|
155
|
+
throw new Error(`WASM error ${code}: ${msg}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// Exports: ESM-style. Also attach globals in browser for convenience.
|
|
160
|
+
export { webGL2, WasmWebGL2RenderingContext, ERR_OK, ERR_INVALID_HANDLE };
|
|
161
|
+
|
|
162
|
+
if (typeof window !== 'undefined' && window) {
|
|
163
|
+
// also populate globals when running in a browser environment
|
|
164
|
+
try {
|
|
165
|
+
window.webGL2 = webGL2;
|
|
166
|
+
window.WasmWebGL2RenderingContext = WasmWebGL2RenderingContext;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// ignore if window is not writable
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function nodeDemo() {
|
|
173
|
+
console.log('Running index2.js demo...');
|
|
174
|
+
const gl = await webGL2();
|
|
175
|
+
console.log(`ā Context created (handle will be managed by destroy())`);
|
|
176
|
+
|
|
177
|
+
// 1x1 texture with CornflowerBlue (100, 149, 237, 255)
|
|
178
|
+
const tex = gl.createTexture();
|
|
179
|
+
console.log(`ā Texture created (handle: ${tex})`);
|
|
180
|
+
|
|
181
|
+
gl.bindTexture(0, tex);
|
|
182
|
+
const pixel = new Uint8Array([100, 149, 237, 255]);
|
|
183
|
+
gl.texImage2D(0, 0, 0, 1, 1, 0, 0, 0, pixel);
|
|
184
|
+
console.log(`ā Texture uploaded`);
|
|
185
|
+
|
|
186
|
+
const fb = gl.createFramebuffer();
|
|
187
|
+
console.log(`ā Framebuffer created (handle: ${fb})`);
|
|
188
|
+
|
|
189
|
+
gl.bindFramebuffer(0, fb);
|
|
190
|
+
gl.framebufferTexture2D(0, 0, 0, tex, 0);
|
|
191
|
+
console.log(`ā Texture attached to framebuffer`);
|
|
192
|
+
|
|
193
|
+
const out = new Uint8Array(4);
|
|
194
|
+
gl.readPixels(0, 0, 1, 1, 0, 0, out);
|
|
195
|
+
console.log(
|
|
196
|
+
`ā Pixel read: r=${out[0]}, g=${out[1]}, b=${out[2]}, a=${out[3]}`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (out[0] === 100 && out[1] === 149 && out[2] === 237 && out[3] === 255) {
|
|
200
|
+
console.log('ā Pixel matches expected CornflowerBlue!');
|
|
201
|
+
} else {
|
|
202
|
+
console.error('ā Pixel mismatch!');
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
gl.destroy();
|
|
207
|
+
console.log('ā Context destroyed');
|
|
208
|
+
console.log('\nā Demo passed!');
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// CLI demo: run when executed directly in Node
|
|
8
213
|
if (isNode) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
214
|
+
(async () => {
|
|
215
|
+
const { fileURLToPath } = await import('url');
|
|
216
|
+
const path = (await import('path'));
|
|
217
|
+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
218
|
+
nodeDemo();
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
15
221
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webgl2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "WebGL2 tools to derisk large GPU projects on the web beyond toys and demos.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "index.js",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"start": "node ./index.js",
|
|
8
|
-
"build
|
|
9
|
-
"build": "node
|
|
9
|
+
"build": "cargo build --target wasm32-unknown-unknown --release && cp target/wasm32-unknown-unknown/release/*.wasm .",
|
|
10
|
+
"build:wasm": "node scripts/build-wasm.cjs",
|
|
10
11
|
"prepublishOnly": "npm run build",
|
|
11
|
-
"test": "
|
|
12
|
+
"test": "node --test test/*.test.js",
|
|
12
13
|
"test:smoke": "npm run build:wasm && node ./test/smoke.js"
|
|
13
14
|
},
|
|
14
15
|
"wasmBuild": {
|