WebGPU for subscript
programs — the standard WebGPU API, in its standard JavaScript
shape, for a statically-typed embedded scripting language, over any
webgpu.h
implementation.
subscript is a TypeScript-subset embedded language with a C execution
model: sound types, deterministic memory, zero-copy C interop, a
hot-reload development tier and a native shipping tier. subscript-gpu
gives its scripts the GPU — through the same requestAdapter /
createRenderPipeline / beginRenderPass API every WebGPU tutorial,
book, and browser devtool teaches, not through a bespoke engine
binding.
import { gpu, GPUAdapter, GPUBuffer, GPUBufferUsage, GPUDevice } from "./webgpu";
export async function main(): Promise<void> {
const adapter: GPUAdapter | null = await gpu.requestAdapter();
if (adapter === null) { gpu.release(); return; }
const device: GPUDevice | null = await adapter.requestDevice();
if (device === null) { adapter.release(); gpu.release(); return; }
const buffer: GPUBuffer = device.createBuffer({
size: 32,
usage: GPUBufferUsage.COPY_DST,
});
// … encode, submit, await completion — then release what you created.
}That is condensed from a real program in the repository's test suite
(programs/a17-api-roundtrip.ts — the full version also prints its
progress); the suite runs headless on every test run, on both
execution tiers, against committed byte-exact goldens.
A native application that embeds subscript owns a C ABI and its main loop. When its scripts need the GPU, the usual options are:
- Invent an engine-specific script API — every function, every descriptor, every lifetime rule is yours to design, document, and teach. Nobody outside the project knows it.
- Bind the raw C API 1:1 — scripts inherit
sTypechains, count-plus-pointer pairs, and integer enums. It works, and reads like C with extra steps.
subscript-gpu takes the third option: the WebGPU JavaScript API is
already the best-documented GPU API in existence, and subscript's
sound TypeScript subset can carry its shape — string-literal enums,
descriptor dictionaries with defaults, async adapter and device
requests, method-per-object encoders. Scripts written against
subscript-gpu look like the WebGPU code on MDN and in the gpuweb
samples, and tsserver completes and checks them with no custom
editor plugin.
Underneath, the backend is an ordinary webgpu.h implementation,
chosen at link time.
script: webgpu.ts gpuweb-IDL shape (GPUDevice, createBuffer, "rgba8unorm", await)
│ generated API layer — subscript source
mirror: sgpu.generated.d.ts ambient C mirror + CEnum wire-mapped enum aliases
│ generated by `subscript bind` from sgpu.h
facade: subscript-gpu-facade Rust crate, 151 extern "C" entry points (sgpu*)
│ generated from webgpu.yml + policy.toml
C ABI: webgpu.h the standard WebGPU C API
│ link-time choice
backend: yawgpu │ Dawn │ wgpu-native
Every layer above webgpu.h is generated from pinned sources — the
webgpu-headers
webgpu.yml for the facade and the
gpuweb WebIDL for the API layer —
and committed under byte-identical regeneration gates: hand-editing a
generated file fails a test that tells you to run the generator.
Three design decisions carry most of the weight:
- The shape is JavaScript's; the semantics are subscript's.
asyncmethods are real subscriptasyncfunctions the host steps at its loop boundary — no promise objects, no microtask queue, no GC. Every wrapper has an explicitrelease(); create-owns, borrow never releases. Events (device loss, uncaptured errors) are poll-drain methods, not anEventTarget. Each deviation is a recorded policy decision, not an accident. - Enums are strings in script and integers on the wire — with no
conversion. A
GPUTextureFormatlike"rgba8unorm"is a wire-mapped literal union (28 aliases, 292 members): its runtime representation is the C enum value, so writing a descriptor member is a plain store, and an unmapped integer coming back from C traps at the boundary with the alias name and value. The string never exists at run time. - The backend is a link-time choice, resolved by environment. yawgpu is the Tier-1 backend — its CPU-only Noop backend is what the whole test suite runs on, headless. Dawn is the conformance oracle for arbitration. GPU semantics live in the backend; this project validates what a binding must — argument conversion, lifetimes, completion delivery.
One comparison matters: the same per-frame encode loop
(beginRenderPass → 1000 × (setBindGroup with a dynamic offset →
draw) → end → finish), written once as a script on the
shipping tier and once as hand-written Rust calling the same
webgpu.h directly.
Script through the full JS-shaped API layer encodes at 1.056× of hand-written native code against the same backend — a measured +5.6% on the encode loop, inside the project's pre-registered ≤1.2× budget. Script is a first-class place to encode frames, not a scripting tax.
The decomposition behind that number was isolated with a generated
measurement backend whose calls cost ~nothing: of the binding's
total overhead, a bit over half is the JS-shaped API layer's
wrappers, and the rest splits about evenly between the
script-to-C boundary and the Rust facade. Absolute per-draw times
(machine-specific), the protocol, the spread, and every run are in
specs/tracking/p7-perf.md.
- The program suite is the definition. 35 programs with committed goldens run under both subscript tiers — the in-process dev JIT and the ship tier's emitted-C-compiled binary — and the outputs must be byte-identical to each other and to the golden, headless, on every test run. Accept programs and reject programs both count: a binding is defined as much by what it refuses.
- Everything generated is gated. Facade, header, mirror, API
layer, enum aliases: byte-identical regeneration tests, each
demonstrated red before it was trusted.
cargo fmt --checkandcargo clippy -D warningsare standing gates under a pinned toolchain. - Three desktop platforms. The full gate is green on macOS (arm64, where the goldens are authored), Windows (MSVC), and Linux (x86-64, GNU toolchain) — the Linux bring-up found and fixed a linker-argument-order defect upstream on its first run.
- Tutorials are measured. Code snippets quoted in the docs are checked against their source files by a test, and every command and output shown was run against the repository as committed.
- Real-device runs are recorded, never CI-required. The windowed example has device runs on macOS (Apple M2, Metal) and Windows (NVIDIA RTX 5060 Ti, Vulkan); CI needs no GPU anywhere.
- Co-developed with its dependencies, in the open. This project's measurements drove twenty-plus recorded upstream changes in subscript (R-series: wire-mapped enums across the FFI boundary, entry-less dev sessions, host entry hooks, …) and found real defects in yawgpu (a superlinear encode path, a surface-configure sentinel rejection) — each with a minimal reproduction in the tracking records, and none worked around silently.
examples/windowed-triangle/ is a
teaching artifact: a winit host that owns the window, the surface,
and the event loop, and a subscript script that owns the pipeline and
encodes every frame — hot-reloadable, on the dev tier.
The split is the lesson. The host translates platform events into
plain values (engineRecordKey); the script decides what they mean
(space advances the clear color) and builds its pipeline against the
format the host actually configured (engineFrameFormat, typed as
GPUTextureFormat end to end). Runs on macOS/Metal and
Windows/Vulkan; see its README for the build and the key bindings.
- subscript-gpu for Rust embedders — the minimal compute host, the four-step frame host, and the rules a Rust host must know.
- subscript-gpu for C and C++ embedders —
subscript check/emit/bind, two completemain.chosts, and the exact link line.
Rust (the pinned toolchain in rust-toolchain.toml), plus Node +
TypeScript for the tsc gate.
sh tools/gate.sh # fmt, clippy, workspace tests, hygiene, tscWithout a backend library the gate runs everything headless-testable
and prints one loud pending line for the backend suite. To run the
full differential suite, point it at a
yawgpu build:
SUBSCRIPT_GPU_BACKEND_LIB_DIR=<dir with libyawgpu> sh tools/gate.shThe suite runs on yawgpu's Noop backend — no GPU, no window, no
device. SUBSCRIPT_GPU_BACKEND=metal|vulkan selects a real adapter
at run time for the examples and device runs.
The library is complete: the generated facade over the pinned
webgpu.h, the generated JS-shaped API layer, the two-tier
differential program suite, the performance gate with its recorded
result, host-embedding for Rust and C with measured tutorials, the
windowed example, validation programs derived from the WebGPU CTS,
and an experimental wgpu-native backend with a recorded catalogue of
where it diverges from the pinned header.
The full gate is green headless on macOS, Windows, and Linux.
Design records live in specs/: specs/blocks/ holds the
area contracts, specs/tracking/ the evidence, and
specs/subscript-gpu-project-plan.md the plan and its phase history.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.