Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/tinywebgpu.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/tinywebgpu.tiny.js

Large diffs are not rendered by default.

36 changes: 33 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ await G.init(canvas); // or a 'webgpu' context, or a CSS selector
## Setup

### `await G.init(ctx?, opts?)`
Requests adapter + device (with the adapter's max buffer-size limits) and configures the canvas.
Requests adapter + device (with the adapter's max buffer-size limits and its
`maxStorageBuffersPerShaderStage`) and configures the canvas.
`ctx` can be a canvas `'webgpu'` context, a canvas element / OffscreenCanvas, or a CSS selector
for one — `init('#c')` works. Omit it for compute-only use — render calls then need an explicit
target view. Populates `G.device`, `G.context`, `G.format`, `G.features`. Throws if WebGPU is
Expand Down Expand Up @@ -158,12 +159,28 @@ auto-managed texture needs the target's size, which the library knows for the ca
`GPUTexture` passed to `drawTo`; handing `drawTo` a raw `GPUTextureView` it has never seen makes
it throw and ask for a texture or `depth.texture`.

### `G.makeCompute(bodyWGSL, mainWGSL, uniforms?, resources?, { wg = [8,8,1] })`
### `G.makeCompute(bodyWGSL, mainWGSL, uniforms?, resources?, { wg = [8,8,1], async, cache, moduleOnly })`
Compute pipeline. `bodyWGSL` = declarations (functions, structs); `mainWGSL` = statements placed
inside the generated entry point, which receives `gid` (global), `lid` (local), `wid`
(workgroup) invocation ids. `wg` is the `@workgroup_size`; missing axes default to 1, so
`wg: [64]` means `[64, 1, 1]`.

- **`async: true`** compiles off-thread with `createComputePipelineAsync`. The handle comes back
at once with `p.pipeline` still `0` and a `p.ready` promise; await it before dispatching (a
dispatch before then throws and says so). `setResources` called meanwhile is kept and bound
when the pipeline arrives. Worth it for big shaders, whose synchronous compile blocks the page.
- **The compute pipeline cache.** The same WGSL text (schema, `@workgroup_size` and `G.defines`
included) on the same device compiles once: a second `makeCompute` gets the first one's
`GPUComputePipeline` (immutable, so shared safely) and its own uniform buffer and bind groups.
`p.cached` says whether it was a hit. Bounded by `G.pipelineCacheMax` (48 entries) and
`G.pipelineCacheChars` (24e6 characters of key); the least recently used goes first. A
synchronous pipeline is entered only once its error scopes come back clean, which DIAG builds
alone check, so the minified builds cache async pipelines only. `cache: false` neither looks
up nor enters — for measuring what a compile costs.
- **`moduleOnly: true`** returns `{ wgsl, module }` — the exact source and its shader module —
and makes no pipeline. `module.getCompilationInfo()` then validates the WGSL without handing it
to the driver's compiler.

### Schemas
- `uniforms`: `{ name: 'f32' | 'i32' | 'u32' | 'vec2/3/4<f32|u32|i32>' | 'matCxR<f32>' }` →
generated into `struct Uniforms {...}` bound as `UB` at `@group(0) @binding(0)`. Layout
Expand All @@ -179,6 +196,10 @@ inside the generated entry point, which receives `gid` (global), `lid` (local),
buffer. Supported forms:
- `'array<T>'` → `var<storage, read_write>` runtime array (T may be a struct you define in
`bodyWGSL` or inline via the struct form below)
- `'read array<T>'` (any storage type after `read `) → bound `var<storage, read>` by every
pipeline that declares it, as if named in that pipeline's `readOnly`
- `'uniform array<vec4<f32>, 256>'` (any sized type after `uniform `) → bound `var<uniform>`;
its buffer needs UNIFORM usage and fits `maxUniformBufferBindingSize` (64 KiB by default)
- `'struct Foo { ... }'` → struct emitted + bound as `var<storage, read_write> name: Foo`
- primitive/vector/matrix → auto-wrapped in a single-field struct, bound as storage
- `'texture_2d<f32>'`, `'texture_storage_2d<rgba32float, write>'`, … → texture binding
Expand All @@ -189,7 +210,10 @@ inside the generated entry point, which receives `gid` (global), `lid` (local),
|---|---|
| `p.uniforms = {vals}` / `p.setUniforms(vals)` / `p.setUniform(name, v)` | Merge values into the CPU staging struct and upload the whole buffer. Vectors/matrices take arrays or TypedArrays (or scalars for 1-wide). A name that is not in the schema throws. |
| `p.resources = {vals}` / `p.setResources(vals)` | (Re)build the bind group. Pass buffer handles (`{b,w,r}`) or raw `GPUBuffer`s, and textures or views for textures. **Merged** into what is already bound, so partial updates are fine, and **compared by value**, so re-passing the same resources every frame is free. Bind groups are cached per resource combination (up to 8), so a ping-pong that alternates two states builds two groups total, not one per swap. A name that is not in the schema throws, like `setUniforms`. |
| `p.pipeline` | The raw `GPURenderPipeline`/`GPUComputePipeline`. |
| `p.pipeline` | The raw `GPURenderPipeline`/`GPUComputePipeline` — `0` until `p.ready` settles for an `async: true` one. |
| `p.ready` | Promise of the pipeline. Already resolved for a synchronous pipeline, so `await p.ready` is always safe. |
| `p.wgsl` | The source the driver was given, schema and `G.defines` included. |
| `p.cached` | Whether a compute pipeline came from the device's pipeline cache. |
| `p.uniformFields` / `p.resourceFields` | Names, in binding order. |

Compute-only:
Expand Down Expand Up @@ -228,6 +252,12 @@ Render-only:
render as large as `maxTextureDimension2D` allows and save once. The canvas texture is not
saveable unless you passed `canvasUsage` with `COPY_SRC` to `init`.

### `G.timePasses(fn)`
`fn(pipeline)` may return a `GPUComputePassDescriptor` — typically `{ timestampWrites }` — for
each compute pass a dispatch opens for itself; returning nothing opens a plain pass. A
`beginCompute()` chain's single pass keeps the descriptor it was opened with. `null` turns it off.
Needs the `'timestamp-query'` feature for timestamps.

## Frame API (batching)

```js
Expand Down
33 changes: 32 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,38 @@

All notable changes to TinyWebGPU. Semver; pre-1.0, minor versions may break API.

## Unreleased
## 0.5.0 — 2026-10-10

**Added — what ShadyTracer's copy had grown, brought home.** ShadyTracer (lampmaker/ShadyTracer)
carried its own copy of this file and had moved ahead of the release; it now consumes this
repository instead, so its additions land here:
- **`async: true`** on `makeCompute`/`makeCompute2D` (and the pipeline factory): compiled with the
`create*PipelineAsync` twins, `p.pipeline` a getter that is `0` until `p.ready` settles, and
resources bound meanwhile parked and replayed. A dispatch before then throws error 24 by name.
- **A compute pipeline cache per device**: the same text compiles once (`p.cached`), bounded by
`G.pipelineCacheMax` and `G.pipelineCacheChars`, least recently used first; `cache: false` opts
out. Keyed by the whole text, never a hash. Synchronous pipelines are cached only once their
error scopes come back clean, so only DIAG builds cache them.
- **`moduleOnly: true`** returns `{ wgsl, module }` without a pipeline; **`p.wgsl`** is the source
the driver was given.
- **`read ` and `uniform ` resource prefixes**: a buffer bound read-only wherever it is declared,
or bound `var<uniform>` with UNIFORM usage.
- **`G.timePasses(fn)`**: a descriptor (e.g. `timestampWrites`) for each compute pass a dispatch
opens.
- `init()` also requests the adapter's `maxStorageBuffersPerShaderStage`.
- `makeCompute` and `makeCompute2D` pass every option through, where they used to keep only `wg`.

**Changed** — DIAG builds push out-of-memory and internal error scopes as well as validation around
pipeline creation. The checks a dispatch or draw makes now run before a pass is opened, so a throw
no longer leaves the frame's encoder with an open pass.

**Fixed** — a dispatch of an async pipeline still compiling, with its resources already set, said
"No resources bound"; it now says the pipeline is still compiling.

**Build** — `build-min` keeps the `export let WEBGPU=()=>{` splice working when esbuild folds the
module-level cache declaration into the factory's statement.

## Unreleased (folded into 0.5.0)

**Fixed — the demo pages load their library again when the site is not at the root of its origin**

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinywebgpu",
"version": "0.4.0",
"version": "0.5.0",
"type": "module",
"description": "One file, no dependencies, no build step. A WebGPU micro-framework for shader-first work: dual-schema pipelines, fullscreen shaders and compute.",
"main": "./src/tinywebgpu.js",
Expand Down
31 changes: 28 additions & 3 deletions src/tinywebgpu.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ export interface PingPong<T> {
}

export interface Pipeline {
pipeline: GPURenderPipeline | GPUComputePipeline;
/** 0 until `ready` settles for an `async: true` pipeline; the pipeline itself otherwise. */
readonly pipeline: GPURenderPipeline | GPUComputePipeline | 0;
/** Resolves to the pipeline once compiled. Already resolved for a synchronous one. */
readonly ready: Promise<GPURenderPipeline | GPUComputePipeline>;
/** The source the driver was given: schema, entry point and G.defines included. */
readonly wgsl: string;
/** Whether the pipeline came from the device's compute pipeline cache. */
readonly cached: boolean;
/** Merge values into the CPU struct and upload. Unknown names throw. */
uniforms: Record<string, number | number[] | ArrayBufferView>;
setUniforms(values: Record<string, number | number[] | ArrayBufferView>): void;
Expand Down Expand Up @@ -125,6 +132,15 @@ export interface MakeDrawOptions {
depth?: Depth | None;
}

export interface ComputeOptions {
/** @workgroup_size, default [8,8,1]. */
wg?: number[];
/** Compile off-thread: `pipeline` is 0 until `ready` settles. */
async?: boolean;
/** false: always compile, and keep nothing in the device's pipeline cache. Default true. */
cache?: boolean;
}

export interface MakeFragOptions {
format?: GPUTextureFormat | GPUTextureFormat[];
blend?: Blend | None;
Expand Down Expand Up @@ -158,19 +174,28 @@ export interface TinyWebGPU {
makeDraw(opts: MakeDrawOptions): RenderPipeline;
/** Compute pipeline: `body` = declarations, `main` = entry-point statements (gid/lid/wid in scope). */
makeCompute(body: string, main: string, uniforms?: UniformSchema, resources?: ResourceSchema,
opts?: { wg?: number[] }): ComputePipeline;
opts?: ComputeOptions & { moduleOnly?: false }): ComputePipeline;
/** `moduleOnly: true`: the WGSL and its shader module as the pipeline would have them, and no pipeline. */
makeCompute(body: string, main: string, uniforms: UniformSchema | None, resources: ResourceSchema | None,
opts: ComputeOptions & { moduleOnly: true }): { wgsl: string; module: GPUShaderModule };
/** makeFrag plus `run(uniforms?, view?)`. All makeFrag options pass through. */
makeQuad(opts: { frag: string; uniforms?: UniformSchema; resources?: ResourceSchema; clear?: Clear } & MakeFragOptions): QuadPipeline;
/** makeCompute with a default grid size, so `run()` with no arguments covers it. */
makeCompute2D(opts: { body: string; decls?: string; uniforms?: UniformSchema; resources?: ResourceSchema;
size?: number[]; wg?: number[] }): ComputePipeline;
size?: number[] } & ComputeOptions): ComputePipeline;

// ─── Frame API ────────────────────────────────────────────────────────────────────────────
/** One encoder + one swapchain view for the whole frame; endFrame() is the single submit. */
beginFrame(opts?: { view?: GPUTextureView }): void;
endFrame(): void;
/** beginFrame/endFrame around `fn`, exception-safe. Returns fn's result. */
frame<T>(fn: () => T, opts?: { view?: GPUTextureView }): T;
/** Per-pass descriptors (e.g. timestampWrites) for each compute pass a dispatch opens itself; null stops. */
timePasses(fn: ((pipeline: ComputePipeline) => GPUComputePassDescriptor | undefined) | None): void;
/** Bounds of the per-device compute pipeline cache: entries, and total characters of their WGSL. */
pipelineCacheMax: number;
pipelineCacheChars: number;

/** Open a chained compute pass: dispatches join it and rebind only on pipeline change. */
beginCompute(opts?: GPUComputePassDescriptor): GPUComputePassEncoder;
endCompute(): void;
Expand Down
Loading
Loading