Skip to content

Repository files navigation

subscript-gpu

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.

Why it exists

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 sType chains, 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.

How it works

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. async methods are real subscript async functions the host steps at its loop boundary — no promise objects, no microtask queue, no GC. Every wrapper has an explicit release(); create-owns, borrow never releases. Events (device loss, uncaptured errors) are poll-drain methods, not an EventTarget. Each deviation is a recorded policy decision, not an accident.
  • Enums are strings in script and integers on the wire — with no conversion. A GPUTextureFormat like "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.

Performance

One comparison matters: the same per-frame encode loop (beginRenderPass → 1000 × (setBindGroup with a dynamic offset → draw) → endfinish), 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.

Quality

  • 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 --check and cargo clippy -D warnings are 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.

The windowed example

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.

Tutorials

Building and testing

Rust (the pinned toolchain in rust-toolchain.toml), plus Node + TypeScript for the tsc gate.

sh tools/gate.sh        # fmt, clippy, workspace tests, hygiene, tsc

Without 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.sh

The 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.

Status

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.

License

Dual-licensed under either of

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.

About

WebGPU scripting for native apps, in a statically-typed TypeScript subset

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages