Update and extend the TypeGPU skill (0.12) (#35)

This commit is contained in:
Konrad Reczko
2026-08-20 09:14:22 +02:00
committed by GitHub
parent c4ac0abdbd
commit 16f16b06fc
15 changed files with 790 additions and 224 deletions
+4
View File
@@ -282,13 +282,17 @@ skills/
└── typegpu/
├── references/
│ ├── advanced.md
│ ├── encoders.md
│ ├── matrices.md
│ ├── noise.md
│ ├── pipelines.md
│ ├── react.md
│ ├── sdf.md
│ ├── setup.md
│ ├── shaders.md
│ ├── std.md
│ ├── textures.md
│ ├── timing.md
│ └── types.md
└── SKILL.md
```
+39 -44
View File
@@ -1,22 +1,23 @@
---
name: typegpu
description: >-
TypeGPU is type-safe WebGPU in TypeScript. Use whenever the user writes, debugs, or designs TypeGPU code: 'use gpu' shader functions, tgpu.fn, buffers, textures, bind groups, compute and render pipelines, vertex layouts, slots, accessors, and any TypeGPU API. Shader logic and CPU-side resources are tightly coupled - handle both sides here even if the user only mentions one (e.g. "how do I write a shader", "how do I create a buffer"). Trigger on any mention of typegpu, tgpu, "use gpu", TypedGPU, or WebGPU code written using TypeGPU's schema API (d.*, tgpu.*, std.*). Do NOT trigger for raw WebGPU (using GPUDevice/GPURenderPipeline directly without tgpu), WGSL-only questions, Three.js, Babylon.js, or WebGL.
TypeGPU is type-safe WebGPU in TypeScript. Use whenever the user writes, debugs, or designs TypeGPU code: 'use gpu' shader functions, tgpu.fn, buffers, textures, bind groups, compute and render pipelines, command encoders, render passes, render bundles, vertex layouts, slots, accessors, @typegpu/react hooks (useRoot, useFrame, useUniform), React Native worklet rendering, and any TypeGPU API. Shader logic and CPU-side resources are tightly coupled - handle both sides here even if the user only mentions one (e.g. "how do I write a shader", "how do I create a buffer"). Trigger on any mention of typegpu, tgpu, "use gpu", TypedGPU, or WebGPU code written using TypeGPU's schema API (d.*, tgpu.*, std.*). Do NOT trigger for raw WebGPU (using GPUDevice/GPURenderPipeline directly without tgpu), WGSL-only questions, Three.js, Babylon.js, or WebGL.
---
# TypeGPU
A single schema (`d.*`) defines a GPU type, CPU buffer layout, and TypeScript type at once - no manual alignment, type mapping, or casting. The build plugin `unplugin-typegpu` transforms `'use gpu'`-marked TypeScript for runtime WGSL transpilation, enabling type inference and polymorphism across the CPU/GPU boundary.
This skill targets TypeGPU `0.11.2`. If the user's project is on an older release, verify API availability before relying on examples or recommended patterns here.
This skill targets TypeGPU `0.12`. If the user's project is on an older release, verify API availability before relying on examples or recommended patterns here.
---
## When to read reference files
**Read before writing virtually any shader or GPU function** — these two cover the rules that trip people up most:
- `references/types.md` — abstract type resolution, exactly when `d.f32()` is required vs redundant, sampler/texture schemas for `tgpu.fn` signatures, CPU-side `TgpuBuffer`/`TgpuTexture` TypeScript types. **If you skip this, you'll hit type errors.**
- `references/shaders.md` full `std` library listing, loops (`std.range`, `tgpu.unroll`), `tgpu.comptime`, outer-scope capture rules, complete builtin reference for all three shader stages, `console.log`. **Read this for any non-trivial shader logic.**
- `references/types.md` — abstract type resolution, exactly when `d.f32()` is required vs redundant, vector constructor overloads, sampler/texture schemas for `tgpu.fn` signatures, CPU-side `TgpuBuffer`/`TgpuTexture` TypeScript types. **If you skip this, you'll hit type errors.**
- `references/shaders.md` — loops (`std.range`, `tgpu.unroll`), ternary/logical-operator semantics, `tgpu.comptime`, outer-scope capture rules, complete builtin reference for all three shader stages, `console.log`. **Read this for any non-trivial shader logic.**
- `references/std.md` — full `std` function listing (math, comparison/boolean vectors, matrix builders, texture, atomics, packing, subgroups, environment probes). Consult before hand-rolling any math/utility function.
**Read when the task specifically involves:**
- `references/pipelines.md` — vertex buffers/layouts, `attribs` wiring, MRT, fullscreen triangle, depth/stencil, blend modes, `fragDepth` output, loading 3D models (`@loaders.gl`), resolve API
@@ -24,23 +25,26 @@ This skill targets TypeGPU `0.11.2`. If the user's project is on an older releas
- `references/textures.md` — texture creation, views, samplers, storage textures, mipmaps, multisampling
- `references/noise.md``@typegpu/noise` (random, distributions, Perlin 2D/3D)
- `references/sdf.md``@typegpu/sdf` (2D/3D primitives, operators, ray marching, AA masking)
- `references/setup.md` — install, `unplugin-typegpu` build plugin, `tsover` operator overloading
- `references/advanced.md` — buffer reinterpretation, indirect drawing/dispatch, custom encoders
- `references/encoders.md` — typed command encoders, multi-pipeline render/compute passes, render bundles, batched submission, raw-WebGPU encoder interop (unstable API, stable behavior)
- `references/timing.md` — GPU timing via timestamp queries: `withPerformanceCallback` vs a shared query set, the `available` guard, why per-pass timings overlap
- `references/react.md``@typegpu/react` hooks (useRoot, useFrame, useUniform, ...), React Native worklet render loops
- `references/setup.md` — TypeGPU CLI, install, `unplugin-typegpu` build plugin, `tsover` operator overloading, troubleshooting
- `references/advanced.md` — buffer reinterpretation, indirect drawing/dispatch, ArrayBuffer IO, minification, warning silencing, `root.unwrap`
---
## Setup
```ts
import tgpu, { d, std, common } from 'typegpu';
import { tgpu, d, std, common } from 'typegpu';
const root = await tgpu.init(); // request a GPU device
const root = tgpu.initFromDevice(device); // or wrap an existing GPUDevice
const root = await tgpu.init(); // request a GPU device
const root = tgpu.initFromDevice({ device }); // or wrap an existing GPUDevice
const context = root.configureContext({ canvas, alphaMode: 'premultiplied' });
```
Create one root at app startup. Resources from different roots cannot interact.
Create one root at app startup. Resources from different roots cannot interact. Teardown: `root.destroy()` destroys all resources created through the root, plus the device itself if the root came from `tgpu.init` (not `initFromDevice`).
---
@@ -50,7 +54,7 @@ A schema defines memory layout and infers TypeScript types; the same schema is u
### Scalars
```ts
d.f32 d.i32 d.u32 d.f16
d.f32 d.i32 d.u32 d.f16 // f16 needs the 'shader-f16' device feature (references/setup.md)
// d.bool is NOT host-shareable - use d.u32 in buffers
```
@@ -60,31 +64,14 @@ d.vec2f d.vec3f d.vec4f // f32
d.vec2i d.vec3i d.vec4i // i32
d.vec2u d.vec3u d.vec4u // u32
d.vec2h d.vec3h d.vec4h // f16
d.vec2b d.vec3b d.vec4b // bool - shader-side only (not host-shareable)
d.mat2x2f d.mat3x3f d.mat4x4f
```
Instance types: `d.vec3f()` -> `d.v3f`, `d.mat4x4f()` -> `d.m4x4f`.
**Vector constructors are richly overloaded - use them.** They compose from any mix of scalars and smaller vectors that adds up to the right component count:
```ts
d.vec3f() // zero-init: (0, 0, 0)
d.vec3f(1) // broadcast: (1, 1, 1)
d.vec3f(1, 2, 3) // individual components
d.vec3f(someVec2, 1) // vec2 + scalar
d.vec3f(1, someVec2) // scalar + vec2
d.vec4f() // zero-init: (0, 0, 0, 0)
d.vec4f(0.5) // broadcast: (0.5, 0.5, 0.5, 0.5)
d.vec4f(rgb, 1) // vec3 + scalar (common: color + alpha)
d.vec4f(v2a, v2b) // two vec2s
d.vec4f(1, uv, 0) // scalar + vec2 + scalar
```
Swizzles (`.xy`, `.zw`, `.rgb`, `.ba`, etc.) return vector instances that work as constructor arguments: `d.vec4f(pos.xy, vel.zw)`.
**Prefer these overloads over manual component decomposition.** Instead of `d.vec3f(v.x, v.y, newZ)`, write `d.vec3f(v.xy, newZ)`.
**Vector constructors are richly overloaded** — they compose from any mix of scalars, smaller vectors, and swizzles that adds up to the right component count (`d.vec4f(rgb, 1)`, `d.vec3f(v.xy, newZ)`). Prefer them over manual component decomposition; full overload listing in `references/types.md`.
### Compound types
```ts
@@ -180,13 +167,7 @@ const myFragment = tgpu.fragmentFn({
Vertex `in` may include builtins: `d.builtin.vertexIndex`, `d.builtin.instanceIndex`.
Full shader syntax, branch pruning, the `std` library, and type inference: see `references/shaders.md`.
---
**Values vs references** — the most common source of `ResolutionError`. See `references/shaders.md`.
**Idiomatic patterns** (vector ops, struct constructors, register pressure): see `references/shaders.md`.
Full shader syntax, branch pruning, the `std` library, type inference, and idiomatic patterns (vector ops, struct constructors, register pressure): see `references/shaders.md`. Read it before any non-trivial shader — values-vs-references handling lives there and is the most common source of `ResolutionError`.
---
@@ -219,7 +200,7 @@ buf.write(12);
| `'uniform'` | `var<uniform>` |
| `'storage'` | `var<storage, read>` (or `read_write` with `access: 'mutable'`) |
| `'vertex'` | vertex input, paired with `tgpu.vertexLayout` |
| `'index'` | index buffer (`d.u16` or `d.u32` schema only) |
| `'index'` | index buffer (array of `d.u16` or `d.u32` only) |
| `'indirect'` | indirect dispatch/draw |
All buffers get `COPY_SRC | COPY_DST` automatically. `$addFlags(GPUBufferUsage.X)` adds any flag not covered by `$usage`.
@@ -255,7 +236,7 @@ planetBuffer.patch({
**`common.writeSoA(buffer, { field: Float32Array, ... })`** - scatter separate packed per-field arrays into the GPU's AoS layout with correct padding. The idiomatic path for particle systems, simulations, and model loading where CPU data is already field-separated. See `references/matrices.md` for examples and `references/pipelines.md` for the model-loading pattern.
**GPU-side copy:** `destBuffer.copyFrom(srcBuffer)` (schemas must match).
**GPU-side copy:** `destBuffer.copyFrom(srcBuffer)` (schemas must match). **Zeroing:** `buffer.clear()`. **Cleanup:** `buffer.destroy()`. Both `copyFrom` and `clear` take an optional command encoder — see `references/encoders.md`.
### Reading
@@ -275,6 +256,8 @@ const bufReadonly = root.createReadonly(d.arrayOf(d.f32, N)); // var<
Access inside shaders via `particles.$`, `config.$`. Prefer fixed resources by default; switch to manual bind groups when you need to swap resources per frame, manage `@group` indices, or share layouts across pipelines.
A manually created buffer converts to the same kind of binding with `buffer.as('uniform' | 'readonly' | 'mutable')` (requires the matching `$usage`) — use it when you hold a `TgpuBuffer` but need `.$` access in a shader.
---
## Bind group layouts (manual binding)
@@ -299,6 +282,8 @@ const bindGroup = root.createBindGroup(layout, {
pipeline.with(bindGroup).dispatchWorkgroups(N);
```
Buffer bindings from `createUniform`/`createMutable`/`createReadonly` are accepted directly as entries (no need to unwrap to a buffer).
Explicit `@group` index (only needed when integrating with raw WGSL that hardcodes group indices): `layout.$idx(0)`.
---
@@ -375,6 +360,12 @@ Full MRT example, per-target blend/writeMask config, and the `fragDepth` footgun
For vertex buffer layouts, the attribs spread trick, and the `common.fullScreenTriangle` helper: `references/pipelines.md`.
### Batching work
`draw()`/`dispatchWorkgroups()` each record and submit their own single-pipeline pass. To run several pipelines in one pass (shared attachments) or batch several passes into one submission, use the typed command encoder — `root['~unstable'].createCommandEncoder()``beginRenderPass`/`beginComputePass``pipeline.with(pass).draw(...)``pass.end()``encoder.submit()`. Render bundles and raw-WebGPU encoder interop too: see `references/encoders.md`.
Pipelines initialize lazily on first use; `pipeline.initSync()` / `await pipeline.initAsync()` move that cost to a loading screen (see `references/pipelines.md`).
---
## GPU-scoped variables
@@ -385,7 +376,7 @@ For vertex buffer layouts, the attribs spread trick, and the `common.fullScreenT
## Slots
`tgpu.slot<T>()` is a typed placeholder; fill with `.with(slot, value)` at pipeline, root, or function scope. Any type fits: GPU values, functions, callbacks. Slots are the idiomatic way to build configurable/reusable shaders.
`tgpu.slot<T>()` is a typed placeholder; fill with `.with(slot, value)` at root scope (before pipeline creation) or function scope — pipelines do not accept slots in `.with()`. Any type fits: GPU values, functions, callbacks. Slots are the idiomatic way to build configurable/reusable shaders.
```ts
const distFnSlot = tgpu.slot<(pos: d.v3f) => number>();
@@ -410,7 +401,7 @@ Scalar/vector slot with a default:
```ts
const colorSlot = tgpu.slot(d.vec4f(1, 0, 0, 1));
pipeline.with(colorSlot, d.vec4f(0, 1, 0, 1)).draw(3);
root.with(colorSlot, d.vec4f(0, 1, 0, 1)).createRenderPipeline({ ... });
```
---
@@ -438,7 +429,7 @@ Write access: `tgpu.mutableAccessor(schema, initial?)`.
## Type utilities
`d.InferInput<typeof Schema>` — CPU-side type accepted by `.write()`. `d.InferGPU<typeof Schema>` — type inside `'use gpu'` functions. `AnyData` (from `'typegpu'`) — broadest schema constraint for generics. Full buffer/texture TypeScript types (`TgpuBuffer`, `TgpuUniform`, `TgpuTexture`, usage flags): `references/types.md`.
`d.InferInput<typeof Schema>` — CPU-side type accepted by `.write()`. `d.InferGPU<typeof Schema>` — type inside `'use gpu'` functions. `d.AnyData` (also importable from `'typegpu/data'`) — broadest schema constraint for generics. Full buffer/texture TypeScript types (`TgpuBuffer`, `TgpuUniform`, `TgpuTexture`, usage flags): `references/types.md`.
---
@@ -449,8 +440,8 @@ Write access: `tgpu.mutableAccessor(schema, initial?)`.
3. **TypedArray/ArrayBuffer alignment**: bytes copied verbatim. `vec3f` elements are 16 bytes (12 + 4 padding). Plain arrays handle padding; typed arrays must include it.
4. **Integer division**: `a / b` on primitives is `f32`. Use `d.i32()`/`d.u32()` for integer semantics. See types.md.
5. **Uninitialised variables**: `let x;` is invalid - always initialise so the type can be inferred: `let x = d.f32(0)`.
6. **Ternary operators**: runtime ternaries aren't supported. Use `std.select(falseVal, trueVal, condition)`.
7. **Fragment output is always `d.vec4f`**, even for fewer-channel formats. A pipeline with `targets: { format: 'r8unorm' }` or `'rg16float'` still requires `out: d.vec4f` and `return d.vec4f(...)`. WebGPU drops the unused channels.
6. **Ternary operators**: runtime ternaries compile to WGSL `select` — both branches always evaluate, so branches must be side-effect-free and scalar/vector-valued (no structs/arrays/matrices; use `if`/`else` for those). Comptime-known conditions prune the dead branch entirely. See shaders.md.
7. **Fragment output is always 4-component** (`d.vec4f`; `d.vec4i`/`d.vec4u` for integer formats), even for fewer-channel formats. A pipeline with `targets: { format: 'r8unorm' }` or `'rg16float'` still requires `out: d.vec4f` and `return d.vec4f(...)`. WebGPU drops the unused channels.
---
@@ -460,4 +451,8 @@ Write access: `tgpu.mutableAccessor(schema, initial?)`.
- **`@typegpu/sdf`** - 2D/3D signed distance primitives (`sdDisk`, `sdBox2d`, `sdRoundedBox2d`, `sdBezier`, `sdSphere`, `sdBox3d`, `sdCapsule`, `sdPlane`, ...) and operators (`opUnion`, `opSmoothUnion`, `opSmoothDifference`, `opExtrudeX/Y/Z`). All `tgpu.fn` with pinned types, callable directly from `'use gpu'`. For ray marching, UI masking, AA vector drawing. See `references/sdf.md`.
- **`@typegpu/react`** - hooks for TypeGPU in React and React Native (`useRoot`, `useFrame`, `useUniform`, ...), including UI-thread render loops via `react-native-worklets`. See `references/react.md`.
- **TypeGPU CLI** - `npx typegpu@latest` scaffolds a new project; `--enhance` retrofits TypeGPU into an existing one. See `references/setup.md`.
- **[`wgpu-matrix`](https://github.com/greggman/wgpu-matrix)** - canonical math library for TypeGPU. TypeGPU vectors/matrices can be passed as `dst` to `wgpu-matrix` calls to avoid allocations. See `references/matrices.md` for full integration patterns.
+53 -10
View File
@@ -6,22 +6,24 @@ Pass an existing `GPUBuffer` as `initialData` to create a TypeGPU buffer aliasin
```ts
const packedBuffer = root
.createBuffer(d.arrayOf(d.unorm8x4))
.createBuffer(d.disarrayOf(d.unorm8x4, N)) // compact vertex data
.$usage('vertex');
// Add STORAGE to the underlying GPUBuffer:
// Add STORAGE to the underlying GPUBuffer BEFORE it is materialized:
packedBuffer.$addFlags(GPUBufferUsage.STORAGE);
// Alias same memory, typed as u32 storage:
const storageView = root.createBuffer(d.arrayOf(d.u32), packedBuffer.buffer);
// Alias the same memory, typed as u32 storage:
const storageView = root
.createBuffer(d.arrayOf(d.u32, N), packedBuffer.buffer)
.$usage('storage'); // TypeGPU-level usability + typing; no effect on GPU flags
```
Pairs well with WGSL pack/unpack builtins (`std.pack4x8unorm`, `std.unpack4x8unorm`, `std.pack2x16float`, etc.) - reinterpret a buffer as `u32` storage and pack/unpack in the shader for compact vertex data, color encoding, or quantized weights.
**Caveats:**
- The original buffer's lifecycle is NOT transferred - keep it alive while the alias is in use.
- `$usage()` and `$addFlags()` cannot be called on the aliased buffer.
- The original must have all needed usage flags before the alias is created.
- Real GPU flags cannot be applied through the alias: accessing `.buffer` materializes the `GPUBuffer` with its flags baked, so the original must carry every needed raw flag (via `$usage`/`$addFlags`) before the alias is created. `$addFlags()` on the alias throws.
- `$usage()` on the alias IS still required for each intended use - it never touches the GPU flags, but it satisfies TypeScript (`StorageFlag` etc.) and TypeGPU's runtime bind checks (`.as(...)`, bind group creation).
---
@@ -86,9 +88,51 @@ pipeline.drawIndirect(MyBuffer, offset);
---
## Custom command encoders
## Command encoders
TypeGPU supports passing an existing `GPUCommandEncoder` or active `GPURenderPassEncoder`/`GPUComputePassEncoder` via `.with(encoder)` or `.with(pass)`, allowing TypeGPU calls to interleave with raw WebGPU commands in a shared command buffer.
Typed command encoders, multi-pipeline passes, render bundles, and raw WebGPU encoder interop are covered in `references/encoders.md`.
---
## CPU-side serialization without buffers
Top-level exports mirror `buffer.write`/`patch`/`read` but operate on a raw `ArrayBuffer` with an explicit schema — useful for pre-serializing schema-shaped data (workers, files, staging). All three are **synchronous** (pure CPU-side (de)serialization — no `await`, unlike `buffer.read()`):
```ts
import { d, writeToArrayBuffer, patchArrayBuffer, readFromArrayBuffer } from 'typegpu';
const bytes = new ArrayBuffer(64);
writeToArrayBuffer(bytes, d.vec4u, d.vec4u(1, 2, 3, 4));
// slice write: same { startOffset } option + d.memoryLayoutOf as buffer.write
patchArrayBuffer(bytes, Boids, { 2: { pos: d.vec2u() } });
const value = readFromArrayBuffer(bytes, d.mat4x4f);
```
---
## Raw WGSL escape hatches
Never reach for these unless integrating with an existing raw-WGSL codebase specifically requires it - TypeGPU code should express everything else through typed APIs. They are the sanctioned paths for injecting hand-written WGSL (plain strings are not shader values):
**`tgpu['~unstable'].rawCodeSnippet(expression, type, origin?, possibleSideEffects?)`** — a typed WGSL expression usable inside shaders (e.g. referencing a variable you know exists in the final bundle). `origin` defaults to `'runtime'`; `possibleSideEffects` defaults to `true`.
**`tgpu['~unstable'].declare(source)`** — emits a WGSL declaration whenever a depending object resolves (diagnostic directives, hand-written bindings). Reference TypeGPU resources with `.$uses({...})` (at most once); use slots/accessors when dependencies need to vary.
```ts
const declaration = tgpu['~unstable']
.declare('@group(0) @binding(0) var<uniform> settings: Settings;')
.$uses({ Settings });
```
---
## Minification and warnings
**Shader minification**`tgpu.init({ unstable_minify: true })` minifies generated WGSL for that root; `tgpu.resolve(objs, { unstable_minify: true })` for standalone resolves. The build plugin additionally offers `unstable_obfuscate: true` (renames identifiers in emitted metadata; requires `autoNamingEnabled: false`).
**Silencing warnings**`import { warn } from 'typegpu'`; `warn.disable('implicit-conversion')` silences one warning type, `warn.reset()` restores defaults. Prefer fixing the cause.
**Shared resolution namespaces**`tgpu['~unstable'].namespace()` passed as `{ names }` to several `tgpu.resolve` calls shares one naming/declaration scope (no duplicate declarations across chunks). Stateful: later chunks may omit declarations emitted earlier, so don't treat each result as self-contained.
---
@@ -111,11 +155,10 @@ const gpuSampler = root.unwrap(tgpuSampler); // GPUSampler
`root.device` gives the underlying `GPUDevice` directly.
**Forced initialization.** All TypeGPU resources are lazy — buffers (even those with initial data), pipelines, and shader compilation all defer until first use. **This is usually exactly what you want.** `unwrap` forces a resource to initialize immediately, which can be useful in specific situations where you want explicit control over when the work happens (e.g. a loading screen):
**Forced initialization.** All TypeGPU resources are lazy — buffers (even those with initial data), pipelines, and shader compilation all defer until first use. **This is usually exactly what you want.** For pipelines, use the explicit `pipeline.initSync()` / `await pipeline.initAsync()` (see `references/pipelines.md`); for buffers and textures, `unwrap` is the forcing mechanism:
```ts
// Optional: force init during a loading screen rather than on first use
root.unwrap(computePipeline);
root.unwrap(particleBuffer); // initial data written here instead of on first dispatch
```
+95
View File
@@ -0,0 +1,95 @@
# Command Encoders, Passes, and Render Bundles
> **Unstable API.** These live on `root['~unstable']` — the surface may change between minor releases. If a call documented here errors or doesn't typecheck, verify the signature against the installed `typegpu` version (its `.d.ts` or docs) before debugging elsewhere.
By default, `pipeline.draw()` and `pipeline.dispatchWorkgroups()` each record their own single-pipeline pass and submit it immediately. Encoders are for the cases that need more control: **several pipelines in one pass** (shared attachments, e.g. scene + lights + sky into one MSAA target) and **several passes in one submission**.
## Typed command encoder and render passes
```ts
const encoder = root['~unstable'].createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: msaaTexture, // TypeGPU texture/view, canvas context, or GPUTextureView
resolveTarget: context,
}],
depthStencilAttachment: {
view: depthTexture,
},
});
scenePipeline.with(pass).draw(mesh.vertexCount);
lightPipeline.with(pass).draw(6, lightCount);
skyPipeline.with(pass).draw(3);
pass.end();
encoder.submit();
```
Descriptor conveniences over raw `GPURenderPassDescriptor`:
- Attachment `view` accepts TypeGPU textures, texture views, and canvas contexts, as well as raw `GPUTextureView`s.
- `loadOp` / `storeOp` / `depthClearValue` default to `'clear'` / `'store'` / `1`.
- A single color attachment doesn't need to be wrapped in an array.
- `occlusionQuerySet` / `timestampWrites` accept TypeGPU query sets.
## Two equivalent execution styles
`pipeline.with(pass).draw(...)` keeps the pipeline-centric API (all `with*` methods available). Alternatively the pass mirrors `GPURenderPassEncoder`, accepting TypeGPU resources:
```ts
pass.setPipeline(renderPipeline);
pass.setBindGroup(bindGroup);
pass.setVertexBuffer(vertexLayout, vertexBuffer);
pass.draw(3);
```
Both styles share one pass state, applied lazily at draw time and following WebGPU ordering rules (state persists until overwritten). Footgun: `pipeline.with(pass).draw(...)` sets the pass's current pipeline — a subsequent bare `pass.draw(...)` runs *that* pipeline, not one set earlier via `setPipeline`.
## Compute passes
```ts
const pass = encoder.beginComputePass();
computePipeline.with(pass).dispatchWorkgroups(16);
pass.end();
encoder.submit();
```
Caveat: guarded compute pipelines (`createGuardedComputePipeline` / `dispatchThreads`) cannot record into passes or encoders — each `dispatchThreads` submits on its own.
## `submit()` vs `finish()`
- `encoder.submit()` finishes and submits to the device queue; shader `console.log` output and performance callbacks are processed as part of that submission.
- `encoder.finish()` returns the raw `GPUCommandBuffer` for manual `device.queue.submit([...])` batching — TypeGPU never sees that submission, so **logs and performance callbacks are not processed**.
## Render bundles
Pre-record a static draw sequence once, replay it cheaply every frame:
```ts
const bundleEncoder = root['~unstable'].createRenderBundleEncoder({
colorFormats: ['rgba8unorm'], // must match the pass it will run in
});
scenePipeline.with(bundleEncoder).draw(vertexCount);
const bundle = bundleEncoder.finish();
// each frame:
pass.executeBundles([bundle]);
```
## Raw WebGPU interop
Pipelines also accept raw WebGPU encoders via `.with(...)`:
- `pipeline.with(gpuCommandEncoder)` — TypeGPU opens and ends the needed pass; you finish/submit the encoder.
- `pipeline.with(gpuRenderPass | gpuComputePass)` — state is applied to the existing pass without ending it; you end it.
- `pipeline.with(gpuRenderBundleEncoder)` — records draws into the bundle; you finish it.
Same logging caveat as `finish()`: TypeGPU can't process shader logs or perf callbacks for submissions it doesn't own.
Escape hatch in the other direction: `root.unwrap(encoder)` / `root.unwrap(pass)` return the raw `GPUCommandEncoder` / pass encoder (e.g. for texture copies). Commands recorded raw are invisible to TypeGPU, so after unwrapping a pass, the next typed draw re-applies its full state.
## Encoder-aware buffer ops
`buffer.clear(encoder)` and `dst.copyFrom(src, encoder)` record into the given encoder instead of submitting immediately — use them to fold buffer maintenance into the same submission as your passes.
+6 -2
View File
@@ -35,6 +35,8 @@ vec3.normalize(dir, dir); // in-place
> Requires `wgpu-matrix >= 3.3.0`.
For building matrices *inside shaders* (no CPU round-trip), `std` has `identity2/3/4`, `translation4`, `scaling4`, `rotationX4/Y4/Z4`, `transpose`, `determinant` — see `references/std.md`.
Without `dst`, `wgpu-matrix` allocates a new `Float32Array` per call. In a render loop doing 3-6 matrix ops per frame, that's 200+ allocations/sec - enough for GC stutters, just like per-frame `createView`/`createBindGroup`. Allocate once at setup, reuse forever.
---
@@ -45,7 +47,7 @@ WGSL matrices are column-major in memory. Key implications:
- **Constructor order.** `d.mat4x4f(c0, c1, c2, c3)` takes four columns. If you expected row-major (numpy/HLSL), you'll get a transpose.
- **`mat * vec` is column x column.** `M * v` applies `M`'s transform. Composition: `projection * view * model * position`.
- **Shader element access.** `mat[i]` is not allowed - use `mat.columns[i]` for the i-th column, `mat.columns[c][r]` for an element.
- **Shader element access.** `mat[i]` is not allowed - use `mat.columns[i]` for the i-th column, `mat.columns[c][r]` for an element. (Flat `mat[i]` works only on CPU-side instances, which is what makes wgpu-matrix interop possible - not inside `'use gpu'`.)
- **`Float32Array` layouts** (raw byte writes):
| Schema | Floats | Layout |
@@ -123,10 +125,12 @@ function updateCamera(eye: Float32Array, target: Float32Array, up: Float32Array,
mat4.invert(view, viewInv);
mat4.perspective(Math.PI / 4, aspect, 0.1, 1000, proj);
mat4.invert(proj, projInv);
cameraBuffer.write(raw); // bytes straight through - no serialization
cameraBuffer.write(raw.buffer); // bytes straight through - no serialization
}
```
For struct schemas, `.write()` accepts a record or an `ArrayBuffer` - pass `raw.buffer`, not the `Float32Array` view (vector/matrix/array schemas accept TypedArrays directly).
Layout notes:
- `mat4x4f` is 16 floats each, packed - subarrays `(0,16)`, `(16,32)`, etc. align cleanly.
- `vec3f` or `mat3x3f`: leave WGSL padding (4 floats per `vec3f`, 4 per `mat3x3f` column). TypeGPU does **not** add padding for `TypedArray`/`ArrayBuffer` - it copies verbatim.
+11 -7
View File
@@ -6,26 +6,24 @@
import { randf, perlin2d, perlin3d } from '@typegpu/noise';
```
Works in TypeGPU shaders (auto-linked on pipeline resolve) and in raw WGSL via `tgpu.resolve({ template, externals: { randf } })`.
Works in TypeGPU shaders (auto-linked on pipeline resolve) and in raw WGSL via `tgpu.resolve({ template, externals: { randf } })` - see `references/pipelines.md` for the full resolve API.
## PRNG (`randf`)
`randf.sample()` returns a uniform `f32` in `[0, 1)`. Each thread has its own generator state - **seed each thread differently** or they all produce the same sequence (usually a bug). Seed once at the top from something thread-unique (pixel position, global invocation id, hashed instance index).
`randf.sample()` returns a uniform `f32` in `[0, 1)`. The default generator is **xoroshiro64\*\*** (seeds are hashed internally, so seed magnitude doesn't matter). Each thread has its own generator state - **seed each thread differently** or they all produce the same sequence (usually a bug). Seed once at the top from something thread-unique (pixel position, global invocation id, hashed instance index).
```ts
const main = tgpu.fragmentFn({
in: { pos: d.builtin.position },
out: d.vec4f,
})(({ pos }) => {
randf.seed2(pos.xy.mul(0.001)); // unique per pixel; keep magnitude small
randf.seed2(pos.xy); // unique sequence per pixel
const r = randf.sample();
const g = randf.sample();
return d.vec4f(r, g, 0, 1);
});
```
**Seed magnitude matters.** Float precision means large seeds repeat quickly. Keep seeds in `[-1000, 1000]`, ideally `[0, 1]`. For pixel coordinates, multiply by `~0.001`; for global invocation ids, divide by dispatch size.
### Seed functions
| Function | Seed type |
@@ -35,7 +33,11 @@ const main = tgpu.fragmentFn({
| `randf.seed3(v)` | `d.v3f` |
| `randf.seed4(v)` | `d.v4f` |
Canonical compute-shader pattern: `seed2(globalInvocationId.xy / dispatchSize.xy)`.
Canonical compute-shader pattern: `seed2(d.vec2f(gid.xy))`.
A seed built only from thread-unique values makes every dispatch replay the identical sequence (fine for static noise, a bug for animation). For per-frame variation, mix a time or frame-count uniform into the seed: `randf.seed3(d.vec3f(pos.xy, time.$))`.
The generator is swappable via `randomGeneratorSlot`: `root.with(randomGeneratorSlot, gen)`
## Distributions
@@ -87,7 +89,7 @@ const main = tgpu.fragmentFn({
in: { pos: d.builtin.position },
out: d.vec4f,
})(({ pos }) => {
const n = perlin2d.sample(pos.xy.mul(0.05)); // "interesting" scale ~1 unit/cell
const n = perlin2d.sample(pos.xy * 0.05); // "interesting" scale ~1 unit/cell
return d.vec4f(n * 0.5 + 0.5, 0, 0, 1); // remap [-1,1] to [0,1]
});
```
@@ -109,6 +111,8 @@ const pipeline = root
.createComputePipeline({ compute: main });
```
`root.pipe(transform)` applies a configuration function (a bundle of slot/accessor bindings, like `cache.inject()` returns) to the root's configuration chain - equivalent to the corresponding `.with(...)` calls.
Inside `main`, `perlin3d.sample(pos)` reads from the cache automatically - no shader code change. Sampling wraps at the domain boundary. Use `perlin2d.staticCache({ root, size: d.vec2u(...) })` for 2D.
### Dynamic cache
+57 -9
View File
@@ -1,5 +1,20 @@
# TypeGPU Pipelines and Vertex Buffers
## Contents
- **Vertex layouts** — `tgpu.vertexLayout`, step modes, compact formats for `unstruct` / `disarrayOf`
- **Wiring layouts into a render pipeline** — the `attribs` spread
- **Binding vertex buffers**
- **Loading 3D models** — `@loaders.gl`, scattering attributes with `common.writeSoA`
- **Index buffers**
- **Depth / stencil**
- **Multiple render targets (MRT)** — named-record `out`, single-target shorthand, per-target blend and writeMask, custom `fragDepth` output, name-matching rules, why fragment output is always 4-component
- **`common.fullScreenTriangle`**
- **Pipeline initialization** — `initSync` / `initAsync` and moving cost to a loading screen
- **GPU timing** — pointer to `timing.md`
- **Multiple pipelines per pass / batched submission** — pointer to `encoders.md`
- **Resolve API** — WGSL code generation
## Vertex layouts
`tgpu.vertexLayout(schemaFn, stepMode?)` describes how a vertex buffer maps to shader `in` parameters.
@@ -141,7 +156,7 @@ pipeline
.drawIndexed(6);
```
Only `d.u16` and `d.u32` schemas are valid.
Only arrays of `d.u16` or `d.u32` are valid.
---
@@ -297,9 +312,9 @@ pipeline
- Keys in `withColorAttachment` must match `targets`. TypeScript rejects missing/extra entries.
- Shaders authored with explicit `d.location(0, ...)` respect your manual indices (as long as they don't conflict with vertex-side locations).
### Fragment output is always `d.vec4f`
### Fragment output is always 4-component
Even for formats with fewer than 4 channels (`r8unorm`, `rg16float`), the output is still `d.vec4f`. WebGPU drops unused channels.
Even for formats with fewer than 4 channels (`r8unorm`, `rg16float`), the output is still `d.vec4f` (`d.vec4i`/`d.vec4u` for integer formats). WebGPU drops unused channels.
```ts
const luminanceFrag = tgpu.fragmentFn({
@@ -313,7 +328,7 @@ root.createRenderPipeline({
});
```
This is a WebGPU rule, not a TypeGPU one - but it surprises people used to frameworks that derive output types from texture formats.
TypeGPU's fragment `out` accepts only 4-component vectors (plus builtins) - it does not derive narrower output types from texture formats.
---
@@ -338,13 +353,46 @@ pipeline.withColorAttachment({ view: context }).draw(3);
---
## Resolve API (WGSL code generation)
## Pipeline initialization
Generate the complete WGSL for a set of functions/pipelines - useful for debugging or integrating with other tools:
Pipelines are lazy: shader resolution, module creation, and WebGPU pipeline creation all happen on the first `draw`/`dispatchWorkgroups`. To move that cost to a moment you control (e.g. a loading screen):
```ts
const wgsl = tgpu.resolve([pipeline]);
console.log(wgsl);
pipeline.initSync(); // start initialization now (JS side + issue device work)
await pipeline.initAsync(); // additionally wait until the device finishes - fully
// avoids the first-use stall
```
All transitive dependencies (helpers, layouts, buffers, constants) are included automatically.
For small/medium shaders this makes no noticeable difference - use it only for large or numerous shaders.
---
## GPU timing
`pipeline.withPerformanceCallback((start: bigint, end: bigint) => ...)` for quick one-off measurement; `root.createQuerySet('timestamp', n)` + `pipeline.withTimestampWrites(...)` for durable multi-pass timing. Requires the `timestamp-query` device feature; without it a warning is logged and timing is skipped. Allocation behavior, the `available` guard, and how to interpret overlapping pass timings: `references/timing.md`.
---
## Multiple pipelines per pass / batched submission
`draw()` and `dispatchWorkgroups()` each record and submit their own single-pipeline pass. To batch several pipelines into one render/compute pass, or several passes into one submission, use the typed command encoder API (`root['~unstable'].createCommandEncoder()`) - see `references/encoders.md`. Render bundles and raw-WebGPU encoder interop live there too.
---
## Resolve API (WGSL code generation)
`tgpu.resolve` generates complete WGSL, with all transitive dependencies (helpers, layouts, buffers, constants) included and deduplicated automatically:
```ts
const wgsl = tgpu.resolve([pipeline]); // debugging, tooling, shader inspection
```
Several pipelines can be resolved in one call; all items must originate from the same root (mixing roots throws).
There's also a raw-WGSL interop form, `tgpu.resolve({ template, externals })` - each `externals` entry (even objects with member access, e.g. `randf.sample`) becomes available inside the WGSL `template` string. Niche; only for injecting TypeGPU objects into existing hand-written WGSL.
Options: `names: 'strict'` (default - generated names closely match JS identifiers, sanitized and suffixed on conflict; override per-object with `.$name('...')`) or `'random'`.
Related, for advanced integration (see `references/advanced.md` for namespaces):
- `tgpu.resolveWithContext(...)` additionally returns `usedBindGroupLayouts` and a `catchall` bind group.
- `tgpu['~unstable'].namespace()` shares one naming/declaration scope across multiple resolve calls.
+109
View File
@@ -0,0 +1,109 @@
# `@typegpu/react` - React and React Native Bindings
Hooks for creating and managing TypeGPU resources in React components. The same import works on web and React Native — the package's `react-native` export condition selects the RN build automatically.
```sh
npm install @typegpu/react
```
## Minimal complete example
```tsx
import { useMemo } from 'react';
import { d, common } from 'typegpu';
import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react';
function MyEffect() {
const root = useRoot();
const time = useUniform(d.f32);
const renderPipeline = useMemo(
() =>
root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: ({ uv }) => {
'use gpu';
return d.vec4f((uv * 5 + time.$) % 1, 0, 1);
},
}),
[root, time],
);
const { ref, ctxRef } = useConfigureContext();
useFrame(({ elapsedSeconds }) => {
if (!ctxRef.current) return;
time.write(elapsedSeconds);
renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
});
return <canvas ref={ref} />;
}
```
## Hook reference
| Hook | Purpose |
|---|---|
| `useRoot()` | Root from the nearest `<Root>` provider (global root if none). Suspends until initialized; throws on init failure. `useRootOrError` / `useRootWithStatus` for manual handling. |
| `<Root root?>` | Optional context provider; all descendants share one `TgpuRoot`. Creates one on demand if `root` not given. |
| `useConfigureContext(opts?)` | Returns `{ ref, ctxRef }` — pass `ref` to the `<canvas>`, use `ctxRef.current` as the attachment view. |
| `useFrame(cb)` | Runs `cb` every frame (rAF) with `{ deltaSeconds, elapsedSeconds }`. No `useCallback` needed — latest closure values are visible. |
| `useUniform(schema, opts?)` | Uniform buffer binding tied to component lifetime; update via `.write()` outside the React lifecycle. Options: `initial`, `onInit`. |
| `useMutable(schema, opts?)` | Same, `var<storage, read_write>`. |
| `useReadonly(schema, opts?)` | Same, `var<storage, read>`. |
| `useMirroredUniform(schema, value)` | Uniform re-synced to `value` on every React re-render — for values living in the React lifecycle (theme, props). |
| `useBuffer(schema, opts?)` | Raw buffer equivalent of the above. |
| `useBindGroup(layout, entries)` | Bind group; API matches `root.createBindGroup`. |
| `ClientOnly` | SSR guard — render GPU components only on the client. |
Guidance:
- **Memoize pipelines** with `useMemo` and `[root, ...capturedResources]` deps — pipeline creation implies shader resolution.
- **Per-frame values go through `.write()` inside `useFrame`**, never through React state (state updates re-render and can re-create resources).
- `useUniform` vs `useMirroredUniform`: updated-every-frame → `useUniform` + `.write()`; derived from React data → `useMirroredUniform`.
## React Native worklets (UI-thread render loops)
With `react-native-worklets` installed, `useFrame` callbacks run on the UI thread, unaffected by RN-thread load. Setup (after the usual RN/webgpu setup) — babel config:
```js
const workletsPluginOptions = {
bundleMode: true,
importForwarding: {
moduleNames: ['typegpu'],
relativePaths: ['my-app/components'], // dirs with module-scope shader definitions
},
};
// plugins: ['unplugin-typegpu/babel', ['react-native-worklets/plugin', workletsPluginOptions]]
```
Clear the Metro cache after changing babel config (`npx expo start --clear`).
No extra imports — `@typegpu/react` detects `react-native-worklets` at runtime. While active:
- **Every `useFrame` callback must start with the `'worklet'` directive** and runs on the UI thread; a plain callback throws. Opt out with `<Root disableWorklets>`.
- Call `ctx.present?.()` after drawing (RN canvas contexts need an explicit present).
```tsx
useFrame(({ elapsedSeconds }) => {
'worklet';
const ctx = ctxRef.current;
if (!ctx) return;
color.write(d.vec3f(0.5 + Math.sin(elapsedSeconds) * 0.5, 0.447, 0.941));
pipeline.withColorAttachment({ view: ctx }).draw(3);
ctx.present?.();
});
```
### Rules of transfer
Resources captured by a worklet are transferred to the UI runtime automatically on first use; both runtimes share the same GPU objects.
- **Transfers**: buffers (incl. `createUniform`/`createMutable`/`createReadonly`), textures, samplers, bind groups + layouts, vertex layouts, query sets, pipelines, roots, slots, accessors, consts.
- **Does NOT transfer**: shader definitions (`tgpu.fn`, entry functions, `tgpu.comptime`) and standalone schemas/vector instances. Keep definitions at module scope in files covered by `importForwarding` — worklets re-import them natively.
- **Pipelines carrying attachments/passes throw on transfer** — transfer the bare pipeline and call `withColorAttachment` on the worklet side (as above).
- **Callbacks crossing runtimes** (e.g. `withPerformanceCallback`) must themselves be `'worklet'`-marked.
- **Pipelines created on the UI thread need an explicit `targets: { format: '...' }`** — `navigator.gpu.getPreferredCanvasFormat()` is unavailable on worklet runtimes.
- Query sets: `resolve()` and `read()` on one runtime only; shader logs print only on the runtime that resolved the pipeline.
+45 -7
View File
@@ -99,18 +99,56 @@ Notes:
- **Pack extra channels.** `rgba16float` can hold `(dist, progressAlongCurve, normalX, normalY)`.
- **Re-bake only when source geometry changes.** Static fields: bake once at startup.
## Jump flooding — SDF texture from raster content
When the source is pixels rather than an analytic SDF (painted strokes, glyphs, a rendered mask), `createJumpFlood` builds the distance field with the Jump Flood Algorithm. You supply shader callbacks; the executor owns the ping-pong textures and compute passes:
```ts
import { createJumpFlood } from '@typegpu/sdf';
const runner = createJumpFlood({
root,
size: { width: 512, height: 512 },
classify: (coord, size) => { // true = inside the shape
'use gpu';
return std.textureLoad(srcLayout.$.source, coord, 0).w > 0.5;
},
getSdf: (coord, size, signedDist) => { // signedDist in pixels, negative inside
'use gpu';
return signedDist / d.f32(std.min(size.x, size.y)); // e.g. normalize
},
getColor: (coord, size, signedDist, insidePx, outsidePx) => {
'use gpu';
return std.textureLoad(srcLayout.$.source, insidePx, 0); // nearest inside pixel
},
}).with(sourceBindGroup); // .with(bindGroup) supplies resources the callbacks read
runner.run();
runner.sdfOutput; // rgba16float texture (distance in .x), 'storage' + 'sampled'
runner.colorOutput; // rgba8unorm texture from getColor
```
`runner.initSync()`/`await runner.initAsync()` pre-initialize the pipelines; `runner.destroy()` frees the executor's textures. This is the input format `@typegpu/radiance-cascades` consumes for 2D global illumination.
## Bounding shapes for early-out
For unions over many sources (particles, agents, instanced obstacles), reject with a cheap bounding distance before expensive per-source calculation:
```ts
for (let i = d.u32(0); i < activeCount; i++) {
const src = sources.$[i];
const rough = std.length(point - src.center) - src.radius;
if (rough * k > 7) { continue; } // exp(-k * rough) < 1e-3 past this
const exact = expensiveSourceSDF(point, src);
accum += std.exp(-k * exact);
}
// Given: sources = root.createReadonly(d.arrayOf(Source, MAX)) where Source has
// center/radius fields, count = active source count, k = smooth-blend sharpness.
const fieldAt = tgpu.fn([d.vec3f, d.u32], d.f32)((point, count) => {
'use gpu';
let accum = d.f32(0);
for (let i = d.u32(0); i < count; i++) { // classic for: runtime bound
const src = sources.$[i];
const rough = std.length(point - src.center) - src.radius;
if (rough * K > 7) { continue; } // exp(-k * rough) < 1e-3 past this
const exact = expensiveSourceSDF(point, src);
accum += std.exp(-K * exact);
}
return accum;
});
```
Two common forms:
+63 -34
View File
@@ -1,5 +1,20 @@
# TypeGPU Project Setup
## Fastest path: TypeGPU CLI
For a **new project** or when **adding TypeGPU to an existing one**, prefer the CLI over the manual steps below - it handles the install, build plugin, and types in one go:
```sh
npx typegpu@latest # scaffold a new project (interactive)
npx typegpu@latest my-app --yes # non-interactive, defaults
npx typegpu@latest my-app --yes --template vite-react --addons @typegpu/sdf,@typegpu/noise
npx typegpu@latest --enhance # retrofit TypeGPU into the current project
```
Templates: `vite-bare`, `vite-complex`, `vite-react`, `expo-bare`. `--enhance` installs `typegpu`, wires up the build plugin, adds `@webgpu/types`, and can install the TypeGPU AI skill.
The manual steps below are useful for understanding what the CLI sets up, and for repairing a broken setup.
## 1. Install TypeGPU
```sh
@@ -27,6 +42,38 @@ Add to `tsconfig.json`:
---
## GPU features (`tgpu.init` options)
Optional device capabilities are requested at init; `d.f16`/`vec*h` need `shader-f16`, subgroup ops need `subgroups`, GPU timing needs `timestamp-query`:
```ts
const root = await tgpu.init({
adapter: { powerPreference: 'high-performance' }, // GPURequestAdapterOptions
device: {
requiredFeatures: ['shader-f16'], // init throws if unavailable
optionalFeatures: ['timestamp-query'], // requested when available
},
});
root.enabledFeatures.has('timestamp-query'); // ReadonlySet<GPUFeatureName>
```
**Every `requiredFeatures` entry shrinks the set of devices the app runs on** — init fails outright on hardware without it. Require a feature only when that trade-off is deliberate. Otherwise request it via `optionalFeatures` and write both paths, branching on a captured `root.enabledFeatures.has(...)` result. The result is comptime-known, so branch pruning emits only reachable statements for the selected path:
```ts
const hasF16 = root.enabledFeatures.has('shader-f16');
const process = (x: number) => {
'use gpu';
if (hasF16) {
return fastF16Path(x); // only the taken branch survives in WGSL
}
return f32Path(x); // emitted only when this path remains reachable
};
```
---
## 3. Build plugin - `unplugin-typegpu` (required for `'use gpu'`)
The `'use gpu'` directive and JS/TS shader functions need the build plugin. Without it, TypeGPU functions implemented in TypeScript won't work.
@@ -77,9 +124,9 @@ The plugin also auto-names TypeGPU resources from variable names, improving debu
---
## 4. Operator overloading - `tsover` (highly recommended)
## 4. Operator overloading - `tsover` (the default TypeGPU way)
`tsover` is a drop-in TypeScript replacement adding operator overloading (`+ - * / %` on vectors and matrices). Without it, the IDE treats `d.vec3f() * 2` as a type error, even though it compiles and runs.
`tsover` is a drop-in TypeScript replacement adding operator overloading (`+ - * / %` on vectors and matrices). It is technically optional — without it the code still compiles and runs, but the IDE treats `d.vec3f() * 2` as a type error. **Treat it as part of the standard setup**: operators are the idiomatic TypeGPU style, so install `tsover` unless the user explicitly declines or the project has concrete TypeScript language-server constraints that rule it out; only then fall back to infix methods (`.add()`, `.mul()`).
**Note:** `unplugin-typegpu` already handles runtime operator overloads inside `'use gpu'` functions — no bundler plugin needed for shader code. `tsover` adds IDE type-checking support and enables operators outside `'use gpu'` blocks (CPU-side vector math).
@@ -107,17 +154,13 @@ For monorepos, add overrides:
Match major.minor: if your project uses `typescript@5.8.x`, use `tsover@5.8.x`.
### Configure
### Enable in code
Add `"tsover"` to `lib` in `tsconfig.json`:
No `tsconfig.json` changes. Operators typecheck inside `'use gpu'` functions as-is once tsover is the project's TypeScript. For CPU-side code outside `'use gpu'`, add a `'use tsover'` directive at file or function scope:
```json
{
"compilerOptions": {
"types": ["@webgpu/types"],
"lib": ["tsover", "DOM", "ES2022"]
}
}
```ts
'use tsover'; // file-level; or place inside a single function
const c = a + b; // d.v2f + d.v2f
```
### Bundler plugin (only for CPU-side operators)
@@ -145,35 +188,21 @@ Zed: set `tsdk` in `.zed/settings.json` for `vtsls` or `typescript-language-serv
---
## Minimal `vite.config.js`
## Troubleshooting
```js
import { defineConfig } from 'vite';
import typegpu from 'unplugin-typegpu/vite';
**`'use gpu'` silently does nothing / `ResolutionError` about an untranspiled function** - the build plugin isn't running on that file. This is the #1 setup failure. Check, in order:
1. The plugin is in the right config for your bundler (`unplugin-typegpu/vite` in `vite.config.js` vs `'unplugin-typegpu/babel'` in `babel.config.js`).
2. The file matches the plugin's `include` pattern (default `[/\.m?[jt]sx?$/]`).
3. The dev server was restarted after adding the plugin; on React Native, clear the Metro cache (`npx expo start --clear`).
4. If the `tgpu` import is aliased unusually, set `forceTgpuAlias`.
export default defineConfig({
plugins: [typegpu()],
});
```
**IDE flags `vec * scalar` as a type error while the code runs fine** - `tsover` isn't active in the editor: select the workspace TypeScript version (see above); for CPU-side code also check the `'use tsover'` directive is present.
## Minimal `tsconfig.json`
```json
{
"compilerOptions": {
"types": ["@webgpu/types"],
"lib": ["tsover", "DOM", "ES2022"],
"strict": true
}
}
```
---
## Lint plugin - `eslint-plugin-typegpu`
Highlights common pitfalls and unsupported syntax in `'use gpu'` functions.
Optional, but highly recommended.
Use unless told otherwise, or unless the project uses incompatible linter.
Include a `lint` script in `package.json`.
Highlights common pitfalls and unsupported syntax in `'use gpu'` functions. Optional but highly recommended - use it unless told otherwise or the project's linter is incompatible, and include a `lint` script in `package.json`.
```sh
npm install --save-dev eslint-plugin-typegpu
+63 -100
View File
@@ -1,5 +1,25 @@
# TypeGPU Shader Authoring
## Contents
- **`tgpu.fn` vs plain callback** — when to pin a signature; WGSL-implemented bodies as an escape hatch
- **Polymorphism and branch pruning** — one function, many WGSL variants
- **Syntax limitations inside `'use gpu'`** — unsupported TS features; ternaries; `&&` and `||`
- **Register pressure** — why large locals cost occupancy
- **Arithmetic operators** — `+ - * / %` on vectors/matrices, `tsover`, infix fallbacks
- **Numeric literal gotcha** — when `1.0` degrades to `abstractInt`
- **Do not assign textures or samplers to variables** — use them directly
- **Iteration** — `for...of`, `std.range`, `tgpu.unroll` (numeric ranges, supported iterables)
- **Arrays inside shaders**
- **`tgpu.comptime`** — compile-time evaluation and pruning
- **Shader entrypoints** — `tgpu.computeFn`, `tgpu.vertexFn`, `tgpu.fragmentFn`, with the builtin lists
- **Outer-scope capture** — captured values are inlined as WGSL literals
- **`std` standard library** — pointer to the full listing
- **`console.log` in shaders**
- **GPU-scoped variables** — `workgroupVar`, `privateVar`, `const`
- **Values vs references** — the most common source of `ResolutionError`
- **Idiomatic shader code** — vector ops, struct constructors
## `tgpu.fn` vs plain callback
| | Plain callback | `tgpu.fn` |
@@ -23,6 +43,16 @@ const scale2D = tgpu.fn([d.vec2f, d.f32], d.vec2f)((v, factor) => {
});
```
### WGSL-implemented functions (escape hatch — prefer TS bodies)
`tgpu.fn` also accepts a WGSL body as a template literal. **Write function bodies in TypeScript by default**; the WGSL form is only for edge cases — a WGSL builtin `std` doesn't expose, or dropping in existing WGSL verbatim. External TypeGPU objects referenced in the body are wired in with `.$uses({...})` (callable at most once per function):
```ts
const getGradient = tgpu.fn([d.f32], d.vec3f)`(t) {
return mix(startColor, endColor, t);
}`.$uses({ startColor, endColor });
```
---
## Polymorphism and branch pruning
@@ -44,7 +74,9 @@ area(d.vec2f(3, 4)); // fn area_vec2f(shape: vec2f) -> f32 { return shape.x *
area(d.vec3f(3, 4, 5)); // fn area_vec3f(shape: vec3f) -> f32 { return shape.x * shape.y * shape.z; }
```
Branch pruning also applies to compile-time-known captured values - `if` conditions resolvable at compile time keep only the winning branch in WGSL. This is how you write configurable shaders with no runtime overhead.
Branch pruning also applies to compile-time-known captured values - `if` (and ternary) conditions resolvable at compile time, including inequality comparisons, keep only the winning branch in WGSL. This is how you write configurable shaders with no runtime overhead.
Pruning follows terminating control flow. When a compile-time-selected branch returns, statements after the `if` are emitted only for specializations where they remain reachable. This makes both an explicit `if`/`else` and an early-return form valid for comptime two-path splits.
> **Caution:** each unique type combination generates a new WGSL function. Calling the same polymorphic function with many signatures bloats output. Use `tgpu.fn` to pin the signature when polymorphism isn't needed.
@@ -54,7 +86,6 @@ Branch pruning also applies to compile-time-known captured values - `if` conditi
| Unsupported | Alternative |
|---|---|
| `a ? b : c` (runtime ternary) | `std.select(falseVal, trueVal, condition)` |
| Object/array spreading (`{...obj}`) | Build the result manually |
| Inline functions / arrow fns | Define outside and capture |
| Most Web APIs | Compile-time constants only (`Math.PI` is fine) |
@@ -62,22 +93,30 @@ Branch pruning also applies to compile-time-known captured values - `if` conditi
| `async`/`await`, `Promise` | Not supported |
| `try`/`catch` | Not supported |
| `let x;` without initializer | `let x = d.f32(0)` - type must be inferrable |
| Update as expression (`const a = i++`) | Split into two statements |
| `$uses()` called more than once | Merge into a single call |
### Ternaries
- **Comptime-known condition** (captured JS value, slot, `comptime` result - equality *and* inequality comparisons): the dead branch is pruned from WGSL entirely.
- **Runtime condition**: lowered to WGSL `select(falseVal, trueVal, cond)`. `select` evaluates **both** branches, so branches must be side-effect-free (no assignments, increments, effectful calls) and produce scalar or vector values - no structs, arrays, or matrices. Use `if`/`else` for anything heavier.
- `std.select(falseVal, trueVal, cond)` is the explicit equivalent: branches are limited to scalars/vectors, and both arguments always evaluate.
### `&&` and `||`
When the left-hand side is comptime-known (a slot, simple accessor, or captured external), the expression is evaluated at compile time - JS short-circuit semantics apply and the dead side is pruned from WGSL. Otherwise both operands must be booleans and the operators compile to WGSL's strictly-boolean `&&`/`||` - value-returning JS idioms like `maybeVec || fallback` don't work at runtime; use `std.select` or `if`.
---
## Register pressure
When the GPU runs out of registers per thread it spills to slow memory, which can crater performance. Modern shader compilers (LLVM, SPIR-V, DXC) are smart — SSA optimisation means naming an intermediate `const` costs nothing, and aggressive inlining erases function-call boundaries before register allocation. Don't contort your code trying to outsmart them.
The one thing compilers can't fix: **variable liveness**. A `mat4x4f` holds 16 registers for its entire live range. If you compute it at the top of a function and only use it at the bottom, those registers are locked out for everything in between. Compute large values close to where they're consumed.
Vector ops and swizzles are still the right style — not because they pack registers differently (modern GPU hardware is scalar underneath), but because they express the math directly and let the compiler see the whole operation at once.
When the GPU runs out of registers per thread it spills to slow memory, which can crater performance. Modern shader compilers are smart — naming an intermediate `const` costs nothing (SSA), and inlining erases call boundaries before register allocation — so don't contort your code to outsmart them. The one thing they can't fix is **variable liveness**: a `mat4x4f` holds 16 registers for its entire live range, so compute large values close to where they're consumed. Vector ops and swizzles are the right style because they express the math directly.
---
## Arithmetic operators
With `tsover`, `+ - * / %` work on scalars, vectors, and matrices. Infix methods (`.add()`, `.mul()`) and `std` functions (`std.dot`, `std.mod`) are alternatives.
`+ - * / %` on scalars, vectors, and matrices are the idiomatic TypeGPU style, used throughout this skill. Inside `'use gpu'` they always work at runtime (the build plugin handles them); `tsover` is what makes the IDE/typechecker accept them and enables them CPU-side — set it up by default (see `references/setup.md`). Infix methods (`.add()`, `.mul()`) and `std` functions (`std.dot`, `std.mod`) are the fallback for projects that genuinely can't use `tsover`.
```ts
const a = d.vec3f(1, 2, 3);
@@ -89,17 +128,13 @@ const dot = std.dot(a, b); // 32
Division on primitives defaults to `f32`. Integer division: `d.i32(10 / 3)`.
Bitwise and shift operators (`& | ^ << >> >>>`) work on integer scalars and vectors; `>>>` requires a `u32` left-hand side (where it's the same as `>>`).
---
## Numeric literal gotcha
`.0` suffixes may be stripped by bundlers before transpilation:
```ts
let x = 1.0; // BAD: "1.0" may become "1" -> abstract integer / i32
let x = d.f32(1); // OK
let y = 1.1; // OK - fractional part prevents integer interpretation
```
`.0` suffixes may be stripped by bundlers before transpilation: `let x = 1.0` can become `1` → abstract integer. Use `let x = d.f32(1)` (a fractional part like `1.1` is safe). Details in `references/types.md`.
---
@@ -128,6 +163,8 @@ for (const item of items.$) {
}
```
Classic C-style loops also work, and are the tool for runtime bounds: `for (let i = d.u32(0); i < count; i++) { ... }` (`std.range` below is comptime-only).
### `std.range` - numeric ranges for loops
Generates a sequence of integers. Three forms:
@@ -144,7 +181,7 @@ Used directly in `for...of`, compiles to a WGSL `for` loop:
for (const i of std.range(0, 8, 2)) {
result += data.$[i];
}
// -> for (var i = 0i; i < 8i; i += 2i) { ... }
// -> for (var i = 0u; i < 8u; i += 2u) { ... } (i32 when any bound/step is negative)
```
Descending ranges work with negative step: `std.range(10, -10, -1)`.
@@ -161,9 +198,9 @@ for (const dy of tgpu.unroll([-1, 0, 1])) {
}
```
Hard rules: **no `continue` or `break` inside an unrolled loop**, and the length must be known at compile time.
Hard rules: **no `continue` or `break` targeting the unrolled loop itself** (a nested runtime loop inside the body may use them), and the length must be known at compile time.
**Warning — register spill.** Unrolling generates straight-line code, and the GPU register file is finite. Too many iterations means the compiler spills registers to slow memory, which can tank performance worse than the loop overhead you were avoiding. As a rough ceiling: **keep unrolled counts under ~816 for anything inside a hot shader; ~27 is an upper bound.** If you find yourself unrolling more than that, a regular `for...of` with `std.range` is almost certainly the better call.
**Warning — register spill.** Unrolled code is straight-line and the register file is finite. Keep unrolled counts roughly under ~816 in hot shaders (~27 as an upper bound); beyond that, a regular `for...of` with `std.range` is almost certainly better.
#### Unrolling a numeric range
@@ -191,17 +228,7 @@ for (const i of tgpu.unroll(std.range(FBM_OCTAVES))) {
| Iterable in a variable | `const arr = [1,2,3]; tgpu.unroll(arr)` | Indexed access into WGSL `array<...>` - still unrolled |
| Buffers / accessors / `const` / `comptime` | `tgpu.unroll(acc.$)` | Works whenever length is known at compile time |
#### Conditional unrolling
Branch on a JS-side flag to pick between unrolled and regular - both branches are statically resolved, only one survives in WGSL:
```ts
const shouldUnroll = true;
for (const x of shouldUnroll ? tgpu.unroll(arr) : arr) {
r += x;
}
```
Conditional unrolling: `for (const x of shouldUnroll ? tgpu.unroll(arr) : arr)` - the JS-side flag is statically resolved, only one form survives in WGSL.
---
@@ -241,6 +268,8 @@ const material = tgpu.fn([d.vec3f], d.vec3f)((diffuse) => {
The function passed to `tgpu.comptime` runs in JS - any JS APIs are fair game, but the return value must be a schema-typed value TypeGPU can embed. Comptime calls are not cached - each call is evaluated separately.
Related: `tgpu.lazy(fn)` also runs JS at resolution time, but wraps a single deferred value (accessed via `.$`) instead of a callable - use it when a captured value isn't known at module-eval time.
---
## Shader entrypoints
@@ -256,6 +285,8 @@ tgpu.computeFn({
wgid: d.builtin.workgroupId, // vec3u
lidx: d.builtin.localInvocationIndex, // u32
nwg: d.builtin.numWorkgroups, // vec3u
gidx: d.builtin.globalInvocationIndex, // u32 - flat global thread index
wgidx: d.builtin.workgroupIndex, // u32 - flat workgroup index
},
})((input) => { 'use gpu'; });
```
@@ -305,81 +336,13 @@ const check = (x: number) => { 'use gpu'; return x > threshold; };
Anything that changes at runtime must go through a buffer, uniform, slot, or accessor.
Captures aren't limited to plain identifiers - nested member access resolves too: `matrix.columns[0]`, `Math.sin`, `config.colors[2]`, and private class fields (`this.#field`) in class-owned functions.
---
## `std` standard library
`std` wraps WGSL built-in functions ([WGSL spec section 16](https://www.w3.org/TR/WGSL/#builtin-functions)) plus some TypeGPU additions. The WGSL documentation applies.
```ts
import { std } from 'typegpu';
```
**Math**
```ts
std.abs std.sign std.floor std.ceil std.round std.fract std.trunc
std.sqrt std.inverseSqrt
std.exp std.exp2 std.log std.log2 std.pow
std.min std.max std.clamp(x, lo, hi)
std.mix(a, b, t) // linear interpolation
std.smoothstep(edge0, edge1, x)
std.step(edge, x)
std.select(falseVal, trueVal, cond)
```
**Trig**
```ts
std.sin std.cos std.tan std.asin std.acos std.atan std.atan2
std.sinh std.cosh std.tanh std.degrees std.radians
```
**Vector / matrix**
```ts
std.dot(a, b) std.cross(a, b) std.length(v)
std.normalize(v) std.distance(a, b)
std.reflect(i, n) std.refract(i, n, eta)
std.faceForward(n, i, nRef)
std.mul(mat, vec) // matrix-vector multiply
```
**Texture** (see sampling rules below)
```ts
std.textureSample(view.$, sampler.$, uv)
std.textureSampleLevel(view.$, sampler.$, uv, mipLevel)
std.textureSampleGrad(view.$, sampler.$, uv, ddx, ddy)
std.textureLoad(view.$, coords, mipLevel)
std.textureStore(storageView.$, coords, value)
std.textureDimensions(view.$)
```
**Atomic**
```ts
std.atomicLoad(ptr) std.atomicStore(ptr, val)
std.atomicAdd(ptr, val) std.atomicSub(ptr, val)
std.atomicMin(ptr, val) std.atomicMax(ptr, val)
std.atomicAnd(ptr, val) std.atomicOr(ptr, val) std.atomicXor(ptr, val)
std.atomicExchange(ptr, val)
std.atomicCompareExchangeWeak(ptr, cmp, val)
```
**Packing**
```ts
std.pack4x8snorm(v) std.unpack4x8snorm(x)
std.pack4x8unorm(v) std.unpack4x8unorm(x)
std.pack2x16snorm(v) std.unpack2x16snorm(x)
std.pack2x16unorm(v) std.unpack2x16unorm(x)
std.pack2x16float(v) std.unpack2x16float(x)
```
---
## Texture sampling rules
Sampling function reference table: see `references/textures.md`.
- `textureSample` fails with a WGSL validation error if called inside a branch/loop whose condition depends on per-pixel data (fragment-only, must be uniform).
- `textureSampleLevel` does **not** use implicit derivatives — pass `0` for single-mip textures, or the level you want. Any stage, non-uniform OK.
- `textureSampleGrad` is the way to get auto-LOD in compute or non-uniform branches.
`std` (`import { std } from 'typegpu'`) wraps WGSL built-in functions plus TypeGPU additions (environment probes, `std.copy`, `std.bitcast`, comparison/boolean vector helpers, in-shader matrix builders). Full function listing: `references/std.md`. Texture sampling stage/uniformity rules: `references/textures.md`.
---
+111
View File
@@ -0,0 +1,111 @@
# TypeGPU `std` Library Reference
`std` wraps WGSL built-in functions ([WGSL spec section 16](https://www.w3.org/TR/WGSL/#builtin-functions)) plus some TypeGPU additions. The WGSL documentation applies.
```ts
import { std } from 'typegpu';
```
The listing below is the commonly used subset — `std` exposes the WGSL builtin set under the same names (e.g. `fma`, `saturate`, `modf`, `frexp`, `ldexp`, `quantizeToF16`, `countOneBits`, `reverseBits`, `extractBits`, `insertBits`, `firstLeadingBit`, `firstTrailingBit` all exist).
**Math**
```ts
std.abs std.sign std.floor std.ceil std.round std.fract std.trunc
std.sqrt std.inverseSqrt
std.exp std.exp2 std.log std.log2 std.pow
std.min std.max std.clamp(x, lo, hi)
std.mod(a, b)
std.mix(a, b, t) // linear interpolation
std.smoothstep(edge0, edge1, x)
std.step(edge, x)
std.select(falseVal, trueVal, cond)
std.copy(x) // schema-agnostic deep copy (works when exact schema is generic)
std.bitcast(d.u32, d.f32)(x) // reinterpret bits; any scalar/vector pair of equal size
```
**Comparison / boolean** — componentwise on vectors, returning bool vectors (`d.vec2b`/`d.vec3b`/`d.vec4b`); combine with `std.select` for vectorized branching
```ts
std.eq std.ne std.lt std.le std.gt std.ge // componentwise -> vecNb
std.all(bv) std.any(bv) std.allEq(a, b)
std.and(bv, bv) std.or(bv, bv) std.not(bv)
```
**Trig**
```ts
std.sin std.cos std.tan std.asin std.acos std.atan std.atan2
std.sinh std.cosh std.tanh std.degrees std.radians
```
**Vector / matrix**
```ts
std.dot(a, b) std.cross(a, b) std.length(v)
std.normalize(v) std.distance(a, b)
std.reflect(i, n) std.refract(i, n, eta)
std.faceForward(n, i, nRef)
std.mul(mat, vec) // matrix-vector multiply
std.transpose(m) std.determinant(m)
std.identity2/3/4() std.translation4(v) std.scaling4(v) // build matrices in-shader
std.rotationX4(rad) std.rotationY4(rad) std.rotationZ4(rad)
```
**Arrays**
```ts
std.arrayLength(arr) // runtime-sized storage array -> WGSL arrayLength;
// fixed-size array -> folds to the constant
```
**Texture** — stage/uniformity rules and the sampling reference table live in `references/textures.md`. Short version: `textureSample` is fragment-only and must be in uniform control flow; `textureSampleLevel` works in any stage; `textureSampleGrad` gives auto-LOD in compute/non-uniform branches.
```ts
std.textureSample(view.$, sampler.$, uv)
std.textureSampleLevel(view.$, sampler.$, uv, mipLevel)
std.textureSampleGrad(view.$, sampler.$, uv, ddx, ddy)
std.textureSampleBias(view.$, sampler.$, uv, bias)
std.textureSampleBaseClampToEdge(view.$, sampler.$, uv)
std.textureSampleCompare(depthView.$, comparisonSampler.$, uv, ref) // + CompareLevel
std.textureGather(component, view.$, sampler.$, uv)
std.textureLoad(view.$, coords, mipLevel)
std.textureStore(storageView.$, coords, value)
std.textureDimensions(view.$)
```
**Fragment control**
```ts
std.discard() // fragment-only; discards the fragment (WGSL `discard`)
```
**Derivatives** (fragment only)
```ts
std.dpdx(v) std.dpdy(v) std.fwidth(v) // + Coarse/Fine variants of each
```
**Synchronization** (compute only)
```ts
std.workgroupBarrier() std.storageBarrier()
```
**Atomic**
```ts
std.atomicLoad(ptr) std.atomicStore(ptr, val)
std.atomicAdd(ptr, val) std.atomicSub(ptr, val)
std.atomicMin(ptr, val) std.atomicMax(ptr, val)
std.atomicAnd(ptr, val) std.atomicOr(ptr, val) std.atomicXor(ptr, val)
// atomicExchange / atomicCompareExchangeWeak are NOT exposed
```
**Packing** (the full WGSL set is not exposed — only these four)
```ts
std.pack4x8unorm(v) std.unpack4x8unorm(x)
std.pack2x16float(v) std.unpack2x16float(x)
```
**Subgroups** (require the `subgroups` device feature — see `references/setup.md`)
```ts
std.subgroupAdd / Mul / Min / Max / And / Or / Xor (value)
std.subgroupExclusiveAdd / ExclusiveMul / InclusiveAdd / InclusiveMul (value)
std.subgroupAll / Any (bool) std.subgroupBallot(bool) std.subgroupElect()
std.subgroupBroadcast(value, lane) std.subgroupBroadcastFirst(value)
std.subgroupShuffle / ShuffleUp / ShuffleDown / ShuffleXor (value, x)
// builtins: d.builtin.subgroupId, subgroupSize, subgroupInvocationId, numSubgroups
```
**Environment probes** - branch on where the code is running (e.g. CPU fallback path vs generated shader): `std.isBeingTranspiled()`, `std.getTargetShaderLanguage()` (`'wgsl'` during generation, `undefined` otherwise), `std.getShaderStage()` (`'vertex' | 'fragment' | 'compute' | undefined`).
+41 -10
View File
@@ -16,31 +16,40 @@ const tex = root.createTexture({
## Usage flags
```ts
.$usage('sampled') // TEXTURE_BINDING - read via textureSample/textureLoad
.$usage('storage') // STORAGE_BINDING - read/write as storage texture
.$usage('render') // RENDER_ATTACHMENT - render targets and resampling writes
.$usage('sampled') // TEXTURE_BINDING - read via textureSample/textureLoad
.$usage('storage') // STORAGE_BINDING - read/write as storage texture
.$usage('render') // RENDER_ATTACHMENT - render targets and image-source writes
.$usage('transient') // TRANSIENT_ATTACHMENT | RENDER_ATTACHMENT - attachments never read back
// (e.g. MSAA/depth intermediates); cannot combine with 'sampled'/'storage'
```
Multiple: `.$usage('sampled', 'render')`.
Escape hatch: `.$overrideFlags(GPUTextureUsage...)` replaces the inferred flags with raw WebGPU ones (don't call `$usage` after it).
---
## Writing data
```ts
// Accepts: ImageBitmap, ImageData, HTMLCanvasElement, HTMLVideoElement,
// HTMLImageElement, or an array of them (for array textures).
await texture.write(imageBitmap);
// Image sources: ImageBitmap, ImageData, HTMLCanvasElement, HTMLVideoElement,
// HTMLImageElement, OffscreenCanvas, VideoFrame - or an array of them
// (one per layer for array/3D textures; each must match the layer size).
// Image-source writes require 'render' usage.
texture.write(imageBitmap); // source size must match - throws otherwise
texture.write(imageBitmap, { fit: 'stretch' }); // resample the source to the texture size
// If source size != texture size, 'render' usage is required (TypeGPU resamples).
// If source size === texture size, 'render' is not needed.
// Raw binary data: ArrayBuffer, TypedArray, or DataView ('render' not needed).
// Bytes are copied verbatim in the texture's format layout (e.g. 4 bytes/pixel for rgba8unorm).
texture.write(new Uint8Array([255, 0, 0, 255 /* ...one entry per pixel */]));
texture.write(mipData, 1); // optional second arg: target mip level
```
---
## Mipmap generation
Requires `mipLevelCount > 1` and `'render'` usage.
2D textures only; requires `'render'` usage (throws without it). With `mipLevelCount: 1` the call is a warning + no-op.
```ts
texture.generateMipmaps(); // all levels from level 0
@@ -57,6 +66,8 @@ texture.clear(); // write zeros to all mip levels
texture.clear(mipLevel); // specific mip level
```
**Cleanup:** `texture.destroy()`.
---
## Texture views
@@ -149,9 +160,14 @@ const sampler = root.createSampler({
mipmapFilter?: 'nearest' | 'linear',
lodMinClamp?: number,
lodMaxClamp?: number,
compare?: GPUCompareFunction, // for comparison samplers
maxAnisotropy?: number, // 1-16
});
// Comparison sampler (shadow mapping) - separate constructor, `compare` required:
const shadowSampler = root.createComparisonSampler({
compare: 'less', // GPUCompareFunction
magFilter: 'linear',
});
```
In bind group layouts: `{ sampler: 'filtering' | 'non-filtering' | 'comparison' }`.
@@ -192,3 +208,18 @@ const layout = tgpu.bindGroupLayout({
});
// Compute shader: std.textureStore(layout.$.output, coords, d.vec4f(r, g, b, 1));
```
### Depth comparison (shadow mapping)
```ts
const layout = tgpu.bindGroupLayout({
shadowMap: { texture: d.textureDepth2d() },
shadowSampler: { sampler: 'comparison' },
});
// Shader: std.textureSampleCompare(layout.$.shadowMap, layout.$.shadowSampler, uv, refDepth)
// returns the comparison result in [0, 1]; textureSampleCompareLevel for explicit-LOD variants.
```
### External texture (video)
Layout entry `{ frame: { externalTexture: d.textureExternal() } }`, bound to a `GPUExternalTexture` (`device.importExternalTexture({ source: video })`); sample with `std.textureSampleBaseClampToEdge`.
+56
View File
@@ -0,0 +1,56 @@
# GPU Timing (Timestamp Queries)
Requires the `timestamp-query` device feature (see `references/setup.md`) — gate all timing code on `root.enabledFeatures.has('timestamp-query')` and no-op without it. All timestamps are nanosecond `bigint`s.
**Timestamps are quantized by default in most environments** as a timing-attack mitigation ([WebGPU spec, device/queue timing](https://www.w3.org/TR/webgpu/#security-timing-device)) — Chrome rounds them to 100 µs, so very fast pipelines read as 0 or noise. Full precision requires opting out per browser; in Chrome that's `chrome://flags/#enable-webgpu-developer-features`, which is a bundle, not a timestamp-only switch: it also removes the timing-attack mitigation, exposes extended adapter information (driver, backend, memory heaps — a device-fingerprinting surface), and enables other non-standard developer features. Inform the user of that full trade-off before recommending it; it is for local development/testing only and can never be a production requirement.
## Quick timing: `withPerformanceCallback`
```ts
const pipeline = root
.createComputePipeline({ compute: computeShader })
.withPerformanceCallback((start, end) => {
console.log(`took ${Number(end - start)} ns`);
});
```
- Callback signature `(start: bigint, end: bigint) => void | Promise<void>`; calling `.withPerformanceCallback()` again replaces the previous callback — attach once after pipeline creation.
- **Each pipeline with a callback allocates its own query set** (plus resolve buffers), and resolves + reads back per submission. Four timed pipelines = four query sets and four readbacks. Fine for quick, temporary measurement of a pipeline or two; for a durable setup (perf HUD, profiler across many passes) use one shared query set instead (below).
- Works when the pipeline records into an encoder via `pipeline.with(encoder)` (callback fires after `encoder.submit()`), but **not** when drawing into a shared pass — timestamp writes are part of the pass descriptor. There, pass `timestampWrites` to `encoder.beginRenderPass`/`beginComputePass`.
## Durable timing: one shared query set
Create one query set sized for all tracked passes and give each pass a begin/end index pair:
```ts
const querySet = root.createQuerySet('timestamp', 2 * PASS_COUNT);
// pass i writes to slots 2i / 2i+1:
const timedPipeline = pipeline.withTimestampWrites({
querySet,
beginningOfPassWriteIndex: 2 * i,
endOfPassWriteIndex: 2 * i + 1, // omit either index to skip that write
});
```
Pass descriptors take the same shape: `encoder.beginRenderPass({ ..., timestampWrites })` accepts a `TgpuQuerySet` directly; a raw WebGPU encoder needs `root.unwrap(querySet)`.
Timestamp *writes* are near-free, so tracked passes can be timed on every dispatch. The expensive part is the readback — `resolve()` + `read()` (a buffer map). Sample at a low rate (a HUD refresh a few times per second), and one readback covers every tracked pass:
```ts
if (querySet.available) { // false while a previous read is in flight
querySet.resolve();
const ts: bigint[] = await querySet.read();
const passMs = Number(ts[2 * i + 1] - ts[2 * i]) / 1e6;
}
```
- **Always check `querySet.available` before `resolve()`/`read()`** — both throw while a previous read is still in progress (CPU-side buffer mapping regularly outlives a GPU frame). Keep at most one read in flight.
- `read()` without a prior `resolve()` throws or returns stale data.
- A pass that didn't run since the last sample keeps its previous timestamps — track the last seen end value per slot and report 0 when it hasn't changed, rather than repeating the stale duration.
## Interpreting the numbers: passes overlap
GPUs run passes with no resource dependency between them concurrently. Per-pass durations therefore double-count shared wall time — **the sum of per-pass times inside an encoder is usually more than the actual GPU time for the frame**. Treat per-pass numbers as relative attribution, not as budget shares.
The true GPU wall time of a frame is the span from the earliest `begin` to the latest `end` across all of the frame's slots — compute that from the same readback when the total matters.
+37 -1
View File
@@ -23,6 +23,28 @@ So `d.f32(0.88)` as an arithmetic operand is always redundant — write `0.88`.
Type annotations are stripped before transpilation. WGSL type comes from the runtime value — the constructor called, the buffer schema, or the abstract literal type. `let x: d.v3f` still errors; the annotation does nothing.
## Vector constructors are richly overloaded — use them
They compose from any mix of scalars and smaller vectors that adds up to the right component count:
```ts
d.vec3f() // zero-init: (0, 0, 0)
d.vec3f(1) // broadcast: (1, 1, 1)
d.vec3f(1, 2, 3) // individual components
d.vec3f(someVec2, 1) // vec2 + scalar
d.vec3f(1, someVec2) // scalar + vec2
d.vec4f() // zero-init: (0, 0, 0, 0)
d.vec4f(0.5) // broadcast: (0.5, 0.5, 0.5, 0.5)
d.vec4f(rgb, 1) // vec3 + scalar (common: color + alpha)
d.vec4f(v2a, v2b) // two vec2s
d.vec4f(1, uv, 0) // scalar + vec2 + scalar
```
Swizzles (`.xy`, `.zw`, `.rgb`, `.ba`, etc.) return vector instances that work as constructor arguments: `d.vec4f(pos.xy, vel.zw)`.
**Prefer these overloads over manual component decomposition.** Instead of `d.vec3f(v.x, v.y, newZ)`, write `d.vec3f(v.xy, newZ)`.
---
## Samplers and textures — three contexts, different syntax
@@ -41,9 +63,11 @@ const sampleColor = (samp: d.sampler, tex: d.texture2d<d.F32>, uv: d.v2f) => {
'use gpu';
return std.textureSample(tex, samp, uv);
};
```
```ts
// tgpu.fn — factory calls in the schema array:
const sampleColor = tgpu.fn([d.sampler(), d.texture2d(d.f32), d.vec2f], d.vec4f)(
const sampleColorFn = tgpu.fn([d.sampler(), d.texture2d(d.f32), d.vec2f], d.vec4f)(
(samp, tex, uv) => { 'use gpu'; return std.textureSample(tex, samp, uv); }
);
```
@@ -81,6 +105,10 @@ const lut: TgpuReadonly<typeof LutArray> = root.createReadonly(LutArray);
`typeof Schema` is the idiomatic generic argument — `Config`, `ParticleArray`, etc. are schema objects created with `d.struct(...)` or `d.arrayOf(...)`.
These bindings are also accepted directly as `root.createBindGroup` entries (matching a `uniform`/`storage` layout entry with the right access) — no need to reach for the underlying buffer.
The same binding types are reachable from a manually created buffer: `buffer.as('uniform' | 'readonly' | 'mutable')` returns a `TgpuUniform`/`TgpuReadonly`/`TgpuMutable` (requires the matching `$usage`) — the bridge when you hold a `TgpuBuffer` but need `.$` access in a shader. Runtime type guards: `isBufferBinding`, `isUniformBinding`, `isReadonlyBinding`, `isMutableBinding`.
### Function parameters
Constrain only what you need:
@@ -98,6 +126,14 @@ function runSim(state: TgpuMutable<typeof SimState>) { ... }
---
## Pointer schemas
Wrap a schema in the pointer constructor for the target address space to declare pointer-typed `tgpu.fn` parameters (out-params, atomics helpers): `d.ptrFn(schema)` (function-local), `d.ptrPrivate`, `d.ptrWorkgroup`, `d.ptrStorage`, `d.ptrUniform`. Workgroup/storage/uniform pointers as function parameters need WGSL's `unrestricted_pointer_parameters` extension — enabled automatically when available.
## Layout attribute schemas
`d.align(n, schema)` and `d.size(n, schema)` override alignment/size to match an externally defined WGSL layout — they take effect only as struct field types, not on standalone schemas. `d.location(n, schema)` pins an IO location; `d.interpolate('flat', schema)` sets interpolation (required for integer inter-stage varyings); `d.invariant(schema)` marks the position builtin invariant.
## CPU-side texture types (`TgpuTexture<TProps>`)
Never use `any` for texture variables. `TgpuTexture` takes a `TextureProps` generic: