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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.js +217 -11
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A **Rust + WASM** based toolkit for debugging GLSL shaders and generating ergonomic WebGL2 bindings.
4
4
 
5
+ ![WebGL2 Singing Dog Logo](./webgl2.png)
6
+
5
7
  ## šŸŽÆ Quick Start
6
8
 
7
9
  ```bash
package/index.js CHANGED
@@ -1,15 +1,221 @@
1
- // index.js (compat shim)
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
- const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
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
- // In Node.js environments re-export everything from index2.js
10
- module.exports = require('./index2.js');
11
- } else {
12
- // In browsers assume `index2.js` is loaded alongside this file and has
13
- // populated `window.webGL2`. Nothing else to do here.
14
- // (Keeping a no-op shim avoids duplicating the implementation.)
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.4",
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:wasm": "node ./scripts/build-wasm.js",
9
- "build": "node -e \"(async()=>{const p=require('path');const dir=process.env.INIT_CWD||process.cwd();require(p.join(dir,'scripts','build-wasm.js'));})()\"",
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": "echo \"Error: no test specified\" && exit 1",
12
+ "test": "node --test test/*.test.js",
12
13
  "test:smoke": "npm run build:wasm && node ./test/smoke.js"
13
14
  },
14
15
  "wasmBuild": {