webgl2
Version:
WebGL2 tools to derisk large GPU projects on the web beyond toys and demos.
463 lines (419 loc) • 16.5 kB
JavaScript
// @ts-check
import {
WasmWebGL2RenderingContext,
ERR_OK,
ERR_INVALID_HANDLE,
readErrorMessage,
getShaderModule,
getShaderWat,
getShaderGlsl,
decompileWasmToGlsl
} from './src/webgl2_context.js';
import { GPU, GPUBufferUsage, GPUMapMode, GPUTextureUsage, GPUShaderStage } from './src/webgpu_context.js';
export const debug = {
getLcovReport,
resetLcovReport
};
export { ERR_OK, ERR_INVALID_HANDLE, GPUBufferUsage, GPUMapMode, GPUTextureUsage, GPUShaderStage, getShaderModule, getShaderWat, getShaderGlsl, decompileWasmToGlsl };
/**
* Simple allocator for function table indices.
* Tracks which slots are in use to enable reuse.
*/
class TableAllocator {
constructor() {
// Rust uses many slots for its indirect function table (dyn calls, etc).
// We must avoid collision by starting allocations after that region.
// Increased from 2000 to 5000 for safety in larger modules.
this.nextIndex = 5000;
this.freeList = [];
}
allocate() {
if (this.freeList.length > 0) {
return this.freeList.pop();
}
return this.nextIndex++;
}
free(index) {
this.freeList.push(index);
}
}
/**
* WebGL2 Prototype: Rust-owned Context, JS thin-forwarder
* Implements docs/1.1.1-webgl2-prototype.md
*
* This module provides:
* - WasmWebGL2RenderingContext: JS class that forwards all calls to WASM
* - webGL2(): factory function to create a new context
*
* WASM owns all runtime state (textures, framebuffers, contexts).
* JS is a thin forwarder with no emulation of WebGL behavior.
*
* Explicit lifecycle: caller must call destroy() to free resources.
* All operations return errno (0 = OK). Non-zero errno causes JS to throw
* with the message from wasm_last_error_ptr/len.
*/
const isNode =
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null;
/** @typedef {number} u32 */
/**
* Factory function: create a new WebGL2 context.
*
* This function:
* 1. Auto-loads webgl2.wasm (expects it next to index2.js)
* 2. Instantiates the WASM module with memory
* 3. Creates a Rust-owned context via wasm_create_context_with_flags(flags)
* 4. Returns a WasmWebGL2RenderingContext JS wrapper
*
* @param {{
* debug?: boolean | 'shaders' | 'rust' | 'all',
* size?: { width: number, height: number },
* }} [opts] - options
* @returns {Promise<WasmWebGL2RenderingContext>}
* @throws {Error} if WASM loading or instantiation fails
*/
export async function webGL2({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true', size } = {}) {
// Determine if we need the debug WASM binary (Rust symbols)
const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
// Load WASM binary
let promise = wasmCache.get(useDebugWasm);
if (!promise) {
promise = initWASM({ debug: useDebugWasm });
wasmCache.set(useDebugWasm, promise);
// ensure success is cached but not failure
promise.catch(() => {
if (wasmCache.get(useDebugWasm) === promise) {
wasmCache.delete(useDebugWasm);
}
});
}
const { ex, instance, sharedTable, tableAllocator, turboGlobals } = await promise;
// Initialize coverage if available
if (ex.wasm_init_coverage && ex.COV_MAP_PTR) {
const mapPtr = ex.COV_MAP_PTR.value;
// Read num_entries from the start of the map data
// mapPtr is aligned to 16 bytes, so we can use Uint32Array
const mem = new Uint32Array(ex.memory.buffer);
const numEntries = mem[mapPtr >>> 2];
ex.wasm_init_coverage(numEntries);
}
// Determine debug flags for creation
const debugShaders = debug === true || debug === 'shaders' || debug === 'all';
const debugRust = debug === true || debug === 'rust' || debug === 'all';
const flags = (debugShaders ? 1 : 0); // only shader debug encoded in flags
// Default size to 640x480 if not provided
const width = size?.width ?? 640;
const height = size?.height ?? 480;
// Create a context in WASM using the flags-aware API (mandatory)
const ctxHandle = ex.wasm_create_context_with_flags(flags, width, height);
if (ctxHandle === 0) {
const msg = readErrorMessage(instance);
throw new Error(`Failed to create context: ${msg}`);
}
// Wrap and return, pass debug booleans to the JS wrapper
const gl = new WasmWebGL2RenderingContext({
instance,
ctxHandle,
width,
height,
debugShaders: !!debugShaders,
sharedTable,
tableAllocator,
turboGlobals
});
if (size && typeof size.width === 'number' && typeof size.height === 'number') {
gl.resize(size.width, size.height);
gl.viewport(0, 0, size.width, size.height);
}
return gl;
}
/**
* Factory function: create a new WebGPU instance.
*
* @param {{
* debug?: boolean | 'shaders' | 'rust' | 'all',
* }} [opts] - options
* @returns {Promise<GPU>}
*/
export async function webGPU({ debug = (typeof process !== 'undefined' ? process?.env || {} : typeof window !== 'undefined' ? window : globalThis).WEBGL2_DEBUG === 'true' } = {}) {
const useDebugWasm = debug === true || debug === 'rust' || debug === 'all';
let promise = wasmCache.get(useDebugWasm);
if (!promise) {
promise = initWASM({ debug: useDebugWasm });
wasmCache.set(useDebugWasm, promise);
promise.catch(() => {
if (wasmCache.get(useDebugWasm) === promise) {
wasmCache.delete(useDebugWasm);
}
});
}
// Resolve the WASM initialization and return a WebGPU wrapper (GPU).
// Tests expect an object with `requestAdapter()`; return a `GPU` instance
// backed by the WASM exports and memory.
const { ex } = await promise;
return new GPU(ex, ex.memory);
}
/**
* @type {Map<boolean, ReturnType<typeof initWASM>>}
*/
const wasmCache = new Map();
/**
* @param {{ debug?: boolean }} [options]
*/
async function initWASM({ debug } = {}) {
const wasmFile = debug ? 'webgl2.debug.wasm' : 'webgl2.wasm';
let wasmBuffer;
if (isNode) {
// Use dynamic imports so this module can be loaded in the browser too.
const path = await import('path');
const fs = await import('fs');
const { fileURLToPath } = await import('url');
const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), wasmFile);
if (!fs.existsSync(wasmPath)) {
throw new Error(`WASM not found at ${wasmPath}. Run: npm run build:wasm`);
}
// readFileSync is available on the imported namespace
wasmBuffer = fs.readFileSync(wasmPath);
} else {
// Browser: fetch the wasm relative to this module
const resp = await fetch(new URL('./' + wasmFile, import.meta.url));
if (!resp.ok) {
throw new Error(`Failed to fetch ${wasmFile}: ${resp.status}`);
}
wasmBuffer = await resp.arrayBuffer();
}
// Compile WASM module
const wasmModule = await WebAssembly.compile(wasmBuffer);
/**
* Instantiate WASM (no imports needed, memory is exported)
* @type {WebAssembly.Instance}
*/
let instance;
// Create shared function table for direct shader calls
const sharedTable = new WebAssembly.Table({
initial: 8192,
maximum: 65536,
element: "anyfunc"
});
const tableAllocator = new TableAllocator();
// Create shared mutable globals that will be used by both the main module and
// transient shader modules. These are WebAssembly.Global objects with mutable
// i32 value so shaders can update pointer state directly.
const turboGlobals = {
ACTIVE_ATTR_PTR: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
ACTIVE_UNIFORM_PTR: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
ACTIVE_VARYING_PTR: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
ACTIVE_PRIVATE_PTR: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
ACTIVE_TEXTURE_PTR: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
ACTIVE_FRAME_SP: new WebAssembly.Global({ value: 'i32', mutable: true }, 0),
};
const importObject = {
env: {
__indirect_function_table: sharedTable, // Exact name LLVM expects
memory: new WebAssembly.Memory({ initial: 100 }),
ACTIVE_ATTR_PTR: turboGlobals.ACTIVE_ATTR_PTR,
ACTIVE_UNIFORM_PTR: turboGlobals.ACTIVE_UNIFORM_PTR,
ACTIVE_VARYING_PTR: turboGlobals.ACTIVE_VARYING_PTR,
ACTIVE_PRIVATE_PTR: turboGlobals.ACTIVE_PRIVATE_PTR,
ACTIVE_TEXTURE_PTR: turboGlobals.ACTIVE_TEXTURE_PTR,
ACTIVE_FRAME_SP: turboGlobals.ACTIVE_FRAME_SP,
print: (ptr, len) => {
const mem = new Uint8Array(instance.exports.memory.buffer);
const bytes = mem.subarray(ptr, ptr + len);
console.log(new TextDecoder('utf-8').decode(bytes));
},
wasm_register_shader: (ptr, len) => {
const mem = new Uint8Array(instance.exports.memory.buffer);
const bytes = mem.slice(ptr, ptr + len);
const shaderModule = new WebAssembly.Module(bytes);
const index = tableAllocator.allocate();
const env = {
memory: instance.exports.memory,
__indirect_function_table: sharedTable,
ACTIVE_ATTR_PTR: turboGlobals.ACTIVE_ATTR_PTR,
ACTIVE_UNIFORM_PTR: turboGlobals.ACTIVE_UNIFORM_PTR,
ACTIVE_VARYING_PTR: turboGlobals.ACTIVE_VARYING_PTR,
ACTIVE_PRIVATE_PTR: turboGlobals.ACTIVE_PRIVATE_PTR,
ACTIVE_TEXTURE_PTR: turboGlobals.ACTIVE_TEXTURE_PTR,
ACTIVE_FRAME_SP: turboGlobals.ACTIVE_FRAME_SP,
};
env.gl_sin = instance.exports.gl_sin;
env.gl_cos = instance.exports.gl_cos;
env.gl_tan = instance.exports.gl_tan;
env.gl_asin = instance.exports.gl_asin;
env.gl_acos = instance.exports.gl_acos;
env.gl_atan = instance.exports.gl_atan;
env.gl_atan2 = instance.exports.gl_atan2;
env.gl_exp = instance.exports.gl_exp;
env.gl_exp2 = instance.exports.gl_exp2;
env.gl_log = instance.exports.gl_log;
env.gl_log2 = instance.exports.gl_log2;
env.gl_pow = instance.exports.gl_pow;
env.gl_ldexp = instance.exports.gl_ldexp;
env.gl_sinh = instance.exports.gl_sinh;
env.gl_cosh = instance.exports.gl_cosh;
env.gl_tanh = instance.exports.gl_tanh;
env.gl_asinh = instance.exports.gl_asinh;
env.gl_acosh = instance.exports.gl_acosh;
env.gl_atanh = instance.exports.gl_atanh;
// Link diagnostic and math helpers from the main module
env.gl_debug4 = instance.exports.gl_debug4;
env.gl_inverse_mat2 = instance.exports.gl_inverse_mat2;
env.gl_inverse_mat3 = instance.exports.gl_inverse_mat3;
const shaderInstance = new WebAssembly.Instance(shaderModule, { env });
if (shaderInstance.exports.main) {
sharedTable.set(index, shaderInstance.exports.main);
}
return index;
},
wasm_release_shader_index: (idx) => {
tableAllocator.free(idx);
},
wasm_sync_turbo_globals: (attr, uniform, varying, private_, texture, frame_sp) => {
try {
turboGlobals.ACTIVE_ATTR_PTR.value = attr >>> 0;
turboGlobals.ACTIVE_UNIFORM_PTR.value = uniform >>> 0;
turboGlobals.ACTIVE_VARYING_PTR.value = varying >>> 0;
turboGlobals.ACTIVE_PRIVATE_PTR.value = private_ >>> 0;
turboGlobals.ACTIVE_TEXTURE_PTR.value = texture >>> 0;
turboGlobals.ACTIVE_FRAME_SP.value = frame_sp >>> 0;
} catch (e) {
// Defensive: if the globals are immutable or not set, at least avoid crashing
console.warn('wasm_sync_turbo_globals failed to set globals', e);
}
},
dispatch_uncaptured_error: (ptr, len) => {
const mem = new Uint8Array(instance.exports.memory.buffer);
const bytes = mem.subarray(ptr, ptr + len);
const msg = new TextDecoder('utf-8').decode(bytes);
if (typeof GPU !== 'undefined' && typeof GPU.dispatchUncapturedError === 'function') {
GPU.dispatchUncapturedError(msg);
} else {
console.error("GPU.dispatchUncapturedError not available", msg);
}
},
// Required by egg crate for timing measurements
now: () => {
return performance.now();
}
},
math: {
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
asin: Math.asin,
acos: Math.acos,
atan: Math.atan,
atan2: Math.atan2,
exp: Math.exp,
exp2: (x) => Math.pow(2, x),
log: Math.log,
log2: Math.log2,
pow: Math.pow
}
};
instance = await WebAssembly.instantiate(wasmModule, importObject);
// Verify required exports
const ex = instance.exports;
if (typeof ex.wasm_create_context_with_flags !== 'function') {
throw new Error('WASM module missing wasm_create_context_with_flags export');
}
if (!(ex.memory instanceof WebAssembly.Memory)) {
throw new Error('WASM module missing memory export');
}
return { ex, instance, module: wasmModule, sharedTable, tableAllocator, turboGlobals };
}
/**
* Reads an error message from WASM memory and returns it.
* @param {WebAssembly.Instance} instance
* @returns {string}
*/
function _readErrorMessage(instance) {
const ex = instance.exports;
if (!ex || typeof ex.wasm_last_error !== 'function') {
if (typeof ex.wasm_last_error_ptr === 'function') {
const ptr = ex.wasm_last_error_ptr();
const len = ex.wasm_last_error_len();
if (ptr === 0 || len === 0) return '';
const mem = new Uint8Array(ex.memory.buffer);
const bytes = mem.subarray(ptr, ptr + len);
return new TextDecoder('utf-8').decode(bytes);
}
return '(no error message available)';
}
const ptr = ex.wasm_last_error();
if (ptr === 0) return '';
const dv = new DataView(ex.memory.buffer);
const len = dv.getUint32(ptr - 16, true);
const mem = new Uint8Array(ex.memory.buffer);
const bytes = mem.slice(ptr, ptr + len);
return new TextDecoder('utf-8').decode(bytes);
}
/**
* Get LCOV coverage report from a context or device.
* @param {any} glOrGpu
* @returns {string}
*/
function getLcovReport(glOrGpu) {
if (!glOrGpu) return '';
let ex;
if (glOrGpu._instance && glOrGpu._instance.exports) {
ex = glOrGpu._instance.exports;
} else if (glOrGpu.wasm) {
ex = glOrGpu.wasm;
} else if (glOrGpu._instance) {
ex = glOrGpu._instance;
}
if (ex && typeof ex.wasm_get_lcov_report_ptr === 'function' && typeof ex.wasm_get_lcov_report_len === 'function') {
const ptr = ex.wasm_get_lcov_report_ptr();
const len = ex.wasm_get_lcov_report_len();
if (ptr === 0 || len === 0) return '';
const mem = new Uint8Array(ex.memory.buffer);
const bytes = mem.subarray(ptr, ptr + len);
return new TextDecoder('utf-8').decode(bytes);
}
return '';
}
/**
* Reset LCOV coverage counters.
* @param {any} glOrGpu
*/
export function resetLcovReport(glOrGpu) {
if (!glOrGpu) return;
let ex;
if (glOrGpu._instance && glOrGpu._instance.exports) {
ex = glOrGpu._instance.exports;
} else if (glOrGpu.wasm) {
ex = glOrGpu.wasm;
} else if (glOrGpu._instance) {
ex = glOrGpu._instance;
}
if (ex && typeof ex.wasm_reset_coverage === 'function') {
ex.wasm_reset_coverage();
}
}
/**
* Checks a WASM return code (errno).
* If non-zero, reads the error message and throws.
* @param {number} code
* @param {WebAssembly.Instance} instance
* @throws {Error} if code !== 0
*/
function _checkErr(code, instance) {
if (code === ERR_OK) return;
const msg = _readErrorMessage(instance);
throw new Error(`WASM error ${code}: ${msg}`);
}
if (typeof window !== 'undefined' && window) {
// also populate globals when running in a browser environment
try {
window.webGL2 = webGL2;
window.webGPU = webGPU;
window.getLcovReport = getLcovReport;
window.resetLcovReport = resetLcovReport;
window.WasmWebGL2RenderingContext = WasmWebGL2RenderingContext;
} catch (e) {
// ignore if window is not writable
}
}