Latest commit: Tuesday, November 10, 2026, current.
Parent commit: Friday, November 6, 2026, c65d056.
Latest release: Monday, August 31, 2026, v0.10.0.
- 1. Architecture
- 1.1. Architecture diagram
- 1.2. Public API surface
- 1.3. Runtime creation and frame loop
- 1.4. WebGPU engine
- 1.5. Scene and object model
- 1.6. Transforms
- 1.7. Geometry, materials, textures, and colormaps
- 1.8. Pointcloud, glyphfield, nodelink, splatfield, and latticespace data
- 1.9. Lights, cameras, and controls
- 1.10. Rendering pipeline
- 1.11. Browser runtime resources
- 1.12. Picking, overlays, and annotations
- 1.13. Compute and ndarray systems
- 1.14. Scaling service
- 1.15. Rendering effects
- 1.16. Asset loading and glTF import
- 1.17. Animating, skinning, and morphing
- 1.18. Python interop
- 1.19. WebAssembly driver
- 1.20. Shader organization
- 1.21. Per-frame work and performance-sensitive paths
- 1.22. Visible invariants
- 2. Codebase
- 2.1. Public API and entry points
- 2.2. Runtime and renderer
- 2.3. Scene and world objects
- 2.4. Graphics data
- 2.5. Compute, ndarray, and scaling
- 2.6. Asset loading and glTF
- 2.7. Overlays and annotations
- 2.8. Python, WebAssembly, and WebGPU interop
- 2.9. Rust driver
- 2.10. WGSL shaders
- 2.11. Examples
- 2.12. Tests
- 2.13. Benchmarks
- 2.14. Documentation and website
- 2.15. Build, generated, and release files
- 2.16. Assets and repository metadata
- 3. Guidelines
- 3.1. Start with local context
- 3.2. Code style and formatting
- 3.3. Public API behavior
- 3.4. TypeScript changes
- 3.5. Rust changes
- 3.6. WGSL changes
- 3.7. Memory and per-frame work
- 3.8. Examples, documentation, and release-facing files
- 3.9. Generated files and release artifacts
- 3.10. Validation commands
- 3.11. Commit messages
- 3.12. Updating this file
flowchart LR
subgraph API["Public API"]
APP["User Application"]
ENG["WasmGPU"]
FAC["Factory surface: scene, camera, controls, light, effects, geometry, material, texture, mesh, pointcloud, glyphfield, nodelink, splatfield, latticespace, asset import, animation, overlay, annotation, interop"]
end
subgraph WGPU["WebGPU Engine"]
LOOP["Frame loop"]
REND["Renderer"]
EFF["Effects facade"]
SCALE["Scaling service"]
OVER["Overlay framework"]
ANNO["Annotation toolkit"]
PICK["Picking utility"]
COMP["Compute subsystem"]
CBUF["Buffer resource manager"]
CPIP["Pipeline controller"]
CDIS["Dispatch workgroup planner"]
CKER["Kernels library"]
CND["N-dimensional array model, views, & transformations for CPU & GPU memory"]
CREAD["Asynchronous readback ring"]
CSCR["Scratch allocator"]
end
subgraph DATA["Object & Data Model"]
SCN["Scene"]
TSTORE["Transform store in SoA memory"]
MESH["Mesh with geometry, material, texture, morphing, & skinning"]
PGN["Pointcloud, glyphfield, nodelink, splatfield, & latticespace"]
CMAP["Colormapping"]
SKIN["Skinning instance data"]
ASTORE["Annotation store"]
ALOAD["Loader for glTF 2.0 asset data"]
ADEC["Accessor decoding & data conversion"]
AIMP["Importer from asset data to scene resources"]
AMETA["Imported nodes, metadata, variants, cameras, & lights"]
GINT["WebGPU interop"]
WINT["WebAssembly interop"]
PYINT["Python interop"]
end
subgraph WASM["WebAssembly Driver"]
WHEAP["Heap allocation for persistent typed memory"]
WFRAME["Frame arena for transient typed memory"]
WTRANS["Transform propagation"]
WMATH["Matrix, vector, & quaternion mathematics"]
WND["N-dimensional array indexing, stride-offsetting, strided gathering, casting, padding, normalization, & resampling"]
WNORM["Mesh normal generation"]
WGLTF["glTF accessor decoding, sparse patching, & numeric conversion"]
WANIM["Animation sampling & joint matrix generation"]
WBOUNDS["Bounds computation"]
WCULL["Frustum culling"]
end
subgraph GPU["Browser Resources"]
DEV["Graphics device & queue"]
CACHE["Pipeline cache & bindgroup cache"]
RES["Buffers, textures, & samplers"]
RPASS["Render passes for opaques, transparents, transmissions, effects, post-processing, & interaction"]
CPASS["Compute passes for kernels"]
end
classDef darkblue fill:#4E79FF,stroke:#0B2B8F,stroke-width:2px,color:#06153D;
classDef green fill:#22D37D,stroke:#0A6D3C,stroke-width:2px,color:#04311A;
classDef lightblue fill:#17C9FF,stroke:#005E80,stroke-width:2px,color:#022433;
classDef yellow fill:#FFB238,stroke:#9A4D00,stroke-width:2px,color:#5A2C00;
classDef purple fill:#B18AFF,stroke:#5A2FA6,stroke-width:2px,color:#2E165E;
classDef pink fill:#FF5EA8,stroke:#9A2E62,stroke-width:2px,color:#4D1532;
class APP,ENG,FAC darkblue;
class LOOP,REND,EFF,SCALE,OVER,ANNO,PICK green;
class COMP,CBUF,CPIP,CDIS,CKER,CND,CREAD,CSCR lightblue;
class SCN,TSTORE,MESH,PGN,CMAP,SKIN,ASTORE,ALOAD,ADEC,AIMP,AMETA,GINT,WINT,PYINT yellow;
class WHEAP,WFRAME,WTRANS,WMATH,WND,WNORM,WGLTF,WANIM,WBOUNDS,WCULL purple;
class DEV,CACHE,RES,RPASS,CPASS pink;
APP --> ENG
ENG --> FAC
ENG --> LOOP
ENG --> REND
ENG --> COMP
ENG --> SCALE
ENG --> OVER
ENG --> ANNO
ENG --> PYINT
FAC --> SCN
FAC --> TSTORE
FAC --> EFF
FAC --> MESH
FAC --> PGN
FAC --> ALOAD
FAC --> AIMP
FAC --> GINT
FAC --> WINT
SCN --> MESH
SCN --> PGN
MESH --> TSTORE
PGN --> TSTORE
SKIN --> MESH
ALOAD --> ADEC
ADEC --> AIMP
AIMP --> SCN
AIMP --> MESH
AIMP --> SKIN
AIMP --> AMETA
CMAP --> MESH
CMAP --> PGN
SCALE --> CMAP
SCALE --> PGN
LOOP --> REND
LOOP --> WFRAME
REND --> DEV
REND --> CACHE
REND --> RES
REND --> RPASS
REND --> EFF
EFF --> RPASS
REND --> PICK
REND --> SCN
REND --> TSTORE
REND --> WCULL
REND --> WBOUNDS
PICK --> RPASS
OVER --> SCN
ANNO --> ASTORE
ANNO --> PICK
ANNO --> SCN
COMP --> CBUF
COMP --> CPIP
COMP --> CDIS
COMP --> CKER
COMP --> CND
COMP --> CREAD
COMP --> CSCR
CPIP --> GINT
MESH --> GINT
GINT -.-> RES
CBUF --> RES
CPIP --> CPASS
CDIS --> CPASS
CKER --> CPASS
CKER --> CSCR
CKER --> CND
CND --> CBUF
CND --> CPIP
CND --> CPASS
CND --> CSCR
CREAD --> RES
CSCR --> RES
CPASS --> DEV
TSTORE -.-> WTRANS
MESH -.-> WNORM
MESH -.-> WBOUNDS
PGN -.-> WBOUNDS
ADEC -.-> WGLTF
SKIN -.-> WANIM
CND -.-> WND
REND -.-> WFRAME
REND -.-> WMATH
COMP -.-> WHEAP
WINT -.-> CBUF
PYINT --> COMP
PYINT --> CND
WHEAP -.-> RES
WFRAME -.-> RES
This diagram describes the current source tree, including unreleased work after v0.10.0. Solid arrows indicate creation, ownership, stored references, or call direction. Dashed arrows indicate data movement through WebAssembly memory or WebGPU resources.
The public API is exported from ./typescript/index.ts. It exports the WasmGPU class, the renderer, compute helpers including the type of the per-instance compute.ndarray namespace, scaling helpers, world objects, graphics objects, glTF helpers, rendering effects, overlay and annotation types, Python interop, WebAssembly interop, WebGPU interop, and math helpers.
Applications normally enter through WasmGPU.create(canvas, descriptor) in ./typescript/core/engine.ts. The runtime initializes WebAssembly, creates a WebGPU renderer, constructs the compute subsystem, and exposes factories for scenes, cameras, controls, geometry, materials, textures, meshes, pointclouds, glyphfields, nodelinks, splatfields, latticespaces, lights, effects, glTF loading, animation, overlays, and annotations.
The public API reads and writes data owned by the objects it creates. For example, a Mesh owns a transform, references geometry and material objects, and may own skin or morph runtime state. Geometry, PointCloud, GlyphField, NodeLink, SplatField, and LatticeSpace keep CPU-side records for external WebAssembly memory upload paths only when configured to retain CPU data. PointCloud, GlyphField, NodeLink, SplatField, and LatticeSpace follow an explicit external-buffer ownership model of owning GPU buffers they create internally and destroying caller-supplied external buffers only when constructed with ownBuffers: true. PointCloud supports setter-level ownBuffer: true on point and color buffer replacement, GlyphField supports setter-level ownBuffers: true on instance-buffer replacement, NodeLink supports setter-level ownBuffer: true on per-channel node and edge buffer replacement, and LatticeSpace supports setter-level ownBuffer: true on data and mask buffer replacement. DataMaterial handles externally supplied data buffers differently because it does not destroy a dataBuffer supplied by the caller.
Before changing exported names, constructor descriptors, or factory return values, inspect ./typescript/index.ts, ./typescript/core/engine.ts, ./tests/*.test.js, and the examples under ./examples/. Public API changes often require updates to examples and release-facing documentation when preparing a release.
Runtime creation starts in ./typescript/core/engine.ts. WasmGPU.create() calls initWebAssembly() from ./typescript/wasm/index.ts, then calls Renderer.create() from ./typescript/core/renderer.ts. It then creates instance-bound Compute, PythonInterop, ScaleService, and PerformanceStats services.
The runtime owns:
- the active renderer;
- the compute subsystem;
- the Python interop toolkit;
- the effects facade;
- the scaling service;
- performance stats;
- requestAnimationFrame loop state.
It also exposes accessors from ./typescript/wasm/index.ts for the WebAssembly driver (driver), external WebAssembly interop (webassembly), declarative WebGPU interop (webgpu), and instance-only Python interop (python).
WasmGPU.run(callback) starts a browser animation loop. Each frame resets the WebAssembly frame arena, computes timing values, invokes the callback, and records frame statistics. WasmGPU.render(scene, camera) delegates to Renderer.render(). If the runtime is not inside run(), it resets the frame arena before rendering.
WasmGPU.destroy() stops the frame loop, clears the scaling cache, clears effects configuration, destroys compute resources, and destroys the renderer. Contributors changing lifetime behavior should inspect both ./typescript/core/engine.ts and the resource destroy() methods in the objects that can be attached to a scene.
The WebGPU engine coordinates the browser-facing parts of the runtime. Its main files are ./typescript/core/engine.ts, ./typescript/core/renderer.ts, the internal renderer helper modules under ./typescript/core/, ./typescript/core/stats.ts, ./typescript/core/transform.ts, and the subsystem directories under ./typescript/.
The engine reads scene objects, transform state, camera state, material state, texture state, compute descriptors, overlay descriptors, and WebAssembly memory views. It mutates cached renderer state, GPU buffers, bind groups, draw lists, pick state, performance counters, per-frame staging memory, and optional occlusion hierarchy resources used by render-only previous-frame occlusion culling.
./typescript/core/renderer.ts owns the public Renderer class and the renderer-facing orchestration path. It creates the WebGPU adapter, device, queue, context, fallback textures, global buffers, bind group layouts, pipeline caches, shader caches, draw-list pools, culling scratch state, pick resources, optional occlusion hierarchy textures and readback slots, and optional SMAA resources. Internal modules under ./typescript/core/ hold the mechanical helper bodies for resources, timing, postprocessing, transmission, shadows, drawlists, materials, objects, picking, and occlusion. Renderer is the only public runtime renderer class.
./typescript/core/stats.ts stores frame timing, GPU timing when timestamp queries are available, and frame counters. ./typescript/core/transform.ts owns the global transform store and calls Rust functions for transform propagation.
The scene model is implemented in ./typescript/world/scene.ts. A Scene owns arrays of meshes, pointclouds, glyphfields, nodelinks, splatfields, latticespaces, lights, and a background color. It mutates those arrays through add(), remove(), clear(), and typed add/remove helpers.
The renderer reads visible scene objects through scene getters. It also reads scene lighting data and scene bounds. The scene limits non-ambient lights used for packed lighting data to MAX_LIGHTS, currently eight.
Objects in the scene own different data:
./typescript/world/mesh.ts: geometry, material references, transform, optional skin, optional morph runtime state, visibility flags, shadow caster/receiver flags, and scene owner references. Mesh vertex and index data enters the renderer throughGeometry../typescript/world/pointcloud.ts: point data, optional borrowed external WebAssembly memory views, bounds, scale source metadata, colormap state, transform, GPU buffers, uniform buffers, optional CPU records for picking, and per-buffer ownership flags for external point and color buffers../typescript/world/glyphfield.ts: glyph geometry, instance data, optional borrowed external WebAssembly memory views, scale source metadata, transform, GPU buffers, uniform buffers, optional CPU records for picking, and per-buffer ownership flags for external instance buffers../typescript/world/nodelink.ts: node data, edge data, optional borrowed external WebAssembly memory views, node and edge rendering modes, scale source metadata, transform, GPU buffers, uniform buffers, optional CPU records for picking, and per-buffer ownership flags for external node and edge buffers../typescript/world/splatfield.ts: Gaussian splat instance data, optional borrowed external WebAssembly memory views, packed GPU buffers, transform, bounds, optional CPU records for upload and bounds computation, and per-object ownership flags for external buffers../typescript/world/latticespace.ts: regular 2D or 3D cell dimensions, X-fastest scalar/vector/RGBA data, optional activity masks, CPU/WebAssembly/GPU source ownership, scale and colormap state, analytic bounds, and retained cell records for picking.
Mesh geometry and materials are reference-counted. Mesh.destroy() detaches the mesh from scene owners, destroys morph runtime state, disposes the skin instance, disposes the transform, and releases geometry and material references. Scene.clear() and typed clear methods remove scene references; they do not call destroy() on removed objects. Scene.destroy() calls destroy() on current meshes, pointclouds, glyphfields, nodelinks, splatfields, and latticespaces, then clears lights. Pointcloud, glyphfield, nodelink, splatfield, and latticespace changes should be checked against renderer draw-list building, object-family culling behavior, GPU buffer ownership, picking paths, overlay legends where applicable, and tests.
Transforms are implemented in ./typescript/core/transform.ts and backed by WebAssembly memory. The global TransformStore uses structure-of-arrays storage for positions, rotations, scales, local matrices, world matrices, parent indices, and traversal order.
Each Transform object keeps JavaScript-side position, rotation, and scale arrays plus parent/child links. Mutating a transform marks it dirty. The store then calls Rust functions from ./rust/src/transform.rs through ./typescript/wasm/index.ts to compose local matrices and update world matrices.
The transform store mutates:
- WebAssembly memory for transform components and matrices;
- parent indices and ordered traversal data;
- dirty sets and free-list state.
Capacity growth allocates and copies replacement structure-of-arrays blocks before releasing the superseded WebAssembly allocations. Capacity-independent temporary vector and quaternion blocks remain attached to the active global store rather than being reallocated during growth. The renderer calls Transform.updateAll() before gathering draw lists. Cameras, scene objects, controls, animation clips, and glTF import all write transform data. Contributors changing transform behavior should preserve parent cycle checks, dirty marking, disposal checks, allocation replacement ordering, and the index mapping between TypeScript objects and WebAssembly slots.
Graphics data is implemented under ./typescript/graphics/.
./typescript/graphics/geometry.ts owns vertex arrays, index arrays, optional tangents, RGBA vertex colors, skinning attributes, morph target arrays, GPU vertex and index buffers, bounds, and reference counts. It can also borrow external WasmMemoryView sources for mesh vertex attributes, colors, and indices, explicitly refresh them, and upload active ranges into geometry-owned GPU buffers without owning or freeing the external WebAssembly memory. It calls Rust bounds and normal-generation functions through ./typescript/wasm/index.ts; these synchronous operations use scoped WebAssembly heap scratch that is released after results are copied into JavaScript-owned arrays. Geometry factories create primitives such as boxes, spheres, cylinders, curves, surfaces, torus geometry, and prism geometry.
./typescript/graphics/material.ts implements Material, UnlitMaterial, StandardMaterial, DataMaterial, and CustomMaterial. Built-in materials own their managed uniform buffers, bind groups, texture references, WebGPU state, and dirty state, while standard materials expose glTF-style physically based rendering properties. Standard material layouts and imported WGSL shaders are specialized and cached by material features and mesh variant, with resource-limit validation before layout creation.
DataMaterial reads f32 data or external GPU buffers, uses a ScaleTransform, and can feed the scaling service. It owns GPU data buffers created from CPU data; setDataBuffer() stores caller-provided buffer handles without marking them owned. CustomMaterial is the renderer-side caller-WGSL escape hatch. Renderer camera/model/global state remains bind group 0, while its caller-declared material resources occupy bind group 1. Its binding layout is immutable after construction, resources can be replaced without changing render-pipeline identity, and external buffers, samplers, and texture views are borrowed rather than destroyed with the material. A custom material may also use an empty group-1 layout and bind group. Custom materials remain outside built-in instancing and skinning paths. Material changes should be checked against renderer pipeline selection, WGSL shader variants, bind group layout creation, texture upload paths, tests, and glTF import.
./typescript/graphics/texture.ts wraps image bytes, URLs, ImageBitmap sources, sampler descriptors, GPU textures, and mipmap generation. Texture upload is asynchronous, supports configurable browser decoding and safe cancellation, and exposes terminal upload errors while renderer paths use fallback resources. Shared URL and glTF sources reuse encoded image data, while differing linear and sRGB uses receive separate GPU textures and mip chains.
./typescript/graphics/colormap.ts stores built-in or caller-provided color stops, optional CPU lookup tables, and optional caller-provided GPU texture views and samplers. It can create cached GPU lookup resources for a device, and CPU sampling works only for colormaps that have CPU lookup data.
Pointclouds, glyphfields, nodelinks, splatfields, and latticespaces are separate scene object types. They share several patterns: a transform, visibility flags, GPU buffer references, uniform buffers, bind groups, bounds, and renderer-owned draw-list state. Pointclouds, glyphfields, nodelinks, and latticespaces carry scale transform state plus optional CPU records for picking, while splatfields carry packed Gaussian attributes, optional CPU records for picking, and private renderer-side GPU sort state for transparent rendering.
./typescript/world/pointcloud.ts handles point records. It reads packed point attributes from typed arrays, borrowed external WasmMemoryView<Float32Array> sources, or an external GPU buffer, writes point and uniform GPU buffers, and computes bounds through Rust bounds helpers when CPU data is available or external WebAssembly data is explicitly refreshed with bounds recomputation. Pointcloud WebAssembly sources are borrowed, refresh is explicit, and upload copies the active packed f32 vec4 ranges into pointcloud-owned GPU buffers without freeing or owning the external WebAssembly memory. It owns GPU buffers it creates from CPU data or external WebAssembly sources and destroys caller-supplied external point and color buffers only when ownBuffers: true or setter-level ownBuffer: true is used. Its destroy() method always destroys the stored uniform buffer.
./typescript/world/glyphfield.ts handles instanced glyph geometry. It can read CPU arrays, internal WebAssembly structure-of-arrays pointers, borrowed external WasmMemoryView<Float32Array> sources, or external GPU buffers. It writes instance buffers and uniform buffers. Glyphfield external WebAssembly sources are borrowed, refresh is explicit, and upload copies active packed f32 vec4 channel ranges into glyphfield-owned GPU buffers without freeing or owning the external WebAssembly memory. It owns GPU buffers it creates from CPU arrays, internal WebAssembly pointers, or external WebAssembly sources and borrows caller-supplied external instance buffers by default, destroying them only when ownBuffers: true or setter-level ownBuffers: true is used. Glyph geometry is a Geometry reference used by the renderer; GlyphField.destroy() currently destroys the stored uniform buffer plus only the instance buffers it owns, and it disposes its transform. It does not release the geometry reference. Glyphfield bounds use Rust bounds helpers when CPU arrays, internal WebAssembly pointers, or explicitly refreshed external WebAssembly position/scale data are available.
./typescript/world/nodelink.ts handles node positions, node scalar or color data, node radii, edges, node geometry modes, edge geometry modes, separate node and edge scale transforms, separate colormaps, uniform data, and picking records. It can read CPU arrays, borrowed external WasmMemoryView sources, or external GPU buffers per channel. Nodelink external WebAssembly sources are borrowed, refresh is explicit, and upload copies active node and edge ranges into nodelink-owned GPU buffers without freeing or owning the external WebAssembly memory. It computes bounds from retained CPU node positions in TypeScript. Its update methods can queue or write GPU-buffer changes for node positions, edges, scalars, radii, and colors. It owns GPU buffers it creates from CPU data or external WebAssembly sources and borrows caller-supplied external node and edge buffers by default, destroying them only when ownBuffers: true or setter-level ownBuffer: true is used. Its destroy() method always destroys the stored uniform buffer. The renderer has nodelink draw and pick paths, and overlay legends can read nodelink scale and colormap state.
./typescript/world/splatfield.ts handles precomputed Gaussian splat records. It reads friendly CPU arrays, borrowed external WasmMemoryView<Float32Array> sources, or caller-supplied packed GPU buffers for center-plus-opacity, rotation, scale, and either direct color data or spherical harmonic color coefficients. Splatfield external WebAssembly sources are borrowed, refresh is explicit, and upload copies active packed ranges into splatfield-owned GPU buffers without freeing or owning the external WebAssembly memory. Splatfield keeps CPU arrays, external WebAssembly memory upload, and external GPU buffers as distinct source families. Spherical harmonic coefficients use flat f32 RGB-triple storage for degree 0 through degree 3 and can come from sh0, sh1, sh2, and sh3 CPU arrays, an external shBuffer, or an external wasmSphericalHarmonics source. CPU direct colors in sRGB space are decoded to linear during packing. External direct colors in sRGB space and spherical harmonic colors in sRGB space are decoded in the shader. In spherical harmonic mode, the splatfield render shader evaluates RGB from the local-frame view direction; it does not rotate coefficients with Wigner-D matrices. Bounds can come from explicit descriptors or from conservative CPU-side center-plus-scale expansion. SplatField.destroy() destroys internally created buffers and optionally destroys caller-supplied external buffers when ownBuffers: true. The renderer treats splatfields as transparent-only objects, GPU-sorts each field by camera depth for rendering, includes them in nearest-footprint picking without consuming the sorted index buffer, but excludes them from occlusion culling.
./typescript/world/latticespace.ts handles regular 2D and 3D cell lattices. Public dimensions are spatial [x, y] or [x, y, z], while flat data is C-compatible X-fastest storage: x + width * y in 2D and x + width * (y + height * z) in 3D. Dimensions and component count are immutable structural properties. Scalar, vector, RGBA, and solid-color visualization share scale, colormap, clipping, mask, filtering, ownership, WebAssembly refresh, and CPU-retention behavior with the other scientific primitives. Bounds are analytic from origin, spacing, cell scale, index range, and transform. The renderer draws one procedural quad for 2D lattices and procedural cube instances for 3D lattices; cellScale < 1 exposes separated cell faces, while fully packed cells suppress shared internal faces. Transparent 3D lattices use a latticespace-private GPU radix sorter over implicit cell centers; it reuses the raw scan WGSL sources but does not use or extend the public compute kernel API. Picking returns exact linear and spatial cell indices, and opaque latticespaces participate in occlusion capture and filtering. This surface renderer does not implement raymarched participating-media volume rendering, bricking, or out-of-core streaming.
Before changing one of these object types, inspect the matching WGSL under ./wgsl/world/, the renderer drawlist and object helper code under ./typescript/core/renderer.ts, ./typescript/core/drawlists.ts, ./typescript/core/objects.ts, ./typescript/core/picking.ts, and ./typescript/core/occlusion.ts when applicable, picking types in ./typescript/world/picking.ts when the object participates in picking, scale source descriptors in ./typescript/scaling/ when the object participates in scaling, and tests such as ./tests/nodelink.test.js or ./tests/splatfield.test.js.
Lights are implemented in ./typescript/world/light.ts. Ambient, directional, point, and spot lights share a base class. Scene lighting data reads light colors, intensities, positions, directions, ranges, and cone angles. glTF import can bind light transforms so imported lights follow imported node transforms.
Cameras are implemented in ./typescript/world/camera.ts. The base Camera owns a transform and cached view and projection matrices, with camera orientation derived from hierarchy rotations while ignoring scale. PerspectiveCamera and OrthographicCamera expose their projection parameters; perspective cameras support automatic or fixed aspect ratios and finite or infinite far planes. glTF import preserves authored fixed aspect ratios and omitted infinite perspective far planes.
Controls are implemented in ./typescript/world/controls.ts. NavigationControls handles orbit, trackball, and fly modes, pointer, keyboard, and wheel input, damping, target state for target-centric modes, fitting to bounds, pointer lock, configurable fly yaw, and event listener cleanup. OrbitControls, TrackballControls, and FlyControls are mode-specific subclasses and runtime factories. Controls mutate camera transforms and emit events used by overlays.
Lights, cameras, and controls changes should be checked against renderer uniform packing, scene bounds fitting, overlay projection, glTF camera and light import, and tests that inspect matrix or lighting output.
The public renderer class is implemented in ./typescript/core/renderer.ts. It stores the WebGPU device, queue, canvas context, and swapchain format. It owns renderer-created bind group layouts, fallback resources, pipeline caches, shader caches, a growable packed model-uniform buffer, instance buffers, culling scratch memory, pick resources, optional shadow resources, optional occlusion hierarchy resources, and optional SMAA resources. The helper files under ./typescript/core/ split the implementation mechanically by responsibility: ./typescript/core/resources.ts, ./typescript/core/timing.ts, ./typescript/core/postprocessing.ts, ./typescript/core/transmission.ts, ./typescript/core/shadows.ts, ./typescript/core/drawlists.ts, ./typescript/core/materials.ts, ./typescript/core/objects.ts, ./typescript/core/picking.ts, and ./typescript/core/occlusion.ts.
Renderer.create() requests a WebGPU adapter and device. It currently supports descriptor fields for antialiasing, power preference, canvas format, frustum culling, frustum culling stats, occlusion culling, occlusion culling stats, and requested device limits such as maximum buffer and binding sizes.
Renderer.render(scene, camera) currently performs these steps:
- resize the canvas and recreate size-dependent resources when needed;
- reset pick-uniform and instance-buffer offsets;
- allocate camera, lighting, and packed model staging memory from the WebAssembly frame arena;
- update camera matrices and scene transforms;
- resolve explicitly enabled directional shadow views, lazily create compatible shadow resources, and prepare shadow metadata and independent caster lists;
- write camera and lighting uniforms;
- build unfiltered draw lists for meshes, pointclouds, glyphfields, nodelinks, splatfields, and latticespaces;
- collect distinct non-instanced model records, pack their model and normal matrices in one Rust call, and upload one aligned dynamic-uniform range;
- aggregate render-only frustum culling stats when enabled;
- optionally reuse a valid previous-frame occlusion hierarchy to conservatively filter opaque meshes, pointclouds, glyphfields, nodelinks, and latticespaces;
- GPU-sort dirty visible splats and transparent 3D lattice cells inside their respective renderer-private sort paths before transparent draw encoding, reusing stable sorted indices and persistent specialized bind groups when inputs are unchanged;
- encode required shadow depth layers before the main lighting pass;
- encode render passes for opaque objects, transmission copies and passes when needed, transparent objects, and optional SMAA;
- encode timestamp query resolve and readback when GPU timing is available;
- submit the command buffer;
- optionally capture a low-resolution opaque depth hierarchy for a later frame and schedule its readback without blocking the current frame.
The renderer reads scene objects, transform world matrices, material state, texture views, geometry buffers, pointcloud buffers, glyphfield buffers, nodelink buffers, splatfield buffers, latticespace data and mask buffers, camera matrices, light data, and WebAssembly culling results. It mutates GPU buffers, bind groups, pipeline caches, draw-list pools, pick textures, timing query buffers, private splatfield and latticespace sort buffers, internal counters, and the bounded ring of occlusion readback slots. Splatfield bind groups include a spherical harmonic storage-buffer binding; non-SH splatfields use a renderer-owned dummy buffer for that binding. Picking helpers are in ./typescript/core/picking.ts while render-only previous-frame occlusion helpers are in ./typescript/core/occlusion.ts. Note that picking and warmup do not apply render-only previous-frame occlusion filtering.
Opaque mesh submission reuses packed instance runs while their exact membership, geometry identities, buffer generation, and world matrices remain unchanged. Transparent submission preserves the exact global comparator across all six object-family lists with a fixed-head k-way merge, avoiding construction and sorting of a copied combined list; a single populated family executes directly.
Changing render behavior usually means checking ./typescript/core/renderer.ts, the relevant helper module(s) under ./typescript/core/, the graphics classes in ./typescript/graphics/, object classes in ./typescript/world/, WGSL shader variants in ./wgsl/, and renderer-focused tests in ./tests/.
Browser runtime resources are WebGPU objects created and mutated by renderer and compute paths. The renderer creates the adapter, device, queue, canvas context, render targets, depth targets, texture views, samplers, bind group layouts, bind groups, render pipelines, and render pass descriptors. The compute subsystem stores device and queue references from the runtime and creates storage buffers, uniform buffers, compute pipelines, bind groups, scratch buffers, and readback staging buffers.
Model and normal matrices occupy 128-byte records in one growable uniform buffer. Record offsets use minUniformBufferOffsetAlignment, one bind group spans the buffer, and draw calls select records with dynamic offsets. Picking retains its independent uniform-buffer pool because its identifier payload has a different lifecycle. Static opaque instance runs reuse their prior packed buffer range until a conservative run-cache check detects a change.
Active shadows add one renderer-owned depth32float texture array, per-layer render views, one comparison sampler, metadata/view/model uniform buffers, bind groups, and cached caster pipelines. Reconfiguration destroys and replaces incompatible shadow textures and buffers; disabling the final active scene shadow releases the lazy resources on the next prepare.
Geometry, materials, textures, pointclouds, glyphfields, nodelinks, splatfields, latticespaces, skin instances, and compute objects store WebGPU handles. Render and compute command encoding read those handles. Destruction paths exist for renderer resources, textures, geometry and material buffers, pointclouds, glyphfields, nodelinks, splatfields, latticespaces, private splatfield and latticespace sort scratch buffers, skin instances, compute buffers, readback rings, the compute scratch allocator, and GPUndarray-owned buffers. Compute pipeline wrappers do not expose a destroy() method because WebGPU pipeline objects are not explicitly destroyed.
Changing browser resource layout requires checking usage flags, bind group layout entries, pipeline layout creation, shader bindings, fallback resources, resize paths, and destruction paths. A resource layout change usually spans TypeScript, WGSL, and at least one test or example.
Picking types and selection helpers are implemented in ./typescript/world/picking.ts. The renderer keeps the public pick wrappers in ./typescript/core/renderer.ts, while ./typescript/core/picking.ts implements the actual pick passes. Pick rendering writes object and element identifiers into GPU textures, then reads back the requested pixel or region.
The runtime exposes pick(), pickRect(), and pickLasso() from ./typescript/core/engine.ts. These methods call the renderer and then add object-specific attributes for pointclouds, glyphfields, nodelinks, splatfields, and latticespaces when CPU records are available. Retained splatfield CPU records can include direct color data or spherical harmonic degree and coefficient data. Picking is nearest-footprint/depth picking and does not evaluate spherical harmonic visual contribution. Pick preparation uses the renderer's base scene-preparation path only, so it does not apply render-only occlusion filtering and does not consume previous-frame occlusion results.
The overlay framework lives under ./typescript/overlay/. ./typescript/overlay/system.ts creates a DOM overlay root next to the canvas, registers discoverable layers by ID, and gives each registration a system-owned wrapper so all present and future layer output can be hidden without detaching it or rewriting child display styles. The update context still exposes the real system root. Removal remains the resource-lifecycle boundary. The system observes resize and scroll changes, listens to controls when attached, and collects dirty reasons for camera, viewport, layout, scale, colormap, or interaction changes.
Built-in axis triad, grid, and legend layers retain their optional system relationship and self-invalidate after typed runtime mutations. They are DOM/canvas overlays with fixed or pooled node bounds. Grid label collision work only considers candidates that survive its spacing, stride, and budget filters, while legend gradient backing scales with display pixel ratio without changing its CSS-space layout.
Annotations live under ./typescript/overlay/annotation/. The annotation toolkit owns an AnnotationStore; Scene does not own annotation records. The store owns marker, distance, and angle records. The toolkit reads picking results, creates marker objects in the scene through the marker renderer, and creates labels through the label layer.
Overlay and annotation changes cross both DOM and WebGPU paths. Inspect layer types, projection helpers, picking behavior, annotation store revisions, marker scene objects, and examples that attach overlays to controls.
The compute subsystem is implemented under ./typescript/compute/. ./typescript/compute/index.ts stores the WebGPU device and queue references used for compute work. It creates storage buffers, uniform buffers, compute pipelines, readback rings, one scratch allocator and one GPU ndarray engine shared by the kernels and the per-instance compute.ndarray namespace, and an optional RGBA8 blitter.
Compute.wrapStorageBuffer() wraps a caller-supplied GPUBuffer without allocating or copying GPU storage. External buffers are borrowed by default; ownBuffer: true transfers destruction responsibility to the wrapper. StorageBuffer.buffer remains the raw handle, while offsetBytes and byteLength describe its logical range and usage preserves the underlying usage flags. Wrapping validates safe integer bounds, storage usage, and the device's storage-offset alignment. Same-device provenance and the lifetime of externally destroyed resources remain caller preconditions; wrappers do not track aliases or reference-count external buffers. Wrapper destruction is idempotent, invalidates later buffer operations, and destroys the raw resource only when owned.
Storage writes, direct reads, shared ReadbackRing copies, compute bindings, blits, and scaling preserve the logical range. Explicit binding offsets are relative to the wrapper, and readback and write operations require the existing COPY_SRC and COPY_DST flags respectively. Incompatible ranges or usage flags fail without fallback allocation or copies. Aligned transfers and bindings must fit inside an external view; internal allocations retain their existing padding for odd-byte and empty storage. Blit and scaling caches distinguish wrappers over different ranges of one raw buffer. WebGPU still validates device provenance and resource usage conflicts, including simultaneous read-only and writable storage uses of one raw buffer.
The main compute files are:
./typescript/compute/buffer.ts: storage and uniform GPU buffer wrappers, writes, reads, and typed reads../typescript/compute/pipeline.ts: compute pipeline descriptors, pipeline creation, and bind group creation../typescript/compute/dispatch.ts: command encoding and dispatch validation../typescript/compute/workgroups.ts: workgroup size, count, and folding helpers../typescript/compute/kernels.ts: built-in GPU-onlyGPUndarrayelementwise, scalar, dot, matrix product, reduce, arg-reduce, scan, histogram, compact, radix sort, scaling, and LU factor/solve kernels../typescript/compute/readback.ts: reusable asynchronous GPU readback slots../typescript/compute/scratch.ts: the shared scratch allocator for kernel and ndarray parameter regions and pooled temporaries../typescript/compute/ndarray.ts: CPU and GPU ndarray layout, dtype, stride, offset, and view logic, thecompute.ndarraynamespace, transfers, and the WebAssembly and WebGPU transformation and reduction engines, over./wgsl/compute/ndarray.wgsl, its reductions fragment./wgsl/compute/ndarray-reduce.wgsl, and its affine fragment./wgsl/compute/ndarray-affine.wgsl../typescript/compute/blit.ts: RGBA8 storage-buffer-to-canvas blitting.
The compute subsystem reads WGSL from ./wgsl/compute/, shared explicit WebGPU layouts and resources from ./typescript/wgsl/, WebGPU buffer descriptors, ndarray descriptors, and caller-provided typed arrays. ComputePipeline remains responsible for arbitrary-WGSL pipeline creation, automatic or explicit layouts, bind-group creation, and dispatch integration, as the WebGPU interop subsystem does not execute or schedule compute work. It mutates GPU buffers, command encoders, readback staging buffers, scratch allocator buffers, and WebAssembly-backed CPU ndarray memory. CPUndarray owns its backing bytes plus shape and stride allocations; scalar indexing is checked locally in TypeScript, and destroy() releases the owned WebAssembly allocations idempotently and makes later memory access throw. GPUndarray destroys only buffers it owns and continues to borrow buffers supplied through wrap(), while every destroyed array, owned or borrowed, rejects later bindings and ndarray operations.
compute.ndarray is an NdarrayNamespace constructed by each Compute with its device, queue, and readback ring, and it is the single entry point for creating, transferring, viewing, and transforming ndarrays. x instanceof compute.ndarray tests for any ndarray, Compute.destroy() destroys the namespace before the readback ring, kernels, and shared GPU ndarray engine and destroys the shared scratch allocator last, while the RGBA8 blitter's parameter buffers and the ndarray engine's empty binding buffer are dropped rather than destroyed because a caller's pending encoder may still reference them; later namespace calls throw. empty(), zeros(), and fromArray() create CPU arrays by default or GPU arrays with residency: "gpu"; GPU arrays created by the namespace default to copySrc: true and copyDst: true, and GPU buffer options are rejected for CPU residency. wrap() borrows a StorageBuffer from the namespace's device. toGPU() and toCPU() replace the former per-object upload and readback methods, preserve shape, strides, and layout offset exactly, and copy only the array's own byte extent; toCPU() requires COPY_SRC and uses the shared readback ring as an optional accelerator, with direct storage-buffer readback remaining available after that ring is destroyed. The static constructors on CPUndarray and GPUndarray remain the low-level layer used by Python interop and by CPU-only code that initializes WebAssembly without a GPU device.
Metadata-only views (slice() with Python basic-slicing semantics, transpose(), swapAxes(), moveAxis(), flip(), reshape() with NumPy's no-copy algorithm, squeeze(), expandDims(), and NumPy-rule broadcastTo()) are synchronous, keep the input's residency, and borrow the root owning array's storage. A view's base is that root rather than an intermediate view, destroying a view releases only what the view allocated, and destroying the root invalidates every view; CPU views check the root's destruction state and allocate their WebAssembly shape and stride arrays only on first access, while GPU views also fail through StorageBuffer destruction tracking. Each view is rebased onto the byte extent of its own elements, as CPU views advance their base pointer to the extent start and GPU views advance baseOffsetBytes to the 4-byte-aligned extent start and keep the remainder in offsetBytes, so transfers stay proportional to the view. isContiguousC follows NumPy's relaxed C_CONTIGUOUS rule, ignoring the strides of size-1 dimensions and the layout offset, and CPUndarray.data() therefore works for contiguous views. CPUndarray.set() writes through views to the base storage. real(), imag(), and viewAsReal() view a complex array as its f32 or f64 components, imag() at an offset of one component and viewAsReal() with a trailing axis of length 2, while viewAsComplex() follows PyTorch's view_as_complex shape rules and requires a last axis of length 2 with a one-component stride, element-multiple strides on every other axis longer than 1, and an element-aligned first element. Views may therefore change dtype, and each is placed with its own element size.
Materialized operations (contiguous(), cast(), pad(), normalize(), resize(), and resample()) always return new owned C-contiguous arrays in the input's residency, while copyInto() writes a broadcastable source into a caller-supplied destination of the same residency and GPU operations accept a caller-supplied C-contiguous out. CPU operations run in Rust through the ndarrayf WebAssembly wrappers with per-call heap descriptors, and GPU operations run as storage-buffer WGSL dispatches; nothing transfers implicitly. Every operation validates liveness, coalesces dimensions that are contiguous in every participating layout, including normalize()'s parameter layouts, while keeping padded and resized axes separate, and supports at most 64 coalesced dimensions on the CPU and 8 on the GPU. copyInto() rejects destinations that may address the same memory twice and copies through a temporary when CPU source and destination storage overlap or when GPU source and destination share a raw GPUBuffer, as WebGPU forbids read-only and writable bindings of one buffer in a dispatch. GPU destinations with 8-bit or 16-bit dtypes must be C-contiguous so that each invocation owns whole 32-bit words and merges the partially covered words at either end, while GPU invocations move 8-byte and 16-byte elements as two or four 32-bit words. normalize() rejects complex inputs and complex dtype options before validating its other parameters, while resize() and resample() interpolate complex arrays componentwise with real weights and reject complex-to-real dtype options, and resized input axes are limited to 2³¹−1 elements because tap indices are i32 on both backends.
cast() follows Rust as semantics on both backends: integer conversions wrap unless overflow: "saturate" is requested, float-to-integer conversions truncate and saturate with NaN mapped to zero unless rounding: "nearest-even" or overflow: "wrap" is requested, integer-to-f32 and f64-to-f32 conversions round to nearest even, and widening to f64 is exact. Casts into c64 and c128 convert the real part by the same rules from every real dtype and write a +0 imaginary part, c64↔c128 casts convert each component, and casts from complex to real or integer dtypes are rejected on both backends with a hint to cast a real() or imag() view instead. NaN conversions between f32 and f64 keep the sign and payload bits that fit and set the quiet bit. These conversions deliberately differ from the typed-array assignment used by fromArray(). normalize() applies affine maps whose parameters are numbers, per-axis number arrays, broadcast ndarrays, or statistics of the data, as described after the reductions below. Integer outputs of normalize(), resize(), and resample() round half to even, saturate, and map NaN to zero, so cubic overshoot saturates, while floating-point outputs are not clamped. Layout operations and casts are bit-exact and identical on the CPU and GPU, and normalization computes in f64 on the CPU and f32 on the GPU. GPU operations reject f64↔integer and c128↔integer casts and f64 inputs and outputs of normalize(), while f64↔f32, f64→c64, f32→c128, and c64↔c128 casts and f64 and c128 layout operations use integer bit routines.
resize() interpolates along any number of distinct axes with nearest-exact, linear, or Keys cubic interpolation (cubicCoefficient, −0.75 by default), half-pixel or corner alignment, optional antialiasing that widens the linear or cubic filter by the scale factor on shrinking axes, and per-axis pad()-style boundaries plus "renormalize", which drops out-of-range taps and rescales the rest; size and boundary take one value or one per axis. resample() takes the same methods, antialiasing, and boundaries but no alignCorners: output index d of each resampled axis reads the source at the continuous index x = d × scale + offset, computed in f64 from per-axis scale and offset (0 by default), with integer indices at sample centers as in LatticeSpace and diagonal-affine scipy and ITK resampling. For physical grids with sample centers at origin + index × spacing, scale = outSpacing / inSpacing and offset = (outOrigin − inOrigin) / inSpacing, where LatticeSpace orders origin and spacing as [x, y, z] while its data is the ndarray axes [z, y, x]; scale = nIn / nOut with offset = scale / 2 − 1 / 2 matches half-pixel resize() within rounding but not bitwise, because resize() uses exact rational coordinates. Negative and zero scales flip or broadcast the source, antialiasing widens the filter by max(1, |scale|), nearest sampling rounds half up from i = floor(x) and t = x − i (not floor(x + 0.5), which rounds 0.49999999999999994 up), a nearest constant tap writes value as output-dtype bits after value passes the output dtype's representability check, weighted constants keep resize()'s value rules, value applies to "constant" and "renormalize" boundaries, a "renormalize" row left without in-range taps, or whose in-range weights sum to a non-positive value (including a single in-range tap with a non-positive weight), becomes the constant value, and both endpoints of every axis must satisfy |x| + reach < 2³¹ − 1, where reach is the filter support (½ nearest, 1 linear, 2 cubic) times max(1, |scale|) with antialiasing. Both read per-axis CSR tap tables built once per call in TypeScript: zero-weight taps are omitted, taps that map to one source index are merged, every row is normalized by its sum, single-tap rows pass samples through without arithmetic, a table holds at most 2³¹−1 taps, and an axis whose table is the identity is dropped unless every axis is, in which case the highest one stays so that the output conversion still applies. Axes are sorted ascending whatever order the caller lists them in, and each output sums the taps of the highest axis innermost and of the lowest axis outermost, left to right from the first product, with a constant tap at any level contributing exactly the constant and skipping the levels inside it.
A pass planner groups the remaining axes, in that canonical order, into contiguous passes of at most three levels that run innermost first. It minimizes Σ output elements × (product of the pass's mean taps per row + a write cost of 0.4 tap reads on the CPU and 6 on the GPU), calibrated on both backends, with one backward recurrence over the first level of each pass, since a pass's output shape does not depend on how the levels inside it were grouped; separable passes are the usual plan for 3D linear and cubic upsampling and wide antialiased filters, while downsampling without antialiasing and 2D linear GPU upsampling stay in one pass. For the same reason an intermediate's shape depends only on the level where the pass that writes it starts, so the recurrence excludes start points whose intermediate does not fit (on the GPU maxStorageBufferBindingSize, maxBufferSize, and 2³²−1 elements, on the CPU the wasm32 address space) and finds a feasible grouping whenever one exists; a GPU resize without one fails with an error naming both device limits and the smallest intermediate that every grouping would have to write, and every pass's parameter table must fit maxStorageBufferBindingSize. Because WebAssembly memory never shrinks, ComputeDescriptor.ndarray.maxCPUIntermediateBytes (a safe integer ≥ 0, 64 MiB by default) caps the CPU intermediates of consecutive passes, which are alive together, as a soft preference rather than an error: the recurrence keeps every such pair within the cap when some grouping allows it and otherwise within the smallest pair any grouping allows, so 0 runs every resize of up to three levels in one pass and gives larger ones their smallest intermediates. The default leaves the plans of the resize benchmarks unchanged, while, for example, a 4096² f32 → 8192² linear resize stays in one pass instead of growing the heap by a 256 MiB f64 intermediate. The CPU output is allocated only for the final pass. Weighted intermediates are f64 or c128 on the CPU, so CPU results are bit-identical for every grouping and equal successive single-axis resizes from the highest axis down, and f32 or c64 on the GPU, which is the GPU's accumulator format, so a split adds no rounding; nearest chains through output-dtype intermediates that convert in their first pass and gather raw bits after it. A GPU call acquires at most two ping-pong scratch temporaries and re-wraps them per pass, and a CPU call keeps at most two intermediates alive and destroys each once the next pass has consumed it. Nearest resizing and resampling are bit-identical gathers on the CPU and GPU for every dtype pair that each backend accepts, including constant taps, and linear and cubic interpolation computes in f64 on the CPU and f32 on the GPU: a GPU value lies within γ_N·B + (1 + γ_N)·2⁻¹²⁶·F of the CPU value before output conversion, with γ_N = N·2⁻²⁴ / (1 − N·2⁻²⁴) and N = 2 + Σ(K_l + 1) over the output's rows with K_l > 1 taps (to first order (ΣK_l + L + 2)·2⁻²⁴·B for L levels: K_l roundings in each row's sequential sum, one for each row's f32 weights, one for converting the sample or constant to f32, and one for the CPU's own f64 rounding), where B sums over tap paths the product of |w| and the absolute sample or constant, and F = Π S_l + Σ (2K_l − 1)·Π_{k<l} S_k with S_l the row's sum of |w| bounds flushing of subnormal products, sums, and samples. The bound does not depend on the pass grouping, and integer outputs may differ by 1 only near rounding midpoints and saturation edges. On the GPU, linear and cubic resize() and resample() reject f64 and c128 inputs and outputs and compute complex values in c64, while nearest resize() and resample() follow the GPU cast() dtype rules.
sum(), prod(), mean(), var(), std(), min(), max(), argmin(), and argmax() reduce any strided view, including negative, zero, and unaligned strides, and return new owned C-contiguous arrays in the input's residency or a GPU out. axes takes one axis, several, none, or by default every axis, negative or in any order, and keepDims keeps reduced axes with length 1, so reducing every axis without keepDims returns a 0-d array and axes: [] returns a converted copy of each element's own result; argmin() and argmax() take one axis instead and otherwise report the flat C-order index of the logical view. The fused minMax(), varMean(), and stdMean() return named outputs and take per-output out objects whose entries are optional individually and must not overlap, their values equal the separate operations bitwise, and minMax() reads the source once. Integer inputs sum and multiply into i32 or u32 by default and average into f32, following normalize() rather than NumPy's f64; var() and std() of c64 and c128 are f32 and f64; min() and max() keep the input dtype and reject dtype; and the arg-reductions write u32. dtype overrides other outputs within their kinds: integer inputs of sum() and prod() may produce any real dtype, float inputs f32 or f64, and complex inputs c64 or c128, while mean(), var(), and std() produce f32 or f64, or c64 or c128 for a complex mean(), and a fused mean of a complex input uses the complex counterpart of a real dtype. Complex values have no ordering, so the ordering reductions reject them with a hint to reduce real() or imag() views. Accumulators are fixed per backend, so dtype chooses only the conversion of the result, and an integer prod() written as a float converts its wrapped 32-bit value.
Integer sums accumulate exactly in 64 bits on both backends, because at most 2³²−1 elements reach a reduction, and then convert once: integer outputs saturate by default or wrap with overflow: "wrap", and f32 and f64 outputs round to nearest even like Rust's as, so integer sums written as any dtype are bit-identical on the CPU and GPU. prod() is never compensated: integer products wrap modulo 2³², always and on both backends, and reject overflow: "saturate", float products multiply in f64 on the CPU and f32 on the GPU, and complex products use the usual complex multiplication. overflow applies only to integer outputs of integer sum() and prod(), and other reductions reject it. With nan: "propagate", the default, any NaN makes sum(), prod(), mean(), var(), std(), min(), and max() NaN, and argmin() and argmax() return the lowest NaN's index. nan: "omit" skips NaN and does not count it toward n or ddof, keeps infinities as data, and on a slice with nothing left gives 0 for sum(), 1 for prod(), NaN for mean(), var(), std(), min(), and max(), and 0xFFFFFFFF, never a valid index, for argmin() and argmax(), because throwing on data would need a readback. nan is accepted and ignored for integer inputs. A complex element counts as NaN when either component is, and every NaN a reduction writes is the canonical positive quiet NaN, in both components of a complex result, so NaN bits are identical whenever both backends produce NaN. On the GPU, NaN and infinities are detected only with bitcast bit tests and combined by bit rules, never with comparisons or the min() and max() builtins, because WGSL lets implementations assume they are absent.
min(), max(), and minMax() follow IEEE 754-2019 minimum and maximum, ordering −0 below +0, by comparing integer total-order keys formed from raw element bits: a float maps to ~bits when its sign bit is set and bits | signBit otherwise, integers map so that unsigned comparison gives their numeric order, and f64 keys are two words on the GPU. Key values outside every float's range stand for NaN, so states need no flag: a propagated NaN takes the key that beats every number, an omitted NaN changes nothing, and a state still at its identity decodes to NaN. Results therefore do not depend on the reduction tree, and the value written is the winning element's original bits. argmin() and argmax() compare numerically, canonicalizing −0 to +0 before forming the key, and resolve ties to the lowest index, so [+0, −0] has min() −0 while argmin() is 0. A reduced extent of 0 with a non-empty output gives +0 for sum(), 1 for prod(), and NaN for mean(), var(), and std() without reading the source, while min(), max(), minMax(), and the arg-reductions throw synchronously because they have no identity; an output with no elements returns without a dispatch or a WebAssembly call. The additive identity inside float sums is −0, so a sum of only −0 is −0, matching sequential IEEE addition. var() and std() run two passes on both backends, the mean first and then the sum of squared deviations from it with the same accumulator, which keeps their accuracy on large offsets and outliers where a single-pass Chan–Welford or shifted update loses digits; var = M2 / (n − ddof) for a finite ddof ≥ 0, including fractional values, gives NaN when n − ddof ≤ 0, and a complex variance sums the two component variances.
CPU reductions coalesce kept and reduced axes separately, so adjacent axes merge only within a class and only when contiguous, which preserves the logical C-order of the reduced elements, and call ndarrayf.reduce with per-call descriptors. Floats and complex components accumulate in f64 in one canonical order: each output's reduced elements split into consecutive blocks of 64 in logical C-order, each block sums sequentially, and block sums combine in a binary-counter cascade that merges equal levels as they finish and folds what remains from the newest entry down. prod() uses the same order with multiplication. Every loop strategy produces exactly this order, so results depend only on the logical values, the reduced axes, and the options, and op(view) is bitwise equal to op(contiguous(view)) for every op and dtype. To keep memory access sequential, a reduction advances a tile of outputs adjacent along the kept axis with the smallest stride, whatever its place in the shape, one reduced element at a time and pushes each output's finished block into its own cascade: 1,024 outputs when that kept axis has the smallest stride of all axes or nothing is reduced, 512 or 64 when a reduced axis has the smallest stride and fewer than 32 or at most 1,536 elements reach each output, where the tile's vertical additions vectorize and a sequential sum cannot, and one output at a time otherwise. Products always run one output at a time. Scratch is per call, and integer sums and ordering reductions need no fixed order but share the same walk.
GPU reductions accumulate floats in float-float (hi, lo) f32 states, renormalizing after every addition with TwoSum and FastTwoSum and setting lo to 0 once a result is no longer finite, and combine integers in 64-bit vec2<u32> states with carry. WGSL cannot mark arithmetic as exact, and shader compilers do rewrite (a + b) − a to b and s − (s − a) to a, as Mesa does, or reassociate (a − (s − b′)) + (b − b′) into (a + b) − s, as the Metal and Direct3D 12 paths behind Firefox do, so every float-float intermediate passes through an integer exclusive-or with a parameter word that is always 0 but unknown to the compiler, at a cost of under 2% on a discrete GPU. For K outputs of R reduced elements, a cooperative reduction runs one 256-invocation workgroup per output and chunk of min(R, max(32768, ⌈K·R / 512⌉)) elements, which keeps a large reduction to about max(K, 512) workgroups, whose invocations stride through the chunk, accumulate privately, and merge through a fixed shared-memory tree, while a thread reduction runs one invocation per output and chunk, looping over its elements. The planner, which depends only on shape, axes, dtype, and op, uses threads below R = 512 and when a kept axis has the smallest stride of all axes, K is at least 128, and K·R is at least 2²¹, with min(R, 256)-element chunks there because long-looping threads lose badly, and cooperative workgroups otherwise. Adjacent invocations follow the kept axis with the smallest stride wherever it sits in the shape, so transposed and permuted views read memory as coherently as C-order ones. These constants were calibrated on one discrete GPU, on inner-axis, outer-axis, whole-array, few-channel, short-axis, and permuted shapes. No divisor was best on every shape there, and WebGPU does not expose core counts, so 512 leans toward more workgroups in flight, on the untested expectation that larger GPUs need them. The innermost reduced axis steps by increment and other axes decompose an index by division only when it wraps, so coalesced axes pay no division per element. Reductions with more than one chunk write a [K, C] array of partial states to a scratch temporary and merge it in one further pass. Outputs run in batches, each dispatch covering [base, base + count) through a base parameter, so that every temporary of a batch (states, partials, pass-1 means, and narrowing targets) and the part of each output that it writes fit both maxStorageBufferBindingSize and maxBufferSize, in the largest batches that fit. Batches record in order in one command list and reuse their temporaries, because dispatches in one encoder are ordered, an output larger than one binding is written through a window per batch, and because the plan does not depend on limits, no batch changes an output's reduction tree and batched results are bitwise identical to unbatched ones. Only one output's temporaries exceeding those limits, or an input binding that does, raises the error naming both, which includes the staged copy of an aliased input, since for a broadcast view that copy can be far larger than the input's own extent. var() and std() dispatch pass 2 with the pass-1 states at the start of its own read-write temporary, forming each deviation as (x − m_hi) + (x_lo − m_lo), with x_lo nonzero only for integers above 2²⁴, and finish in a separate dispatch, as do fused outputs. Means divide float-float by a float-float n with a correction step because counts above 2²⁴ are inexact in f32 and WGSL / is accurate only to 2.5 ULP, integer means first convert the exact 64-bit sum to float-float, and std() applies WGSL sqrt, which is not correctly rounded. 8-bit and 16-bit outputs are written as u32 values to a scratch temporary and narrowed with the engine's gather, so every invocation owns whole words, and an input that shares a raw GPUBuffer with any output is staged first. Results use no atomics or subgroups and are bitwise identical across repeated runs and between self-submitted calls and calls recorded into a caller's encoder, though not across layouts or devices, since the reduction tree follows the layout's strategy and subnormals may flush. Reductions live in ./wgsl/compute/ndarray-reduce.wgsl and the affine maps of normalize() in ./wgsl/compute/ndarray-affine.wgsl, fragments that use the bindings and helpers of ./wgsl/compute/ndarray.wgsl and each compile appended to it, while gather and resize pipelines compile that file alone, so neither fragment's size slows the other pipelines' creation.
GPU sum(), prod(), mean(), var(), std(), varMean(), and stdMean() reject f64 and c128 inputs and outputs, with one exception: an integer sum() may write dtype: "f64", a storage-only output converted exactly from the 64-bit accumulator. min(), max(), minMax(), argmin(), and argmax() accept f64 because they compare integer keys and never do f64 arithmetic. min(), max(), minMax(), the arg-reductions, integer sum() and prod(), integer sums written as f32 or f64, and empty results are bit-identical on the CPU and GPU, and NaN bits are identical whenever both backends produce NaN. Whether a float sum() or prod() is NaN, however, can differ across backends where f32 overflow or underflow occurs, because GPU partial results are f32 and CPU partial results are f64: prod([1e30, 1e30, 0]) is 0 on the CPU and NaN on the GPU, prod([1e-30, 1e-30, inf]) is inf and NaN, and sum([3e38, 3e38, −inf]) is −inf and NaN. Grouping matters there too, as on the GPU (1e-30 × 1e-30) × inf is NaN while (inf × 1e-30) × 1e-30 is inf. A complex prod() takes its first factor directly rather than multiplying by an identity, so prod([(1, 0), (inf, 0)]) is NaN on both backends while prod([(inf, 0)]) is (inf, 0). Float results agree within a bound: for n reduced elements with S = Σ|x| and r the result, a GPU sum() lies within ulp(r) + 3n·2⁻⁴⁸·S + n·2⁻¹²⁵ of the CPU value, mean() within 4·ulp(r) + 3·2⁻⁴⁸·S + 2⁻¹²⁵, var() within 8·ulp(r) + T, and std() within 10·ulp(r) + √T, where T = (n / (n − ddof))·((3·2⁻⁴⁸·S)² + 2⁻¹²⁵), and complex values satisfy these per component with S over both components. 3·2⁻⁴⁸ covers one float-float addition per element or merge relative to S, n·2⁻¹²⁵ covers flushing of subnormal partial sums, the ulp(r) factors cover the final rounding, the corrected division, the rounding of each deviation and its square, and sqrt, and T covers the mean's error, which enters a variance squared. The bounds hold while GPU partial sums stay within the f32 range, because f64 CPU sums cannot overflow where f32 GPU sums can.
normalize() maps every element through x × scale + offset, (x − mean) / std, or statistics of the data. Explicit parameters are numbers, arrays of numbers varying along one axis, or ndarrays of the input's class, residency, and device that broadcast to the input's shape by NumPy rules and are read in place through their strides, flipped, broadcast, and unaligned views included, and numbers and ndarrays mix freely, as in { scale: 2, offset: column }. Numbers must be finite and a number std nonzero, while an ndarray std gets no zero rule; ndarray parameters must be real, the GPU rejects f64 ones, and axis applies only to number arrays, since ndarrays carry their own axes, so the keepDims statistics of one array apply directly to another, as in normalize(test, stdMean(train, { axes: 0, keepDims: true })). method: "minmax" and method: "zscore" compute the statistics instead, over axes (every axis by default) or over every axis except one axis, with nan, epsilon (0 by default), ddof for "zscore", and range ([0, 1] by default, with range[0] < range[1]) for "minmax", and map x to (x − lo) / z(hi − lo) × (r1 − r0) + r0, evaluated in that order and as (x − lo) / z(hi − lo) for the default range because a reciprocal scale loses up to all digits near outputs of 0, or to (x − mean) / z(std). The zero rule z(d) keeps NaN, maps |d| ≤ epsilon, which includes ±0 when epsilon is 0, to 1, and otherwise returns d, as scikit-learn's scalers do, so constant min-max slices map to r0. A z-score slice whose min equals its max (on the GPU after conversion to f32, where 32-bit integers that round to one f32 already give 0 through the mean) and whose std is not NaN is constant and maps to x − min, exactly ±0, because its mean and std can carry rounding noise that z cannot catch: the f64 cascade mean of three 0.1s is 0.10000000000000002 with a std of 1.4e-17, and the f32 mean of the i32 value 2²⁴ + 1 can round away from f32(2²⁴ + 1), so z-score also computes the fused minMax(). With nan: "propagate" any NaN makes its slice's statistics and outputs NaN, nan: "omit" skips NaN in the statistics and keeps NaN inputs in place, an all-NaN slice and n − ddof ≤ 0 give NaN outputs, integer outputs map NaN to 0, and nan is ignored for integer inputs. NaN inputs and NaN statistics or parameters give NaN on both backends, detected by bit tests on the GPU, which writes them as the canonical positive quiet NaN. Infinities are data: the CPU follows IEEE f64 arithmetic, so an ndarray std of 0 divides to ±infinity or NaN and a min-max spread that overflows to infinity maps finite values to r0 and infinite ones to NaN, while GPU results for infinite inputs, infinite or zero denominators, and an overflowed spread follow the implementation's f32 arithmetic, which is IEEE on the tested backends but not guaranteed by WGSL. An empty input returns an empty output without reductions, dispatches, or WebAssembly calls, and "percentile" and "robust" are reserved names that fail with a hint. Validation runs under the normalize name in a fixed order: liveness and complex rejection, form exclusivity, option applicability, values, ndarray parameters, and then the GPU's f64 rule.
Statistics and outputs stay in the input's residency, and the GPU path never maps or reads back a buffer: normalize() uses the shared engine's reductions, never the public methods, and records the reductions, any parameter staging or casts, and the affine map into one command list with one submission, none with a caller's encoder, and one scratch reset. The CPU computes minMax in the input dtype and converts it exactly to f64, computes stdMean in f64, converts other ndarray parameters to f64 temporaries, and calls ndarrayf.affine, whose descriptor passes up to four f64 parameter arrays as pointer, element count, element offset, and per-dimension element strides that coalesce with the input's byte strides. The Rust loop classifies each parameter's innermost run as fixed, loaded once per run, contiguous, indexed as a slice so that the loop vectorizes, or strided, forms the z-score and min-max terms in f64, and evaluates x × a + b and (x − a) / b as before, so explicit results are bit-identical to the previous axis-based kernel on both backends, except that the GPU now writes every NaN output as the canonical quiet NaN instead of whatever bits its arithmetic produced. CPU method results therefore depend only on the logical values, so normalize(view) is bitwise equal to normalize(contiguous(view)), and a z-score that does not trigger the zero rule equals stdMean(a, { axes, keepDims: true, dtype: "f64" }) followed by { mean, std } bitwise. The GPU computes f32 statistics, rejects f64 inputs and outputs for every form, and runs the affine entry point of the fragment ./wgsl/compute/ndarray-affine.wgsl, the only one with five bindings (src, dst, params, p0, and p1); only that fragment declares p0 and p1, so every other entry point keeps its three-binding layout. Its forms (AFFINE_FORM) are scale and offset, mean and std, min-max, and min-max with a range. Number parameters follow the 88-word parameter header as f32 values read through params, and arrays read through p0 and p1 at byte offsets with two 8-word stride sets that coalesce with the input's, because each binding the element loop reads cost about 6% as measured on one discrete GPU; parameters in one buffer, as statistics always are, share p0. Non-f32 ndarray parameters are cast to f32 temporaries, and parameters sharing a raw buffer with out are staged like the input. Min-max applies the zero rule inline from lo and hi, which 8-bit and 16-bit inputs read in their own dtype and 32-bit integer inputs read after one cast to f32, since their exact conversion per element costs more than the cast, while z-score writes [center, denominator] per slice with the fragment's three-binding zscore entry point from std, mean, min, and max in one scratch temporary and then runs the mean and std form, because reading four statistics per element costs up to a third more. range and epsilon round to f32 and must stay finite with f32(r0) < f32(r1), and the GPU compares |d| with f32(epsilon). The statistics temporary, the input, and the output must each fit one binding and otherwise fail with the error naming both limits before anything is recorded, while the reductions batch as usual. GPU results are bitwise identical across runs and between self-submitted and encoder-recorded calls but not across layouts, and a z-score that does not trigger the zero rule equals stdMean() with keepDims followed by { mean, std } bitwise. Against the CPU, a GPU z-score lies within (δm + |x − f32(x)| + ulp(x − m)) / s + |y|·δs / s + 4·ulp(y), where δm and δs are the mean() and std() bounds above for the slice and y is the output, and a min-max of f32 input, whose lo and hi are bit-identical, within (ulp(x − lo) + |q|·ulp(d)) / d·|w| + 4·ulp(q)·|w| + ulp(q·w) + 2·ulp(y), with d = hi − lo, q = (x − lo) / d, and w = r1 − r0, about 5 to 7 ULP of the output: the terms cover the rounded difference and spread, WGSL /, which is accurate to 2.5 ULP, q·w + r0, which may contract to a fused multiply-add, and the CPU's own rounding. Integer inputs above 2²⁴ add their f32 conversion error to x, lo, and hi, ranges that f32 cannot represent add |r0 − f32(r0)| + |q|·|w − f32(w)|, hi − lo can overflow f32 where the CPU's f64 cannot, constant z-score slices whose n-fold f32 sum overflows give NaN on the GPU, and integer outputs may differ by 1 only near rounding midpoints and saturation edges.
Each Compute owns one ScratchAllocator that its kernels and compute.ndarray share. Kernel and GPU ndarray parameters are written with queue.writeBuffer into aligned regions of parameter blocks (4 KiB first, doubling to 64 KiB, with larger parameter sets such as big resize tap tables in a block of their own), the GPU ndarray engine assembles them in one reused array of up to 1 MiB because writeBuffer copies its data synchronously, and a region is rewritten only after every command buffer that reads it has been submitted. Self-submitted calls share one block chain that rewinds after their own submit, while each caller-supplied encoder gets its own chain that is never reused and is released with the encoder by the garbage collector. Because an encoder's regions are never rewritten, a recorded call whose parameters (of at most 256 bytes) repeat that chain's previous region reads the same region. Temporaries such as reduction partials, scan block sums, radix and compaction prefixes, same-buffer staging arrays, resize pass intermediates, and kernel operand staging and write-back arrays come from size-class pools (powers of two up to 1 MiB, then multiples of one sixteenth of the next power of two, so large temporaries waste under 12.5%), are fully written and read within one call, and are reused by later calls in either mode because queue order serializes their use. A temporary that a caller's encoder used is never destroyed, only dropped, while idle unused temporaries and self-submitted parameter blocks are destroyed least recently used first whenever their total exceeds ComputeDescriptor.scratch.maxRetainedBytes (64 MiB by default; 0 is allowed). That option governs only idle GPU scratch memory; transient CPU resize intermediates follow ComputeDescriptor.ndarray.maxCPUIntermediateBytes instead. Every public kernel and GPU ndarray call that uses the allocator resets it exactly once at its outer boundary, including when it throws. Empty dot results are zero: owned outputs start zeroed, and a caller's out is cleared with clearBuffer, or through a cleared scratch temporary and a gather when it lacks COPY_DST, recorded in order with the call's other commands so that it lands after commands already recorded in a caller's encoder, empty reductions write their identity through a constant-padding gather that needs only storage usage, in the same order, and the scale and LU kernels accept encoder like every other kernel, with blocked LU encoding each 16-column panel's parameters, bind groups, and dispatches into one submission. The RGBA8 blitter writes each distinct parameter set (canvas size, output size, and flip) once into its own immutable uniform buffer, keeps the most recently used uniformCapacity sets with their bind groups, and so never rewrites parameters that a recorded blit may still read. GPU ndarray operations use one cached pipeline per entry point and override set and bind every array as raw 32-bit words. Dispatches use the shared workgroupsFolded() with 256-invocation workgroups. Each binding starts at the array's absolute start aligned down to minStorageBufferOffsetAlignment, with the difference added to element offsets; odd-length external wrappers whose 4-byte-rounded binding would leave the logical range, bindings above maxStorageBufferBindingSize, including resize parameter tables, and outputs above maxBufferSize fail with errors naming the device limits.
WasmGPU ndarrays support ten dtypes: the integer dtypes i8, u8, i16, u16, i32, and u32, the floating-point dtypes f32 and f64, and the complex dtypes c64 and c128, whose elements store interleaved little-endian [re, im] f32 or f64 components. DTypeInfo reports each dtype's kind, component typed-array constructor, component count and dtype, whole-element byte size, and WGSL element type (vec2<f32> for c64), so CPUndarray.data() and fromArray() use interleaved component arrays, and get() and set() read and write [re, im] tuples for complex dtypes, where a number sets a +0 imaginary part. WasmGPU chooses floating-point precision by execution domain rather than imposing one width globally. CPU ndarrays and WasmGPU-owned memory support f32, f64, c64, and c128, including Python interop, and external WebAssembly memory views support f32 and f64. GPUndarray<f64> and GPUndarray<c128> are valid storage and transfer data, as their bytes may be uploaded to a WebGPU buffer, copied, viewed, padded, and read back without loss, and cast to f32 or c64 on the GPU with the same rounding as on the CPU, while c64 is also the GPU's complex computation dtype. However, current WebGPU/WGSL has no concrete runtime f64 scalar type, so f64 and c128 GPU storage does not imply native f64 shader computation or rendering arithmetic. Where a shader-facing path requires f32 or c64, explicitly converting f64 or c128 values through compute.ndarray.cast() or the existing typed-array or ndarray construction behavior is intentional.
compute.ndarray.wrap() and GPUndarray.wrap() layer dtype, shape, strides, and layout offset over an existing internal or external StorageBuffer without copying or taking ownership. The complete backing extent, including custom positive or negative strides, must fit within the logical storage range after adding baseOffsetBytes. Element addresses compose the storage offset, ndarray base offset, layout offset, and indexed strides; bindingResource() binding offsets must also satisfy storage alignment. Transfers preserve this metadata, and the compute.ndarray operations and the kernels in ./typescript/compute/kernels.ts honor arbitrary layouts. Arrays created by GPUndarray.empty() own their allocated storage.
compute.kernels is a GPU-only primitives layer over GPUndarrays. Every method takes live GPUndarrays from its own device, returns owned arrays or the caller's out, rejects CPUndarrays with a compute.ndarray.toGPU() hint, and prefixes errors with compute.kernels.<op>:. Kernels dispatch on dtype without promotion: arithmetic, dot, matmul, and sum accept f32, u32, or c64 operands of one dtype, min and max accept f32 or u32, argmin, argmax, and the scale kernels accept f32, LU accepts f32 or c64, scan inputs, histogram and sort keys, and compaction flags accept u32, sort payloads and compacted values accept f32 or u32, and every other dtype, including i32 and c128, fails with a compute.ndarray.cast() hint, except that ordering kernels reject c64 with a hint to reduce real() or imag() views. An operand binds directly when it is non-empty, C-contiguous, starts at an absolute offset aligned to minStorageBufferOffsetAlignment, and fits the binding limits; otherwise the shared engine gathers it into a C-contiguous scratch temporary. add, sub, mul, and axpy broadcast with NumPy rules and read a broadcast, strided, flipped, or unaligned operand in place through the coalesced byte strides of ./wgsl/compute/elementwise-strided.wgsl (at most 8 dimensions after coalescing), keeping the contiguous shaders for dense operands that already have the output shape. matmul reads every operand through an element origin and row and column strides, staging only c64 operands whose element origin is not 8-byte aligned within their binding and, from 512³ multiply-adds, both operands when neither has a unit column stride; it promotes 1-D operands like NumPy, applies alpha and beta (a nonzero beta requires out), and does not batch. scaleExtract reads [count] and [count, components] sources natively when rows have a positive stride and components a unit stride.
Owned kernel outputs default to copySrc: true, copyDst: true, and a compute.kernels.<op> label, and a caller's out must match the dtype and shape exactly and be C-contiguous. An out that is unaligned or shares a raw GPUBuffer with an input bound read-only in the same dispatch is computed in a scratch temporary, preloaded from out when beta, clear: false, or compaction leaves part of it unwritten, and written back with an engine gather that needs only storage usage, which also makes forms such as axpy(x, y, a, { out: y }) work. Radix sorts run in place through out: keys or outKeys and outValues because only their first pass reads the input, luFactor() with overwrite: true factors a in place and stages and writes back a non-dense view, and the writable outputs of one dispatch may share a raw buffer but must not overlap. Complex kernels take their dtype from c64 operands, so complex shapes broadcast like any other shape, alpha and beta may be numbers or [re, im] tuples for c64 and only numbers otherwise, and dot does not conjugate. sum, min, max, argmin, and argmax are thin adapters over the shared engine's reductions, reading every layout in place, and dot sums its per-workgroup products through the same path, so u32 sum and dot wrap, f32 and c64 results are compensated, and min and max propagate NaN as the canonical quiet NaN. scanExclusive, histogram, compact, sort, and sortPairs take 1-D inputs; whole-array reductions return 0-d arrays; argmin and argmax return the 0-d u32 flat C-order index of the logical view, skip NaN, resolve ties to the lowest index, and return 0xFFFFFFFF for all-NaN input; sum and dot of empty inputs return zero while min, max, argmin, and argmax throw; and compact keeps its count on the GPU as a 0-d u32 array. LU pivots in ipiv are 0-based row indices in LAPACK getrf order.
Contributors changing compute code should check buffer usage flags, copy alignment, dispatch dimensions, shader bindings, readback slot reuse, scratch parameter-region reuse, and tests that assert kernel results.
The scaling service is implemented in ./typescript/scaling/service.ts with scale transform logic in ./typescript/scaling/transform.ts. It wraps each raw GPUBuffer source in a borrowed StorageBuffer, views the source as an f32 [count, componentCount] ndarray with the configured stride and offset, and uses the ndarray kernels to extract finite values, compact them, compute min and max values over the compacted finite prefix, build histograms when percentile clamping is requested, and read results back asynchronously.
The service reads scale source descriptors from data materials, pointclouds, glyphfields, nodelinks, and latticespaces. It caches in-flight and completed results using source buffer identity, revision, component selection, value mode, stride, offset, count, transform settings, and percentile settings.
Unlike compute.ndarray.normalize() with a statistics method, which reduces any axes of any real ndarray exactly in the input's residency, propagates or omits NaN, and never reads statistics back, the scaling service covers a whole buffer of f32 components, keeps only finite values, approximates percentiles with histograms, and reads its results back to the CPU for the scale transform.
Scale transforms pack values for WGSL in a fixed float layout. The current modes include linear, logarithmic, and symmetric logarithmic behavior, plus range or percentile clamping, gamma, and inversion.
Changing scaling behavior requires checking ./typescript/scaling/, data-source objects in ./typescript/graphics/material.ts and ./typescript/world/, WGSL scaling kernels under ./wgsl/compute/, legend behavior under ./typescript/overlay/, and examples that show colormap legends.
./typescript/effects/index.ts and ./typescript/effects/shadows.ts implement the small public effects facade and shadow configuration. There is deliberately no generic Effect interface or third-party render-pass lifecycle. ShadowSystem associates configuration with directional lights, does not own their lifetime, defaults to a 1024-pixel map, four active views, 3×3 PCF, and every-frame updates, and supports a hard-filter mode plus manual updates. Public getters return configuration copies; renderer dirty state and texture-layer assignment remain private, while needsUpdate() intentionally exposes manual-update status. Descriptors reject non-finite values, unsupported modes, invalid dimensions, and map/view counts beyond the active WebGPU device limits. No shadow texture or pass exists until an enabled directional light is also active in the rendered scene.
./typescript/core/shadows.ts is the renderer-side implementation. The current mapping is one DirectionalLight to one ShadowView; metadata and a depth texture array preserve a future path to multiple views without implementing cascades, spot shadows, or point-light cube faces. Camera-focused orthographic fitting projects all eight relevant camera-frustum corners into the directional-light basis, pads the XY bounds for stabilization, and snaps the fitted center to shadow texels. This includes camera depth in the fit for both perspective and orthographic cameras. Visible caster bounding spheres that intersect the fitted light-space XY footprint conservatively extend only its near/far range, allowing off-camera casters to shadow visible receivers without reducing XY texel density. A per-light manual volume can replace automatic fitting. The renderer stores light-index-to-layer metadata separately from the packed lighting uniform so the existing light ABI remains intact.
Shadow caster collection traverses all visible scene meshes with castShadow === true; it does not reuse camera-frustum or previous-frame occlusion-filtered draw lists. Geometry upload precedes morph-buffer selection so indexed and skinned morph casters have their ancillary GPU buffers on the first shadow frame. Compatible consecutive static casters sharing geometry use a renderer-shadow-owned instance buffer and one instanced depth draw; this buffer is isolated from main-render instance-buffer growth so encoded shadow commands cannot retain a subsequently destroyed buffer. Skin matrices are prepared once per skin per renderer frame and reused by every shadow view and the main pass. Caster pipelines apply bounded slope-aware raster depth bias. The first implementation renders opaque silhouettes and does not reproduce material alpha cutoff or semi-transparent transmission in the caster pass. Pointcloud, glyphfield, nodelink, splatfield, and latticespace objects do not cast or receive these shadows.
Standard material mesh receivers select a shadow/non-shadow pipeline variant from the per-object Mesh.receiveShadow flag. The renderer binds one global shadow texture-array/comparison-sampler/metadata group, and a named, assertion-guarded standard material visibility hook multiplies only each associated direct-light radiance by the comparison or PCF result. Hard shadows use a nearest comparison sampler and one lookup. PCF uses a linear comparison sampler with a 3×3 footprint plus receiver-plane per-sample depth compensation. Filter-only changes replace the sampler/bind group and metadata without recreating the depth array or regenerating a clean manual shadow layer. Shadow normal offset and angle-aware reference bias use the interpolated geometric normal before normal mapping, so material normal maps do not perturb shadow coordinates. Ambient illumination and emissive output remain outside shadow attenuation. Unlit material, data material, custom material, non-mesh object families, point lights, and spot lights do not receive shadows. Because material instances may be shared, receiver state is never stored on or written into standard material.
Changing rendering effects behavior requires checking ./typescript/core/, ./typescript/effects/, ./typescript/graphics/, ./wgsl/core/, ./wgsl/effects/, and ./wgsl/graphics/.
glTF loading and import are implemented under ./typescript/gltf/. The loader reads JSON .gltf files, binary .glb files, external buffers, embedded buffers, and image resources. URI resolution is implemented in ./typescript/gltf/uri.ts; GLB parsing is in ./typescript/gltf/glb.ts.
Root sources are fetched or read once, classified from the first four bytes, decoded with fatal UTF-8 JSON parsing when they are not GLB, and passed through the shared glTF compatibility gate. URL loads retain the final response URL as GltfDocument.resourceBaseUrl for WHATWG external-resource resolution, while an explicit LoadGltfOptions.resourceBaseUrl and in-memory sources are normalized as directories. Data URLs are parsed as RFC 2397 octets, including percent-decoded bytes, literal plus signs, media-type parameters, and percent-escaped base64.
Accessor decoding is split between TypeScript and Rust. ./typescript/gltf/accessors.ts reads accessor descriptors, buffer views, sparse data, component types, normalization rules, and typed-array output requirements. It compacts padded MAT2/MAT3/MAT4 column layouts, including interleaved and sparse values, into logical typed arrays while retaining tight aligned zero-copy access. Numeric conversion, generalized compaction/deinterleaving, and sparse patching call Rust functions in ./rust/src/accessors.rs.
./typescript/gltf/import.ts converts loaded asset data into scene resources. Current import code covers geometry, materials, textures, skins, animations, morph targets, node transforms, node visibility, animation pointers, scene selection, metadata, material variants, cameras, punctual lights including spot lights, texture transforms, XMP metadata, Gaussian splat primitives from KHR_gaussian_splatting into SplatField, and material extensions for clearcoat, specular, transmission, volume, diffuse transmission, dispersion, sheen, iridescence, anisotropy, index of refraction, and emissive strength. KHR_gaussian_splatting import keeps spherical harmonic degree 0 through degree 3 data as SplatField coefficients when complete degree data is present.
Import performs a pure required-extension preflight before constructing scene or runtime resources. extensionsRequired failures are strict except for explicitly declared approximation policies: required KHR_materials_pbrSpecularGlossiness assets may import through the existing diffuse/base-color and glossiness-to-roughness approximation with explicit warnings while remaining reported as partial. Optional deferred features may use usable core representations with explicit warnings. metadata.extensions.support reports the per-asset outcome using supported, partial, deferred, and unsupported; outcomes degrade monotonically as asset-specific fallback or loss is discovered, and the same assessment feeds required-extension enforcement.
The importer mutates scenes, meshes, splatfields, geometry, materials, textures, animations, skin instances, camera data, and light bindings. It reads loaded asset data, WebAssembly decoding helpers, material extension descriptors, texture resources, and transform state. Before changing glTF import, check ./tests/gltf.test.js, examples using glTF, renderer material paths, texture upload behavior, and animation sampling.
importGltf() is failure-atomic. A reverse-order ownership transaction registers every transform, resource reference, scene insertion, and light binding immediately after acquisition. Geometry and material reference entries are transferred to a mesh only after the mesh assumes them; the successful GltfImportResult adopts the remaining ledger as its idempotent destroy() implementation. If any synchronous import phase throws, rollback continues through all registered disposers while preserving the original error, removes only imported objects from a caller-owned target scene, and never truncates the global transform store or destroys the target scene itself. Texture destruction invalidates pending decode/upload generations so asynchronous completion after rollback cannot recreate a GPU resource.
Animation code is implemented in ./typescript/graphics/animation.ts. AnimationClip stores channels and samplers. It samples translation, rotation, scale, weight, and glTF animation-pointer channels. Transform sampling calls Rust functions in ./rust/src/anim.rs; morph weight sampling and animation-pointer setter sampling are currently handled in TypeScript.
AnimationPlayer advances time, handles looping and clamping, and samples a clip. It mutates target transforms, morph weights, imported node visibility, material factors, texture transforms, camera projection properties, and punctual-light properties when a clip contains those channels.
Skin stores joint transform indices and inverse bind matrices in WebAssembly memory. SkinInstance owns a GPU bone buffer and bind group, and exposes the mesh transform's live world-matrix pointer for joint-matrix evaluation. The renderer updates joint matrices through Rust before drawing skinned meshes. Shader variants under ./wgsl/graphics/ cover normal, skinned, and skinned8 paths.
Morph target runtime state is attached to meshes in ./typescript/world/mesh.ts. When morph weights change, the mesh updates CPU-side morphed positions, normals, and RGBA colors, clamps morphed colors to [0,1], updates GPU buffers, and recomputes bounds used by the renderer. glTF node skins are node-local; they are not inherited by child mesh nodes.
Animation changes should be checked against glTF import, mesh morph handling, skin buffer layout, shader variants, and renderer draw-list paths.
Python interop is implemented in ./typescript/python/index.ts and ./python/interop.py. Every WasmGPU instance owns a PythonInterop service bound to its existing compute device, queue, and readback resources. The TypeScript boundary accepts only Pyodide proxies or resolved Pyodide-like numeric buffers in the engine's ten supported dtypes, including NumPy complex64 and complex128 as c64 and c128, and requires C-contiguous layouts. Proxy buffers are acquired as bytes, so their dtype comes from the native-byte-order buffer format and itemsize rather than the typed data, while caller-supplied buffers without a format keep the dtype of their typed data. Ordinary JavaScript typed arrays use the existing compute APIs instead.
toCPU() performs one bulk copy from the Pyodide buffer into a caller-owned canonical CPUndarray. toGPU() directly uploads the Pyodide buffer into the storage resource owned by a canonical GPUndarray. Both results use the normal canonical destroy() semantics. copyInto() strictly matches dtype and shape, requires a C-contiguous canonical destination under the relaxed contiguity rule, writes at the destination's layout offset, and updates its existing CPU allocation or COPY_DST-capable GPU buffer without reallocating it. GPU destinations are written with one queue write when the write is 4-byte aligned and cannot pad into neighboring data; otherwise, as for sub-word views at unaligned offsets, the source is uploaded to a temporary staging array and merged through the canonical compute.ndarray.copyInto().
When PythonInterop obtains a buffer by calling getBuffer(), it releases that buffer exactly once on success or failure. A resolved PyBufferLike passed directly by the caller is borrowed and is never released by PythonInterop. Source offsets are validated and locally accommodated for WebGPU alignment without changing the storage-buffer subsystem's alignment contract.
The Python helper binds to one concrete WasmGPU instance. It normalizes ordinary NumPy inputs and non-contiguous views to C-contiguous storage according to the explicit never, if_needed, or always copy policy and NumPy casting policy. It returns the real JavaScript ndarray proxies, copies CPU results into independent NumPy arrays, and implements GPU conversion through the bound instance's compute.ndarray.toCPU() with destruction of only the temporary CPU ndarray. Python interop has no independent pointer, allocator, ndarray wrapper, or lifetime model.
WebAssembly driver initialization is implemented in ./typescript/wasm/driver.ts and exported through ./typescript/wasm/index.ts. It imports the generated loader from ./wasm/wasm.js, creates an eight-mebibyte (8,388,608 bytes) frame arena, and exposes grouped Rust functions for accessors, animation, bounds, culling, matrix math, mesh normals, ndarray indexing and transformations, quaternion math, transforms, and mathematics, which includes vec3, vec3f, vec3d, mat4, mat4f, mat4d, quat, quatf, and quatd namespaces.
WasmSlice, WasmHeapArena, frameArena, wasm, and the driver namespace are part of the WebAssembly driver. WasmSlice records pointer, length, dtype, allocation kind, and epoch. Frame arena and heap arena slices check epochs so callers do not reuse memory after a reset or destroyed arena, and freed slices reject later view or handle creation. WasmHeapArena allocates a WebAssembly heap block and provides bump allocation within that block. Explicit release remains retryable if its allocator call throws; garbage-collection finalization is only a fallback for abandoned heap slices.
WebAssembly interop with external modules lives in ./typescript/wasm/interop.ts. It wraps foreign WebAssembly.Instance or exports objects, resolves explicit memory and export descriptors, validates byte ranges and alignment, constructs real-dtype typed views or raw byte/DataView accessors over external linear memory, and provides small shared validation helpers for object-owned uploads from borrowed WasmMemoryView sources.
Rust memory allocation is implemented in ./rust/src/heap.rs through Rust's standard allocator. Exported byte, f32, f64, and u32 allocation/free pairs use checked layouts, as a successful allocation remains valid until its matching one-time free, after which its storage can be reused. WebAssembly memory pages do not shrink after growth. WasmHeapArena.destroy() releases its backing heap block, while the frame arena keeps one persistent backing block and reuses its contents through resets. WebAssembly's wasm32/u32 pointer width is an addressing concern independent of whether stored numeric values use f32 or f64.
The Rust driver separates portable slice and fixed-array computation from WebAssembly-only heap and frame-arena adapters where native testing requires it. The heap and frame-arena modules compile only for wasm32; their 32-bit pointer, memory-growth, alignment, capacity, and epoch contracts are tested against the compiled WebAssembly module rather than emulated with native pointers.
Each Rust ABI entry point that borrows WebAssembly memory creates an opaque driver-call scope inside a higher-ranked callback. Slice helpers consume a copy of that capability and return references tied to its generated lifetime, so the compiler prevents those references from escaping the driver call rather than relying on an unconstrained or fabricated static lifetime. Pointer-consuming and deallocation exports are unsafe extern "C" functions on the Rust side, so Rust callers must acknowledge their foreign-memory preconditions at each call site; this marker does not change their WebAssembly signatures, so JavaScript callers remain responsible for the same ABI contract. Fixed-width matrix, quaternion, and vector kernels stage their inputs in local arrays before creating mutable output slices, so output may exactly alias or partially overlap an input without violating Rust's reference-aliasing rules. Their copy exports use memmove semantics and support exact, forward, and backward overlap. Variable-length accessor, animation, bounds, culling, mesh, ndarray, and transform kernels require every pointer range to be valid, initialized, naturally aligned for its element type, contained within its live allocation, and large enough for the supplied count; mutable output ranges must be mutually disjoint and must not overlap input ranges unless an export explicitly documents otherwise. The ndarray transformation exports ndarray_copy, ndarray_pad, ndarray_affine, and ndarray_resize instead borrow the entire source and destination backing ranges as byte slices, take layout descriptors and tap tables as small u32, i32, and f64 arrays, compute every strided element offset with checked arithmetic and validate the complete extents before any access, read and write elements through little-endian byte conversions so element offsets need no alignment, and require disjoint source and destination ranges. They move elements of up to 16 bytes, cast into complex dtypes one component at a time, read one element of constant bytes for ndarray_pad, reject complex dtype codes in ndarray_affine, which also borrows up to four 8-byte-aligned f64 parameter arrays that must not overlap the destination and whose strided extents are validated like the source's, and reject complex-to-real pairs in ndarray_resize. ndarray_resize takes ranks 1 to 64 and a layout of the destination and source offsets, a level count from 1 to 3, per level the coalesced axis (strictly ascending, which checks the canonical order and distinctness), source extent, and tap count, and then the shape and both stride vectors; its i32 index array holds every level's row offsets, then every level's source indices with −1 for a constant tap under nearest and weighted methods, then four u32 words with the nearest constant's output-dtype bytes, which never pass through f64 so NaN payloads survive, and its f64 weight array holds every level's weights and the two-component weighted constant. It validates the levels, offsets, indices, and finite nonzero weights with checked lengths, evaluates weighted levels with one evaluator monomorphized per level count, resizes complex dtypes one component at a time, writes nearest constant taps from the trailing bytes, including the imaginary bytes of real-to-complex outputs, and gathers nearest samples of equal dtypes as raw bytes. They check null, misaligned, and destination-overlapping descriptor arrays and overlapping source and destination ranges themselves, and they report every validation failure as a nonzero status code that TypeScript converts into a descriptive error.
Data movement commonly follows these paths:
- TypeScript typed arrays are copied into WebAssembly memory for Rust processing.
- Rust writes results into WebAssembly memory views read by TypeScript.
- TypeScript writes WebAssembly-backed views or JavaScript typed arrays into GPU buffers through WebGPU queue writes.
- GPU readbacks copy GPU buffer ranges into staging buffers, map them asynchronously, and return JavaScript typed arrays.
- Textures upload through browser image and bitmap APIs before becoming WebGPU texture views.
Changing memory movement requires checking alignment, buffer usage flags, frame arena epochs, heap arena ownership, typed-array lifetimes, and GPU resource destruction.
WGSL shaders live under ./wgsl/. They are imported as text into TypeScript and bundled by ./esbuild.config.js, whose WGSL plugin removes comments and extra whitespace.
Shader directories currently map to architecture areas:
./wgsl/core/: SMAA, mesh picking shaders, mesh occlusion capture, and occlusion hierarchy reduction../wgsl/effects/: renderer-integrated shadow caster variants and the StandardMaterial receiver declarations and sampling function../wgsl/graphics/: mesh material shaders, transmission shaders, data material shaders, custom material defaults, mipmap generation, and skinned or instanced variants../wgsl/world/: pointcloud, glyphfield, nodelink, splatfield, latticespace, picking, private splatfield and latticespace sorting, and object-family occlusion capture shaders../wgsl/compute/: scan, histogram, compact, radix sort, scaling, blitting, contiguous & strided vector & matrix arithmetic, LU solving & factoring, and ndarray strided gathering, casting, padding, normalization, resampling, & reduction kernels.
WGSL changes must stay in sync with bind group layouts, vertex buffer layouts, uniform packing, material descriptors, object uniform layouts, and compute pipeline resource bindings in TypeScript.
Per-frame work spans TypeScript, WebAssembly, and WebGPU:
WasmGPU.run()resets the frame arena and calls the frame callback.Renderer.render()updates canvas-sized resources, camera matrices, transforms, active shadow views and metadata, lighting uniforms, unfiltered draw lists, render-only culling stats, optional previous-frame occlusion filtering of opaque lists, GPU uploads, shadow and main render passes, optional post-processing, timing readback, and optional asynchronous occlusion hierarchy capture.- Transform propagation runs in Rust using WebAssembly memory.
- Culling and bounds helpers run in Rust when enabled by renderer paths. Current Rust culling work includes frustum culling and conservative previous-frame hierarchical-Z occlusion tests over world-space spheres.
- Animation sampling mutates transforms and morph weights before rendering when the application updates players.
- Overlay updates are dirty-driven DOM work. Mutable built-in layers self-invalidate, automatic work is coalesced, and disabled registered layers are skipped without detach/recreate churn.
The code contains several performance-sensitive patterns: reusable draw-list arrays, draw item pools, packed dynamic-uniform staging, static instance-run reuse, fixed-head transparent merging, pipeline and shader caches, bind group caches, culling scratch buffers, frame arena staging memory, specialized radix-sort scratch buffers, the shared compute scratch allocator, and readback ring slots. Splatfield and latticespace sorting use separate renderer-private four-elements-per-invocation radix kernels and persistent per-object GPU state, and they cache their folded dispatch counts in that keyed sort state and in each scan level rather than recomputing them inside the 32 bit passes; external splat GPU buffers conservatively sort every frame because mutations are not observable. The occlusion path uses a bounded ring of hierarchy readback slots, suppresses equivalent in-flight captures, keeps the newest decoded hierarchy in persistent WebAssembly memory, and intentionally becomes a no-op when no safe previous-frame hierarchy is ready. Changes in these areas should avoid new per-object or per-frame allocations unless the allocation is bounded and measured.
These invariants are visible in the current code:
- WebAssembly must be initialized before code calls Rust-backed functions or reads
wasmexports. - Frame arena memory is valid only for the current frame arena epoch.
WasmHeapArenaslices are invalid after the arena is reset or destroyed.- Python imports return canonical ndarrays, and acquired Pyodide buffers are released exactly once and directly supplied buffers remain borrowed.
- Destroyed
CPUndarrayobjects reject WebAssembly pointer and data access, and repeated destruction is harmless. - ndarray views borrow their root array's storage, never free it, and become invalid when that root is destroyed.
- Materialized ndarray operations return owned C-contiguous arrays in the input's residency unless a GPU
outdestination is supplied, and nothing transfers between residencies implicitly. - ndarray layout operations and casts, including complex ones, are bit-exact and identical on the CPU and GPU, casts out of complex dtypes are rejected, and GPU ndarray operations reject
f64andc128arithmetic andf64↔integer andc128↔integer casts. - Resize tap tables contain no zero-weight or duplicate taps, single-tap rows reproduce samples without arithmetic, every output sums its taps in the canonical order with the highest resized axis innermost, and CPU
resize()andresample()results do not depend on how the planner groups the axes into passes. - CPU ndarray reductions depend only on the logical values, the reduced axes, and the options, so
op(view)is bitwise equal toop(contiguous(view)), and every CPU loop strategy produces the one canonical order of 64-element blocks and a binary-counter cascade. - GPU ndarray reductions use no atomics or subgroups, so they are bitwise identical across repeated runs and between self-submitted calls and calls recorded into a caller's encoder, and their plan does not depend on device limits, so they are bitwise identical however many batches their outputs need.
- ndarray
min(),max(),minMax(),argmin(),argmax(), integersum()andprod(), integer sums written asf32orf64, and empty results are bit-identical on the CPU and GPU, NaN bits are identical whenever both backends produce NaN, and floatsum(),mean(),var(), andstd()agree within the documented bound while GPU partial results stay within thef32range. compute.ndarray.normalize()never reads statistics back or transfers residency, computes its statistics through the shared engine's reductions (never the public methods) within its single scratch reset, and with amethodon the CPU depends only on the logical values, sonormalize(view)is bitwise equal tonormalize(contiguous(view)).- Only
./wgsl/compute/ndarray-affine.wgsldeclares the parameter bindingsp0andp1, and only itsaffineentry point reaches them, sogather,resize,zscore, and the reduction entry points keep the three-binding layout. - Every NaN an ndarray reduction writes is the canonical positive quiet NaN, in both components of a complex result, and GPU reductions detect NaN and infinities only with bit tests.
- Integer ndarray sums are exact in 64 bits before their one conversion to the output dtype, and integer ndarray products always wrap modulo 2³².
- Compute scratch parameter regions are rewritten only after every command buffer that reads them has been submitted, so a caller's encoder keeps its own regions until the garbage collector releases it.
- Compute scratch temporaries never carry contents across calls and are never returned to callers; anything consumed by a later call is an owned buffer.
- Compute scratch buffers, the ndarray empty binding buffer, and the RGBA8 blitter's parameter buffers, all of which an unsubmitted caller encoder may reference, are never destroyed by
Compute.destroy(), and blitter parameter buffers are never rewritten. - The compute scratch allocator resets exactly once at the outermost boundary of every public kernel and GPU ndarray call that uses it, including when the call throws, and kernels never call public
compute.ndarraymethods internally, using only the shared engine, which never resets it. - Kernels return owned arrays or the caller's
out, never scratch temporaries, and never cast or transfer their inputs implicitly. - A kernel never binds the same raw buffer read-only and writable in one dispatch; an aliased
outgoes through a scratch temporary and a write-back. workgroups1D,workgroups2D, andworkgroups3Dnever fold, and theirComputemethods and dispatch helpers reject counts abovemaxComputeWorkgroupsPerDimensionbefore encoding, whileworkgroupsFoldedanddispatchFoldedfold one-, two-, and three-dimensional grids into at most three dimensions, so kernels dispatched with them rebuild their linear workgroup index fromworkgroup_idandnum_workgroupsand tolerate over-dispatched workgroups.- Built-in compute kernels accept only folded grids with a z count of 1, and GPU ndarray operations and the private splatfield and latticespace sorts fold 256 or 1024 elements per workgroup into grids that always have a z count of 1, which is why their WGSL rebuilds the linear workgroup index from two terms.
- Built-in compute kernels and the private splatfield and latticespace sorts reject bindings above
maxStorageBufferBindingSizeand buffers abovemaxBufferSizewith errors naming those limits before allocating or encoding. - Disposed animation clips, skins, and skin instances reject operations that would consume their released WebAssembly allocations.
- Mesh shadow flags default to true but have no render effect until a directional light is explicitly enabled through
effects.shadows, ascastShadowcontrols independent depth-pass membership andreceiveShadowcontrols only standard material receiver variants. - Shadow visibility attenuates direct light only. Ambient and emissive terms remain visible, and configured lights are referenced rather than owned by the shadow system.
- Shadow caster visibility is independent of main-camera frustum and previous-frame occlusion results.
- Rust heap allocations remain valid until their matching one-time free; freed storage is reusable, but WebAssembly memory pages do not shrink.
- Transform and renderer culling capacity growth releases superseded WebAssembly blocks only after replacement allocation succeeds; renderer destruction releases its final culling blocks.
- Transform indices map TypeScript
Transformobjects to WebAssembly transform slots. - Disposed transforms throw on later use.
- Transform parenting rejects cycles.
- The renderer calls transform updates before reading world matrices for draw lists.
- Packed model-uniform offsets satisfy the device's dynamic-uniform alignment, and each submitted transform maps to one model/normal record for the frame.
- Transparent object-family lists use the same total-order comparator as the global fixed-head merge, including deterministic cross-family ties.
- Cached opaque instance runs are reused only while exact membership, geometry, instance-buffer generation, and world matrices remain unchanged.
- Splatfield and transparent 3D latticespace sorted indices are reused only while their conservative sort revision, count, camera matrix, and object transform inputs remain unchanged, and externally mutable splat GPU buffers always invalidate reuse.
- Render-only previous-frame occlusion culling reuses hierarchy data only when viewport size, hierarchy layout, camera type, view-projection matrix, and occluder signature still match; otherwise it is skipped for that frame.
- Render-only occlusion filtering never runs for picking or warmup.
- Geometry and materials use reference counts, and meshes release references on destruction.
- Imported glTF node visibility has local and effective state; effective visibility fans out to attached meshes and lights, while cameras remain importable and unaffected by visibility.
- Scene lighting uniform data currently uses at most eight non-ambient lights.
- WebGPU buffer readback requires buffers with copy-source usage.
- Buffer writes are padded to four-byte alignment where needed.
- Opaque occluder capture uses only coverage-safe subsets of meshes, pointclouds, glyphfields, nodelinks, and latticespaces; splatfields are currently excluded from occlusion capture and render-only occlusion filtering.
- Picking attributes for pointclouds, glyphfields, nodelinks, splatfields, and latticespaces depend on retained CPU-side records.
- Pointcloud, glyphfield, nodelink, splatfield, and latticespace external GPU buffers are borrowed by default and are destroyed only when their ownership options request it.
- Shader uniform layouts, TypeScript packing code, and WGSL structs must match.
Architectural role:
- Implements the Public API area in the diagram.
- Creates runtime objects and exposes factory helpers.
- Re-exports types and classes from the rest of
./typescript/.
Important files:
./typescript/index.ts: main module export surface. Check this file before adding or renaming public classes, descriptors, or helper functions../typescript/index.iife.ts: browser global entry point for IIFE bundles. It imports the public API and attaches it for script-tag examples and bundles../typescript/core/engine.ts:WasmGPUruntime class, factory methods, frame loop, renderer delegation, picking wrappers, interop accessors, and destruction.
Common interactions:
./typescript/index.tsimports from every public subsystem../typescript/core/engine.tscreatesRenderer,Compute,ScaleService, scene objects, cameras, controls, overlays, annotations, glTF helpers, and animation objects.- Examples under
./examples/use the exported API directly.
Architectural role:
- Implements the WebGPU Engine and most Browser Resources areas in the diagram.
- Reads scene and graphics data.
- Writes WebGPU commands, GPU buffers, caches, timing data, and pick results.
Important files:
./typescript/core/renderer.ts: publicRendererclass and orchestrator. It creates the WebGPU device and context, configures limits, owns renderer state, coordinates the helper modules below, preserves the public renderer API surface, and keeps render, warmup, resize, pick wrapper, and destroy sequencing in one place../typescript/core/resources.ts: renderer-global buffers, fallback textures, staging views, object IDs, and uniform writes../typescript/core/timing.ts: GPU timestamp-query setup and readback../typescript/core/postprocessing.ts: current SMAA resource creation, resize, and execution../typescript/core/transmission.ts: transmission target lifecycle and transmissive draw detection../typescript/core/shadows.ts: lazy shadow resource lifecycle, directional fitting and stabilization, metadata packing, caster collection, caster pipelines, and depth-pass encoding../typescript/core/drawlists.ts: drawitem pools, drawlist building, culling-capacity helpers, and transparent merged execution../typescript/core/materials.ts: material pipeline, shader-module, bind-group, blend, and cull helpers../typescript/core/objects.ts: mesh, pointcloud, glyphfield, nodelink, splatfield, and latticespace helpers, including renderer-private sort state../typescript/core/picking.ts: pick target resources, pick queries, pick draw execution, and pick pipelines../typescript/core/occlusion.ts: render-only previous-frame occlusion resource lifecycle, hierarchy validity checks, conservative opaque filtering, capture/readback, and occlusion pipelines../typescript/core/transform.ts: global transform store andTransformclass. It stores transform data in WebAssembly memory and calls Rust transform functions../typescript/core/stats.ts: frame and GPU timing counters plus the nested public culling stats display used by the runtime../typescript/utils/index.ts: shared internal helpers used across subsystems.
Contributors should inspect ./typescript/core/renderer.ts together with the object class, material class, and shader file for any feature that changes draw behavior. Renderer changes often affect tests, examples, and shader layouts.
Architectural role:
- Implements the Object & Data Model area in the diagram.
- Supplies renderable and pickable objects to the renderer.
- Contains scene composition, renderable object types, bounds helpers, cameras, controls, lights, picking types, and selection helpers.
Important files:
./typescript/world/scene.ts: scene object arrays, add/remove helpers, light packing, bounds collection, and traversal../typescript/world/mesh.ts: mesh lifetime, transform ownership, material and geometry references, scene owner references, skin attachment, morph runtime state, and per-object shadow caster/receiver flags../typescript/world/pointcloud.ts: point data, GPU-buffer upload, CPU-array upload, WASM-memory upload, scale source metadata, bounds, and picking records../typescript/world/glyphfield.ts: glyph instance data, geometry modes, GPU-buffer upload, CPU-array upload, WASM-memory upload, scale source metadata, bounds, and picking records../typescript/world/nodelink.ts: node and edge data, rendering modes, GPU-buffer upload, CPU-array upload, WASM-memory upload, scale source metadata, bounds, and picking records../typescript/world/splatfield.ts: Gaussian splat data, direct colors, spherical harmonic coefficient data, GPU-buffer upload, CPU-array upload, WASM-memory upload, bounds, color-space handling, and picking records../typescript/world/latticespace.ts: regular 2D/3D cell data, X-fastest indexing, masks, GPU-buffer upload, CPU-array upload, WASM-memory upload, analytic bounds, scale and colormap state, and picking records../typescript/world/camera.ts: base, perspective, and orthographic cameras../typescript/world/controls.ts: navigation controls, orbit controls, trackball controls, fly controls, pointer and keyboard input, damping, pointer lock, and scene fitting../typescript/world/light.ts: ambient, directional, point, and spot lights plus glTF light transform binding helpers../typescript/world/picking.ts: pick result types, region query types, andSelectionStore../typescript/world/bounds.ts: bounds helpers.
Common interactions:
./typescript/core/renderer.tsreads visible world objects and their GPU resources../typescript/gltf/import.tscreates and mutates world objects from assets../typescript/overlay/annotation/creates marker objects and reads picking results../typescript/scaling/reads scale source descriptors from pointclouds, glyphfields, nodelinks, latticespaces, and data materials.
Architectural role:
- Implements mesh data, materials, textures, color maps, animation, skinning, and morph target support.
- Supplies GPU resources and uniform data to the renderer.
- Reads WebAssembly helpers for bounds, normals, animation, and math.
Important files:
./typescript/graphics/geometry.ts: vertex data, index data, RGBA color channels, WASM-memory upload, tangent generation, normal generation, bounds, morph target arrays, GPU buffers, reference counting, and primitive geometry factories../typescript/graphics/material.ts: material base class, unlit material, standard material, data material, custom material, layout planning, uniform packing, texture references, bind groups, and reference counting../wgsl/graphics/standard*.wgsl: canonical standard material mesh, instanced, skinned-4, skinned-8, and transmission shader bodies, whilematerial.tsperforms narrow declaration/sample-path specialization from these sources../typescript/graphics/texture.ts: texture descriptors, image loading, strict decode policy, GPU texture upload, mipmap generation, sampler setup, fallback behavior, and destruction../typescript/graphics/colormap.ts: built-in and caller-provided color stops, CPU lookup tables, optional external GPU texture views, GPU resource caching, and CPU sampling helpers../typescript/graphics/animation.ts: animation clips, players, skins, skin instances, transform sampling, morph weight sampling, and bone buffer updates.
Common interactions:
./typescript/core/renderer.tsreads graphics data and binds buffers, textures, samplers, and material uniforms../typescript/gltf/import.tscreates geometry, materials, textures, animations, skins, and morph data../wgsl/graphics/must match material and geometry layouts.- Tests under
./tests/material.test.js,./tests/geometry.test.js,./tests/mesh.test.js,./tests/renderer.test.js, and./tests/gltf.test.jscover important behavior.
Architectural role:
- Implements the Compute subsystem, N-dimensional array model, readback ring, scratch allocator, and Scaling service areas in the diagram.
- Stores device and queue references for compute work.
- Creates compute pipelines and buffer wrapper objects; returned wrappers own or reference their WebGPU handles.
- Supplies data scaling for visual objects and data materials.
Important files:
./typescript/compute/index.ts:Computefacade, buffer creation, pipeline creation, dispatch helpers, readback rings, thecompute.ndarraynamespace, the GPU ndarray engine it shares with the kernels, and blitter creation../typescript/compute/buffer.ts: storage and uniform buffer wrappers../typescript/compute/pipeline.ts: compute pipeline wrapper and bind group creation../typescript/wgsl/index.ts: public WebGPU interop barrel export../typescript/wgsl/interop.ts: shared explicit bind group layout helpers, resource types, normalization, and validation../typescript/compute/dispatch.ts: compute pass encoding../typescript/compute/workgroups.ts: workgroup normalization, size, count, and folding helpers../typescript/compute/kernels.ts: built-in compute kernels that stage, bind, and write back operands through the shared GPU ndarray engine, with whole-array reductions anddotpartials as adapters over its reductions../typescript/compute/readback.ts: asynchronous readback ring../typescript/compute/scratch.ts: shared per-Computescratch allocator for parameter regions and pooled temporaries../typescript/compute/ndarray.ts: CPU and GPU ndarray classes, dtype handling, shape and stride validation, deterministic WebAssembly-backed CPU allocation lifetime, borrowed views, theNdarrayNamespacebehindcompute.ndarray, transfers, and the WebAssembly and WebGPU transformation and reduction engines, the latter shared byComputebetweencompute.ndarrayandcompute.kernels../typescript/compute/blit.ts: RGBA8 compute output blitting../typescript/scaling/service.ts: cached statistics requests over GPU data../typescript/scaling/transform.ts: scale transform packing and CPU-side helpers../typescript/scaling/types.ts: scaling descriptors and enums../typescript/scaling/index.ts: scaling exports.
Common interactions:
./typescript/world/pointcloud.ts,./typescript/world/glyphfield.ts,./typescript/world/nodelink.ts,./typescript/world/latticespace.ts, and./typescript/graphics/material.tsexpose scale sources../typescript/overlay/legendLayer.tsreads scaling and colormap state for legends.- WGSL kernels under
./wgsl/compute/implement GPU work, including thegatherandresizeentry points of./wgsl/compute/ndarray.wgsl, thereduce,reduceShared, andreducePartialsentry points of its fragment./wgsl/compute/ndarray-reduce.wgsl, and theaffineandzscoreentry points of its fragment./wgsl/compute/ndarray-affine.wgsl, used bycompute.ndarrayand by kernel staging, write-back, and reductions. - Caller-authored compute WGSL may use automatic pipeline layouts or explicit layouts and borrowed resources from
webgpuInterop, but execution remains owned byComputeandComputePipeline.
Architectural role:
- Implements the asset loading, glTF accessor, and importer areas in the diagram.
- Converts asset data into scene, graphics, splatfield, animation, camera, and light objects.
- Uses Rust for hot accessor conversion paths.
Important files:
./typescript/gltf/index.ts: public glTF exports../typescript/gltf/types.ts: loaded asset, descriptor, accessor, and extension types../typescript/gltf/loader.ts: asset loading, buffer loading, image loading, and resource resolution../typescript/gltf/glb.ts: binary GLB parsing../typescript/gltf/uri.ts: URI handling../typescript/gltf/accessors.ts: accessor decoding, padded matrix compaction, sparse handling, and conversion../typescript/gltf/import.ts: conversion from loaded asset data to scene resources, transfer-function-aware texture caching, and extension compatibility assessments.
Common interactions:
./typescript/gltf/import.tscreates objects from./typescript/world/and./typescript/graphics/../typescript/gltf/accessors.tscalls Rust accessors through./typescript/wasm/index.ts.KHR_gaussian_splattingmesh primitives import intoSplatFieldscene objects for the supported Gaussian splat subset.- Unsupported optional
KHR_gaussian_splattingprimitive features are skipped with warnings rather than converted to fallback pointclouds. - glTF animation pointers compile to existing transform and morph paths when possible and to JavaScript setters for imported nodes, materials, cameras, and lights.
- Material extension import must match
StandardMaterialfields and renderer shader support. Valid combinations within WebGPU's guaranteed baseline limits of 16 sampled textures and 16 samplers per shader stage report"supported", and over-baseline combinations report"partial". - glTF changes should be checked against
./tests/gltf.test.jsand./examples/gltf.html.
Architectural role:
- Implements overlay framework, annotation store, annotation toolkit, and DOM layers in the diagram.
- Reads camera state, viewport data, scene objects, controls events, picking results, scale state, and colormaps.
- Mutates DOM nodes and annotation records.
Important files:
./typescript/overlay/index.ts: overlay exports../typescript/overlay/types.ts: layer, anchor, legend, and overlay descriptors../typescript/overlay/system.ts: overlay root, ID lookup and enabled-state registry, dirty tracking, resize and scroll observers, controls binding, and coalesced update loop../typescript/overlay/projection.ts: world-to-screen projection helpers../typescript/overlay/pool.ts: DOM element pooling../typescript/overlay/axisTriadLayer.ts: axis triad DOM layer../typescript/overlay/gridLayer.ts: grid DOM layer../typescript/overlay/legendLayer.ts: legend DOM layer for explicit and data-driven sources../typescript/overlay/annotation/store.ts: annotation records and revisions../typescript/overlay/annotation/toolkit.ts: high-level annotation operations and pointer interaction../typescript/overlay/annotation/markerRenderer.ts: marker scene objects../typescript/overlay/annotation/labelLayer.ts: annotation label DOM layer../typescript/overlay/annotation/units.ts: units and formatting helpers.
Common interactions:
- Annotation picking calls runtime picking methods from
./typescript/core/engine.ts. - Marker rendering mutates scenes under
./typescript/world/scene.ts. - Legends read data from graphics and world objects.
- Controls under
./typescript/world/controls.tsdrive dirty updates during camera interaction.
Architectural role:
- Implements the Python/Pyodide, external WebAssembly, and WebGPU interop toolkits.
- Bridges C-contiguous Pyodide buffers to canonical CPU and GPU ndarrays, separately wraps foreign WebAssembly memory, and adapts caller-defined WebGPU layouts and borrowed resources for the existing compute and rendering systems.
- Interop layers describe or adapt foreign data and resources, whereas the canonical WasmGPU subsystems retain responsibility for allocation, ownership, scheduling, pipeline integration, rendering, compute dispatch, and lifetime.
Important files:
./typescript/python/index.ts: instance-bound validation and transfer API producing and updating canonical ndarrays../python/interop.py: thin NumPy normalization and canonical ndarray conversion helper../typescript/wasm/index.ts: exports for the WebAssembly driver and the WebAssembly interop../typescript/wasm/interop.ts: external WebAssembly module wrappers, descriptor resolution, typed memory views, raw byte reads, UTF-8/DataView helpers, and shared external memory validation/count helpers used by geometry, pointcloud, glyphfield, nodelink, and splatfield upload paths../typescript/wgsl/index.ts: exports the WebGPU interop../typescript/wgsl/interop.ts: WebGPU buffer, sampler, texture-view, bind-group resource, and layout vocabulary plus normalization, validation, and common binding-layout helpers.
Common interactions:
- Python interop uses the existing compute device, queue, readback resources,
compute.ndarraynamespace, and canonicalCPUndarrayandGPUndarrayimplementations rather than maintaining a separate ndarray or lifetime model. ./typescript/compute/ndarray.tsuses WebAssembly memory for CPU ndarray storage../typescript/core/transform.ts,./typescript/graphics/geometry.ts,./typescript/graphics/animation.ts, and./typescript/gltf/accessors.tscall Rust helpers through./typescript/wasm/index.ts.- External WebAssembly views can feed existing object and buffer upload paths without transferring ownership of the foreign memory.
- Compute pipelines consume WebGPU interop layouts and resources for caller-authored WGSL while retaining responsibility for pipeline creation, bind groups, and dispatch.
CustomMaterialconsumes WebGPU interop layouts and borrowed resources for caller-authored rendering WGSL while the renderer retains responsibility for pipeline integration and render passes.- WebGPU interop accepts native descriptor shapes and raw
GPUBindGroupLayoutEntryvalues for less-common binding kinds, and it does not own a device or queue, reflect or rewrite WGSL, pack arbitrary caller structs, or introduce a separate rendering or compute engine. ./scripts/build-rust-wasm.jscreates generated WebAssembly files consumed by./typescript/wasm/index.ts.
Architectural role:
- Implements the WebAssembly Driver area in the diagram.
./typescript/wasm/driver.tsis the TypeScript layer over the generated WebAssembly output of the Rust.- Provides exported functions for allocation, frame scratch memory, transforms, math, bounds, culling, normals, accessors, animation, and ndarray layout and transformations.
- Mutates WebAssembly memory owned by the current WebAssembly instance.
Important files:
./typescript/wasm/driver.ts: internal "glue" between the TypeScript and Rust sides of the overall system,WasmSlice,WasmHeapArena, frame arena helpers, loader integration, and grouped Rust function wrappers../rust/Cargo.toml: Rust crate metadata and release profile../rust/src/lib.rs: module wiring and exported entry points../rust/src/heap.rs: checked heap allocation and reclamation through Rust's global allocator../rust/src/frame_arena.rs: resettable per-frame bump allocator../rust/src/transform.rs: transform composition and world propagation../rust/src/mat4.rs,./rust/src/vec3.rs, and./rust/src/quat.rs: math helpers../rust/src/ndarray.rs: shape, stride, and offset helpers plus strided gathering with dtype conversion, padding, affine normalization, multi-axis resizing and resampling, and axis reductions in one canonical order../rust/src/mesh.rs: normal generation../rust/src/accessors.rs: glTF accessor conversion, generalized padded-layout compaction/deinterleaving, sparse patching, and numeric conversion../rust/src/anim.rs: animation sampling and joint matrix generation../rust/src/bounds.rs: geometry, pointcloud, and glyph bounds helpers../rust/src/cull.rs: frustum plane extraction, sphere culling, and conservative hierarchical-Z occlusion culling over packed world-space spheres../rust/src/shared.rsand./rust/src/utils.rs: shared Rust helpers../rust/src/tests/: native Rust conformance tests for portable algorithms and edge cases.
Common interactions:
- TypeScript calls Rust through generated bindings from
./wasm/wasm.js. ./scripts/build-rust-wasm.jscompiles the Rust crate and emits generated files under./wasm/.- Native tests exercise portable Rust computation without converting native pointers to the WebAssembly ABI's 32-bit offsets.
./tests/wasm.test.jsvalidates the compiled function ABI, generated bridge and declarations, heap allocations, frame-arena behavior, and representative pointer-based calls.- Changes to Rust exports must be reflected in TypeScript wrappers and tests.
Architectural role:
- Implements renderer, picking, material, world-object, compute, scaling, blit, and post-processing GPU programs.
- Must match TypeScript bind group layouts, buffer layouts, vertex layouts, and uniform packing. Mesh material variants carry a four-component vertex color stream through unlit, standard, transmission, instanced, and skinned paths before alpha testing.
Important directories:
./wgsl/core/: core render support shaders such as SMAA, mesh picking, mesh occlusion capture, and occlusion hierarchy reduction../wgsl/effects/: directional shadow depth shaders for static, instanced, skinned-4, and skinned-8 meshes plus the injected standard material receiver sampling path../wgsl/graphics/: material shaders, standard and transmission shader variants, data material shaders, mipmap generation, and custom material defaults../wgsl/world/: pointcloud, glyphfield, nodelink, splatfield, latticespace, picking, private object-family sorting, and occlusion capture shaders../wgsl/compute/: compute kernels for scans, histograms, compaction, radix sort, scaling, blitting, contiguous and strided elementwise and matrix arithmetic, LU factoring/solving, and ndarray gathering, casting, padding, normalization, resampling, and reductions, wherendarray-reduce.wgslandndarray-affine.wgslare fragments that compile appended tondarray.wgsl.
Common interactions:
./typescript/core/renderer.tsand the internal renderer helper modules under./typescript/core/import render shaders../typescript/compute/kernels.tsimports compute shaders, and./typescript/compute/ndarray.tsimports./wgsl/compute/ndarray.wgsl,./wgsl/compute/ndarray-reduce.wgsl, and./wgsl/compute/ndarray-affine.wgsl../typescript/wgsl/interop.tssupplies shared explicit layout and resource normalization to compute pipelines andCustomMaterial, but it does not inspect shader source../esbuild.config.jsminifies WGSL imports during bundling.
Architectural role:
- Demonstrate public API usage and browser workflows.
- Provide automated integration coverage and manual visual coverage for runtime, rendering, controls, picking, overlays, scaling, compute, glTF, and examples that combine several subsystems.
Important files:
./examples/benchmark.html: performance benchmarking WasmGPU against Three.js and Babylon.js for both rendering and computing../examples/controls.html: camera controls and interaction../examples/esm.html: ES module bundle usage../examples/iife.html: IIFE bundle usage../examples/gltf.html: glTF loading and import../examples/overlay.html: overlay layers../examples/picking.html: picking and selection../examples/scaling.html: scaling and colormap behavior../examples/fluid.html,./examples/galaxy.html,./examples/graphing.html,./examples/mandelbulb.html,./examples/protein.html,./examples/terrain.html,./examples/lego.html, and./examples/quantum.html: larger rendering and compute examples.
Examples import built bundles from ./release/. When source changes public behavior, update examples when preparing a release or when a specific example fix is needed.
Architectural role:
- Validate selected public API behavior and internal behavior through Node and Rust test files.
- Cover renderer setup, geometry, mesh behavior, materials, glTF import, accessors, compute behavior, transforms, math, controls, scaling, overlays, annotations, picking data, and scientific visualization primitives.
- Keep the manually generated Rust-to-WebAssembly function bridge synchronized with the compiled module and its TypeScript declarations.
Important files:
./rust/src/tests/*.rs: native Rust conformance tests run bynpm run test:rs../tests/*.test.js: Node tests which are browser modules loaded one per isolated Playwright browser page bynpm run test:js../tests/manifests/setup.spec.js: independent Playwright setup suite that verifies the secure browser origin, WebGPU adapter and device creation, command submission, real-canvas presentation, and complete examples-manifest coverage before the primary suites run../tests/manifests/runner.spec.js: Playwright entry point that discovers test modules, forwards browser and WebGPU diagnostics, records device and queue-submission counts, and imports each module in isolation../tests/manifests/suites.js: single source of truth for example files, page titles, readiness signals, bounded example-specific timeouts, browser-module discovery, and expected-count invariants used by setup coverage, execution, report provenance, and merging../tests/manifests/examples.spec.js: Playwright entry point that loads each manifest example in an isolated page, checks browser, network, and WebGPU diagnostics, waits for completed GPU work, and exercises stable user-facing interactions. Examples retain their normal production workloads and live external resources during these tests../tests/utils/: dual-runtime Playwright and browser-safe assertions, terminal formatting, WebGPU instrumentation and device lifecycles, deterministic random data, approximate comparisons, scoped real canvases, narrowly focused observable canvas doubles, compute-buffer readback, and other reusable test helpers. Test modules bind test-specific helpers first, callsetupTest()once when shared initialization is needed, then bind and validate subsystem exports before entering numbered theme blocks../playwright.config.js: discovers Node-side./tests/manifests/*.spec.jsentry points and pins the independent setup, runner, and examples projects, local origin, selected browser, browser-specific WebGPU launch configuration, isolation, retries, reports, traces, and screenshots.WASMGPU_BROWSERselectschromium,firefox, orwebkit; Chromium alone receives Dawn, Vulkan, and SwiftShader command-line arguments, Firefox alone receives its runtime WebGPU preference and runs headed on Windows and MacOS so its GPU process can remain enabled, and WebKit receives no foreign-engine flags.WASMGPU_FORCE_FALLBACK_ADAPTER=1makes the test monitor require Firefox's fallback adapter and is rejected for other browsers../scripts/configure-firefox-webgpu.js: installs the Windows Firefox job's WebGPU blocklist override and parent-process permission into Playwright Firefox's application startup preferences before the browser launches. These preferences are startup-only and therefore cannot be supplied reliably through Playwright's later runtime preference update../scripts/serve-tests.js: minimal same-origin static server for test modules, bundled JavaScript, generated WebAssembly, declarations, and WGSL assets../scripts/merge-test-reports.js: serves as the Playwright provenance reporter, cleans aggregate output before full runs, validates report completeness, and combines the independently generated setup, JavaScript, and example blob reports../tests/wasm.test.js: WebAssembly ABI, generated-binding, heap, frame-arena, and external-module interop coverage../tests/wgsl.test.js: checks shader-module compilation and diagnostics plus WebGPU interop behavior through the selected Playwright browser's WebGPU implementation. It requests supported optional features and skips only modules that explicitly require unavailable features. It does not construct pipelines or validate shader interfaces against TypeScript bind group, vertex, override-constant, or render-target descriptors, so focused subsystem tests cover those integration contracts../.github/workflows/test.yaml: GitHub Actions workflow for automatically running tests on every push and pull request.
The CI selection table below makes the platform decision explicit instead of treating the eighteen-entry operating-system, architecture, and browser product as uniformly useful. A row is included only when GitHub supplies the host, Playwright supplies a native browser for that architecture, WebGPU is expected to exercise WasmGPU rather than fail at browser setup, and the row adds material coverage. Chromium supplies the portable operating-system and CPU-architecture baseline, while Firefox is retained on its supported Windows and Apple Silicon targets. Linux Firefox remains outside CI while its WebGPU implementation exhibits external backend failures, Playwright's WebKit embedder does not expose WebGPU even on a host where Safari supports it, and Windows ARM64 remains outside CI because Playwright currently supplies x64 rather than native ARM64 Windows browser packages. GitHub's windows-2025 AMD64 image is the available Windows Server 2025 surrogate for the Windows 11 rows rather than a literal Windows 11 client installation.
✅ marks a combination that is expected to work, ❌ marks a combination that is not expected to work,
| Candidate job | Hosted runner | Native browser | WebGPU ready | Adds distinct coverage | Stable setup | In CI workflow |
|---|---|---|---|---|---|---|
| Windows 11 AMD64 Chromium | ✅ | ✅ | ✅ | ✅ | ✅ | |
| Windows 11 AMD64 Firefox | ✅ | ✅ | ✅ | ✅ | ✅ | |
| Windows 11 AMD64 WebKit | ✅ | ❌ | ❌ | ❌ | ||
| Windows 11 ARM64 Chromium | ❌ | ❓ | ❌ | ❌ | ||
| Windows 11 ARM64 Firefox | ❌ | ❓ | ❌ | ❌ | ||
| Windows 11 ARM64 WebKit | ❌ | ❌ | ❌ | ❌ | ||
| Ubuntu 24 AMD64 Chromium | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 24 AMD64 Firefox | ✅ | ✅ | ✅ | ❌ | ||
| Ubuntu 24 AMD64 WebKit | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Ubuntu 24 ARM64 Chromium | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 24 ARM64 Firefox | ✅ | ✅ | ✅ | ❌ | ||
| Ubuntu 24 ARM64 WebKit | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| MacOS 26 AMD64 Chromium | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| MacOS 26 AMD64 Firefox | ✅ | ✅ | ❌ | |||
| MacOS 26 AMD64 WebKit | ✅ | ✅ | ❌ | ❌ | ❌ | |
| MacOS 26 ARM64 Chromium | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| MacOS 26 ARM64 Firefox | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| MacOS 26 ARM64 WebKit | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
npm run test first removes prior aggregate artifacts, then uses an ordinary npm command chain to check Rust formatting, run Clippy over all native targets and the WebAssembly library, run the Rust tests, run two setup checks, run the 28 fast browser-module tests, run the 16 slower example tests, and merge reports. The chain stops at the first failing command. npm run test:setup, npm run test:js, and npm run test:ex select only their respective Playwright projects, so their terminal counts remain 2, 28, and 16 and development iteration does not implicitly rerun setup or examples. The full command and CI explicitly sequence setup before the primary suites. In CI, setup is a prerequisite for the browser-module, example, and merge steps, preventing an unavailable WebGPU implementation from turning every subsequent test into a timeout; after setup passes, the browser-module and example suites still run independently so both can retain diagnostics if either fails.
Playwright owns each test server it starts, requests graceful shutdown after its suite, and forcefully terminates the process group if shutdown exceeds the bounded timeout; a server explicitly started with npm run test:js:serve remains open until that command is stopped. Each invocation writes separate HTML, blob, attachment, and provenance records; merging rejects missing, filtered, listed-only, or incomplete runs before combining all three blob reports under ./playwright-report/combined/. Before diagnostics are asserted, every monitored queue is settled so late uncaptured WebGPU errors and device losses are observable.
The CI workflow in ./.github/workflows/test.yaml runs seven explicit platform/browser entries. Every entry uses one short-circuiting command chain to check Rust formatting, run Clippy over all native targets and the WebAssembly library with warnings denied, and run the native Rust tests before the browser suites; this preserves fail-fast behavior under the Windows PowerShell runner as well as Unix shells. A successful Rust step is an explicit prerequisite for browser setup, while the browser-module and example suites remain independent after setup succeeds so both can retain diagnostics if either fails.
Chromium uses bundled SwiftShader on Windows and MacOS, and the MacOS AMD64 job additionally pins Chromium's ANGLE presentation path to SwiftShader so real WebGPU canvas presentation does not depend on the hosted Intel graphics path. Chromium on Linux installs Mesa Lavapipe, restricts native Vulkan and ANGLE presentation to its ICD, and runs headed inside Xvfb. Firefox uses its own WebGPU implementation with dom.webgpu.enabled. Firefox runs headed on Windows and MacOS because headless mode can disable its GPU process on those platforms; Linux can remain headless outside the workflow's existing Xvfb execution. GitHub's Windows VM has no Firefox-compatible hardware adapter, so that job writes gfx.webgpu.ignore-blocklist and dom.webgpu.allow-in-parent into the installed browser's startup preferences before launch, then makes the test monitor add forceFallbackAdapter: true to every adapter request to select Firefox's WARP software implementation. Playwright's ordinary firefoxUserPrefs update occurs after Firefox has frozen these startup-only graphics decisions and is not used for them. This is test-harness policy rather than a WasmGPU public API change, and applies consistently to setup, browser modules, renderer creation, and examples.
The setup suite verifies that the selected adapter identifies itself as a fallback adapter and includes browser WebGPU warnings in adapter-acquisition failures, so browser startup restrictions remain distinguishable from WasmGPU failures in the job log. Real-canvas tests normally use navigator.gpu.getPreferredCanvasFormat(), matching the renderer's production default and avoiding a nonpreferred format from turning a browser-backend defect into a false WasmGPU failure; controlled canvas doubles retain explicit format coverage. The monitor settles outstanding work before entering an explicit renderer-teardown scope and classifies errors emitted while that scope is active separately. The runner filters Firefox's exact unlabeled-buffer destruction diagnostic only from that teardown collection; the same diagnostic during normal operations, every other teardown diagnostic, and all other monitor errors remain active.
The setup presentation resources likewise remain alive until Playwright closes their isolated page, avoiding immediate teardown of a just-presented canvas. The suite tests WasmGPU in its production execution environment: a real DOM, browser WebAssembly, browser image/canvas APIs, and navigator.gpu. Renderer-facing integration themes and example tests use real canvas contexts, so controlled doubles remain only where a test must inspect configuration calls, swapchain acquisition counts, injected picking results, event-listener ownership, or similar isolated contracts. Each test module and example gets a fresh page and requests its own device, preventing mutable DOM, WebAssembly, and GPU state from leaking between tests. Example tests use a fixed smaller viewport while retaining normal data sizes and live external dependencies; they fail on page crashes, uncaught exceptions, console errors, required request failures, HTTP error responses, uncaptured WebGPU errors, unexpected device loss, missing completed GPU work, and failed readiness or interaction assertions, while reporting warnings without failing on them. Tests use generated or bundled files when needed.
If a change modifies Rust exports, generated WebAssembly bindings, renderer behavior, glTF import, shader layouts, public descriptors, or examples, add or update focused tests where the current test setup can exercise the behavior.
Architectural role:
- Provides local developer infrastructure for repeatable performance measurement, scaling observation, and regression analysis.
- Executes the built production bundle in a Playwright-managed Chromium browser with WebGPU and analyzes preserved JSON measurements separately with Python.
Important files and directories:
./benchmarks/harness/: Node browser orchestration, native-adapter policy, warmup and sample lifecycle, statistics, structured reporting, and shared browser-side execution../benchmarks/manifest.jsand./benchmarks/**/*.bench.jsfiles: explicit registration plus self-contained definitions for representative subsystem workloads../benchmarks/machine.example.json: committed template for required manually declared machine facts, as the completed./benchmarks/machine.local.jsonis ignored../benchmarks/analysis/: NumPy and Matplotlib post-processing that creates Markdown summaries and PNG plots../benchmarks/reports/: chronological local or opt-in tracked run directories. Each stores its environment manifest and per-benchmark JSON logs underlogs/, PNG charts underplots/, and a Markdown summary at the run root. Tracked artifacts use.benchbefore the extension, whereas local artifacts use.bench.localand are ignored by Git.
Benchmarks import ./release/WasmGPU.js, use quick workloads by default, and accept --full for larger workloads and more samples. Executable files follow ./benchmarks/<subsystem>/<owner>-<operation>-<type>.bench.js. Throughput and microbenchmark samples repeat measured work until a minimum timing interval is reached, while latency, frame, and end-to-end samples remain single operations. Engine-backed render and pick definitions use additional warmup before recorded samples. Using --tracked selects trackable report filenames but cannot be combined with the diagnostic-only fallback override. Native hosts launch local Chromium with platform-aware arguments. Under WSL2, an automatic compatibility path starts native Windows Chrome or Edge with remote debugging and an isolated temporary Windows profile, keeps Chrome CDP on Windows loopback, relays only through the private Windows WSL interface, attaches from WSL with Playwright CDP, and loads the WSL-served bundle without another checkout. Note that --windows-browser overrides autodetection and --linux-browser forces local Linux Chromium. Adapter validation is identical for either launch path. Reports distinguish controller and browser environments and record the actual WebGPU adapter separately from the physical GPU. Normal and tracked runs reject unidentified, software, and fallback adapters. GPU measurements wait for submitted work where completion is part of the stated metric. Resources owned by a workload are destroyed during teardown.
Architectural role:
- Contains release-facing documentation and website source.
- Uses built bundles and examples when the website is generated.
Important files and directories:
./README.md: release-facing project overview. It currently describes v0.10.0../CHANGELOG.md: release notes through the latest documented release../CONTRIBUTING.md: shorter contributor guide for questions, issues, feature requests, and code contributions../website/docs/: MkDocs documentation source../website/home/: homepage source copied to the website root during generation../website/examples/: examples page source copied beside the generated example pages../website/performance/: performance page source with benchmark data and charts fetched dynamically based on the latest benchmark report in the GitHub repository../website/build/: generated website output../mkdocs.yaml: MkDocs configuration for documentation pages../scripts/build_website.py: website build script. It copies website files, assets without./assets/videos/, examples, performance, and./release/WasmGPU.min.js, rewrites the copied examples from local./release/imports to the pinned./release/URLs, runs MkDocs, and copies the assets including./assets/videos/only into the documentation output../scripts/website_videos.py: documentation video generator. It renders the narrated overview videos, WebVTT captions, and poster title cards with Manim and offline Kokoro text-to-speech, bootstraps its own Python 3.11-3.13 virtual environment under./tools/videos/, and is run withnpm run videos../.agents/skills/maintain-docs/: OpenAI Codex skill for auditing, writing, maintaining, and verifying Markdown documentation under./website/docs/../.github/workflows/website.yaml: GitHub Pages workflow for website deployment.
./scripts/build_website.py writes generated website output under ./website/build/. That directory is not present in the codebase unless the website build has been run locally.
The documentation index page and the eight section index pages under ./website/docs/ embed the generated overview videos from ./assets/videos/ with their WebVTT captions and poster title cards.
./website/docs/, ./website/home/, ./website/examples/, ./website/performance/, ./README.md, and ./CHANGELOG.md are release-facing sources. They are often updated significantly during release work, not for every source change.
The $maintain-docs skill exists to keep the large Markdown reference aligned with the current public API without replacing source inspection or human review. Invoke $maintain-docs in release mode for a versioned documentation delta, maintain mode for a focused documentation change, or verify mode for a read-only accuracy and integrity pass. Release mode writes an audit ledger under the gitignored ./.cache/ directory, stops for approval before editing, normalizes all files under ./website/docs/ to LF line endings during approved implementation, writes in source-backed subsystem batches, and performs final coverage and link validation. The skill does not update this file or run the website build.
Architectural role:
- Defines TypeScript, Rust, WebAssembly, bundle, package, and website build behavior.
- Produces generated WebAssembly files and distributable bundles.
Important files and directories:
./.cargo/config.toml: shared Cargo policy for warning-free local compilation, the Rust target directory, future-incompatibility reporting, MSRV-aware dependency resolution, the crates.io protocol, and the WebAssembly linker../rust-toolchain.toml: pinned Rust development toolchain, rustfmt and Clippy components, and thewasm32-unknown-unknownstandard-library target../package.json: npm scripts and package metadata../rust/Cargo.toml: Rust crate metadata and release profile../tsconfig.json: TypeScript compiler settings../esbuild.config.js: bundle configuration, release banner metadata, WGSL loader, minification, and copy steps from./wasm/to./release/../scripts/build-rust-wasm.js: Rust-to-WebAssembly build script. It invokes Cargo, generates.wasm,.wat, JavaScript loader, and declaration output, and can run WebAssembly optimization tooling../wasm/: generated WebAssembly loader, binary, text format, and declarations../release/: bundled ESM and IIFE JavaScript files plus WebAssembly copies../.agents/skills/prepare-release/: OpenAI Codex skill for auditing, preparing, and verifying versioned WasmGPU releases after engine source is finalized../rust/target/: local Rust build output when present. This is build output rather than source.
./wasm/ and ./release/ are checked-in release artifacts. Development builds can modify them, and npm run restore restores them via Git.
The $prepare-release skill exists to coordinate versioned release housekeeping around engine source that the human considers finalized. Invoke $prepare-release in audit mode to inspect the release delta, write a compact audit and GitHub release-note candidate under the gitignored ./.cache/ directory, and stop for approval; prepare mode to update package and Cargo versions, ./CHANGELOG.md, ./README.md, release-facing website state, release metadata in this file, and contextual release URLs before rebuilding and preparing the checked-in release artifacts; or verify mode for a read-only release-readiness pass. The skill treats engine source defects as release blockers, does not perform Markdown documentation maintenance, does not run benchmarks, and does not commit, tag, push, publish, or create a GitHub release.
Cargo commands invoked from the repository root or ./rust/ inherit the repository Cargo policy and rustup toolchain selection. Package and release-profile settings remain in ./rust/Cargo.toml, while dynamic WebAssembly SIMD, shared-memory, memory-limit, and post-processing variants remain owned by ./scripts/build-rust-wasm.js.
Architectural role:
- Supplies website and example assets plus repository-level configuration.
Important files and directories:
./assets/: static images and assets used by documentation, the website, or examples../assets/videos/: generated MP4 documentation overview videos and their WebVTT captions../assets/fonts/: Montserrat and Ubuntu Mono fonts with their OFL and UFL license texts, used by logo and video generation../LICENSE.md: project license (MPL-2.0)../.gitignore: ignored build and local files../.gitattributes: line endings, linguist options, and generated files../.github/: workflow configuration.
These files are not part of the runtime architecture, but they affect packaging, website output, and repository maintenance.
Read ./CONTRIBUTING.md first for the shorter contributor-facing guide. It covers asking questions, reporting issues, requesting features, and contributing code.
Before changing a subsystem, inspect the source files that own it, the renderer or compute paths that call it, related WGSL, related tests, and at least one example that uses it. Use the current source as the main reference when release-facing documentation describes an older version.
For history context, inspect recent commits touching the same area. History is useful for renamed files, release-update patterns, commit style, and files that often change together. Current source wins when history and source differ.
Match the code around the change. The TypeScript source currently uses strict compiler settings, explicit descriptor types, classes for runtime objects, private fields where useful, and targeted helper functions. Rust code uses small modules with exported extern "C" functions for WebAssembly entry points. WGSL files keep shader logic close to the subsystem that imports it.
./package.json defines npm run fmt:rs for checking Rust formatting and npm run lint:rs for running Clippy over all native targets and the WebAssembly library with warnings denied. Use TypeScript, Rust, and WGSL formatting that is consistent with nearby lines and files.
Do not add broad rewrites while making a focused change. Renderer, transform, compute, glTF, and shader changes should stay close to the behavior being changed.
Treat exports from ./typescript/index.ts and runtime factories in ./typescript/core/engine.ts as public API. Preserve names, descriptor fields, default values, return shapes, and error behavior unless the change is intentionally public.
When public behavior changes, update tests and examples that use that behavior. Update release-facing files when preparing a release or when a specific small fix requires it. Do not rely on ./README.md alone for current architecture, because it only describes the latest release while the source tree contains all of the unreleased work.
For TypeScript changes:
- run
npm run build:tsfor type checking; - add or update
./tests/*.test.jswhen behavior is testable in the current Node setup; - check object lifetime and
destroy()paths when adding GPU resources; - keep renderer, compute, and transform hot paths allocation-conscious;
- update exports in
./typescript/index.tswhen adding public functionality; - update IIFE exposure through
./typescript/index.iife.tswhen needed.
When changing renderer paths, inspect the object class, material class, texture path, shader variants, and tests together. Renderer changes often depend on matching layouts across TypeScript and WGSL.
For Rust changes:
- use rustup so
./rust-toolchain.tomlselects the pinned development compiler, rustfmt and Clippy components, and WebAssembly target; - edit source under
./rust/src/; - add portable algorithm coverage under
./rust/src/tests/and WebAssembly-only ABI coverage in./tests/wasm.test.js; - update TypeScript wrappers in
./typescript/wasm/index.tsor./typescript/wasm/interop.tswhen exports change; - run
npm run fmt:rsandnpm run lint:rsbefore committing Rust changes; - run
npm run build:rsto regenerate WebAssembly files; - run
npm run test:rsfor native Rust checks andnpm run testafter wrapper changes; - keep pointer, length, dtype, stride, and alignment assumptions explicit at call sites.
Rust heap allocation/free calls must use the same pointer, allocator family, and byte or element count exactly once. Prefer the frame arena for per-frame data and WasmHeapArena for grouped temporary allocations. If allocation semantics change, update ./typescript/wasm/driver.ts, interop callers, WebAssembly tests, and this file.
For WGSL changes:
- update matching TypeScript bind group layouts, vertex layouts, uniform packing, and pipeline creation;
- check shader imports in
./typescript/core/renderer.ts,./typescript/compute/kernels.ts, or related subsystem files; - keep material and picking variants aligned when adding geometry attributes or object types;
- keep shadow receiver injection aligned across standard material static, instanced, skinned-4, skinned-8, and transmission variants, and keep shadow caster vertex inputs aligned with geometry, morph, and skin sources;
- run
npm run build:jswhen checking bundling and WGSL import handling.
Shader changes are often coupled to tests that validate material defaults, renderer behavior, glTF import, nodelink picking, scaling kernels, or compute kernels.
Reason explicitly about data ownership and lifetime:
- TypeScript typed arrays may copy into WebAssembly memory, GPU buffers, or both.
- WebAssembly frame arena slices expire on reset.
- WebAssembly heap slices remain valid until explicitly freed; their storage may be reused immediately afterward, while the grown WebAssembly memory pages remain allocated to the instance.
WasmHeapArenaslices expire when the arena resets or is destroyed, and destroying the arena releases its backing heap allocation for reuse.- Python interop owns no separate memory handles, as returned
CPUndarrayandGPUndarrayobjects follow their canonical destruction and readback rules. - ndarray views borrow their root's storage and never free it; destroying a view releases only the shape and stride arrays a CPU view allocated on first access, while destroying the root invalidates every view.
- The compute scratch allocator lives as long as its
Compute. It retains idle temporaries and self-submitted parameter blocks up toscratch.maxRetainedBytes, while CPU resize intermediates are freed within their call and only bounded, as a preference, byndarray.maxCPUIntermediateBytes; it keeps temporaries a caller's encoder used pooled without ever destroying them, and leaves each encoder's parameter blocks to the garbage collector with that encoder. CPUndarray, animation, skin, renderer culling, geometry scratch, and glTF import paths release the WebAssembly allocations they own; borrowed external WebAssembly views are never freed by those consumers.- The active global transform store and frame arena retain their current backing storage for the WebAssembly module lifetime, while superseded transform capacity and scoped scratch blocks are reclaimed.
- GPU buffers need correct usage flags for writes, copies, storage binding, vertex binding, index binding, uniform binding, or readback.
- Textures may be pending upload while render paths use fallback resources.
- Pick attribute records may require CPU data retention.
- Shadow texture arrays and metadata/model/view buffers are renderer-owned and lazy; configuration changes and final renderer destruction must release incompatible or final resources without taking ownership of configured lights.
Avoid unnecessary allocations in frame loops, draw-list building, transform updates, GPU readbacks, compute dispatch, and overlay updates. Reuse existing pools, caches, scratch buffers, and frame arena memory where those patterns already exist.
Update examples when a public API change affects example code, when a new public feature needs a usage reference, or when an example is part of release work. Examples import ./release/ bundles, so source-only changes may not appear in examples until bundles are rebuilt.
Release-update convention:
./release/and./wasm/contain build bundles and binaries and are updated only when a new WasmGPU version is released../website/contains the website and documentation and is usually updated significantly only when a new WasmGPU version is released../CHANGELOG.mdis usually updated significantly only when a new version is released../README.mdis usually updated significantly only when a new version is released.- The generated documentation videos, captions, and title cards are release-facing website assets, usually regenerated only when a new version is released.
- Small fixes to those files may happen outside a release, such as typo fixes or small bug and documentation corrections.
./ARCHITECTURE.mdis different: update it whenever there is a significant source-code architecture change, not only when preparing a release.
Do not update ./README.md, ./CHANGELOG.md, ./website/, ./release/, or ./wasm/ for every source change. Update ./ARCHITECTURE.md for significant architecture changes. Update release-facing files when preparing a release or when a specific small fix requires it.
Use the OpenAI Codex $maintain-docs skill for work under ./website/docs/. Its release mode requires an audit and approval checkpoint before documentation changes, its maintain mode keeps focused updates narrowly scoped, and its verify mode checks source accuracy and documentation integrity without editing by default. The skill keeps release audit ledgers under ./.cache/ and reserves repository-wide LF normalization of the documentation tree for approved release implementation.
Use the OpenAI Codex $prepare-release skill for versioned release preparation after engine source is finalized. Its audit mode requires a release-delta audit and approval checkpoint before tracked release files change, its prepare mode updates contextual version metadata and release-facing sources, rebuilds and prepares the checked-in release artifacts, computes final release metrics, and runs release validation, and its verify mode checks an already-prepared release worktree without rebuilding or editing by default. The skill keeps its audit and GitHub release-note candidate under ./.cache/, treats engine-source defects as release blockers rather than repairing them, and leaves Markdown documentation maintenance to a separately human-invoked $maintain-docs workflow. The two skills are independent and do not invoke or depend on one another.
npm run build:rs updates generated WebAssembly files under ./wasm/. npm run build:js updates bundles under ./release/. npm run build runs the Rust, TypeScript, and bundle steps.
During non-release development, generated changes under ./wasm/ and ./release/ may be local build output. Use npm run restore to restore those directories from Git when generated output is not part of the intended change. Changes to ./wasm/ and ./release/ should normally only be part of a release commit.
During an approved $prepare-release prepare run, update the target release version and date in the releaseBanner literal in ./esbuild.config.js before running npm run build. The JavaScript build automatically emits that banner followed by one empty line at the start of ./release/WasmGPU.js, ./release/WasmGPU.min.js, ./release/WasmGPU.iife.min.js, and ./release/wasm.js. Note that the binary ./release/wasm.wasm should remain unbannered. Do not manually edit generated files under ./release/ or ./wasm/. $prepare-release verify mode does not run npm run build because verification must preserve the prepared artifacts.
The release preparation workflow derives the homepage JavaScript line count from the final bannered ./release/WasmGPU.js and the displayed minified bundle size from the final bannered ./release/WasmGPU.min.js. It derives release additions, deletions, and changed-file count from the baseline release to the complete intended current workspace, then rechecks those values after writing them into ./website/home/index.html. npm run website is used as a build-process check only, as visual website approval remains manual. Release preparation does not run benchmarks.
npm run videos regenerates the committed documentation videos, captions, and title cards from ./scripts/website_videos.py. Intermediate media, the voice cache, the text-to-speech model, and the virtual environment stay in the gitignored ./tools/videos/ directory. Generated video outputs should not be edited by hand.
Website generation uses npm run website, which runs ./scripts/build_website.py. That script reads ./release/WasmGPU.min.js, copies assets and examples, rewrites the copied examples from their local ./release/ imports to the pinned v0.10.0 CDN ./release/ URLs, and writes output under ./website/build/. The repository examples therefore exercise the current local bundles, while the generated website examples continue to exercise the documented v0.10.0 release.
Use the commands from ./package.json:
| Command | Use |
|---|---|
npm run dev |
Run npm run build and npm run test and npm run restore. |
npm run build |
Run Rust build, TypeScript check, and bundle build. |
npm run build:rs |
Compile Rust with ./scripts/build-rust-wasm.js. |
npm run build:ts |
Type-check TypeScript with ./tsconfig.json. |
npm run build:js |
Bundle the package with ./esbuild.config.js. |
npm run test |
Check formatting and linting, then run all of the Rust and Node tests. |
npm run test:rs |
Run Rust tests under ./rust/src/tests/. |
npm run test:clean |
Remove prior test reports before a new full run. |
npm run test:setup |
Run Node tests for the setup of npm run test:js and npm run test:ex. |
npm run test:js |
Run Node tests under ./tests/*.test.js with Playwright Chromium. |
npm run test:ex |
Run Node tests under ./examples/ with Playwright Chromium. |
npm run test:merge |
Merge all three of the Node test reports after they run. |
npm run test:js:headed |
Run the Node tests with a visible Chromium window. |
npm run test:js:debug |
Run the Node tests with Playwright's interactive debugger. |
npm run test:js:serve |
Serve the repository locally for the Node tests. |
npm run test:report |
Open the latest combined test report generated by npm run test. |
npm run test:setup:report |
Open the latest test report generated by npm run test:setup. |
npm run test:js:report |
Open the latest test report generated by npm run test:js. |
npm run test:ex:report |
Open the latest test report generated by npm run test:ex. |
npm run restore |
Restore ./wasm/ and ./release/ from Git. |
npm run fmt |
Check formatting. |
npm run fmt:rs |
Check Rust formatting with rustfmt. |
npm run lint |
Check linting. |
npm run lint:rs |
Check Rust linting with Clippy. |
npm run bench |
Run the quick benchmark suite and report. |
npm run bench -- --full |
Run the full benchmark suite and report. |
npm run bench:render |
Run render benchmarks. |
npm run bench:compute |
Run compute benchmarks. |
npm run bench:math |
Run math benchmarks. |
npm run bench:objects |
Run objects benchmarks. |
npm run bench:scaling |
Run scaling benchmarks. |
npm run bench:gltf |
Run glTF benchmarks. |
npm run bench:interop |
Run interop benchmark. |
npm run bench:interact |
Run interact benchmarks. |
npm run website |
Build the website into ./website/build/. |
npm run videos |
Generate the documentation overview videos, captions, and title cards via ./scripts/website_videos.py. |
npm run logo |
Regenerate logo raster assets via ./scripts/rasterize_logo.py. |
The appropriate minimum depends on the change. Documentation-only changes usually need Markdown review. TypeScript behavior changes need at least npm run build:ts and focused tests. Rust, shader, or bundle changes need the matching build steps.
Recent history mostly follows Conventional Commits:
- preferred pattern:
type(scope): subject; - common types:
feat,fix,test,docs,chore,refactor, andperf; - scopes are file or folder names without extensions, such as
renderer,gltf,material,nodelink,examples, orrust; - subjects are usually lowercase, concise, and have no final period;
- release commits use a pattern like
chore(release): v0.10.0.
Examples from repository history include:
feat(latticespace): add WebGPU lattice space renderingfeat(compute): add optimized batched LU factor and solve kernelsrefactor(renderer): split into modulesfix(gltf): stabilize variants, skins, geometry, & morph renderingfix(wasm): add deterministic resource lifetimestest(material): cover defaults, uniforms, shaders, data, colormaps, & cleanupbench(suite): cover compute kernels, data transfer, render objects, culling, & effectsdocs(examples): update terrain with fly controlsfeat(rust): reclaim freed WebAssembly heap allocationsfeat(effects): add directional shadow mappingchore(build): bump Rust from v1.93.0-2021 to v1.97.1-2024chore(release): v0.10.0
Use a commit body when the subject cannot explain the change, especially for API behavior, memory ownership, generated artifacts, release work, or changes that touch multiple subsystems.
Update ./ARCHITECTURE.md when source-code architecture changes significantly. Examples include new public subsystems, renamed directories, changed data ownership, new renderer passes, new WebAssembly exports, new shader families, changed build artifact rules, or release conventions that have changed in practice.
When updating this file:
- inspect current source before editing;
- keep the diagram and section order aligned;
- keep the writing style completely unchanged despite the writing substance changes;
- describe current behavior, not only released behavior;
- remove claims that are not visible from source, tests, documentation, build configuration, or git history.
This file (and more broadly, WasmGPU) is available under the Mozilla Public License 2.0.