---
title: CLI
description: vgpu CLI commands, arguments, flags, and exit codes.
---

# CLI



The vgpu CLI provides command-line tooling for working with vgpu. Use it to validate WGSL shaders, query the vgpu documentation, inspect canonical example source, serve those same docs and examples over MCP, diagnose your local GPU environment, and set up the native runtime for Node.js workflows.

## Installation and usage

The CLI ships with the `vgpu` package, so no separate installation is required. Run any command with `npx vgpu`:

```bash
npx vgpu <command> [args] [flags]
npx vgpu --help
npx vgpu --version
```

The `examples` commands never execute fetched code.

## Command inventory

| Command                     | Dispatcher description                                         |
| --------------------------- | -------------------------------------------------------------- |
| `check`                     | Validate and reflect a WGSL file as JSON                       |
| `docs`                      | Explore bundled VGPU documentation                             |
| `examples`                  | Inspect canonical gallery source (never executes code)         |
| `mcp`                       | Serve documentation and examples as MCP tools over stdio       |
| `snapshot`                  | Compare the representative GPU pixel snapshot                  |
| `install-dawn`              | Download and verify the portable Node Dawn prebuild            |
| `install-software-renderer` | Download and verify the portable CPU renderer                  |
| `doctor`                    | Verify this machine can render headless (JSON verdict + fixes) |

## check

The `vgpu check` command validates a WGSL file without running it. On success it prints the shader's reflection data as JSON; on failure it reports the validation errors and exits non-zero. Use it to catch shader problems early, in your editor, pre-commit hooks, or CI.

```text
Usage: vgpu check <file.wgsl> [--require-validation]
```

| Flag                   | Argument |
| ---------------------- | -------- |
| `--require-validation` | none     |

Device-backed WGSL validation runs in `resolveShader`'s default `"auto"` mode: when this machine has a WebGPU device, invalid WGSL fails the command; when it does not, `check` warns once on stderr and still reports reflection. Pass `--require-validation` (or set `VGPU_VALIDATE=require`) to fail instead of degrading — useful in CI, where a missing device would otherwise silently reduce `check` to a parse-and-reflect pass. The JSON payload includes a `validation` object (`{ mode, attempted, ok, skipped? }`) describing exactly what ran, and error payloads carry `fix`/`where` when the underlying error provides them.

A failing device check never costs you the rest of the document: when validation rejects the shader (or, under `--require-validation`, when no device could be acquired), `check` still prints the full payload — `diagnostics`, `reflection` and `wgsl` — and reports the failure as `validation.error` (`{ code, message, fix?, where?, ... }`) with `ok: false`, exiting 1. So the JSON contract is the same whether or not the machine running `check` has a WebGPU device; only `validation` differs. Resolution failures (a missing import, a module that declares bindings, an invalid `VGPU_VALIDATE`) remain hard errors: they print a single error object on stderr with no payload.

```bash
npx vgpu check ./shaders/main.wgsl
npx vgpu check ./shaders/main.wgsl --require-validation
VGPU_VALIDATE=require npx vgpu check ./shaders/main.wgsl
```

## docs

The `vgpu docs` commands let you explore the vgpu documentation from the terminal. The full corpus — API reference and guides — ships inside the package, so every query runs locally and works offline. Use `ls` to browse the documentation tree, `cat` to print a page or symbol, `grep` to search across content, and `find` to look up the page to read next by name, keyword, or phrase.

```text
Usage: vgpu docs <command> [args] [flags]

Start here: vgpu docs cat getting-started.md   (the guide for using the latest API correctly)

Commands:
  ls [path]                  List packages or docs under a virtual path
  cat <path|symbol>          Print docs by virtual path or unique symbol
  grep [-i] [--package <pkg>] <pattern>
                             Search docs content; case-sensitive unless -i is used
  find <query>               Find symbols and docs paths by substring
  path <symbol|path>         Resolve a symbol or virtual path for shell usage
  symbols                    List indexed symbols
  help                       Show this help

Examples:
  vgpu docs cat getting-started.md
  vgpu docs ls /guides
  vgpu docs ls
  vgpu docs cat /@vgpu/core/Buffer.docs.md
  vgpu docs grep -i --package @vgpu/wgsl minify
  vgpu docs path Buffer
```

### docs cat

```bash
npx vgpu docs cat <path|symbol>
npx vgpu docs cat /@vgpu/core/Buffer.docs.md
```

### docs find

```bash
npx vgpu docs find <query>
npx vgpu docs find buffer
npx vgpu docs find "wgsl loader"
```

Every whitespace-separated word in the query must match, so multi-word phrases
narrow the result instead of returning nothing. `find` looks at symbol names,
doc paths, page titles, and the search keywords a page declares; only when that
finds nothing does it fall back to searching page bodies, which is what makes
prose (`"typescript wgsl import"`) and error codes
(`VGPU-WGSL-PKG-NOTFOUND`) resolve to a page. Use `grep` when you want every
content match with its line, and `find` when you want the page to read next.
Results are ranked best-match-first and capped at 20; a truncated response ends
with a line telling you how many matches were hidden so you can add a word.

### docs grep

```bash
npx vgpu docs grep [-i] [--package <pkg>] <pattern>
npx vgpu docs grep -i --package @vgpu/wgsl minify
```

| Flag        | Argument |
| ----------- | -------- |
| `-i`        | none     |
| `--package` | `<pkg>`  |

### docs help

```bash
npx vgpu docs help
npx vgpu docs --help
```

### docs ls

```bash
npx vgpu docs ls [path]
npx vgpu docs ls /guides
```

### docs path

```bash
npx vgpu docs path <symbol|path>
npx vgpu docs path Buffer
```

### docs symbols

```bash
npx vgpu docs symbols
```

## doctor

The `vgpu doctor` command verifies that the current machine can render headless with vgpu. It runs its checks end to end — including a real render unless you pass `--no-render` — and prints a JSON verdict with suggested fixes. The command exits `0` when the environment is healthy and non-zero when it is not.

```text
Usage: vgpu doctor [--no-render] [--pretty]

Diagnose whether this machine can render headless with vgpu/node. JSON is written by default.
```

| Flag          | Argument |
| ------------- | -------- |
| `--no-render` | none     |
| `--pretty`    | none     |

```bash
npx vgpu doctor
npx vgpu doctor --no-render
npx vgpu doctor --pretty
```

## examples

The `vgpu examples` commands let you search and inspect the source code of the vgpu example gallery without cloning the repository. Use `search` to find examples, `show` to list an example's files and metadata, `cat` to print a single file, and `pull` to copy an example's complete source into a local directory.

```text
vgpu examples — inspect canonical gallery source (never executes code)

Official origin: https://vgpu.sh

Usage:
  vgpu examples search <query> [--any] [--limit <n>] [--revision <sha256>] [--offline] [--pretty]
  vgpu examples show <id> [--revision <sha256>] [--offline] [--pretty]
  vgpu examples cat <id> <path> [--revision <sha256>] [--offline] [--json]
  vgpu examples pull <id> --out <directory> [--revision <sha256>] [--offline] [--force] [--pretty]
  vgpu examples cache path
  vgpu examples cache clear

Canonical agent invocation: npx vgpu examples ...
```

### examples search

```bash
npx vgpu examples search <query>
npx vgpu examples search "raymarching hdr" --any --limit 10 --pretty
```

| Flag         | Argument or range                             |
| ------------ | --------------------------------------------- |
| `--any`      | none                                          |
| `--limit`    | integer `<n>` from `1` to `100`; default `20` |
| `--revision` | lowercase `<sha256>`                          |
| `--offline`  | none                                          |
| `--pretty`   | none                                          |

### examples show

```bash
npx vgpu examples show <id>
npx vgpu examples show raymarched-fractal --pretty
```

| Flag         | Argument             |
| ------------ | -------------------- |
| `--revision` | lowercase `<sha256>` |
| `--offline`  | none                 |
| `--pretty`   | none                 |

### examples cat

```bash
npx vgpu examples cat <id> <path>
npx vgpu examples cat raymarched-fractal renderer.ts
npx vgpu examples cat raymarched-fractal renderer.ts --json
```

| Flag         | Argument             |
| ------------ | -------------------- |
| `--revision` | lowercase `<sha256>` |
| `--offline`  | none                 |
| `--json`     | none                 |

### examples pull

```bash
npx vgpu examples pull <id> --out <directory>
npx vgpu examples pull raymarched-fractal --out ./fractal --pretty
```

| Flag         | Argument               |
| ------------ | ---------------------- |
| `--out`      | required `<directory>` |
| `--revision` | lowercase `<sha256>`   |
| `--offline`  | none                   |
| `--force`    | none                   |
| `--pretty`   | none                   |

### examples cache

```bash
npx vgpu examples cache path
npx vgpu examples cache clear
```

### Revision and offline fields

| Input or output  | Value                                                         |
| ---------------- | ------------------------------------------------------------- |
| `--revision`     | Immutable lowercase SHA-256 revision                          |
| `--offline`      | No network requests; requires previously verified cached data |
| `lastVerifiedAt` | Included in applicable structured offline results             |

### Exit codes

| Code | Error class                                           |
| ---- | ----------------------------------------------------- |
| `0`  | success                                               |
| `2`  | `VGPU-EXAMPLES-USAGE`                                 |
| `3`  | `VGPU-EXAMPLES-NOT-FOUND`                             |
| `4`  | `VGPU-EXAMPLES-NETWORK`                               |
| `5`  | `VGPU-EXAMPLES-INTEGRITY` and incompatible API errors |
| `6`  | `VGPU-EXAMPLES-DESTINATION-EXISTS`                    |
| `7`  | `VGPU-EXAMPLES-FILESYSTEM`                            |

## mcp

VGPU exposes the existing docs and examples behavior as two typed MCP tools:

* `docs` supports `search`, `read`, `resolve`, `list`, `grep`, and `symbols` operations against the documentation bundled with the package.
* `examples` supports `search`, `show`, and `read`. On Linux and macOS, the local stdio transport also supports `download`.

Both `read` operations are paginated for transport-safe responses. They accept an optional UTF-16 `offset` and `limit`; `limit` defaults to and cannot exceed 65,536 code units. When more content remains, structured output includes `truncated: true` and the `nextOffset` to request.

Use the public, read-only Streamable HTTP endpoint when an agent only needs to inspect content:

```text
https://vgpu.sh/api/mcp
```

The hosted endpoint is stateless and implements the modern MCP 2026-07-28 transport. Configure clients for automatic or modern protocol negotiation; legacy session-based HTTP is intentionally rejected because a request may be served by any deployment instance. The endpoint is also advertised at `https://vgpu.sh/.well-known/mcp.json`.

Start the stdio server without filesystem writes when an agent is running locally:

```bash
npx vgpu mcp
```

Bare stdio exposes the same read-only operations as HTTP. To enable `download` on Linux or macOS, explicitly select its output boundary in one of three ways:

```bash
# Project-scoped clients that launch the server from the project directory
npx vgpu mcp --project-from-cwd

# A fixed project directory
npx vgpu mcp --output-dir /absolute/path/to/project

# A host-managed environment
VGPU_MCP_OUTPUT_DIR=/absolute/path/to/project npx vgpu mcp
```

`--output-dir` and `VGPU_MCP_OUTPUT_DIR` must name an existing absolute directory; VGPU canonicalizes it before serving. An explicit CLI selector overrides the environment variable, and `--output-dir` cannot be combined with `--project-from-cwd`. Without one of these configurations, `download` is omitted from the tool schema.

The agent supplies a normalized relative destination beneath that boundary:

```json
{
  "operation": "download",
  "id": "gradient",
  "destination": "examples/gradient"
}
```

Absolute destinations, dot segments, encoded paths, backslashes, control characters, the boundary directory itself, and existing destinations are rejected. Successful structured output reports the canonical absolute `destination`. VGPU coordinates concurrent VGPU writers with a lock and never exposes the human-operated `vgpu examples pull --force` behavior through MCP. Node does not expose a portable atomic no-replace rename for directories, so another process with write access to the output directory must not concurrently claim the same destination during final publication.

Use project-scoped Claude Code (`.mcp.json`) or Cursor (`.cursor/mcp.json`) configuration with `--project-from-cwd` only when that client launches the command from the project directory:

```json
{
  "mcpServers": {
    "vgpu": {
      "command": "npx",
      "args": ["-y", "vgpu", "mcp", "--project-from-cwd"]
    }
  }
}
```

Codex can use the same project-scoped pattern in `.codex/config.toml` when Codex launches the MCP process from the active workspace; omitting `cwd` preserves that inherited working directory:

```toml
[mcp_servers.vgpu]
command = "npx"
args = ["-y", "vgpu", "mcp", "--project-from-cwd"]
```

For global MCP configuration, use a fixed `--output-dir` or set `VGPU_MCP_OUTPUT_DIR` in the server environment. Claude Code and Codex MCP configurations load inside Conductor. Cursor reads `.cursor/mcp.json` only after you open the Conductor workspace in Cursor. Conductor does not define a separate MCP format. There is no cross-editor MCP convention that safely grants a local server write access to whichever workspace is currently active, so VGPU does not infer one. On Windows, the stdio server remains read-only even when an output boundary is configured because the CLI cannot provide the same safe publication guarantees there.

## install-dawn

The `vgpu install-dawn` command downloads and verifies the portable Dawn prebuild, the native WebGPU implementation vgpu uses to render in Node.js. Run it when `vgpu doctor` reports a missing Dawn runtime.

```text
Usage: vgpu install-dawn

Download and verify the portable Dawn binary for this platform.
Honors GH_TOKEN/GITHUB_TOKEN and VGPU_CACHE_DIR.
```

```bash
npx vgpu install-dawn
```

## install-software-renderer

The `vgpu install-software-renderer` command downloads and verifies a portable CPU renderer. Use it on machines without a usable GPU — such as CI runners or headless servers — so vgpu can still render.

```text
Usage: vgpu install-software-renderer

Download and sha256-verify the portable CPU software renderer for this platform.
Honors VGPU_CACHE_DIR.
```

```bash
npx vgpu install-software-renderer
```

## snapshot

The `vgpu snapshot` command is an internal self-test used by vgpu's own CI: it renders a scene built into the CLI inside the Docker GPU harness (`VGPU_DOCKER_TEST=1`) and compares the pixels against a committed baseline to catch toolchain regressions. To verify that your machine is set up correctly, use `vgpu doctor` instead.

```text
Usage: vgpu snapshot [--ci] [--update] [--baseline <path>]
```

`VGPU_DOCKER_TEST=1` is required.

| Flag         | Argument |
| ------------ | -------- |
| `--ci`       | none     |
| `--update`   | none     |
| `--baseline` | `<path>` |

```bash
VGPU_DOCKER_TEST=1 npx vgpu snapshot --ci
VGPU_DOCKER_TEST=1 npx vgpu snapshot --update
VGPU_DOCKER_TEST=1 npx vgpu snapshot --baseline <path>
```

## Global options

| Flag        | Shorthand | Output                |
| ----------- | --------- | --------------------- |
| `--help`    | `-h`      | CLI help              |
| `--version` | `-v`      | installed CLI version |

```bash
npx vgpu --help
npx vgpu --version
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: vgpu Examples API
description: Read-only, tokenless discovery for versioned vgpu example manifests and source artifacts.
---

# vgpu Examples API



The examples API is the machine-readable source behind the vgpu examples workflow. Coding agents should prefer the stateless, read-only MCP endpoint at `https://vgpu.sh/api/mcp` with automatic or modern protocol negotiation; use `npx vgpu mcp --project-from-cwd` or an absolute `--output-dir` when the agent also needs to download an example. The MCP and CLI surfaces both reuse the compatibility checks and integrity verification described here.

## Discovery

Start with `GET /.well-known/vgpu-examples.json`. It lists supported contracts and links to the mutable latest pointer. The API requires no token or other authentication.

The complete OpenAPI 3.1 description is available at [`/openapi.json`](https://vgpu.sh/openapi.json).

```bash
curl https://vgpu.sh/.well-known/vgpu-examples.json
curl https://vgpu.sh/api/examples/v1/latest.json
```

The latest pointer names an immutable revision and provides the SHA-256 digest of its index. Follow its `indexUrl`; do not construct a revision URL from an unvalidated value.

## Indexes and manifests

An immutable index lists examples and links each one to a manifest. A manifest contains metadata plus a `files` array. Every file entry is a hypermedia link with its raw artifact URL, byte size, content type, and SHA-256 digest.

Raw artifact paths may contain slashes and are intentionally not modeled as a conventional OpenAPI path parameter. Follow the `url` returned in the manifest rather than assembling a path yourself.

## Caching and integrity

Discovery and latest-pointer responses use short, revalidated caches. Revision indexes, manifests, and raw source artifacts are immutable and may be cached for one year. All successful artifact responses include an `ETag`; send it in `If-None-Match` to receive `304 Not Modified` when appropriate.

Before using downloaded content, verify the index, manifest, aggregate, and file SHA-256 values. The preferred CLI workflow performs these checks automatically.

## Methods and CORS

Every API resource supports `GET`, `HEAD`, and `OPTIONS`. `HEAD` returns the same status and headers as `GET` without a body. `OPTIONS` returns the public CORS policy. Cross-origin reads are allowed without credentials, and conditional requests may send `If-None-Match`.

`POST`, `PUT`, `PATCH`, and `DELETE` return `405 Method Not Allowed` with `Allow: GET, HEAD, OPTIONS`. The API is read-only.

## Errors

Errors keep a stable JSON envelope:

```json
{
  "error": {
    "code": "VGPU-EXAMPLES-NOT-FOUND",
    "message": "Artifact not found"
  }
}
```

`404` means the requested immutable revision or artifact does not exist. Rediscover from `/.well-known/vgpu-examples.json` and follow its current links. `405` means the method is unsupported. `500` uses the opaque `VGPU-EXAMPLES-STORAGE` code; retry later rather than treating it as a missing example. The frozen wire body intentionally contains no deployment-specific recovery details.

## CLI workflow

Use the CLI to list, inspect, and copy examples without hand-implementing this protocol:

```bash
npx vgpu examples
npx vgpu examples search gradient
npx vgpu examples show gradient
npx vgpu examples pull gradient --out ./gradient
```

See the [CLI reference](/docs/cli) for command options and exit codes.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: vgpu Docs
description: Build WebGPU applications for browsers, Node.js, and automated workflows.
---

# vgpu Docs



vgpu is a TypeScript library for building WebGPU applications with one composable API. Write modular WGSL once, then use the same shaders for interactive canvases, headless rendering, images, video, and automated tests.

## How to use these docs

* [Get started](/docs/get-started) — install vgpu and render your first frame.
* [Learn the concepts](/docs/concepts) — understand GPUs, targets, frames, effects, and adapters.
* [Explore the examples](/examples) — see live WebGPU demos and inspect their source.
* [Explore the guides](/docs/guides) — solve specific problems involving performance, testing, and shader authoring.
* [Browse the API reference](/docs/reference) — look up packages, types, and functions.

Using a coding agent? Run `npx vgpu` to give it access to the docs, verified examples, validation tools, and runtime diagnostics.

## Tools and workflows

* [CLI](/docs/cli) — explore documentation, inspect examples, validate WGSL, and diagnose the runtime.
* [MCP](/docs/mcp) — connect agents to VGPU documentation and verified examples.
* [ML](/docs/ml) — run ONNX models alongside vgpu on a shared `GPUDevice`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: MCP
description: Set up VGPU MCP in coding agents to search documentation, inspect verified examples, and opt into scoped local downloads.
---

# MCP



Connect coding agents directly to VGPU documentation and verified examples through the [Model Context Protocol](https://modelcontextprotocol.io). Start with the hosted HTTP server for a read-only setup that requires no authentication or installation. Use local stdio when an agent needs package-versioned docs, an offline examples cache, or explicitly scoped example downloads.

## Quick setup

Use `add-mcp` to detect installed MCP clients and add the hosted VGPU server globally:

```bash
npx -y add-mcp https://vgpu.sh/api/mcp -g
```

Remove `-g` to configure clients for the current project instead. The installer lets you review its detected clients before writing their configuration. No VGPU account or authorization flow is required.

## What is VGPU MCP?

VGPU MCP is the official agent interface for the same documentation and verified example source exposed by the VGPU website and CLI. It gives an agent typed tools to:

* Search documentation by concept and read the matching guide or API reference.
* Resolve API symbols without guessing their package or documentation path.
* Find examples by topic, inspect their manifests, and read individual source files.
* Download a verified example only when a local server has been given an explicit output boundary.

VGPU offers two transports. Hosted HTTP is public, stateless, and read-only. Local stdio runs from the `vgpu` npm package and can additionally use the verified local cache or enable confined filesystem writes.

## Hosted HTTP

The recommended server for searching and reading content is:

```text
https://vgpu.sh/api/mcp
```

| Setting        | Value                                  |
| -------------- | -------------------------------------- |
| Name           | `vgpu`                                 |
| Transport      | Streamable HTTP                        |
| Authentication | None                                   |
| Access         | Read-only                              |
| Protocol       | Modern MCP `2026-07-28`                |
| Discovery      | `https://vgpu.sh/.well-known/mcp.json` |

Use automatic protocol negotiation when the client offers it. The endpoint is stateless and intentionally rejects legacy session-based HTTP because requests may be served by different deployment instances.

Hosted HTTP never writes to disk and does not expose the local-only `download` operation or `offline` input.

## Connect manually

Use the hosted URL in any client that supports modern Streamable HTTP. These are common configurations.

### Claude Code

```bash
claude mcp add --transport http vgpu https://vgpu.sh/api/mcp
```

Start a new Claude Code session or run `/mcp` to confirm that the `vgpu` server and its two tools are available.

### Codex CLI

```bash
codex mcp add vgpu --url https://vgpu.sh/api/mcp
codex mcp list
```

### Cursor

Add the server to a project-specific `.cursor/mcp.json` or your global Cursor MCP configuration:

```json
{
  "mcpServers": {
    "vgpu": {
      "url": "https://vgpu.sh/api/mcp"
    }
  }
}
```

### Other clients

In clients that provide an **Add MCP server** or **Add custom connector** form, use:

| Field          | Value                     |
| -------------- | ------------------------- |
| Name           | `vgpu`                    |
| URL            | `https://vgpu.sh/api/mcp` |
| Transport      | Streamable HTTP           |
| Authentication | None                      |

Client commands and settings screens can change. If a client asks for a protocol version, choose automatic negotiation or modern MCP rather than legacy session-based HTTP.

## Try it

After connecting, ask the agent naturally. For example:

* “Search the VGPU docs for render pipelines and summarize the setup.”
* “Resolve the API reference for the texture type used by VGPU.”
* “Find examples related to gradients and show me the files in the best match.”
* “Read the main source file from the `gradient` example and explain how it works.”

The agent can compose operations. A typical documentation flow is `search` or `resolve`, followed by `read`. A typical example flow is `search`, `show` to inspect the manifest, and then `read` for selected files.

## Tools

### `docs`

Search and navigate the canonical VGPU documentation corpus.

| Operation | Purpose                                                      | Main input                     |
| --------- | ------------------------------------------------------------ | ------------------------------ |
| `search`  | Find relevant documents by concept                           | `query`                        |
| `resolve` | Resolve a symbol or documentation target                     | `target`                       |
| `list`    | Browse packages and virtual documentation paths              | `path`, default `/`            |
| `grep`    | Find an exact pattern with optional package and case filters | `pattern`                      |
| `symbols` | Search or list indexed API symbols                           | optional `query` and `package` |
| `read`    | Read a resolved guide or API document                        | `target`                       |

### `examples`

Search and inspect canonical examples without executing their code.

| Operation  | Purpose                                                       | Main input                                               |
| ---------- | ------------------------------------------------------------- | -------------------------------------------------------- |
| `search`   | Find examples by topic                                        | `query`                                                  |
| `show`     | Inspect an example manifest and its file list                 | `id`                                                     |
| `read`     | Read one verified file from an example                        | `id` and `path`                                          |
| `download` | Publish a verified example beneath an approved local boundary | `id` and relative `destination`; scoped local stdio only |

Example operations can pin an immutable lowercase SHA-256 `revision`. On local stdio, `search`, `show`, and `read` also accept `offline: true` to prohibit network access and use only previously verified cache entries.

Both `read` operations accept an optional UTF-16 `offset` and `limit`; `limit` defaults to and cannot exceed 65,536 code units. When more content remains, the structured result includes `truncated: true` and `nextOffset`. Pass that value as the next `offset` to continue reading.

Tool failures return bounded structured errors with stable VGPU error codes. Example manifests and files retain the same compatibility and SHA-256 integrity verification used by the human CLI.

## Local stdio

Run the read-only local server from the public `vgpu` package:

```bash
npx -y vgpu mcp
```

For Claude Code, Cursor, and clients that use the common JSON shape, configure the command instead of a URL:

```json
{
  "mcpServers": {
    "vgpu": {
      "command": "npx",
      "args": ["-y", "vgpu", "mcp"]
    }
  }
}
```

Codex uses TOML:

```toml
[mcp_servers.vgpu]
command = "npx"
args = ["-y", "vgpu", "mcp"]
```

Bare stdio does not advertise `download`. It serves documentation bundled with that installed VGPU package and, unlike hosted HTTP, can read from the verified examples cache with `offline: true`.

## Enable local downloads

Filesystem writes are opt-in and supported only on Linux and macOS. Select one output boundary when starting the server:

```bash
# The MCP host launches the process from the active project
npx -y vgpu mcp --project-from-cwd

# A fixed project
npx -y vgpu mcp --output-dir /absolute/path/to/project

# A host-managed environment
VGPU_MCP_OUTPUT_DIR=/absolute/path/to/project npx -y vgpu mcp
```

`--output-dir` and `VGPU_MCP_OUTPUT_DIR` must name an existing absolute directory. VGPU resolves symlinks and canonicalizes that boundary before starting. An explicit CLI selector overrides the environment variable, and `--output-dir` cannot be combined with `--project-from-cwd`.

The agent supplies a normalized relative destination beneath the selected boundary:

```json
{
  "operation": "download",
  "id": "gradient",
  "destination": "examples/gradient"
}
```

VGPU returns the canonical absolute destination after publication. It rejects absolute or encoded agent paths, dot segments, backslashes, control characters, symlink ancestors, the boundary itself, and existing destinations before repository access. MCP never exposes the human CLI's `--force` behavior.

Use `--project-from-cwd` only when the MCP host launches the command from the active project directory. For a global configuration, prefer a fixed `--output-dir` or `VGPU_MCP_OUTPUT_DIR`. Codex configurations should omit `cwd` when they are meant to inherit the active workspace. Conductor inherits the selected host's Claude Code, Codex, or Cursor MCP configuration; it does not define another MCP format. MCP does not provide a portable workspace-root authorization boundary, so VGPU never infers one.

On Windows, local stdio remains read-only even when an output boundary is configured. This allows one shared cross-platform configuration without advertising a filesystem operation the CLI cannot publish with the same guarantees.

## Security

* Verify that remote configurations use the official `https://vgpu.sh/api/mcp` endpoint. No token or authentication header is required.
* Hosted HTTP is read-only. It cannot execute example source, access your filesystem, or publish example directories.
* Local stdio does not publish into a project unless its user-controlled startup command selects an output boundary. The agent can choose only a new relative descendant within that boundary.
* Keep human confirmation enabled for filesystem tool calls when your MCP client supports it, and review generated code before running it.
* Example downloads verify their manifest and file hashes, coordinate cooperating writers with a lock, and clean up failed or cancelled staging directories.

Node.js does not expose a portable atomic no-replace rename for directories. Do not let another process concurrently claim the exact same destination during the final publication step.

## Troubleshooting

| Symptom                                       | What to check                                                                                                                                                       |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The client cannot connect to hosted HTTP      | Confirm the exact URL and select automatic or modern protocol negotiation; legacy session-based HTTP is rejected.                                                   |
| The server is connected but tools are missing | Reload or restart the client, then confirm both `docs` and `examples` appear in its MCP tool list.                                                                  |
| `download` is missing                         | Use local stdio on Linux or macOS and configure `--project-from-cwd`, `--output-dir`, or `VGPU_MCP_OUTPUT_DIR`. Hosted HTTP, bare stdio, and Windows are read-only. |
| `offline` is rejected                         | Use local stdio. Hosted HTTP reads deployed artifacts and intentionally omits the local-cache option.                                                               |
| A read result is truncated                    | Call the same `read` operation again with the returned `nextOffset`.                                                                                                |
| A destination is rejected                     | Choose a new normalized relative directory without `.`, `..`, backslashes, encoded separators, or an existing path.                                                 |

## Choose a transport

| Need                                           | Recommended setup                              |
| ---------------------------------------------- | ---------------------------------------------- |
| Search and read docs or examples               | Hosted HTTP at `https://vgpu.sh/api/mcp`       |
| Use VGPU docs matching an installed package    | Bare local stdio                               |
| Work from a previously verified examples cache | Bare local stdio with `offline: true`          |
| Download into the active project               | Local stdio with `--project-from-cwd`          |
| Download into a fixed project                  | Local stdio with `--output-dir /absolute/path` |

See the [CLI reference](/docs/cli#mcp) for the complete command syntax.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Compilation
description: Pipelines compile lazily on first use; pre-warm them during load so the first frame doesn't hitch.
---

# Compilation



Pipelines compile lazily: the first `draw()` against a new target pays the pipeline creation cost, and that cost lands inside your frame. WebGPU keys pipelines by shader *and* render signature — the tuple of color formats, depth format, and sample count — so the same WGSL rendering into a canvas and into an MSAA target means two compilations. `compile()` moves that work into load time.

## Pre-warming with a target

Most of the time you already have the target in hand. `await draw.compile(target)` and `await effect.compile(target)` warm exactly that signature and resolve back to the same object:

```ts
import { init, draw, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const ocean = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.8, 1.0);
  }
`);
const tri = draw(gpu, {
  shader: `
    struct Out { @builtin(position) position: vec4f }
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> Out {
      var pts = array<vec2f, 3>(vec2f(-0.5, -0.5), vec2f(0.5, -0.5), vec2f(0.0, 0.5));
      var out: Out;
      out.position = vec4f(pts[vi], 0.0, 1.0);
      return out;
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1.0, 0.4, 0.2, 1.0); }
  `,
});

await Promise.all([ocean.compile(canvasSurface), tri.compile(canvasSurface)]);
tri.draw(canvasSurface);
ocean.draw(canvasSurface);
```

The pipelines are cached per signature at the device level, so those first `draw()` calls — and every draw after them — just encode work.

## Compiling without a target

Sometimes the target doesn't exist yet. Pass a signature object instead: `colors` is required, `depth` and `sampleCount` are optional.

```ts
import { init, draw, geometry } from "vgpu";
import { box } from "vgpu/scene";

const gpu = await init();
const sceneShader = `/* vertex + fragment WGSL */`;
const msaaScene = draw(gpu, { shader: sceneShader, geometry: geometry(gpu, box({ size: 1 })) });

await msaaScene.compile({
  colors: ['bgra8unorm'],
  depth: 'depth24plus',
  sampleCount: 4,
});
```

<Callout type="info">
  Good to know: surface formats are platform-dependent — `bgra8unorm` on most browsers, `rgba8unorm` on others. Compiling the wrong signature doesn't error; it's just a warm-up you didn't need, and the real draw compiles lazily on first use anyway. When in doubt, compile against the actual target.
</Callout>

## `compileSync()`

`compileSync(target)` is the blocking twin: same cache, same signatures, but it creates the pipeline right now. Use it in tools and tests where jank doesn't matter. If an async `compile()` for the same signature is in flight, the synchronous result wins and the pending promise resolves with it.

```ts
import { init, draw, target } from "vgpu";

const gpu = await init();
const offscreen = target(gpu, { size: [2048, 2048], depth: true });
const grid = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var pts = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
      return vec4f(pts[vi], 0.0, 1.0);
    }
    @fragment fn fs_main() -> @location(0) vec4f {
      return vec4f(0.1, 0.4, 0.7, 1.0);
    }
  `,
});

grid.compileSync(offscreen);
```

## Errors

A failed `compile()` rejects its promise — the error belongs to the call site, so catch it where you scheduled the warm-up:

```ts
import { init, draw } from "vgpu";

const gpu = await init();
const tri = draw(gpu, { shader: `@vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0); }` });

try {
  await tri.compile({ colors: ['bgra8unorm'] });
} catch (error) {
  console.error('Pipeline failed to compile', error);
}
```

The lazy path is different: since `draw()` returns immediately, a pipeline that fails to compile on first use reports through [`gpu.onError`](/docs/reference/vgpu/gpu#onerror), and `gpu.settled()` lets tests wait for those deliveries. Pre-warmed or not, the failure never lands twice.

## Render bundles

Recording a [bundle](/docs/concepts/render-bundles) needs every pipeline immediately, so anything you didn't pre-warm compiles synchronously at record time. See [compilation at record time](/docs/concepts/render-bundles#compilation-at-record-time) for that flow.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Context
description: init() creates the Gpu context; every surface, target, effect, and frame is created from it.
---

# Context



Everything in vgpu starts from one call. `init()` requests the WebGPU adapter and device and returns a [`Gpu`](/docs/reference/vgpu/gpu#gpu) context. Every other object — surfaces, targets, effects, draws, frames — is created from that context, so all of them share one device.

```ts
import { init, effect, surface, target } from "vgpu";

const gpu = await init();

const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas); // the canvas you render into
const colorTarget = target(gpu, { size: [256, 256] }); // an offscreen texture
const gradient = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.4, 1.0);
  }
`);

// Render the gradient onto the canvas once
gradient.draw(canvasSurface);
```

## Create resources once, draw every frame

Create the context, surface, and effects once, up front. Each `draw()` renders immediately; rendering should encode work, not rebuild long-lived resources every tick.

The expensive objects — context, surface, effect — live outside your render code and are reused by every draw. What changes per frame is data, not resources.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Draws
description: draw(gpu, opts) renders geometry with custom vertex buffers — you write the vertex stage, geometry(gpu, ...) supplies the buffers.
---

# Draws



A [`Draw`](/docs/reference/vgpu/draw#draw) renders geometry with custom vertex buffers: you write both the vertex and the fragment stage, and a geometry supplies the buffers. If you want to render a full-screen shader instead, use an [Effect](/docs/concepts/effects).

## Draw a geometry

`geometry(gpu, geometry)` turns geometry from `vgpu/scene` into vertex and index buffers. Your vertex shader declares the attributes it consumes — `@location(0) position`, `@location(1) normal` — and the geometry feeds them.

```ts
import { init, draw, geometry, target } from "vgpu";

const gpu = await init();

// ---cut---
import { box, orbit, perspectiveCamera } from "vgpu/scene";

const shader = `
  struct Camera { viewProjection: mat4x4f }
  struct Model { model: mat4x4f }
  @group(0) @binding(0) var<uniform> camera: Camera;
  @group(0) @binding(1) var<uniform> model: Model;

  struct VertexOut { @builtin(position) position: vec4f, @location(0) normal: vec3f }

  @vertex fn vs_main(@location(0) position: vec3f, @location(1) normal: vec3f) -> VertexOut {
    var out: VertexOut;
    out.position = camera.viewProjection * model.model * vec4f(position, 1.0);
    out.normal = normal;
    return out;
  }

  @fragment fn fs_main(@location(0) normal: vec3f) -> @location(0) vec4f {
    let light = max(dot(normalize(normal), normalize(vec3f(1.0, 1.0, 1.0))), 0.15);
    return vec4f(vec3f(0.2, 0.5, 1.0) * light, 1.0);
  }
`;

const colorTarget = target(gpu, { size: [1280, 720], depth: true });
const camera = perspectiveCamera({ fov: 45, aspect: 16 / 9, position: [2, 2, 3], target: [0, 0, 0] });

const cube = draw(gpu, { shader, geometry: geometry(gpu, box({ size: 1 })) });
cube.set({
  camera: { viewProjection: camera.viewProjection },
  model: { model: orbit(0) },
});

cube.draw(colorTarget);
```

Everything works like the rest of vgpu: bindings are reflected from the WGSL, `set()` writes uniforms by name, and the draw renders one-shot into any target. Pipelines are compiled per target format and cached, so the same `Draw` can render into different targets. See [Compilation](/docs/concepts/compilation) to pre-warm each signature before the first draw.

Three details specific to geometry:

* 3D needs a depth buffer, and surfaces don't have one — render into a `target(gpu, { depth: true })` and composite it to the canvas. [Effects](/docs/concepts/effects) and [Passes](/docs/concepts/passes) show how. Deep scenes fight z-fighting with reversed-Z: `depth: { compare: "greater" }` on the draw, `clearDepth: 0` on the pass.
* A closed geometry like this box never shows its back faces — add `cull: "back"` to the draw and skip roughly half the fragment work.
* `GeometryLike` is an open interface: `geometry(gpu)` builds one from `vgpu/scene` geometry, but you can also pass your own `GPUBuffer`s and vertex layouts. See the [reference](/docs/reference/vgpu/draw#geometrylike).

## No geometry? You spawn triangles

Leave `geometry` out and the draw runs with no buffers at all: `vertices` defaults to `3`, so every instance is one triangle whose corners you position from `@builtin(vertex_index)`. Combined with `instances`, that spawns a particle system from nothing:

```ts
import { init, draw, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const smokeShader = `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  struct Out { @builtin(position) position: vec4f, @location(0) fade: f32 }

  @vertex fn vs_main(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
    var corners = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(0.0, 1.5));
    let seed = fract(sin(f32(i) * 12.9898) * 43758.5453);
    let life = fract(seed + params.time * 0.05);          // 0 -> 1, then respawn
    let center = vec2f(seed * 2.0 - 1.0, life * 2.2 - 1.1); // drifts upward
    let size = 0.01 + life * 0.04;                          // grows as it rises

    var out: Out;
    out.position = vec4f(center + corners[v] * size, 0.0, 1.0);
    out.fade = 1.0 - life;
    return out;
  }

  @fragment fn fs_main(@location(0) fade: f32) -> @location(0) vec4f {
    return vec4f(vec3f(0.35) * fade, 1.0); // dims into the dark background
  }
`;

const smoke = draw(gpu, { shader: smokeShader, instances: 10_000 });

smoke.set({ params: { time: 2.5 } }); // drive with clock(gpu).time in a frame loop
smoke.draw(canvasSurface);
```

One draw call, 10,000 smoke puffs, zero buffers — each particle derives its position, size, and fade from `instance_index` and `time`. Counts can also change per call: `smoke.draw({ target: surface, instances: 500 })`.

See it live: the [instanced rendering example](/examples/instanced-rendering) drives a 125k-cube lattice from a single instance stream.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Effects
description: An effect is a full-screen fragment shader; chain effects by binding a target as another effect's input.
---

# Effects



An [`Effect`](/docs/reference/vgpu/effect#effect) is a full-screen fragment shader created with `effect(gpu, source)`. Its pipeline compiles lazily on first use; call `await effect.compile(target)` during load if you want to pre-warm it. See [Compilation](/docs/concepts/compilation) for the full pre-warm flow. Every draw fills the whole target — you only write the fragment.

Effects chain through targets: render one effect into an offscreen [`Target`](/docs/reference/vgpu/target#target), then bind that target as a texture input of the next effect with `set()`.

The `uv` varying that `effect(gpu)` injects is top-origin: `(0, 0)` is the
top-left corner and `v` grows downward — the same convention as WebGPU texture
coordinates, `@builtin(position)`, and `target.read()`. Sampling any texture
with this `uv` needs no flip: a pass that samples `src` at `uv` reproduces the
image exactly. If you are porting a WebGL or Shadertoy shader that assumes
`v` grows upward, invert once at the boundary (`1.0 - uv.y`) and keep
everything else flip-free.

```ts
import { init, effect, sampler, surface, target } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const sceneSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 1.0, 1.0);
  }
`;

// Post-processing: reads the scene texture and inverts its colors.
const postSource = `
  @group(0) @binding(0) var src: texture_2d<f32>;
  @group(0) @binding(1) var samp: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let base = textureSampleLevel(src, samp, uv, 0.0);
    return vec4f(1.0 - base.rgb, 1.0);
  }
`;

const scene = target(gpu, { size: [1280, 720] });

const sceneEffect = effect(gpu, sceneSource);
const post = effect(gpu, postSource);
post.set({
  src: scene,
  samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
}); // the offscreen result becomes the post input

sceneEffect.draw(scene); // render the scene offscreen
post.draw(canvasSurface); // invert it onto the canvas
```

Reach for `textureLoad` only when you need exact texels or an unfilterable
format — for ordinary sampling, a filtering sampler is simpler and faster.

`post.set(...)` exposes the offscreen result and filtering sampler to WGSL as bindings named `src` and `samp`. Each one-shot `draw()` encodes and submits its own work immediately, in call order.

## Updating bindings

You can update bindings at any time by using `.set`.

`set()` writes immediately — there is no change detection, so every call is a
real GPU write. Match your calls to how often values actually change: constants
once at creation, size- and resolution-class uniforms at init and on resize,
and per-frame calls only for genuinely dynamic values like time or pointer
input. Rebinding the same resources is free — bind groups are cached by
resource identity — so this rule is purely about avoiding redundant writes.

One more rule keeps multi-pass frames predictable: a frame records into a
single command buffer, and `set()` writes land before any of it executes — so
re-recording the same effect with mutated uniforms makes every pass read the
final values. When two passes need different values (a horizontal and a
vertical blur, say), create two effects; they are cheap, and each owns its
uniforms.

```ts
import { clock, init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const pulseSource = `
  struct Params { time: f32, width: f32, height: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let glow = sin(params.time) * 0.5 + 0.5;
    return vec4f(uv.x, uv.y, glow, 1.0);
  }
`;

const pulse = effect(gpu, pulseSource, {
  // initial uniform defaults
  set: {
    params: {
      time: 0,
      width: canvasSurface.size[0],
      height: canvasSurface.size[1]
    }
  },
});

// update uniforms before drawing
pulse.set({
  params: {
    time: clock(gpu).time,
  },
});

pulse.draw(canvasSurface);
```

You should also only update uniforms when they need to change, for example, react to canvas size changes:

```ts
import { clock, init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);
const pulse = effect(gpu, `
  struct Params { time: f32, width: f32, height: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0, width: canvasSurface.size[0], height: canvasSurface.size[1] } } });

// ---cut---
const unsubscribe = canvasSurface.onResize(({ width, height }) => {
  pulse.set({ params: { width, height } }); // partial update: time keeps its value
});
```

`onResize()` fires the callback once immediately with the current size, then again on every resize. It returns an `unsubscribe` function — call it when you tear the effect down.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Frames
description: frame(gpu, cb) encodes your passes and submits once; frameLoop(gpu, cb) drives animation.
---

# Frames



A frame is one unit of GPU work. Inside it you open passes, each drawing into a target you choose, and draw the effects you created earlier. When the callback returns, vgpu encodes everything into one command encoder and submits it once.

## Render a single frame

[`frame(gpu)`](/docs/reference/vgpu/frame#framerunner) runs synchronously and renders immediately — every pass inside is encoded into one command encoder and submitted once. That single submit is what the frame is for:

```ts
import { init, effect, frame, sampler, surface, target } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });
const postEffect = effect(gpu, `
  @group(0) @binding(0) var src: texture_2d<f32>;
  @group(0) @binding(1) var samp: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let base = textureSampleLevel(src, samp, uv, 0.0);
    return vec4f(1.0 - base.rgb, 1.0);
  }
`);

// ---cut---
const sceneTarget = target(gpu, { size: [canvasTarget.size[0], canvasTarget.size[1]] });
postEffect.set({
  src: sceneTarget,
  samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
});

frame(gpu, (currentFrame) => {
  currentFrame.pass(sceneTarget, pulseEffect);
  currentFrame.pass(canvasTarget, postEffect);
}); // two passes, one encoder, one submit
```

One-shot draws like `pulseEffect.draw(canvasTarget)` are the simple default for a single pass. Multi-pass hot paths should use `frame(gpu)` to batch passes into one command encoder and one submit. One-shot draws never join a surrounding frame; inside `frame(gpu)`, always go through `frame.pass()`.

<Callout type="warn">
  Warning: one-shot `draw()` calls do not join a surrounding frame — inside a frame callback they submit on their own immediately. Inside `frame(gpu)`, always draw through `frame.pass()`.
</Callout>

<Callout type="warn">
  Warning: Do not call `frame(gpu)` from inside another frame callback or from a surface resize callback. vgpu throws `VGPU-FRAME-REENTRANT` so command encoders stay ordered and predictable.
</Callout>

## Render loops

For animation, use [`frameLoop(gpu)`](/docs/reference/vgpu/frame#framerunner) — it runs your frame every tick:

```ts
import { clock, init, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// ---cut---
const time = clock(gpu);
const handle = frameLoop(gpu, (frame) => {
  pulseEffect.set({ params: { time: time.time } }); // update uniforms every tick
  frame.pass(canvasTarget, pulseEffect);
}, { fps: 30 });

handle.stop(); // call it when your component unmounts
```

The loop advances the frame clock — `clock(gpu).time`, `deltaTime` and `frameCount` — and runs surface auto-resize before each tick. The optional `fps` throttles it.

This is what the same loop looks like by hand with `requestAnimationFrame`:

```ts
import { init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const pulseEffect = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// ---cut---
function tick() {
  pulseEffect.set({ params: { time: performance.now() / 1000 } }); // you own the clock now
  pulseEffect.draw(canvasTarget);
  requestAnimationFrame(tick); // and the scheduling
}
requestAnimationFrame(tick);
```

Both work. `frameLoop(gpu)` is the same loop with the clock, throttling, and resize handling done for you.

See it live: the [fluid example](/examples/fluid) runs a compute-driven simulation with exactly this frame loop shape.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Concepts
description: The core ideas behind every vgpu program, in reading order.
---

# Concepts



These ideas cover every vgpu program. Read them in order — each page builds on the previous one.

<Cards>
  <Card title="Context" description="init() creates the Gpu context; every surface, target, effect, and frame is created from it." href="/docs/concepts/context" />

  <Card title="Draws" description="draw(gpu, opts) renders geometry with custom vertex buffers — you write the vertex stage, geometry(gpu, ...) supplies the buffers." href="/docs/concepts/draws" />

  <Card title="Compilation" description="Pipelines compile lazily on first use; pre-warm them during load so the first frame doesn't hitch." href="/docs/concepts/compilation" />

  <Card title="Effects" description="An effect is a full-screen fragment shader; chain effects by binding a target as another effect's input." href="/docs/concepts/effects" />

  <Card title="Passes" description="A pass composites any number of draws into one target; a single shader can draw directly." href="/docs/concepts/passes" />

  <Card title="Frames" description="frame(gpu, cb) encodes your passes and submits once; frameLoop(gpu, cb) drives animation." href="/docs/concepts/frames" />

  <Card title="Render bundles" description="bundle(gpu, opts, record) records draws once; replaying them each frame skips re-encoding." href="/docs/concepts/render-bundles" />
</Cards>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Passes
description: A pass composites any number of draws into one target; a single shader can draw directly.
---

# Passes



A pass is a render-pass section inside a frame. It has one target, one clear color, and any number of draw calls. Open a pass by hand when you want to composite multiple draws into the same render target — here, an ocean and a boat rendered straight to the canvas:

```ts
import { init, effect, frame, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

const oceanSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let wave = sin(uv.x * 24.0) * 0.01;
    let depth = smoothstep(0.4 + wave, 1.0, uv.y);
    return vec4f(0.1, 0.3 + depth * 0.2, 0.55 + depth * 0.3, 1.0);
  }
`;

// Draws only the hull pixels; discards everything else.
const boatSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let inHull = abs(uv.x - 0.5) < 0.12 && abs(uv.y - 0.42) < 0.05;
    if (!inHull) { discard; }
    return vec4f(0.45, 0.26, 0.13, 1.0);
  }
`;

// ---cut---
const ocean = effect(gpu, oceanSource);
const boat = effect(gpu, boatSource);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: canvasSurface, clear: [0, 0, 0, 1] }, (pass) => {
    pass.draw(ocean); // fill the canvas with water
    pass.draw(boat); // paint the boat on top — same target
  });
});
```

Both draws share one render pass and one target. Order inside the pass is paint order: the ocean fills the canvas first, then the boat draws on top of it.

<Callout type="info">
  Good to know: [`FramePass.draw()`](/docs/reference/vgpu/frame#framepass) accepts a fullscreen [`Effect`](/docs/reference/vgpu/effect#effect) or an explicit [`Draw`](/docs/reference/vgpu/draw#draw). Use `draw(gpu)` when you need meshes, vertex counts, instancing, or raw bind groups.
</Callout>

## One shader? Draw it directly

Now add postprocessing. The pass is the same — the only change is its target: an offscreen [`Target`](/docs/reference/vgpu/target#target) with the same size as the canvas. Then the postprocessing effect (bound with `set({ src: scene })`) needs no pass ceremony to reach the screen:

```ts
import { init, effect, frame, sampler, surface, target } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

const ocean = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let wave = sin(uv.x * 24.0) * 0.01;
    let depth = smoothstep(0.4 + wave, 1.0, uv.y);
    return vec4f(0.1, 0.3 + depth * 0.2, 0.55 + depth * 0.3, 1.0);
  }
`);
const boat = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let inHull = abs(uv.x - 0.5) < 0.12 && abs(uv.y - 0.42) < 0.05;
    if (!inHull) { discard; }
    return vec4f(0.45, 0.26, 0.13, 1.0);
  }
`);
const postSource = `
  @group(0) @binding(0) var src: texture_2d<f32>;
  @group(0) @binding(1) var samp: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let base = textureSampleLevel(src, samp, uv, 0.0);
    let vignette = 1.0 - 0.4 * length(uv - vec2f(0.5));
    return vec4f(base.rgb * vignette, 1.0);
  }
`;

// ---cut---
const scene = target(gpu, { size: [canvasSurface.size[0], canvasSurface.size[1]] });
const postprocessing = effect(gpu, postSource);
postprocessing.set({
  src: scene,
  samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
}); // the offscreen scene becomes the post input

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene, clear: [0, 0, 0, 1] }, (pass) => {
    pass.draw(ocean);
    pass.draw(boat);
  });
});

postprocessing.draw(canvasSurface); // rendering an effect creates a pass
```

The one-shot `draw()` runs after the frame has submitted, so the scene is already rendered when postprocessing reads it. Under the hood it creates an encoder, opens a render pass on `canvasSurface`, encodes the draw, and submits — the same GPU work you would write by hand with `frame.pass`.

<Callout type="info">
  Good to know: `frame.pass()` always needs a target. Use a canvas-backed [`Surface`](/docs/reference/vgpu/surface#surface) from `surface(gpu, canvas)` or an offscreen [`Target`](/docs/reference/vgpu/target#target) from `target(gpu, { size })`.
</Callout>

<Callout type="info">
  Good to know: a pass takes more than a target and a clear color. [`FramePassOptions`](/docs/reference/vgpu/frame#framepassoptions) also sets `clearDepth` (`0` for reversed-Z), `clearStencil`, a `viewport` or `scissor` rectangle for split-screen and partial redraws, `depthReadOnly` to depth-test while sampling the depth texture, a `timer` span for GPU timing, and `visibility` for occlusion queries.
</Callout>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Render bundles
description: bundle(gpu, opts, record) records draws once; replaying them each frame skips re-encoding.
---

# Render bundles



A render loop re-encodes every pipeline, bind group, and draw on every tick — even when nothing changed. A bundle records those commands once; replaying it each frame costs almost nothing.

## Record once, replay every frame

[`bundle(gpu)`](/docs/reference/vgpu/bundle#bundle) records draws against a target and returns a [`Bundle`](/docs/reference/vgpu/bundle#bundle). Replay it inside a pass with `pass.bundles()`:

```ts
import { init, bundle, clock, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const ocean = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.1, 0.3, sin(params.time + uv.y) * 0.2 + 0.6, 1.0);
  }
`, { set: { params: { time: 0 } } });
const boat = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.6, 0.4, 0.2, step(distance(uv, vec2f(0.5, 0.6)), 0.1));
  }
`);

// ---cut---
const scene = bundle(gpu, { target: canvasTarget }, (b) => {
  b.draw(ocean);
  b.draw(boat);
}); // encoded once, right here

const time = clock(gpu);
frameLoop(gpu, (frame) => {
  ocean.set({ params: { time: time.time } }); // uniforms still animate
  frame.pass(canvasTarget, (pass) => pass.bundles(scene)); // replay — no re-encoding
});
```

Record what doesn't change, `set()` what does: the bundle references your buffers, so uniform updates flow through on every replay.

<Callout type="info">
  Good to know: draws inside a bundle can use different shaders and pipelines. What a bundle freezes is the target's render signature — color formats, depth format, sample count — plus bind groups, not a material or a target size.
</Callout>

## Compilation at record time

`bundle(gpu)` encodes right when you call it, so it needs every pipeline immediately: any draw whose pipeline isn't cached yet for the recording signature compiles synchronously, on the spot. That's the one place vgpu still blocks on pipeline creation — and the reason to [pre-warm](/docs/concepts/compilation) before recording. If one of those synchronous creates fails, the error reports through `gpu.onError`, like any lazy compile.

The `target` option also takes a plain signature, so you can pre-warm and record during load, before the real target exists:

```ts
import { init, bundle, clock, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const ocean = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.1, 0.3, 0.6, 1.0);
  }
`);
const boat = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.6, 0.4, 0.2, 1.0);
  }
`);

// ---cut---
await Promise.all([
  ocean.compile({ colors: ['bgra8unorm'] }),
  boat.compile({ colors: ['bgra8unorm'] }),
]);

const scene = bundle(gpu, { target: { colors: ['bgra8unorm'] } }, (b) => {
  b.draw(ocean);
  b.draw(boat);
}); // everything was pre-warmed — recording creates nothing

frameLoop(gpu, (frame) => {
  frame.pass(canvasTarget, (pass) => pass.bundles(scene));
});
```

Two caveats. Bindings must be `set()` before recording — the signature relaxes the target requirement, not the resources. And replay targets must match the recorded signature exactly: when they don't, the error prints both keys, which is how you catch a platform surface-format surprise (`bgra8unorm` recorded, `rgba8unorm` actual).

## Mix recorded and dynamic draws

A pass can replay bundles and encode fresh draws side by side:

```ts
import { init, bundle, clock, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const ocean = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.1, 0.3, 0.6, 1.0);
  }
`);
const cursor = effect(gpu, `
  struct Params { pos: vec2f }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(1.0, 1.0, 1.0, step(distance(uv, params.pos), 0.02));
  }
`, { set: { params: { pos: [0.5, 0.5] } } });
const scene = bundle(gpu, { target: canvasTarget }, (b) => b.draw(ocean));

// ---cut---
frameLoop(gpu, (frame) => {
  frame.pass(canvasTarget, (pass) => {
    pass.bundles(scene); // the static part, replayed
    pass.draw(cursor); // the dynamic part, encoded fresh on top
  });
});
```

Some draws must stay on the dynamic side. Draws that set a `blendConstant` or a `stencil` `ref` cannot be recorded — bundle encoders have no way to set those pass-level values — so encode them with `pass.draw()`. A bundle also cannot replay inside a `depthReadOnly` pass, because bundles always record with writable depth. Indirect draws record fine: the GPU re-reads the argument buffer on every replay.

## Resizes and sampled targets

A bundle matches replay targets by render signature, not size, so drawing onto a resized surface keeps working:

```ts
import { init, bundle, clock, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasTarget = surface(gpu, canvas);
const ocean = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(0.1, 0.3, 0.6, 1.0);
  }
`);

// ---cut---
function recordScene() {
  return bundle(gpu, { target: canvasTarget }, (b) => b.draw(ocean));
}

let scene = recordScene();
canvasTarget.onResize(() => { scene = recordScene(); }); // needed only if the bundle samples resized resources

frameLoop(gpu, (frame) => {
  frame.pass(canvasTarget, (pass) => pass.bundles(scene));
});
```

## When not to bother

Recording is not free, and a couple of draws per frame cost almost nothing to encode. Bundles pay off with many draws in a hot loop. The full ladder: `effect.draw(target)` for a single pass, `frame(gpu)` to batch passes into one submit, `bundle(gpu)` to skip re-encoding what never changes.

See it live: the [batch rendering example](/examples/batch-rendering) packs four primitive types into one buffer and replays them from a single bundle.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: WebGPU screenshots with agent-browser
description: Use `agent-browser` to verify and capture vgpu previews that run WebGPU on Linux, including containers without a GPU. This complements [Browser testing with Playwright WebGPU](browser-testing.docs.md): use Playwright for API tests and this CDP automation recipe for visual captures.
---

# WebGPU screenshots with agent-browser



## Install and validate the environment

Install `agent-browser` (the verified version is v0.33.0). It automates Chromium through CDP. On Linux or in a container, install Vulkan and virtual-display dependencies before capturing screenshots:

```bash
npm i -g agent-browser@latest
apt-get install -y libvulkan1 mesa-vulkan-drivers xvfb xauth
agent-browser doctor --webgpu --headed
```

`doctor` runs a two-stage render and screenshot probe with a pixel check. In this sandbox, rendering passed with `google swiftshader` and the screenshot passed with `rgb(255,0,0)`.

Do not use headless Chrome without `--webgpu` to verify WebGPU. It does not expose WebGPU by default and can silently produce a black canvas.

## Choose the WebGPU mode

Always use `--webgpu` for the preset. It enables `--enable-unsafe-webgpu` on every platform. On Linux, it also selects software Vulkan through SwiftShader:

```text
--enable-features=Vulkan
--use-angle=vulkan
--use-vulkan=swiftshader
--use-webgpu-adapter=swiftshader
--disable-vulkan-surface
```

SwiftShader does not require a GPU or `/dev/dri`. It is slower than hardware: wait about six seconds and two `requestAnimationFrame` calls before capturing heavy previews, especially `fluid`, `fft-ocean`, and `raymarched-fractal`.

On Linux, headless mode with `--webgpu` renders, but headless Chrome captures the canvas as black; this is an upstream limitation. Add `--headed`. If `DISPLAY` is absent and Xvfb is installed, agent-browser automatically starts a virtual display.

```bash
agent-browser --session vgpu-webgpu --webgpu --headed open http://localhost:3001/preview/gradient
agent-browser --session vgpu-webgpu --webgpu --headed screenshot gradient.png
```

To prefer hardware Vulkan when it is available, override the preset adapters with user arguments; user arguments win over the preset:

```bash
agent-browser --webgpu --args "--use-vulkan=native,--use-webgpu-adapter=default" open http://localhost:3001
```

You can also enable the preset through an environment variable or configuration:

```bash
AGENT_BROWSER_WEBGPU=1 agent-browser --headed open http://localhost:3001
# agent-browser.json: { "webgpu": true }
```

## Capture and validate one preview

Isolate each job in a dedicated session and close it when finished. `screenshot` can succeed even when WebGPU failed and the image is black, so always validate pixels and application state.

```bash
SESSION=vgpu-webgpu
URL=http://localhost:3001/preview/gradient

agent-browser --session "$SESSION" --webgpu --headed open "$URL"
agent-browser --session "$SESSION" --webgpu --headed wait 6000
agent-browser --session "$SESSION" --webgpu --headed eval 'new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))'
agent-browser --session "$SESSION" --webgpu --headed screenshot gradient.png
identify -format '%[fx:standard_deviation]\n' gradient.png
agent-browser --session "$SESSION" close
```

A low or zero standard deviation indicates a uniform or black capture. Also inspect vgpu error overlays: `Preview error`, `WebGPU is not available`, and `No WebGPU adapter was found`.

`agent-browser` does not support `text=` selectors. Use CSS selectors instead; a `text=` attempt can fail silently, which is another reason pixel validation must be the assertion of record.

`agent-browser eval` returns JSON-escaped output. Unescape it before grepping or parsing it:

```bash
META=$(agent-browser --session "$SESSION" --webgpu --headed eval 'JSON.stringify({ text: document.body.innerText, hasCanvas: Boolean(document.querySelector("canvas")), webgpu: Boolean(navigator.gpu) })')
printf '%s' "$META" | sed 's/\\"/"/g' | grep -E 'Preview error|WebGPU is not available|No WebGPU adapter was found'
```

## Capture every vgpu preview

With the documentation server already running from `apps/docs` on port 3001, this recipe rendered and captured all nine `/preview/<slug>` previews in this sweep through SwiftShader. The simplified loop waits for initialization, requires a canvas, captures, and rejects uniform pixels:

```bash
SESSION=vgpu-previews
BASE_URL=http://localhost:3001
for slug in gradient triangle-led-front anti-aliasing black-hole fluid instanced-rendering batch-rendering fft-ocean raymarched-fractal; do
  agent-browser --session "$SESSION" --webgpu --headed open "$BASE_URL/preview/$slug"
  agent-browser --session "$SESSION" --webgpu --headed wait 6000
  agent-browser --session "$SESSION" --webgpu --headed eval 'new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))'
  agent-browser --session "$SESSION" --webgpu --headed eval 'JSON.stringify({hasCanvas:Boolean(document.querySelector("canvas")),webgpu:Boolean(navigator.gpu),text:document.body.innerText})' | sed 's/\\"/"/g' | grep -q '"hasCanvas":true'
  agent-browser --session "$SESSION" --webgpu --headed screenshot "$slug.png"
  identify -format '%[fx:standard_deviation]\n' "$slug.png" | awk '$1 > 50'
done
agent-browser --session "$SESSION" close
```

Do not omit pixel checks or error-overlay checks for individual previews. For a complete automation flow with logs, retries, `navigator.gpu`, and a WebGPU context check, keep this sequence and record console output and errors before closing the session.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Authoring shaders for performance
description: Write WGSL so reflection can build stable layouts. Bindings should be explicit, structs should be host-shareable, and hot paths should avoid per-frame resource identity changes.
---

# Authoring shaders for performance



Layouts mirror what each entry point statically uses, the same way WebGPU's
`layout: 'auto'` behaves: bindings an entry never touches are omitted,
visibility covers only the stages that actually read a binding, and sampled
`f32` textures are declared filterable exactly when the shader samples them
through a filtering sampler (`textureLoad`-only access stays
unfilterable, keeping `rgba32float` readbacks valid). Mismatches fail eagerly
with structured errors — `VGPU-LIMIT-STORAGE-VERTEX`/`-FRAGMENT` when a
storage binding would exceed a device's per-stage limits, and
`VGPU-SET-TEXTURE-FILTERABILITY` when a non-filterable format meets a
filtering sampler — instead of surfacing as native pipeline failures.

## WGSL defaults

```wgsl
struct Globals {
  time: f32,
  mouse: vec2f,
  enabled: u32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
```

* Use `u32` instead of `bool` in host-written uniforms; encode false/true as `0`/`1`.
* Put target resolution in a uniform value sourced from `target.size` or `target.texelSize`.
* Keep imported WGSL modules binding-free. Modules may export structs/functions/constants; entry shaders own `@group/@binding` declarations.
* Prefer storage buffers plus `instances` for many similar particles or sprites.

## JavaScript defaults

```text
const globals = uniforms(gpu, { time: 0, mouse: [0, 0], enabled: 1 });
const draw = draw(gpu, { shader: WGSL, set: { globals } });
await draw.compile(target);
frameLoop(gpu, (f) => {
  globals.set({ time: clock(gpu).time, mouse });
  f.pass({ target }, (p) => p.draw(draw));
});
```

Use the performance playbook before writing a new shader: bundles for static draws, `compile()` for pre-warm, `draw.group()` for many objects, `uniforms(gpu)` for shared state, ping-pong for iterative effects, and target-owned depth/MSAA.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Browser testing with Playwright WebGPU
description: Browser tests should exercise the same public API users copy: `init()`, `surface(gpu, canvas, opts)`, explicit targets, and deterministic frame submission. Avoid hidden app globals and avoid relying on a continuous loop in assertions.
---

# Browser testing with Playwright WebGPU



```text
import { init } from "vgpu";

export async function renderOnce(canvas: HTMLCanvasElement) {
  const gpu = await init();
  const surface = surface(gpu, canvas, { dpr: 1, autoResize: false });
  const effect = effect(gpu, WGSL, { set: { time: 0, texel: surface.texelSize } });
  frame(gpu, (f) => f.pass({ target: surface, clear: [0, 0, 0, 1] }, (p) => p.draw(effect)));
  return gpu;
}
```

## Test checklist

* Use fixed DPR/size (`dpr: 1`, `autoResize: false`, or explicit `size`) for pixel snapshots.
* Submit with `frame(gpu, ...)` for one deterministic frame, not `requestAnimationFrame` loops.
* Read from explicit surfaces or offscreen targets with `target.read()`.
* Keep WGSL imports pure: modules export helpers only; bindings live in the entry shader. If a module declares a binding, fix `VGPU-RESOLVE-MODULE-BINDING`.
* For headless tests use `vgpu/mock` for deterministic unit tests and `vgpu/node` only when Dawn/WebGPU behavior is under test.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: External ticker
description: Drive the vgpu clock from GSAP, Motion, an XR frame callback or a fixed timestep with clock(gpu).advance(dt).
---

# External ticker



# Driving vgpu with an external ticker — GSAP/Motion/XR

By default vgpu owns the clock: every `frame(gpu)` moves `clock(gpu).time` forward by the wall-clock delta since the last frame, and `frameLoop(gpu, cb)` schedules those frames on `requestAnimationFrame`. That is the right default for a page whose only animation is the render.

It stops being the right default the moment something else already owns the timeline: a GSAP or Motion ticker, an XR session's frame callback, a physics loop with a fixed timestep, or a test that must produce the same pixels twice. Two clocks running side by side drift, and drift shows up as animation that stutters against everything else on the page.

The fix is one call. `clock(gpu).advance(dtSeconds)` moves the vgpu clock forward *now*, and claims that frame's tick: the next `frame(gpu)` counts the frame and runs its passes, but does not advance the clock again. One tick per frame, with the manual one winning.

```ts
import { init, clock, effect, frame, surface } from "vgpu";

declare const canvas: HTMLCanvasElement;
declare const gsap: { ticker: { add(cb: (time: number, deltaMs: number) => void): void } };

const gpu = await init();
const canvasSurface = surface(gpu, canvas);
const wave = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// ---cut---
const time = clock(gpu);

// GSAP owns the rAF; vgpu renders inside its tick, on GSAP's delta.
gsap.ticker.add((_total, deltaMs) => {
  time.advance(deltaMs / 1000);          // the clock moves here...
  wave.set({ params: { time: time.time } });
  frame(gpu, (f) => f.pass(canvasSurface, wave)); // ...and not again here
});
```

Note what disappears: there is no `frameLoop(gpu, ...)`. The external ticker is the loop, and `frame(gpu, cb)` is the render — encode, submit, done.

## Motion, XR, and anything else with a delta

Every ticker hands you the same thing under a different name, so the shape never changes:

```ts
import { init, clock, frame } from "vgpu/mock";

const gpu = await init();
const time = clock(gpu);
declare function render(): void;

// ---cut---
// Motion (frame + delta):
//   frame.update(({ delta }) => { time.advance(delta / 1000); render(); });

// WebXR (absolute timestamps, one session frame at a time):
declare const session: { requestAnimationFrame(cb: (timestampMs: number, xrFrame: unknown) => void): number };
let previousMs: number | undefined;
const onXRFrame = (timestampMs: number) => {
  time.advance(previousMs === undefined ? 0 : (timestampMs - previousMs) / 1000);
  previousMs = timestampMs;
  frame(gpu, () => render());
  session.requestAnimationFrame(onXRFrame);
};
session.requestAnimationFrame(onXRFrame);
```

The first XR frame advances by `0`: there is no previous timestamp to measure against, and a made-up first delta is the classic source of a one-frame jump when the headset starts.

## Timescale: slow motion is a multiplication

Because the delta is yours, scaling it is the whole feature — no separate "speed" uniform threaded through every shader, and no second clock:

```ts
import { init, clock, frameLoop } from "vgpu/mock";

const gpu = await init();

// ---cut---
const time = clock(gpu);
let timescale = 1;         // 0 pauses, 0.25 is slow motion, 2 is fast forward
let previousMs = performance.now();

frameLoop(gpu, () => {
  const nowMs = performance.now();
  time.advance(((nowMs - previousMs) / 1000) * timescale);
  previousMs = nowMs;
  // ... render with time.time
});
```

`frameLoop` still schedules the frames; it just no longer decides what a frame is worth. `advance(0)` is legal and is the honest way to pause: the clock stops, frames keep rendering, `frameCount` keeps counting.

## Fixed timestep and determinism

A simulation that must not depend on frame rate advances in fixed steps and renders whatever the accumulator leaves behind:

```ts
import { init, clock, frame, frameLoop } from "vgpu/mock";

const gpu = await init();
declare function step(dt: number): void;
declare function render(): void;

// ---cut---
const time = clock(gpu);
const STEP = 1 / 120;                    // simulate at 120 Hz, render at display rate
let accumulator = 0;
let previousMs = performance.now();

frameLoop(gpu, () => {
  const nowMs = performance.now();
  accumulator += Math.min(0.25, (nowMs - previousMs) / 1000); // clamp: a hidden tab must not spiral
  previousMs = nowMs;

  let advanced = 0;
  while (accumulator >= STEP) {
    step(STEP);
    accumulator -= STEP;
    advanced += STEP;
  }
  time.advance(advanced);                // one advance per frame, however many steps ran
  render();
});
```

The same technique makes headless renders reproducible: drop the wall clock entirely and advance by a constant.

```ts
import { init, clock, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [64, 64] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

// ---cut---
const time = clock(gpu);
for (let i = 0; i < 90; i++) {
  time.advance(1 / 60);                  // frame 90 always lands on t = 1.5s
  frame(gpu, (f) => f.pass(scene, shader));
}
const pixels = await scene.read();       // same bytes on every machine, every run
void pixels;
```

## Rules of the technique

* **One advance per frame.** `advance()` before `frame()` is the pattern. Calling `advance()` twice before a single frame accumulates both deltas into `time` and leaves `deltaTime` at the last one — usually a bug in the ticker wiring.
* **Mixing is fine.** Skip `advance()` for a frame and that frame falls back to the wall-clock delta, measured from the previous tick. There is no mode to switch.
* **`frameCount` counts frames, not advances.** It only moves inside `frame()` / `frameLoop()`, so it stays a reliable "how many times did we render".
* **`advance()` takes seconds.** Most tickers hand out milliseconds — divide by 1000. Negative or non-finite deltas throw `VGPU-CLOCK-DELTA-INVALID` instead of quietly running time backwards.
* **Read the clock, don't cache the numbers.** `clock(gpu)` returns the same live object every time; `const time = clock(gpu)` outside the loop and `time.time` inside it always reads the current value.

## See also

* [Frames](/docs/guides/concepts-frames) — what `frame(gpu, cb)` and `frameLoop(gpu, cb)` actually do.
* `clock` — the full API of the clock, including its error codes.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Getting started
description: Start with the public `vgpu` package. A program has one `Gpu` context, explicit WGSL bindings, and explicit frames. There are no global uniforms: time comes from the frame clock (`clock(gpu).time`, `.deltaTime`, `.frameCount`) and resolution comes from targets (`target.size`, `target.texelSize`).
---

# Getting started



```ts
import { clock, init, effect, frameLoop, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const gradient = effect(gpu, `
struct Params { time: f32, texel: vec2f }
@group(0) @binding(0) var<uniform> params: Params;
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
  return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
}
`, { set: { params: { time: 0, texel: canvasSurface.texelSize } } });

canvasSurface.onResize(() => {
  gradient.set({ params: { texel: canvasSurface.texelSize } });
});

const time = clock(gpu);
frameLoop(gpu, (frame) => {
  gradient.set({ params: { time: time.time } });
  frame.pass(canvasSurface, gradient);
});
```

Two habits keep this correct as it grows: bindings are set by their WGSL
names — `params` is a struct, so its members nest inside it — and `set()`
writes immediately, so the render loop only writes what actually changes
(`time`); size-class values like `texel` belong in the resize handler.

## Validate with static renders

You do not need a browser — or eyes — to prove a shader right. First, verify
the machine can render at all:

```bash
npx vgpu doctor   # JSON verdict: healthy | unhealthy — each problem with its exact fix
```

`doctor` acquires a real adapter and renders a frame; when something is
missing it prescribes the exact package install or environment variable to
set. Once healthy, render headless and read the pixels back — objective
evidence instead of guesswork:

```ts
import { writeFileSync } from "node:fs";
import { PNG } from "pngjs";
import { init, effect, target } from "vgpu/node";

const SHADER = `
  @fragment fn main() -> @location(0) vec4f {
    return vec4f(0.25, 0.5, 0.75, 1.0);
  }
`;
const width = 160;
const height = 90;
const gpu = await init();
const colorTarget = target(gpu, { size: [width, height] }); // small targets stay fast, even on CPU
effect(gpu, SHADER).draw(colorTarget);
const pixels = await colorTarget.read();                   // RGBA bytes — assert on them
const png = new PNG({ width, height });               // ...and write a PNG you can open
png.data.set(pixels);
writeFileSync("frame.png", PNG.sync.write(png));
gpu.dispose();                                        // stops Dawn's polling so the process exits
```

Keep the loop tight: render → read → adjust → render. Every visual claim you
make should be backed by pixels you actually read — assert on `pixels` when you
know the expected value, and open `frame.png` when you need to judge
composition. PNG encoding is project-owned: `pngjs` is one option, any encoder
works.

If the shader lives in its own `.wgsl` file instead of a template string — even
without a bundler — resolve it first with `resolveShader()`:
[Using vgpu without a bundler](/docs/guides/no-bundler).

The full step-by-step playbook — from `vgpu docs` to browser validation — is
[The default workflow for developing shaders with vgpu](/docs/guides/shader-workflow).
When a shader is multi-pass or a user reports a visual bug, do not iterate by
eye: extract the shader's internal values as pixels and diff them against a CPU
reference, following
[Debugging shaders by extracting internal values](/docs/guides/shader-debugging).

## Default choices

* Use `effect(gpu)` for fullscreen fragment work.
* Use `draw(gpu)` for vertex shaders, meshes, storage-driven vertices, instancing, MRT, and depth.
* Use `effect.draw(target)` for simple single-pass draws; use `frame(gpu, (f) => ...)` to batch multi-pass work and `frameLoop(gpu, ...)` for animation.
* Use `set()` for every binding declared in WGSL; missing bindings fail with `VGPU-R1-BINDING-NEVER-SET`.
* Keep plain JS values plain from their first `set()`; if you need user-owned lifetime, pass a resource from the first `set()`.
* Request optional device capabilities at startup with `init({ requiredFeatures: [...] })` — for example `"timestamp-query"` for `timer(gpu)`; a name the adapter lacks fails init with `VGPU-FEATURE-UNSUPPORTED`.

## Where to go next

Read the concept guides in order — each builds on the previous one:

```bash
vgpu docs cat concepts-context.md         # the Gpu context, surfaces, targets
vgpu docs cat concepts-draws.md           # draw(), meshes, instancing
vgpu docs cat concepts-compilation.md     # compile() and pipeline warmup
vgpu docs cat concepts-effects.md         # fragment effects and set()
vgpu docs cat concepts-passes.md          # frame.pass and multi-pass work
vgpu docs cat concepts-frames.md          # frame batching and animation loops
vgpu docs cat concepts-render-bundles.md  # record draws once, replay cheap
```

Shipping this inside an app? Shaders in their own `.wgsl` files need a bundler
loader plus one ambient TypeScript declaration, and the canvas has to live in a
client component:

```bash
vgpu docs cat nextjs.md   # Next.js (Turbopack or webpack), Vite, .wgsl types, canvas component
```

Not using a bundler at all — plain Node, a script, or a test runner? Resolve
`.wgsl` files and their imports yourself:

```bash
vgpu docs cat no-bundler.md   # resolveShader(), headless Node rendering, ESM + CommonJS usage
```

Rendering an actual 3D scene rather than a fullscreen effect? Depth lives on an
offscreen target, so the scene needs two passes — the unified recipe:

```bash
vgpu docs cat two-pass-rendering.md   # offscreen depth target -> composite to canvas
```

For performance work and testing:

```bash
vgpu docs cat /guides/performance-model.docs.md
vgpu docs cat /guides/performance-patterns.docs.md
vgpu docs cat browser-testing
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Guides
description: Task-oriented guides for using vgpu effectively.
---

# Guides



Task-oriented notes for performance, browser testing, measurement, and shader authoring.

## General

<Cards>
  <Card title="Getting started" href="/docs/guides/getting-started" />
</Cards>

## Performance

<Cards>
  <Card title="Performance model" href="/docs/guides/performance-model" />

  <Card title="Performance patterns" href="/docs/guides/performance-patterns" />

  <Card title="Authoring shaders for performance" href="/docs/guides/authoring-for-perf" />

  <Card title="Measuring" href="/docs/guides/measuring" />

  <Card title="Optimize a pass" href="/docs/guides/optimize-pass" />

  <Card title="Shader diagnostics and fix-its" href="/docs/guides/shader-fix-its" />

  <Card title="Performance playbook: write fast vgpu by default" href="/docs/guides/performance-playbook" />
</Cards>

## Testing

<Cards>
  <Card title="Browser testing with Playwright WebGPU" href="/docs/guides/browser-testing" />
</Cards>

## More guides

<Cards>
  <Card title="WebGPU screenshots with agent-browser" href="/docs/guides/agent-browser-webgpu" />

  <Card title="External ticker" href="/docs/guides/external-ticker" />

  <Card title="Using vgpu with Next.js and other bundlers" href="/docs/guides/nextjs" />

  <Card title="Using vgpu without a bundler" href="/docs/guides/no-bundler" />

  <Card title="Publishing WGSL module packages" href="/docs/guides/publishing-wgsl-packages" />

  <Card title="Debugging shaders by extracting internal values" href="/docs/guides/shader-debugging" />

  <Card title="The default workflow for developing shaders with vgpu" href="/docs/guides/shader-workflow" />

  <Card title="Practical texture-format matrix" href="/docs/guides/texture-formats" />

  <Card title="Two-pass rendering: offscreen depth target composited to the canvas" href="/docs/guides/two-pass-rendering" />
</Cards>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Measuring
description: Measure the thing you intend to optimize: CPU encoding, pipeline warm-up, bind-group churn, target memory, or shader cost. The public API makes those boundaries visible.
---

# Measuring



## CPU encoding vs replay

If the CPU is busy rebuilding the same render pass, compare a direct loop with a bundle:

```text
const staticScene = bundle(gpu, { target }, (b) => staticDraws.forEach((draw) => b.draw(draw)));
frameLoop(gpu, (f) => f.pass({ target }, (p) => p.bundles(staticScene)));
```

## First-frame hitches

If the first visible frame stutters, pre-warm target signatures with `compile()`:

```text
const hdr = target(gpu, { size: [256, 256], format: "rgba16float", depth: true, msaa: true });
const draw = draw(gpu, { shader: WGSL, mesh });
await draw.compile(hdr);
```

## Binding churn

If allocations or bind-group count grows every frame, switch repeated JS values to in-place `set()`, shared state to `uniforms(gpu)`, or many object uniforms to `UniformPool` + dynamic offsets.

## GPU pass cost

If a pass looks expensive, confirm it on the GPU before optimizing it. CPU time around `frame.pass(...)` measures encoding only — encoders record commands, they do not run them. Mark the pass with a `timer(gpu)` span instead:

```text
const gpu = await init({ requiredFeatures: ["timestamp-query"] });
const timer = timer(gpu);
timer.onResults((spans) => console.log(`shadows ${spans.shadows}ms, main ${spans.main}ms`));
frameLoop(gpu, (f) => {
  f.pass({ target: shadowMap, timer: timer.span("shadows") }, (p) => p.draw(casters));
  f.pass({ target: scene, timer: timer.span("main") }, (p) => p.draw(world));
});
```

Durations arrive through `onResults` in milliseconds, one or two frames after submit; readback never blocks a frame. A span times the whole pass, not individual draws — move suspect work into its own pass to isolate it.

## Correctness before speed

When measuring visual output, render one deterministic `frame(gpu, ...)` into an explicit target and read it back. Do not measure while also resizing, compiling pipelines lazily, or creating temporary targets inside the loop.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Using vgpu with Next.js and other bundlers
description: Wire the WGSL loader into Next.js (Turbopack or webpack) or Vite, type `.wgsl` imports for TypeScript, and render a shader from a client component with a canvas.
---

# Using vgpu with Next.js and other bundlers



`effect(gpu, source)` takes WGSL as a string, so nothing forces you to use a bundler loader. Reach for the loader when you want shaders in their own `.wgsl` files with `import` between them — the loader resolves that import graph at build time and hands `effect()` one finished shader.

This guide is the bundler half of [Getting started](/docs/guides/getting-started): install, loader config, TypeScript types for `.wgsl` imports, and the client component that owns the canvas.

## Install

```bash
npm install vgpu
```

That is the whole install. `@vgpu/wgsl` (the loaders) and `@vgpu/wgsl-std` (the pure-WGSL standard modules) are dependencies of `vgpu`, so WGSL package imports such as `import { voronoi3d } from "@vgpu/wgsl-std/noise";` resolve with no second install step — under npm, pnpm, and Yarn alike, including pnpm's isolated `node_modules` and Yarn PnP, where transitive packages never appear in your project's `node_modules` tree.

A WGSL package import resolves from your project's own `node_modules` first, so a copy you installed yourself always wins, and then next to `@vgpu/wgsl` itself, which is what makes a transitive install of `@vgpu/wgsl-std` work. `VGPU-WGSL-PKG-NOTFOUND` therefore means the package is reachable from neither — install it (`npm install <pkg>`) and check the specifier spelling. Third-party and workspace packages work the same way, including `import { customNoise } from "@packages/shaders";` for a `workspace:*` package in a monorepo: see [Publishing WGSL module packages](/docs/guides/publishing-wgsl-packages) for the full resolution order and the `exports` map a package needs.

## Next.js with Turbopack

Turbopack is the default dev bundler in current Next.js versions. Register the loader with a top-level `turbopack.rules` entry; `as: "*.js"` is required so Turbopack treats the loader output as a JavaScript module:

```ts
// next.config.ts
const nextConfig = {
  turbopack: {
    rules: {
      "*.wgsl": {
        loaders: ["@vgpu/wgsl/loader-webpack"],
        as: "*.js",
      },
    },
  },
};

export default nextConfig;
```

Requires Next.js 15.5 or newer for the top-level `turbopack` key. Next 15.0–15.2 used `experimental.turbo.rules`, deprecated since. Turbopack runs webpack-compatible loaders through a bridge: `this.addDependency()` is not honored there, but `@vgpu/wgsl` also tracks transitive `.wgsl` reads through Turbopack's patched `fs.readFile`, so editing an imported module still invalidates.

The same shape is exercised end to end by `examples/next-wgsl` in the vgpu repository.

## Next.js with webpack

`next dev` / `next build` without `--turbopack` use webpack. Push a rule from the `webpack` hook — the loader registers itself for `test: /\.wgsl$/`:

```ts
// next.config.ts
type WebpackConfig = { module?: { rules?: unknown[] } };

const nextConfig = {
  webpack(config: WebpackConfig) {
    config.module ??= {};
    config.module.rules ??= [];
    config.module.rules.push({
      test: /\.wgsl$/,
      loader: "@vgpu/wgsl/loader-webpack",
    });
    return config;
  },
};

export default nextConfig;
```

Keep both blocks when your app runs webpack in one command and Turbopack in another: Next reads `turbopack` only under `--turbopack` and calls `webpack()` only without it, so the two configurations coexist. In a project that has Next's own types available, annotate with `NextConfig` (`import type { NextConfig } from "next";`) instead of the local `WebpackConfig` alias; the alias above only exists so this snippet compiles on its own.

Add `options: { minify: true }` to the rule for production builds. Full option reference: `npx vgpu docs cat /@vgpu/wgsl/loader-webpack/index.docs.md`.

## Vite

```ts
// vite.config.ts — wrap in defineConfig() from "vite" if you want its typing
import { wgslVitePlugin } from "@vgpu/wgsl/loader-vite";

export default { plugins: [wgslVitePlugin()] };
```

## Type `.wgsl` imports in TypeScript

TypeScript does not know what a `.wgsl` module is, so `import shader from "./plasma.wgsl"` fails with `TS2307: Cannot find module` until you add an ambient declaration. `@vgpu/wgsl` ships one — reference it from a `.d.ts` file anywhere in your project (`src/wgsl-env.d.ts` is a good spot):

```text
// src/wgsl-env.d.ts
/// <reference types="@vgpu/wgsl/wgsl-types" />
```

Prefer that one-liner: it stays correct if the emitted shape ever changes. If you would rather declare the module yourself — for example to reuse the exported `ShaderSource` type — put this in `src/wgsl-env.d.ts` instead. It must be the first statement in the file: a `declare module` that follows other code is read as a module augmentation and fails with `TS2664`.

```ts
declare module "*.wgsl" {
  import type { ShaderSource } from "@vgpu/wgsl";
  const source: ShaderSource;
  export default source;
}
```

Either way the default export is a `ShaderSource` object (`{ version: 1, wgsl: string }`), **not** a plain string. Pass it straight to `effect(gpu, source)`, which accepts `string | ShaderSource` — do not reach into `.wgsl` yourself.

## Render it from a client component

WebGPU is browser-only, so the canvas lives in a `"use client"` component and `init()` runs in an effect after mount. Keep the vgpu work in a plain function: it is easier to read, and it is the part worth testing.

```ts
// src/app/plasma.ts
import { clock, effect, frameLoop, init, surface } from "vgpu";
import type { FrameLoopHandle } from "vgpu";
import plasmaShader from "./plasma.wgsl";

/** Starts the render loop on `canvas`; call the returned function to tear it down. */
export function startPlasma(canvas: HTMLCanvasElement): () => void {
  let disposed = false;
  let loop: FrameLoopHandle | undefined;
  let gpu: Awaited<ReturnType<typeof init>> | undefined;

  void (async () => {
    gpu = await init();
    if (disposed) return gpu.dispose();

    const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
    const plasma = effect(gpu, plasmaShader, {
      label: "plasma",
      set: { params: { time: 0, texel: canvasSurface.texelSize } },
    });
    canvasSurface.onResize(() => plasma.set({ params: { texel: canvasSurface.texelSize } }));

    const time = clock(gpu);
    loop = frameLoop(gpu, (frame) => {
      plasma.set({ params: { time: time.time } });
      frame.pass(canvasSurface, plasma);
    });
  })();

  return () => {
    disposed = true;
    loop?.stop();
    gpu?.dispose();
  };
}
```

```tsx
// src/app/page.tsx
"use client";

import { useEffect, useRef } from "react";
import { startPlasma } from "./plasma";

export default function Page() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    return startPlasma(canvas);
  }, []);

  return (
    <main style={{ margin: 0, height: "100vh", overflow: "hidden" }}>
      <canvas ref={canvasRef} style={{ display: "block", width: "100%", height: "100%" }} />
    </main>
  );
}
```

The cleanup function matters in development: React's strict mode mounts effects twice, and without `stop()` plus `dispose()` you leak a device and a render loop per remount.

## Validate before you run the app

**`next build`/`next dev` never validate WGSL — neither the webpack loader nor the Turbopack path, for
leaf `.wgsl` files or for import graphs.** The loader/plugin call `resolveShader({ validate: false })`
for import graphs (parsing, purity checks, DCE, mangling, and minification still run, but the
device-backed check does not), and a leaf `.wgsl` file with no imports never calls `resolveShader()`
at all. There is no loader/plugin option to opt into validation — `next build --webpack` and
`next build` (Turbopack) both exit `0` and ship invalid WGSL unchanged. Do not use `next dev`/
`next build` as your shader compiler.

The validation gate is `vgpu check --require-validation`, run in CI or as a pre-commit hook. Check
every `.wgsl` file — including pure helper modules — with the CLI, which resolves the same import
graph the loader does, prints the reflection, and actually validates against a WebGPU device:

```bash
npx vgpu check src/app/plasma.wgsl --require-validation
```

Then prove the pixels in Node instead of squinting at a browser tab: [Getting started](/docs/guides/getting-started) shows the headless render-and-read-pixels loop, and [The default workflow for developing shaders with vgpu](/docs/guides/shader-workflow) is the full playbook.

## Troubleshooting

| Symptom                                               | Cause                                                                                    | Fix                                                                                  |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `TS2307: Cannot find module './x.wgsl'`               | No ambient declaration for `.wgsl`                                                       | Add the `wgsl-env.d.ts` above                                                        |
| `VGPU-WGSL-PKG-NOTFOUND: Package <pkg> was not found` | A WGSL package import is not installed in `node_modules`                                 | `npm install <pkg>`, or fix the specifier                                            |
| `VGPU-WGSL-RUNTIME-IMPORT`                            | The bundler ran the loader synchronously while the WGSL file has top-level imports       | Let the loader use async mode (webpack/Turbopack/Vite all do by default)             |
| `VGPU-RESOLVE-MODULE-BINDING`                         | An imported `.wgsl` module declares `@group`/`@binding`                                  | Keep resources in the entry shader; modules export only structs and functions        |
| Shader compiles but nothing draws                     | Passing `source.wgsl` (a string field) where the object was expected, or no `frame.pass` | Pass the imported object to `effect(gpu, source)`; render inside `frame`/`frameLoop` |

## See also

* `npx vgpu docs cat /@vgpu/wgsl/loader-webpack/index.docs.md` — every loader option
* `npx vgpu docs cat /@vgpu/wgsl/loader-vite/index.docs.md` — the Vite plugin
* `npx vgpu docs cat /@vgpu/wgsl/runtime/resolve-shader.docs.md` — resolving import graphs without a bundler
* `npx vgpu docs cat /@vgpu/wgsl-std/noise/index.docs.md` — WGSL modules you can import by package name
* [Publishing WGSL module packages](/docs/guides/publishing-wgsl-packages) — ship your own `.wgsl` modules as a package, or share them across a monorepo


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Using vgpu without a bundler
description: Resolve a `.wgsl` entry file's import graph with `resolveShader()` and render it from Node, a script, or a test — no webpack, Vite, or Turbopack loader required.
---

# Using vgpu without a bundler



`effect(gpu, source)` and `draw(gpu, { shader })` take WGSL as a plain string, so nothing forces you to use a bundler. This guide is the no-bundler half of [Getting started](/docs/guides/getting-started): resolve a `.wgsl` entry file — and everything it imports — yourself with `resolveShader()`, then render it headless from Node.

## When you need this

* Your shader lives in its own `.wgsl` file(s) rather than a template string, and you are not running webpack, Vite, or Turbopack.
* Your shader imports WGSL packages (`@vgpu/wgsl-std/noise`, your own workspace package) from a plain script, a Node test, or a CI job.
* You want to render and read pixels back without a browser, the way [Getting started](/docs/guides/getting-started) validates a shader with a static render.

If you *are* shipping this inside a bundler-based app, use the loader instead: [Using vgpu with Next.js and other bundlers](/docs/guides/nextjs). Reaching for `readFileSync` and passing the text straight to `effect()` also works — but only while the shader has no `import` of its own; the moment it does, you need the resolver below.

## Resolve a `.wgsl` entry file

`resolveShader()` reads an entry module from disk, follows its imports (relative, `@/`, and package imports like `@vgpu/wgsl-std/noise`), and emits one finished WGSL string.

Here is the entry file the rest of this page uses — a fullscreen effect with one `params` uniform. `effect()` injects the vertex stage and exposes the interpolated `uv`, so the file only declares a fragment entry point:

```wgsl
// shader.wgsl
struct Params { time: f32 }
@group(0) @binding(0) var<uniform> params: Params;

@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
  return vec4f(uv, abs(sin(params.time)), 1.0);
}
```

Resolve it:

```ts
import { fileURLToPath } from "node:url";
import { resolveShader } from "@vgpu/wgsl/runtime";

const resolved = await resolveShader({
  entry: fileURLToPath(new URL("./shader.wgsl", import.meta.url)),
});

// `resolved.wgsl` is a plain string — pass it straight to effect() or draw({ shader }).
console.log(resolved.wgsl.length, resolved.deps.length);
```

The entry path is resolved on disk and may omit `.wgsl` when a matching file or `index.wgsl` exists. Pass `rootDir` when your modules use `@/foo.wgsl` aliases. Full parameters, the return shape, and every `VGPU-WGSL-*` error code live in the [`resolveShader` reference](/docs/reference/wgsl/resolve-shader#resolvedshader) (`npx vgpu docs cat /@vgpu/wgsl/runtime/resolve-shader.docs.md`).

Validate the same file from the command line before you render it — `vgpu check` runs the same resolver and prints the reflection:

```bash
npx vgpu check shader.wgsl
```

## Render it headless with `vgpu/node`

Combine the resolved shader with `vgpu/node`'s `init` / `target` / `effect` and read the pixels back. This is the static-render recipe from [Getting started](/docs/guides/getting-started), with the shader loaded from disk instead of inlined:

```ts
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { PNG } from "pngjs";
import { resolveShader } from "@vgpu/wgsl/runtime";
import { effect, init, target } from "vgpu/node";

const resolved = await resolveShader({
  entry: fileURLToPath(new URL("./shader.wgsl", import.meta.url)),
});

const width = 160;
const height = 90;
const gpu = await init();
const colorTarget = target(gpu, { size: [width, height] });
const shader = effect(gpu, resolved.wgsl, { set: { params: { time: 0 } } });
shader.draw(colorTarget);

const pixels = await colorTarget.read();   // RGBA bytes — assert on them
const png = new PNG({ width, height });
png.data.set(pixels);
writeFileSync("frame.png", PNG.sync.write(png));
gpu.dispose();                              // stops Dawn's polling so the process exits
```

Nothing about this changes when the shader grows: `resolveShader()` inlines the whole import graph, so `effect()` still sees one string. Animating? Call `shader.set({ params: { time } })` and draw again in a loop, reading the target after each draw.

Rendering an actual 3D scene rather than a fullscreen effect? See [Two-pass rendering](/docs/guides/two-pass-rendering) for the offscreen-depth-target recipe — it composes with this same no-bundler setup.

## `@vgpu/wgsl/runtime` works from ESM and CommonJS

The `./runtime` subpath's `exports` map declares both an `import` and a `require` condition, both pointing at the same ESM file — Node resolves the `require` condition and loads it through its native `require(esm)` support. `resolveShader()` is reachable the same way from either module system, no rename or `"type": "module"` needed:

```ts
// CommonJS entry point — no "type": "module" required
const { resolveShader } = require("@vgpu/wgsl/runtime");
```

```ts
// ES module entry point
import { resolveShader } from "@vgpu/wgsl/runtime";
```

Pick whichever matches how the rest of the script/project is written; both resolve to the same file and behave identically.

## Troubleshooting

| Symptom                       | Cause                                                   | Fix                                                                                           |
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `VGPU-WGSL-RES-NOTFOUND`      | The entry path or an imported module does not exist     | Fix the path; `entry` is resolved relative to the process, so build it from `import.meta.url` |
| `VGPU-WGSL-PKG-NOTFOUND`      | A WGSL package import is not installed                  | `npm install <pkg>`, or fix the specifier                                                     |
| `VGPU-RESOLVE-MODULE-BINDING` | An imported `.wgsl` module declares `@group`/`@binding` | Keep resources in the entry shader; modules export only structs and functions                 |

## See also

* [`resolveShader` reference](/docs/reference/wgsl/resolve-shader#resolvedshader) — full signature, options, and every error code.
* [Getting started](/docs/guides/getting-started) — the browser-first walkthrough and the static-render recipe this guide extends.
* [Two-pass rendering](/docs/guides/two-pass-rendering) — offscreen depth target plus composite, for 3D scenes rendered this same headless way.
* [Using vgpu with Next.js and other bundlers](/docs/guides/nextjs) — the bundler-loader alternative.
* [Publishing WGSL module packages](/docs/guides/publishing-wgsl-packages) — how package imports inside your `.wgsl` files resolve.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Optimize a pass
description: Optimize one pass by first deciding what changes every frame.
---

# Optimize a pass



## 0. Measure first

Attach a `timer(gpu)` span (requires `init({ requiredFeatures: ["timestamp-query"] })`) and judge every change by the reported GPU milliseconds:

```text
const timer = timer(gpu);
timer.onResults((spans) => console.log(`pass ${spans.pass}ms`));
frameLoop(gpu, (f) => f.pass({ target, timer: timer.span("pass") }, (p) => p.draw(effect)));
```

## 1. Static commands

If the draw list is static, record it once:

```text
const effectBundle = bundle(gpu, { target }, (b) => {
  b.draw(background);
  b.draw(grid);
});
frameLoop(gpu, (f) => f.pass(target, (p) => p.bundles(effectBundle)));
```

## 2. Animated scalar/vector values

Keep the pass object and write values in place:

```text
const effect = effect(gpu, WGSL, { set: { time: 0, exposure: 1 } });
const time = clock(gpu);
frameLoop(gpu, (f) => {
  effect.set({ time: time.time });
  f.pass(target, effect);
});
```

## 3. Resources that swap

Use ping-pong rather than allocating a new target or storage buffer:

```text
const state = pingPong(gpu, 512, 512, { format: "rgba16float" });
frameLoop(gpu, (f) => {
  step.set({ src: state.read.color });
  f.pass(state.write, step);
  state.swap();
});
```

## 4. Many objects

Use instancing for many copies of the same draw, or use `UniformPool` plus `draw.group()` when every object needs a different uniform block.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Performance model
description: vgpu's public API is organized around stable identities.
---

# Performance model



## Binding ownership

The first `set()` for a binding decides ownership. Plain JS values are lib-owned and can be updated in place; resources are user-owned and their identity is bound directly. Switching ownership triggers `VGPU-R1-OWNERSHIP-FLIP`.

## Identity cache

Bind groups are cached by resource identity. Updating a JS value in place keeps the identity stable; replacing a texture/storage/sampler changes identity and may stale bundles.

## Bundle staleness

Bundles freeze encoded commands and bind groups. Buffer contents may change, and replay targets may resize as long as their render signature matches. Re-record when a sampled resource identity changes; replay reports `VGPU-R3-BUNDLE-STALE`.

## Claimed groups

`draw.group(group, bindGroup)` claims an entire reflected group. vgpu validates the group layout and forbids `set()` into that group. Dynamic offsets are passed at draw time:

```text
p.draw(draw, { offsets: { 1: [offset] } });
```

## Cost model defaults

* Pre-warm pipelines with `await draw.compile(target)`.
* Share globals with `uniforms(gpu)`.
* Use `instances` for repeated geometry.
* Use ping-pong for iterative read/write resources.
* Put depth/MSAA/format on targets, not global state.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Performance patterns
description: This is the quick index. Open `performance-playbook` for copy-paste before/after snippets.
---

# Performance patterns



## Static scene

Use `bundle(gpu, { target }, recorder)` and replay with `p.bundles(bundle)`.

## First-frame stability

Use `await draw.compile(surfaceOrTarget)` so pipeline compilation happens before the transition frame; pass a canvas `surface` or an offscreen `target` explicitly.

## Animated uniforms

Create the pass/draw once and call `.set({ changedValue })`. Do not allocate a new pass or uniform buffer every frame.

## Many objects

Use `instances` when geometry and material are shared. Use `UniformPool` + `draw.group()` + dynamic offsets when each object needs a different uniform block. Skip draws hidden behind occluders with `visibility(gpu)` proxy queries. When a compute pass decides the counts, draw with `indirect` arguments instead of reading them back to the CPU.

## Shared globals

Use one `uniforms(gpu, { time, mouse, camera })` object and bind it into every shader that needs the same struct.

## Iterative effects

Use `pingPong(gpu)` for targets or `pingPongStorage(gpu)` for compute. Do not allocate temporary targets/storage in the loop.

## 3D targets

Create targets with `depth: true` and `msaa: true` when needed; pre-warm those signatures with `await draw.compile(target)`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Performance playbook: write fast vgpu by default
description: This guide is for LLMs and humans writing shaders. Treat these as default shapes, not late-stage optimizations: each **After** snippet is the pattern to copy when the situation matches.
---

# Performance playbook: write fast vgpu by default



## 1. Bundles / replay (`bundle()` + `p.bundles`)

Use when static draws repeat every frame. Bundles freeze commands, bind groups, target formats, sample count, and attachment identity; they do **not** freeze buffer contents.

Before:

```text
frameLoop(gpu, (f) => f.pass({ target: scene }, (p) => {
  p.draw(floor);
  p.draw(walls);
  p.draw(player);
}));
```

After:

```text
const staticScene = bundle(gpu, { target: scene }, (b) => {
  b.draw(floor);
  b.draw(walls);
});
frameLoop(gpu, (f) => f.pass({ target: scene }, (p) => {
  p.bundles(staticScene);
  p.draw(player);
}));
```

Default: bundle static work once and replay with `p.bundles(...)`.

## 2. Pipeline pre-warm (`compile`)

Use before the first visible frame or route transition. This compiles render pipelines for the target color/depth/MSAA signature before the hitch-sensitive frame.

Before:

```text
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()) });
```

After:

```text
const scene = target(gpu, { size: [256, 256], format: "rgba16float", depth: true, msaa: true });
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()) });
await cube.compile(scene);
frame(gpu, (f) => f.pass({ target: scene }, (p) => p.draw(cube)));
```

Default: `await draw.compile(target)` for every target signature a draw will hit before the hitch-sensitive frame. `targets: [target]` remains synchronous creation-time sugar when blocking is acceptable.

## 3. Manual group claim + dynamic offsets (`draw.group`)

Use for hundreds or thousands of objects that share one shader and one bind-group layout. `draw.group()` claims a reflected group; offsets travel per draw call.

Before:

```text
for (const obj of objects) {
  cube.set({ model: obj.model });
  p.draw(cube);
}
```

After:

```text
import { UniformPool, type UniformLayout } from "vgpu/core";

type ObjectUniforms = { model: Float32Array };
const objectLayout: UniformLayout<ObjectUniforms> = {
  size: 64,
  bindGroupLayout: cube.layout(1, { dynamicOffsets: true }),
  encode(value, dst, byteOffset) {
    new Float32Array(dst, byteOffset, 16).set(value.model);
  },
};
const pool = new UniformPool(gpu.device, { capacityBytes: 1 << 20 });
const slot = pool.alloc(objectLayout);
cube.group(1, slot.bindGroup);

frameLoop(gpu, (f) => {
  pool.beginFrame(clock(gpu).frameCount);
  f.pass({ target: scene }, (p) => {
    for (const obj of objects) {
      const offset = slot.push({ model: obj.model });
      p.draw(cube, { offsets: { 1: [offset] } });
    }
  });
  pool.endFrame();
});
```

Default: for many per-object uniforms, allocate a `UniformPool` slot with an `encode(...)` function, call `pool.beginFrame(...)`, push values, draw with offsets, then `pool.endFrame()` before the frame submits.

## 4. `set()` in-place

Use for animated JS values. The first `set()` latches ownership: plain JS values are lib-owned and update in place; resources are user-owned and keep their identity.

Before:

```text
const wave = effect(gpu, WAVE_WGSL, { set: { time: 0, speed: 2 } });
frameLoop(gpu, (frame) => {
  wave.set({ time: clock(gpu).time, speed: 2 });
  frame.pass(target, wave);
});
```

After:

```text
const wave = effect(gpu, WAVE_WGSL, { set: { time: 0, speed: 2 } });
frameLoop(gpu, (frame) => {
  wave.set({ time: clock(gpu).time });
  frame.pass(target, wave);
});
```

Default: create once; update changing numbers/vectors/structs with `set()`. `set()` performs no equality check — a value written every frame is uploaded
every frame, so hoist static and resize-class values out of the render loop.

## 5. Bake static inputs once

Use when a heavy pass produces a texture that does not change every frame.

Before:

```text
frameLoop(gpu, (f) => {
  f.pass({ target: baked }, (p) => p.draw(heavyScene));
  post.set({ src: baked.color, texel: baked.texelSize });
  f.pass({ target: screen }, (p) => p.draw(post));
});
```

After:

```text
frame(gpu, (f) => f.pass({ target: baked }, (p) => p.draw(heavyScene)));
post.set({ src: baked.color, texel: baked.texelSize });
frameLoop(gpu, (f) => f.pass({ target: screen }, (p) => p.draw(post)));
```

Default: if an input is static, bake it outside the loop with one `frame(gpu, ...)`.

## 6. Instancing (`instances`, `vertices`)

Use for N copies of the same geometry. `DrawOptions.instances/vertices/firstInstance` set defaults; `DrawCallOptions.instances/vertices/firstVertex/firstInstance` override per call. `instances: 0` is valid; indexed geometries ignore `vertices` and `firstVertex`.

Before:

```text
for (let i = 0; i < COUNT; i++) {
  particles.set({ particleIndex: i });
  p.draw(particles);
}
```

After:

```text
const particles = draw(gpu, { shader: PARTICLE_WGSL, instances: COUNT, vertices: 6 });
await particles.compile(scene);
particles.set({ particleBuffer });
frameLoop(gpu, (f) => f.pass({ target: scene }, (p) => p.draw(particles)));
```

Default: one draw with `instances` beats N draw calls.

## 7. `uniforms(gpu)` shared values

Use when many shaders consume the same time, camera, mouse, or exposure values.

Before:

```text
const time = clock(gpu);
wave.set({ time: time.time, mouse });
blur.set({ time: time.time, mouse });
post.set({ time: time.time, mouse });
```

After:

```text
const globals = uniforms(gpu, { time: 0, mouse: [0, 0] });
const wave = effect(gpu, WAVE_WGSL, { set: { globals } });
const blur = effect(gpu, BLUR_WGSL, { set: { globals } });
frameLoop(gpu, (frame) => {
  globals.set({ time: clock(gpu).time, mouse });
  frame.pass(target, (pass) => {
    pass.draw(wave);
    pass.draw(blur);
  });
});
```

Default: shared values belong in one `uniforms(gpu)` object.

## 8. Ping-pong (`pingPong`) without churn + two bundles

Use for iterative effects. Ping-pong keeps two stable identities, so bind-group caches can reuse them.

Before:

```text
frameLoop(gpu, (f) => {
  const tmp = target(gpu, { size: [256, 256], format: "rgba16float" });
  sim.set({ src: previous.color });
  f.pass({ target: tmp }, (p) => p.draw(sim));
  previous = tmp;
});
```

After:

```text
const state = pingPong(gpu, 512, 512, { format: "rgba16float" });
const even = bundle(gpu, { target: state.write }, (b) => { sim.set({ src: state.read.color }); b.draw(sim); });
state.swap();
const odd = bundle(gpu, { target: state.write }, (b) => { sim.set({ src: state.read.color }); b.draw(sim); });
state.swap();
let parity = 0;
frameLoop(gpu, (f) => {
  f.pass({ target: state.write }, (p) => p.bundles(parity === 0 ? even : odd));
  state.swap();
  parity ^= 1;
});
```

Default: create ping-pong resources once; if you bundle, record both parity cases and replay the matching one.

## 9. MSAA/depth in the target

Use for 3D anti-aliasing and depth testing. Resolution, depth, color format, and sample count are target state.

Before:

```text
const scene = target(gpu, { size: [256, 256], format: "rgba8unorm" });
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()) });
```

After:

```text
const scene = target(gpu, { size: [256, 256], format: "rgba16float", depth: true, msaa: true });
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()) });
await cube.compile(scene);
frameLoop(gpu, (f) => f.pass({ target: scene, clear: [0, 0, 0, 1] }, (p) => p.draw(cube)));
```

Default: put depth/MSAA on the target; do not invent global render settings.

## 10. Back-face culling (`cull: "back"`)

Use for closed geometries. With the default `cull: "none"`, triangles facing away from the camera still rasterize; culling them drops roughly half of a closed geometry's fragment work.

Before:

```text
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()) });
```

After:

```text
const cube = draw(gpu, { shader: LIT_WGSL, geometry: geometry(gpu, box()), cull: "back" });
```

Default: `cull: "back"` for closed geometries. Keep `"none"` for planes, alpha-tested foliage, and anything seen from both sides.

## 11. Occlusion culling (`visibility()`)

Use for many-object scenes with large occluders. Query a cheap proxy — a bounding box — and skip the expensive draw when the GPU confirmed it was hidden.

Before:

```text
f.pass({ target: scene }, (p) => {
  p.draw(world);
  p.draw(statue); // full cost even when a wall hides it
});
```

After:

```text
const vis = visibility(gpu);
const qStatue = vis.query("statue");
frameLoop(gpu, (f) => {
  f.pass({ target: scene, visibility: vis }, (p) => {
    p.draw(world); // occluders first
    p.occlusion(qStatue, statueProxy); // cheap bounding-box proxy
    if (!qStatue.hidden) p.draw(statue);
  });
});
```

Default: draw occluders first, query proxies, condition real draws on `hidden`. Results lag one or two frames and `hidden` stays `false` until a query confirms zero passing samples, so the fallback is always to draw. The pass target needs `depth: true`.

## 12. Indirect draws and dispatches (`indirect`)

Use when the GPU decides the counts — compute-driven particles, culled instance lists. Reading counts back to the CPU stalls on a round-trip; `indirect` keeps them on the GPU.

Before:

```text
const data = await counts.read(); // GPU-to-CPU round-trip, a frame late
p.draw(particles, { instances: decodeCount(data) });
```

After:

```text
const args = storage(gpu, 16, { indirect: true });
emit.dispatch(Math.ceil(COUNT / 64)); // compute writes the draw arguments into `args`
frameLoop(gpu, (f) => f.pass({ target: scene }, (p) => p.draw(particles, { indirect: args })));
```

Default: counts produced on the GPU stay on the GPU. The same option shape drives compute: `sim.dispatch({ indirect: args })`.

## 13. Time passes before optimizing (`timer()`)

Use before reaching for any pattern above. CPU timers see encoding only — encoders record commands, the GPU runs them later — so a "slow pass" verdict needs GPU timestamps.

Before:

```text
const t0 = performance.now();
frame(gpu, (f) => f.pass({ target: scene }, (p) => p.draw(world)));
const ms = performance.now() - t0; // encode + submit time, not GPU cost
```

After:

```text
const gpu = await init({ requiredFeatures: ["timestamp-query"] });
const timer = timer(gpu);
timer.onResults((spans) => console.log(`main ${spans.main}ms`));
frameLoop(gpu, (f) => f.pass({ target: scene, timer: timer.span("main") }, (p) => p.draw(world)));
```

Default: attach `timer.span(name)` to each pass you plan to touch and optimize the worst milliseconds first. Open `measuring` for what else to measure.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Publishing WGSL module packages
description: Ship reusable WGSL modules as an npm package — exports map pointing at .wgsl files, files array, how consumers install and import it, the monorepo workspace:* case, and how to verify resolution with npx vgpu check.
---

# Publishing WGSL module packages



A WGSL module package is an ordinary npm package whose `exports` map points at `.wgsl` files instead of JavaScript. Consumers install it and import from it inside their shaders:

```wgsl
import { customNoise } from '@acme/shaders/noise';

@fragment fn main(@builtin(position) position: vec4f) -> @location(0) vec4f {
  return vec4f(customNoise(position.xy), 0.0, 0.0, 1.0);
}
```

`@vgpu/wgsl-std` is exactly this kind of package — there is nothing private about the mechanism. Publish a package this way when you want to reuse shader code across apps or share it with other teams; use [a relative import](/docs/guides/shader-workflow) when the code lives in the same project.

## The package

Nothing is compiled and nothing is bundled: you publish the `.wgsl` sources.

```json
{
  "name": "@acme/shaders",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    "./noise": "./src/noise/index.wgsl",
    "./color": "./src/color/index.wgsl"
  },
  "files": ["src/**/*.wgsl", "README.md", "LICENSE"]
}
```

| Field     | Why it matters                                                                                                                                                                                                                      |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `exports` | The only thing the resolver reads. Each subpath maps to one `.wgsl` file: `"./noise": "./src/noise/index.wgsl"` makes `@acme/shaders/noise` resolve to that file. A subpath that is not listed fails with `VGPU-WGSL-PKG-NOTFOUND`. |
| `files`   | Must include the `.wgsl` sources, or `npm pack` publishes a package with no shaders in it. Verify with `npm pack --dry-run`.                                                                                                        |
| `type`    | Irrelevant to WGSL resolution — set it to match the package's JavaScript, if it has any.                                                                                                                                            |

Supported `exports` shapes:

```json
{
  "exports": {
    ".": "./src/index.wgsl",
    "./noise": "./src/noise/index.wgsl",
    "./shaders/*": "./src/*.wgsl"
  }
}
```

* **String targets** — the normal case.
* **Wildcards** (`"./shaders/*": "./src/*.wgsl"`) — one entry exposes a whole directory.
* **Directory targets** — a target that is a directory resolves to `index.wgsl` inside it, and a target with no extension is tried as `<target>.wgsl` and then `<target>/index.wgsl`.
* **Conditional targets** (`{ "import": ..., "default": ... }`) — the `default` branch is selected and a `VGPU-WGSL-PKG-CONDITIONAL` warning is reported. WGSL has no `import`/`require` distinction, so prefer a plain string target.

Inside the package, modules import each other with relative paths (`import { hash } from './hash.wgsl';`) or with bare specifiers for the package's own dependencies. A dependency you import from WGSL is a real `dependencies` entry:

```json
{
  "name": "@acme/fbm",
  "dependencies": { "@acme/shaders": "^1.0.0" }
}
```

Every module in the package must be pure WGSL: exported functions, structs, constants and type aliases. Bindings (`@group`/`@binding`) in an imported module fail with `VGPU-RESOLVE-MODULE-BINDING` — only the entry shader may declare them, so a package cannot dictate a consumer's bind group layout.

## Consuming it

Install it like any other dependency — no loader configuration is specific to WGSL packages beyond the [normal bundler setup](/docs/guides/nextjs):

```bash
npm install @acme/shaders
```

Then import it from a shader with the bare specifier, as in the example at the top of this page. The specifier is resolved against the *importing file's* project, so the same shader works whether it lives in your app or inside another package.

**Declare what you import.** A package you import from WGSL must be in your own `dependencies`; relying on a transitive copy that npm happened to hoist breaks under pnpm and Yarn PnP, where transitives are not visible to your project.

### Monorepos: workspace packages

A workspace package needs no publishing step. Give the shader package the same `exports` map as above and depend on it from the app with the workspace protocol:

```json
{
  "name": "web",
  "dependencies": { "@packages/shaders": "workspace:*" }
}
```

```wgsl
import { customNoise } from '@packages/shaders';
```

The package manager links `packages/shaders` into `apps/web/node_modules/@packages/shaders`, and resolution follows that symlink — including a second hop when the shader package imports another workspace package. Turborepo, pnpm workspaces, npm workspaces and Yarn workspaces all produce a layout that works.

Editing the shared `.wgsl` file invalidates the consumer's build the same way a local file does: resolved dependencies are reported to the bundler, so HMR and rebuilds see the change with no extra configuration.

## Verify resolution

Run `check` on the *entry* shader — the one with the `@fragment`/`@compute` entry point. It resolves the whole import graph, validates the result, and prints the reflection:

```bash
npx vgpu check ./shaders/main.wgsl
```

A resolution failure exits non-zero and names the package:

| Error                                                                         | Cause                                                                         | Fix                                                                               |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `VGPU-WGSL-PKG-NOTFOUND: Package @acme/shaders was not found`                 | The package is not installed in the importing project.                        | `npm install @acme/shaders`, and check the specifier's spelling.                  |
| `VGPU-WGSL-PKG-NOTFOUND: Package export ./fbm was not found in @acme/shaders` | The package is installed but its `exports` map has no such subpath.           | Add the subpath to the package's `exports`, or import an existing one.            |
| `VGPU-WGSL-RES-NOTFOUND: WGSL module … was not found`                         | The `exports` target points at a file the published tarball does not contain. | Add the `.wgsl` glob to `files` and republish; confirm with `npm pack --dry-run`. |
| `VGPU-WGSL-SYM-NOEXPORT`                                                      | The module resolved, but it does not `export` that name.                      | Export it from the package, or fix the imported name.                             |

## How resolution works

Bare specifiers in WGSL are resolved by vgpu, not by the bundler, in this order:

1. **The importing project's `node_modules`**, walking up from the importing file and stopping at its workspace root (a directory with `pnpm-workspace.yaml` or `.git`). A copy in your project therefore always wins over a copy someone else's package brought in, which is what lets you pin or patch a WGSL package.
2. **The same walk from the importing file's real path**, bounded by the workspace root found in step 1 — not by markers along the real path. Under pnpm a package is a symlink into `node_modules/.pnpm`, and its own dependencies sit next to that store entry rather than next to the symlink; resolving the link first is what makes a WGSL package that imports another WGSL package work. Because the boundary comes from step 1, this pass can reach another *spelling* of the same project (the store lives inside it) but never another project: when the real path lands outside the workspace root, the pass is skipped entirely.
3. **Node's resolver, from the importing file, under Yarn PnP only.** PnP keeps packages inside zip archives with no `node_modules` directories to walk, so Yarn's own resolver is asked instead. This requires the PnP runtime to be active in the process doing the resolution, which is the case for `yarn` commands, `yarn node`, and bundlers launched through them; a plain `node script.mjs` in a PnP project has no PnP runtime and reports `VGPU-WGSL-PKG-NOTFOUND`.
4. **`@vgpu/*` packages next to `@vgpu/wgsl` itself**, so `@vgpu/wgsl-std` resolves without a second install even though it reaches your project transitively through `vgpu`. This step is limited to the `@vgpu/` scope: it exists to rescue vgpu's own modules, and must never resolve a third-party specifier that your project did not install.

**`npm link` is not supported for a linked package's own WGSL imports.** A linked package is a symlink to a checkout outside your project, and resolution deliberately stops at your workspace root rather than searching that external tree — otherwise a shader could pick up packages belonging to an unrelated project on the same machine. The linked package itself resolves, and so do its imports if you install them in the linking project; a dependency that exists only next to the external checkout fails with `VGPU-WGSL-PKG-NOTFOUND`. Use a workspace package (above) for local development of a shader package.

Two things do *not* participate in this: `packageMap` and the in-memory `modules` option of `resolveShader()`. When you pass `modules`, there is no filesystem to search, so a bare specifier fails with `VGPU-WGSL-PKG-NOTFOUND` and the fix-it "Map it with packageMap or add the module to modules". Map the package prefix explicitly in that mode.

## Notes

* Keep exported names unlikely to collide: identifiers are namespaced per module during emission, but two packages exporting the same *entry-point* name still conflict in the final shader.
* Ship a `README.md` documenting each subpath and its exported signatures — consumers cannot infer them from types, because there are none.
* Do not publish `.ts` or `.js` wrappers that read the `.wgsl` files at runtime. The point of the `exports` map is that vgpu's resolver, the bundler loaders, and `npx vgpu check` all see the same file.
* **See also:** [Using vgpu with Next.js and other bundlers](/docs/guides/nextjs) for loader and TypeScript setup, [The default workflow for developing shaders](/docs/guides/shader-workflow) for the check/render loop, and [Shader fix-its](/docs/guides/shader-fix-its) for the full error-code list.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Debugging shaders by extracting internal values
description: A shader has no `console.log`. Its only output is pixels, so make the pixels carry the numbers you need: render the shader's internal values into a tiny target, read them back, and compare them against a CPU reference. Use this methodology by default for any multi-pass or mathematically non-trivial shader, and immediately whenever someone reports a visual bug. Do not iterate by eye — while building the `transmission` example, two rounds of by-eye fixes changed the image without fixing it, and one extraction pass found both root bugs.
---

# Debugging shaders by extracting internal values



## 1. Split the WGSL into pure sub-modules

A debug shader should exercise the WGSL that ships. Keep broadly reused math in a pure module, but tightly coupled one-use math can stay beside the entry point. The current `transmission` example uses both shapes: reusable environment sampling lives in `env-common.wgsl`, while its transmission math stays in `glass.wgsl`.

```wgsl
// glass.wgsl
import { env_lod, sample_env } from "./env-common.wgsl";

fn dielectric_fresnel(ior: f32, facing: f32) -> f32 {
  let f0 = pow((ior - 1.0) / (ior + 1.0), 2.0);
  return f0 + (1.0 - f0) * pow(1.0 - facing, 5.0);
}

fn transmission_lod(roughness: f32, levels: f32) -> f32 {
  return pow(roughness, 0.8) * max(levels - 1.0, 0.0);
}
```

Validate the complete import graph with `npx vgpu check ./apps/docs/examples/transmission/glass.wgsl`.

For a raw `effect(gpu)` debug shader, extract the binding-free prefix from the live entry shader and inline its live helper module:

```ts
import { readFileSync } from "node:fs";

const stripModuleSyntax = (source: string): string =>
  source.replace(/^import .*$/gmu, "").replace(/\bexport\s+/gu, "");

const glass = readFileSync(
  "./apps/docs/examples/transmission/glass.wgsl",
  "utf8"
);
const entryStart = glass.indexOf("\n// The glass bends rays");
if (entryStart < 0) throw new Error("Could not isolate transmission math");

const helpers = [
  readFileSync("./apps/docs/examples/transmission/env-common.wgsl", "utf8"),
  glass.slice(0, entryStart),
]
  .map(stripModuleSyntax)
  .join("\n");
```

`effect(gpu)` reflects one raw WGSL string and rejects remaining imports with `VGPU-WGSL-REFLECT-SOURCE-IMPORT`. The extraction therefore uses the exact shipped math while excluding bindings and entry points.

## 2. Encode internals as pixels

Render an 8×1 or 1×1 target where each pixel is a slot and each channel carries one internal value. Pick an encoding that survives `rgba8unorm` quantization, and keep every value in `[0, 1]`:

| Value                  | Encoding                                                   | Decode                             |
| ---------------------- | ---------------------------------------------------------- | ---------------------------------- |
| weight, Fresnel, alpha | write as-is                                                | `byte / 255`                       |
| LOD level              | `lod / (levels - 1)`                                       | `byte / 255 * (levels - 1)`        |
| direction / normal     | `dir * 0.5 + 0.5`, or `dir * 0.1 + 0.5` for unbounded rays | `(byte / 255 - 0.5) * 2` or `* 10` |
| distance, thickness    | `distance * scale` with a fixed scale                      | `byte / 255 / scale`               |

```ts
import { init, effect, target } from "vgpu/node";

// Reuse `helpers` extracted from the shipped shaders in step 1.
declare const helpers: string;

const gpu = await init();
const colorTarget = target(gpu, { size: [8, 1] });

effect(
  gpu,
  `
  ${helpers}
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let slot = i32(floor(uv.x * 8.0));
    if (slot == 0) {
      // three Fresnel probes, one per channel
      return vec4f(dielectric_fresnel(1.5, 0.2), dielectric_fresnel(1.5, 0.5), dielectric_fresnel(1.5, 1.0), 1.0);
    }
    // normalized LOD: divide by levels - 1 so full roughness reaches exactly 1.0
    return vec4f(transmission_lod(0.0, 8.0) / 7.0, transmission_lod(0.5, 8.0) / 7.0, transmission_lod(1.0, 8.0) / 7.0, 1.0);
  }
`
).draw(colorTarget);

const pixels = await colorTarget.read();
const slot0 = [...pixels.slice(0, 3)].map((byte) => byte / 255);
console.log(slot0);
gpu.dispose();
```

Give each slot exactly one meaning and comment it. A debug shader nobody can decode is worthless the next day.

## 3. Compare against a CPU reference

Reimplement the same math in TypeScript and diff value by value. The tolerance is **`2 / 255` ≈ 0.0078** — the quantization floor of an `rgba8unorm` target. Write the comparison to JSON so the run leaves evidence behind:

```ts
import { writeFileSync } from "node:fs";

const tolerance = 2 / 255; // rgba8unorm quantization

const fresnel = (ior: number, facing: number): number => {
  const f0 = ((ior - 1) / (ior + 1)) ** 2;
  return f0 + (1 - f0) * (1 - facing) ** 5;
};

export function compare(
  reference: number[],
  pixels: Uint8Array,
  out: string
): boolean {
  const gpu = reference.map((_, index) => pixels[index] / 255);
  const maxError = Math.max(
    ...reference.map((value, index) => Math.abs(value - gpu[index]))
  );
  const pass = maxError <= tolerance;
  writeFileSync(
    out,
    JSON.stringify({ reference, gpu, maxError, tolerance, pass }, null, 2)
  );
  return pass;
}

export const reference = [
  fresnel(1.5, 0.2),
  fresnel(1.5, 0.5),
  fresnel(1.5, 1),
];
```

The original extraction run used while building `transmission` reported `maxError: 0.0019` against `tolerance: 0.0078` across Fresnel, dispersion weights, LOD selection, refracted ray direction, cube-exit distance, and eleven cone samples. `2 / 255` is only the quantization floor for values **stored** in `rgba8unorm`; for a derived or iterative quantity, set the budget from the algorithm's physical epsilon instead — for example, compare a sphere tracer's impact point after N steps over a filtered field against its hit-distance epsilon, not `2 / 255`. Exit non-zero when `pass` is false so the harness works in CI, and keep the JSON next to the PNGs as the evidence for your claim.

## 4. Dump the intermediate render targets

In a multi-pass pipeline the numbers can be right and the image still wrong, because a pass reads the wrong thing. Write **every** intermediate target to a PNG and look at them one by one:

```ts
import { writeFileSync } from "node:fs";
import { PNG } from "pngjs";
import type { Target } from "vgpu";

export async function dump(target: Target, file: string): Promise<void> {
  const [width, height] = target.size;
  const png = new PNG({ width, height });
  png.data.set(await target.read());
  writeFileSync(file, PNG.sync.write(png));
}
```

### Encode HDR targets before reading

Float targets can be read directly: `target.read()` returns their raw texel bytes, while `target.readFloats()` decodes `rgba16float` and `rgba32float` into a `Float32Array` without clipping HDR values. For a displayable PNG, render an encode pass into a separate `rgba8unorm` target and read that target. Choose an encoding for the quantity: the example maps signed directions with `x * 0.5 + 0.5`; use a fixed range appropriate to distances or radiance instead.

```ts
import { init, effect, sampler, target } from "vgpu/node";

const gpu = await init();
const hdr = target(gpu, { size: [64, 64], format: "rgba16float" });
const encoded = target(gpu, { size: [64, 64], format: "rgba8unorm" });

const encode = effect(
  gpu,
  `
  @group(0) @binding(0) var source: texture_2d<f32>;
  @group(0) @binding(1) var sourceSampler: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let value = textureSampleLevel(source, sourceSampler, uv, 0.0);
    return vec4f(value.rgb * 0.5 + vec3f(0.5), 1.0); // signed direction -> [0, 1]
  }
`
);
encode
  .set({
    source: hdr,
    sourceSampler: sampler(gpu, { minFilter: "linear", magFilter: "linear" }),
  })
  .draw(encoded);
const pixels = await encoded.read();
gpu.dispose();
```

Dump each level of a blur pyramid (`pyramid-0.png` … `pyramid-7.png`), each G-buffer attachment, and the composite. During the original `transmission` investigation, its then-current pipeline used a back-face G-buffer: those historical dumps showed that the top blur levels were never selected and that exit normals followed the camera ray instead of the refracted ray. The current pipeline no longer has that G-buffer, but the lesson remains: neither bug was obvious in the final image, while both were unmistakable in the intermediates.

## 5. Keep renders deterministic

Evidence is only evidence if it reproduces byte for byte. In debug renders:

* Do not read a clock. Pass time in as a fixed constant, never `Date.now()` or `clock(gpu).time`.
* Use a fixed number of warmup frames before the frame you read, and always the same number.
* Jitter by pixel hash, never by frame index — a stable per-pixel rotation breaks up banding without changing between runs:

```wgsl
// glass.wgsl
/** Stable per-pixel rotation: breaks up rings without temporal noise. */
fn cone_rotation(pixel: vec2f) -> f32 {
  return fract(sin(dot(floor(pixel), vec2f(12.9898, 78.233))) * 43758.5453) * 6.28318531;
}
```

* Keep target sizes fixed and small. Two runs of the same harness must produce identical bytes; if they do not, fix the nondeterminism before debugging anything else.

## 6. When the host has no adapter: the Docker fallback

If `npx vgpu doctor` still fails after applying its own fixes, run the harness inside a container with a software GPU. In the vgpu repository, that image is the one CI uses:

```bash
docker build -t vgpu-test-dev:ci -f infra/test-docker/Dockerfile.dev .
docker run --rm -v "$PWD:/workspace" -w /workspace -e VGPU_DOCKER_TEST=1 vgpu-test-dev:ci \
  sh -lc 'Xvfb :99 -screen 0 1024x768x24 & DISPLAY=:99 node snippet.mjs'
```

Mount an output directory (`-v "$OUT:/out"`) and have the harness write its JSON and PNGs there so the evidence survives the container. This is the vgpu repository's own infrastructure: results are deterministic and identical to CI. In other projects, any Linux container with a software Vulkan stack (Mesa lavapipe or SwiftShader) plus Xvfb works the same way.

## See also

* [The default workflow for developing shaders with vgpu](/docs/guides/shader-workflow) — the eight steps this methodology escalates from.
* [Shader diagnostics and fix-its](/docs/guides/shader-fix-its) — error codes such as `VGPU-RESOLVE-MODULE-BINDING`.
* [Getting started](/docs/guides/getting-started) — the minimal headless render-and-read loop.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Shader diagnostics and fix-its
description: Use these messages as the self-correction map for generated shader code. Prefer fixing the shader/binding shape over suppressing errors.
---

# Shader diagnostics and fix-its



## `VGPU-RESOLVE-MODULE-BINDING`

WGSL modules must be pure helpers. A module may export structs, functions, constants, and types, but it must not declare `@group(...) @binding(...)` variables. Move bindings to the entry shader:

```wgsl
// noise.wgsl
export struct NoiseConfig { seed: u32 }
export fn noise(p: vec2f, cfg: NoiseConfig) -> f32 { return f32(cfg.seed) * 0.0; }
```

```wgsl
// entry.wgsl
import { NoiseConfig, noise } from "./noise.wgsl";
@group(0) @binding(0) var<uniform> cfg: NoiseConfig;
```

## `VGPU-SHADER-SOURCE-INVALID`

main API (vgpu) shader arguments are either a WGSL string or a loader `ShaderSource { version: 1, wgsl }`. If importing `.wgsl` returns a URL/object without `version` and `wgsl`, configure `@vgpu/wgsl/loader-vite`, `@vgpu/wgsl/loader-webpack`, or pass a raw WGSL string.

```text
import shader from "./shader.wgsl";
const draw = draw(gpu, { shader });
```

## Stage storage limits: `VGPU-LIMIT-STORAGE-VERTEX` / `VGPU-LIMIT-STORAGE-FRAGMENT`

**Symptom:** creating a draw reports that the selected vertex or fragment entry uses more storage buffers than the device grants for that stage.

**Cause:** bindings statically reached by the selected entry point count against `maxStorageBuffersInVertexStage` or `maxStorageBuffersInFragmentStage` (falling back to `maxStorageBuffersPerShaderStage` when a stage-specific property is unavailable). Unused declarations and resources used only by another stage do not count.

**Fix:** if the adapter supports it, request the reported count through `init({ requiredLimits: { maxStorageBuffersInVertexStage: count } })` or the fragment sibling. For vertex data, prefer `geometry(gpu, ...)` vertex streams where possible. Otherwise reduce the number of storage buffers reached by that stage. The error detail includes `stage`, `entryPoint`, `count`, `limit`, and the `{ name, group, binding }` bindings that were counted.

## Missing binding: `VGPU-R1-BINDING-NEVER-SET`

Every reflected binding must be set by name or covered by a claimed group. Do not rely on globals or implicit buffers.

```text
const effect = effect(gpu, WGSL);
effect.set({ params: { time: clock(gpu).time }, tex: target.color, samp: sampler(gpu) });
```

## Ownership flip: `VGPU-R1-OWNERSHIP-FLIP`

The first `set()` decides ownership. Plain JS values are lib-owned and updated in place. Resources (`Uniform`, storage, textures, samplers, bind groups) are user-owned. Do not switch the same binding from JS value to resource later.

```text
// Pick one from the start:
wave.set({ params: { time: 0 } });     // lib-owned
// or
wave.set({ params: sharedUniform });   // user-owned
```

## Bool host-shareable layouts

Rule of thumb: treat every bool host-shareable uniform as a `u32` in WGSL. WGSL `bool` is not a stable host-shareable uniform field for JS packing. Use `u32` and encode booleans as `0` or `1`.

```wgsl
struct Params { enabled: u32 }
```

## Bundle stale

`VGPU-R3-BUNDLE-STALE` means a bundle was recorded for a different render signature or an old bind-group/resource identity. A bundle survives resizing the target it draws onto when formats/depth/sample count match; re-record after resource identity changes, including sampling a resized target. Plain JS `set()` updates are safe because buffers are written in place.

## Manual bind-group claims

`VGPU-R4-GROUP-CLAIMED`, `VGPU-R4-GROUP-INCOMPATIBLE`, and `VGPU-R4-GROUP-VALIDATION` all point to manual bind-group ownership. Build the bind group with `draw.layout(group)` or `draw.layout(group, { dynamicOffsets: true })`, call `draw.group(group, bindGroup)`, and send dynamic offsets through `p.draw(draw, { offsets })`.

## Compute aliasing

`VGPU-R1-STORAGE-ALIASING` means a writable storage buffer is bound as both source and destination. Use `pingPongStorage(gpu)` and swap after dispatch.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: The default workflow for developing shaders with vgpu
description: Follow these eight steps in order every time you write or change a shader. Each step has one exact command and one thing to check before you move on. Do not skip a step because the shader "looks fine": every claim about pixels must come from pixels you read.
---

# The default workflow for developing shaders with vgpu



## 1. Discover the docs

```bash
npx vgpu docs ls          # top-level paths: /guides plus one entry per package
npx vgpu docs ls /guides  # every guide, by file name
```

The corpus ships inside the package, so it works offline. Subcommands: `ls`, `cat`, `grep`, `find`, `path`, `symbols`. Full reference: `npx vgpu docs cat cli.docs.md`.

## 2. Read getting started

```bash
npx vgpu docs cat getting-started.md
```

`ls /guides` lists the file as `getting-started.docs.md`; both `getting-started.md` and `/guides/getting-started.docs.md` resolve to it. This is the only page that shows the current API end to end — read it before writing any `init()` call.

## 3. Read the concepts you actually need

```bash
npx vgpu docs cat concepts-effects.md   # fullscreen fragment work and set()
npx vgpu docs cat concepts-passes.md    # frame.pass, multi-pass work
npx vgpu docs cat concepts-frames.md    # frame batching and animation loops
```

Single fullscreen shader: read effects only. Anything with more than one target — blur pyramids, G-buffers, transmission — read passes and frames too.

## 4. Validate every WGSL file

```bash
npx vgpu check ./shaders/glass.wgsl
```

The command is `check`, not `wgsl`. It validates without running and prints the shader's reflection as JSON — binding names, groups, entry points — so you can confirm the names you will pass to `set()`. It exits non-zero with the validation errors when the shader is wrong. Run it on every `.wgsl` file, including pure helper modules.

## 5. Verify this machine can render

```bash
npx vgpu doctor --pretty
```

`doctor` acquires a real adapter and renders a real frame, then prints a JSON verdict. When it is unhealthy, apply the fix it prescribes literally — usually `npx vgpu install-software-renderer`, sometimes a `VK_ICD_FILENAMES` export. Re-run until it reports healthy. If it still fails after its own fixes, use the Docker fallback in [Debugging shaders by extracting internal values](/docs/guides/shader-debugging).

## 6. Render a static frame in Node and look at it

Do not open a browser to see whether a shader draws. Render one frame headless, read the pixels, and write a PNG you can open:

```ts
import { writeFileSync } from "node:fs";
import { PNG } from "pngjs";
import { init, effect, target } from "vgpu/node";

const width = 320;
const height = 180;
const gpu = await init();
const colorTarget = target(gpu, { size: [width, height] });

effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.5, 1.0);
  }
`).draw(colorTarget);

const pixels = await colorTarget.read();          // RGBA bytes, row-major, no padding
const png = new PNG({ width, height });
png.data.set(pixels);
writeFileSync("frame.png", PNG.sync.write(png));
gpu.dispose();                                // stops Dawn's polling so the process exits
```

Keep the target small (a few hundred pixels wide) so the loop stays fast on a CPU renderer. Assert on `pixels` when you know the expected value; open the PNG when you need to judge composition. See [Getting started](/docs/guides/getting-started) for the same pattern without PNG encoding.

## 7. Connect it to the browser when the shader ships to a browser

Port the same code to `vgpu` (not `vgpu/node`) with `surface(gpu, canvas, ...)` and drive it from `frameLoop(gpu, ...)`. Test it the way users run it, with the public API and deterministic frame submission: [Browser testing with Playwright WebGPU](/docs/guides/browser-testing).

## 8. Validate visually with agent-browser

```bash
agent-browser --session shader-check --webgpu --headed open http://localhost:3001/preview/glass
agent-browser --session shader-check --webgpu --headed wait 6000
agent-browser --session shader-check --webgpu --headed screenshot glass.png
agent-browser --session shader-check close
```

On Linux always pass `--webgpu --headed`: headless Chrome captures the canvas as black even when the render succeeded. Always check the pixels of the capture, never just its existence. Full recipe, including error-overlay checks: [WebGPU screenshots with agent-browser](/docs/guides/agent-browser-webgpu).

## Escalate to extraction, never to another guess

Stop iterating by eye and switch to numeric extraction the moment any of these is true:

* the shader is multi-pass, or composes several passes (pyramids, G-buffers, ping-pong);
* the math is non-trivial (refraction, dispersion, LOD selection, importance sampling);
* a user reports a visual bug you cannot reproduce as a single wrong number.

In those cases apply [Debugging shaders by extracting internal values](/docs/guides/shader-debugging) **from the start**, before your first fix attempt. The evidence: while building the `transmission` example, two rounds of by-eye fixes changed the image without fixing it. Encoding the shader's internals as pixels and diffing them against a CPU reference found both root bugs — a clamped blur LOD and a G-buffer storing the wrong ray's surface — in a single pass.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Practical texture-format matrix
description: Choose a target format from the operations it must support, not just its channel precision. In vgpu, `format` is passed through to WebGPU for render-target capability; the API rejects a filtering binding for 32-bit float textures unless the device was initialized with `float32-filterable` support. `target.read()` returns raw texel bytes and `target.readFloats()` decodes them to f32 components, so HDR and scalar targets read back directly.
---

# Practical texture-format matrix



## Common color and scalar formats

| Format            | Render target | `sampler` with linear filtering | Readback                     | Practical use                                                                          |
| ----------------- | ------------: | ------------------------------: | ---------------------------- | -------------------------------------------------------------------------------------- |
| `rgba8unorm`      |           Yes |                             Yes | `read()` — 4 bytes/texel     | Default target; screenshots, debug probes, and LDR color.                              |
| `rgba8unorm-srgb` |           Yes |                             Yes | `read()` — 4 bytes/texel     | LDR color encoded as sRGB; do color math in linear space.                              |
| `rgba16float`     |           Yes |                             Yes | `readFloats()` — 4 f32/texel | Filterable HDR color, bloom, and lighting intermediates.                               |
| `rgba32float`     |           Yes |                   No by default | `readFloats()` — 4 f32/texel | Full-precision HDR/data; requires `float32-filterable` to bind to a filtering sampler. |
| `r16float`        |           Yes |                             Yes | `readFloats()` — 1 f32/texel | Filterable scalar data such as a height or distance field.                             |
| `r32float`        |           Yes |                   No by default | `readFloats()` — 1 f32/texel | Full-precision scalar data; requires `float32-filterable` for linear sampling.         |
| `r8unorm`         |           Yes |                             Yes | `read()` — 1 byte/texel      | Compact normalized scalar/mask data.                                                   |

`rg8unorm`, `rg16float`, `rg32float`, and the `bgra8unorm` canvas formats read back too; `read()` always hands back the raw unpadded texel bytes of the format, and `readFloats()` decodes any of them to a `Float32Array` of components (`unorm8` normalized to `[0, 1]`). Depth/stencil, packed (`rgb10a2unorm`, `rg11b10ufloat`), snorm/uint/sint, and compressed formats still throw `VGPU-CORE-UNSUPPORTED-FORMAT`.

“Render target” means the format can be requested through `target(gpu, { format })` as a color attachment in the normal WebGPU profile; use an adapter that supports the requested format and feature set. `rgba32float` is renderable even though it is not linearly filterable by default.

## Reading HDR and scalar targets

`read()` returns `Uint8Array` raw texel bytes for every supported format — 4 bytes per texel for `rgba8unorm`, 8 for `rgba16float`, 16 for `rgba32float` — with row padding removed and BGRA swizzled to RGBA. For anything but 8-bit formats, prefer `readFloats()`, which decodes half/float texels into a `Float32Array` of components and preserves values above `1` and below `0`:

```ts
import { init, target } from "vgpu/node";

const gpu = await init();
const hdr = target(gpu, { size: [256, 256], format: "rgba16float" });
// ...render into hdr...
const floats = await hdr.readFloats(); // 256 * 256 * 4 components, HDR values intact
console.log(floats[0]);
```

An encode pass into an `rgba8unorm` target (mapping values to `[0, 1]`, for example `direction * 0.5 + 0.5`) is still the right tool when the goal is a PNG snapshot or a byte-exact visual diff. See [Debugging shaders by extracting internal values](/docs/guides/shader-debugging) for that recipe. For formats without a readback layout, blit into a supported format first, or write the data into a storage buffer and use `StorageBuffer.read()`.

## Filtering 32-bit float textures

A texture declaration and a sampler must agree. On the default device, bind `rgba32float` and `r32float` with a non-filtering sampler and use `textureLoad`, or select a lower-precision filterable format such as `rgba16float`/`r16float`. If the adapter exposes and the application requests `float32-filterable`, linear sampling becomes valid for the 32-bit float formats.

See also: [Effects](/docs/guides/concepts-effects) for chaining targets, and [Debugging shaders by extracting internal values](/docs/guides/shader-debugging) for readback workflows.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Two-pass rendering: offscreen depth target composited to the canvas
description: A 3D scene needs a depth buffer that surfaces and canvases do not have — render into an offscreen `target(gpu, { depth: true })`, then composite that target onto the canvas in a second pass.
---

# Two-pass rendering: offscreen depth target composited to the canvas



Surfaces and canvases have no depth buffer, and [Draws](/docs/guides/concepts-draws) need one for any real 3D scene. The answer is always the same two passes:

1. Render the geometry into an offscreen `target(gpu, { size, depth: true })`.
2. Draw that target's color texture onto the canvas as one full-screen effect.

[Draws](/docs/guides/concepts-draws), [Passes](/docs/guides/concepts-passes), and [Frames](/docs/guides/concepts-frames) each describe one third of this. This guide is the copy-pasteable whole.

## The recipe

```ts
import { draw, effect, frame, geometry, init, sampler, surface, target } from "vgpu";
import { box, orbit, perspectiveCamera, sphere } from "vgpu/scene";

const gpu = await init();
const canvas = document.querySelector("canvas")!;

// Both objects share one shader; `model.color` tells them apart.
const objectShader = `
  struct Camera { viewProjection: mat4x4f }
  struct Model { model: mat4x4f, color: vec3f }
  @group(0) @binding(0) var<uniform> camera: Camera;
  @group(0) @binding(1) var<uniform> model: Model;

  struct VertexOut { @builtin(position) position: vec4f, @location(0) normal: vec3f }

  @vertex fn vs_main(@location(0) position: vec3f, @location(1) normal: vec3f) -> VertexOut {
    var out: VertexOut;
    out.position = camera.viewProjection * model.model * vec4f(position, 1.0);
    out.normal = (model.model * vec4f(normal, 0.0)).xyz;
    return out;
  }

  @fragment fn fs_main(@location(0) normal: vec3f) -> @location(0) vec4f {
    let light = max(dot(normalize(normal), normalize(vec3f(1.0, 1.0, 1.0))), 0.15);
    return vec4f(model.color * light, 1.0);
  }
`;

// Pass 2 reads pass 1's color texture and writes it to the canvas.
const presentShader = `
  @group(0) @binding(0) var scene: texture_2d<f32>;
  @group(0) @binding(1) var sceneSampler: sampler;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return textureSampleLevel(scene, sceneSampler, uv, 0.0);
  }
`;

// ---cut---
const width = 960;
const height = 540;

// Pass 1 target: offscreen, WITH depth — this is the piece a surface cannot give you.
const scene = target(gpu, { size: [width, height], depth: true });
// Pass 2 target: the canvas the user actually sees.
const canvasSurface = surface(gpu, canvas);

const camera = perspectiveCamera({ fov: 45, aspect: width / height, position: [2.5, 2, 3.5], target: [0, 0, 0] });

const cube = draw(gpu, { shader: objectShader, geometry: geometry(gpu, box({ size: 1 })), cull: "back" });
cube.set({
  camera: { viewProjection: camera.viewProjection },
  model: { model: orbit(0), color: [0.95, 0.45, 0.2] },
});

const ball = draw(gpu, { shader: objectShader, geometry: geometry(gpu, sphere({ radius: 0.6 })), cull: "back" });
ball.set({
  camera: { viewProjection: camera.viewProjection },
  model: { model: orbit(2.1), color: [0.3, 0.6, 1] },
});

// The present pass is a single full-screen effect bound to the offscreen target.
const present = effect(gpu, presentShader, {
  set: { scene, sceneSampler: sampler(gpu, { minFilter: "linear", magFilter: "linear" }) },
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene, clear: [0.04, 0.05, 0.08, 1], clearDepth: 1 }, (pass) => {
    pass.draw(cube);
    pass.draw(ball);
  });
  currentFrame.pass(canvasSurface, present);
});
```

Three things are doing the work:

* **`depth: true` on the offscreen target.** Without it the draws have no depth attachment and the two objects paint over each other in submission order. `clearDepth: 1` resets it every frame; use `clearDepth: 0` together with `depth: { compare: "greater" }` on the draw for reversed-Z in deep scenes.
* **Binding the target itself.** `set({ scene })` passes the `Target` where the WGSL declares a `texture_2d<f32>`; vgpu binds its color texture. Pair it with a `sampler(gpu, ...)` for the `sampler` binding.
* **One `frame()`.** Both passes are encoded into one command encoder and submitted once — see [Frames](/docs/guides/concepts-frames). Do not use one-shot `.draw()` calls inside a frame callback; they submit on their own and break the ordering.

Animating? Move the `frame(gpu, ...)` body into [`frameLoop(gpu, ...)`](/docs/guides/concepts-frames) and re-`set()` the model matrices from `clock(gpu).time` each tick. The targets, draws, and the present effect are all created once, outside the loop.

## Headless / no-bundler variant

Rendering this from Node, a script, or a test instead of a browser? Everything is identical except that the second target is another offscreen target rather than a canvas surface, and you read the pixels back at the end:

```ts
import { draw, effect, frame, geometry, init, sampler, target } from "vgpu/node";
import { box, orbit, perspectiveCamera } from "vgpu/scene";

const objectShader = "/* the same vertex + fragment shader as above */";
const presentShader = "/* the same present shader as above */";
const width = 960;
const height = 540;

// ---cut---
const gpu = await init();
const scene = target(gpu, { size: [width, height], depth: true });
const output = target(gpu, { size: [width, height] });   // stands in for the canvas surface

const camera = perspectiveCamera({ fov: 45, aspect: width / height, position: [2.5, 2, 3.5], target: [0, 0, 0] });
const cube = draw(gpu, { shader: objectShader, geometry: geometry(gpu, box({ size: 1 })), cull: "back" });
cube.set({
  camera: { viewProjection: camera.viewProjection },
  model: { model: orbit(0), color: [0.95, 0.45, 0.2] },
});
const present = effect(gpu, presentShader, {
  set: { scene, sceneSampler: sampler(gpu, { minFilter: "linear", magFilter: "linear" }) },
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene, clear: [0.04, 0.05, 0.08, 1], clearDepth: 1 }, (pass) => {
    pass.draw(cube);
  });
  currentFrame.pass(output, present);
});

const pixels = await output.read();   // RGBA bytes — assert on them, or encode a PNG
gpu.dispose();
```

To load the two shaders from `.wgsl` files instead of inline strings in this setup, resolve them first: [Using vgpu without a bundler](/docs/guides/no-bundler).

## Do you actually need two passes?

* **One full-screen fragment shader, no geometry?** No. `effect(gpu, source).draw(canvasSurface)` renders straight to the canvas — see [Getting started](/docs/guides/getting-started).
* **Flat 2D geometry with explicit paint order?** No. Open a single pass on the canvas and draw in order, as [Passes](/docs/guides/concepts-passes) shows.
* **Any 3D geometry that can occlude itself or another object?** Yes — you need the depth attachment, and only an offscreen target has one.
* **Post-processing on top of a 3D scene?** Yes, and the present pass is where it goes: replace `presentShader` with your post effect, which already samples the scene texture.

## See also

* [Draws](/docs/guides/concepts-draws) — why 3D geometry needs a depth target, plus `cull` and reversed-Z.
* [Passes](/docs/guides/concepts-passes) — the single-shader present-pass pattern used here.
* [Frames](/docs/guides/concepts-frames) — how `frame()` batches passes into one submit, and `frameLoop()` for animation.
* [Getting started](/docs/guides/getting-started) — the browser-first walkthrough this recipe extends.
* [Using vgpu without a bundler](/docs/guides/no-bundler) — loading the shaders above from `.wgsl` files.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Agents
description: Point your agent at npx vgpu, install the vgpu skill, or connect through MCP.
---

# Agents



# Agents

vgpu is an agent-first library. Start with the CLI for documentation versioned with the API in your `node_modules`, install the skill for guided reference loading, or connect the hosted MCP server for remote read-only access.

## Point your agent at the docs

```bash
npx vgpu
```

That's the whole instruction. The command prints its own guide — starting with the agent get-started, `vgpu docs cat getting-started.md` — plus the tools to explore and search every reference page, guide, and error code. Your agent takes it from there.

## Install the skill

```bash
npx skills add vercel-labs/vgpu
```

For agents that support skills, this installs the vgpu skill: the same reference docs plus guidance on when to load each one, so the agent reads only the doc it needs.

## Connect the hosted MCP server

Use `add-mcp` to detect installed MCP clients and add the hosted VGPU server globally:

```bash
npx -y add-mcp https://vgpu.sh/api/mcp -g
```

The installer lets you review its detected clients before writing their configuration. The server requires no authentication and gives the agent two read-only tools: `docs` for searching and resolving the VGPU documentation, and `examples` for finding and reading verified example source.

If the agent needs to download an example, run `npx -y vgpu mcp --project-from-cwd` locally on Linux or macOS. Filesystem writes remain disabled until you explicitly select an output boundary. See the [MCP reference](/docs/mcp) for hosted HTTP details, local stdio setup, safe download destinations, and editor-specific guidance.

Everything an agent learns this way, you can read on this site — it is the human mirror of the same content.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Get started
description: Set up your coding agent and render your first frame with vgpu.
---

# Get started



vgpu is designed to be used with coding agents. Give your agent access to version-matched docs, verified examples, and diagnostics, then choose where to render.

<Cards>
  <Card title="Agents" description="Start here. Run `npx vgpu`, install the vgpu skill, or connect through MCP." href="/docs/get-started/agents" className="col-span-2" />

  <Card title="Web" description="Render a shader to an interactive canvas with Next.js or Vite." href="/docs/get-started/web" />

  <Card title="Node.js" description="Render headlessly, save an image, or run GPU tests without a browser." href="/docs/get-started/node" />
</Cards>

## Next: the concepts

Once you can render on your platform, learn the ideas every vgpu program is built from.

<Cards>
  <Card title="Concepts" description="Context, effects, passes, frames, and render bundles — in reading order." href="/docs/concepts" />
</Cards>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Node.js
description: Render headless in Node.js through Dawn — save a PNG or assert on pixels in a test.
---

# Node.js



# Node.js

In Node.js, vgpu renders headless through Dawn — the same API, no canvas, no browser. Import `init` from `vgpu/node`, render into an offscreen [`Target`](/docs/reference/vgpu/target#target), and read the pixels back.

## Install

```bash
npm install vgpu
```

On Linux, vgpu resolves a working Dawn binary automatically. When the stock
prebuild matches your system it is used directly; on older-GLIBC hosts vgpu
downloads its own portable Dawn build from GitHub Releases, verified against a
pinned SHA-256, and caches it locally. If your install runs with scripts
disabled (`--ignore-scripts`, pnpm v10 allow-lists), the download happens
lazily on first `init()` instead — or run it yourself:

```bash
npx vgpu install-dawn
```

Before your first render, `npx vgpu doctor` checks the machine end to end — it
acquires an adapter and renders a real frame, and when something is missing it
prints the exact fix as JSON. Run it once before debugging anything else.

<Callout type="info">
  Good to know: no GPU and no driver? `npx vgpu install-software-renderer` sets
  up a portable CPU renderer once; `init()` uses it automatically after that.
</Callout>

Air-gapped or custom environments can point `VGPU_DAWN_BINARY` at a local
binary. When no binary can be resolved, `init()` throws a structured
`VGPU-NODE-PREBUILD-MISSING` error that names the exact reason and fix. See
[`createNodeAdapter`](/docs/reference/vgpu-adapter-node/create-node-adapter) for the
full resolution order.

## Choose an environment

Start every new machine with `npx vgpu doctor`. After that, choose the mode
that matches what the environment is supposed to guarantee:

| Environment                               | Setup                                                                                 | Runtime choice                                                                  |
| ----------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Local workstation                         | Install the normal GPU driver                                                         | `init()` — use normal system discovery                                          |
| Server or sandbox without a GPU           | Run `npx vgpu install-software-renderer` once                                         | `init()` — use the cached CPU renderer only when normal discovery finds nothing |
| Deterministic screenshots and pixel tests | Cache the software renderer in the CI image                                           | `init({ adapter: "software" })` — force the same Mesa renderer everywhere       |
| GPU-required CI                           | Install the vendor driver                                                             | `init({ adapter: "hardware" })` — fail instead of silently running on CPU       |
| Air-gapped runner                         | Pre-populate the Dawn and software-renderer caches while the image has network access | Select `auto`, `hardware`, or `software` normally at runtime                    |

### Configure it from TypeScript

```ts
import { init } from "vgpu/node";

const gpu = await init();                        // auto: normal adapter first, cached CPU fallback last
const hardware = await init({ adapter: "hardware" }); // require a real GPU
const software = await init({ adapter: "software" }); // force deterministic CPU rendering

console.log(software.adapter);
// { name: "llvmpipe: Mesa 25.0.7 ...", type: "cpu" }
```

`auto` never downloads the software renderer. It uses lavapipe only after you
have explicitly installed it and normal adapter discovery returned nothing.
A working system adapter always wins.

### Override it from CI

Environment overrides win over the TypeScript option and announce themselves
once on stderr, so CI can force a policy without changing application code:

```bash
VGPU_ADAPTER=software node render.mjs  # deterministic CPU pixels
VGPU_ADAPTER=hardware pnpm test        # require a GPU and fail loudly without one
```

Use the SDK option for the project's declared behavior and the environment
variable for temporary CI or debugging intervention. The browser API does not
need this configuration — `import { init } from "vgpu"` continues to let the
browser choose its adapter.

Whichever mode you use, release the device when the process is done:

```ts
try {
  // render, read pixels, write artifacts
  await gpu.settled();
} finally {
  gpu.dispose();
}
```

For loader resolution, distro-specific dependencies, Docker, and lifecycle
details, see [`createNodeDevice`](/docs/reference/vgpu-adapter-node/create-node-device).

## Save a PNG

`target.read()` resolves to the rendered RGBA bytes. Hand them to any PNG encoder — here, `pngjs`:

```ts
import { writeFileSync } from "node:fs";
import { PNG } from "pngjs";
import { init, effect, target } from "vgpu/node";

const gradientSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.4, 1.0);
  }
`;

const gpu = await init();
const colorTarget = target(gpu, { size: [256, 256] });

effect(gpu, gradientSource).draw(colorTarget);
const pixels = await colorTarget.read(); // RGBA bytes

const png = new PNG({ width: colorTarget.size[0], height: colorTarget.size[1] });
png.data.set(pixels);
writeFileSync("gradient.png", PNG.sync.write(png));
```

## Write a test

The same render runs inside a test. Read the pixels and assert on them:

```ts
import { expect, test } from "vitest";
import { init, effect, target } from "vgpu/node";

const gradientSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.4, 1.0);
  }
`;

test("gradient renders", async () => {
  const gpu = await init();
  const colorTarget = target(gpu, { size: [64, 64] });

  effect(gpu, gradientSource).draw(colorTarget);
  const pixels = await colorTarget.read();

  // center pixel: blue is the constant 0.4 (~102 of 255), alpha is 1.0
  const center = (32 * 64 + 32) * 4;
  expect(pixels[center + 2]).toBeGreaterThan(95);
  expect(pixels[center + 2]).toBeLessThan(110);
  expect(pixels[center + 3]).toBe(255);
});
```

<Callout type="info">
  Good to know: use `vgpu/mock` for deterministic unit tests that need no GPU; reach for `vgpu/node` when real Dawn/WebGPU behavior is under test.
</Callout>

<Callout type="info">
  Good to know: call `gpu.dispose()` when you are done — it stops Dawn's event
  polling so the process exits on its own instead of hanging.
</Callout>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Web
description: Install vgpu, render a gradient to a canvas, and load .wgsl files in Next.js or Vite.
---

# Web



# Web

In the browser, vgpu renders with WebGPU straight to a `<canvas>`. Install the package, write a fragment shader, and draw.

## Install

```bash
npm install vgpu
```

## Render a gradient

Create a context, wrap your canvas in a [`Surface`](/docs/reference/vgpu/surface#surface), and draw an effect:

```ts
import { init, effect, surface } from "vgpu";
import gradientSource from "./gradient.wgsl";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);
const gradient = effect(gpu, gradientSource);

gradient.draw(canvasSurface); // renders immediately
```

```wgsl
// gradient.wgsl
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
  return vec4f(uv, 0.4, 1.0);
}
```

## Load .wgsl files in Next.js

Add the loader rule to your Next.js config:

```js
// next.config.mjs
const config = {
  turbopack: {
    rules: {
      "*.wgsl": {
        loaders: ["@vgpu/wgsl/loader-webpack"],
        as: "*.js",
      },
    },
  },
};

export default config;
```

<Callout type="info">
  Good to know: for webpack builds, add the same loader as a module rule: `config.module.rules.push({ test: /\.wgsl$/, use: "@vgpu/wgsl/loader-webpack" })`.
</Callout>

## Load .wgsl files in Vite

Add the plugin from `@vgpu/wgsl/loader-vite`:

```js
// vite.config.js
import { defineConfig } from "vite";
import { wgslVitePlugin } from "@vgpu/wgsl/loader-vite";

export default defineConfig({
  plugins: [wgslVitePlugin()],
});
```

## Type .wgsl imports

Create a `wgsl-env.d.ts` in your project so TypeScript types every `.wgsl` import as a string:

```tsx
/// <reference types="@vgpu/wgsl/wgsl-types" />
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: API Reference
description: API reference for vgpu packages, generated from the documentation manifest.
---

# API Reference



Browse generated API docs by topic. Every symbol comes from the docs manifest and deep-links to an anchor on its topic page.

## Start with vgpu

<Card title="vgpu" description="Public API: init, Gpu, effect, draw, compute, frame, bundle, target, ping-pong, and uniforms." href="/docs/reference/vgpu" />

## Packages

{/* Prod (https://vgpu.sh/docs/reference) serves each of these 8 packages as a heading with an
    id="<packageSlug>" anchor (apps/docs/app/docs/reference/page.tsx: `<section id={group.packageSlug}>`),
    and the 19 ported `/packages/<pkg>` redirects in lib/docs-redirects.mjs land on exactly those
    fragments (e.g. /packages/vgpu-adapter-node -> /docs/reference#vgpu-adapter-node). The `id` prop
    below is required so those deep links keep landing on a fragment instead of the bare page. */}

<Cards>
  <Card id="vgpu" title="vgpu" description="Public API: init, Gpu, effect, draw, compute, frame, bundle, target, ping-pong, and uniforms." href="/docs/reference/vgpu" />

  <Card id="vgpu-scene" title="vgpu/scene" description="Tree-shakeable geometry, camera, color, and orbit helpers without a retained scene graph." href="/docs/reference/vgpu-scene" />

  <Card id="wgsl" title="@vgpu/wgsl" description="WGSL compile-time entry points, runtime resolution, reflection metadata, and bundler loaders." href="/docs/reference/wgsl" />

  <Card id="wgsl-std" title="@vgpu/wgsl-std" description="Standard WGSL modules for color, fullscreen triangles, hashes, and procedural noise." href="/docs/reference/wgsl-std" />

  <Card id="vgpu-core" title="vgpu/core" description="Advanced escape hatches for native WebGPU handles, buffers, textures, bind groups, and structured uniforms." href="/docs/reference/vgpu-core" />

  <Card id="render" title="@vgpu/render" description="Advanced render tooling for inspection, performance measurement, utilities, and mesh editing." href="/docs/reference/render" />

  <Card id="vgpu-adapter-node" title="@vgpu/adapter-node" description="The Node.js WebGPU adapter — createNodeAdapter() and the Dawn binary resolution it wraps." href="/docs/reference/vgpu-adapter-node" />

  <Card id="vgpu-mock" title="vgpu/mock" description="A software Gpu implementation for tests and CI that has no GPU at all." href="/docs/reference/vgpu-mock" />
</Cards>


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Quickstart: Browser
description: Copy the model output once into a vgpu-owned buffer, then release the tensor.
---

# Quickstart: Browser



In this quickstart you run an ONNX model with ONNX Runtime Web's WebGPU execution provider and consume its output with vgpu shaders — on one shared `GPUDevice`, without the result ever leaving the GPU.

Prerequisites:

* A browser with WebGPU enabled.
* `onnxruntime-web` in your app — vgpu does not bundle or import ORT.
* A model that runs on the WebGPU execution provider.

Create the ORT session first so ORT owns the device, then pass `ort.env.webgpu.device` to `initFromDevice`. Set `preferredOutputLocation: "gpu-buffer"` so outputs stay on the GPU.

## Browser snapshot

Copy the model output once into a vgpu-owned buffer, then release the tensor:

```ts
import * as ort from "onnxruntime-web/webgpu";
import { initFromDevice } from "vgpu";

declare const modelBytes: Uint8Array;
declare const input: ort.Tensor;

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("WebGPU adapter unavailable");
ort.env.webgpu.adapter = adapter;
const session = await ort.InferenceSession.create(modelBytes, {
  executionProviders: ["webgpu"],
  preferredOutputLocation: "gpu-buffer",
});
const gpu = await initFromDevice(await ort.env.webgpu.device);
const output = (await session.run({ input })).output;
const destination = gpu.device.createBuffer({ size: output.gpuBuffer.size, usage: ["storage", "copy_dst"] });
try {
  const encoder = gpu.gpu.createCommandEncoder();
  encoder.copyBufferToBuffer(output.gpuBuffer, 0, destination.gpu, 0, output.gpuBuffer.size);
  gpu.gpu.queue.submit([encoder.finish()]);
  await gpu.device.queue.flush();
} finally {
  destination.dispose();
  output.dispose();
  gpu.dispose();
  await session.release();
}
```

## Browser reference

Wrap the model output directly — zero copies, strict lifetime:

```ts
import * as ort from "onnxruntime-web/webgpu";
import { initFromDevice, type Buffer, type Compute } from "vgpu";

declare const modelBytes: Uint8Array;
declare const input: ort.Tensor;
declare const compute: Compute;
declare const destination: Buffer;
declare const workgroups: number;

const session = await ort.InferenceSession.create(modelBytes, {
  executionProviders: ["webgpu"],
  preferredOutputLocation: "gpu-buffer",
});
const gpu = await initFromDevice(await ort.env.webgpu.device);
const output = (await session.run({ input })).output;
const source = gpu.device.wrapBuffer(output.gpuBuffer);
try {
  compute.set({ source, destination }).dispatch(workgroups);
  await gpu.device.queue.flush();
} finally {
  source.dispose();
  output.dispose();
  gpu.dispose();
  await session.release();
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Buffers & ownership
description: The integration is deliberately narrow: vgpu adopts a device and wraps buffers — everything else stays in your hands.
---

# Buffers & ownership



## SDK signatures

```ts
export interface InitOptions {
  readonly adapter?: VGPUAdapter;
  /** Never set: adoption lives in `initFromDevice(device)`. */
  readonly device?: never;
  readonly powerPreference?: GPUPowerPreference;
  readonly requiredFeatures?: readonly GPUFeatureName[];
  readonly requiredLimits?: RequiredDeviceLimits;
  readonly label?: string;
}

/** vgpu creates the device and destroys it on dispose. */
export declare function init(options?: InitOptions): Promise<Gpu>;

/** vgpu adopts a device it did not create and never destroys it. */
export declare function initFromDevice(device: GPUDevice): Promise<Gpu>;
```

```ts
declare class Device {
  /** Wraps a caller-owned GPUBuffer without taking ownership of its native lifetime. */
  wrapBuffer(buffer: GPUBuffer): Buffer;
}
```

| Error code                     | Condition                                                          |
| ------------------------------ | ------------------------------------------------------------------ |
| `VGPU-INIT-DEVICE-INVALID`     | The device passed to `initFromDevice` fails structural validation. |
| `VGPU-EXTERNAL-BUFFER-INVALID` | `wrapBuffer` receives a value without finite `size` and `usage`.   |

To adopt a device owned by another library, call `initFromDevice(device)` instead of `init()`. The two entry points are separate on purpose: `init()` stays byte-minimal for apps that let vgpu create its own device, and bundlers drop `initFromDevice` entirely when you do not import it.

```ts
import * as ort from "onnxruntime-web/webgpu";
import { init, initFromDevice } from "vgpu";

// Requested device — vgpu creates it and destroys it on dispose.
const owned = await init({ powerPreference: "high-performance" });

// Adopted device — vgpu borrows it and never destroys it.
const gpu = await initFromDevice(await ort.env.webgpu.device);

// A device is not an init() option.
// @ts-expect-error adoption is initFromDevice(device)
await init({ device: await ort.env.webgpu.device });
```

`Device.wrapBuffer` wraps a caller-owned `GPUBuffer` in a vgpu `Buffer` without taking ownership. `wrapper.gpu` is the exact object you passed in. Disposing the wrapper detaches it from vgpu but never destroys the underlying buffer, and dispose is idempotent — a double dispose is a no-op.

```ts
import type { Gpu } from "vgpu";

declare const gpu: Gpu;
declare const raw: GPUBuffer;

const wrapper = gpu.device.wrapBuffer(raw); // raw: a GPUBuffer you own

wrapper.gpu === raw; // true — same object, no copy
wrapper.dispose();   // detaches from vgpu; raw is NOT destroyed
wrapper.dispose();   // no-op — dispose is idempotent
raw.size;            // still valid — its lifetime never left your hands
```

## Platform and ownership matrix

| Platform | Mode      | Public route                                                                 | Required lifetime                                                 |
| -------- | --------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Browser  | Snapshot  | `initFromDevice(device)`; one raw encoder copy into a vgpu-owned destination | retain Tensor through submit and `await gpu.device.queue.flush()` |
| Browser  | Reference | `gpu.device.wrapBuffer(tensor.gpuBuffer)`                                    | retain → wrap → submit → flush → wrapper dispose → Tensor dispose |
| Node     | Snapshot  | same route after user-side pinned Dawn and ORT initialization                | retain Tensor through submit and `await gpu.device.queue.flush()` |
| Node     | Reference | same route after user-side pinned Dawn and ORT initialization                | retain → wrap → submit → flush → wrapper dispose → Tensor dispose |

Choose one of two consumption modes. Snapshot copies the model output once, GPU-to-GPU, into a buffer vgpu owns; after the copy you are decoupled from the runtime and may free its tensor at any time. Reference wraps the runtime's buffer directly with zero copies; the runtime keeps ownership and you must respect its lifetime.

### Reference mode

In reference mode, always `await gpu.device.queue.flush()` before disposing the source tensor. Do not dispose the tensor while vgpu work that reads it is still in flight — skipping the flush is an experimental fast path, not a supported contract.

```ts
import * as ort from "onnxruntime-web/webgpu";
import type { Buffer, Compute, Gpu } from "vgpu";

declare const session: ort.InferenceSession;
declare const input: ort.Tensor;
declare const gpu: Gpu;
declare const compute: Compute;
declare const destination: Buffer;
declare const workgroups: number;

const output = (await session.run({ input })).output;       // 1. retain the tensor
const source = gpu.device.wrapBuffer(output.gpuBuffer);     // 2. wrap — zero copies
compute.set({ source, destination }).dispatch(workgroups);  // 3. submit vgpu work
await gpu.device.queue.flush();                             // 4. flush — required
source.dispose();                                           // 5. drop the wrapper
output.dispose();                                           // 6. runtime may free its buffer now
```

### Snapshot mode

Snapshot needs no new API: copy once with the raw escape hatch (`gpu.gpu` is the shared `GPUDevice`, `buffer.gpu` the raw `GPUBuffer`), then treat the destination as any other vgpu buffer.

```ts
import * as ort from "onnxruntime-web/webgpu";
import type { Gpu } from "vgpu";

declare const session: ort.InferenceSession;
declare const input: ort.Tensor;
declare const gpu: Gpu;

const output = (await session.run({ input })).output;
const destination = gpu.device.createBuffer({
  size: output.gpuBuffer.size,
  usage: ["storage", "copy_dst"],
});

const encoder = gpu.gpu.createCommandEncoder();
encoder.copyBufferToBuffer(output.gpuBuffer, 0, destination.gpu, 0, output.gpuBuffer.size);
gpu.gpu.queue.submit([encoder.finish()]);
await gpu.device.queue.flush();

output.dispose(); // decoupled — destination is yours, independent of the runtime
```

## Scope

The integration is deliberately narrow: vgpu adopts a device and wraps buffers — everything else stays in your hands.

vgpu does not import ORT, resolve ORT WASM assets, mutate Node globals, validate GPUBuffer provenance, interpret Tensor dtype/shape/layout, recover a lost device, or transfer ownership of the borrowed device or buffer.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Overview
description: Share one GPUDevice between vgpu and a machine learning runtime so model outputs stay on the GPU.
---

# Overview



Share one `GPUDevice` between vgpu and a machine learning runtime so model outputs stay on the GPU.

The `initFromDevice(device)` entry point adopts a `GPUDevice` that another library created. Use it when an ML runtime such as ONNX Runtime Web already owns a WebGPU device and you want vgpu shaders to consume the model's output buffers without a CPU roundtrip. The API is model-agnostic: vision, diffusion, embedding, or LLM outputs are all just `GPUBuffer`s to vgpu.

vgpu never takes ownership of an adopted device. `gpu.dispose()` releases the resources vgpu created, but it never calls `device.destroy()` on a device it did not request.

```ts
import * as ort from "onnxruntime-web/webgpu";
import { initFromDevice } from "vgpu";

declare const session: ort.InferenceSession;
declare const input: ort.Tensor;

const gpu = await initFromDevice(await ort.env.webgpu.device); // one shared GPUDevice
const output = (await session.run({ input })).output;      // model output stays on the GPU
const source = gpu.device.wrapBuffer(output.gpuBuffer);    // consume it with zero copies
```

There are two ways to consume a model output: snapshot copies it once, GPU-to-GPU, into a buffer vgpu owns; reference wraps the runtime's buffer directly with zero copies. [Buffers & ownership](/docs/ml/buffers) explains when to use each and the lifetime contract that comes with them.

Start with the quickstart for your environment:

* [Quickstart: Browser](/docs/ml/browser) — share ONNX Runtime Web's device in a page and consume a model output.
* [Quickstart: Node](/docs/ml/node) — the pinned Dawn and ORT recipe, plus the portable fallback for hosts the stock binaries reject.
* [Buffers & ownership](/docs/ml/buffers) — snapshot vs reference, `wrapBuffer` semantics, errors, and lifetime.

For the full API surface, see the [reference](/docs/reference) for `init`, `initFromDevice`, `Device`, and `Buffer`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Quickstart: Node
description: Same route after the pinned Dawn and ORT setup.
---

# Quickstart: Node



In this quickstart you run the same integration in Node, with Dawn providing WebGPU. Node has no global WebGPU, so the recipe wires one up explicitly before ORT initializes — pin the versions below exactly; the combination is what vgpu validates.

The primary Node matrix is Node 22, `webgpu@0.4.0`, `onnxruntime-web@1.27.0`, and vgpu/software-renderer 0.1.6. The `webgpu@0.4.0` Linux ARM64 prebuilt requires glibc 2.38; run the primary recipe and generic-WASM negative proof on x64 CI or ARM64 with glibc 2.38 or newer. The explicitly labeled host fallback uses the supported `@vgpu/adapter-node` portable Dawn and software renderer. Executable recipes are under `experiments/ort-init-device/`.

If `require("webgpu")` fails with a `GLIBC_2.38` error, run `npx vgpu doctor` and follow its prescription: `npx vgpu install-dawn` installs vgpu's portable Dawn build (glibc 2.31 floor), and `npx vgpu install-software-renderer` adds a portable software renderer for hosts without a GPU. The recipes below run unchanged on that fallback.

## Node snapshot

Same route after the pinned Dawn and ORT setup:

```ts
import * as ort from "onnxruntime-web/webgpu";
import { create, globals } from "webgpu";
import { initFromDevice } from "vgpu/node";

declare const modelBytes: Uint8Array;
declare const input: ort.Tensor;
declare function createOrtSession(dawn: GPU, modelBytes: Uint8Array): Promise<ort.InferenceSession>;

Object.assign(globalThis, globals);
const dawn = create([]);
Object.defineProperty(globalThis, "navigator", { configurable: true, value: { gpu: dawn } });
const session = await createOrtSession(dawn, modelBytes);
const rawDevice = await ort.env.webgpu.device;
const gpu = await initFromDevice(rawDevice);
const output = (await session.run({ input })).output;
const destination = gpu.device.createBuffer({ size: output.gpuBuffer.size, usage: ["storage", "copy_dst"] });
try {
  const encoder = gpu.gpu.createCommandEncoder();
  encoder.copyBufferToBuffer(output.gpuBuffer, 0, destination.gpu, 0, output.gpuBuffer.size);
  gpu.gpu.queue.submit([encoder.finish()]);
  await gpu.device.queue.flush();
} finally {
  destination.dispose();
  output.dispose();
  gpu.dispose();
  await session.release();
}
```

## Node reference

Same zero-copy route on Node:

```ts
import * as ort from "onnxruntime-web/webgpu";
import { initFromDevice, type Buffer, type Compute } from "vgpu/node";

declare const session: ort.InferenceSession;
declare const input: ort.Tensor;
declare const compute: Compute;
declare const destination: Buffer;
declare const workgroups: number;

const rawDevice = await ort.env.webgpu.device;
const gpu = await initFromDevice(rawDevice);
const output = (await session.run({ input })).output;
const source = gpu.device.wrapBuffer(output.gpuBuffer);
try {
  compute.set({ source, destination }).dispatch(workgroups);
  await gpu.device.queue.flush();
} finally {
  source.dispose();
  output.dispose();
  gpu.dispose();
  await session.release();
}
```


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: canvasMouseTracker
description: Listens for pointer movement over a canvas and exposes the latest coordinates. Use it to feed shaders with mouse positions without wiring global event listeners.
---

# canvasMouseTracker



## Import

```ts
import { canvasMouseTracker } from "@vgpu/render/utils";
```

## Signature

```ts
export function canvasMouseTracker(spec: CanvasMouseTrackerSpec): CanvasMouseTracker;
```

## Parameters

| Param          | Type                   | Required | Default | Notes                                                                                                            |
| -------------- | ---------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
| spec           | CanvasMouseTrackerSpec | ✔        | —       | Configuration object describing the canvas and how to normalize coordinates.                                     |
| spec.canvas    | HTMLCanvasElement      | ✔        | —       | Target element that receives `pointermove` events.                                                               |
| spec.normalize | boolean                | ✖        | false   | When true, `position` is expressed in \[0, 1] relative coordinates; otherwise uses raw canvas pixel coordinates. |
| spec.flipY     | boolean                | ✖        | false   | Reflects the Y axis (top → bottom) while preserving the chosen unit (normalized or pixel).                       |

**Returns:** `CanvasMouseTracker` — exposes a live `position` tuple (`[x, y]`) and a `dispose()` method that removes the internal event listener.

## Examples

```ts
import { canvasMouseTracker } from "@vgpu/render/utils";

const canvas = document.createElement("canvas");
const effect = { set(values: { readonly mouse: readonly [number, number] }): void { void values; } };
const mouse = canvasMouseTracker({ canvas, normalize: true, flipY: true });

function frame() {
  const [u, v] = mouse.position; // normalized UV with origin at bottom-left
  effect.set({ mouse: [u, v] });
  requestAnimationFrame(frame);
}

frame();
// Later:
mouse.dispose();
```

## Notes

* The initial `position` is `[0, 0]` until the first pointer event fires; guard against that if your shader requires seeded values.
* The tracker prefers `PointerEvent.offsetX/Y` when available, falling back to `clientX/Y` minus the canvas bounds, so it works with both pointer-lock and classic pointer events.
* Call `dispose()` before removing the canvas from the DOM to avoid dangling listeners.
* **See also:** `canvasResolution`, `frameClock`

***

# CanvasMouseTrackerSpec

Configuration object accepted by `canvasMouseTracker`.

## Fields

| Field     | Type              | Required | Default | Notes                                                                                    |
| --------- | ----------------- | -------- | ------- | ---------------------------------------------------------------------------------------- |
| canvas    | HTMLCanvasElement | ✔        | —       | Canvas to observe.                                                                       |
| normalize | boolean           | ✖        | false   | Enables normalized `[0, 1]` output in both axes; otherwise uses raw pixel units.         |
| flipY     | boolean           | ✖        | false   | Mirrors the Y coordinate so normalized output matches WebGPU clip space (`0` at bottom). |

***

# CanvasMouseTracker

Handle returned by `canvasMouseTracker`.

## Fields

| Field    | Type                       | Required | Default | Notes                                                                                                |
| -------- | -------------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------- |
| position | readonly \[number, number] | ✔        | —       | Latest `[x, y]` coordinates (normalized or pixels depending on spec). Always returns a frozen tuple. |
| dispose  | () => void                 | ✔        | —       | Removes the internal `pointermove` listener; idempotent.                                             |


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: canvasResolution
description: Reads the current drawing buffer size of a canvas and optionally watches for resize changes. Use it when you need `[width, height]` uniforms without wiring observers manually.
---

# canvasResolution



## Import

```ts
import { canvasResolution } from "@vgpu/render/utils";
```

## Signature

```ts
export function canvasResolution(
  canvas: HTMLCanvasElement,
  opts?: { readonly observe?: boolean },
): CanvasResolution;
```

## Parameters

| Param        | Type                   | Required | Default | Notes                                                                              |
| ------------ | ---------------------- | -------- | ------- | ---------------------------------------------------------------------------------- |
| canvas       | HTMLCanvasElement      | ✔        | —       | Target element; `width`/`height` are read from its drawing buffer, not CSS pixels. |
| opts         | \{ observe?: boolean } | ✖        | `{}`    | Optional behavior flags. Omitted options behave like `{ observe: false }`.         |
| opts.observe | boolean                | ✖        | false   | When true, attaches a `ResizeObserver` that keeps the cached width/height in sync. |

**Returns:** `CanvasResolution` — exposes `width`, `height`, and `dispose()`. Without `observe: true`, `width` and `height` are the initial drawing-buffer snapshot; with `observe: true`, they update when the `ResizeObserver` callback runs.

## Examples

```ts
import { canvasResolution } from "@vgpu/render/utils";

const canvas = document.createElement("canvas");
const effect = { set(values: { readonly resolution: readonly [number, number] }): void { void values; } };
const resolution = canvasResolution(canvas, { observe: true });

function frame() {
  effect.set({ resolution: [resolution.width, resolution.height] });
  requestAnimationFrame(frame);
}

frame();
// Later:
resolution.dispose();
```

## Notes

* When `observe` is `false`, `width`/`height` stay at the values captured when `canvasResolution(...)` was called. Create a new `CanvasResolution` or pass `observe: true` if later canvas attribute changes must be reflected.
* The helper calls `ResizeObserver.observe(canvas)` only when `observe: true`; call `dispose()` before removing the canvas to disconnect the observer.
* The returned values reflect the drawing buffer size (`canvas.width/height`), which already accounts for DPR scaling if you manage it manually.
* **See also:** `canvasMouseTracker`, `frameClock`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/render/edit — Mesh Editing
description: CPU-side triangle mesh editing built on the render package's half-edge kernel. Use this entrypoint to convert render meshes into `EditableMesh`, select topology, run edit operators, and bake back to render meshes after the edit pipeline.
---

# @vgpu/render/edit — Mesh Editing



## Index

* Creation and conversion: [EditableMesh](#editablemesh), [toEditable](#toeditable), [toEditableWithDiagnostics](#toeditablewithdiagnostics)
* Selection and views: [EditableMeshValue](#editablemeshvalue), [ElementDomain](#elementdomain), [ElementSelection](#elementselection), [ElementSet](#elementset), [ScoredSelection](#scoredselection), [VertexView](#vertexview), [EdgeView](#edgeview), [FaceView](#faceview), [KernelHandle](#kernelhandle)
* Shape editing: [extrude](#extrude), [bevel](#bevel), [inset](#inset)
* Subdivision and cuts: [subdivideEdges](#subdivideedges), [subdivideFaces](#subdividefaces), [loopCut](#loopcut)
* Topology fill/bridge: [bridge](#bridge), [fillHole](#fillhole), [gridFill](#gridfill)
* Dissolve/weld/heal/bake: [dissolveVertices](#dissolvevertices), [dissolveEdges](#dissolveedges), [dissolveFaces](#dissolvefaces), [mergeByDistance](#mergebydistance), [healManifold](#healmanifold), [recomputeNormals](#recomputenormals)
* Diagnostics: [MeshEditError](#meshediterror), [MeshEditWarning](#mesheditwarning)

All imports in this file use the public edit entrypoint:

```ts
import { EditableMesh } from "@vgpu/render/edit";
```

## EditableMesh

Factory object for creating and baking `EditableMeshValue` instances. Use `fromArrays` when you already have typed geometry arrays; use `EditableMesh.toRenderMesh` or `mesh.toRenderMesh` only after the final edit step.

## Import

```ts
import { EditableMesh } from "@vgpu/render/edit";
```

## Signature

```ts
import type { Device } from "@vgpu/core";
import type { EditableMeshValue } from "@vgpu/render/edit";

declare const EditableMesh: {
  fromArrays(opts: {
    readonly positions: Float32Array;
    readonly normals?: Float32Array;
    readonly uvs?: Float32Array;
    readonly colors?: Float32Array;
    readonly indices?: Uint16Array | Uint32Array;
    readonly sharpEdges?: Uint8Array;
    readonly useSmooth?: Uint8Array;
    readonly creaseAngle?: number;
  }): EditableMeshValue;
  toRenderMesh(em: EditableMeshValue, opts: { readonly device: Device }): unknown;
};
```

## Parameters

| Param            | Type                       | Required | Default                                  | Notes                                                                                   |
| ---------------- | -------------------------- | -------- | ---------------------------------------- | --------------------------------------------------------------------------------------- |
| opts.positions   | Float32Array               | ✔        | —                                        | XYZ triples. Vertices with identical XYZ are welded by position during kernel build.    |
| opts.indices     | Uint16Array \| Uint32Array | ✖        | sequential `0..positions.length / 3 - 1` | Triangle indices; length must represent triangles.                                      |
| opts.normals     | Float32Array               | ✖        | omitted                                  | Preserved only as `hasNormals`; edit operators recompute face topology.                 |
| opts.uvs         | Float32Array               | ✖        | omitted                                  | Preserved only as `hasUVs`; operators may drop seams.                                   |
| opts.colors      | Float32Array               | ✖        | omitted                                  | Preserved only as `hasVertexColors`.                                                    |
| opts.sharpEdges  | Uint8Array                 | ✖        | auto from `creaseAngle`                  | Per-edge sharp mask in kernel edge order; if present it overrides auto-sharp detection. |
| opts.useSmooth   | Uint8Array                 | ✖        | all faces smooth (`1`)                   | Per-face smoothing flags.                                                               |
| opts.creaseAngle | number                     | ✖        | `Math.PI / 6`                            | Radians used to auto-mark sharp edges when `sharpEdges` is omitted.                     |
| em               | EditableMeshValue          | ✔        | —                                        | Mesh to bake for static `EditableMesh.toRenderMesh`.                                    |
| opts.device      | Device                     | ✔        | —                                        | Device used to create the render mesh buffers.                                          |

**Returns:** `EditableMeshValue` from `fromArrays`; render `Mesh` from `toRenderMesh`.
**Throws:** — no `MeshEditError` is thrown directly. Invalid or mismatched raw arrays can still produce invalid geometry at JavaScript/WebGPU boundaries.

## Examples

```ts
import { EditableMesh } from "@vgpu/render/edit";

const editableMeshExample = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
  indices: new Uint32Array([0, 1, 2]),
});

const editableFaceCount: number = editableMeshExample.faceCount;
```

## Notes

* The editable kernel is triangle-only; higher-order fills are represented as deterministic triangles.
* Bake once at the end of a pipeline instead of after every operator.
* **See also:** `toEditable`, `toEditableWithDiagnostics`, `EditableMeshValue`, `recomputeNormals`.

## toEditable

Converts a render `Mesh` into an editable half-edge mesh and discards diagnostics. Use when warnings are not important; otherwise call `toEditableWithDiagnostics`.

## Import

```ts
import { toEditable } from "@vgpu/render/edit";
```

## Signature

```ts
import { toEditable } from "@vgpu/render/edit";
import type { EditableMeshValue } from "@vgpu/render/edit";

type EditableInputMesh = Parameters<typeof toEditable>[0];
declare function toEditableSignature(mesh: EditableInputMesh, opts?: { readonly creaseAngle?: number }): EditableMeshValue;
```

## Parameters

| Param            | Type   | Required | Default                                         | Notes                                                                                                                              |
| ---------------- | ------ | -------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| mesh             | Mesh   | ✔        | —                                               | Render mesh-like object with attributes and bounds. Source arrays are used when available; otherwise a bbox box fallback is built. |
| opts.creaseAngle | number | ✖        | `Math.PI / 6` through `EditableMesh.fromArrays` | Radians for auto-sharp edge detection.                                                                                             |

**Returns:** `EditableMeshValue` — editable mesh ready for selections/operators.
**Throws:** — no `MeshEditError` is thrown directly.

## Examples

```ts
import { toEditable } from "@vgpu/render/edit";

const renderMeshForEdit = {
  vertexBuffer: {},
  vertexCount: 3,
  attributes: { stride: 12, position: { offset: 0, format: "float32x3" as const } },
  bbox: { min: new Float32Array([0, 0, 0]), max: new Float32Array([1, 1, 1]) },
} as unknown as Parameters<typeof toEditable>[0];
const toEditableMesh = toEditable(renderMeshForEdit, { creaseAngle: Math.PI / 4 });
```

## Notes

* Tangent-stripping warnings are hidden by this convenience function.
* **See also:** `toEditableWithDiagnostics`, `EditableMesh`, `MeshEditWarning`.

## toEditableWithDiagnostics

Converts a render `Mesh` and returns warnings such as stripped tangents. Use this at import/conversion boundaries so LLM-generated pipelines do not silently lose render-layer data.

## Import

```ts
import { toEditableWithDiagnostics } from "@vgpu/render/edit";
```

## Signature

```ts
import { toEditableWithDiagnostics } from "@vgpu/render/edit";
import type { EditableMeshValue, MeshEditWarning } from "@vgpu/render/edit";

type DiagnosticInputMesh = Parameters<typeof toEditableWithDiagnostics>[0];
declare function toEditableWithDiagnosticsSignature(
  mesh: DiagnosticInputMesh,
  opts?: { readonly creaseAngle?: number },
): { readonly mesh: EditableMeshValue; readonly warnings: readonly MeshEditWarning[] };
```

## Parameters

| Param            | Type   | Required | Default                                         | Notes                                                                                          |
| ---------------- | ------ | -------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| mesh             | Mesh   | ✔        | —                                               | Render mesh-like object. If edit-source arrays are absent, bbox fallback arrays are generated. |
| opts.creaseAngle | number | ✖        | `Math.PI / 6` through `EditableMesh.fromArrays` | Radians for auto-sharp edge detection.                                                         |

**Returns:** `{ mesh, warnings }` — `warnings` is an array of `MeshEditWarning` objects.
**Throws:** — no `MeshEditError` is thrown directly.

## Examples

```ts
import { toEditableWithDiagnostics } from "@vgpu/render/edit";

const renderMeshWithDiagnostics = {
  vertexBuffer: {},
  vertexCount: 3,
  attributes: { stride: 12, position: { offset: 0, format: "float32x3" as const } },
  bbox: { min: new Float32Array([0, 0, 0]), max: new Float32Array([1, 1, 1]) },
} as unknown as Parameters<typeof toEditableWithDiagnostics>[0];
const diagnostics = toEditableWithDiagnostics(renderMeshWithDiagnostics);
const diagnosticsWarnings = diagnostics.warnings.map((warning) => warning.code);
```

## Notes

* Current explicit warning source is `TANGENTS_STRIPPED` when `mesh.attributes.tangent` exists.
* **See also:** `toEditable`, `MeshEditWarning`, `EditableMesh`.

## EditableMeshValue

Runtime shape of an editable mesh. It exposes counts, bounds, typed element sets, material/topology flags, hard-edge selection, an opaque kernel handle, and `toRenderMesh`.

## Import

```ts
import type { EditableMeshValue } from "@vgpu/render/edit";
```

## Signature

```ts
import type { Device } from "@vgpu/core";
import type { ElementSelection, ElementSet, KernelHandle } from "@vgpu/render/edit";

type Vec3 = Float32Array;

declare interface EditableMeshValue {
  readonly vertexCount: number;
  readonly edgeCount: number;
  readonly faceCount: number;
  readonly bounds: { readonly min: Vec3; readonly max: Vec3 };
  readonly vertices: ElementSet<"vertex">;
  readonly edges: ElementSet<"edge">;
  readonly faces: ElementSet<"face">;
  readonly isManifold: boolean;
  readonly hasUVs: boolean;
  readonly hasNormals: boolean;
  readonly hasVertexColors: boolean;
  readonly hardEdges: ElementSelection;
  readonly gpu: { readonly halfEdgeKernel: KernelHandle };
  toRenderMesh(opts: { readonly device: Device }): unknown;
}
```

## Parameters

| Field                                 | Type             | Required | Default | Notes                                                                     |
| ------------------------------------- | ---------------- | -------- | ------- | ------------------------------------------------------------------------- |
| vertexCount / edgeCount / faceCount   | number           | ✔        | —       | Counts in the current immutable editable mesh value.                      |
| bounds.min / bounds.max               | Vec3             | ✔        | —       | Axis-aligned bounds computed from positions.                              |
| vertices / edges / faces              | ElementSet       | ✔        | —       | Selection factories and traversal helpers for each domain.                |
| isManifold                            | boolean          | ✔        | —       | `true` only when every edge has two incident faces in the current kernel. |
| hasUVs / hasNormals / hasVertexColors | boolean          | ✔        | —       | Flags copied from input arrays; most operators rebuild topology arrays.   |
| hardEdges                             | ElementSelection | ✔        | —       | Edge selection where the kernel `isSharp` mask is nonzero.                |
| gpu.halfEdgeKernel                    | KernelHandle     | ✔        | —       | Opaque handle; do not construct or mutate directly.                       |
| toRenderMesh                          | function         | ✔        | —       | Bakes the editable mesh with `{ device }`.                                |

**Returns:** N/A — this is an interface/type export.
**Throws:** N/A — `toRenderMesh` can fail at render/WebGPU boundaries if passed an invalid `Device`.

## Examples

```ts
import { EditableMesh, type EditableMeshValue } from "@vgpu/render/edit";

const editableValueExample: EditableMeshValue = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const editableBoundsMin = editableValueExample.bounds.min;
```

## Notes

* Operators return new `EditableMeshValue` objects; do not assume selections from an old mesh are valid on a new mesh unless the result explicitly returns them.
* **See also:** `ElementSet`, `ElementSelection`, `KernelHandle`, `EditableMesh`.

## ElementDomain

String union naming the selectable topology domains.

## Import

```ts
import type { ElementDomain } from "@vgpu/render/edit";
```

## Signature

```ts
export type ElementDomain = "vertex" | "edge" | "face" | "loop";
```

## Parameters

| Variant    | Type          | Required | Default | Notes                                                                               |
| ---------- | ------------- | -------- | ------- | ----------------------------------------------------------------------------------- |
| `"vertex"` | ElementDomain | ✔        | —       | Vertex selections and `VertexView`.                                                 |
| `"edge"`   | ElementDomain | ✔        | —       | Edge selections, loops, rings, bridge/fill boundaries.                              |
| `"face"`   | ElementDomain | ✔        | —       | Face selections for extrusion/inset/dissolve.                                       |
| `"loop"`   | ElementDomain | ✔        | —       | Declared domain variant; public element sets currently operate on vertex/edge/face. |

**Returns:** N/A — type alias.
**Throws:** N/A.

## Examples

```ts
import type { ElementDomain } from "@vgpu/render/edit";

const selectedDomain: ElementDomain = "edge";
```

## Notes

* Operator validation is strict: passing a selection with the wrong domain throws `WRONG_DOMAIN`.
* **See also:** `ElementSelection`, `ElementSet`.

## ElementSelection

Immutable selection object passed to operators. Use `ElementSet` helpers (`mesh.faces.byIndex`, `mesh.edges.loop`, etc.) instead of hand-building selections unless you need an ordered boundary loop.

## Import

```ts
import type { ElementSelection } from "@vgpu/render/edit";
```

## Signature

```ts
import type { ElementDomain } from "@vgpu/render/edit";

declare interface ElementSelection {
  readonly domain: ElementDomain;
  readonly indices: ReadonlyArray<number>;
  readonly count: number;
  readonly ordered?: boolean;
}
```

## Parameters

| Field   | Type                  | Required | Default           | Notes                                                                              |
| ------- | --------------------- | -------- | ----------------- | ---------------------------------------------------------------------------------- |
| domain  | ElementDomain         | ✔        | —                 | Must match the operator target domain.                                             |
| indices | ReadonlyArray<number> | ✔        | —                 | Element indices in the mesh that owns the selection.                               |
| count   | number                | ✔        | —                 | Usually `indices.length`; operators check `count === 0` for empty selections.      |
| ordered | boolean               | ✖        | omitted / `false` | Required as `true` for loop-boundary operators (`bridge`, `fillHole`, `gridFill`). |

**Returns:** N/A — interface.
**Throws:** N/A.

## Examples

```ts
import { EditableMesh, type ElementSelection } from "@vgpu/render/edit";

const selectionMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const oneFaceSelection: ElementSelection = selectionMesh.faces.byIndex([0]);
```

## Notes

* Selections are mesh-local. Do not reuse a selection from the input mesh against an operator result unless the operator returned that selection for the new mesh.
* **See also:** `ElementSet`, `ScoredSelection`, `MeshEditError`.

## ElementSet

Domain-specific helper collection available as `mesh.vertices`, `mesh.edges`, and `mesh.faces`. Use it to create validated selections and compute simple adjacency expansions.

## Import

```ts
import type { ElementSet } from "@vgpu/render/edit";
```

## Signature

```ts
import type { ElementDomain, ElementSelection, EdgeView, FaceView, ScoredSelection, VertexView } from "@vgpu/render/edit";

type ElementView<D extends ElementDomain> = D extends "vertex" ? VertexView : D extends "edge" ? EdgeView : D extends "face" ? FaceView : never;

declare interface ElementSet<D extends ElementDomain> {
  readonly domain: D;
  readonly count: number;
  where(pred: (e: ElementView<D>) => boolean): ElementSelection;
  scoreBy(score: (e: ElementView<D>) => number): ScoredSelection;
  byIndex(indices: readonly number[]): ElementSelection;
  all(): ElementSelection;
  none(): ElementSelection;
  loop(seedEdge: number): D extends "edge" ? ElementSelection : never;
  ring(seedEdge: number): D extends "edge" ? ElementSelection : never;
  grow(sel: ElementSelection, layers?: number): ElementSelection;
  shrink(sel: ElementSelection, layers?: number): ElementSelection;
  boundaryOf(sel: ElementSelection): ElementSelection;
  connectedComponentOf(seed: number): ElementSelection;
}
```

## Parameters

| Method/Field                  | Type               | Required | Default | Notes                                                                                        |
| ----------------------------- | ------------------ | -------- | ------- | -------------------------------------------------------------------------------------------- |
| domain                        | D                  | ✔        | —       | `"vertex"`, `"edge"`, or `"face"` for the owning set.                                        |
| count                         | number             | ✔        | —       | Number of elements in the owning domain.                                                     |
| where.pred                    | function           | ✔        | —       | Called with `VertexView`, `EdgeView`, or `FaceView`; returned indices are sorted/deduped.    |
| scoreBy.score                 | function           | ✔        | —       | Produces a `ScoredSelection` sorted by score descending.                                     |
| byIndex.indices               | readonly number\[] | ✔        | —       | Out-of-range indices are filtered out.                                                       |
| loop.seedEdge / ring.seedEdge | number             | ✔        | —       | For edge sets only; current implementation returns connected edge walk with `ordered: true`. |
| grow\.layers / shrink.layers  | number             | ✖        | `1`     | Number of adjacency layers to expand or contract.                                            |
| boundaryOf.sel                | ElementSelection   | ✔        | —       | Returns an edge selection around face/vertex selections; edge input returns itself.          |
| connectedComponentOf.seed     | number             | ✔        | —       | Flood-fills adjacent elements in the same domain.                                            |

**Returns:** `ElementSelection` or `ScoredSelection` depending on the method.
**Throws:** — no `MeshEditError` is thrown directly by the public methods.

## Examples

```ts
import { EditableMesh } from "@vgpu/render/edit";

const elementSetMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const longEdges = elementSetMesh.edges.where((edge) => edge.length > 0.5);
```

## Notes

* `loop` and `ring` are typed for edge sets; do not call them on `vertices` or `faces`.
* **See also:** `ElementSelection`, `ScoredSelection`, `VertexView`, `EdgeView`, `FaceView`.

## ScoredSelection

Ranked selection helper returned by `ElementSet.scoreBy`. Use it to pick strongest or weakest candidates without manually sorting indices.

## Import

```ts
import type { ScoredSelection } from "@vgpu/render/edit";
```

## Signature

```ts
import type { ElementDomain, ElementSelection } from "@vgpu/render/edit";

declare interface ScoredSelection {
  readonly domain: ElementDomain;
  readonly entries: ReadonlyArray<{ readonly index: number; readonly score: number }>;
  top(): ElementSelection;
  topN(n: number): ElementSelection;
  threshold(min: number): ElementSelection;
  bottom(): ElementSelection;
  bottomN(n: number): ElementSelection;
}
```

## Parameters

| Method/Field       | Type          | Required | Default | Notes                                             |
| ------------------ | ------------- | -------- | ------- | ------------------------------------------------- |
| domain             | ElementDomain | ✔        | —       | Domain of all ranked entries.                     |
| entries            | ReadonlyArray | ✔        | —       | Sorted by score descending, then index ascending. |
| top / bottom       | function      | ✔        | —       | Equivalent to `topN(1)` / `bottomN(1)`.           |
| topN.n / bottomN.n | number        | ✔        | —       | Negative values clamp to `0`.                     |
| threshold.min      | number        | ✔        | —       | Keeps entries with `score >= min`.                |

**Returns:** `ElementSelection` from ranking methods.
**Throws:** — no `MeshEditError` is thrown directly.

## Examples

```ts
import { EditableMesh, type ScoredSelection } from "@vgpu/render/edit";

const scoredMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 2, 0, 0, 0, 1, 0]),
});
const scoredEdges: ScoredSelection = scoredMesh.edges.scoreBy((edge) => edge.length);
const longestEdge = scoredEdges.top();
```

## Notes

* `bottomN` re-sorts ascending at call time; `entries` remains descending.
* **See also:** `ElementSet`, `ElementSelection`.

## VertexView

Read-only per-vertex data passed to vertex `where`/`scoreBy` callbacks.

## Import

```ts
import type { VertexView } from "@vgpu/render/edit";
```

## Signature

```ts
type Vec3 = Float32Array;

declare interface VertexView {
  readonly index: number;
  readonly position: Vec3;
  readonly normal: Vec3;
  readonly valence: number;
  readonly isBoundary: boolean;
  readonly isManifold: boolean;
}
```

## Parameters

| Field      | Type    | Required | Default | Notes                                          |
| ---------- | ------- | -------- | ------- | ---------------------------------------------- |
| index      | number  | ✔        | —       | Vertex index in the current mesh.              |
| position   | Vec3    | ✔        | —       | XYZ position.                                  |
| normal     | Vec3    | ✔        | —       | Kernel-computed vertex normal.                 |
| valence    | number  | ✔        | —       | Number of incident edges.                      |
| isBoundary | boolean | ✔        | —       | True if any incident edge is boundary.         |
| isManifold | boolean | ✔        | —       | True when local incident topology is manifold. |

**Returns:** N/A — interface.
**Throws:** N/A.

## Examples

```ts
import { EditableMesh, type VertexView } from "@vgpu/render/edit";

const vertexViewMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const boundaryVertices = vertexViewMesh.vertices.where((vertex: VertexView) => vertex.isBoundary);
```

## Notes

* Views are snapshots produced by callbacks; do not store them as stable handles across edits.
* **See also:** `ElementSet`, `EdgeView`, `FaceView`.

## EdgeView

Read-only per-edge data passed to edge `where`/`scoreBy` callbacks.

## Import

```ts
import type { EdgeView } from "@vgpu/render/edit";
```

## Signature

```ts
type Vec3 = Float32Array;

declare interface EdgeView {
  readonly index: number;
  readonly midpoint: Vec3;
  readonly length: number;
  readonly direction: Vec3;
  readonly vertexA: number;
  readonly vertexB: number;
  readonly faceA: number | null;
  readonly faceB: number | null;
  readonly isBoundary: boolean;
  readonly isManifold: boolean;
  readonly isSharp: boolean;
}
```

## Parameters

| Field                | Type           | Required | Default | Notes                                                |
| -------------------- | -------------- | -------- | ------- | ---------------------------------------------------- |
| index                | number         | ✔        | —       | Edge index in current mesh.                          |
| midpoint / direction | Vec3           | ✔        | —       | Derived from endpoints.                              |
| length               | number         | ✔        | —       | Euclidean length.                                    |
| vertexA / vertexB    | number         | ✔        | —       | Endpoint vertex indices.                             |
| faceA / faceB        | number \| null | ✔        | —       | Incident faces; `faceB === null` indicates boundary. |
| isBoundary           | boolean        | ✔        | —       | True for one-sided edges.                            |
| isManifold           | boolean        | ✔        | —       | True when the edge has manifold incidence.           |
| isSharp              | boolean        | ✔        | —       | True when the kernel sharp mask is set.              |

**Returns:** N/A — interface.
**Throws:** N/A.

## Examples

```ts
import { EditableMesh, type EdgeView } from "@vgpu/render/edit";

const edgeViewMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const sharpEdges = edgeViewMesh.edges.where((edge: EdgeView) => edge.isSharp);
```

## Notes

* Use `mesh.hardEdges` when you just need the current sharp-edge selection.
* **See also:** `VertexView`, `FaceView`, `bevel`, `subdivideEdges`.

## FaceView

Read-only per-face data passed to face `where`/`scoreBy` callbacks.

## Import

```ts
import type { FaceView } from "@vgpu/render/edit";
```

## Signature

```ts
type Vec3 = Float32Array;

declare interface FaceView {
  readonly index: number;
  readonly center: Vec3;
  readonly normal: Vec3;
  readonly area: number;
  readonly vertexCount: number;
  readonly vertexIndices: ReadonlyArray<number>;
  readonly edgeIndices: ReadonlyArray<number>;
  readonly useSmooth: boolean;
}
```

## Parameters

| Field                       | Type                  | Required | Default | Notes                                             |
| --------------------------- | --------------------- | -------- | ------- | ------------------------------------------------- |
| index                       | number                | ✔        | —       | Face index in current mesh.                       |
| center / normal             | Vec3                  | ✔        | —       | Derived from triangle vertices and face normal.   |
| area                        | number                | ✔        | —       | Triangle area.                                    |
| vertexCount                 | number                | ✔        | —       | Always `3` for the triangle-only editable kernel. |
| vertexIndices / edgeIndices | ReadonlyArray<number> | ✔        | —       | Triangle vertex/edge indices.                     |
| useSmooth                   | boolean               | ✔        | —       | Per-face smoothing flag.                          |

**Returns:** N/A — interface.
**Throws:** N/A.

## Examples

```ts
import { EditableMesh, type FaceView } from "@vgpu/render/edit";

const faceViewMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const upwardFaces = faceViewMesh.faces.where((face: FaceView) => face.normal[2] > 0);
```

## Notes

* `vertexCount` is still exposed so future kernels can remain source-compatible with code that checks it.
* **See also:** `ElementSet`, `extrude`, `inset`, `dissolveFaces`.

## KernelHandle

Opaque branded handle to the internal half-edge kernel. It exists so editable values can carry kernel data without exposing mutation APIs.

## Import

```ts
import type { KernelHandle } from "@vgpu/render/edit";
```

## Signature

```ts
declare const kernelBrand: unique symbol;
export type KernelHandle = { readonly [kernelBrand]: never };
```

## Parameters

| Field            | Type          | Required | Default | Notes                                                          |
| ---------------- | ------------- | -------- | ------- | -------------------------------------------------------------- |
| branded property | unique symbol | ✔        | —       | Compile-time brand only; not constructible through public API. |

**Returns:** N/A — type alias.
**Throws:** N/A.

## Examples

```ts
import { EditableMesh, type KernelHandle } from "@vgpu/render/edit";

const kernelHandleMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const kernelHandle: KernelHandle = kernelHandleMesh.gpu.halfEdgeKernel;
```

## Notes

* Do not serialize or construct a `KernelHandle`; use public operators instead.
* **See also:** `EditableMeshValue`, `EditableMesh`.

## extrude

Extrudes selected faces along their face normals or an explicit direction. Use it for raised panels, shells, and block-out modeling.

## Import

```ts
import { extrude } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface ExtrudeOptions {
  readonly distance: number;
  readonly inset?: number;
  readonly direction?: readonly [number, number, number];
  readonly mode?: "region" | "individual";
}

declare interface ExtrudeResult {
  readonly mesh: EditableMeshValue;
  readonly sideFaces: ElementSelection;
  readonly capFaces: ElementSelection;
  readonly boundaryEdges: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function extrude(em: EditableMeshValue, faces: ElementSelection, opts: ExtrudeOptions): ExtrudeResult;
```

## Parameters

| Param          | Type                       | Required | Default                     | Notes                                                                 |
| -------------- | -------------------------- | -------- | --------------------------- | --------------------------------------------------------------------- |
| em             | EditableMeshValue          | ✔        | —                           | Source mesh.                                                          |
| faces          | ElementSelection           | ✔        | —                           | Must be a non-empty face selection from `em`.                         |
| opts.distance  | number                     | ✔        | —                           | Offset distance along selected face normal or normalized `direction`. |
| opts.inset     | number                     | ✖        | `0`                         | Fraction toward each face center before lifting. No clamp is applied. |
| opts.direction | `[number, number, number]` | ✖        | selected face normal        | Normalized internally; zero vector behaves as length `1` denominator. |
| opts.mode      | `"region" \| "individual"` | ✖        | accepted but not used in v1 | Current implementation extrudes each selected triangle independently. |

**Returns:** `ExtrudeResult` — edited mesh plus side, cap, and boundary-edge selections on the result mesh.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `faces.domain !== "face"`; `EMPTY_SELECTION` if `faces.count === 0`.

## Examples

```ts
import { EditableMesh, extrude } from "@vgpu/render/edit";

const extrudeMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const extruded = extrude(extrudeMesh, extrudeMesh.faces.byIndex([0]), { distance: 0.2, inset: 0.1 });
```

## Notes

* Source faces are removed; new side and cap faces are returned for highlighting/chaining.
* **See also:** `inset`, `bevel`, `recomputeNormals`.

## bevel

Bevels selected edges by shrinking incident faces and inserting strip faces. Use it to soften hard edges; v1 supports a single segment.

## Import

```ts
import { bevel } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface BevelOptions {
  readonly offset: number;
  readonly segments?: number;
  readonly profile?: number;
  readonly affect?: "edges" | "vertices";
  readonly markSharp?: boolean;
}

declare interface BevelResult {
  readonly mesh: EditableMeshValue;
  readonly newFaces: ElementSelection;
  readonly originalFaces: ElementSelection;
  readonly profileLoops: readonly ElementSelection[];
  readonly warnings?: readonly MeshEditWarning[];
}

declare function bevel(em: EditableMeshValue, edges: ElementSelection, opts: BevelOptions): BevelResult;
```

## Parameters

| Param          | Type                    | Required | Default                     | Notes                                                                                  |
| -------------- | ----------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------- |
| em             | EditableMeshValue       | ✔        | —                           | Source mesh.                                                                           |
| edges          | ElementSelection        | ✔        | —                           | Must be a non-empty edge selection.                                                    |
| opts.offset    | number                  | ✔        | —                           | Fraction toward each incident face center; clamped to `[0, 0.49]`.                     |
| opts.segments  | number                  | ✖        | `1`                         | Any value other than `1` emits `BEVEL_SEGMENTS_CLAMPED`; geometry remains one segment. |
| opts.profile   | number                  | ✖        | accepted but not used in v1 | Present in type for future bevel profiles.                                             |
| opts.affect    | `"edges" \| "vertices"` | ✖        | accepted but not used in v1 | Current implementation bevels selected edges/incident faces.                           |
| opts.markSharp | boolean                 | ✖        | `true`                      | Marks selected original/profile edges sharp when true.                                 |

**Returns:** `BevelResult` — edited mesh, strip faces, shrunken original faces, and profile loop edge selections.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `edges.domain !== "edge"`; `EMPTY_SELECTION` if `edges.count === 0`.

## Examples

```ts
import { EditableMesh, bevel } from "@vgpu/render/edit";

const bevelMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const beveled = bevel(bevelMesh, bevelMesh.edges.byIndex([0]), { offset: 0.05 });
```

## Notes

* Boundary edges are processed on their one incident face and reported with `NON_MANIFOLD_EDGE_SKIPPED` warnings.
* **See also:** `extrude`, `inset`, `EdgeView`.

## inset

Insets selected faces by adding an inner triangle and boundary rim faces. Use it before `extrude` for panel-like forms.

## Import

```ts
import { inset } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface InsetOptions {
  readonly thickness: number;
  readonly depth?: number;
  readonly individual?: boolean;
}

declare interface InsetResult {
  readonly mesh: EditableMeshValue;
  readonly insetFaces: ElementSelection;
  readonly boundaryFaces: ElementSelection;
  readonly rimEdges: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function inset(em: EditableMeshValue, faces: ElementSelection, opts: InsetOptions): InsetResult;
```

## Parameters

| Param           | Type              | Required | Default                     | Notes                                                                                     |
| --------------- | ----------------- | -------- | --------------------------- | ----------------------------------------------------------------------------------------- |
| em              | EditableMeshValue | ✔        | —                           | Source mesh.                                                                              |
| faces           | ElementSelection  | ✔        | —                           | Must be a non-empty face selection.                                                       |
| opts.thickness  | number            | ✔        | —                           | Fraction toward face center; clamped to `[0, 0.49]`. Clamp emits `INSET_OVERLAP_CLAMPED`. |
| opts.depth      | number            | ✖        | `0`                         | Offset along each face normal after insetting.                                            |
| opts.individual | boolean           | ✖        | accepted but not used in v1 | Current implementation processes selected triangles individually.                         |

**Returns:** `InsetResult` — edited mesh plus inner faces, rim/boundary faces, and rim edges on the result mesh.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `faces.domain !== "face"`; `EMPTY_SELECTION` if `faces.count === 0`.

## Examples

```ts
import { EditableMesh, inset } from "@vgpu/render/edit";

const insetMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const insetResult = inset(insetMesh, insetMesh.faces.all(), { thickness: 0.2, depth: 0.05 });
```

## Notes

* Follow with `extrude(result.mesh, result.insetFaces, ...)` for raised or recessed panels.
* **See also:** `extrude`, `bevel`, `FaceView`.

## subdivideEdges

Splits selected edges and retriangulates incident triangles. Use it to add local resolution before detailed edits.

## Import

```ts
import { subdivideEdges } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection } from "@vgpu/render/edit";

declare interface SubdivideEdgesOptions { readonly cuts?: number }
declare interface SubdivideEdgesResult {
  readonly mesh: EditableMeshValue;
  readonly newVertices: ElementSelection;
  readonly newEdges: ElementSelection;
}

declare function subdivideEdges(em: EditableMeshValue, edges: ElementSelection, opts?: SubdivideEdgesOptions): SubdivideEdgesResult;
```

## Parameters

| Param     | Type                  | Required | Default | Notes                                                                           |
| --------- | --------------------- | -------- | ------- | ------------------------------------------------------------------------------- |
| em        | EditableMeshValue     | ✔        | —       | Source mesh.                                                                    |
| edges     | ElementSelection      | ✔        | —       | Must be a non-empty edge selection.                                             |
| opts      | SubdivideEdgesOptions | ✖        | `{}`    | Options object may be omitted.                                                  |
| opts.cuts | number                | ✖        | `1`     | Floored and clamped to minimum `1`; inserts this many points per selected edge. |

**Returns:** `SubdivideEdgesResult` — edited mesh with selections for inserted vertices and child edges.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `edges.domain !== "edge"`; `EMPTY_SELECTION` if `edges.count === 0`.

## Examples

```ts
import { EditableMesh, subdivideEdges } from "@vgpu/render/edit";

const subdivideEdgesMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const edgeSubdivision = subdivideEdges(subdivideEdgesMesh, subdivideEdgesMesh.edges.byIndex([0]), { cuts: 2 });
```

## Notes

* Sharp selected edges propagate sharpness to child edges.
* **See also:** `subdivideFaces`, `loopCut`, `bevel`.

## subdivideFaces

Subdivides selected triangles into four triangles per cut iteration using edge midpoints.

## Import

```ts
import { subdivideFaces } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection } from "@vgpu/render/edit";

declare interface SubdivideFacesOptions { readonly cuts?: number }
declare interface SubdivideFacesResult {
  readonly mesh: EditableMeshValue;
  readonly newFaces: ElementSelection;
  readonly newEdges: ElementSelection;
}

declare function subdivideFaces(em: EditableMeshValue, faces: ElementSelection, opts?: SubdivideFacesOptions): SubdivideFacesResult;
```

## Parameters

| Param     | Type                  | Required | Default | Notes                                                                                     |
| --------- | --------------------- | -------- | ------- | ----------------------------------------------------------------------------------------- |
| em        | EditableMeshValue     | ✔        | —       | Source mesh.                                                                              |
| faces     | ElementSelection      | ✔        | —       | Must be a non-empty face selection.                                                       |
| opts      | SubdivideFacesOptions | ✖        | `{}`    | Options object may be omitted.                                                            |
| opts.cuts | number                | ✖        | `1`     | Floored and clamped to minimum `1`; repeated cut iterations apply to newly-created faces. |

**Returns:** `SubdivideFacesResult` — edited mesh with selections for descendant faces and new edges.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `faces.domain !== "face"`; `EMPTY_SELECTION` if `faces.count === 0`.

## Examples

```ts
import { EditableMesh, subdivideFaces } from "@vgpu/render/edit";

const subdivideFacesMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const faceSubdivision = subdivideFaces(subdivideFacesMesh, subdivideFacesMesh.faces.all(), { cuts: 1 });
```

## Notes

* Original sharp face edges are split into sharp child edges.
* **See also:** `subdivideEdges`, `loopCut`, `FaceView`.

## loopCut

Attempts to cut an edge loop/ring through coplanar triangle pairs. Falls back to cutting only the seed edge when continuation is ambiguous.

## Import

```ts
import { loopCut } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface LoopCutOptions {
  readonly cuts?: number;
  readonly slide?: number;
  readonly markSharp?: boolean;
}

declare interface LoopCutResult {
  readonly mesh: EditableMeshValue;
  readonly insertedLoop: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function loopCut(em: EditableMeshValue, seedEdge: number, opts?: LoopCutOptions): LoopCutResult;
```

## Parameters

| Param          | Type              | Required | Default              | Notes                                                                  |
| -------------- | ----------------- | -------- | -------------------- | ---------------------------------------------------------------------- |
| em             | EditableMeshValue | ✔        | —                    | Source mesh.                                                           |
| seedEdge       | number            | ✔        | —                    | Edge index; must be `0 <= seedEdge < em.edgeCount`.                    |
| opts           | LoopCutOptions    | ✖        | `{}`                 | Options object may be omitted.                                         |
| opts.cuts      | number            | ✖        | `1` in fallback only | Used only when ambiguous continuation falls back to `subdivideEdges`.  |
| opts.slide     | number            | ✖        | `0`                  | Maps to split factor `0.5 + slide * 0.5`, clamped to `[0.001, 0.999]`. |
| opts.markSharp | boolean           | ✖        | `false`              | Marks inserted loop edges sharp only in successful ring cuts.          |

**Returns:** `LoopCutResult` — edited mesh plus ordered inserted-loop edge selection.
**Throws:** `MeshEditError` `EMPTY_SELECTION` when `seedEdge` is out of range.

## Examples

```ts
import { EditableMesh, loopCut } from "@vgpu/render/edit";

const loopCutMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const loopCutResult = loopCut(loopCutMesh, 0, { slide: 0.25 });
```

## Notes

* Ambiguous topology returns `LOOP_CUT_AMBIGUOUS_CONTINUATION` and cuts only the seed edge.
* **See also:** `subdivideEdges`, `subdivideFaces`, `EdgeView`.

## bridge

Creates faces between two ordered edge loops in one selection. Use for connecting holes or separated boundary rings.

## Import

```ts
import { bridge } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface BridgeOptions {
  readonly twist?: number;
  readonly mode?: "faces" | "merge";
}

declare interface BridgeResult {
  readonly mesh: EditableMeshValue;
  readonly bridgeFaces: ElementSelection;
  readonly chosenTwist: number;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function bridge(em: EditableMeshValue, sel: ElementSelection, opts?: BridgeOptions): BridgeResult;
```

## Parameters

| Param      | Type                 | Required | Default                                    | Notes                                                                                 |
| ---------- | -------------------- | -------- | ------------------------------------------ | ------------------------------------------------------------------------------------- |
| em         | EditableMeshValue    | ✔        | —                                          | Source mesh.                                                                          |
| sel        | ElementSelection     | ✔        | —                                          | Must be a non-empty ordered edge selection containing two loops.                      |
| opts       | BridgeOptions        | ✖        | `{}`                                       | Options object may be omitted.                                                        |
| opts.twist | number               | ✖        | auto by shortest squared endpoint distance | Shift applied to second loop correspondence; returned as positive modulo loop length. |
| opts.mode  | `"faces" \| "merge"` | ✖        | `"faces"`                                  | `"merge"` throws `UNSUPPORTED_INPUT` in the triangle-only kernel.                     |

**Returns:** `BridgeResult` — edited mesh, bridge face selection, chosen twist, optional length-mismatch warnings.
**Throws:** `MeshEditError` `WRONG_DOMAIN`, `EMPTY_SELECTION`, or `NOT_ORDERED` from loop validation; `AMBIGUOUS_TOPOLOGY` when two loops cannot be split; `UNSUPPORTED_INPUT` for `mode: "merge"`; `DEGENERATE_RESULT` can propagate from invalid loop vertices.

## Examples

```ts
import { EditableMesh, bridge, type ElementSelection } from "@vgpu/render/edit";

const bridgeMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1]),
  indices: new Uint32Array([0, 1, 2, 3, 4, 5]),
});
const twoTriangleLoops: ElementSelection = { domain: "edge", indices: [0, 1, 2, 3, 4, 5], count: 6, ordered: true };
const bridged = bridge(bridgeMesh, twoTriangleLoops, { twist: 0 });
```

## Notes

* Different loop lengths are allowed; modulo correspondence is used and `BRIDGE_LOOP_LENGTH_MISMATCH` is emitted.
* **See also:** `fillHole`, `gridFill`, `ElementSelection`.

## fillHole

Fills an ordered boundary loop with a triangle fan.

## Import

```ts
import { fillHole } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface FillHoleOptions { readonly method?: "triangulate" | "ngon" | "beautify" }
declare interface FillHoleResult {
  readonly mesh: EditableMeshValue;
  readonly newFaces: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function fillHole(em: EditableMeshValue, boundary: ElementSelection, opts?: FillHoleOptions): FillHoleResult;
```

## Parameters

| Param       | Type                                    | Required | Default         | Notes                                                                                |
| ----------- | --------------------------------------- | -------- | --------------- | ------------------------------------------------------------------------------------ |
| em          | EditableMeshValue                       | ✔        | —               | Source mesh.                                                                         |
| boundary    | ElementSelection                        | ✔        | —               | Must be a non-empty ordered edge loop.                                               |
| opts        | FillHoleOptions                         | ✖        | `{}`            | Options object may be omitted.                                                       |
| opts.method | `"triangulate" \| "ngon" \| "beautify"` | ✖        | `"triangulate"` | Non-triangulate methods still emit a triangle fan and warn `FILL_HOLE_TRIANGULATED`. |

**Returns:** `FillHoleResult` — edited mesh and newly-created faces.
**Throws:** `MeshEditError` `WRONG_DOMAIN`, `EMPTY_SELECTION`, or `NOT_ORDERED` from loop validation; `AMBIGUOUS_TOPOLOGY`/`DEGENERATE_RESULT` can propagate from invalid loop vertices.

## Examples

```ts
import { EditableMesh, fillHole, type ElementSelection } from "@vgpu/render/edit";

const fillHoleMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const triangleBoundary: ElementSelection = { domain: "edge", indices: [0, 1, 2], count: 3, ordered: true };
const filledHole = fillHole(fillHoleMesh, triangleBoundary);
```

## Notes

* Non-planar loops warn `FILL_NON_PLANAR_BOUNDARY` and are still triangulated.
* **See also:** `gridFill`, `bridge`, `ElementSet.boundaryOf`.

## gridFill

Deterministically represents a grid fill as triangles around the boundary center. Use it when callers request grid-fill semantics but the triangle-only kernel is acceptable.

## Import

```ts
import { gridFill } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface GridFillOptions { readonly spanMode?: "auto" | number }
declare interface GridFillResult {
  readonly mesh: EditableMeshValue;
  readonly newFaces: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function gridFill(em: EditableMeshValue, boundary: ElementSelection, opts?: GridFillOptions): GridFillResult;
```

## Parameters

| Param         | Type               | Required | Default                   | Notes                                                                  |
| ------------- | ------------------ | -------- | ------------------------- | ---------------------------------------------------------------------- |
| em            | EditableMeshValue  | ✔        | —                         | Source mesh.                                                           |
| boundary      | ElementSelection   | ✔        | —                         | Must be a non-empty ordered edge loop.                                 |
| opts          | GridFillOptions    | ✖        | `{}`                      | Options object may be omitted.                                         |
| opts.spanMode | `"auto" \| number` | ✖        | `"auto"` for warning text | Numeric values `< 1` throw `DEGENERATE_RESULT`; all modes triangulate. |

**Returns:** `GridFillResult` — edited mesh, new fan faces, and warnings.
**Throws:** `MeshEditError` `WRONG_DOMAIN`, `EMPTY_SELECTION`, or `NOT_ORDERED` from loop validation; `DEGENERATE_RESULT` for numeric `spanMode < 1`; `AMBIGUOUS_TOPOLOGY` can propagate from invalid loop vertices.

## Examples

```ts
import { EditableMesh, gridFill, type ElementSelection } from "@vgpu/render/edit";

const gridFillMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const gridBoundary: ElementSelection = { domain: "edge", indices: [0, 1, 2], count: 3, ordered: true };
const gridFilled = gridFill(gridFillMesh, gridBoundary, { spanMode: "auto" });
```

## Notes

* Always emits `GRID_FILL_TRIANGULATED`; odd loop lengths also warn with `FILL_NON_PLANAR_BOUNDARY` wording.
* **See also:** `fillHole`, `bridge`, `MeshEditWarning`.

## dissolveVertices

Dissolves selected non-boundary vertices by dissolving their surrounding faces.

## Import

```ts
import { dissolveVertices } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface DissolveVerticesOptions {
  readonly useFaceSplit?: boolean;
  readonly useBoundaryTear?: boolean;
}

declare interface DissolveVerticesResult {
  readonly mesh: EditableMeshValue;
  readonly surroundingFaces: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function dissolveVertices(em: EditableMeshValue, vertices: ElementSelection, opts?: DissolveVerticesOptions): DissolveVerticesResult;
```

## Parameters

| Param                | Type                    | Required | Default                     | Notes                                        |
| -------------------- | ----------------------- | -------- | --------------------------- | -------------------------------------------- |
| em                   | EditableMeshValue       | ✔        | —                           | Source mesh.                                 |
| vertices             | ElementSelection        | ✔        | —                           | Must be a non-empty vertex selection.        |
| opts                 | DissolveVerticesOptions | ✖        | `{}`                        | Options object may be omitted.               |
| opts.useFaceSplit    | boolean                 | ✖        | accepted but not used in v1 | Present in type only.                        |
| opts.useBoundaryTear | boolean                 | ✖        | accepted but not used in v1 | Boundary vertices are skipped with warnings. |

**Returns:** `DissolveVerticesResult` — edited mesh and resulting surrounding face selection.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `vertices.domain !== "vertex"`; `EMPTY_SELECTION` if `vertices.count === 0`.

## Examples

```ts
import { EditableMesh, dissolveVertices } from "@vgpu/render/edit";

const dissolveVerticesMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const dissolvedVertices = dissolveVertices(dissolveVerticesMesh, dissolveVerticesMesh.vertices.byIndex([0]));
```

## Notes

* Boundary vertices are skipped with `NON_MANIFOLD_VERTEX_SKIPPED`.
* **See also:** `dissolveEdges`, `dissolveFaces`, `mergeByDistance`.

## dissolveEdges

Removes selected internal edges by merging each adjacent face pair and retriangulating with a deterministic diagonal.

## Import

```ts
import { dissolveEdges } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface DissolveEdgesOptions { readonly useVerts?: boolean }
declare interface DissolveEdgesResult {
  readonly mesh: EditableMeshValue;
  readonly mergedFaces: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function dissolveEdges(em: EditableMeshValue, edges: ElementSelection, opts?: DissolveEdgesOptions): DissolveEdgesResult;
```

## Parameters

| Param         | Type                 | Required | Default                     | Notes                                                                                    |
| ------------- | -------------------- | -------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| em            | EditableMeshValue    | ✔        | —                           | Source mesh.                                                                             |
| edges         | ElementSelection     | ✔        | —                           | Must be a non-empty edge selection. Boundary/overlapping jobs are skipped with warnings. |
| opts          | DissolveEdgesOptions | ✖        | `{}`                        | Options object may be omitted.                                                           |
| opts.useVerts | boolean              | ✖        | accepted but not used in v1 | Present in type only.                                                                    |

**Returns:** `DissolveEdgesResult` — edited mesh and merged face selection.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `edges.domain !== "edge"`; `EMPTY_SELECTION` if `edges.count === 0`.

## Examples

```ts
import { EditableMesh, dissolveEdges } from "@vgpu/render/edit";

const dissolveEdgesMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const dissolvedEdges = dissolveEdges(dissolveEdgesMesh, dissolveEdgesMesh.edges.byIndex([0]));
```

## Notes

* Successful jobs emit `DISSOLVE_FACES_RETRIANGULATED` because the merged quad remains two triangles.
* **See also:** `dissolveVertices`, `dissolveFaces`, `EdgeView`.

## dissolveFaces

Removes selected face regions and retriangulates each region boundary as a fan.

## Import

```ts
import { dissolveFaces } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface DissolveFacesResult {
  readonly mesh: EditableMeshValue;
  readonly resultFace: ElementSelection;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function dissolveFaces(em: EditableMeshValue, faces: ElementSelection): DissolveFacesResult;
```

## Parameters

| Param | Type              | Required | Default | Notes                                                                                 |
| ----- | ----------------- | -------- | ------- | ------------------------------------------------------------------------------------- |
| em    | EditableMeshValue | ✔        | —       | Source mesh.                                                                          |
| faces | ElementSelection  | ✔        | —       | Must be a non-empty face selection. Connected components are dissolved independently. |

**Returns:** `DissolveFacesResult` — edited mesh and result face selection.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `faces.domain !== "face"`; `EMPTY_SELECTION` if `faces.count === 0`.

## Examples

```ts
import { EditableMesh, dissolveFaces } from "@vgpu/render/edit";

const dissolveFacesMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const dissolvedFaces = dissolveFaces(dissolveFacesMesh, dissolveFacesMesh.faces.all());
```

## Notes

* Degenerate regions warn `DEGENERATE_FACE_DROPPED`; multi-face or n-gon regions warn `DISSOLVE_FACES_RETRIANGULATED`.
* **See also:** `dissolveEdges`, `dissolveVertices`, `fillHole`.

## mergeByDistance

Welds vertices whose positions are within a threshold, removes collapsed faces, and returns an old-to-new vertex map.

## Import

```ts
import { mergeByDistance } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, ElementSelection, MeshEditWarning } from "@vgpu/render/edit";

declare interface MergeByDistanceOptions {
  readonly threshold?: number;
  readonly selection?: ElementSelection;
  readonly key?: "position" | "full-vertex";
}

declare interface MergeByDistanceResult {
  readonly mesh: EditableMeshValue;
  readonly mergeMap: ReadonlyMap<number, number>;
  readonly weldedCount: number;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function mergeByDistance(em: EditableMeshValue, opts?: MergeByDistanceOptions): MergeByDistanceResult;
```

## Parameters

| Param          | Type                          | Required | Default                                    | Notes                                                                                                     |
| -------------- | ----------------------------- | -------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| em             | EditableMeshValue             | ✔        | —                                          | Source mesh.                                                                                              |
| opts           | MergeByDistanceOptions        | ✖        | `{}`                                       | Options object may be omitted.                                                                            |
| opts.threshold | number                        | ✖        | `1e-4`                                     | Euclidean position distance for clustering.                                                               |
| opts.selection | ElementSelection              | ✖        | `em.vertices.all()`                        | Must be a vertex selection. Only selected vertices are clustered; all faces are remapped.                 |
| opts.key       | `"position" \| "full-vertex"` | ✖        | warning mode equivalent to `"full-vertex"` | Clustering is position-based in v1. `"position"` emits `SEAM_DESTROYED` when UV/normal/color flags exist. |

**Returns:** `MergeByDistanceResult` — welded mesh, old vertex index to new vertex index map (`-1` for unused), and welded count.
**Throws:** `MeshEditError` `WRONG_DOMAIN` if `opts.selection.domain !== "vertex"`; `EMPTY_SELECTION` if `opts.selection.count === 0`.

## Examples

```ts
import { EditableMesh, mergeByDistance } from "@vgpu/render/edit";

const mergeMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 0.00001, 0, 0, 0, 1, 0]),
});
const merged = mergeByDistance(mergeMesh, { threshold: 0.001 });
```

## Notes

* Collapsed faces are removed and reported with `MERGE_DEGENERATE_FACES_REMOVED`.
* **See also:** `healManifold`, `dissolveVertices`, `recomputeNormals`.

## healManifold

Deterministic cleanup pass that removes duplicate, degenerate, and overused-edge faces where possible.

## Import

```ts
import { healManifold } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue, MeshEditWarning } from "@vgpu/render/edit";

declare interface HealManifoldReport {
  readonly nonManifoldEdgesFixed: number;
  readonly nonManifoldVerticesFixed: number;
  readonly holesFixed: number;
  readonly duplicateFacesRemoved: number;
}

declare interface HealManifoldResult {
  readonly mesh: EditableMeshValue;
  readonly report: HealManifoldReport;
  readonly warnings?: readonly MeshEditWarning[];
}

declare function healManifold(em: EditableMeshValue): HealManifoldResult;
```

## Parameters

| Param | Type              | Required | Default | Notes        |
| ----- | ----------------- | -------- | ------- | ------------ |
| em    | EditableMeshValue | ✔        | —       | Source mesh. |

**Returns:** `HealManifoldResult` — cleaned mesh plus report. `nonManifoldVerticesFixed` and `holesFixed` are currently always `0`.
**Throws:** — no `MeshEditError` is thrown directly.

## Examples

```ts
import { EditableMesh, healManifold } from "@vgpu/render/edit";

const healMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
  indices: new Uint32Array([0, 1, 2, 0, 1, 2]),
});
const healed = healManifold(healMesh);
```

## Notes

* Remaining non-manifold residue is reported as `HEAL_NON_MANIFOLD_RESIDUE`.
* **See also:** `mergeByDistance`, `recomputeNormals`, `MeshEditWarning`.

## recomputeNormals

Rebuilds the editable mesh and recomputes face normals using smoothing components and sharp edges or a new crease angle.

## Import

```ts
import { recomputeNormals } from "@vgpu/render/edit";
```

## Signature

```ts
import type { EditableMeshValue } from "@vgpu/render/edit";

declare interface RecomputeNormalsOptions {
  readonly weighting?: "angle" | "area" | "uniform";
  readonly creaseAngle?: number;
}

declare function recomputeNormals(em: EditableMeshValue, opts?: RecomputeNormalsOptions): EditableMeshValue;
```

## Parameters

| Param            | Type                             | Required | Default                          | Notes                                                                                    |
| ---------------- | -------------------------------- | -------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
| em               | EditableMeshValue                | ✔        | —                                | Source mesh. Empty meshes are returned unchanged.                                        |
| opts             | RecomputeNormalsOptions          | ✖        | `{}`                             | Options object may be omitted.                                                           |
| opts.weighting   | `"angle" \| "area" \| "uniform"` | ✖        | `"angle"`                        | Weighting mode for smoothing component normals.                                          |
| opts.creaseAngle | number                           | ✖        | preserve current sharp-edge mask | If provided, rebuilds sharp edges from the crease angle instead of preserving `isSharp`. |

**Returns:** `EditableMeshValue` — new mesh with recomputed kernel face normals, or the same mesh when `faceCount === 0`.
**Throws:** — no `MeshEditError` is thrown directly.

## Examples

```ts
import { EditableMesh, recomputeNormals } from "@vgpu/render/edit";

const normalsMesh = EditableMesh.fromArrays({
  positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
});
const normalsRecomputed = recomputeNormals(normalsMesh, { weighting: "area" });
```

## Notes

* Run after topology-changing operators if downstream code relies on smooth normals.
* **See also:** `EditableMesh`, `mergeByDistance`, `healManifold`.

## MeshEditError

Error class thrown by validation and topology operators. Catch by `instanceof MeshEditError` and branch on `code`.

## Import

```ts
import { MeshEditError } from "@vgpu/render/edit";
```

## Signature

```ts
export type MeshEditErrorCode =
  | "NON_MANIFOLD"
  | "STALE_SELECTION"
  | "EMPTY_SELECTION"
  | "WRONG_DOMAIN"
  | "NOT_ORDERED"
  | "DEGENERATE_RESULT"
  | "AMBIGUOUS_TOPOLOGY"
  | "UNSUPPORTED_INPUT";

declare class MeshEditError extends Error {
  readonly code: MeshEditErrorCode;
  readonly suggestion?: string;
  constructor(opts: { readonly code: MeshEditErrorCode; readonly message?: string; readonly suggestion?: string });
}
```

## Parameters

| Param/Field     | Type              | Required | Default           | Notes                                          |
| --------------- | ----------------- | -------- | ----------------- | ---------------------------------------------- |
| opts.code       | MeshEditErrorCode | ✔        | —                 | Machine-readable error code.                   |
| opts.message    | string            | ✖        | `opts.code`       | Error message passed to `Error`.               |
| opts.suggestion | string            | ✖        | omitted           | Optional recovery hint.                        |
| code            | MeshEditErrorCode | ✔        | —                 | Public readonly field copied from constructor. |
| suggestion      | string            | ✖        | omitted           | Public readonly optional field.                |
| name            | string            | ✔        | `"MeshEditError"` | Set by constructor.                            |

**Returns:** `MeshEditError` instance from `new MeshEditError(...)`.
**Throws:** — constructor does not throw.

## Examples

```ts
import { MeshEditError } from "@vgpu/render/edit";

const meshEditError = new MeshEditError({ code: "EMPTY_SELECTION", suggestion: "Select at least one face." });
const meshEditErrorCode = meshEditError.code;
```

## Notes

* Common validation codes: `WRONG_DOMAIN`, `EMPTY_SELECTION`, `NOT_ORDERED`.
* **See also:** `ElementSelection`, `bridge`, `gridFill`.

## MeshEditWarning

Non-fatal diagnostic emitted in operator result `warnings` arrays. Use warnings to explain deterministic fallbacks and data loss.

## Import

```ts
import { MeshEditWarning } from "@vgpu/render/edit";
```

## Signature

```ts
export type MeshEditWarningCode =
  | "NON_MANIFOLD_EDGE_SKIPPED"
  | "NON_MANIFOLD_VERTEX_SKIPPED"
  | "DEGENERATE_FACE_DROPPED"
  | "TANGENTS_STRIPPED"
  | "BEVEL_ACUTE_CLAMPED"
  | "BEVEL_SEGMENTS_CLAMPED"
  | "INSET_OVERLAP_CLAMPED"
  | "SEAM_DESTROYED"
  | "BRIDGE_LOOP_LENGTH_MISMATCH"
  | "FILL_NON_PLANAR_BOUNDARY"
  | "LOOP_CUT_AMBIGUOUS_CONTINUATION"
  | "FILL_HOLE_TRIANGULATED"
  | "GRID_FILL_TRIANGULATED"
  | "DISSOLVE_FACES_RETRIANGULATED"
  | "MERGE_DEGENERATE_FACES_REMOVED"
  | "HEAL_NON_MANIFOLD_RESIDUE";

declare class MeshEditWarning {
  readonly code: MeshEditWarningCode;
  readonly reason: string;
  readonly element?: { readonly domain: "vertex" | "edge" | "face"; readonly index: number };
  constructor(
    code: MeshEditWarningCode,
    reason: string,
    element?: { readonly domain: "vertex" | "edge" | "face"; readonly index: number },
  );
}
```

## Parameters

| Param/Field | Type                                                      | Required | Default | Notes                                                |
| ----------- | --------------------------------------------------------- | -------- | ------- | ---------------------------------------------------- |
| code        | MeshEditWarningCode                                       | ✔        | —       | Machine-readable warning code.                       |
| reason      | string                                                    | ✔        | —       | Human-readable explanation.                          |
| element     | `{ domain: "vertex" \| "edge" \| "face"; index: number }` | ✖        | omitted | Optional source element associated with the warning. |

**Returns:** `MeshEditWarning` instance from `new MeshEditWarning(...)`.
**Throws:** — constructor does not throw.

## Examples

```ts
import { MeshEditWarning } from "@vgpu/render/edit";

const meshEditWarning = new MeshEditWarning("GRID_FILL_TRIANGULATED", "Triangle-only output was used.");
const warningReason = meshEditWarning.reason;
```

## Notes

* Treat warnings as actionable diagnostics, not failures; operators still return a mesh.
* **See also:** `toEditableWithDiagnostics`, `bevel`, `gridFill`, `healManifold`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: frameClock
description: Creates a monotonic time source with pause/resume support for ad-hoc render loops. Use it when you need consistent elapsed seconds outside the built-in `frame()` loop.
---

# frameClock



## Import

```ts
import { frameClock } from "@vgpu/render/utils";
```

## Signature

```ts
export function frameClock(): FrameClock;
```

## Parameters

| Param | Type | Required | Default | Notes                                                                             |
| ----- | ---- | -------- | ------- | --------------------------------------------------------------------------------- |
| —     | —    | —        | —       | `frameClock` does not take arguments; it captures `performance.now()` internally. |

**Returns:** `FrameClock` — exposes `now()`, `delta()`, `reset()`, `pause()`, `resume()`, and an `isPaused` getter. All time values are expressed in seconds.

## Examples

```ts
import { frameClock } from "@vgpu/render/utils";

const clock = frameClock();

function updateScene(elapsed: number, dt: number): void {
  void elapsed;
  void dt;
}

function tick() {
  if (!clock.isPaused) {
    const elapsed = clock.now();
    const dt = clock.delta();
    updateScene(elapsed, dt);
  }
  requestAnimationFrame(tick);
}

tick();
```

## Notes

* `delta()` returns `0` while paused, so you can leave animation code untouched and simply toggle `pause()` / `resume()`.
* `reset()` zeroes both elapsed time and accumulated pause, making it ideal for restarting demos without allocating a new clock.
* Calls to `pause()` are idempotent; repeated calls do nothing until `resume()` runs.
* **See also:** `canvasMouseTracker`, `canvasResolution`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: InspectMaterial
description: Shape returned by inspect materials such as `wireframeMaterial` and `normalDebugMaterial`. It bundles the configured pipeline, group-0 layout, uniform size, and a helper to encode shared matrices into a uniform buffer.
---

# InspectMaterial



## Import

```ts
import type { InspectMaterial } from "@vgpu/render/inspect";
```

## Signature

```ts
import type { InspectMaterialUniformParams } from "@vgpu/render/inspect";

export interface InspectMaterial {
  readonly pipeline: GPURenderPipeline;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly uniformByteSize: number;
  readonly writeUniforms: (
    buffer: GPUBuffer,
    offset: number,
    params: InspectMaterialUniformParams,
  ) => void;
}
```

## Fields

| Field           | Type                                                                                | Required | Default | Notes                                                                                                                                                                                                     |
| --------------- | ----------------------------------------------------------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| pipeline        | `GPURenderPipeline`                                                                 | ✔        | —       | Ready-to-use pipeline configured for the corresponding inspector material.                                                                                                                                |
| bindGroupLayout | `GPUBindGroupLayout`                                                                | ✔        | —       | Group 0 layout with binding 0 as a uniform buffer. Allocate a buffer with at least `uniformByteSize` bytes and bind it with this layout.                                                                  |
| uniformByteSize | `number`                                                                            | ✔        | —       | Exact byte size that `writeUniforms` writes. `normalDebugMaterial()` returns `128`; `wireframeMaterial()` returns `144` because it appends RGB color data.                                                |
| writeUniforms   | `(buffer: GPUBuffer, offset: number, params: InspectMaterialUniformParams) => void` | ✔        | —       | Packs `viewProjectionMatrix` at float offset 0 and `modelMatrix` at float offset 16, then writes bytes with `device.gpu.queue.writeBuffer(...)`. Wireframe materials also write color at float offset 32. |

**Returns:** Not applicable for the interface itself. Inspector factory functions return frozen `InspectMaterial` objects whose fields can be reused across frames.

**Throws:** None from reading the fields. `writeUniforms(...)` itself performs no custom validation; native WebGPU validation can occur if `buffer` is destroyed, too small for `offset + uniformByteSize`, or missing `GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST` compatibility for the caller's bind group/update path.

## Examples

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { normalDebugMaterial } from "@vgpu/render/inspect";

const device = await createMockAdapter().requestDevice();
const material = normalDebugMaterial({ device, targetFormat: "rgba8unorm-srgb" });
const uniformBuffer = device.createBuffer({
  label: "inspect.normal.uniforms",
  size: material.uniformByteSize,
  usage: ["uniform", "copy_dst"],
});

const identity = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
material.writeUniforms(uniformBuffer.gpu, 0, {
  viewProjectionMatrix: identity,
  modelMatrix: identity,
});

uniformBuffer.destroy();
device.destroy();
```

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { wireframeMaterial } from "@vgpu/render/inspect";

const device = await createMockAdapter().requestDevice();
const material = wireframeMaterial({
  device,
  color: [0.25, 0.5, 1],
  targetFormat: "rgba8unorm-srgb",
});

const uniformBuffer = device.createBuffer({ size: material.uniformByteSize, usage: ["uniform", "copy_dst"] });
const identity = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
material.writeUniforms(uniformBuffer.gpu, 0, { viewProjectionMatrix: identity, modelMatrix: identity });

uniformBuffer.destroy();
device.destroy();
```

## Notes

* `writeUniforms` writes the shared camera view-projection and per-mesh model matrices, so every inspect material can share the same uniform allocation logic.
* Reuse one uniform buffer per material instance; pass offsets when interleaving data for multiple meshes.
* `InspectMaterial` is a shared return contract; material-specific defaults live on the factory docs (`wireframeMaterial`, `normalDebugMaterial`).
* **See also:** `InspectMaterialUniformParams`, `wireframeMaterial`, `normalDebugMaterial`

***

# InspectMaterialUniformParams

Uniform inputs shared by all inspect materials. Additional inspector-specific uniforms should extend this interface explicitly.

## Import

```ts
import type { InspectMaterialUniformParams } from "@vgpu/render/inspect";
```

## Signature

```ts
type Mat4 = Float32Array;

export interface InspectMaterialUniformParams {
  readonly viewProjectionMatrix: Mat4;
  readonly modelMatrix: Mat4;
}
```

## Fields

| Field                | Type   | Required | Default | Notes                                                                                     |
| -------------------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------- |
| viewProjectionMatrix | `Mat4` | ✔        | —       | Combined camera matrix written to the inspector's uniform buffer slots 0–63 bytes.        |
| modelMatrix          | `Mat4` | ✔        | —       | Object transform written immediately after `viewProjectionMatrix`, in slots 64–127 bytes. |

**Returns:** Not applicable. This is a parameter object passed to `InspectMaterial.writeUniforms(...)`.

**Throws:** None by itself. The receiving `writeUniforms(...)` call can surface native WebGPU validation if the destination buffer is invalid.

## Examples

```ts
import type { InspectMaterialUniformParams } from "@vgpu/render/inspect";

const identity = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
const params: InspectMaterialUniformParams = {
  viewProjectionMatrix: identity,
  modelMatrix: identity,
};

params.modelMatrix satisfies Float32Array;
```

## Notes

* The interface is intentionally minimal so different inspectors can share the same math; extend it when a tool needs more uniforms.
* Matrices are expected to be column-major `Float32Array` values matching the mesh layout used elsewhere in vgpu.
* **See also:** `InspectMaterial`, `wireframeMaterial`, `normalDebugMaterial`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: meshToReadable
description: Promotes a render `Mesh` into a CPU-readable shape by ensuring its vertex buffer has `GPUBufferUsage.COPY_SRC`. Use it before inspection tools (wireframe, byte comparisons) that need to read vertex data.
---

# meshToReadable



## Import

```ts
import { meshToReadable } from "@vgpu/render/inspect";
```

## Signature

```ts
export function meshToReadable(mesh: Mesh, device: Device): Promise<Mesh>;
```

## Parameters

| Param  | Type   | Required | Default | Notes                                                                                        |
| ------ | ------ | -------- | ------- | -------------------------------------------------------------------------------------------- |
| mesh   | Mesh   | ✔        | —       | Source mesh. Must come from `@vgpu/render` helpers or user code with a valid `vertexBuffer`. |
| device | Device | ✔        | —       | Device that owns the target vertex buffer and command encoder used for the copy.             |

**Returns:** `Promise<Mesh>` — resolves to the original mesh when it already has `copy_src` usage or to a frozen clone that shares all metadata but replaces `vertexBuffer` with a readable copy.

**Throws:** `VGPU-CORE-INVALID-USAGE` when the source vertex buffer exposes an invalid `GPUBuffer.usage` mask (non-finite); fix by creating the geometry through `geometry(gpu, ...)` or adding `copy_src` when constructing buffers manually.

## Examples

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { meshToReadable } from "@vgpu/render/inspect";
import { init, geometry } from "vgpu/mock";
import { box } from "vgpu/scene";

async function main(): Promise<void> {
  const adapter = createMockAdapter();
  const gpu = await init({ adapter });
  const geo = geometry(gpu, box({ size: 1 }));

  const readable = await meshToReadable(geo as never, gpu.device);
  const vertices = await readable.vertexBuffer.read(readable.vertexBuffer.options.size);
  console.log("Readable bytes", new Float32Array(vertices));
}

main().catch((error) => {
  console.error(error);
});
```

## Notes

* When the input already includes `copy_src`, the function returns the exact same object; equality checks remain stable.
* Newly created readable buffers add `copy_src` (and `copy_dst` if it was missing) without removing the original usage flags.
* Index buffers and mesh metadata (`vertexCount`, `attributes`, etc.) are preserved by reference; only the vertex buffer identity changes.
* Await `queue.onSubmittedWorkDone()` in environments that expose it to guarantee the copy finished before reading.
* **See also:** `meshToWireframe`, `wireframeMaterial`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: meshToWireframe
description: Builds a deduplicated line-list index buffer out of a readable triangle-list mesh. Use it alongside `wireframeMaterial` to visualize topology edges during debugging sessions.
---

# meshToWireframe



## Import

```ts
import { meshToWireframe } from "@vgpu/render/inspect";
```

## Signature

```ts
export function meshToWireframe(mesh: Mesh, device: Device): Promise<WireframeMesh>;
```

## Parameters

| Param  | Type   | Required | Default | Notes                                                                                                                  |
| ------ | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| mesh   | Mesh   | ✔        | —       | Must have a vertex buffer created with `GPUBufferUsage.COPY_SRC`; otherwise the function cannot read vertex positions. |
| device | Device | ✔        | —       | Supplies the command encoder, index buffer, and queue submit used to bake the wireframe lines.                         |

**Returns:** `Promise<WireframeMesh>` — frozen mesh-like object that reuses the source `vertexBuffer`, exposes a raw `indexBuffer`, reports whether the index data is `uint16` or `uint32`, and includes the deduplicated `lineCount`.

**Throws:** `VGPU-CORE-INVALID-USAGE` when the source mesh lacks a readable vertex buffer; fix by creating the geometry via `geometry(gpu, ...)` or promoting it through `meshToReadable` first.

## Examples

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { meshToReadable, meshToWireframe, wireframeMaterial } from "@vgpu/render/inspect";
import { init, geometry } from "vgpu/mock";
import { box } from "vgpu/scene";

async function main(): Promise<void> {
  const adapter = createMockAdapter();
  const gpu = await init({ adapter });
  const solid = geometry(gpu, box({ size: 1 }));
  const readable = await meshToReadable(solid as never, gpu.device);
  const wireframe = await meshToWireframe(readable, gpu.device);
  const inspector = wireframeMaterial({ device: gpu.device });

  console.log(wireframe.lineCount); // 12 for a cube
  inspector.pipeline; // ready-to-use GPURenderPipeline
}

main().catch((error) => {
  console.error(error);
});
```

## Notes

* The helper quantizes edge endpoints to a `1e-6` grid before it deduplicates them; bake tiny debug meshes at a larger scale if you need finer detail.
* Triangle pairs that share a coplanar face have their diagonal removed by comparing face normals, so smooth surfaces only emit silhouette edges.
* The returned `indexBuffer` is a raw `GPUBuffer`. Destroy it manually when the wireframe is no longer needed or rely on `device.destroy()` during teardown.
* **See also:** `meshToReadable`, `wireframeMaterial`, `InspectMaterial`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: normalDebugMaterial
description: Creates an `InspectMaterial` that visualizes per-fragment normals as RGB. Use it to confirm vertex normals before passing geometry into lighting passes.
---

# normalDebugMaterial



## Import

```ts
import { normalDebugMaterial } from "@vgpu/render/inspect";
```

## Signature

```ts
export function normalDebugMaterial(spec: NormalDebugMaterialSpec): InspectMaterial;
```

## Parameters

| Param             | Type                    | Required | Default           | Notes                                                                        |
| ----------------- | ----------------------- | -------- | ----------------- | ---------------------------------------------------------------------------- |
| spec              | NormalDebugMaterialSpec | ✔        | —                 | Configuration object used to allocate the material.                          |
| spec.device       | Device                  | ✔        | —                 | Device that owns the pipeline, bind group layout, and uniform buffer writes. |
| spec.targetFormat | GPUTextureFormat        | ✖        | "bgra8unorm-srgb" | Color attachment format; use "rgba8unorm-srgb" if BGRA is unavailable.       |

**Returns:** `InspectMaterial` — exposes the configured `pipeline`, `bindGroupLayout`, 128-byte uniform size, and a writer that uploads view-projection and model matrices.

## Examples

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { InspectMaterial, normalDebugMaterial } from "@vgpu/render/inspect";

const IDENTITY_MATRIX = new Float32Array([
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1,
]);

async function main(): Promise<void> {
  const device = await createMockAdapter().requestDevice();
  const material: InspectMaterial = normalDebugMaterial({ device, targetFormat: "rgba8unorm-srgb" });
  const uniforms = device.createBuffer({ size: material.uniformByteSize, usage: ["uniform", "copy_dst"] });

  material.writeUniforms(uniforms.gpu, 0, {
    viewProjectionMatrix: IDENTITY_MATRIX,
    modelMatrix: IDENTITY_MATRIX,
  });
}

main().catch((error) => {
  console.error(error);
});
```

## Notes

* Renders with `triangle-list` topology, depth testing, and back-face culling so front-facing normals remain visible.
* Outputs `(normal + 1) / 2` in linear space; rotating the model matrix rotates the visualized colors.
* There is no normal-matrix correction: this inspector intentionally shows object-space normals to match authoring errors.
* **See also:** `wireframeMaterial`, `InspectMaterial`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/render/perf
description: WebGPU verification helpers for the optimize loop. These utilities live under `@vgpu/render/perf`; they are not meant for runtime use.
---

# @vgpu/render/perf



***

# gpuFrameTime

Measures GPU frame time statistics for a render routine. The harness handles warmup, command encoding, submission, and timing (timestamp queries when available, wall clock otherwise).

## Import

```ts
import { gpuFrameTime } from "@vgpu/render/perf";
```

## Signature

```ts
export function gpuFrameTime(
  device: Device,
  encode: GpuFrameEncoder,
  options?: GpuFrameTimeOptions,
): Promise<GpuFrameTimeResult>;
```

## Parameters

| Param                  | Type                | Required | Default             | Notes                                                                                                                                      |
| ---------------------- | ------------------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| device                 | Device              | ✔        | —                   | Source device used to create command encoders, submit work, and flush the queue. Must expose `timestamp-query` if you want GPU timestamps. |
| encode                 | GpuFrameEncoder     | ✔        | —                   | Callback invoked per frame with a fresh `GPUCommandEncoder`; record passes exactly as in production.                                       |
| options                | GpuFrameTimeOptions | ✖        | `{}`                | Tunables for warmup, sample count, and measurement mode.                                                                                   |
| options.frames         | number              | ✖        | 120                 | Measured frames after warmup. Values below 1 are clamped to 1.                                                                             |
| options.warmup         | number              | ✖        | 30                  | Throws away the first N frames so shader compilation and lazy allocations settle.                                                          |
| options.batch          | number              | ✖        | 8                   | Frames per batch when falling back to wall-clock. Ignored for timestamp queries.                                                           |
| options.forceWallClock | boolean             | ✖        | false               | Forces the wall-clock path even if `timestamp-query` is available. Useful when you intentionally want queue-submit timings.                |
| options.label          | string              | ✖        | "vgpu-gpuFrameTime" | Assigns encoder labels and buffer names to make debugging captures easier.                                                                 |

**Returns:** `Promise<GpuFrameTimeResult>` — resolves with median, mean, min, p95, sample count, and the measurement `method` (`"timestamp-query"` or `"wall-clock"`).

**Throws:** Propagates any error from the encoder callback, queue submission, or timestamp buffer mapping. Timestamp-specific errors automatically fall back to wall-clock before surfacing.

## Examples

```ts
import { gpuFrameTime } from "@vgpu/render/perf";
import { createMockAdapter } from "@vgpu/adapter-mock";

async function bench(): Promise<void> {
  const adapter = createMockAdapter();
  const device = await adapter.requestDevice();
  const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [{ view: {} as GPUTextureView, loadOp: "clear", storeOp: "store" }] };
  const pipeline = device.gpu.createRenderPipeline({
    layout: "auto",
    vertex: { module: device.gpu.createShaderModule({ code: "@vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(); }" }), entryPoint: "vs_main" },
    fragment: { module: device.gpu.createShaderModule({ code: "@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1.0); }" }), entryPoint: "fs_main", targets: [{ format: "bgra8unorm" }] },
    primitive: { topology: "triangle-list" },
  });

  const result = await gpuFrameTime(device, (encoder) => {
    const pass = encoder.beginRenderPass(renderPassDescriptor);
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
  }, { frames: 60, warmup: 10 });

  console.log(result.method, result.medianMs);
}

bench().catch((error) => {
  console.error(error);
});
```

## Notes

* Timestamp queries run only when the device exposes the `timestamp-query` feature; otherwise wall-clock timings use `device.queue.flush()` to amortize submit latency via batching.
* Warmup still submits commands; budget time for shader compilation before measuring.
* Always compare two runs with identical options; `medianMs` is the headline figure for relative regressions.
* **See also:** `pixelDiff`

***

# GpuFrameTimeOptions

Optional configuration passed to `gpuFrameTime`.

## Fields

| Field          | Type    | Required | Default             | Notes                                                           |
| -------------- | ------- | -------- | ------------------- | --------------------------------------------------------------- |
| frames         | number  | ✖        | 120                 | Number of measured frames (post-warmup). Clamped to at least 1. |
| warmup         | number  | ✖        | 30                  | Frames to submit and discard before measuring.                  |
| batch          | number  | ✖        | 8                   | Wall-clock batch size; ignored for timestamp mode.              |
| forceWallClock | boolean | ✖        | false               | Skip timestamp queries even when supported.                     |
| label          | string  | ✖        | "vgpu-gpuFrameTime" | Label used for encoders and buffers created internally.         |

***

# GpuFrameTimeResult

Return type for `gpuFrameTime`.

## Fields

| Field    | Type                              | Required | Default | Notes                                                                 |
| -------- | --------------------------------- | -------- | ------- | --------------------------------------------------------------------- |
| medianMs | number                            | ✔        | —       | 50th percentile frame time, the main number to compare across builds. |
| meanMs   | number                            | ✔        | —       | Arithmetic mean of collected samples.                                 |
| minMs    | number                            | ✔        | —       | Fastest observed frame.                                               |
| p95Ms    | number                            | ✔        | —       | 95th percentile; highlights long tails.                               |
| samples  | number                            | ✔        | —       | Count of valid samples captured.                                      |
| method   | "timestamp-query" \| "wall-clock" | ✔        | —       | Measurement backend used.                                             |

***

# pixelDiff

Compares two renders byte-for-byte. Accepts either `Texture` instances (will call `read()`) or already read `Uint8Array`s.

## Import

```ts
import { pixelDiff } from "@vgpu/render/perf";
```

## Signature

```ts
export function pixelDiff(
  a: Texture | Uint8Array,
  b: Texture | Uint8Array,
): Promise<PixelDiffResult>;
```

## Parameters

| Param | Type                  | Required | Default | Notes                                                            |
| ----- | --------------------- | -------- | ------- | ---------------------------------------------------------------- |
| a     | Texture \| Uint8Array | ✔        | —       | First render; when a `Texture`, it is read via `Texture.read()`. |
| b     | Texture \| Uint8Array | ✔        | —       | Second render to compare against `a`.                            |

**Returns:** `Promise<PixelDiffResult>` — contains `maxByte`, `meanByte`, `changedBytes`, `totalBytes`, and `changedFraction`.

**Throws:** Propagates any error while reading textures (e.g., unreadable usage flags) or when the inputs cannot be read.

## Examples

```ts
import { pixelDiff } from "@vgpu/render/perf";
import type { Texture } from "@vgpu/core";

async function assertVisualParity(before: Texture, after: Texture): Promise<void> {
  const result = await pixelDiff(before, after);
  if (result.maxByte > 2) {
    throw new Error(`Visual regression detected (max delta ${result.maxByte})`);
  }
}
```

## Notes

* A length mismatch forces `maxByte` to `255`; inspect `totalBytes` before trusting small `maxByte` values.
* Treat `maxByte <= 2` as driver rounding noise on most devices; higher values indicate true visual changes.
* When comparing multiple frames, feed already-read `Uint8Array`s to avoid repeated GPU readbacks.
* **See also:** `gpuFrameTime`

***

# PixelDiffResult

Shape returned by `pixelDiff`.

## Fields

| Field           | Type   | Required | Default | Notes                                                                                                           |
| --------------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| maxByte         | number | ✔        | —       | Largest absolute per-byte difference (0–255). Use this as the headline regression metric.                       |
| meanByte        | number | ✔        | —       | Mean absolute per-byte difference across compared buffers.                                                      |
| changedBytes    | number | ✔        | —       | Count of bytes whose value differs at all.                                                                      |
| totalBytes      | number | ✔        | —       | Number of bytes compared (min of both buffer lengths).                                                          |
| changedFraction | number | ✔        | —       | `changedBytes / totalBytes`; near-zero fractions with low `maxByte` typically indicate harmless rounding noise. |


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: wireframeMaterial
description: Creates an `InspectMaterial` configured to render line segments from a readable mesh. Use it with `meshToWireframe` outputs to visualize topology edges while keeping your primary material untouched.
---

# wireframeMaterial



## Import

```ts
import { wireframeMaterial } from "@vgpu/render/inspect";
```

## Signature

```ts
export function wireframeMaterial(spec: WireframeMaterialSpec): InspectMaterial;
```

## Parameters

| Param             | Type                               | Required | Default           | Notes                                                                                                             |
| ----------------- | ---------------------------------- | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| spec              | WireframeMaterialSpec              | ✔        | —                 | Configuration object used to allocate the pipeline.                                                               |
| spec.device       | Device                             | ✔        | —                 | Device that owns the pipeline, bind group layout, and uniform buffer writes.                                      |
| spec.color        | readonly \[number, number, number] | ✖        | \[1, 1, 1]        | Linear RGB line color; each component must be between 0 and 1.                                                    |
| spec.targetFormat | GPUTextureFormat                   | ✖        | "bgra8unorm-srgb" | Color attachment format for the fragment target. Use "rgba8unorm-srgb" on implementations that lack BGRA support. |

**Returns:** `InspectMaterial` — exposes the configured `pipeline`, `bindGroupLayout`, uniform byte size (144 bytes), and a writer that packs view-projection, model matrices, and the wire color.

## Examples

```ts
import { createMockAdapter } from "@vgpu/adapter-mock";
import { InspectMaterial, wireframeMaterial } from "@vgpu/render/inspect";

const IDENTITY_MATRIX = new Float32Array([
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1,
]);

async function main(): Promise<void> {
  const device = await createMockAdapter().requestDevice();
  const material: InspectMaterial = wireframeMaterial({ device, color: [1, 0.75, 0.5] });
  const uniforms = device.createBuffer({ size: material.uniformByteSize, usage: ["uniform", "copy_dst"] });

  material.writeUniforms(uniforms.gpu, 0, {
    viewProjectionMatrix: IDENTITY_MATRIX,
    modelMatrix: IDENTITY_MATRIX,
  });
}

main().catch((error) => {
  console.error(error);
});
```

## Notes

* Uses `line-list` topology with depth testing enabled and no culling to guarantee all edges stay visible.
* The vertex buffer layout expects interleaved position/normal attributes that match the `meshToWireframe` output stride (24 bytes).
* Uniform writes allocate a temporary `Float32Array`; reuse the same uniform buffer between frames to avoid churn.
* **See also:** `meshToWireframe`, `InspectMaterial`, `normalDebugMaterial`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: bind and explicit binding helpers
description: `bind`, `createBindGroupLayout`, `createPipelineLayout`, `createBindGroup`, and `createSampler` are thin core helpers for explicit WebGPU binding. Use them when you want raw WebGPU layouts, bind groups, and samplers without shader reflection or `layout: "auto"` inference.
---

# bind and explicit binding helpers



## Import

```ts
import { bind, createBindGroup, createBindGroupLayout, createPipelineLayout, createSampler } from "vgpu/core";
```

## Signature

```ts
import type { Buffer, Device, Texture } from "vgpu/core";

type BindVisibility = GPUShaderStageFlags | string | readonly ("vertex" | "fragment" | "compute")[];
type DeviceLike = GPUDevice | Device | { readonly gpu: GPUDevice };

interface CreateBindGroupLayoutOptions {
  readonly label?: string;
  readonly entries: readonly GPUBindGroupLayoutEntry[];
}

interface CreatePipelineLayoutOptions {
  readonly label?: string;
  readonly bindGroups: readonly GPUBindGroupLayout[];
}

interface CreateBindGroupOptions {
  readonly label?: string;
  readonly layout: GPUBindGroupLayout;
  readonly entries: readonly GPUBindGroupEntry[];
}

interface SamplerDescriptorWithSugar extends GPUSamplerDescriptor {
  readonly filter?: "linear" | "nearest";
  readonly wrap?: "clamp" | "repeat" | "mirror";
}

declare function createBindGroupLayout(device: DeviceLike, opts: CreateBindGroupLayoutOptions): GPUBindGroupLayout;
declare function createPipelineLayout(device: DeviceLike, opts: CreatePipelineLayoutOptions): GPUPipelineLayout;
declare function createBindGroup(device: DeviceLike, opts: CreateBindGroupOptions): GPUBindGroup;
declare function createSampler(device: DeviceLike, descriptor?: SamplerDescriptorWithSugar): GPUSampler;

declare const bind: {
  readonly uniform: (binding: number, visibility: BindVisibility, opts?: Omit<GPUBufferBindingLayout, "type">) => GPUBindGroupLayoutEntry;
  readonly storage: (binding: number, visibility: BindVisibility, opts?: Omit<GPUBufferBindingLayout, "type">) => GPUBindGroupLayoutEntry;
  readonly readonlyStorage: (binding: number, visibility: BindVisibility, opts?: Omit<GPUBufferBindingLayout, "type">) => GPUBindGroupLayoutEntry;
  readonly texture: (binding: number, visibility: BindVisibility, opts?: GPUTextureBindingLayout) => GPUBindGroupLayoutEntry;
  readonly storageTexture: (binding: number, visibility: BindVisibility, opts: GPUStorageTextureBindingLayout) => GPUBindGroupLayoutEntry;
  readonly sampler: (binding: number, visibility: BindVisibility, opts?: GPUSamplerBindingLayout) => GPUBindGroupLayoutEntry;
  readonly resource: (binding: number, value: Buffer | Texture | GPUBuffer | GPUBufferBinding | GPUBindingResource | unknown) => GPUBindGroupEntry;
};
```

## Parameters

### Shared parameters

| Param      | Type                                                                                | Required | Default | Notes                                                                                                             |                                                                                                                |
| ---------- | ----------------------------------------------------------------------------------- | -------: | ------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| device     | `DeviceLike`                                                                        |        ✔ | —       | A raw `GPUDevice`, a vgpu `Device`, or an object with `.gpu: GPUDevice`. Helpers unwrap it before calling WebGPU. |                                                                                                                |
| binding    | `number`                                                                            |        ✔ | —       | Explicit non-negative integer `@binding(n)`. Invalid values throw `VGPU-CORE-BINDING-INVALID`.                    |                                                                                                                |
| visibility | `GPUShaderStageFlags \| string \| readonly ("vertex" \| "fragment" \| "compute")[]` |        ✔ | —       | Numeric flags pass through. Strings split on \`                                                                   | `, comma, or whitespace. Arrays use stage names directly. Unknown names throw `VGPU-CORE-VISIBILITY-INVALID\`. |

### `createBindGroupLayout(device, opts)`

| Param        | Type                                 | Required | Default     | Notes                                                                                               |
| ------------ | ------------------------------------ | -------: | ----------- | --------------------------------------------------------------------------------------------------- |
| opts         | `CreateBindGroupLayoutOptions`       |        ✔ | —           | Layout descriptor wrapper.                                                                          |
| opts.label   | `string`                             |        ✖ | `undefined` | Forwarded to `GPUDevice.createBindGroupLayout`.                                                     |
| opts.entries | `readonly GPUBindGroupLayoutEntry[]` |        ✔ | —           | Copied with `[...opts.entries]` before calling WebGPU; metadata is attached to the returned layout. |

### `createPipelineLayout(device, opts)`

| Param           | Type                            | Required | Default     | Notes                                                         |
| --------------- | ------------------------------- | -------: | ----------- | ------------------------------------------------------------- |
| opts            | `CreatePipelineLayoutOptions`   |        ✔ | —           | Pipeline layout descriptor wrapper.                           |
| opts.label      | `string`                        |        ✖ | `undefined` | Forwarded to `GPUDevice.createPipelineLayout`.                |
| opts.bindGroups | `readonly GPUBindGroupLayout[]` |        ✔ | —           | Copied to WebGPU as `bindGroupLayouts: [...opts.bindGroups]`. |

### `createBindGroup(device, opts)`

| Param        | Type                           | Required | Default     | Notes                                                                                                                                      |
| ------------ | ------------------------------ | -------: | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| opts         | `CreateBindGroupOptions`       |        ✔ | —           | Bind group descriptor wrapper.                                                                                                             |
| opts.label   | `string`                       |        ✖ | `undefined` | Forwarded to `GPUDevice.createBindGroup`.                                                                                                  |
| opts.layout  | `GPUBindGroupLayout`           |        ✔ | —           | Required explicit layout. Missing/falsy layout throws `VGPU-CORE-BIND-GROUP-LAYOUT-REQUIRED`; vgpu never uses bind group `layout: "auto"`. |
| opts.entries | `readonly GPUBindGroupEntry[]` |        ✔ | —           | Copied with `[...opts.entries]` before calling WebGPU; metadata is attached to the returned bind group.                                    |

### `createSampler(device, descriptor?)`

| Param                    | Type                              | Required | Default        | Notes                                                                                                                                                             |
| ------------------------ | --------------------------------- | -------: | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| descriptor               | `SamplerDescriptorWithSugar`      |        ✖ | `{}`           | Raw `GPUSamplerDescriptor` plus vgpu sugar. Sugar is stripped before calling WebGPU.                                                                              |
| descriptor.filter        | `"linear" \| "nearest"`           |        ✖ | `undefined`    | Expands to `magFilter` and `minFilter` only. Raw `magFilter`/`minFilter` override this per key.                                                                   |
| descriptor.wrap          | `"clamp" \| "repeat" \| "mirror"` |        ✖ | `undefined`    | Expands to all three address modes: `"clamp"` → `"clamp-to-edge"`, `"repeat"` → `"repeat"`, `"mirror"` → `"mirror-repeat"`. Raw address fields override per axis. |
| descriptor.mipmapFilter  | `GPUMipmapFilterMode`             |        ✖ | WebGPU default | Not set by `filter`; pass `"linear"` explicitly for anisotropic sampling.                                                                                         |
| descriptor.maxAnisotropy | `number`                          |        ✖ | WebGPU default | If `filter` sugar is used and `maxAnisotropy > 1`, all three filters must resolve to `"linear"`.                                                                  |

### `bind.*` layout entry helpers

| Param                     | Type                                   | Required | Default | Notes                                                                                                                                                                                   |
| ------------------------- | -------------------------------------- | -------: | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bind.uniform opts         | `Omit<GPUBufferBindingLayout, "type">` |        ✖ | `{}`    | Returns `{ binding, visibility, buffer: { ...opts, type: "uniform" } }`.                                                                                                                |
| bind.storage opts         | `Omit<GPUBufferBindingLayout, "type">` |        ✖ | `{}`    | Returns `{ binding, visibility, buffer: { ...opts, type: "storage" } }`.                                                                                                                |
| bind.readonlyStorage opts | `Omit<GPUBufferBindingLayout, "type">` |        ✖ | `{}`    | Returns `{ binding, visibility, buffer: { ...opts, type: "read-only-storage" } }`.                                                                                                      |
| bind.texture opts         | `GPUTextureBindingLayout`              |        ✖ | `{}`    | Returns `{ binding, visibility, texture: opts }`.                                                                                                                                       |
| bind.storageTexture opts  | `GPUStorageTextureBindingLayout`       |        ✔ | —       | Returns `{ binding, visibility, storageTexture: opts }`; storage texture layout fields are not inferred.                                                                                |
| bind.sampler opts         | `GPUSamplerBindingLayout`              |        ✖ | `{}`    | Returns `{ binding, visibility, sampler: opts }`.                                                                                                                                       |
| bind.resource value       | `unknown`                              |        ✔ | —       | Converts vgpu `Buffer` to `{ buffer: buffer.gpu }`, vgpu `Texture`/texture-like objects to `createView()`, raw `GPUBuffer` to `{ buffer }`, and passes through other binding resources. |

**Returns:**

* `createBindGroupLayout(...)` returns `GPUBindGroupLayout` with vgpu layout metadata attached.
* `createPipelineLayout(...)` returns raw `GPUPipelineLayout`.
* `createBindGroup(...)` returns `GPUBindGroup` with vgpu bind-group metadata attached.
* `createSampler(...)` returns raw `GPUSampler`.
* `bind.uniform(...)`, `bind.storage(...)`, `bind.readonlyStorage(...)`, `bind.texture(...)`, `bind.storageTexture(...)`, and `bind.sampler(...)` return `GPUBindGroupLayoutEntry`.
* `bind.resource(...)` returns `GPUBindGroupEntry`.

**Throws:**

* `VGPU-CORE-BIND-GROUP-LAYOUT-REQUIRED` when `createBindGroup(...)` receives a missing/falsy `opts.layout` — create and pass an explicit `GPUBindGroupLayout`.
* `VGPU-CORE-SAMPLER-ANISOTROPY-FILTERS` when `createSampler(...)` uses `filter` sugar with `maxAnisotropy > 1` but the resolved `magFilter`, `minFilter`, and `mipmapFilter` are not all `"linear"` — add `mipmapFilter: "linear"` and avoid raw non-linear overrides.
* `VGPU-CORE-BINDING-INVALID` when any `bind.*` helper receives a negative, non-integer, or otherwise invalid `binding` — pass an explicit non-negative integer.
* `VGPU-CORE-VISIBILITY-INVALID` when a string/array visibility contains an unknown shader stage name — use only `"vertex"`, `"fragment"`, and `"compute"`, or pass numeric `GPUShaderStageFlags`.
* Native WebGPU validation errors may occur for descriptor/layout/resource incompatibilities — fix the WebGPU descriptor or resource usage.

## Examples

```ts
import { bind, createBindGroup, createBindGroupLayout, createPipelineLayout, createSampler } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const uniformBuffer = device.createBuffer({ size: 64, usage: ["uniform", "copy_dst"] });
const texture = device.createTexture({
  size: [1, 1],
  format: "rgba8unorm",
  usage: ["texture_binding", "copy_src"],
});
const sampler = createSampler(device, { filter: "linear", wrap: "clamp" });

const groupLayout = createBindGroupLayout(device, {
  label: "scene.group0",
  entries: [
    bind.uniform(0, "vertex|fragment"),
    bind.texture(1, "fragment", { sampleType: "float" }),
    bind.sampler(2, ["fragment"], { type: "filtering" }),
  ],
});

const pipelineLayout = createPipelineLayout(device, { bindGroups: [groupLayout] });
const group = createBindGroup(device, {
  layout: groupLayout,
  entries: [
    bind.resource(0, uniformBuffer),
    bind.resource(1, texture),
    bind.resource(2, sampler),
  ],
});

console.log(pipelineLayout, group);
device.destroy();
```

```ts
import { createSampler } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();

const mixedSampler = createSampler(device, {
  filter: "linear",
  wrap: "repeat",
  magFilter: "nearest", // raw field wins for this key only
});

const anisotropicSampler = createSampler(device, {
  filter: "linear",
  mipmapFilter: "linear",
  maxAnisotropy: 16,
});

console.log(mixedSampler, anisotropicSampler);
device.destroy();
```

```ts
import { bind, createBindGroupLayout } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const computeLayout = createBindGroupLayout(device, {
  entries: [
    bind.storage(0, "compute", { hasDynamicOffset: true }),
    bind.storageTexture(1, "compute", { access: "write-only", format: "rgba8unorm", viewDimension: "2d" }),
  ],
});

console.log(computeLayout);
device.destroy();
```

## Notes

* These helpers are explicit by design: no WGSL reflection, no named binding maps, no hidden resource creation, and no `layout: "auto"` bind groups.
* `filter` sugar does not set `mipmapFilter`. For anisotropic sampling, spell trilinear filtering explicitly.
* Raw WebGPU descriptor fields override sugar per key, not all-or-nothing.
* `bind.resource(...)` creates a default texture view for vgpu `Texture`; use a raw `GPUTextureView` when you need a non-default mip/array/format view.
* Keep shader `@group/@binding` numbers synchronized with helper `binding` numbers; vgpu does not reflect or renumber them here.
* **See also:** `Device`, `Buffer`, `Texture`, `Queue`, `VGPUError`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Buffer
description: `Buffer` is the core wrapper around a `GPUBuffer`. Use it for explicit GPU buffer allocation through `Device.createBuffer(...)`, CPU-to-GPU writes, deterministic readback, and wrapper-aware teardown.
---

# Buffer



## Import

```ts
import { Buffer } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

type BufferUsageName =
  | "map_read"
  | "map_write"
  | "copy_src"
  | "copy_dst"
  | "index"
  | "vertex"
  | "uniform"
  | "storage"
  | "indirect"
  | "query_resolve";

interface BufferOptions {
  readonly size: number;
  readonly usage: readonly BufferUsageName[];
  readonly label?: string;
}

type BufferWriteData = ArrayBuffer | ArrayBufferView<ArrayBuffer>;

declare class Buffer {
  readonly gpu: GPUBuffer;
  readonly options: BufferOptions;
  constructor(device: Device, gpu: GPUBuffer, options: BufferOptions);
  write(data: BufferWriteData, offset?: number): void;
  read(byteLength: number, offset?: number): Promise<ArrayBuffer>;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

### `Device.createBuffer(opts)` / `BufferOptions`

| Param      | Type                         | Required | Default     | Notes                                                                              |
| ---------- | ---------------------------- | -------: | ----------- | ---------------------------------------------------------------------------------- |
| opts.size  | `number`                     |        ✔ | —           | Buffer byte length. Must be finite and greater than `0`.                           |
| opts.usage | `readonly BufferUsageName[]` |        ✔ | —           | One or more vgpu usage names mapped to `GPUBufferUsage` flags. Empty arrays throw. |
| opts.label | `string`                     |        ✖ | `undefined` | Forwarded to `GPUBufferDescriptor.label`.                                          |

Valid `BufferUsageName` values: `"map_read"`, `"map_write"`, `"copy_src"`, `"copy_dst"`, `"index"`, `"vertex"`, `"uniform"`, `"storage"`, `"indirect"`, `"query_resolve"`.

### Constructor

| Param   | Type            | Required | Default | Notes                                                                                                      |
| ------- | --------------- | -------: | ------- | ---------------------------------------------------------------------------------------------------------- |
| device  | `Device`        |        ✔ | —       | Owning device wrapper used for queue writes and readback. Normally supplied by `Device.createBuffer(...)`. |
| gpu     | `GPUBuffer`     |        ✔ | —       | Raw WebGPU buffer.                                                                                         |
| options | `BufferOptions` |        ✔ | —       | Original vgpu descriptor exposed as `buffer.options`.                                                      |

### `write(data, offset?)`

| Param  | Type                                          | Required | Default | Notes                                                                 |
| ------ | --------------------------------------------- | -------: | ------- | --------------------------------------------------------------------- |
| data   | `ArrayBuffer \| ArrayBufferView<ArrayBuffer>` |        ✔ | —       | Bytes passed to `device.queue.writeBuffer(buffer.gpu, offset, data)`. |
| offset | `number`                                      |        ✖ | `0`     | Destination byte offset in the GPU buffer.                            |

### `read(byteLength, offset?)`

| Param      | Type     | Required | Default | Notes                                                        |
| ---------- | -------- | -------: | ------- | ------------------------------------------------------------ |
| byteLength | `number` |        ✔ | —       | Number of bytes to copy from the GPU buffer into CPU memory. |
| offset     | `number` |        ✖ | `0`     | Source byte offset in the GPU buffer.                        |

**Returns:**

* `Device.createBuffer(opts)` returns `Buffer`.
* `write(data, offset?)`, `destroy()`, and `dispose()` return `void`.
* `read(byteLength, offset?)` returns `Promise<ArrayBuffer>` containing exactly the requested byte range.

**Throws:**

* `VGPU-CORE-INVALID-USAGE` from `Device.createBuffer(...)` when `opts.size` is non-finite, `opts.size <= 0`, or `opts.usage.length === 0` — pass a positive byte length and at least one usage.
* `VGPU-BUFFER-DISPOSED` (`"Buffer is destroyed."`) when `write(...)` or `read(...)` is called after `destroy()`/`dispose()` — create a new buffer instead of reusing a destroyed wrapper. Reported for a wrapper you disposed and for one destroyed with its gpu, and in preference to the owning device's own disposal error.
* Native WebGPU validation errors may occur if usage flags do not allow the operation, for example reading a buffer without `"copy_src"` or writing without `"copy_dst"`.

## Examples

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const buffer = device.createBuffer({
  label: "cpu-visible-data",
  size: 16,
  usage: ["copy_dst", "copy_src"],
});

buffer.write(new Uint32Array([1, 2, 3, 4]));
const bytes = await buffer.read(16);
console.log(new Uint32Array(bytes)[2]); // 3

buffer.destroy();
device.destroy();
```

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();

device.pushErrorScope("validation");
const placeholder = device.createBuffer({ size: 0, usage: ["copy_dst"] });
const error = await device.popErrorScope();

console.log(placeholder.options.size); // 0: wrapper exists, backing GPU buffer is mock-sized
console.log(error?.code); // "VGPU-CORE-INVALID-USAGE"

device.destroy();
```

## Notes

* `Buffer` does not infer usages. Include `"copy_dst"` for `write(...)`, `"copy_src"` for `read(...)`, `"vertex"` for vertex input, `"index"` for index input, `"uniform"` for uniforms, and `"storage"` for storage bindings.
* Prefer `buffer.destroy()`/`buffer.dispose()` over `buffer.gpu.destroy()` so lifecycle callbacks, mocks, and wrapper state stay synchronized.
* `destroy()` is idempotent. After destroy, `write(...)` and `read(...)` intentionally fail.
* `read(...)` copies through the device readback path; avoid it in frame loops unless you intentionally need CPU synchronization.
* **See also:** `Device`, `Queue`, `Texture`, `bind.resource`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Device
description: `Device` is the core wrapper around a raw `GPUDevice`. Use it when you need explicit low-level resource creation (`Buffer`, `Texture`, `Shader`), queue access, readback, and structured WebGPU error scopes.
---

# Device



## Import

```ts
import { Device } from "vgpu/core";
```

## Signature

```ts
import type { Buffer, BufferOptions, Queue, Shader, ShaderInput, Texture, TextureOptions, VGPUError } from "vgpu/core";

interface DeviceOptions {
  readonly isCompatibilityMode?: boolean;
}

declare class Device {
  readonly gpu: GPUDevice;
  readonly adapterInfo: GPUAdapterInfo | null;
  readonly queue: Queue;
  readonly isCompatibilityMode: boolean;
  constructor(gpu: GPUDevice, adapterInfo?: GPUAdapterInfo | null, opts?: DeviceOptions);
  get limits(): GPUSupportedLimits;
  get features(): GPUSupportedFeatures;
  createShader(input: ShaderInput): Shader;
  createTexture(opts: TextureOptions): Texture;
  createBuffer(opts: BufferOptions): Buffer;
  pushErrorScope(filter: GPUErrorFilter): void;
  popErrorScope(): Promise<VGPUError | null>;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

### Constructor

| Param                    | Type                     | Required | Default | Notes                                                                                                               |
| ------------------------ | ------------------------ | -------: | ------- | ------------------------------------------------------------------------------------------------------------------- |
| gpu                      | `GPUDevice`              |        ✔ | —       | Raw WebGPU device. `Device` does not request adapters itself.                                                       |
| adapterInfo              | `GPUAdapterInfo \| null` |        ✖ | `null`  | Stored as `device.adapterInfo`; pass adapter metadata when an adapter provides it.                                  |
| opts                     | `DeviceOptions`          |        ✖ | `{}`    | Core-only options for wrapper behavior.                                                                             |
| opts.isCompatibilityMode | `boolean`                |        ✖ | `false` | Stored as `device.isCompatibilityMode`; adapters set it when they requested WebGPU `featureLevel: "compatibility"`. |

### `createShader(input)`

| Param | Type          | Required | Default | Notes                                                                                                                                    |
| ----- | ------------- | -------: | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| input | `ShaderInput` |        ✔ | —       | A WGSL string or a resolved shader object with `.wgsl`. Strings are compiled with `@vgpu/wgsl` before creating the native shader module. |

### `createTexture(opts)`

| Param | Type             | Required | Default | Notes                                                                     |
| ----- | ---------------- | -------: | ------- | ------------------------------------------------------------------------- |
| opts  | `TextureOptions` |        ✔ | —       | Descriptor-first texture options; see `TextureOptions` rows in `Texture`. |

### `createBuffer(opts)`

| Param | Type            | Required | Default | Notes                                                                  |
| ----- | --------------- | -------: | ------- | ---------------------------------------------------------------------- |
| opts  | `BufferOptions` |        ✔ | —       | Descriptor-first buffer options; see `BufferOptions` rows in `Buffer`. |

### Error scopes and teardown

| Param  | Type             | Required | Default | Notes                                                                                 |
| ------ | ---------------- | -------: | ------- | ------------------------------------------------------------------------------------- |
| filter | `GPUErrorFilter` |        ✔ | —       | Passed to `gpu.pushErrorScope(filter)` and also starts a vgpu structured-error scope. |

**Returns:**

* `new Device(...)` returns a wrapper with `.gpu`, `.queue`, `.limits`, `.features`, and resource factory methods.
* `createShader(input)` returns `Shader`.
* `createTexture(opts)` returns `Texture`.
* `createBuffer(opts)` returns `Buffer`.
* `pushErrorScope(filter)`, `destroy()`, and `dispose()` return `void`.
* `popErrorScope()` returns `Promise<VGPUError | null>`; the first captured vgpu error wins, otherwise a native `GPUError` is converted to `VGPU-CORE-VALIDATION`, otherwise `null`.

**Throws:**

* `VGPU-CORE-INVALID-USAGE` when `createBuffer({ size })` receives a non-finite size, `size <= 0`, or an empty `usage` array — pass a positive byte size and at least one buffer usage.
* `VGPU-CORE-VALIDATION` can be returned from `popErrorScope()` when the native WebGPU scope reports a `GPUError` — inspect `.message` and fix the invalid WebGPU descriptor or command.
* Native WebGPU errors may be thrown by `gpu.createShaderModule`, `gpu.createTexture`, `gpu.pushErrorScope`, `gpu.popErrorScope`, or `gpu.destroy`; use error scopes around native validation-sensitive calls.

## Examples

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();

const buffer = device.createBuffer({
  label: "positions",
  size: 16,
  usage: ["vertex", "copy_dst", "copy_src"],
});

buffer.write(new Float32Array([0, 1, 2, 3]));
const bytes = await buffer.read(16);
console.log(bytes.byteLength);

device.destroy();
```

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();

device.pushErrorScope("validation");
const badBuffer = device.createBuffer({ size: 0, usage: ["copy_dst"] });
const error = await device.popErrorScope();

console.log(badBuffer.options.size, error?.code); // "VGPU-CORE-INVALID-USAGE"
device.dispose();
```

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();

if (device.features.has("timestamp-query")) {
  console.log("timestamp queries are available");
}

console.log(device.limits.maxTextureDimension2D);
console.log(device.isCompatibilityMode);

device.destroy();
```

## Notes

* `Device` is intentionally low-level: it does not infer buffer/texture usage from shaders or pipeline state. Provide explicit descriptors.
* Prefer `device.createBuffer(...)` and `device.createTexture(...)` over raw `.gpu` creation when you want vgpu wrappers, readback, lifecycle callbacks, or structured core errors.
* `destroy()` is idempotent and `dispose()` is an alias. Do not call `device.gpu.destroy()` directly unless you intentionally bypass vgpu lifecycle.
* `createBuffer` throws immediately without an error scope, but captures into the current vgpu error scope when one is active.
* `isCompatibilityMode` is only a signal set by the adapter; keep compatibility-specific texture views and WGSL bindings in lockstep yourself.
* **See also:** `Buffer`, `Texture`, `Queue`, `VGPUError`, `VGPUAdapter`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Queue
description: `Queue` is the core wrapper around a raw `GPUQueue`. Use it for explicit buffer writes and for awaiting completion of already-submitted GPU work.
---

# Queue



## Import

```ts
import { Queue } from "vgpu/core";
```

## Signature

```ts
import type { BufferWriteData } from "vgpu/core";

declare class Queue {
  readonly gpu: GPUQueue;
  constructor(gpu: GPUQueue);
  writeBuffer(buffer: GPUBuffer, offset: number, data: BufferWriteData): void;
  flush(): Promise<void>;
}
```

## Parameters

### Constructor

| Param | Type       | Required | Default | Notes                                                                                              |
| ----- | ---------- | -------: | ------- | -------------------------------------------------------------------------------------------------- |
| gpu   | `GPUQueue` |        ✔ | —       | Raw WebGPU queue exposed again as `queue.gpu`. `Device` constructs `new Queue(gpu.queue)` for you. |

### `writeBuffer(buffer, offset, data)`

| Param  | Type                                          | Required | Default | Notes                                                                                   |
| ------ | --------------------------------------------- | -------: | ------- | --------------------------------------------------------------------------------------- |
| buffer | `GPUBuffer`                                   |        ✔ | —       | Raw destination buffer. Pass `buffer.gpu` when you have a vgpu `Buffer`.                |
| offset | `number`                                      |        ✔ | —       | Destination byte offset. Unlike `Buffer.write(...)`, there is no default at this level. |
| data   | `ArrayBuffer \| ArrayBufferView<ArrayBuffer>` |        ✔ | —       | Bytes forwarded to `GPUQueue.writeBuffer(buffer, offset, data)`.                        |

### `flush()`

| Param | Type | Required | Default | Notes                                                      |
| ----- | ---- | -------: | ------- | ---------------------------------------------------------- |
| —     | —    |        — | —       | Takes no parameters. Awaits `gpu.onSubmittedWorkDone?.()`. |

**Returns:**

* `new Queue(gpu)` returns a queue wrapper.
* `writeBuffer(buffer, offset, data)` returns `void`.
* `flush()` returns `Promise<void>` after `GPUQueue.onSubmittedWorkDone()` resolves, or immediately if the queue implementation has no `onSubmittedWorkDone` method.

**Throws:** Native WebGPU validation errors may occur when `writeBuffer(...)` writes beyond the buffer bounds, uses an invalid offset/data size, or targets a buffer without `COPY_DST` usage — fix the buffer descriptor and write range.

## Examples

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const buffer = device.createBuffer({ size: 8, usage: ["copy_dst", "copy_src"] });

device.queue.writeBuffer(buffer.gpu, 0, new Uint32Array([10, 20]));
await device.queue.flush();

const bytes = await buffer.read(8);
console.log(new Uint32Array(bytes)[1]); // 20

device.destroy();
```

```ts
import { Queue } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const queue = new Queue(device.gpu.queue);
await queue.flush();

device.destroy();
```

## Notes

* `Buffer.write(data, offset = 0)` is the safer high-level call for vgpu buffers; it defaults the offset and checks wrapper lifecycle before reaching the queue.
* `Queue.writeBuffer(...)` intentionally accepts raw `GPUBuffer` so it can interoperate with native WebGPU resources.
* `flush()` waits for submitted work; it does not submit command buffers by itself.
* Do not use `flush()` as a per-frame synchronization point unless CPU/GPU synchronization is intentional.
* **See also:** `Device`, `Buffer`, `Texture`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: createRenderBundle
description: Low-level render bundle helper around `GPUDevice.createRenderBundleEncoder`. Prefer main API (`vgpu`) `bundle(gpu, { target }, cb)` when recording main API (`vgpu`) `Draw`/`Effect` commands because it derives formats from `Target` and performs stale-bundle checks (`VGPU-R3-BUNDLE-STALE`).
---

# createRenderBundle



## Import

```ts
import { createRenderBundle, RenderBundleRecorder } from "vgpu/core";
import type { RenderBundleOptions } from "vgpu/core";
```

## Signature

```ts
import type { Buffer } from "vgpu/core";

declare interface RenderPassDrawOptions {
  readonly vertexCount: number;
  readonly instanceCount?: number;
  readonly firstVertex?: number;
  readonly firstInstance?: number;
}

type RenderPassDynamicOffsets = readonly GPUBufferDynamicOffset[] | Uint32Array;

declare interface RenderBundleOptions {
  readonly label?: string;
  readonly colorFormats: readonly (GPUTextureFormat | null)[];
  readonly depthStencilFormat?: GPUTextureFormat;
  readonly sampleCount?: number;
  readonly depthReadOnly?: boolean;
  readonly stencilReadOnly?: boolean;
  readonly record: (bundle: RenderBundleRecorder) => void;
}

declare class RenderBundleRecorder {
  readonly gpu: GPURenderBundleEncoder;
  constructor(gpu: GPURenderBundleEncoder);
  setPipeline(pipeline: GPURenderPipeline): void;
  setBindGroup(index: number, group: GPUBindGroup | null, dynamicOffsets?: RenderPassDynamicOffsets): void;
  setVertexBuffer(slot: number, buffer: Buffer | GPUBuffer | null, offset?: number, size?: GPUSize64): void;
  draw(options: RenderPassDrawOptions): void;
  draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void;
}

declare function createRenderBundle(device: { readonly gpu: GPUDevice }, opts: RenderBundleOptions): GPURenderBundle;
```

## Parameters

| Param                                | Type                                               | Required | Default                      | Notes                                                                               |
| ------------------------------------ | -------------------------------------------------- | -------: | ---------------------------- | ----------------------------------------------------------------------------------- |
| device                               | `{ readonly gpu: GPUDevice }`                      |        ✔ | —                            | Core `Device` or any wrapper exposing a native `GPUDevice` as `.gpu`.               |
| opts                                 | `RenderBundleOptions`                              |        ✔ | —                            | Native render bundle encoder options plus callback.                                 |
| opts.label                           | `string`                                           |        ✖ | `undefined`                  | Passed to `createRenderBundleEncoder` and `finish`.                                 |
| opts.colorFormats                    | `readonly (GPUTextureFormat \| null)[]`            |        ✔ | —                            | Must match the render pass where the bundle will execute.                           |
| opts.depthStencilFormat              | `GPUTextureFormat`                                 |        ✖ | `undefined`                  | Required when the replay pass has depth/stencil.                                    |
| opts.sampleCount                     | `number`                                           |        ✖ | WebGPU encoder default (`1`) | Must match replay pass sample count. main API (`vgpu`) passes `target.sampleCount`. |
| opts.depthReadOnly                   | `boolean`                                          |        ✖ | `undefined`                  | Forwarded to WebGPU encoder descriptor.                                             |
| opts.stencilReadOnly                 | `boolean`                                          |        ✖ | `undefined`                  | Forwarded to WebGPU encoder descriptor.                                             |
| opts.record                          | `(bundle: RenderBundleRecorder) => void`           |        ✔ | —                            | Called synchronously before `encoder.finish()`.                                     |
| recorder.setPipeline.pipeline        | `GPURenderPipeline`                                |        ✔ | —                            | Native pipeline compatible with bundle formats.                                     |
| recorder.setBindGroup.index          | `number`                                           |        ✔ | —                            | Bind group slot.                                                                    |
| recorder.setBindGroup.group          | `GPUBindGroup \| null`                             |        ✔ | —                            | Native bind group or `null`.                                                        |
| recorder.setBindGroup.dynamicOffsets | `readonly GPUBufferDynamicOffset[] \| Uint32Array` |        ✖ | `undefined`                  | Forwarded to `setBindGroup`.                                                        |
| recorder.setVertexBuffer.slot        | `number`                                           |        ✔ | —                            | Vertex buffer slot.                                                                 |
| recorder.setVertexBuffer.buffer      | `Buffer \| GPUBuffer \| null`                      |        ✔ | —                            | Core `Buffer` is unwrapped to `.gpu`; native buffer and `null` pass through.        |
| recorder.setVertexBuffer.offset      | `number`                                           |        ✖ | `0`                          | Byte offset.                                                                        |
| recorder.setVertexBuffer.size        | `GPUSize64`                                        |        ✖ | `undefined`                  | Byte size.                                                                          |
| recorder.draw\.options.vertexCount   | `number`                                           |        ✔ | —                            | Object overload vertex count.                                                       |
| recorder.draw\.options.instanceCount | `number`                                           |        ✖ | `1`                          | Object overload instance count.                                                     |
| recorder.draw\.options.firstVertex   | `number`                                           |        ✖ | `0`                          | Object overload first vertex.                                                       |
| recorder.draw\.options.firstInstance | `number`                                           |        ✖ | `0`                          | Object overload first instance.                                                     |

**Returns:** `createRenderBundle()` returns `GPURenderBundle`; recorder methods return `void`.

**Throws:** No custom `VGPU-*` errors are thrown here. Native WebGPU validation errors occur for incompatible formats, pipelines, bind groups, buffers, or draw parameters. main API (`vgpu`) stale errors (`VGPU-R3-BUNDLE-STALE`, `VGPU-R3-BUNDLE-INVALID`) are available only through `bundle(gpu)` / `FramePass.bundles()`.

## Examples

```ts
import { init } from "vgpu/mock";
import { createRenderBundle } from "vgpu/core";

const gpu = await init();
const bundle = createRenderBundle(gpu.device, {
  label: "empty",
  colorFormats: ["rgba8unorm"],
  sampleCount: 1,
  record(recorder) {
    void recorder;
  },
});
void bundle;
```

```ts
import { init, target } from "vgpu/mock";
import { createRenderBundle } from "vgpu/core";

const gpu = await init();
const colorTarget = target(gpu, { size: [16, 16] });
const bundle = createRenderBundle(gpu.device, {
  colorFormats: colorTarget.colors.map((color) => color.format),
  depthStencilFormat: colorTarget.depth?.format,
  sampleCount: colorTarget.sampleCount,
  record(recorder) {
    recorder.draw({ vertexCount: 0, instanceCount: 0 });
  },
});
void bundle;
```

## Notes

* This helper intentionally does not know about main API (`vgpu`) `Draw`, `Effect`, or `Target`; you must supply formats and native commands yourself.
* Use `bundle(gpu)` for public API examples unless you are already managing native pipelines.
* `RenderBundleRecorder.draw(number, ...)` defaults to `(instanceCount=1, firstVertex=0, firstInstance=0)`; object overload has the same defaults.
* **See also:** `Bundle`, `FramePass.bundles`, `Draw`, `Target`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: StorageBuffer
description: Low-level user-owned storage buffer with a stable bind group at binding `0`. Use it for arrays, large data, compute scratch buffers, or storage-driven rendering when main API (`vgpu`) `storage(gpu)` is not enough.
---

# StorageBuffer



## Import

```ts
import { StorageBuffer } from "vgpu/core";
import type { StorageBufferOptions } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

declare interface StorageBufferOptions {
  readonly size: number;
  readonly label?: string;
  readonly access?: "read" | "read-write";
  readonly visibility?: GPUShaderStageFlags;
  readonly bindGroupLayout?: GPUBindGroupLayout;
}

declare class StorageBuffer {
  readonly device: Device;
  readonly size: number;
  readonly access: "read" | "read-write";
  readonly buffer: import("vgpu/core").Buffer;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly bindGroup: GPUBindGroup;
  constructor(device: Device, opts: StorageBufferOptions);
  get gpu(): GPUBuffer;
  write(data: BufferSource, offset?: number): void;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

| Param                | Type                     | Required | Default                                                                            | Notes                                                                                                   |
| -------------------- | ------------------------ | -------: | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| device               | `Device`                 |        ✔ | —                                                                                  | Core device, usually `gpu.device`.                                                                      |
| opts                 | `StorageBufferOptions`   |        ✔ | —                                                                                  | Buffer/layout options.                                                                                  |
| opts.size            | `number`                 |        ✔ | —                                                                                  | Byte size. Used for storage buffer allocation and `minBindingSize`.                                     |
| opts.label           | `string`                 |        ✖ | `undefined`                                                                        | Forwarded to buffer label; layout label becomes `${label}.bgl`; bind group label becomes `${label}.bg`. |
| opts.access          | `"read" \| "read-write"` |        ✖ | `"read"`                                                                           | Chooses bind group layout type: `"read-only-storage"` or `"storage"`.                                   |
| opts.visibility      | `GPUShaderStageFlags`    |        ✖ | `GPUShaderStage.FRAGMENT \| GPUShaderStage.COMPUTE` with numeric fallback `2 \| 4` | Ignored when `opts.bindGroupLayout` is supplied. Default intentionally excludes vertex stage.           |
| opts.bindGroupLayout | `GPUBindGroupLayout`     |        ✖ | A new binding-0 storage layout                                                     | Reuse a pipeline-owned layout. Binding `0` must match size/access.                                      |
| storage.write.data   | `BufferSource`           |        ✔ | —                                                                                  | Bytes uploaded with `queue.writeBuffer`.                                                                |
| storage.write.offset | `number`                 |        ✖ | `0`                                                                                | Destination byte offset.                                                                                |

**Returns:** Constructor returns `StorageBuffer`; `gpu` returns the underlying `GPUBuffer`; `write()`, `destroy()`, and `dispose()` return `void`.

**Throws:** No custom `VGPU-*` errors are thrown directly by this class. Native/core validation can fail for invalid size, incompatible reused layouts, or bad writes. Compute aliasing with the same buffer can throw `VGPU-R1-STORAGE-ALIASING` when used through `compute(gpu)`.

## Examples

```ts
import { init, compute } from "vgpu/mock";
import { StorageBuffer } from "vgpu/core";

const gpu = await init();
const values = new StorageBuffer(gpu.device, { size: 4 * 16, label: "values" });
values.write(new Float32Array(16));

const sim = compute(gpu, `
  @group(0) @binding(0) var<storage, read> values: array<f32>;
  @compute @workgroup_size(1)
  fn cs_main(@builtin(global_invocation_id) id: vec3u) { _ = values[id.x]; }
`, { set: { values } });
sim.dispatch(1);
```

```ts
import { init, draw } from "vgpu/mock";
import { StorageBuffer } from "vgpu/core";

const gpu = await init();
const drawable = draw(gpu, { shader: `
  @group(0) @binding(0) var<storage, read> positions: array<vec4f>;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f { return positions[vi]; }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
` });
const positions = new StorageBuffer(gpu.device, {
  size: 3 * 16,
  visibility: GPUShaderStage.VERTEX,
  bindGroupLayout: drawable.layout(0),
});
positions.write(new Float32Array(12));
drawable.set({ positions });
```

## Notes

* Default visibility is fragment + compute, not vertex. Vertex-stage storage is legal only on adapters with the needed limits; opt in explicitly and request limits when creating the device.
* `access: "read-write"` cannot be used from the vertex stage in WebGPU.
* For main API (`vgpu`) readback and ping-pong helpers, use `storage(gpu)` and `pingPongStorage(gpu)`.
* **See also:** `Compute`, `storage()`, `Uniform`, `UniformPool`, `Draw.set`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: StructuredUniform
description: Low-level structured uniform buffer that computes WGSL-compatible offsets from a schema and writes typed values into one stable uniform buffer. Prefer main API (`vgpu`) `set({ params: ... })` unless you need a reusable resource and generated WGSL struct text.
---

# StructuredUniform



## Import

```ts
import { StructuredUniform } from "vgpu/core";
import type { StructuredUniformOptions, UniformValues, ScalarUniformType, VectorUniformInput, UniformLayoutInfo, UniformField, WgslUniformType } from "vgpu/core";
```

## Signature

```ts
import type { BindVisibility, Device } from "vgpu/core";

type ScalarUniformType = "f32" | "u32" | "i32";
type VectorUniformInput = readonly number[] | Float32Array | Uint32Array | Int32Array;
type WgslUniformType =
  | "f32" | "u32" | "i32"
  | "vec2f" | "vec3f" | "vec4f"
  | "vec2u" | "vec3u" | "vec4u"
  | "vec2i" | "vec3i" | "vec4i"
  | "mat3x3f" | "mat4x4f";

type UniformValues<S extends Record<string, WgslUniformType>> = {
  [K in keyof S]: S[K] extends ScalarUniformType ? number : VectorUniformInput;
};

interface UniformField {
  readonly name: string;
  readonly type: WgslUniformType;
  readonly offset: number;
  readonly size: number;
  readonly align: number;
}

interface UniformLayoutInfo {
  readonly fields: readonly UniformField[];
  readonly offsets: Readonly<Record<string, number>>;
  readonly byteSize: number;
}

interface StructuredUniformOptions<S extends Record<string, WgslUniformType>> {
  readonly schema: S;
  readonly label?: string;
  readonly visibility?: BindVisibility;
}

declare class StructuredUniform<S extends Record<string, WgslUniformType>> {
  readonly device: Device;
  readonly schema: S;
  readonly layout: UniformLayoutInfo;
  readonly byteSize: number;
  readonly offsets: Readonly<Record<keyof S, number>>;
  readonly buffer: import("vgpu/core").Buffer;
  constructor(device: Device, opts: StructuredUniformOptions<S>);
  get gpu(): GPUBuffer;
  get bindGroupLayout(): GPUBindGroupLayout;
  get bindGroup(): GPUBindGroup;
  write(values: Partial<UniformValues<S>>): void;
  wgsl(structName?: string): string;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

| Param           | Type                                        | Required | Default                  | Notes                                                                                              |
| --------------- | ------------------------------------------- | -------: | ------------------------ | -------------------------------------------------------------------------------------------------- |
| device          | `Device`                                    |        ✔ | —                        | Core device.                                                                                       |
| opts            | `StructuredUniformOptions<S>`               |        ✔ | —                        | Schema and optional labels/visibility.                                                             |
| opts.schema     | `S extends Record<string, WgslUniformType>` |        ✔ | —                        | Insertion order is WGSL member order. Empty schema and unsupported type strings are invalid.       |
| opts.label      | `string`                                    |        ✖ | `undefined`              | Buffer label; lazy layout/bind group labels become `${label}.bgl` and `${label}.bg`.               |
| opts.visibility | `BindVisibility`                            |        ✖ | `["vertex", "fragment"]` | Used only by lazy `bindGroupLayout`; ignored until that getter is read.                            |
| write.values    | `Partial<UniformValues<S>>`                 |        ✔ | —                        | Field patch. Scalars require `number`; vectors/matrices require exact-length array or typed array. |
| wgsl.structName | `string`                                    |        ✖ | `"Uniforms"`             | Name for generated WGSL struct text.                                                               |

**Returns:** Constructor returns `StructuredUniform`; `bindGroupLayout` and `bindGroup` lazily create native objects; `write()` returns `void`; `wgsl()` returns WGSL struct source; `destroy()` / `dispose()` return `void`.

**Throws:** `VGPU-CORE-INVALID-USAGE` when schema is empty, contains unsupported types, writing an unknown field, writing a scalar with a non-number, writing vector/matrix data with a non-array, wrong exact length, or using the object after destroy.

## Examples

```ts
import { init } from "vgpu/mock";
import { StructuredUniform } from "vgpu/core";

const gpu = await init();
const params = new StructuredUniform(gpu.device, {
  label: "params",
  schema: { time: "f32", tint: "vec3f", viewProjection: "mat4x4f" },
});
params.write({
  time: 1,
  tint: [1, 0.5, 0.25],
  viewProjection: new Float32Array(16),
});
const wgsl = params.wgsl("Params");
void wgsl;
```

```ts
import { init, draw } from "vgpu/mock";
import { StructuredUniform } from "vgpu/core";

const gpu = await init();
const params = new StructuredUniform(gpu.device, { schema: { value: "f32" } });
const drawable = draw(gpu, { shader: `
  struct Params { value: f32 }
  @group(0) @binding(0) var<uniform> params: Params;
  @vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(params.value); }
` });
params.write({ value: 1 });
drawable.set({ params });
```

## Notes

* `vec3*` fields align to 16 bytes; inspect `offsets` / `layout` instead of assuming tight packing.
* `mat3x3f` stores columns at 16-byte stride and needs exactly 9 values in `write()`.
* `wgsl()` only emits the struct; you still declare the `@group/@binding var<uniform>` line in your shader.
* **See also:** `Uniform`, `SharedUniforms`, `UniformPool`, `Draw.set`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Texture
description: `Texture` is the core wrapper around a `GPUTexture`. Use it for explicit texture allocation through `Device.createTexture(...)`, cached default views, resizing owned textures, readback, and wrapper-aware teardown.
---

# Texture



## Import

```ts
import { Texture } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

type TextureUsageName = "copy_src" | "copy_dst" | "texture_binding" | "storage_binding" | "render_attachment";

interface TextureOptions {
  readonly size: readonly [width: number, height: number, depthOrArrayLayers?: number];
  readonly format: GPUTextureFormat;
  readonly usage: readonly TextureUsageName[];
  readonly mipLevelCount?: number;
  readonly sampleCount?: 1 | 4;
  readonly dimension?: GPUTextureDimension;
  readonly viewFormats?: readonly GPUTextureFormat[];
  readonly label?: string;
}

declare class Texture {
  constructor(device: Device, gpu: GPUTexture, options: TextureOptions, ownership?: "owned" | "external");
  get gpu(): GPUTexture;
  get options(): TextureOptions;
  get size(): TextureOptions["size"];
  get format(): GPUTextureFormat;
  get usage(): TextureOptions["usage"];
  get mipLevelCount(): number;
  get sampleCount(): 1 | 4;
  get dimension(): GPUTextureDimension;
  get viewFormats(): readonly GPUTextureFormat[];
  get label(): string | undefined;
  get view(): GPUTextureView;
  createView(desc?: GPUTextureViewDescriptor): GPUTextureView;
  resize(size: readonly [number, number] | readonly [number, number, number]): boolean;
  read(): Promise<Uint8Array>;
  readFloats(): Promise<Float32Array>;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

### `Device.createTexture(opts)` / `TextureOptions`

| Param              | Type                                                                    | Required | Default                 | Notes                                                                                                                                             |
| ------------------ | ----------------------------------------------------------------------- | -------: | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts.size          | `readonly [width: number, height: number, depthOrArrayLayers?: number]` |        ✔ | —                       | Stored as tuple and converted to `{ width, height, depthOrArrayLayers: opts.size[2] ?? 1 }` for WebGPU.                                           |
| opts.format        | `GPUTextureFormat`                                                      |        ✔ | —                       | Forwarded to `GPUTextureDescriptor.format`. `read()`/`readFloats()` support the color formats listed under [Readback formats](#readback-formats). |
| opts.usage         | `readonly TextureUsageName[]`                                           |        ✔ | —                       | Vgpu usage names mapped to `GPUTextureUsage` flags.                                                                                               |
| opts.mipLevelCount | `number`                                                                |        ✖ | WebGPU default (`1`)    | Only included in the native descriptor when provided; getter returns `opts.mipLevelCount ?? 1`.                                                   |
| opts.sampleCount   | `1 \| 4`                                                                |        ✖ | WebGPU default (`1`)    | Only included when provided; getter returns `opts.sampleCount ?? 1`. Use `4` for MSAA where WebGPU allows it.                                     |
| opts.dimension     | `GPUTextureDimension`                                                   |        ✖ | WebGPU default (`"2d"`) | Only included when provided; getter returns `opts.dimension ?? "2d"`.                                                                             |
| opts.viewFormats   | `readonly GPUTextureFormat[]`                                           |        ✖ | `[]`                    | Only included when provided; getter returns `opts.viewFormats ?? []`.                                                                             |
| opts.label         | `string`                                                                |        ✖ | `undefined`             | Forwarded to `GPUTextureDescriptor.label` and exposed via `texture.label`.                                                                        |

Valid `TextureUsageName` values: `"copy_src"`, `"copy_dst"`, `"texture_binding"`, `"storage_binding"`, `"render_attachment"`.

### Constructor

| Param     | Type                    | Required | Default   | Notes                                                                                                                        |
| --------- | ----------------------- | -------: | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| device    | `Device`                |        ✔ | —         | Owning device wrapper. Normally supplied by `Device.createTexture(...)`.                                                     |
| gpu       | `GPUTexture`            |        ✔ | —         | Raw WebGPU texture.                                                                                                          |
| options   | `TextureOptions`        |        ✔ | —         | Original vgpu descriptor exposed as `texture.options`.                                                                       |
| ownership | `"owned" \| "external"` |        ✖ | `"owned"` | Owned textures can be resized and destroyed by the wrapper; external textures cannot be resized or destroyed by the wrapper. |

### Views, resize, readback

| Param | Type                                                             | Required | Default     | Notes                                                                             |
| ----- | ---------------------------------------------------------------- | -------: | ----------- | --------------------------------------------------------------------------------- |
| desc  | `GPUTextureViewDescriptor`                                       |        ✖ | `undefined` | `createView(desc?)` forwards directly to `gpu.createView(desc)`.                  |
| size  | `readonly [number, number] \| readonly [number, number, number]` |        ✔ | —           | New extent for `resize(...)`. A 2-tuple preserves the current depth/array layers. |

**Returns:**

* `Device.createTexture(opts)` returns `Texture`.
* `texture.view` returns a cached default `GPUTextureView` created with no descriptor.
* `createView(desc?)` returns a fresh `GPUTextureView`.
* `resize(size)` returns `false` if the extent is unchanged, otherwise reallocates the raw texture and returns `true`.
* `read()` returns `Promise<Uint8Array>` with unpadded texel bytes in the texture's own format: `byteLength` is `width * height * bytesPerPixel(format)` (4 for `rgba8unorm`, 8 for `rgba16float`, 16 for `rgba32float`, …). `bgra*` bytes are swizzled to RGBA order.
* `readFloats()` returns `Promise<Float32Array>` with one f32 per component, row-major, `width * height * components(format)` long. Float formats keep their HDR values (no clamping to `[0, 1]`), `unorm8` formats are normalized by `/ 255` without srgb gamma conversion.
* `destroy()` and `dispose()` return `void`.

**Throws:**

* `VGPU-CORE-TEXTURE-DESTROYED` when `view`, `resize(...)`, `read()`, or `readFloats()` is used after `destroy()`/`dispose()` — create a new texture instead.
* `VGPU-CORE-EXTERNAL-TEXTURE` when `resize(...)` is called on a texture constructed with `ownership: "external"` — resize the owning canvas/swapchain/resource instead.
* `VGPU-CORE-TEXTURE-RESIZE-LOCKED` when an internal resize lock is active — follow the lock message and resize through the owner that installed the lock.
* `VGPU-CORE-UNSUPPORTED-FORMAT` when `read()`/`readFloats()` is called on a format outside [Readback formats](#readback-formats) (depth/stencil, packed, snorm/uint/sint, and compressed formats) — blit into a supported format first, or read the data through a storage buffer.
* Native WebGPU validation errors may occur for invalid size/format/usage combinations.

## Examples

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const target = device.createTexture({
  label: "offscreen-target",
  size: [4, 4],
  format: "rgba8unorm",
  usage: ["render_attachment", "texture_binding", "copy_src"],
});

const defaultView = target.view;
const explicitView = target.createView({ label: "offscreen-target.view" });
console.log(defaultView, explicitView, target.sampleCount); // sampleCount defaults to 1

device.destroy();
```

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const texture = device.createTexture({
  size: [1, 1, 6],
  format: "rgba8unorm",
  usage: ["texture_binding", "copy_src"],
});

console.log(texture.resize([2, 2])); // true; depthOrArrayLayers stays 6
console.log(texture.size); // [2, 2, 6]
console.log(texture.resize([2, 2, 6])); // false; unchanged

const pixels = await texture.read();
console.log(pixels.byteLength); // width * height * 4

device.destroy();
```

```ts
import { createMockAdapter } from "vgpu/mock";

const device = await createMockAdapter().requestDevice();
const hdr = device.createTexture({
  size: [2, 2],
  format: "rgba16float",
  usage: ["render_attachment", "copy_src"],
});

const bytes = await hdr.read();
console.log(bytes.byteLength); // 2 * 2 * 8 — raw half-float bytes

const floats = await hdr.readFloats();
console.log(floats.length); // 2 * 2 * 4 — decoded rgba components, values may exceed 1

device.destroy();
```

## Readback formats

| Format                          | Bytes per texel | Components | `readFloats()` decoding                 |
| ------------------------------- | --------------: | ---------: | --------------------------------------- |
| `r8unorm`                       |               1 |          1 | `byte / 255`                            |
| `rg8unorm`                      |               2 |          2 | `byte / 255`                            |
| `rgba8unorm`, `rgba8unorm-srgb` |               4 |          4 | `byte / 255` (no srgb gamma conversion) |
| `bgra8unorm`, `bgra8unorm-srgb` |               4 |          4 | `byte / 255`, channels swizzled to RGBA |
| `r16float`                      |               2 |          1 | binary16 widened to f32                 |
| `rg16float`                     |               4 |          2 | binary16 widened to f32                 |
| `rgba16float`                   |               8 |          4 | binary16 widened to f32                 |
| `r32float`                      |               4 |          1 | verbatim f32                            |
| `rg32float`                     |               8 |          2 | verbatim f32                            |
| `rgba32float`                   |              16 |          4 | verbatim f32                            |

Subnormals, infinities, and NaN survive the binary16 → f32 widening unchanged.

## Notes

* `texture.view` is cached and descriptorless. Use `createView(descriptor)` for mip, array-layer, cube, or format-specific views.
* `resize(...)` preserves all descriptor fields except `size`; contents are not preserved. Rebuild bind groups or caches keyed by `texture.gpu` after a resize.
* Prefer `texture.destroy()`/`texture.dispose()` over `texture.gpu.destroy()` so the wrapper invalidates cached views and emits lifecycle state correctly.
* Include `"copy_src"` when you plan to call `read()`/`readFloats()` on real WebGPU devices; mock textures can still expose their mock bytes.
* Prefer `readFloats()` for HDR textures (`rgba16float`, `rgba32float`): `read()` hands back the raw half/float bytes, which are only useful after a decode. `read()` remains the right call for `rgba8unorm` snapshots and PNG encoding.
* **See also:** `Device`, `Buffer`, `Queue`, `cubeView`, `layerView`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: UniformPool
description: Dynamic-offset ring allocator for many per-draw uniforms. Allocate a typed `UniformSlot` once, push values each frame, call `endFrame()`, and pass returned offsets through `p.draw(draw, { offsets })`.
---

# UniformPool



## Import

```ts
import { UniformPool } from "vgpu/core";
import type { UniformPoolOptions, UniformLayout, UniformSlot } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

interface UniformPoolOptions {
  readonly capacityBytes?: number;
}

interface UniformLayout<T> {
  readonly size: number;
  readonly bindings?: readonly GPUBindGroupLayoutEntry[];
  readonly bindGroupLayout?: GPUBindGroupLayout;
  encode(value: T, dst: ArrayBuffer, byteOffset: number): void;
}

interface UniformSlot<T> {
  readonly pool: UniformPool;
  readonly layout: UniformLayout<T>;
  readonly bindGroup: GPUBindGroup;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly gpu: GPUBuffer;
  readonly stride: number;
  push(value: T): number;
  pushBytes(bytes: ArrayBufferView<ArrayBuffer>): number;
}

declare class UniformPool {
  readonly device: Device;
  readonly minOffsetAlignment: number;
  readonly capacityBytes: number;
  readonly maxUniformBindingSize: number;
  readonly cpuMirror: ArrayBuffer;
  readonly gpu: GPUBuffer;
  constructor(device: Device, opts?: UniformPoolOptions);
  get usedBytes(): number;
  get disposed(): boolean;
  alloc<T>(layout: UniformLayout<T>): UniformSlot<T>;
  push<T>(slot: UniformSlot<T>, value: T): number;
  pushBytes(slot: UniformSlot<unknown>, bytes: ArrayBufferView<ArrayBuffer>): number;
  beginFrame(frameIndex: number): void;
  endFrame(): void;
  assertReadyForSubmit(where: string): void;
  dispose(): void;
}
```

## Parameters

| Param                      | Type                                 | Required | Default                                                         | Notes                                                                                                             |
| -------------------------- | ------------------------------------ | -------: | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| device                     | `Device`                             |        ✔ | —                                                               | Core device. Device limits determine alignment and max binding size.                                              |
| opts                       | `UniformPoolOptions`                 |        ✖ | `{}`                                                            | Pool capacity options.                                                                                            |
| opts.capacityBytes         | `number`                             |        ✖ | `4 * 1024 * 1024`                                               | CPU mirror and GPU buffer size.                                                                                   |
| layout.size                | `number`                             |        ✔ | —                                                               | Logical uniform byte size before stride alignment. Must fit both pool capacity and `maxUniformBufferBindingSize`. |
| layout.bindings            | `readonly GPUBindGroupLayoutEntry[]` |        ✖ | Binding `0` uniform, dynamic offset, visibility vertex+fragment | Used only when `layout.bindGroupLayout` is omitted.                                                               |
| layout.bindGroupLayout     | `GPUBindGroupLayout`                 |        ✖ | New layout from `layout.bindings` or default binding `0`        | Usually `draw.layout(group, { dynamicOffsets: true })` so the draw pipeline and slot agree.                       |
| layout.encode              | `(value, dst, byteOffset) => void`   |        ✔ | —                                                               | Writes one value into the pool CPU mirror at `byteOffset`.                                                        |
| alloc.layout               | `UniformLayout<T>`                   |        ✔ | —                                                               | Layout used for one reusable slot. Slot stride is `roundUp(layout.size, minUniformBufferOffsetAlignment)`.        |
| push.slot                  | `UniformSlot<T>`                     |        ✔ | —                                                               | Must be allocated by the same pool.                                                                               |
| push.value                 | `T`                                  |        ✔ | —                                                               | Encoded into the CPU mirror; returned offset is passed as dynamic offset.                                         |
| pushBytes.bytes            | `ArrayBufferView<ArrayBuffer>`       |        ✔ | —                                                               | Must have byte length exactly equal to `slot.layout.size`.                                                        |
| beginFrame.frameIndex      | `number`                             |        ✔ | —                                                               | Currently unused marker; call before pushes to reset the ring head to `0`.                                        |
| assertReadyForSubmit.where | `string`                             |        ✔ | —                                                               | Error context if pushes are unflushed.                                                                            |

**Returns:** Constructor returns `UniformPool`; `alloc()` returns `UniformSlot<T>`; `push()` / `pushBytes()` return the dynamic byte offset; `usedBytes` returns current ring head; lifecycle methods return `void`.

**Throws:** `VGPU-UNIFORM-POOL-OVERFLOW` when a push would exceed `capacityBytes`; `VGPU-UNIFORM-LAYOUT-OVERSIZED` when `layout.size` exceeds pool capacity or device `maxUniformBufferBindingSize`; `VGPU-CORE-INVALID-USAGE` when using a disposed pool, pushing a slot from another pool, `pushBytes` length mismatches, or submitting before `endFrame()` flushes unflushed pushes.

## Examples

```ts
import { init, clock, draw, frame, target } from "vgpu/mock";
import { UniformPool, type UniformLayout } from "vgpu/core";

const gpu = await init();
const colorTarget = target(gpu, { size: [32, 32] });
const drawable = draw(gpu, { shader: `
  struct Object { model: mat4x4f }
  @group(0) @binding(0) var<uniform> object: Object;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return object.model * vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
` });

type ObjectUniforms = { model: Float32Array };
const objectLayout: UniformLayout<ObjectUniforms> = {
  size: 64,
  bindGroupLayout: drawable.layout(0, { dynamicOffsets: true }),
  encode(value, dst, byteOffset) {
    new Float32Array(dst, byteOffset, 16).set(value.model);
  },
};
const pool = new UniformPool(gpu.device, { capacityBytes: 1 << 20 });
const slot = pool.alloc(objectLayout);
drawable.group(0, slot.bindGroup);

pool.beginFrame(clock(gpu).frameCount);
const offset = slot.push({ model: new Float32Array(16) });
pool.endFrame();
frame(gpu, (currentFrame) => currentFrame.pass({ target: colorTarget }, (pass) => pass.draw(drawable, { offsets: { 0: [offset] } })));
```

```ts
import { init } from "vgpu/mock";
import { UniformPool, type UniformLayout } from "vgpu/core";

const gpu = await init();
const pool = new UniformPool(gpu.device, { capacityBytes: 1024 });
const layout: UniformLayout<Float32Array> = {
  size: 16,
  encode(value, dst, byteOffset) { new Float32Array(dst, byteOffset, 4).set(value); },
};
const slot = pool.alloc(layout);
pool.beginFrame(0);
const offset = slot.pushBytes(new Float32Array([1, 0, 0, 0]));
pool.endFrame();
void offset;
```

## Notes

* Call `beginFrame()` before pushes and `endFrame()` before submitting commands that use the returned offsets.
* A dynamic-offset bind group stores `offset: 0`; the per-draw offset comes from `DrawCallOptions.offsets`.
* The pool is a ring for transient per-frame data, not long-lived storage. Persistent globals usually fit `Uniform` or `SharedUniforms` better.
* **See also:** `Draw.layout`, `DrawCallOptions.offsets`, `FramePass.draw`, `Uniform`, `StructuredUniform`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Uniform
description: Low-level user-owned uniform buffer with a stable bind group at binding `0`. Prefer main API (`vgpu`) `set({ params: ... })` for ordinary values; use `Uniform` when you need byte-level writes or one buffer shared by many draws.
---

# Uniform



## Import

```ts
import { Uniform } from "vgpu/core";
import type { UniformOptions } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

declare interface UniformOptions {
  readonly size: number;
  readonly label?: string;
  readonly visibility?: GPUShaderStageFlags;
  readonly bindGroupLayout?: GPUBindGroupLayout;
}

declare class Uniform {
  readonly device: Device;
  readonly size: number;
  readonly buffer: import("vgpu/core").Buffer;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly bindGroup: GPUBindGroup;
  constructor(device: Device, opts: UniformOptions);
  get gpu(): GPUBuffer;
  write(data: BufferSource, offset?: number): void;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

| Param                | Type                  | Required | Default                                                                           | Notes                                                                                                   |
| -------------------- | --------------------- | -------: | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| device               | `Device`              |        ✔ | —                                                                                 | Core device, usually `gpu.device` from the main API (`vgpu`) `init()`.                                  |
| opts                 | `UniformOptions`      |        ✔ | —                                                                                 | Buffer/layout options.                                                                                  |
| opts.size            | `number`              |        ✔ | —                                                                                 | Byte size. Used for `device.createBuffer({ usage: ["uniform", "copy_dst"] })` and `minBindingSize`.     |
| opts.label           | `string`              |        ✖ | `undefined`                                                                       | Forwarded to buffer label; layout label becomes `${label}.bgl`; bind group label becomes `${label}.bg`. |
| opts.visibility      | `GPUShaderStageFlags` |        ✖ | `GPUShaderStage.VERTEX \| GPUShaderStage.FRAGMENT` with numeric fallback `1 \| 2` | Ignored when `opts.bindGroupLayout` is supplied.                                                        |
| opts.bindGroupLayout | `GPUBindGroupLayout`  |        ✖ | A new binding-0 uniform layout                                                    | Reuse a pipeline/draw-owned layout. Binding `0` must be a compatible uniform buffer.                    |
| uniform.write.data   | `BufferSource`        |        ✔ | —                                                                                 | Bytes uploaded with `queue.writeBuffer`.                                                                |
| uniform.write.offset | `number`              |        ✖ | `0`                                                                               | Destination byte offset in the buffer.                                                                  |

**Returns:** Constructor returns `Uniform`; `gpu` returns the underlying `GPUBuffer`; `write()`, `destroy()`, and `dispose()` return `void`.

**Throws:** No main API (`vgpu`) `VGPU-*` errors are thrown directly by `Uniform`; invalid sizes, incompatible reused layouts, out-of-range writes, or destroyed-buffer usage can surface as core/native WebGPU validation errors. Binding a `Uniform` through main API (`vgpu`) can still trigger `VGPU-R1-OWNERSHIP-FLIP` if the same shader binding was first set with JS values.

## Examples

```ts
import { init, draw } from "vgpu/mock";
import { Uniform } from "vgpu/core";

const gpu = await init();
const camera = new Uniform(gpu.device, { size: 64, label: "camera" });
camera.write(new Float32Array(16));

const drawable = draw(gpu, { shader: `
  struct Camera { viewProjection: mat4x4f }
  @group(0) @binding(0) var<uniform> camera: Camera;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return camera.viewProjection * vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
` });
drawable.set({ camera });
```

```ts
import { init, draw } from "vgpu/mock";
import { Uniform } from "vgpu/core";

const gpu = await init();
const drawable = draw(gpu, { shader: `
  struct Params { value: f32 }
  @group(0) @binding(0) var<uniform> params: Params;
  @vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(params.value); }
` });
const params = new Uniform(gpu.device, { size: 16, bindGroupLayout: drawable.layout(0) });
params.write(new Float32Array([1, 0, 0, 0]));
drawable.set({ params });
```

## Notes

* `Uniform` is user-owned from the first `draw.set({ name: uniform })`; vgpu binds its identity and never packs JS values into it.
* It creates a non-dynamic bind group. For many per-object uniforms with dynamic offsets, use `UniformPool` instead.
* Call `destroy()` / `dispose()` when the buffer lifetime ends.
* **See also:** `SharedUniforms`, `StructuredUniform`, `UniformPool`, `Draw.set`, `Effect.set`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: VGPUAdapter
description: `VGPUAdapter` is the minimal core adapter interface. Use it when code should request a vgpu `Device` without caring whether the backing implementation is browser WebGPU, Node/Dawn, or the mock adapter.
---

# VGPUAdapter



## Import

```ts
import type { VGPUAdapter } from "vgpu/core";
```

## Signature

```ts
import type { Device, RequiredDeviceLimits } from "vgpu/core";

interface CreateDeviceOptions {
  readonly powerPreference?: GPUPowerPreference;
  readonly requiredFeatures?: readonly GPUFeatureName[];
  readonly requiredLimits?: RequiredDeviceLimits;
  readonly label?: string;
}

interface VGPUAdapter {
  requestDevice(opts?: CreateDeviceOptions): Promise<Device>;
}
```

## Parameters

### `requestDevice(opts?)` / `CreateDeviceOptions`

| Param                 | Type                        | Required | Default     | Notes                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------- | --------------------------- | -------: | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts                  | `CreateDeviceOptions`       |        ✖ | `undefined` | Device request options. Concrete adapters may accept additional adapter-specific keys, but this is the core portable subset.                                                                                                                                                                                                                                                              |
| opts.powerPreference  | `GPUPowerPreference`        |        ✖ | `undefined` | Passed to adapter selection by browser/node implementations. The mock adapter ignores it.                                                                                                                                                                                                                                                                                                 |
| opts.requiredFeatures | `readonly GPUFeatureName[]` |        ✖ | `undefined` | Forwarded to native `adapter.requestDevice({ requiredFeatures })` by browser/node implementations after a `validateRequiredFeatures` check against the adapter's supported features (unsupported names throw `VGPU-FEATURE-UNSUPPORTED`). The mock adapter honors it against its declared `createMockAdapter({ features })` set and enables exactly the requested features on the device. |
| opts.requiredLimits   | `RequiredDeviceLimits`      |        ✖ | `undefined` | Forwarded unchanged to native `adapter.requestDevice({ requiredLimits })`; custom/mock adapters receive the same option.                                                                                                                                                                                                                                                                  |
| opts.label            | `string`                    |        ✖ | `undefined` | Node adapter assigns it to `GPUDevice.label`; browser core request path currently does not assign it in `vgpu-api`, and the mock adapter ignores it.                                                                                                                                                                                                                                      |

**Returns:** `requestDevice(opts?)` returns `Promise<Device>` wrapping the raw `GPUDevice` created by the concrete adapter.

**Throws:**

* `VGPU-RING1-UNSUPPORTED` may be thrown by higher-level browser/node initialization paths when no adapter factory or browser adapter is available — provide a concrete adapter such as `createMockAdapter()` or run in a WebGPU-capable environment.
* Adapter-specific `VGPU-*` errors can be thrown before the core `Device` exists, for example Node adapter binary/adapter failures — inspect `.code`, `.message`, and `.fix` on `VGPUError`.
* `VGPU-FEATURE-UNSUPPORTED` is thrown before device creation when `requiredFeatures` names a feature the adapter does not support — remove the unsupported name(s) or use an adapter that supports them.
* Native WebGPU request errors may be thrown when `requiredLimits` values are unsupported — request only capabilities reported by the chosen adapter.

## Examples

```ts
import type { Device, VGPUAdapter } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

async function withDevice(adapter: VGPUAdapter): Promise<Device> {
  return adapter.requestDevice({ label: "example-device" });
}

const device = await withDevice(createMockAdapter());
console.log(device.queue.gpu);
device.destroy();
```

```ts
import type { VGPUAdapter } from "vgpu/core";
import { createMockAdapter } from "vgpu/mock";

const adapter: VGPUAdapter = createMockAdapter();
const device = await adapter.requestDevice({
  powerPreference: "high-performance",
  requiredFeatures: [],
  requiredLimits: {},
});

const buffer = device.createBuffer({ size: 4, usage: ["copy_dst", "copy_src"] });
buffer.write(new Uint32Array([42]));
console.log(new Uint32Array(await buffer.read(4))[0]);

device.destroy();
```

## Notes

* `VGPUAdapter` is an interface, not a class. Import it with `import type` unless you only need documentation prose.
* The core interface intentionally has a small portable option set. Concrete adapters can extend it without changing `VGPUAdapter`.
* The mock adapter honors `requiredFeatures` against its declared `createMockAdapter({ features })` set (default none) and ignores the other `CreateDeviceOptions`; do not use a passing mock request as proof that native required features/limits are available.
* Use `Device.features` and `Device.limits` after request to gate optional code paths.
* **See also:** `Device`, `CreateDeviceOptions`, `VGPUError`, `createMockAdapter`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: VGPUError
description: `VGPUError` is the structured error base class used by vgpu. Use it when you need stable machine-readable `code`, `severity`, optional `fix`, `where`, and `cause` fields instead of parsing error messages.
---

# VGPUError



## Import

```ts
import { VGPUError, ValidationError } from "vgpu/core";
```

## Signature

```ts
type VGPUErrorSeverity = "error" | "warning" | "info";

interface VGPUErrorData {
  readonly code: string;
  readonly message: string;
  readonly severity?: VGPUErrorSeverity;
  readonly fix?: string;
  readonly where?: string;
  readonly cause?: unknown;
}

declare class VGPUError extends Error {
  readonly code: string;
  readonly severity: VGPUErrorSeverity;
  readonly fix?: string;
  readonly where?: string;
  readonly cause?: unknown;
  constructor(data: VGPUErrorData);
}

declare class ValidationError extends VGPUError {
  constructor(data: Omit<VGPUErrorData, "severity">);
}
```

## Parameters

### `new VGPUError(data)`

| Param         | Type                             | Required | Default     | Notes                                                                        |
| ------------- | -------------------------------- | -------: | ----------- | ---------------------------------------------------------------------------- |
| data.code     | `string`                         |        ✔ | —           | Stable machine-readable code. Core validation sites use `VGPU-CORE-*` codes. |
| data.message  | `string`                         |        ✔ | —           | Human-readable message passed to `Error`.                                    |
| data.severity | `"error" \| "warning" \| "info"` |        ✖ | `"error"`   | Stored as `.severity`; omitted data becomes an error.                        |
| data.fix      | `string`                         |        ✖ | `undefined` | Optional remediation text.                                                   |
| data.where    | `string`                         |        ✖ | `undefined` | Optional source/context label such as `"Device.createBuffer"`.               |
| data.cause    | `unknown`                        |        ✖ | `undefined` | Forwarded to `Error` via `{ cause }` and stored as `.cause`.                 |

### `new ValidationError(data)`

| Param        | Type      | Required | Default     | Notes                                         |
| ------------ | --------- | -------: | ----------- | --------------------------------------------- |
| data.code    | `string`  |        ✔ | —           | Stable machine-readable validation code.      |
| data.message | `string`  |        ✔ | —           | Human-readable validation message.            |
| data.fix     | `string`  |        ✖ | `undefined` | Optional remediation text.                    |
| data.where   | `string`  |        ✖ | `undefined` | Optional source/context label.                |
| data.cause   | `unknown` |        ✖ | `undefined` | Optional original error or validation detail. |

**Returns:**

* `new VGPUError(data)` returns an `Error` instance with `.name === "VGPUError"`, `.code`, `.severity`, optional `.fix`, optional `.where`, and optional `.cause`.
* `new ValidationError(data)` returns a `VGPUError` subclass with `.name === "ValidationError"` and `.severity === "error"`.

**Throws:** Constructors do not throw vgpu errors. They can only throw if the JavaScript runtime cannot construct `Error` or assign fields.

## Examples

```ts
import { VGPUError } from "vgpu/core";

const error = new VGPUError({
  code: "VGPU-CORE-INVALID-USAGE",
  message: "Buffer size must be greater than zero.",
  severity: "error",
  where: "Device.createBuffer",
  fix: "Pass a positive byte length.",
});

console.log(error.name, error.code, error.severity, error.where);
```

```ts
import { ValidationError } from "vgpu/core";

function requirePositive(value: number): void {
  if (value <= 0) {
    throw new ValidationError({
      code: "VGPU-CORE-INVALID-USAGE",
      message: "Expected a positive value.",
      where: "example.requirePositive",
    });
  }
}

try {
  requirePositive(0);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.code); // "VGPU-CORE-INVALID-USAGE"
  }
}
```

```ts
import { createMockAdapter } from "vgpu/mock";
import { VGPUError } from "vgpu/core";

const device = await createMockAdapter().requestDevice();

device.pushErrorScope("validation");
device.createBuffer({ size: 0, usage: ["copy_dst"] });
const error = await device.popErrorScope();

if (error instanceof VGPUError) {
  console.log(error.code); // "VGPU-CORE-INVALID-USAGE"
}

device.destroy();
```

## Notes

* Match on `.code`, not `.message`. Messages can become more descriptive; codes are the stable contract.
* `ValidationError` forces severity to `"error"`; use `VGPUError` directly for `"warning"` or `"info"` severities.
* Core currently emits these `VGPU-CORE-*` codes from core layer (`vgpu/core`) code paths: `VGPU-CORE-INVALID-USAGE`, `VGPU-CORE-VALIDATION`, `VGPU-CORE-EXTERNAL-TEXTURE`, `VGPU-CORE-TEXTURE-RESIZE-LOCKED`, `VGPU-CORE-TEXTURE-DESTROYED`, `VGPU-CORE-UNSUPPORTED-FORMAT`, `VGPU-CORE-BIND-GROUP-LAYOUT-REQUIRED`, `VGPU-CORE-SAMPLER-ANISOTROPY-FILTERS`, `VGPU-CORE-BINDING-INVALID`, and `VGPU-CORE-VISIBILITY-INVALID`.
* Native WebGPU errors are converted to `VGPU-CORE-VALIDATION` only by `Device.popErrorScope()`; other native calls may still throw native errors directly.
* **See also:** `Device`, `Buffer`, `Texture`, `bind`, `VGPUAdapter`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: createMockAdapter
description: `createMockAdapter(options?)` returns a pure-JavaScript `VGPUAdapter`. Its devices allocate in-memory buffers backed by `Uint8Array`, making core testable without Dawn or native GPU libraries.
---

# createMockAdapter



`options.features` declares the optional features the mock adapter supports (its
equivalent of `GPUAdapter.features`); it defaults to none. `requestDevice` rejects a
`requiredFeatures` entry outside that set with `VGPU-FEATURE-UNSUPPORTED`, and —
faithful to WebGPU — the created device's `features` holds exactly the requested
features, not the adapter's full set. This lets tests exercise feature-gated code
paths (e.g. `"depth-clip-control"`) both with and without the feature granted.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Camera
description: Type alias for scene cameras returned by `perspectiveCamera()` and `orthographicCamera()`. Use it when storing a camera without caring which helper produced it.
---

# Camera



## Import

```ts
import type { Camera } from "vgpu/scene";
```

## Signature

```ts
type Camera = import("vgpu/scene").SceneCamera;
```

## Parameters

| Param  | Type          | Required | Default | Notes                                               |
| ------ | ------------- | -------- | ------- | --------------------------------------------------- |
| Camera | `SceneCamera` | ✔        | —       | Alias that keeps helper and consumer types in sync. |

**Returns:** Not a callable; this alias ensures `perspectiveCamera()` and `orthographicCamera()` share the same contract.

**Throws:** None.

## Examples

```ts
import type { Camera } from "vgpu/scene";
import { perspectiveCamera } from "vgpu/scene";

const camera: Camera = perspectiveCamera({
  fov: 60,
  aspect: 16 / 9,
  position: [0, 2, 4],
  target: [0, 0, 0],
});
```

## Notes

* Prefer the `Camera` alias when storing values in state containers so switching between perspective and orthographic helpers stays type-safe.
* **See also:** `SceneCamera`, `perspectiveCamera`, `orthographicCamera`.

***

# SceneCamera

Common contract of stateful camera nodes: column-major matrices plus the camera position, exposed as stable `Float32Array` identities that are updated in place by `set()` / `lookAt()`.

## Import

```ts
import type { SceneCamera } from "vgpu/scene";
```

## Signature

```ts
interface SceneCamera {
  readonly viewProjection: Float32Array;
  readonly viewProjectionMatrix: Float32Array;
  readonly position: Float32Array;
  readonly view: Float32Array;
  readonly projection: Float32Array;
  readonly worldPosition: Float32Array;
}
```

## Parameters

| Field                | Type           | Required | Default | Notes                                                                       |
| -------------------- | -------------- | -------- | ------- | --------------------------------------------------------------------------- |
| viewProjection       | `Float32Array` | ✔        | —       | Column-major projection × view matrix. Bind this to your WGSL uniforms.     |
| viewProjectionMatrix | `Float32Array` | ✔        | —       | Alias of `viewProjection`, kept for naming continuity.                      |
| position             | `Float32Array` | ✔        | —       | Local position (world position for unparented cameras). Mutate via `set()`. |
| view                 | `Float32Array` | ✔        | —       | Inverse of the camera node's world matrix.                                  |
| projection           | `Float32Array` | ✔        | —       | Projection matrix derived from the camera's parameters.                     |
| worldPosition        | `Float32Array` | ✔        | —       | World position used for specular highlights or parallax.                    |

**Returns:** Not a callable — the interface describes what camera helpers return.

**Throws:** None.

## Examples

```ts
import { perspectiveCamera } from "vgpu/scene";

const camera = perspectiveCamera({
  fov: 50,
  aspect: 4 / 3,
  position: [2, 2, 4],
  target: [0, 0, 0],
});

void camera.viewProjection;
```

## Notes

* Cameras are scene nodes: update them in place with `set()` / `lookAt()` instead of recreating them.
* `viewProjectionMatrix` is a duplicate reference so existing consumer code keeps working.
* **See also:** `Camera`, `CameraVec3`, `perspectiveCamera`, `SceneNode`.

***

# CameraVec3

Input-friendly vector type accepted by camera helpers. Accepts tuple literals or typed arrays; helpers always clone into a `Float32Array`.

## Import

```ts
import type { CameraVec3 } from "vgpu/scene";
```

## Signature

```ts
type CameraVec3 = readonly [number, number, number] | Float32Array;
```

## Parameters

| Param            | Type                                | Required | Default | Notes                                                                  |
| ---------------- | ----------------------------------- | -------- | ------- | ---------------------------------------------------------------------- |
| tuple form       | `readonly [number, number, number]` | ✔        | —       | Pass literal XYZ coordinates without allocations.                      |
| typed array form | `Float32Array`                      | ✔        | —       | Use when positions already live in typed arrays (e.g. math libraries). |

**Returns:** Not a callable — this is the accepted input type for camera helpers.

**Throws:** None.

## Examples

```ts
import type { CameraVec3 } from "vgpu/scene";

const orbitPos: CameraVec3 = new Float32Array([0, 3, 5]);
```

## Notes

* Helpers clone the input, so mutating the original array after the call does not affect the camera.
* **See also:** `perspectiveCamera`, `orthographicCamera`.

***

# Vec3

Type-only re-export of the `Vec3` alias from `wgpu-matrix`. Used internally by low-level camera helpers.

## Import

```ts
import type { Vec3 } from "vgpu/scene";
```

## Signature

```ts
type Vec3 = Float32Array;
```

## Parameters

| Param | Type           | Required | Default | Notes                                                                              |
| ----- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------- |
| Vec3  | `Float32Array` | ✔        | —       | Column vector used by low-level helpers; provided for users wiring math utilities. |

**Returns:** Not a callable.

**Throws:** None.

## Examples

```ts
import type { Vec3 } from "vgpu/scene";

const up: Vec3 = new Float32Array([0, 1, 0]);
```

## Notes

* Prefer `CameraVec3` when calling public helpers; `Vec3` is useful when interoperating with `@vgpu/wgsl` math utilities.
* **See also:** `Mat4`, `CameraVec3`.

***

# Mat4

Type-only re-export of the column-major 4×4 matrix type from `wgpu-matrix`. Helpful when authoring math utilities that feed into scene helpers.

## Import

```ts
import type { Mat4 } from "vgpu/scene";
```

## Signature

```ts
type Mat4 = Float32Array;
```

## Parameters

| Param | Type           | Required | Default | Notes                                              |
| ----- | -------------- | -------- | ------- | -------------------------------------------------- |
| Mat4  | `Float32Array` | ✔        | —       | Column-major 4×4 matrix used across scene helpers. |

**Returns:** Not a callable.

**Throws:** None.

## Examples

```ts
import type { Mat4 } from "vgpu/scene";

const identity: Mat4 = new Float32Array([
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1,
]);
```

## Notes

* Use these aliases when building custom math helpers so your APIs stay aligned with vgpu’s scene entrypoints.
* **See also:** `Vec3`, `SceneCamera`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: degToRad
description: Converts degrees to radians for custom scene math. Use it when a shader uniform, animation helper, or math API expects radians while your input is in degrees.
---

# degToRad



## Import

```ts
import { degToRad } from "vgpu/scene";
```

## Signature

```ts
declare function degToRad(deg: number): number;
```

## Parameters

| Param | Type     | Required | Default | Notes                                                                                                                    |
| ----- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| deg   | `number` | ✔        | —       | Angle in degrees. Positive, negative, fractional, `Infinity`, and `NaN` are passed through JavaScript number arithmetic. |

**Returns:** `number` — `deg * Math.PI / 180`, suitable for `Math.sin`, `Math.cos`, matrix helpers, or shader uniforms that expect radians.
**Throws:** None.

## Examples

```ts
import { degToRad } from "vgpu/scene";

const quarterTurn = degToRad(90);
const rotation = { sinAngle: Math.sin(quarterTurn), cosAngle: Math.cos(quarterTurn) };
void rotation;
```

```ts
import { degToRad } from "vgpu/scene";

const clockwise = degToRad(-45);
console.log(clockwise < 0);
```

## Notes

* Public camera helpers in `vgpu/scene` accept degrees for field-of-view values; do not convert `PerspectiveCameraOptions.fov` yourself.
* Use `degToRad` for custom transforms and CPU-side uniform values that explicitly need radians.
* **See also:** `perspectiveCamera`, `orbit`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: box
description: Creates a pure cube descriptor for `geometry(gpu)`. Descriptors are device-agnostic, so you can serialize or clone them freely and upload later.
---

# box



## Import

```ts
import { box } from "vgpu/scene";
```

## Signature

```ts
declare function box(options?: import("vgpu/scene").BoxOptions): import("vgpu/scene").SceneGeometryOfKind<"box">;
```

## Parameters

| Param        | Type         | Required | Default | Notes                                                               |
| ------------ | ------------ | -------- | ------- | ------------------------------------------------------------------- |
| options      | `BoxOptions` | ✖        | `{}`    | Configuration bag for the cube descriptor.                          |
| options.size | `number`     | ✖        | `1`     | Edge length used by the geometry factory. Any positive value works. |

**Returns:** `SceneGeometryOfKind<"box">` — frozen descriptor with `kind: "box"` and the props you provided; omitted fields stay omitted until upload-time defaults are applied.

**Throws:** None. Negative sizes do not throw but invert normals when uploaded.

## Examples

```ts
import { box } from "vgpu/scene";

const tallCube = box({ size: 3 });
console.log(tallCube.kind); // "box"
```

## Notes

* Descriptors contain zero GPU state; call `geometry(gpu, descriptor)` per device.
* **See also:** `BoxOptions`, `SceneGeometryOfKind`.

***

# BoxOptions

Shape configuration shared by `box()` descriptors.

## Import

```ts
import type { BoxOptions } from "vgpu/scene";
```

## Signature

```ts
interface BoxOptions {
  readonly size?: number;
}
```

## Parameters

| Field | Type     | Required | Default | Notes                                |
| ----- | -------- | -------- | ------- | ------------------------------------ |
| size  | `number` | ✖        | `1`     | Edge length measured in scene units. |

**Returns:** Not applicable (type definition).

**Throws:** None.

## Examples

```ts
import type { BoxOptions } from "vgpu/scene";

const solid: BoxOptions = { size: 2 };
```

## Notes

* Undefined fields are filled by the geometry factory when the descriptor is uploaded.
* **See also:** `box`, `SceneGeometry`.

***

# sphere

Generates a UV sphere descriptor with configurable radius and tessellation.

## Import

```ts
import { sphere } from "vgpu/scene";
```

## Signature

```ts
declare function sphere(options?: import("vgpu/scene").SphereOptions): import("vgpu/scene").SceneGeometryOfKind<"sphere">;
```

## Parameters

| Param                  | Type            | Required | Default | Notes                                         |
| ---------------------- | --------------- | -------- | ------- | --------------------------------------------- |
| options                | `SphereOptions` | ✖        | `{}`    | Configure radius or segments before upload.   |
| options.radius         | `number`        | ✖        | `0.5`   | Physical radius. Must be `> 0` once uploaded. |
| options.widthSegments  | `number`        | ✖        | `32`    | Meridians. Integer `>= 3`.                    |
| options.heightSegments | `number`        | ✖        | `16`    | Latitudes. Integer `>= 2`.                    |

**Returns:** `SceneGeometryOfKind<"sphere">`.

**Throws:** None while creating the descriptor. `geometry(gpu, sphere(...))` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`, segment counts drop below limits, or `(widthSegments + 1) * (heightSegments + 1)` exceeds the uint16 vertex cap (65 535).

## Examples

```ts
import { sphere } from "vgpu/scene";

const globe = sphere({ radius: 1.2, widthSegments: 48, heightSegments: 32 });
```

## Notes

* Higher segment counts increase vertex memory exponentially; use `icosphere()` for evenly distributed triangles.
* **See also:** `SphereOptions`, `Geometry`.

***

# SphereOptions

Configuration interface for `sphere()`.

## Import

```ts
import type { SphereOptions } from "vgpu/scene";
```

## Signature

```ts
interface SphereOptions {
  readonly radius?: number;
  readonly widthSegments?: number;
  readonly heightSegments?: number;
}
```

## Parameters

| Field          | Type     | Required | Default | Notes           |
| -------------- | -------- | -------- | ------- | --------------- |
| radius         | `number` | ✖        | `0.5`   | Must be `> 0`.  |
| widthSegments  | `number` | ✖        | `32`    | Integer `>= 3`. |
| heightSegments | `number` | ✖        | `16`    | Integer `>= 2`. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { SphereOptions } from "vgpu/scene";

const detail: SphereOptions = { widthSegments: 96, heightSegments: 64 };
```

## Notes

* Leave properties undefined to rely on the geometry factory defaults shown above.
* **See also:** `sphere`, `IcosphereOptions`.

***

# plane

XZ-aligned quad descriptor centered at the origin with +Y normals. Use it for ground planes, decals, or full-quad geometry.

## Import

```ts
import { plane } from "vgpu/scene";
```

## Signature

```ts
declare function plane(options?: import("vgpu/scene").PlaneOptions): import("vgpu/scene").SceneGeometryOfKind<"plane">;
```

## Parameters

| Param                  | Type                 | Required | Default  | Notes                                        |
| ---------------------- | -------------------- | -------- | -------- | -------------------------------------------- |
| options                | `PlaneOptions`       | ✖        | `{}`     | Width/height and tessellation controls.      |
| options.width          | `number`             | ✖        | `1`      | Total X extent; must be `> 0`.               |
| options.height         | `number`             | ✖        | `1`      | Total Z extent; must be `> 0`.               |
| options.widthSegments  | `number`             | ✖        | `1`      | Integer `>= 1`.                              |
| options.heightSegments | `number`             | ✖        | `1`      | Integer `>= 1`.                              |
| options.shading        | `"flat" \| "smooth"` | ✖        | `"flat"` | Present for parity; normals remain +Y today. |

**Returns:** `SceneGeometryOfKind<"plane">`.

**Throws:** None while creating the descriptor. `geometry(gpu, plane(...))` throws `VGPU-CORE-INVALID-USAGE` if width/height `<= 0`, segment counts `< 1`, or tessellation exceeds 65 535 vertices.

## Examples

```ts
import { plane } from "vgpu/scene";

const tiled = plane({ width: 10, height: 10, widthSegments: 4, heightSegments: 4 });
```

## Notes

* Use `plane()` when you need explicit vertex data instead of the implicit full-screen quad helper.
* **See also:** `PlaneOptions`, `fullscreenQuad`.

***

# PlaneOptions

Options bag used by `plane()`.

## Import

```ts
import type { PlaneOptions } from "vgpu/scene";
```

## Signature

```ts
interface PlaneOptions {
  readonly width?: number;
  readonly height?: number;
  readonly widthSegments?: number;
  readonly heightSegments?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field          | Type                 | Required | Default  | Notes                                   |
| -------------- | -------------------- | -------- | -------- | --------------------------------------- |
| width          | `number`             | ✖        | `1`      | Must be `> 0`.                          |
| height         | `number`             | ✖        | `1`      | Must be `> 0`.                          |
| widthSegments  | `number`             | ✖        | `1`      | Integer `>= 1`.                         |
| heightSegments | `number`             | ✖        | `1`      | Integer `>= 1`.                         |
| shading        | `"flat" \| "smooth"` | ✖        | `"flat"` | Smooth is reserved for future upgrades. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { PlaneOptions } from "vgpu/scene";

const quad: PlaneOptions = { width: 4, height: 2 };
```

## Notes

* Leave fields undefined to inherit the defaults enforced at upload time.
* **See also:** `plane`, `SceneGeometry`.

***

# torus

Donut descriptor with independent major/minor radii and optional arc slices.

## Import

```ts
import { torus } from "vgpu/scene";
```

## Signature

```ts
declare function torus(options?: import("vgpu/scene").TorusOptions): import("vgpu/scene").SceneGeometryOfKind<"torus">;
```

## Parameters

| Param                   | Type                 | Required | Default       | Notes                                                          |
| ----------------------- | -------------------- | -------- | ------------- | -------------------------------------------------------------- |
| options                 | `TorusOptions`       | ✖        | `{}`          | Controls radii, arc, and tessellation.                         |
| options.radius          | `number`             | ✖        | `0.5`         | Distance from origin to tube center; must be `> options.tube`. |
| options.tube            | `number`             | ✖        | `0.2`         | Minor radius; must be `> 0`.                                   |
| options.radialSegments  | `number`             | ✖        | `16`          | Around the tube. Integer `>= 3`.                               |
| options.tubularSegments | `number`             | ✖        | `32`          | Around the ring. Integer `>= 3`.                               |
| options.arc             | `number`             | ✖        | `Math.PI * 2` | Radians to sweep; must be `> 0`.                               |
| options.shading         | `"flat" \| "smooth"` | ✖        | `"smooth"`    | Flat duplicates vertices for per-triangle normals.             |

**Returns:** `SceneGeometryOfKind<"torus">`.

**Throws:** None while creating the descriptor. `geometry(gpu, torus(...))` throws `VGPU-CORE-INVALID-USAGE` if radii are invalid, segment counts fall below limits, `arc <= 0`, or vertex counts exceed 65 535.

## Examples

```ts
import { torus } from "vgpu/scene";

const gauge = torus({ radius: 1, tube: 0.15, arc: Math.PI * 1.25 });
```

## Notes

* Use partial arcs to build gauges or highlights without extra geometry.
* **See also:** `RingOptions`, `TorusOptions`.

***

# TorusOptions

Configuration interface for `torus()`.

## Import

```ts
import type { TorusOptions } from "vgpu/scene";
```

## Signature

```ts
interface TorusOptions {
  readonly radius?: number;
  readonly tube?: number;
  readonly radialSegments?: number;
  readonly tubularSegments?: number;
  readonly arc?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field           | Type                 | Required | Default       | Notes                     |
| --------------- | -------------------- | -------- | ------------- | ------------------------- |
| radius          | `number`             | ✖        | `0.5`         | Must be `> tube`.         |
| tube            | `number`             | ✖        | `0.2`         | Minor radius `> 0`.       |
| radialSegments  | `number`             | ✖        | `16`          | Integer `>= 3`.           |
| tubularSegments | `number`             | ✖        | `32`          | Integer `>= 3`.           |
| arc             | `number`             | ✖        | `Math.PI * 2` | Radians `> 0`.            |
| shading         | `"flat" \| "smooth"` | ✖        | `"smooth"`    | Flat duplicates vertices. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { TorusOptions } from "vgpu/scene";

const tight: TorusOptions = { radius: 0.75, tube: 0.3 };
```

## Notes

* Keep `tube` significantly smaller than `radius` to avoid self-intersection.
* **See also:** `torus`, `ring`.

***

# fullscreenQuad

Descriptor for a clip-space fullscreen quad (two triangles, six vertices). Use it for fragment-only passes recorded via `draw(gpu)`.

## Import

```ts
import { fullscreenQuad } from "vgpu/scene";
```

## Signature

```ts
declare function fullscreenQuad(options?: import("vgpu/scene").FullscreenQuadOptions): import("vgpu/scene").SceneGeometryOfKind<"fullscreenQuad">;
```

## Parameters

| Param   | Type                    | Required | Default | Notes                                                   |
| ------- | ----------------------- | -------- | ------- | ------------------------------------------------------- |
| options | `FullscreenQuadOptions` | ✖        | `{}`    | Reserved for future overrides; currently has no fields. |

**Returns:** `SceneGeometryOfKind<"fullscreenQuad">`.

**Throws:** None.

## Examples

```ts
import { fullscreenQuad } from "vgpu/scene";

const descriptor = fullscreenQuad();
```

## Notes

* Generated geometry exposes only position attributes (xy4). Add UVs in WGSL using built-in coordinates.
* **See also:** `plane`, `Geometry`.

***

# FullscreenQuadOptions

Placeholder interface to keep API surface future-proof.

## Import

```ts
import type { FullscreenQuadOptions } from "vgpu/scene";
```

## Signature

```ts
interface FullscreenQuadOptions {}
```

## Parameters

No fields yet.

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { FullscreenQuadOptions } from "vgpu/scene";

const passthrough: FullscreenQuadOptions = {};
```

## Notes

* Leave the argument undefined; future versions may add configurable fields.
* **See also:** `fullscreenQuad`.

***

# capsule

Rounded capsule descriptor made of a cylinder body and two hemispherical caps.

## Import

```ts
import { capsule } from "vgpu/scene";
```

## Signature

```ts
declare function capsule(options?: import("vgpu/scene").CapsuleOptions): import("vgpu/scene").SceneGeometryOfKind<"capsule">;
```

## Parameters

| Param                  | Type                 | Required | Default    | Notes                                                                |
| ---------------------- | -------------------- | -------- | ---------- | -------------------------------------------------------------------- |
| options                | `CapsuleOptions`     | ✖        | `{}`       | Radius/height/tessellation overrides.                                |
| options.radius         | `number`             | ✖        | `0.5`      | Cap radius; must be `> 0`.                                           |
| options.height         | `number`             | ✖        | `1`        | Cylinder length between caps; total height is `height + 2 * radius`. |
| options.radialSegments | `number`             | ✖        | `32`       | Integer `>= 3`.                                                      |
| options.heightSegments | `number`             | ✖        | `8`        | Integer `>= 2`.                                                      |
| options.shading        | `"flat" \| "smooth"` | ✖        | `"smooth"` | Flat duplicates vertices per triangle.                               |

**Returns:** `SceneGeometryOfKind<"capsule">`.

**Throws:** None while creating the descriptor. `geometry(gpu, capsule(...))` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`, height `< 0`, segment counts fall below limits, or vertex counts exceed 65 535.

## Examples

```ts
import { capsule } from "vgpu/scene";

const pillar = capsule({ radius: 0.3, height: 2, shading: "flat" });
```

## Notes

* Increase `heightSegments` to reduce stretching along the cylinder body.
* **See also:** `CapsuleOptions`, `cylinder`.

***

# CapsuleOptions

Configuration interface for `capsule()`.

## Import

```ts
import type { CapsuleOptions } from "vgpu/scene";
```

## Signature

```ts
interface CapsuleOptions {
  readonly radius?: number;
  readonly height?: number;
  readonly radialSegments?: number;
  readonly heightSegments?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field          | Type                 | Required | Default    | Notes                            |
| -------------- | -------------------- | -------- | ---------- | -------------------------------- |
| radius         | `number`             | ✖        | `0.5`      | Must be `> 0`.                   |
| height         | `number`             | ✖        | `1`        | Can be zero for perfect spheres. |
| radialSegments | `number`             | ✖        | `32`       | Integer `>= 3`.                  |
| heightSegments | `number`             | ✖        | `8`        | Integer `>= 2`.                  |
| shading        | `"flat" \| "smooth"` | ✖        | `"smooth"` | Flat duplicates vertices.        |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { CapsuleOptions } from "vgpu/scene";

const short: CapsuleOptions = { height: 0.5 };
```

## Notes

* Negative heights are rejected when uploading through `geometry(gpu)`.
* **See also:** `capsule`, `SceneGeometry`.

***

# cone

Circular cone descriptor with optional base cap and angular slice control.

## Import

```ts
import { cone } from "vgpu/scene";
```

## Signature

```ts
declare function cone(options?: import("vgpu/scene").ConeOptions): import("vgpu/scene").SceneGeometryOfKind<"cone">;
```

## Parameters

| Param                  | Type                 | Required | Default       | Notes                                  |
| ---------------------- | -------------------- | -------- | ------------- | -------------------------------------- |
| options                | `ConeOptions`        | ✖        | `{}`          | Controls size, caps, and tessellation. |
| options.radius         | `number`             | ✖        | `0.5`         | Base radius; must be `> 0`.            |
| options.height         | `number`             | ✖        | `1`           | Must be `> 0`.                         |
| options.radialSegments | `number`             | ✖        | `32`          | Integer `>= 3`.                        |
| options.heightSegments | `number`             | ✖        | `1`           | Integer `>= 1`.                        |
| options.openEnded      | `boolean`            | ✖        | `false`       | Omits the base cap.                    |
| options.thetaStart     | `number`             | ✖        | `0`           | Start angle in radians.                |
| options.thetaLength    | `number`             | ✖        | `Math.PI * 2` | Sweep `> 0`.                           |
| options.shading        | `"flat" \| "smooth"` | ✖        | `"smooth"`    | Flat duplicates vertices per face.     |

**Returns:** `SceneGeometryOfKind<"cone">`.

**Throws:** None while creating the descriptor. `geometry(gpu, cone(...))` throws `VGPU-CORE-INVALID-USAGE` for invalid radius/height, low segment counts, zero sweep, or vertex counts above 65 535.

## Examples

```ts
import { cone } from "vgpu/scene";

const spotlight = cone({ radius: 0.4, height: 1.2, openEnded: true, thetaLength: Math.PI });
```

## Notes

* Use `openEnded: true` for beams or funnels where the cap would never be visible.
* **See also:** `ConeOptions`, `cylinder`.

***

# ConeOptions

Configuration interface for `cone()`.

## Import

```ts
import type { ConeOptions } from "vgpu/scene";
```

## Signature

```ts
interface ConeOptions {
  readonly radius?: number;
  readonly height?: number;
  readonly radialSegments?: number;
  readonly heightSegments?: number;
  readonly openEnded?: boolean;
  readonly thetaStart?: number;
  readonly thetaLength?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field          | Type                 | Required | Default       | Notes                     |
| -------------- | -------------------- | -------- | ------------- | ------------------------- |
| radius         | `number`             | ✖        | `0.5`         | Must be `> 0`.            |
| height         | `number`             | ✖        | `1`           | Must be `> 0`.            |
| radialSegments | `number`             | ✖        | `32`          | Integer `>= 3`.           |
| heightSegments | `number`             | ✖        | `1`           | Integer `>= 1`.           |
| openEnded      | `boolean`            | ✖        | `false`       | Omits the base cap.       |
| thetaStart     | `number`             | ✖        | `0`           | Start angle in radians.   |
| thetaLength    | `number`             | ✖        | `Math.PI * 2` | Sweep `> 0`.              |
| shading        | `"flat" \| "smooth"` | ✖        | `"smooth"`    | Flat duplicates vertices. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { ConeOptions } from "vgpu/scene";

const halfCone: ConeOptions = { thetaLength: Math.PI };
```

## Notes

* Upload-time validation enforces every invariant listed above.
* **See also:** `cone`, `SceneGeometry`.

***

# cylinder

Pure descriptor for cylinders or frustums with independent top/bottom radii and optional caps.

## Import

```ts
import { cylinder } from "vgpu/scene";
```

## Signature

```ts
declare function cylinder(options?: import("vgpu/scene").CylinderOptions): import("vgpu/scene").SceneGeometryOfKind<"cylinder">;
```

## Parameters

| Param                  | Type                 | Required | Default          | Notes                                                           |
| ---------------------- | -------------------- | -------- | ---------------- | --------------------------------------------------------------- |
| options                | `CylinderOptions`    | ✖        | `{}`             | Provide either `radius` or both `radiusTop` and `radiusBottom`. |
| options.radius         | `number`             | ✖        | `0.5`            | Uniform radius fallback when per-end radii are omitted.         |
| options.radiusTop      | `number`             | ✖        | `options.radius` | Provide with `radiusBottom` for frustums. Must be `>= 0`.       |
| options.radiusBottom   | `number`             | ✖        | `options.radius` | Same as `radiusTop`.                                            |
| options.height         | `number`             | ✖        | `1`              | Must be `> 0`.                                                  |
| options.radialSegments | `number`             | ✖        | `32`             | Integer `>= 3`.                                                 |
| options.heightSegments | `number`             | ✖        | `1`              | Integer `>= 1`.                                                 |
| options.openEnded      | `boolean`            | ✖        | `false`          | Omits caps (zero-radius caps are always skipped).               |
| options.thetaStart     | `number`             | ✖        | `0`              | Start angle.                                                    |
| options.thetaLength    | `number`             | ✖        | `Math.PI * 2`    | Sweep `> 0`.                                                    |
| options.shading        | `"flat" \| "smooth"` | ✖        | `"smooth"`       | Flat duplicates vertices.                                       |

**Returns:** `SceneGeometryOfKind<"cylinder">`.

**Throws:** None while creating the descriptor. `geometry(gpu, cylinder(...))` throws `VGPU-CORE-INVALID-USAGE` if you provide both uniform and explicit radii, resolve radii below zero, keep both radii at zero, use low segment counts, or exceed the vertex limit.

## Examples

```ts
import { cylinder } from "vgpu/scene";

const tapered = cylinder({ radiusTop: 0.2, radiusBottom: 0.4, height: 1.5, radialSegments: 48 });
```

## Notes

* Zero-radius ends are automatically omitted to avoid degenerate caps.
* **See also:** `CylinderOptions`, `capsule`.

***

# CylinderOptions

Configuration interface for `cylinder()`.

## Import

```ts
import type { CylinderOptions } from "vgpu/scene";
```

## Signature

```ts
interface CylinderOptions {
  readonly radius?: number;
  readonly radiusTop?: number;
  readonly radiusBottom?: number;
  readonly height?: number;
  readonly radialSegments?: number;
  readonly heightSegments?: number;
  readonly openEnded?: boolean;
  readonly thetaStart?: number;
  readonly thetaLength?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field          | Type                 | Required | Default                         | Notes                                                                                      |
| -------------- | -------------------- | -------- | ------------------------------- | ------------------------------------------------------------------------------------------ |
| radius         | `number`             | ✖        | `0.5`                           | Uniform fallback radius.                                                                   |
| radiusTop      | `number`             | ✖        | omitted; resolved from `radius` | Provide with `radiusBottom` for frustums. If `radius` is set, omitting this uses `radius`. |
| radiusBottom   | `number`             | ✖        | omitted; resolved from `radius` | Provide with `radiusTop`. If `radius` is set, omitting this uses `radius`.                 |
| height         | `number`             | ✖        | `1`                             | Must be `> 0`.                                                                             |
| radialSegments | `number`             | ✖        | `32`                            | Integer `>= 3`.                                                                            |
| heightSegments | `number`             | ✖        | `1`                             | Integer `>= 1`.                                                                            |
| openEnded      | `boolean`            | ✖        | `false`                         | Skip caps when `true`.                                                                     |
| thetaStart     | `number`             | ✖        | `0`                             | Start angle.                                                                               |
| thetaLength    | `number`             | ✖        | `Math.PI * 2`                   | Sweep `> 0`.                                                                               |
| shading        | `"flat" \| "smooth"` | ✖        | `"smooth"`                      | Flat duplicates vertices.                                                                  |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { CylinderOptions } from "vgpu/scene";

const frustum: CylinderOptions = { radiusTop: 0.25, radiusBottom: 0.6 };
```

## Notes

* Validation occurs when `geometry(gpu)` uploads the descriptor.
* **See also:** `cylinder`, `SceneGeometry`.

***

# disk

Flat disk descriptor oriented on the XZ plane with upward normals.

## Import

```ts
import { disk } from "vgpu/scene";
```

## Signature

```ts
declare function disk(options?: import("vgpu/scene").DiskOptions): import("vgpu/scene").SceneGeometryOfKind<"disk">;
```

## Parameters

| Param               | Type          | Required | Default       | Notes                             |
| ------------------- | ------------- | -------- | ------------- | --------------------------------- |
| options             | `DiskOptions` | ✖        | `{}`          | Radius and polar slice overrides. |
| options.radius      | `number`      | ✖        | `0.5`         | Must be `> 0`.                    |
| options.segments    | `number`      | ✖        | `32`          | Integer `>= 3`.                   |
| options.thetaStart  | `number`      | ✖        | `0`           | Slice start angle.                |
| options.thetaLength | `number`      | ✖        | `Math.PI * 2` | Sweep `> 0`.                      |

**Returns:** `SceneGeometryOfKind<"disk">`.

**Throws:** None while creating the descriptor. `geometry(gpu, disk(...))` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`, segment counts `< 3`, sweep `<= 0`, or vertex counts exceed the uint16 limit.

## Examples

```ts
import { disk } from "vgpu/scene";

const portal = disk({ radius: 1, segments: 64, thetaLength: Math.PI * 1.5 });
```

## Notes

* Disk UVs map linearly from the center; combine with `ring()` for hollow shapes.
* **See also:** `DiskOptions`, `ring`.

***

# DiskOptions

Configuration interface for `disk()`.

## Import

```ts
import type { DiskOptions } from "vgpu/scene";
```

## Signature

```ts
interface DiskOptions {
  readonly radius?: number;
  readonly segments?: number;
  readonly thetaStart?: number;
  readonly thetaLength?: number;
}
```

## Parameters

| Field       | Type     | Required | Default       | Notes              |
| ----------- | -------- | -------- | ------------- | ------------------ |
| radius      | `number` | ✖        | `0.5`         | Must be `> 0`.     |
| segments    | `number` | ✖        | `32`          | Integer `>= 3`.    |
| thetaStart  | `number` | ✖        | `0`           | Slice start angle. |
| thetaLength | `number` | ✖        | `Math.PI * 2` | Sweep `> 0`.       |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { DiskOptions } from "vgpu/scene";

const slice: DiskOptions = { thetaLength: Math.PI };
```

## Notes

* Validation occurs when uploading through `geometry(gpu)`.
* **See also:** `disk`, `ring`.

***

# dodecahedron

Regular dodecahedron descriptor backed by the polyhedron geometry builder.

## Import

```ts
import { dodecahedron } from "vgpu/scene";
```

## Signature

```ts
declare function dodecahedron(options?: import("vgpu/scene").PolyhedronOptions): import("vgpu/scene").SceneGeometryOfKind<"dodecahedron">;
```

## Parameters

| Param          | Type                | Required | Default | Notes                                |
| -------------- | ------------------- | -------- | ------- | ------------------------------------ |
| options        | `PolyhedronOptions` | ✖        | `{}`    | Provide a radius override.           |
| options.radius | `number`            | ✖        | `0.5`   | Circumscribed radius; must be `> 0`. |

**Returns:** `SceneGeometryOfKind<"dodecahedron">`.

**Throws:** None while creating the descriptor. Uploading this descriptor with `geometry(gpu, ...)` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`.

## Examples

```ts
import { dodecahedron } from "vgpu/scene";

const rock = dodecahedron({ radius: 0.8 });
```

## Notes

* Polyhedra stay well under the uint16 vertex cap, making them ideal for stylized rocks.
* **See also:** `PolyhedronOptions`, `icosahedron`.

***

# PolyhedronOptions

Shared options for `dodecahedron`, `icosahedron`, `octahedron`, and `tetrahedron`.

## Import

```ts
import type { PolyhedronOptions } from "vgpu/scene";
```

## Signature

```ts
interface PolyhedronOptions {
  readonly radius?: number;
}
```

## Parameters

| Field  | Type     | Required | Default | Notes                                |
| ------ | -------- | -------- | ------- | ------------------------------------ |
| radius | `number` | ✖        | `0.5`   | Circumscribed radius; must be `> 0`. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { PolyhedronOptions } from "vgpu/scene";

const hudIcon: PolyhedronOptions = { radius: 0.25 };
```

## Notes

* All polyhedron helpers freeze the provided options before returning descriptors.
* **See also:** `dodecahedron`, `tetrahedron`.

***

# icosahedron

Regular 20-faced polyhedron descriptor.

## Import

```ts
import { icosahedron } from "vgpu/scene";
```

## Signature

```ts
declare function icosahedron(options?: import("vgpu/scene").PolyhedronOptions): import("vgpu/scene").SceneGeometryOfKind<"icosahedron">;
```

## Parameters

Same as `dodecahedron`; omit or override `radius`.

**Returns:** `SceneGeometryOfKind<"icosahedron">`.

**Throws:** None while creating the descriptor. Uploading this descriptor with `geometry(gpu, ...)` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`.

## Examples

```ts
import { icosahedron } from "vgpu/scene";

const crystal = icosahedron({ radius: 0.6 });
```

## Notes

* Acts as the seed geometry for `icosphere()` subdivisions.
* **See also:** `PolyhedronOptions`, `icosphere`.

***

# icosphere

Geodesic sphere descriptor obtained by subdividing an icosahedron.

## Import

```ts
import { icosphere } from "vgpu/scene";
```

## Signature

```ts
declare function icosphere(options?: import("vgpu/scene").IcosphereOptions): import("vgpu/scene").SceneGeometryOfKind<"icosphere">;
```

## Parameters

| Param                | Type                 | Required | Default    | Notes                                                     |
| -------------------- | -------------------- | -------- | ---------- | --------------------------------------------------------- |
| options              | `IcosphereOptions`   | ✖        | `{}`       | Radius and subdivision overrides.                         |
| options.radius       | `number`             | ✖        | `0.5`      | Must be `> 0`.                                            |
| options.subdivisions | `number`             | ✖        | `2`        | Integer in `[0, 6]`; each step quadruples triangle count. |
| options.shading      | `"flat" \| "smooth"` | ✖        | `"smooth"` | Flat duplicates vertices for per-face normals.            |

**Returns:** `SceneGeometryOfKind<"icosphere">`.

**Throws:** None while creating the descriptor. `geometry(gpu, icosphere(...))` throws `VGPU-CORE-INVALID-USAGE` if radius/subdivision constraints are violated or vertex counts exceed 65 535.

## Examples

```ts
import { icosphere } from "vgpu/scene";

const moon = icosphere({ radius: 1, subdivisions: 4, shading: "flat" });
```

## Notes

* Prefer `icosphere()` over `sphere()` when you need evenly distributed triangles.
* **See also:** `IcosphereOptions`, `sphere`.

***

# IcosphereOptions

Configuration interface for `icosphere()`.

## Import

```ts
import type { IcosphereOptions } from "vgpu/scene";
```

## Signature

```ts
interface IcosphereOptions {
  readonly radius?: number;
  readonly subdivisions?: number;
  readonly shading?: "flat" | "smooth";
}
```

## Parameters

| Field        | Type                 | Required | Default    | Notes                     |
| ------------ | -------------------- | -------- | ---------- | ------------------------- |
| radius       | `number`             | ✖        | `0.5`      | Must be `> 0`.            |
| subdivisions | `number`             | ✖        | `2`        | Integer in `[0, 6]`.      |
| shading      | `"flat" \| "smooth"` | ✖        | `"smooth"` | Flat duplicates vertices. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { IcosphereOptions } from "vgpu/scene";

const lowPoly: IcosphereOptions = { subdivisions: 1 };
```

## Notes

* Higher subdivision counts grow vertex counts exponentially; stay at `<= 4` for most hardware.
* **See also:** `icosphere`, `SphereOptions`.

***

# octahedron

Regular octahedron descriptor.

## Import

```ts
import { octahedron } from "vgpu/scene";
```

## Signature

```ts
declare function octahedron(options?: import("vgpu/scene").PolyhedronOptions): import("vgpu/scene").SceneGeometryOfKind<"octahedron">;
```

## Parameters

Same as `dodecahedron`.

**Returns:** `SceneGeometryOfKind<"octahedron">`.

**Throws:** None while creating the descriptor. Uploading this descriptor with `geometry(gpu, ...)` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`.

## Examples

```ts
import { octahedron } from "vgpu/scene";

const diamond = octahedron({ radius: 0.7 });
```

## Notes

* Octahedra are useful for axis gizmos or debug visuals.
* **See also:** `PolyhedronOptions`, `tetrahedron`.

***

# ring

Annulus descriptor with independent inner/outer radii and optional polar slices.

## Import

```ts
import { ring } from "vgpu/scene";
```

## Signature

```ts
declare function ring(options?: import("vgpu/scene").RingOptions): import("vgpu/scene").SceneGeometryOfKind<"ring">;
```

## Parameters

| Param               | Type          | Required | Default       | Notes                      |
| ------------------- | ------------- | -------- | ------------- | -------------------------- |
| options             | `RingOptions` | ✖        | `{}`          | Controls radii and slices. |
| options.innerRadius | `number`      | ✖        | `0.25`        | Must be `> 0`.             |
| options.outerRadius | `number`      | ✖        | `0.5`         | Must be `> innerRadius`.   |
| options.segments    | `number`      | ✖        | `32`          | Integer `>= 3`.            |
| options.thetaStart  | `number`      | ✖        | `0`           | Slice start.               |
| options.thetaLength | `number`      | ✖        | `Math.PI * 2` | Sweep `> 0`.               |

**Returns:** `SceneGeometryOfKind<"ring">`.

**Throws:** None while creating the descriptor. `geometry(gpu, ring(...))` throws `VGPU-CORE-INVALID-USAGE` if the radii relationship is invalid, segment counts `< 3`, or vertex counts exceed 65 535.

## Examples

```ts
import { ring } from "vgpu/scene";

const halo = ring({ innerRadius: 0.9, outerRadius: 1, segments: 48 });
```

## Notes

* Combine `ring()` with emissive materials for portals or HUD highlights.
* **See also:** `RingOptions`, `disk`.

***

# RingOptions

Configuration interface for `ring()`.

## Import

```ts
import type { RingOptions } from "vgpu/scene";
```

## Signature

```ts
interface RingOptions {
  readonly innerRadius?: number;
  readonly outerRadius?: number;
  readonly segments?: number;
  readonly thetaStart?: number;
  readonly thetaLength?: number;
}
```

## Parameters

| Field       | Type     | Required | Default       | Notes                    |
| ----------- | -------- | -------- | ------------- | ------------------------ |
| innerRadius | `number` | ✖        | `0.25`        | Must be `> 0`.           |
| outerRadius | `number` | ✖        | `0.5`         | Must be `> innerRadius`. |
| segments    | `number` | ✖        | `32`          | Integer `>= 3`.          |
| thetaStart  | `number` | ✖        | `0`           | Slice start.             |
| thetaLength | `number` | ✖        | `Math.PI * 2` | Sweep `> 0`.             |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { RingOptions } from "vgpu/scene";

const arc: RingOptions = { thetaLength: Math.PI * 0.75 };
```

## Notes

* Upload-time validation enforces every constraint listed above.
* **See also:** `ring`, `SceneGeometry`.

***

# tetrahedron

Regular tetrahedron descriptor.

## Import

```ts
import { tetrahedron } from "vgpu/scene";
```

## Signature

```ts
declare function tetrahedron(options?: import("vgpu/scene").PolyhedronOptions): import("vgpu/scene").SceneGeometryOfKind<"tetrahedron">;
```

## Parameters

Same as `dodecahedron`.

**Returns:** `SceneGeometryOfKind<"tetrahedron">`.

**Throws:** None while creating the descriptor. Uploading this descriptor with `geometry(gpu, ...)` throws `VGPU-CORE-INVALID-USAGE` if radius `<= 0`.

## Examples

```ts
import { tetrahedron } from "vgpu/scene";

const marker = tetrahedron({ radius: 0.3 });
```

## Notes

* Tetrahedra are handy for directional HUD markers or gizmos.
* **See also:** `PolyhedronOptions`, `GeometryKind`.

***

# geometries

Frozen namespace exposing every primitive helper through properties, useful for UI pickers or serialization.

## Import

```ts
import { geometries } from "vgpu/scene";
```

## Signature

```ts
declare const geometries: {
  readonly box: typeof import("vgpu/scene")["box"];
  readonly capsule: typeof import("vgpu/scene")["capsule"];
  readonly cone: typeof import("vgpu/scene")["cone"];
  readonly cylinder: typeof import("vgpu/scene")["cylinder"];
  readonly disk: typeof import("vgpu/scene")["disk"];
  readonly dodecahedron: typeof import("vgpu/scene")["dodecahedron"];
  readonly fullscreenQuad: typeof import("vgpu/scene")["fullscreenQuad"];
  readonly icosahedron: typeof import("vgpu/scene")["icosahedron"];
  readonly icosphere: typeof import("vgpu/scene")["icosphere"];
  readonly octahedron: typeof import("vgpu/scene")["octahedron"];
  readonly plane: typeof import("vgpu/scene")["plane"];
  readonly ring: typeof import("vgpu/scene")["ring"];
  readonly sphere: typeof import("vgpu/scene")["sphere"];
  readonly tetrahedron: typeof import("vgpu/scene")["tetrahedron"];
  readonly torus: typeof import("vgpu/scene")["torus"];
};
```

## Parameters

| Property            | Type       | Required | Default | Notes                                        |
| ------------------- | ---------- | -------- | ------- | -------------------------------------------- |
| `geometries.<name>` | `Function` | ✔        | —       | Each property mirrors the standalone export. |

**Returns:** Frozen object with the same helpers you can import individually.

**Throws:** None.

## Examples

```ts
import { geometries } from "vgpu/scene";

const primitiveNames = Object.keys(geometries);
```

## Notes

* Great for inspector tooling that needs to enumerate available primitives without hard-coding imports.
* **See also:** `GeometryKind`, `SceneGeometry`.

***

# GeometryKind

String-literal union covering every built-in geometry kind.

## Import

```ts
import type { GeometryKind } from "vgpu/scene";
```

## Signature

```ts
type GeometryKind =
  | "box"
  | "capsule"
  | "cone"
  | "cylinder"
  | "disk"
  | "dodecahedron"
  | "fullscreenQuad"
  | "icosahedron"
  | "icosphere"
  | "octahedron"
  | "plane"
  | "ring"
  | "sphere"
  | "tetrahedron"
  | "torus";
```

## Parameters

| Value           | Type           | Required | Default | Notes                                     |
| --------------- | -------------- | -------- | ------- | ----------------------------------------- |
| listed literals | `GeometryKind` | ✔        | —       | Each literal matches a descriptor `kind`. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import type { GeometryKind } from "vgpu/scene";

function isPolyhedron(kind: GeometryKind): boolean {
  return ["dodecahedron", "icosahedron", "octahedron", "tetrahedron"].includes(kind);
}
```

## Notes

* Use `GeometryKind` when switching on `geometry.kind` to retain exhaustiveness checking.
* **See also:** `SceneGeometry`, `SceneGeometryOfKind`.

***

# SceneGeometry

Union of every primitive descriptor returned by the geometry helpers.

## Import

```ts
import type { SceneGeometry } from "vgpu/scene";
```

## Signature

```ts
type SceneGeometry = import("vgpu/scene").SceneGeometry;
```

## Parameters

| Field | Type            | Required | Default | Notes                                                                                                                           |
| ----- | --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| kind  | `GeometryKind`  | ✔        | —       | Identifies the primitive helper that produced the descriptor.                                                                   |
| props | `Readonly<...>` | ✔        | —       | Captured options exactly as provided and frozen; upload-time geometry factories apply the defaults listed on each option table. |

**Returns:** Not applicable (type definition).

**Throws:** None.

## Examples

```ts
import { box } from "vgpu/scene";
import type { SceneGeometry } from "vgpu/scene";

const primitives: SceneGeometry[] = [box({ size: 2 })];
```

## Notes

* `SceneGeometry` objects are fully serializable, making them ideal for editor save files.
* **See also:** `SceneGeometryOfKind`, `Geometry`.

***

# SceneGeometryOfKind

Conditional helper that narrows a `SceneGeometry` union to a specific primitive.

## Import

```ts
import type { SceneGeometryOfKind } from "vgpu/scene";
```

## Signature

```ts
type SceneGeometryOfKind<K extends import("vgpu/scene").GeometryKind> = Extract<import("vgpu/scene").SceneGeometry, { readonly kind: K }>;
```

## Parameters

| Param | Type           | Required | Default | Notes                                |
| ----- | -------------- | -------- | ------- | ------------------------------------ |
| K     | `GeometryKind` | ✔        | —       | Primitive to extract from the union. |

**Returns:** Not applicable.

**Throws:** None.

## Examples

```ts
import { sphere } from "vgpu/scene";
import type { SceneGeometryOfKind } from "vgpu/scene";

const glossy: SceneGeometryOfKind<"sphere"> = sphere({ radius: 1 });
```

## Notes

* Use this helper when writing utilities that only accept a single primitive while keeping props strongly typed.
* **See also:** `SceneGeometry`, `GeometryKind`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: directionalLight
description: Creates a directional (sun-style) light node. Direction is explicit, not transform-derived.
---

# directionalLight



## Import

```ts
import { directionalLight } from "vgpu/scene";
```

## Signature

```ts
declare function directionalLight(options?: import("vgpu/scene").DirectionalLightOptions): import("vgpu/scene").DirectionalLight;
```

## Parameters

| Param             | Type       | Required | Default      | Notes                                           |
| ----------------- | ---------- | -------- | ------------ | ----------------------------------------------- |
| options.direction | `Vec3Like` | ✖        | `[0, -1, 0]` | World-space direction the light travels toward. |
| options.color     | `Vec3Like` | ✖        | `[1, 1, 1]`  | Linear RGB.                                     |
| options.intensity | `number`   | ✖        | `1`          | Must be `>= 0`.                                 |

**Returns:** `DirectionalLight` node with `kind: "directional-light"`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` for malformed vectors or negative intensity.

## Examples

```ts
import { directionalLight, scene } from "vgpu/scene";

const sun = directionalLight({ direction: [-1, -2, -1], intensity: 1.2 });
scene().add(sun);
sun.set({ intensity: 0.8 });
```

## Notes

* Lit materials (`lambertMaterial`, custom lit shaders) read lights collected from the tree by the renderer.
* **See also:** `DirectionalLight`, `ambientLight`, `lambertMaterial`.

***

# DirectionalLight

Class returned by `directionalLight()`. Extends `SceneNode`.

## Import

```ts
import type { DirectionalLight } from "vgpu/scene";
```

## Signature

```ts
declare class DirectionalLight {
  set(values: import("vgpu/scene").DirectionalLightValues): this;
  readonly direction: Float32Array;
  readonly color: Float32Array;
  readonly intensity: number;
}
```

## Examples

```ts
import { directionalLight } from "vgpu/scene";

directionalLight().set({ direction: [1, -1, 0], color: [1, 0.9, 0.8] });
```

## Notes

* `direction` and `color` keep stable array identities; mutate via `set()`.
* **See also:** `directionalLight`, `DirectionalLightValues`.

***

# DirectionalLightOptions

Options accepted by `directionalLight()`: light parameters plus `NodeOptions`.

## Import

```ts
import type { DirectionalLightOptions } from "vgpu/scene";
```

## Signature

```ts
interface DirectionalLightOptions {
  readonly direction?: import("vgpu/scene").Vec3Like;
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly intensity?: number;
  readonly label?: string;
  readonly children?: readonly import("vgpu/scene").SceneNode[];
}
```

## Examples

```ts
import { directionalLight } from "vgpu/scene";

directionalLight({ direction: [0, -1, -1], intensity: 2, label: "key" });
```

## Notes

* **See also:** `directionalLight`, `NodeOptions`.

***

# DirectionalLightValues

Values accepted by `DirectionalLight.set()`.

## Import

```ts
import type { DirectionalLightValues } from "vgpu/scene";
```

## Signature

```ts
interface DirectionalLightValues {
  readonly direction?: import("vgpu/scene").Vec3Like;
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly intensity?: number;
}
```

## Examples

```ts
import { directionalLight } from "vgpu/scene";

directionalLight().set({ intensity: 0.5 });
```

## Notes

* **See also:** `DirectionalLight`, `NodeTransformValues`.

***

# ambientLight

Creates an ambient fill light node applied uniformly to lit materials.

## Import

```ts
import { ambientLight } from "vgpu/scene";
```

## Signature

```ts
declare function ambientLight(options?: import("vgpu/scene").AmbientLightOptions): import("vgpu/scene").AmbientLight;
```

## Examples

```ts
import { ambientLight, scene } from "vgpu/scene";

scene().add(ambientLight({ color: [0.4, 0.45, 0.6], intensity: 0.3 }));
```

## Notes

* Combine with `directionalLight()` to soften shadows on lambert-shaded meshes.
* **See also:** `AmbientLight`, `directionalLight`.

***

# AmbientLight

Class returned by `ambientLight()`. Extends `SceneNode`.

## Import

```ts
import type { AmbientLight } from "vgpu/scene";
```

## Signature

```ts
declare class AmbientLight {
  set(values: import("vgpu/scene").AmbientLightValues): this;
  readonly color: Float32Array;
  readonly intensity: number;
}
```

## Examples

```ts
import { ambientLight } from "vgpu/scene";

ambientLight().set({ intensity: 0.2 });
```

## Notes

* **See also:** `ambientLight`, `AmbientLightValues`.

***

# AmbientLightOptions

Options accepted by `ambientLight()`.

## Import

```ts
import type { AmbientLightOptions } from "vgpu/scene";
```

## Signature

```ts
interface AmbientLightOptions {
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly intensity?: number;
  readonly label?: string;
}
```

## Examples

```ts
import { ambientLight } from "vgpu/scene";

ambientLight({ intensity: 0.25 });
```

## Notes

* **See also:** `ambientLight`, `NodeOptions`.

***

# AmbientLightValues

Values accepted by `AmbientLight.set()`.

## Import

```ts
import type { AmbientLightValues } from "vgpu/scene";
```

## Signature

```ts
interface AmbientLightValues {
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly intensity?: number;
}
```

## Examples

```ts
import { ambientLight } from "vgpu/scene";

ambientLight().set({ color: [1, 1, 1] });
```

## Notes

* **See also:** `AmbientLight`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: unlitMaterial
description: Creates a flat-color material descriptor; renders without lights.
---

# unlitMaterial



## Import

```ts
import { unlitMaterial } from "vgpu/scene";
```

## Signature

```ts
declare function unlitMaterial(options?: import("vgpu/scene").ColorMaterialOptions): import("vgpu/scene").UnlitMaterial;
```

## Examples

```ts
import { srgb, unlitMaterial } from "vgpu/scene";

const material = unlitMaterial({ color: srgb("#3b82f6") });
material.set({ opacity: 0.5 });
```

## Notes

* Materials are pure descriptors — no GPU resources; pipelines compile when a tree is bound with the scene renderer.
* **See also:** `lambertMaterial`, `normalMaterial`, `shaderMaterial`, `ColorMaterialOptions`.

***

# lambertMaterial

Creates an N·L diffuse material descriptor lit by scene lights (uses `@vgpu/wgsl-std/light` lambert).

## Import

```ts
import { lambertMaterial } from "vgpu/scene";
```

## Signature

```ts
declare function lambertMaterial(options?: import("vgpu/scene").ColorMaterialOptions): import("vgpu/scene").LambertMaterial;
```

## Examples

```ts
import { directionalLight, lambertMaterial, mesh, scene, sphere } from "vgpu/scene";

const root = scene({
  children: [
    mesh(sphere({ radius: 0.5 }), lambertMaterial({ color: [0.4, 0.6, 1] })),
    directionalLight({ direction: [-1, -2, -1], intensity: 1.2 }),
  ],
});
void root;
```

## Notes

* Pair with `directionalLight()` / `ambientLight()` nodes in the tree.
* **See also:** `unlitMaterial`, `directionalLight`, `ambientLight`.

***

# normalMaterial

Creates a debug material that shades world-space normals; needs no lights or parameters. It is the default material of `mesh()`.

## Import

```ts
import { normalMaterial } from "vgpu/scene";
```

## Signature

```ts
declare function normalMaterial(): import("vgpu/scene").NormalMaterial;
```

## Examples

```ts
import { mesh, normalMaterial, torus } from "vgpu/scene";

const donut = mesh(torus(), normalMaterial());
void donut;
```

## Notes

* **See also:** `mesh`, `unlitMaterial`.

***

# shaderMaterial

Creates a custom material from a WGSL fragment stage compiled against the scene renderer's vertex contract. Scene globals live in `@group(0)` (renderer-owned); material bindings start at `@group(1)`.

## Import

```ts
import { shaderMaterial } from "vgpu/scene";
```

## Signature

```ts
declare function shaderMaterial(
  source: string,
  options?: import("vgpu/scene").ShaderMaterialOptions,
): import("vgpu/scene").ShaderMaterial;
```

## Parameters

| Param         | Type                      | Required | Default | Notes                                                                  |
| ------------- | ------------------------- | -------- | ------- | ---------------------------------------------------------------------- |
| source        | `string`                  | ✔        | —       | WGSL fragment stage. May import from `@vgpu/wgsl-std/*`.               |
| options.set   | `Record<string, unknown>` | ✖        | `{}`    | Initial binding values keyed by WGSL variable name, like `draw.set()`. |
| options.blend | `MaterialBlend`           | ✖        | —       | `"alpha"`, `"additive"`, or `"premultiplied"`.                         |
| options.label | `string`                  | ✖        | —       | Used in error `where` strings.                                         |

**Returns:** `ShaderMaterial` descriptor with `source`, `values`, and `set()`.
**Throws:** None at construction; WGSL errors surface when the renderer compiles the material.

## Examples

```ts
import { shaderMaterial } from "vgpu/scene";

const glow = shaderMaterial(GLOW_WGSL, { set: { params: { color: [0.2, 0.5, 1], intensity: 2 } } });
glow.set({ params: { intensity: 4 } }); // merges per binding, like draw.set()

declare const GLOW_WGSL: string;
```

## Notes

* The renderer owns the vertex stage and `@group(0)` by default; write fragment-only WGSL with params in `@group(1)`+.
* **See also:** `ShaderMaterialOptions`, `ShaderMaterial`, `unlitMaterial`.

***

# SceneMaterial

Abstract base of every material descriptor. Discriminate concrete materials by `kind`.

## Import

```ts
import { SceneMaterial } from "vgpu/scene";
```

## Signature

```ts
declare abstract class SceneMaterial {
  abstract readonly kind: import("vgpu/scene").SceneMaterialKind;
  label: string | undefined;
  blend: import("vgpu/scene").MaterialBlend | undefined;
}
```

## Examples

```ts
import { UnlitMaterial, unlitMaterial, type SceneMaterial } from "vgpu/scene";

const material: SceneMaterial = unlitMaterial();
if (material instanceof UnlitMaterial) void material.color;
```

## Notes

* **See also:** `SceneMaterialKind`, `unlitMaterial`, `shaderMaterial`.

***

# UnlitMaterial

Class returned by `unlitMaterial()`. `kind: "unlit"` with `color`, `opacity`, and `set()`.

## Import

```ts
import type { UnlitMaterial } from "vgpu/scene";
```

## Signature

```ts
declare class UnlitMaterial {
  readonly kind: "unlit";
  set(values: import("vgpu/scene").ColorMaterialValues): this;
  readonly color: Float32Array;
  readonly opacity: number;
}
```

## Examples

```ts
import { unlitMaterial } from "vgpu/scene";

unlitMaterial().set({ color: [1, 0, 0] });
```

## Notes

* `color` keeps a stable array identity; mutate via `set()`.
* **See also:** `unlitMaterial`, `ColorMaterialValues`.

***

# LambertMaterial

Class returned by `lambertMaterial()`. `kind: "lambert"` with `color`, `opacity`, and `set()`.

## Import

```ts
import type { LambertMaterial } from "vgpu/scene";
```

## Signature

```ts
declare class LambertMaterial {
  readonly kind: "lambert";
  set(values: import("vgpu/scene").ColorMaterialValues): this;
  readonly color: Float32Array;
  readonly opacity: number;
}
```

## Examples

```ts
import { lambertMaterial } from "vgpu/scene";

lambertMaterial({ color: [0.4, 0.6, 1] }).set({ opacity: 0.8 });
```

## Notes

* **See also:** `lambertMaterial`, `ColorMaterialValues`.

***

# NormalMaterial

Class returned by `normalMaterial()`. `kind: "normal"`, no parameters.

## Import

```ts
import type { NormalMaterial } from "vgpu/scene";
```

## Signature

```ts
declare class NormalMaterial {
  readonly kind: "normal";
}
```

## Examples

```ts
import { normalMaterial, type NormalMaterial } from "vgpu/scene";

const material: NormalMaterial = normalMaterial();
void material;
```

## Notes

* **See also:** `normalMaterial`.

***

# ShaderMaterial

Class returned by `shaderMaterial()`. Holds the WGSL `source` plus binding `values` merged by `set()`.

## Import

```ts
import type { ShaderMaterial } from "vgpu/scene";
```

## Signature

```ts
declare class ShaderMaterial {
  readonly kind: "shader";
  readonly source: string;
  readonly values: Readonly<Record<string, unknown>>;
  set(values: Record<string, unknown>): this;
}
```

## Examples

```ts
import { shaderMaterial } from "vgpu/scene";

const material = shaderMaterial("@fragment fn fs_main() {}", { set: { params: { t: 0 } } });
material.set({ params: { t: 1 } });
```

## Notes

* `set()` merges plain-object values per top-level key and replaces everything else, mirroring `draw.set()` semantics.
* **See also:** `shaderMaterial`, `ShaderMaterialOptions`.

***

# ColorMaterialOptions

Options shared by `unlitMaterial()` and `lambertMaterial()`.

## Import

```ts
import type { ColorMaterialOptions } from "vgpu/scene";
```

## Signature

```ts
interface ColorMaterialOptions {
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly opacity?: number;
  readonly blend?: import("vgpu/scene").MaterialBlend;
  readonly label?: string;
}
```

## Parameters

| Field   | Type            | Required | Default     | Notes                                                    |
| ------- | --------------- | -------- | ----------- | -------------------------------------------------------- |
| color   | `Vec3Like`      | ✖        | `[1, 1, 1]` | Linear RGB; pass `srgb("#…")` for hex input.             |
| opacity | `number`        | ✖        | `1`         | In `[0, 1]`. Values below 1 need a `blend` mode to show. |
| blend   | `MaterialBlend` | ✖        | —           | Blend preset shared with `DrawOptions.blend`.            |
| label   | `string`        | ✖        | —           | Used in error `where` strings.                           |

## Examples

```ts
import { srgb, unlitMaterial } from "vgpu/scene";

unlitMaterial({ color: srgb("#ff8800"), opacity: 0.75, blend: "alpha" });
```

## Notes

* **See also:** `ColorMaterialValues`, `unlitMaterial`, `lambertMaterial`.

***

# ColorMaterialValues

Values accepted by `set()` on color materials.

## Import

```ts
import type { ColorMaterialValues } from "vgpu/scene";
```

## Signature

```ts
interface ColorMaterialValues {
  readonly color?: import("vgpu/scene").Vec3Like;
  readonly opacity?: number;
}
```

## Examples

```ts
import { unlitMaterial } from "vgpu/scene";

unlitMaterial().set({ color: [0, 1, 0], opacity: 0.9 });
```

## Notes

* **See also:** `ColorMaterialOptions`.

***

# MaterialBlend

Blend preset accepted by materials; the same presets as `DrawOptions.blend`.

## Import

```ts
import type { MaterialBlend } from "vgpu/scene";
```

## Signature

```ts
type MaterialBlend = "alpha" | "additive" | "premultiplied";
```

## Examples

```ts
import { unlitMaterial, type MaterialBlend } from "vgpu/scene";

const blend: MaterialBlend = "additive";
unlitMaterial({ blend });
```

## Notes

* Materials with a blend mode render after opaque ones.
* **See also:** `ColorMaterialOptions`, `ShaderMaterialOptions`.

***

# SceneMaterialKind

Discriminator for material descriptors.

## Import

```ts
import type { SceneMaterialKind } from "vgpu/scene";
```

## Signature

```ts
type SceneMaterialKind = "unlit" | "normal" | "lambert" | "shader";
```

## Examples

```ts
import { normalMaterial, type SceneMaterialKind } from "vgpu/scene";

const kind: SceneMaterialKind = normalMaterial().kind;
void kind;
```

## Notes

* **See also:** `SceneMaterial`.

***

# ShaderMaterialOptions

Options accepted by `shaderMaterial()`.

## Import

```ts
import type { ShaderMaterialOptions } from "vgpu/scene";
```

## Signature

```ts
interface ShaderMaterialOptions {
  readonly set?: Record<string, unknown>;
  readonly blend?: import("vgpu/scene").MaterialBlend;
  readonly label?: string;
}
```

## Examples

```ts
import { shaderMaterial } from "vgpu/scene";

shaderMaterial("@fragment fn fs_main() {}", { label: "glow", blend: "additive" });
```

## Notes

* **See also:** `shaderMaterial`, `ShaderMaterial`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: orbitControls
description: Creates shared drag-orbit + wheel-zoom controls that drive a camera (or any node) around a target point. Input adjusts goal values; `update(deltaTime)` eases the current state toward them and writes the node's transform, so it composes with `frameLoop()` and on-demand rendering alike.
---

# orbitControls



## Import

```ts
import { orbitControls, type OrbitControlsOptions } from "vgpu/scene";
```

## Signature

```ts
declare function orbitControls(
  node: import("vgpu/scene").SceneNode,
  options?: import("vgpu/scene").OrbitControlsOptions,
): import("vgpu/scene").OrbitControls;
```

## Parameters

| Param               | Type                   | Required | Default       | Notes                                                                               |
| ------------------- | ---------------------- | -------- | ------------- | ----------------------------------------------------------------------------------- |
| node                | `SceneNode`            | ✔        | —             | Usually a camera; any node with `set()`/`lookAt()` works.                           |
| options.element     | `OrbitControlsElement` | ✖        | —             | Pointer/wheel source (usually the canvas). Omit for programmatic-only control.      |
| options.target      | `Vec3Like`             | ✖        | `[0, 0, 0]`   | World-space orbit/look-at point.                                                    |
| options.damping     | `number`               | ✖        | `0.1`         | Easing time constant in seconds (\~63% convergence). `0` applies input immediately. |
| options.rotateSpeed | `number`               | ✖        | `0.005`       | Drag sensitivity in radians per pixel.                                              |
| options.zoomSpeed   | `number`               | ✖        | `1`           | Wheel zoom sensitivity multiplier.                                                  |
| options.distance    | `{ min?, max? }`       | ✖        | —             | Zoom clamp range.                                                                   |
| options.pitch       | `{ min?, max? }`       | ✖        | ±(π/2 − 0.01) | Pitch limits in radians; defaults keep the camera off the poles.                    |

**Returns:** `OrbitControls` with `update()`, `set()`, `dispose()`, and `yaw`/`pitch`/`distance`/`target` state.
**Throws:** `VGPU-SCENE-VALUE-INVALID` for malformed `target` vectors.

## Examples

```ts
import { clock, init, frameLoop } from "vgpu";
import { orbitControls, perspectiveCamera } from "vgpu/scene";

declare const canvas: HTMLCanvasElement;

const gpu = await init();
const camera = perspectiveCamera({ fov: 45, position: [2, 2, 3], target: [0, 0, 0] });
const controls = orbitControls(camera, { element: canvas, damping: 0.1 });

frameLoop(gpu, () => {
  controls.update(clock(gpu).deltaTime); // explicit update, no hidden rAF
});
```

## Notes

* The initial yaw/pitch/distance pose is derived from the node's current position relative to `target`.
* `update()` returns `true` only when the node moved — use it to skip re-rendering static frames.
* Replaces the per-example `installOrbitInput`/`installDragOrbit` copies; one shared implementation.
* **See also:** `OrbitControls`, `OrbitControlsOptions`, `perspectiveCamera`.

***

# OrbitControls

Class returned by `orbitControls()`.

## Import

```ts
import type { OrbitControls } from "vgpu/scene";
```

## Signature

```ts
declare class OrbitControls {
  update(deltaTime?: number): boolean;
  set(values: import("vgpu/scene").OrbitControlsValues): this;
  dispose(): void;
  readonly yaw: number;
  readonly pitch: number;
  readonly distance: number;
  readonly target: Float32Array;
}
```

## Examples

```ts
import { orbitControls, perspectiveCamera } from "vgpu/scene";

const camera = perspectiveCamera({ fov: 45, position: [0, 0, 5], target: [0, 0, 0] });
const controls = orbitControls(camera, { damping: 0 });
controls.set({ yaw: Math.PI / 2, distance: 8 });
controls.update();
controls.dispose();
```

## Notes

* `set()` jumps immediately (state and goal); pointer/wheel input eases through `damping`.
* `dispose()` removes DOM listeners; the node keeps its last transform.
* **See also:** `orbitControls`, `OrbitControlsValues`.

***

# OrbitControlsElement

Structural event-target contract so controls work with an `HTMLCanvasElement` without requiring DOM types, and with mocks in Node tests.

## Import

```ts
import type { OrbitControlsElement } from "vgpu/scene";
```

## Signature

```ts
interface OrbitControlsElement {
  addEventListener(type: string, listener: (event: never) => void, options?: { passive?: boolean } | boolean): void;
  removeEventListener(type: string, listener: (event: never) => void): void;
  setPointerCapture?(pointerId: number): void;
  releasePointerCapture?(pointerId: number): void;
}

// (listener parameters are typed loosely in the real declaration so the DOM's
// overloaded addEventListener stays assignable)
```

## Examples

```ts
import type { OrbitControlsElement } from "vgpu/scene";

declare const canvas: HTMLCanvasElement;
const element: OrbitControlsElement = canvas;
void element;
```

## Notes

* Listened events: `pointerdown`, `pointermove`, `pointerup`, `pointercancel`, `wheel` (non-passive).
* **See also:** `orbitControls`.

***

# OrbitControlsOptions

Options accepted by `orbitControls()`.

## Import

```ts
import type { OrbitControlsOptions } from "vgpu/scene";
```

## Signature

```ts
interface OrbitControlsOptions {
  readonly element?: import("vgpu/scene").OrbitControlsElement;
  readonly target?: import("vgpu/scene").Vec3Like;
  readonly damping?: number;
  readonly rotateSpeed?: number;
  readonly zoomSpeed?: number;
  readonly distance?: { readonly min?: number; readonly max?: number };
  readonly pitch?: { readonly min?: number; readonly max?: number };
  readonly label?: string;
}
```

## Examples

```ts
import { orbitControls, perspectiveCamera } from "vgpu/scene";

orbitControls(perspectiveCamera({ fov: 45, position: [0, 0, 5] }), {
  target: [0, 0.5, 0],
  distance: { min: 1, max: 20 },
  pitch: { min: -1.2, max: 1.2 },
});
```

## Notes

* **See also:** `orbitControls`, `OrbitControlsElement`.

***

# OrbitControlsValues

Values accepted by `OrbitControls.set()`; applied immediately to both state and goal.

## Import

```ts
import type { OrbitControlsValues } from "vgpu/scene";
```

## Signature

```ts
interface OrbitControlsValues {
  readonly yaw?: number;
  readonly pitch?: number;
  readonly distance?: number;
  readonly target?: import("vgpu/scene").Vec3Like;
}
```

## Examples

```ts
import { orbitControls, perspectiveCamera } from "vgpu/scene";

const controls = orbitControls(perspectiveCamera({ fov: 45, position: [0, 0, 5] }));
controls.set({ pitch: 0.4, distance: 6 });
```

## Notes

* `pitch` and `distance` are clamped to the configured limits.
* **See also:** `OrbitControls`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: orbit
description: Builds a column-major model matrix that rotates and translates around the Y axis using explicit JavaScript time. Use it for deterministic examples, tests, and simple scene animation when you do not need a scene graph.
---

# orbit



## Import

```ts
import { orbit } from "vgpu/scene";
```

## Signature

```ts
interface OrbitOptions {
  readonly radius?: number;
  readonly height?: number;
  readonly speed?: number;
}

type Mat4 = Float32Array;

declare function orbit(time: number, options?: OrbitOptions): Mat4;
```

## Parameters

| Param          | Type           | Required | Default | Notes                                                                                                            |
| -------------- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
| time           | `number`       | ✔        | —       | Explicit time value in seconds or any deterministic unit you choose. The helper computes `angle = time * speed`. |
| options        | `OrbitOptions` | ✖        | `{}`    | Optional transform controls. Omit it for a unit-radius orbit at Y=0 with speed 1.                                |
| options.radius | `number`       | ✖        | `1`     | XZ orbit radius. Negative values are allowed by JavaScript math and mirror the translation through the origin.   |
| options.height | `number`       | ✖        | `0`     | Constant Y translation stored in matrix element 13.                                                              |
| options.speed  | `number`       | ✖        | `1`     | Angular multiplier. `0` freezes rotation; negative values orbit in the opposite direction.                       |

**Returns:** `Mat4` (`Float32Array`) — 16 column-major matrix values. The upper-left 3×3 rotates around Y; the translation is `[cos(angle) * radius, height, sin(angle) * radius]`.
**Throws:** None.

## Examples

```ts
import { orbit } from "vgpu/scene";

const model = orbit(1.5, { radius: 2, height: 0.5, speed: 0.4 });
console.log(model.length); // 16
```

```ts
import { orbit, type Mat4 } from "vgpu/scene";

const paused: Mat4 = orbit(10, { speed: 0 });
void paused;
```

## Notes

* `orbit` does not read global time. Pass `clock(gpu).time` from the main API (`vgpu`) or your own clock explicitly.
* The matrix is column-major, matching WebGPU/WGSL matrix memory order and the scene camera helpers.
* **See also:** `Mat4`, `degToRad`, `perspectiveCamera`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: orthographicCamera
description: Creates a stateful orthographic camera node that maps a box in world space directly into clip space. Use it for UI, CAD, or any scene where perspective foreshortening is undesirable.
---

# orthographicCamera



## Import

```ts
import { orthographicCamera, type OrthographicCameraOptions } from "vgpu/scene";
```

## Signature

```ts
interface OrthographicCameraOptions {
  readonly left: number;
  readonly right: number;
  readonly bottom: number;
  readonly top: number;
  readonly near?: number;
  readonly far?: number;
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly target?: import("vgpu/scene").CameraVec3;
  readonly up?: import("vgpu/scene").CameraVec3;
  readonly label?: string;
}

declare function orthographicCamera(options: OrthographicCameraOptions): import("vgpu/scene").OrthographicCamera;
```

## Parameters

| Param            | Type       | Required | Default     | Notes                                                         |
| ---------------- | ---------- | -------- | ----------- | ------------------------------------------------------------- |
| options.left     | number     | ✔        | —           | Left plane of the world-space box.                            |
| options.right    | number     | ✔        | —           | Right plane. Must be greater than `left`.                     |
| options.bottom   | number     | ✔        | —           | Bottom plane.                                                 |
| options.top      | number     | ✔        | —           | Top plane. Must be greater than `bottom`.                     |
| options.near     | number     | ✖        | `0.1`       | Near clip plane distance. Must be positive.                   |
| options.far      | number     | ✖        | `100`       | Far clip plane distance. Must be greater than `near`.         |
| options.position | Vec3Like   | ✖        | `[0, 0, 0]` | Initial local position.                                       |
| options.target   | CameraVec3 | ✖        | —           | When given, the camera is oriented with `lookAt(target, up)`. |
| options.up       | CameraVec3 | ✖        | `[0, 1, 0]` | Up vector used only with `target`.                            |

**Returns:** `OrthographicCamera` — a scene node (`kind: "orthographic-camera"`) with `viewProjection`, `view`, `projection`, `position`, and `worldPosition`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` for non-positive `near` or `far <= near`. Avoid `left === right` and `bottom === top`; they produce unusable matrices rather than a validation error.

## Examples

```ts
import { orthographicCamera } from "vgpu/scene";

const cam = orthographicCamera({
  left: -2,
  right: 2,
  bottom: -2,
  top: 2,
  position: [0, 0, 5],
  target: [0, 0, 0],
});

cam.set({ left: -4, right: 4 }); // updates the projection in place
```

## Notes

* Orthographic cameras still use a real view matrix; `set({ position })` and `lookAt()` orbit the box without perspective.
* On canvas resize, call `set({ left, right })` (or top/bottom) to keep pixel-perfect scaling.
* **See also:** `OrthographicCamera`, `perspectiveCamera`, `Camera`, `orbitControls`.

***

# OrthographicCamera

Class returned by `orthographicCamera()`. Extends `SceneNode`, implements `SceneCamera`.

## Import

```ts
import type { OrthographicCamera } from "vgpu/scene";
```

## Signature

```ts
declare class OrthographicCamera {
  set(values: import("vgpu/scene").OrthographicCameraValues): this;
  lookAt(target: import("vgpu/scene").Vec3Like, up?: import("vgpu/scene").Vec3Like): this;
  readonly left: number;
  readonly right: number;
  readonly bottom: number;
  readonly top: number;
  readonly near: number;
  readonly far: number;
  readonly viewProjection: Float32Array;
  readonly view: Float32Array;
  readonly projection: Float32Array;
  readonly worldPosition: Float32Array;
}
```

**Returns:** Not a callable; construct with `orthographicCamera(options)`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` from `set()` on invalid near/far planes.

## Examples

```ts
import { orthographicCamera, type OrthographicCamera } from "vgpu/scene";

const cam: OrthographicCamera = orthographicCamera({ left: -1, right: 1, bottom: -1, top: 1 });
void cam.projection;
```

## Notes

* **See also:** `orthographicCamera`, `OrthographicCameraValues`, `SceneNode`.

***

# OrthographicCameraValues

Values accepted by `OrthographicCamera.set()`: projection planes plus node transform keys.

## Import

```ts
import type { OrthographicCameraValues } from "vgpu/scene";
```

## Signature

```ts
interface OrthographicCameraValues {
  readonly left?: number;
  readonly right?: number;
  readonly bottom?: number;
  readonly top?: number;
  readonly near?: number;
  readonly far?: number;
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly rotation?: import("vgpu/scene").Vec3Like;
  readonly quaternion?: import("vgpu/scene").QuatLike;
  readonly scale?: number | import("vgpu/scene").Vec3Like;
  readonly visible?: boolean;
  readonly label?: string;
}
```

## Examples

```ts
import { orthographicCamera } from "vgpu/scene";

orthographicCamera({ left: -1, right: 1, bottom: -1, top: 1 }).set({ near: 0.01 });
```

## Notes

* **See also:** `OrthographicCamera`, `NodeTransformValues`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: `perspectiveCamera`
description: Creates a stateful perspective camera node. FOV is in degrees for the public scene API. Matrices are stable `Float32Array` identities updated in place, so a binding set once stays fresh after `set()` / `lookAt()` calls.
---

# `perspectiveCamera`



## Import

```ts
import { perspectiveCamera, type PerspectiveCameraOptions } from "vgpu/scene";
```

## Signature

```ts
interface PerspectiveCameraOptions {
  readonly fov: number;
  readonly aspect?: number;
  readonly near?: number;
  readonly far?: number;
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly target?: import("vgpu/scene").CameraVec3;
  readonly up?: import("vgpu/scene").CameraVec3;
  readonly label?: string;
}

declare function perspectiveCamera(options: PerspectiveCameraOptions): import("vgpu/scene").PerspectiveCamera;
```

## Parameters

| Param            | Type       | Required | Default     | Notes                                                           |
| ---------------- | ---------- | -------- | ----------- | --------------------------------------------------------------- |
| options.fov      | number     | ✔        | —           | Vertical field of view in degrees. Must be in `(0, 180)`.       |
| options.aspect   | number     | ✖        | `1`         | Width / height. Update on resize with `camera.set({ aspect })`. |
| options.near     | number     | ✖        | `0.1`       | Near clip plane distance. Must be positive.                     |
| options.far      | number     | ✖        | `100`       | Far clip plane distance. Must be greater than `near`.           |
| options.position | Vec3Like   | ✖        | `[0, 0, 0]` | Initial local position.                                         |
| options.target   | CameraVec3 | ✖        | —           | When given, the camera is oriented with `lookAt(target, up)`.   |
| options.up       | CameraVec3 | ✖        | `[0, 1, 0]` | Up vector used only with `target`.                              |

**Returns:** `PerspectiveCamera` — a scene node (`kind: "perspective-camera"`) with `viewProjection`, `view`, `projection`, `position`, and `worldPosition`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` for out-of-range `fov`, non-positive `near`, or `far <= near`.

## Examples

```ts
import { perspectiveCamera } from "vgpu/scene";

const cam = perspectiveCamera({ fov: 45, position: [2, 2, 3], target: [0, 0, 0] });

const matrix = cam.viewProjection; // stable identity — bind once
cam.set({ fov: 60, position: [0, 2, 5] });
cam.lookAt([0, 0, 0]);
void matrix; // same array, already updated in place
```

## Notes

* Cameras are scene nodes: parent them under a `group()` rig and the view matrix accounts for the whole chain.
* `set()` accepts projection params (`fov`, `aspect`, `near`, `far`) plus every transform key from `NodeTransformValues` (see `PerspectiveCameraValues`).
* **See also:** `PerspectiveCamera`, `orthographicCamera`, `SceneCamera`, `orbitControls`.

***

# PerspectiveCamera

Class returned by `perspectiveCamera()`. Extends `SceneNode`, implements `SceneCamera`.

## Import

```ts
import type { PerspectiveCamera } from "vgpu/scene";
```

## Signature

```ts
declare class PerspectiveCamera {
  set(values: import("vgpu/scene").PerspectiveCameraValues): this;
  lookAt(target: import("vgpu/scene").Vec3Like, up?: import("vgpu/scene").Vec3Like): this;
  readonly fov: number;
  readonly aspect: number;
  readonly near: number;
  readonly far: number;
  readonly viewProjection: Float32Array;
  readonly view: Float32Array;
  readonly projection: Float32Array;
  readonly worldPosition: Float32Array;
}
```

**Returns:** Not a callable; construct with `perspectiveCamera(options)`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` from `set()` on invalid projection parameters.

## Examples

```ts
import { perspectiveCamera, type PerspectiveCamera } from "vgpu/scene";

const cam: PerspectiveCamera = perspectiveCamera({ fov: 45 });
cam.set({ aspect: 16 / 9 });
```

## Notes

* Matrix getters recompute lazily; reading `viewProjection` after a transform change is always fresh.
* **See also:** `perspectiveCamera`, `PerspectiveCameraValues`, `SceneNode`.

***

# PerspectiveCameraValues

Values accepted by `PerspectiveCamera.set()`: projection parameters plus node transform keys.

## Import

```ts
import type { PerspectiveCameraValues } from "vgpu/scene";
```

## Signature

```ts
interface PerspectiveCameraValues {
  readonly fov?: number;
  readonly aspect?: number;
  readonly near?: number;
  readonly far?: number;
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly rotation?: import("vgpu/scene").Vec3Like;
  readonly quaternion?: import("vgpu/scene").QuatLike;
  readonly scale?: number | import("vgpu/scene").Vec3Like;
  readonly visible?: boolean;
  readonly label?: string;
}
```

## Examples

```ts
import { perspectiveCamera } from "vgpu/scene";

perspectiveCamera({ fov: 45 }).set({ aspect: 2, position: [0, 1, 4] });
```

## Notes

* **See also:** `PerspectiveCamera`, `NodeTransformValues`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: scene
description: Creates a root node for a scene tree. Any node can act as a root; `scene()` names the intent and tags the node with `kind: "scene"`.
---

# scene



## Import

```ts
import { scene } from "vgpu/scene";
```

## Signature

```ts
declare function scene(options?: import("vgpu/scene").NodeOptions): import("vgpu/scene").SceneNode;
```

## Examples

```ts
import { box, group, mesh, scene, unlitMaterial } from "vgpu/scene";

const root = scene();
const spinner = group({ label: "spinner" });
root.add(spinner);
spinner.add(mesh(box({ size: 1 }), unlitMaterial({ color: [0.2, 0.5, 1] })));
```

## Notes

* The tree is pure JS state — no GPU resources. Rendering binds a tree later (`gpu.scene()`, phase 2).
* **See also:** `group`, `mesh`, `SceneNode`.

***

# group

Creates a plain transform node used to group children.

## Import

```ts
import { group } from "vgpu/scene";
```

## Signature

```ts
declare function group(options?: import("vgpu/scene").NodeOptions): import("vgpu/scene").SceneNode;
```

## Examples

```ts
import { group } from "vgpu/scene";

const rig = group({ position: [0, 2, 0], rotation: [0, Math.PI / 4, 0] });
rig.set({ rotation: [0, Math.PI / 2, 0] });
```

## Notes

* Children inherit the group's transform through `worldMatrix`.
* **See also:** `scene`, `mesh`, `SceneNode`, `NodeOptions`.

***

# mesh

Creates a renderable node: a pure geometry descriptor paired with a material.

## Import

```ts
import { mesh } from "vgpu/scene";
```

## Signature

```ts
declare function mesh(
  geometry: import("vgpu/scene").SceneGeometry,
  material?: import("vgpu/scene").SceneMaterial,
  options?: import("vgpu/scene").NodeOptions,
): import("vgpu/scene").MeshNode;
```

## Parameters

| Param    | Type            | Required | Default            | Notes                                                  |
| -------- | --------------- | -------- | ------------------ | ------------------------------------------------------ |
| geometry | `SceneGeometry` | ✔        | —                  | Pure descriptor from `box()`, `sphere()`, `plane()`, … |
| material | `SceneMaterial` | ✖        | `normalMaterial()` | Shading descriptor; swap by assigning `node.material`. |
| options  | `NodeOptions`   | ✖        | `{}`               | Initial transform, label, visibility, children.        |

**Returns:** `MeshNode` with `kind: "mesh"`.
**Throws:** `VGPU-SCENE-VALUE-INVALID` for malformed transform options.

## Examples

```ts
import { lambertMaterial, mesh, sphere } from "vgpu/scene";

const ball = mesh(sphere({ radius: 0.5 }), lambertMaterial({ color: [0.9, 0.4, 0.2] }), {
  position: [0, 0.5, 0],
});
ball.set({ rotation: [0, 1, 0] });
```

## Notes

* This is the scene-tree `mesh()`; the low-level vertex-buffer API remains `geometry(gpu, ...)` on the main `vgpu` entrypoint.
* **See also:** `MeshNode`, `SceneMaterial`, `SceneGeometry`, `group`.

***

# MeshNode

Class returned by `mesh()`. Extends `SceneNode` with `geometry` and `material` fields the renderer keys its caches by.

## Import

```ts
import type { MeshNode } from "vgpu/scene";
```

## Signature

```ts
declare class MeshNode {
  geometry: import("vgpu/scene").SceneGeometry;
  material: import("vgpu/scene").SceneMaterial;
}
```

## Examples

```ts
import { box, mesh, unlitMaterial } from "vgpu/scene";

const node = mesh(box());
node.material = unlitMaterial({ color: [1, 0, 0] });
```

## Notes

* Both fields are swappable; identity changes re-key renderer caches (phase 2).
* **See also:** `mesh`, `SceneNode`.

***

# SceneNode

Base scene-tree node: a TRS transform with parent/children links. Mutation goes through `set()` so world matrices recompute lazily and only for dirty subtrees; exposed arrays keep a stable identity and are updated in place.

## Import

```ts
import { SceneNode } from "vgpu/scene";
```

## Signature

```ts
declare class SceneNode {
  set(values: import("vgpu/scene").NodeTransformValues): this;
  lookAt(target: import("vgpu/scene").Vec3Like, up?: import("vgpu/scene").Vec3Like): this;
  add(...children: SceneNode[]): this;
  remove(...children: SceneNode[]): this;
  removeFromParent(): this;
  traverse(visit: (node: SceneNode) => void): void;
  readonly kind: import("vgpu/scene").SceneNodeKind;
  readonly parent: SceneNode | null;
  readonly children: readonly SceneNode[];
  readonly position: Float32Array;
  readonly quaternion: Float32Array;
  readonly scale: Float32Array;
  readonly localMatrix: Float32Array;
  readonly worldMatrix: Float32Array;
  readonly worldPosition: Float32Array;
  visible: boolean;
  label: string | undefined;
}
```

**Returns:** Not directly constructed in user code; use `scene()`, `group()`, `mesh()`, cameras, or lights.
**Throws:** `VGPU-SCENE-CYCLE` when `add()` would create a cycle; `VGPU-SCENE-VALUE-INVALID` for malformed vectors.

## Examples

```ts
import { group } from "vgpu/scene";

const parent = group({ position: [1, 0, 0] });
const child = group({ position: [0, 1, 0] });
parent.add(child);

const world = child.worldMatrix; // stable identity
parent.set({ position: [2, 0, 0] });
void world; // same array, refreshed on next read
```

## Notes

* `position`/`quaternion`/`scale`/`worldMatrix` return the internal arrays: safe to bind, but mutate only via `set()` so dirty tracking stays exact.
* `lookAt()` orients the node's -Z axis at a world-space target and compensates for parent transforms.
* `add()` reparents nodes that already have a parent.
* **See also:** `group`, `mesh`, `NodeTransformValues`, `SceneCamera`.

***

# NodeOptions

Options accepted by node factories: all of `NodeTransformValues` plus initial `children`.

## Import

```ts
import type { NodeOptions } from "vgpu/scene";
```

## Signature

```ts
interface NodeOptions {
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly rotation?: import("vgpu/scene").Vec3Like;
  readonly quaternion?: import("vgpu/scene").QuatLike;
  readonly scale?: number | import("vgpu/scene").Vec3Like;
  readonly visible?: boolean;
  readonly label?: string;
  readonly children?: readonly import("vgpu/scene").SceneNode[];
}
```

## Examples

```ts
import { group, mesh, box } from "vgpu/scene";

const rig = group({ label: "rig", children: [mesh(box())] });
void rig;
```

## Notes

* **See also:** `NodeTransformValues`, `group`, `scene`.

***

# NodeTransformValues

Transform and flag values accepted by `node.set()`.

## Import

```ts
import type { NodeTransformValues } from "vgpu/scene";
```

## Signature

```ts
interface NodeTransformValues {
  readonly position?: import("vgpu/scene").Vec3Like;
  readonly rotation?: import("vgpu/scene").Vec3Like;
  readonly quaternion?: import("vgpu/scene").QuatLike;
  readonly scale?: number | import("vgpu/scene").Vec3Like;
  readonly visible?: boolean;
  readonly label?: string;
}
```

## Parameters

| Field      | Type                 | Required | Default     | Notes                                                                      |
| ---------- | -------------------- | -------- | ----------- | -------------------------------------------------------------------------- |
| position   | `Vec3Like`           | ✖        | `[0, 0, 0]` | Local position.                                                            |
| rotation   | `Vec3Like`           | ✖        | —           | Intrinsic XYZ Euler angles in radians. Ignored when `quaternion` is given. |
| quaternion | `QuatLike`           | ✖        | identity    | Local rotation (x, y, z, w).                                               |
| scale      | `number \| Vec3Like` | ✖        | `1`         | Uniform number or per-axis vector.                                         |
| visible    | `boolean`            | ✖        | `true`      | Invisible nodes (and their subtrees) are skipped by the renderer.          |
| label      | `string`             | ✖        | —           | Used in error `where` strings and debugging.                               |

## Examples

```ts
import { group } from "vgpu/scene";

group().set({ position: [0, 1, 0], rotation: [0, Math.PI, 0], scale: 2 });
```

## Notes

* **See also:** `SceneNode`, `NodeOptions`, `Vec3Like`, `QuatLike`.

***

# Vec3Like

Three-component vector input accepted by scene-tree setters: tuples, plain arrays, or typed arrays.

## Import

```ts
import type { Vec3Like } from "vgpu/scene";
```

## Signature

```ts
type Vec3Like = readonly [number, number, number] | readonly number[] | Float32Array;
```

## Examples

```ts
import type { Vec3Like } from "vgpu/scene";

const up: Vec3Like = [0, 1, 0];
```

## Notes

* Setters copy the input; mutating it afterwards does not affect the node.
* **See also:** `QuatLike`, `NodeTransformValues`.

***

# QuatLike

Quaternion input (x, y, z, w) accepted by scene-tree setters.

## Import

```ts
import type { QuatLike } from "vgpu/scene";
```

## Signature

```ts
type QuatLike = readonly [number, number, number, number] | readonly number[] | Float32Array;
```

## Examples

```ts
import type { QuatLike } from "vgpu/scene";

const identity: QuatLike = [0, 0, 0, 1];
```

## Notes

* **See also:** `Vec3Like`, `NodeTransformValues`.

***

# SceneNodeKind

Discriminator for scene-tree node types, used by renderers and traversal code.

## Import

```ts
import type { SceneNodeKind } from "vgpu/scene";
```

## Signature

```ts
type SceneNodeKind =
  | "scene"
  | "group"
  | "mesh"
  | "perspective-camera"
  | "orthographic-camera"
  | "directional-light"
  | "ambient-light";
```

## Examples

```ts
import { scene, mesh, box, type SceneNode } from "vgpu/scene";

const root = scene({ children: [mesh(box())] });
const meshes: SceneNode[] = [];
root.traverse((node) => {
  if (node.kind === "mesh") meshes.push(node);
});
```

## Notes

* **See also:** `SceneNode`, `traverse` on `SceneNode`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: srgb
description: Converts sRGB color literals into linear RGB floats for CPU-side scene constants. Use it when your source color is a web-style hex value or normalized sRGB tuple but your shader math expects linear color.
---

# srgb



## Import

```ts
import { srgb } from "vgpu/scene";
```

## Signature

```ts
type SrgbInput = number | string | [number, number, number];
type LinearRgb = [number, number, number];

declare function srgb(input: SrgbInput): LinearRgb;
```

## Parameters

| Param | Type                                           | Required | Default | Notes                                                                                                                                                                 |
| ----- | ---------------------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| input | `number \| string \| [number, number, number]` | ✔        | —       | A packed `0xRRGGBB` number, a `"#rrggbb"` hex string, or a three-channel sRGB tuple. Tuple channels are expected in normalized `0..1` units, not byte `0..255` units. |

**Returns:** `LinearRgb` (`[number, number, number]`) — normalized linear RGB channels. Each channel uses the standard sRGB transfer curve: `channel / 12.92` for `channel <= 0.04045`, otherwise `((channel + 0.055) / 1.055) ** 2.4`.
**Throws:** `VGPU-CORE-INVALID-USAGE` for malformed hex strings; numeric and tuple inputs are not validated.

## Examples

```ts
import { srgb } from "vgpu/scene";

const albedo = srgb(0xff8040);
const sky = srgb("#3b82f6");
console.log(albedo.length, sky.length); // 3 3
```

```ts
import { srgb } from "vgpu/scene";

const normalizedWhite = srgb([1, 1, 1]);
const normalizedGray = srgb([0.5, 0.5, 0.5]);
void normalizedWhite;
void normalizedGray;
```

## Notes

* The numeric form is a packed hexadecimal color (`0xff8040`); the string form accepts `"#rrggbb"` or `"rrggbb"`. Do not call `srgb(255, 128, 64)`; that is not the function signature.
* Tuple input is not clamped. Values outside `0..1`, `NaN`, and `Infinity` flow through JavaScript arithmetic and can produce non-display color values.
* `srgb` is CPU-side only. For texture sampling and post-processing, keep color-management decisions explicit in WGSL.
* **See also:** `degToRad`, `SceneGeometry`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: compile
description: Converts one raw runtime WGSL string into the data-only `ResolvedShader` shape used by `@vgpu/wgsl`. Use it when the shader source is already a complete WGSL entry module and does not need import resolution.
---

# compile



## Import

```ts
import { compile } from "@vgpu/wgsl";
```

## Signature

```ts
import type { ResolvedShader } from "@vgpu/wgsl";

declare function compile(wgsl: string): ResolvedShader;
```

## Parameters

| Param | Type   | Required | Default | Notes                                                                                                                                                                                                 |
| ----- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| wgsl  | string | ✔        | —       | Complete WGSL source for one runtime shader. It is copied byte-for-byte to `resolved.wgsl`. Top-level `import` syntax is rejected; use `resolveShader` or a build-time loader for WGSL import graphs. |

**Returns:** `ResolvedShader` — a data object with `kind: "wgsl"`, the original `wgsl`, passthrough source/AST/source-map metadata, deterministic `cacheKey`, detected `entryPoints`, and source `stats`.

**Throws:** `VGPU-WGSL-RUNTIME-IMPORT` when the trimmed source starts with a top-level `import` statement after comments are stripped — remove the import, pre-resolve with `resolveShader`, or use the Vite/webpack loader.

## Examples

```ts
import { compile } from "@vgpu/wgsl";

const shader = compile(`
@compute @workgroup_size(1)
fn main() {
}
`);

shader.kind satisfies "wgsl";
shader.entryPoints satisfies readonly string[];
console.log(shader.entryPoints.includes("main"));
```

```ts
import { compile } from "@vgpu/wgsl";

try {
  compile(`import { helper } from "./helper.wgsl";`);
} catch (error) {
  if (error instanceof Error && "code" in error) {
    console.log(error.code === "VGPU-WGSL-RUNTIME-IMPORT");
  }
}
```

## Notes

* `compile()` does not read files, resolve packages, mangle imported symbols, reflect binding layouts, or validate WGSL semantics.
* The only syntax check is import rejection. Invalid WGSL without `import` still returns a `ResolvedShader`; WebGPU validation happens later when a shader module is created.
* `stats.bindGroups` is `0` in this runtime passthrough shape. Do not use it as reflection for resource bindings.
* Runtime WGSL modules with resources are allowed because there is no import graph. For imported WGSL modules, keep modules pure and declare every `@group/@binding` resource in the entry module.
* **See also:** `ResolvedShader`, `ShaderSource`, `resolveShader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: wgslVitePlugin and transformWgsl
description: Vite/Rollup transform that turns `.wgsl` files into JavaScript modules exporting `ShaderSource` v1 objects. Use the plugin in Vite apps and `transformWgsl()` in tests or custom tooling.
---

# wgslVitePlugin and transformWgsl



## Import

```ts
import wgslVitePlugin, { transformWgsl } from "@vgpu/wgsl/loader-vite";
import type { ViteLoadResult } from "@vgpu/wgsl/loader-vite";
```

## Signature

```ts
interface ViteLoadResult { readonly code: string; readonly map: null }

interface WgslVitePluginOptions {
  readonly minify?: boolean | { readonly whitespace?: boolean; readonly identifiers?: "none" | "safe" };
}

interface TransformWgslOptions extends WgslVitePluginOptions {
  readonly source: string;
  readonly id: string;
  readonly onDependency?: (absPath: string) => void;
}

declare function transformWgsl(source: string, id: string, options?: WgslVitePluginOptions): Promise<ViteLoadResult>;
declare function transformWgsl(opts: TransformWgslOptions): Promise<ViteLoadResult>;
declare function wgslVitePlugin(options?: WgslVitePluginOptions): {
  readonly name: string;
  readonly transform: (this: { addWatchFile(fileName: string): void }, source: string, id: string) => Promise<ViteLoadResult | null>;
};
```

## Parameters

| Param             | Type                        | Required        | Default     | Notes                                                                                                                                                                                                             |                                                                                                                                                                       |
| ----------------- | --------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options.minify    | \`boolean                   | MinifyOptions\` | ✖           | `false`                                                                                                                                                                                                           | Shared plugin/transform minify option. `true` means `{ whitespace: true, identifiers: "safe" }`; object form defaults to `{ whitespace: true, identifiers: "none" }`. |
| source            | string                      | ✔               | —           | Raw WGSL file contents. Leaf files without top-level imports are emitted directly, optionally minified.                                                                                                           |                                                                                                                                                                       |
| id                | string                      | ✔               | —           | WGSL file id/path. Used as resolver entry for import graphs. Plugin transform ignores ids that do not end with `.wgsl`.                                                                                           |                                                                                                                                                                       |
| opts.source       | string                      | ✔               | —           | Object-overload source field.                                                                                                                                                                                     |                                                                                                                                                                       |
| opts.id           | string                      | ✔               | —           | Object-overload id field.                                                                                                                                                                                         |                                                                                                                                                                       |
| opts.onDependency | `(absPath: string) => void` | ✖               | no callback | Called for each transitive dependency as soon as its path resolves, before it is loaded. Discovered dependencies are still reported when a later resolution step throws. Leaf files intentionally do not call it. |                                                                                                                                                                       |

**Returns:** `Promise<ViteLoadResult>` from `transformWgsl()` with JavaScript module `code` and `map: null`; plugin `transform` returns that result for `.wgsl` ids or `null` for other ids.

**Throws:** Any `resolveShader()` `VGPU-WGSL-*` or `VGPU-RESOLVE-MODULE-BINDING` error when import graph resolution fails — fix imports, module purity, package resolution, duplicates, or WGSL validation/minification.
**Throws:** `VGPU-WGSL-MINIFY-IDENTIFIERS` or `VGPU-WGSL-MINIFY-BLOCK` when minification options/source are invalid for a leaf file — pass a valid minify mode or fix unterminated comments.

## Examples

```ts
import wgslVitePlugin from "@vgpu/wgsl/loader-vite";

const viteConfig = {
  plugins: [wgslVitePlugin({ minify: true })],
};

export default viteConfig;
```

```ts
import { transformWgsl } from "@vgpu/wgsl/loader-vite";

const result = await transformWgsl(
  "@compute @workgroup_size(1) fn main() {}",
  "/shader.wgsl",
  { minify: { whitespace: true } },
);

console.log(result.map === null, result.code.includes("version"));
```

## Notes

* Transform output default-exports `ShaderSource` v1: `{ version: 1, wgsl: "..." }`.
* `wgslVitePlugin()` only handles ids ending with `.wgsl`; use `transformWgsl()` directly for tests and non-Vite tooling.
* Leaf shader transforms do not call `onDependency` because Vite already tracks the entry file. Imported graph transforms call it for transitive dependencies before loading them, including on resolution paths that later fail.
* A leaf WGSL file may declare entry resources. Shared/imported modules must be pure: no `@group/@binding` outside the entry.
* **The plugin never validates WGSL, in any mode, for either leaf files or import graphs.** It calls `resolveShader({ validate: false })` for imported graphs — parsing, purity checks, DCE, mangling, and optional minification still apply, but the device-backed `createShaderModule` check does not run. Leaf files (no imports) never call `resolveShader()` at all and are emitted directly, so they get no semantic processing beyond the raw text. There is no plugin option to opt into validation; a `validate` key in the plugin options is silently ignored. `vite build`/`vite dev` will happily compile and ship invalid WGSL. The validation gate is `npx vgpu check --require-validation <file>` — run it in CI or as a pre-commit hook; see `npx vgpu docs cat cli.docs.md`.
* **See also:** `ShaderSource`, `resolveShader`, `wgslWebpackLoader`, and the `nextjs` guide (`npx vgpu docs cat nextjs.md`) for the ambient `.d.ts` that types `.wgsl` imports.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: wgslWebpackLoader
description: Webpack loader that turns `.wgsl` files into JavaScript modules exporting `ShaderSource` v1 objects. Use it when webpack should inline WGSL and resolve vgpu WGSL imports during bundling.
---

# wgslWebpackLoader



## Import

```ts
import wgslWebpackLoader from "@vgpu/wgsl/loader-webpack";
```

## Signature

```ts
interface WgslWebpackLoaderOptions {
  readonly minify?: boolean | { readonly whitespace?: boolean; readonly identifiers?: "none" | "safe" };
}

type LoaderContext = {
  resourcePath?: string;
  async?: () => (error: Error | null, result?: string) => void;
  addDependency?: (file: string) => void;
  getOptions?: () => unknown;
};

type WgslWebpackLoader = (this: LoaderContext, source: string) => string | void;
```

## Parameters

| Param              | Type                     | Required        | Default                                   | Notes                                                                                                                                                                                           |                                                                                                                                |
| ------------------ | ------------------------ | --------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| source             | string                   | ✔               | —                                         | Raw WGSL file contents supplied by webpack. Leaf files without top-level imports are emitted directly, optionally minified. Files with top-level imports are resolved from `this.resourcePath`. |                                                                                                                                |
| this.resourcePath  | string                   | ✖               | `"<webpack>"` for resolver entry fallback | Absolute path to the `.wgsl` file. Needed for relative import resolution and dependency reporting.                                                                                              |                                                                                                                                |
| this.async         | `() => callback`         | ✖               | synchronous mode                          | Required only when the WGSL source has top-level imports. Without async mode, imports throw `VGPU-WGSL-RUNTIME-IMPORT`.                                                                         |                                                                                                                                |
| this.addDependency | `(file: string) => void` | ✖               | no explicit extra dependencies            | Called as each transitive dependency is discovered, before it is loaded, so webpack invalidates on imported `.wgsl` changes even when the current resolution fails.                             |                                                                                                                                |
| this.getOptions    | `() => unknown`          | ✖               | `{}`                                      | Reads `options.minify` when present. Unknown options are ignored.                                                                                                                               |                                                                                                                                |
| options.minify     | \`boolean                | MinifyOptions\` | ✖                                         | `false`                                                                                                                                                                                         | `true` means `{ whitespace: true, identifiers: "safe" }`; object form defaults to `{ whitespace: true, identifiers: "none" }`. |

**Returns:** `string | void` — for leaf shaders, returns JavaScript module source synchronously. For import graphs, returns `void` and passes JavaScript module source to webpack's async callback.

**Throws:** `VGPU-WGSL-RUNTIME-IMPORT` when a WGSL file contains imports but the loader context does not provide async mode — enable webpack asynchronous loader execution.
**Throws:** Any `resolveShader()` `VGPU-WGSL-*` or `VGPU-RESOLVE-MODULE-BINDING` error when import graph resolution fails — fix the WGSL import graph, module purity, or minify options.
**Throws:** `VGPU-WGSL-MINIFY-IDENTIFIERS` or `VGPU-WGSL-MINIFY-BLOCK` when minification options/source are invalid for a leaf file — pass a valid minify mode or fix unterminated comments.

## Examples

```ts
const config = {
  module: {
    rules: [
      {
        test: /\.wgsl$/,
        loader: "@vgpu/wgsl/loader-webpack",
        options: { minify: true },
      },
    ],
  },
};

export default config;
```

```ts
import type { ShaderSource } from "@vgpu/wgsl";

const shader: ShaderSource = {
  version: 1,
  wgsl: "@compute @workgroup_size(1) fn main() {}",
};

console.log(shader.version);
```

## Notes

* **Framework setup lives in a guide, not here.** For `next.config.ts` (Turbopack rules or the `webpack()` hook), the ambient `.d.ts` that types `import shader from "./x.wgsl"`, and the client component that owns the canvas, read `npx vgpu docs cat nextjs.md`.
* TypeScript needs an ambient declaration before it accepts a `.wgsl` import. `@vgpu/wgsl` ships one: add `/// <reference types="@vgpu/wgsl/wgsl-types" />` to any `.d.ts` in your project.
* Loader output is `ShaderSource` v1: default export `{ version: 1, wgsl: "..." }`, not a bare string and not a reflection/binding map.
* Imported files are registered with webpack before they are loaded. If a transient edit causes resolution to fail, a later valid save can therefore invalidate and rebuild the failed importer without restarting the dev server.
* A leaf WGSL file may declare entry resources. The imported-module purity rule is enforced only when the file imports other modules and `resolveShader()` sees a graph.
* **The loader never validates WGSL, in any mode, for either leaf files or import graphs.** It calls `resolveShader({ validate: false })` for imported graphs — parsing, purity checks, DCE, mangling, and optional minification still run, but the device-backed `createShaderModule` check does not. Leaf files (no imports) never call `resolveShader()` at all and are emitted directly, so they are not merely unvalidated — they receive no semantic processing beyond the raw text. There is no loader option to opt into validation; a `validate` key in the loader options is silently ignored. `npx next build`/`next dev` (webpack or Turbopack) will happily compile and ship invalid WGSL. The validation gate is `npx vgpu check --require-validation <file>` — run it in CI or as a pre-commit hook; see `npx vgpu docs cat cli.docs.md`.
* Do not put `@group/@binding` declarations in shared WGSL modules. Put resources in the entry file and export shared structs/functions from modules.
* **See also:** `ShaderSource`, `resolveShader`, `wgslVitePlugin`, and the `nextjs` guide (`npx vgpu docs cat nextjs.md`).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: reflectSource
description: Reflects one raw WGSL string — bindings, entry points, structs, aliases, overrides, and host-shareable layouts — through the same scanner/parser/reflection path `resolveShader()` uses internally. Use it when you already have a single, import-free WGSL string (for example the `wgsl` field of a `ShaderSource` emitted by a loader, or a string you built yourself) and want reflection data without resolving an import graph.
---

# reflectSource



## Import

```ts
import { reflectSource } from "@vgpu/wgsl/reflect-source";
import type { EntryPointInfo, Reflection } from "@vgpu/wgsl/reflect-source";
```

## Signature

```ts
declare function reflectSource(wgsl: string, path?: string): Reflection;

interface Reflection {
  readonly bindings: readonly BindingInfo[];
  readonly entryPoints: readonly EntryPointInfo[];
  readonly overrides: readonly OverrideInfo[];
  readonly featuresRequired: readonly string[];
  readonly aliases: readonly AliasInfo[];
  readonly structs: readonly StructInfo[];
  readonly hostShareableLayouts: readonly HostShareableLayout[];
}

interface EntryPointInfo {
  readonly name: string;
  readonly mangledName: string;
  readonly stage: "vertex" | "fragment" | "compute";
  readonly workgroupSize?: readonly [number, number, number];
  readonly inputs?: readonly EntryPointInputInfo[];
  readonly bindings?: readonly BindingRef[];
  readonly samplingPairs?: readonly SamplingPair[];
}

interface BindingRef {
  readonly group: number;
  readonly binding: number;
}
```

`Reflection` also re-exports the supporting types used by its fields: `BindingInfo`, `BindingKind`,
`AddressSpace`, `AccessMode`, `StructInfo`, `StructMemberInfo`, `AliasInfo`, `OverrideInfo`,
`EntryPointInputInfo`, `SamplingPair`, `HostShareableLayout`, `LayoutMember`, `LayoutMode`,
`ReflectedBindingLayout`, and `WGSLType` — all importable from `@vgpu/wgsl/reflect-source`.
`ReflectionFacade` is a deprecated alias kept for backwards compatibility with the pre-#252 name;
prefer `Reflection`.

## Parameters

| Param | Type   | Required | Default       | Notes                                                                                                                               |
| ----- | ------ | -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| wgsl  | string | ✔        | —             | A single WGSL module as text. Must not contain top-level `import` — `reflectSource()` reflects one raw string, not an import graph. |
| path  | string | ✖        | `"<runtime>"` | Label attached to diagnostics/positions for this string. Purely cosmetic when you have no real file path.                           |

**Returns:** `Reflection` — the same plain-data shape `resolveShader()` exposes as `ResolvedShader.reflection`: `bindings` (every `@group/@binding` declaration), `entryPoints` (every `@vertex`/`@fragment`/`@compute` function), `overrides`, `featuresRequired`, `aliases`, `structs`, and `hostShareableLayouts` (CPU-memory layout for uniform/storage types).

**Throws:** `VGPU-WGSL-REFLECT-SOURCE-IMPORT` when `wgsl` contains a top-level `import` — `reflectSource()` intentionally rejects WGSL import graphs. Use `resolveShader()` (`@vgpu/wgsl/runtime`) instead, which resolves imports first and exposes the identical `Reflection` shape on `ResolvedShader.reflection`.
**Throws:** The same lexer/parser `VGPU-WGSL-LEX-*`/`VGPU-WGSL-*` diagnostics `resolveShader()` raises for malformed WGSL (unterminated comments/strings, non-ASCII identifiers, etc.) — fix the WGSL reported by the diagnostic.

## Examples

```ts
import { reflectSource } from "@vgpu/wgsl/reflect-source";

const reflection = reflectSource(`
@group(0) @binding(0) var<uniform> scale: f32;

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(scale);
}
`);

console.log(reflection.entryPoints[0]?.name); // "fs_main"
console.log(reflection.bindings[0]?.group, reflection.bindings[0]?.binding); // 0 0
```

```ts
import { reflectSource, type EntryPointInfo } from "@vgpu/wgsl/reflect-source";

const reflection = reflectSource(`
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {}
`);

const entry: EntryPointInfo = reflection.entryPoints[0]!;

// EntryPointInfo is plain data: JSON.stringify, spread, and Object.keys all see the
// complete shape — nothing is hidden behind a getter, a class instance, or a toJSON hook.
const json = JSON.stringify(entry);
const copy = { ...entry };
const keys = Object.keys(entry);

console.log(json.includes("\"stage\":\"compute\""), copy.stage, keys);
```

```ts
import { reflectSource } from "@vgpu/wgsl/reflect-source";

// Serialization survives a structured clone / worker postMessage boundary too — no `toJSON`
// hook runs, so the clone has exactly the same enumerable own keys as the original.
const reflection = reflectSource("@fragment\nfn fs_main() -> @location(0) vec4f { return vec4f(1.0); }");
const cloned = structuredClone(reflection.entryPoints[0]);
console.log(Object.keys(cloned!).length === Object.keys(reflection.entryPoints[0]!).length);
```

## Notes

* **Serialization contract (issue [#252](https://github.com/vercel-labs/vgpu/issues/252)):** every field of `EntryPointInfo` — and of `Reflection` as a whole — is an ordinary enumerable, own, writable property. `JSON.stringify`, `{ ...entry }`, `Object.keys`/`Object.entries`/`Object.assign`, `structuredClone`, and worker `postMessage` all see the complete shape. There is no `toJSON` hook and no hidden/non-enumerable metadata. `workgroupSize`, `inputs`, `bindings`, and `samplingPairs` are simply *absent* (not present as an `undefined`-valued key) when they do not apply, so the key set stays identical across a serialization boundary — an own key valued `undefined` would survive `structuredClone` but get dropped by `JSON.stringify`, which would otherwise make the two disagree.
* `Reflection` and `EntryPointInfo` are the **frozen/stable** reflection shapes: this is the same interface `ResolvedShader.reflection` exposes from `resolveShader()` (`@vgpu/wgsl/runtime`), not a different or looser one. Do not confuse this with `ResolvedShader.entryPoints` from the top-level `@vgpu/wgsl` `compile()` (`npx vgpu docs cat /@vgpu/wgsl/resolved-shader.docs.md`) — that is an unrelated, older, and much simpler `readonly string[]` of matched entry-point names with no bindings/stage/inputs. `reflectSource()`/`resolveShader()`'s `EntryPointInfo[]` is the one to reach for whenever you need stage, workgroup size, vertex inputs, or bound resources.
* `reflectSource()` only accepts a single raw WGSL string with no imports. If your WGSL imports other modules, call `resolveShader()` (`@vgpu/wgsl/runtime`) instead and read `.reflection` off the result — it walks the same scanner/parser/`ReflectionFacade` path and returns the identical `Reflection` shape.
* `bindings`/`samplingPairs` on an `EntryPointInfo` are scoped to that entry point and its transitive callees (a conservative "used by the whole module" fallback applies when static analysis cannot prove otherwise); `Reflection.bindings` at the top level lists every declared `@group/@binding` in the module regardless of which entry points use it.
* **See also:** `ResolvedShader` (`compile()`, `@vgpu/wgsl`), `resolveShader` (`@vgpu/wgsl/runtime`).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: resolveShader
description: Loads a WGSL entry module, resolves vgpu WGSL imports, enforces pure imported modules, and emits one plain WGSL string. Use it in build/setup/tooling code when shader source is split across files or in-memory modules.
---

# resolveShader



## Import

```ts
import { resolveShader } from "@vgpu/wgsl/runtime";
import type { ResolveOptions, ResolvedShader } from "@vgpu/wgsl/runtime";
```

## Signature

```ts
import type { ResolvedShader } from "@vgpu/wgsl/runtime";

interface ResolveOptions {
  readonly entry: string;
  readonly rootDir?: string;
  readonly packageMap?: Record<string, string>;
  readonly modules?: Record<string, string>;
  readonly onDependency?: (path: string) => void;
  readonly validate?: "off" | "auto" | "require" | boolean;
  readonly minify?: boolean | { readonly whitespace?: boolean; readonly identifiers?: "none" | "safe" };
}

declare function resolveShader(opts: ResolveOptions): Promise<ResolvedShader>;
```

## Parameters

| Param             | Type                     | Required        | Default                                                                | Notes                                                                                                                                                                                                                                                                                                              |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| ----------------- | ------------------------ | --------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts.entry        | string                   | ✔               | —                                                                      | Entry WGSL module path. With `modules`, it is canonicalized against the virtual module map; without `modules`, it is resolved on disk and may omit `.wgsl` when a matching file or `index.wgsl` exists. The entry may declare `@group/@binding` resources.                                                         |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| opts.rootDir      | string                   | ✖               | `dirname(entry)` for cache-key grouping; no `@/` alias unless provided | Base directory for `@/foo.wgsl` imports. Also used as the default root passed to cache key generation when present.                                                                                                                                                                                                |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| opts.packageMap   | `Record<string, string>` | ✖               | `{}`                                                                   | Prefix map for package-style WGSL imports. If a specifier starts with a key, the target prefix is joined with the remainder.                                                                                                                                                                                       |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| opts.modules      | `Record<string, string>` | ✖               | filesystem reads                                                       | In-memory WGSL filesystem. Keys are normalized with `/`; relative imports use virtual paths and package imports require `packageMap`.                                                                                                                                                                              |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| opts.onDependency | `(path: string) => void` | ✖               | no callback                                                            | Called once for each imported module as soon as its path resolves, before that module is read or parsed. Already-loaded modules and the entry are omitted. Discovered dependencies are still reported when a later resolution step throws, allowing build tools to watch the files that can repair a failed graph. |                                                                                                                                                                                                                  |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |
| opts.validate     | \`"off"                  | "auto"          | "require"                                                              | boolean\`                                                                                                                                                                                                                                                                                                          | ✖                                                                                                                                                                                                                | `"auto"`, or `VGPU_VALIDATE` when set | Device-backed validation of emitted WGSL (`createShaderModule` plus a compilation-info round trip) through a lazily imported `@vgpu/adapter-node`. `"auto"` attempts validation and throws `VGPU-WGSL-NAGA-UNKNOWN` for invalid WGSL, but when no device/adapter is available it warns once to stderr and continues, recording the skip on `ResolvedShader.validation`. `"require"` (or `true`) throws `VGPU-WGSL-VALIDATE-NO-DEVICE`/`VGPU-WGSL-VALIDATE-ADAPTER-MISSING` instead of skipping. `"off"` (or `false`) never attempts validation and never imports device code. An explicit value here always wins over `VGPU_VALIDATE` (\`"off" | "auto" | "require"`; anything else throws `VGPU-WGSL-VALIDATE-ENV-INVALID`). Independent of this option, `minify`with`identifiers: "safe"` always self-checks that renaming left no dangling reference (`VGPU-WGSL-MINIFY-DANGLING-IDENT\`), with no GPU involved. |
| opts.minify       | \`boolean                | MinifyOptions\` | ✖                                                                      | `false`                                                                                                                                                                                                                                                                                                            | `true` means `{ whitespace: true, identifiers: "safe" }`; object form defaults to `{ whitespace: true, identifiers: "none" }`; `false` or omitted preserves whitespace/comments after resolver emission and DCE. |                                       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |        |                                                                                                                                                                                                                                                           |

Validation always covers the WGSL you get back. When `minify` rewrites the emitted text, validation runs twice: first on the unminified emission, because that text is what gives diagnostics accurate line/column mapping back to your modules, and then on the final minified string. So a successful resolve with `validation.ok === true` means the exact `wgsl` returned was accepted by the device — a minifier bug cannot slip corrupt output past a passing validation. Both passes share the same leased device, and the second one is skipped when minification changed nothing (the returned text was already validated).

Validation acquires one WebGPU device per process through `@vgpu/adapter-node` and reuses it across calls, then destroys it shortly after the last validation finishes — a live device holds handles on the Node event loop, so without that release a script that validated a shader would never exit on its own. A later `resolveShader` transparently acquires a new device; a *failed* acquisition is remembered and **not retried for the lifetime of the process**, so after installing a device (for example `npx vgpu install-software-renderer`) restart the process rather than expecting the next call to pick it up.

**Returns:** `Promise<ResolvedShader>` — resolved WGSL plus dependency paths, cache keys, lightweight AST modules, source map, diagnostics, reflection for entry points/resources, and `validation: { mode, attempted, ok, skipped? }` reporting what the device-backed check actually did (`skipped` carries the `code`, `message`, and `fix` when `"auto"` could not get a device).

**Throws:** `VGPU-RESOLVE-MODULE-BINDING` when a non-entry imported module declares any `@group(...)` or `@binding(...)` resource — move the resource declaration into the entry module and export only structs/functions from the module. The error message is exactly:

```text
VGPU-RESOLVE-MODULE-BINDING: <module> declares '@group(<group>) @binding(<binding>) <name>'.
Modules cannot declare bindings — export the struct and declare it in your entry:
  export struct NoiseConfig { seed: u32 }
  // in your entry: @group(0) @binding(0) var<uniform> cfg: NoiseConfig;
```

**Throws:** `VGPU-WGSL-RES-ABS` when an import specifier starts with `/` — use a relative, `@/`, or package import.
**Throws:** `VGPU-WGSL-RES-NOTFOUND` when the entry/import path or virtual module cannot be found, or when an import path token is not a string — add the module, fix the spelling, or add `.wgsl`/`index.wgsl`.
**Throws:** `VGPU-WGSL-PKG-NOTFOUND` when a package or package export cannot be found. WGSL package imports resolve through `node_modules` exactly like JavaScript imports, so the message names the package and the fix:

```text
Package @vgpu/wgsl-std was not found. Install the package (npm install @vgpu/wgsl-std) or check the specifier
```

With `modules` (in-memory resolution) `node_modules` is never consulted, so the same code reports `Map it with packageMap or add the module to modules`. An unknown subpath reports `Package export ./missing was not found in <pkg>. Check the package's exports map or fix the import subpath`.

Package specifiers resolve in two steps: first by walking `node_modules` up from the importing file, so a copy in the project always wins; then, only if that fails, through Node's own resolver next to `@vgpu/wgsl`. The second step is what lets a package that reaches the project transitively — `@vgpu/wgsl-std` through `vgpu`, for example — resolve under pnpm's isolated `node_modules` and Yarn PnP, where it is installed but absent from the project's own tree.
**Throws:** `VGPU-WGSL-IMP-SELF` when the graph contains an import cycle — break the cycle.
**Throws:** `VGPU-WGSL-IMP-ORDER` when an `import` appears after declarations — move imports before declarations.
**Throws:** `VGPU-WGSL-IMP-SIDEEFFECT` for `import "x"` — import named symbols or a namespace.
**Throws:** `VGPU-WGSL-IMP-DEFAULT` for default import syntax or malformed import bindings — use `import { name } from "..."` or `import * as ns from "..."`.
**Throws:** `VGPU-WGSL-EXP-REEXPORT-CYCLE` for `export { ... }` re-export syntax — export declarations directly.
**Throws:** `VGPU-WGSL-EXP-NOTDECL` for invalid export attributes or exported declarations without a name — attach `export` to a declaration.
**Throws:** `VGPU-WGSL-SYM-NOEXPORT` when an imported binding is not exported by the target module — export it or fix the import name.
**Throws:** `VGPU-WGSL-SYM-IMPORT-SHADOW` when imports conflict with each other or shadow locals — rename with `as` or rename the local declaration.
**Throws:** `VGPU-WGSL-OVERRIDE-DUP` or `VGPU-WGSL-ENTRYPOINT-DUP` when JavaScript-visible override or entry-point names appear in multiple modules — rename one declaration.
**Throws:** `VGPU-WGSL-MANGLE-COLLISION` when canonical path hashes collide — rename one directory in either path.
**Throws:** `VGPU-WGSL-NS-NOTVALUE` or `VGPU-WGSL-NS-NOMEMBER` for invalid namespace use — access exported namespace members directly.
**Throws:** `VGPU-WGSL-MINIFY-IDENTIFIERS` when `minify.identifiers` is not `"none"` or `"safe"` — pass a valid mode.
**Throws:** `VGPU-WGSL-MINIFY-BLOCK`, `VGPU-WGSL-LEX-UNTERM-COMMENT`, or `VGPU-WGSL-LEX-UNTERM-STRING` for unterminated WGSL comments/strings during scanning/minification — close the token.
**Throws:** `VGPU-WGSL-IDENT-NONASCII` when an identifier in any resolved module contains a non-ASCII character (`let Ω = 1.0;`, `fn åhelper()`, `let café = 1.0;`) — rename it using ASCII letters, digits and `_`. WGSL itself allows Unicode (XID) identifiers; vgpu does not yet, and rejects them in the scanner so no path can half-support them (support is tracked in [issue #294](https://github.com/vercel-labs/vgpu/issues/294)). The diagnostic names the identifier and carries `line`/`column` plus `range.file` for the module that declared it. Non-ASCII text in **comments**, in string literals and in import paths stays legal, as does a leading byte-order mark; only code positions are rejected.
**Throws:** `VGPU-WGSL-NAGA-UNKNOWN` when validation is active (`"auto"` or `"require"`) and WebGPU/Naga rejects emitted WGSL — fix the WGSL reported by the diagnostic.
**Throws:** `VGPU-WGSL-VALIDATE-NO-DEVICE` in `"require"` mode when `@vgpu/adapter-node` cannot acquire a device — the error forwards adapter-node's own `fix` (and `metadata.causeCode`); run `npx vgpu doctor`, or use `"auto"`/`"off"` on machines without a GPU.
**Throws:** `VGPU-WGSL-VALIDATE-ADAPTER-MISSING` in `"require"` mode when `@vgpu/adapter-node` (an optional peer dependency) cannot be imported — install it (`pnpm add -D @vgpu/adapter-node`), or pass `validate: "off"`.
**Throws:** `VGPU-WGSL-VALIDATE-ENV-INVALID` when `VGPU_VALIDATE` is set to anything other than `off`, `auto`, or `require` — unset it or use one of those values.

**Diagnostic:** `VGPU-WGSL-RESERVED-IDENT` (severity `error`, never thrown) for every declared identifier that WGSL reserves — struct names, struct members, type aliases, module-scope variables, overrides, functions, parameters, and local variables. Each diagnostic carries the offending name, `line`/`column`, and a `range` pointing at the source module. Rename the declaration; Dawn/Tint would otherwise reject the shader only at pipeline creation.

## Examples

```ts
import { resolveShader } from "@vgpu/wgsl/runtime";

const resolved = await resolveShader({
  entry: "/entry.wgsl",
  validate: false,
  modules: {
    "/math.wgsl": `
export fn tint(value: vec3f) -> vec3f {
  return value * vec3f(1.0, 0.5, 0.25);
}
`,
    "/entry.wgsl": `
import { tint } from "./math.wgsl";

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(tint(vec3f(1.0)), 1.0);
}
`,
  },
});

console.log(resolved.wgsl.includes("fs_main"));
```

```ts
import { resolveShader } from "@vgpu/wgsl/runtime";

const resolved = await resolveShader({
  entry: "/entry.wgsl",
  validate: false,
  minify: { whitespace: true },
  modules: {
    "/types.wgsl": `
export struct NoiseConfig { seed: u32 }
export fn noise(cfg: NoiseConfig) -> f32 { return f32(cfg.seed); }
`,
    "/entry.wgsl": `
import { NoiseConfig, noise } from "./types.wgsl";

@group(0) @binding(0) var<uniform> cfg: NoiseConfig;

@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(noise(cfg));
}
`,
  },
});

console.log(resolved.deps.length);
```

## Notes

* Imported modules are pure: no `@group`/`@binding` in any non-entry module. Export structs, aliases, constants, and functions from modules; declare uniforms/storage/textures/samplers only in the entry.
* `resolveShader()` is for setup, tests, loaders, and build tooling. Do not call it per frame; resolve and create pipelines off the render hot path.
* Declaration-level DCE always runs before validation/minification when entry points exist. There is no DCE opt-out in this release.
* `minify: true` is the production preset. Safe identifier minification is conservative and does not rename entry points, resources, overrides, structs, fields, import/export names, attributes, builtins, or predeclared WGSL names.
* Reserved-word diagnostics are collected per loaded module before emission, so imported modules report their own file/line. `import`/`export`/`from`/`as` module syntax is exempt; only declared identifiers are checked.
* Validation maps diagnostics back to generated module headers. Columns can be approximate when substituted identifiers appear; those diagnostics include `VGPU-WGSL-COL-APPROX` metadata.
* **`wgslWebpackLoader` and `wgslVitePlugin` always pass `validate: false` and expose no `validate` option** — the bundler build never runs this device-backed check, for leaf files or import graphs. `opts.validate` above only takes effect when you call `resolveShader()` yourself (setup scripts, tests, tooling) or through the CLI. The gate for a Next.js/Vite app is `npx vgpu check --require-validation <file>` run in CI or pre-commit, not the dev server or the production build. See `wgslWebpackLoader`, `wgslVitePlugin`, and `npx vgpu docs cat cli.docs.md`.
* **Works from ESM and CommonJS:** the `./runtime` subpath's `exports` map in `@vgpu/wgsl` declares both an `import` and a `require` condition, both pointing at the same ESM file. `import { resolveShader } from "@vgpu/wgsl/runtime"` works as expected, and so does `const { resolveShader } = require("@vgpu/wgsl/runtime")` from a plain CommonJS entry point (no `"type": "module"`, no `.mjs`/`.mts` rename needed) — Node resolves the `require` condition and loads the ESM file through its native `require(esm)` support. See the [no-bundler guide](/docs/guides/no-bundler) for the full script-from-disk workflow.
* **See also:** `compile`, `ShaderSource`, `wgslVitePlugin`, `wgslWebpackLoader`, `@vgpu/wgsl-std/hash`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: ResolvedShader and ShaderSource
description: Data shapes returned or consumed by the WGSL helpers. Use `ResolvedShader` for `compile()` output and `ShaderSource` for loader-emitted `.wgsl` modules.
---

# ResolvedShader and ShaderSource



## Import

```ts
import type { ResolvedShader, ShaderSource, SourceMap, WGSLAst, WGSLSource } from "@vgpu/wgsl";
```

## Signature

```ts
interface ShaderSource {
  readonly version: 1;
  readonly wgsl: string;
}

interface WGSLSource {
  readonly text: string;
  readonly path?: string;
  readonly imports?: readonly { readonly path: string; readonly from: string }[];
}

interface SourceMap {
  readonly version: 1;
  readonly mappings: readonly [];
}

interface WGSLAst {
  readonly version: 1;
  readonly modules: readonly [{ readonly path: string; readonly text: string }];
  readonly diagnostics: readonly [];
  readonly sourceMap: SourceMap;
  readonly cacheKey: Record<string, string>;
}

interface ResolvedShader {
  readonly kind: "wgsl";
  readonly wgsl: string;
  readonly source: WGSLSource;
  readonly ast: WGSLAst;
  readonly sourceMap: SourceMap;
  readonly diagnostics: readonly [];
  readonly cacheKey: Record<string, string>;
  readonly entryPoints: readonly string[];
  readonly stats: { readonly lines: number; readonly bytes: number; readonly bindGroups: number };
}
```

## Parameters

`ResolvedShader` fields:

| Param       | Type                                                   | Required | Default | Notes                                                                                                          |          |                                     |
| ----------- | ------------------------------------------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------- |
| kind        | `"wgsl"`                                               | ✔        | —       | Discriminant for WGSL shader data returned by `compile()`.                                                     |          |                                     |
| wgsl        | string                                                 | ✔        | —       | Original source string passed to `compile()`.                                                                  |          |                                     |
| source      | `WGSLSource`                                           | ✔        | —       | Runtime source metadata. `compile()` sets `text` to the input, `path` to `"<runtime>"`, and `imports` to `[]`. |          |                                     |
| ast         | `WGSLAst`                                              | ✔        | —       | Lightweight passthrough AST metadata with one runtime module and no diagnostics.                               |          |                                     |
| sourceMap   | `SourceMap`                                            | ✔        | —       | Passthrough v1 source map with empty `mappings`.                                                               |          |                                     |
| diagnostics | `readonly []`                                          | ✔        | —       | Always empty for `compile()` output.                                                                           |          |                                     |
| cacheKey    | `Record<string, string>`                               | ✔        | —       | Deterministic FNV-style key in the form `vgpu-wgsl-1:<hash>` under `default`.                                  |          |                                     |
| entryPoints | `readonly string[]`                                    | ✔        | —       | Names matched by \`@(vertex                                                                                    | fragment | compute) fn <name>\` in the source. |
| stats       | `{ lines: number; bytes: number; bindGroups: number }` | ✔        | —       | Line count, UTF-8 byte length, and `bindGroups: 0`.                                                            |          |                                     |

`ShaderSource` fields:

| Param   | Type   | Required | Default | Notes                                       |
| ------- | ------ | -------- | ------- | ------------------------------------------- |
| version | `1`    | ✔        | —       | Loader artifact version.                    |
| wgsl    | string | ✔        | —       | Plain WGSL emitted by a loader or resolver. |

**Returns:** These are TypeScript interfaces, not callables. They return nothing.

**Throws:** These type declarations throw nothing. `compile()` throws before constructing `ResolvedShader` when runtime WGSL contains a top-level import.

## Examples

```ts
import { compile, type ResolvedShader, type ShaderSource } from "@vgpu/wgsl";

const resolved: ResolvedShader = compile(`
@fragment
fn fs_main() -> @location(0) vec4f {
  return vec4f(1.0, 0.0, 0.0, 1.0);
}
`);

const source: ShaderSource = { version: 1, wgsl: resolved.wgsl };
console.log(source.version, resolved.entryPoints[0]);
```

```ts
import type { ShaderSource } from "@vgpu/wgsl";

function acceptsLoaderOutput(shader: ShaderSource): string {
  return shader.wgsl;
}

acceptsLoaderOutput({
  version: 1,
  wgsl: "@compute @workgroup_size(1) fn main() {}",
});
```

## Notes

* `ShaderSource` v1 is exactly `{ version: 1, wgsl: string }`. It has no `bindings`, reflection, layouts, or cache metadata; `bindings` is reserved for a future version bump.
* Treat `ResolvedShader` fields as read-only data. Do not patch placeholder AST internals to represent imports; use `resolveShader()` for import graphs.
* `compile()` output does not prove WGSL validity. It only packages the string and rejects top-level `import`.
* Pure-module contract for resolver graphs: imported modules may export structs/functions/constants/aliases, but no imported module may declare `@group/@binding`; declare resources only in the entry module.
* **`entryPoints` here is not reflection.** `ResolvedShader.entryPoints` (this page, `compile()`'s output) is just `readonly string[]` — names matched by a regex over `@(vertex|fragment|compute) fn <name>`, nothing more. It is a different, older, and much simpler shape than the reflection `EntryPointInfo[]` returned by `reflectSource()` and by `resolveShader()`'s `ResolvedShader.reflection.entryPoints` (`@vgpu/wgsl/runtime`), which carries `stage`, `workgroupSize`, `inputs`, `bindings`, and `samplingPairs` per entry point. If you need stage/binding/input data, reach for `reflectSource` (`npx vgpu docs cat /@vgpu/wgsl/reflect-source/reflect-source.docs.md`) or `resolveShader`, not `compile()`.
* **See also:** `compile`, `resolveShader`, `reflectSource`, `wgslVitePlugin`, `wgslWebpackLoader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Bundle
description: Main API (`vgpu`) render bundle recorded by `bundle(gpu, { target }, cb)`. Bundles freeze commands, attachment formats, sample count, and bind-group identities for static work; `FramePass.bundles()` checks signature and resource staleness (`VGPU-R3-BUNDLE-STALE`) when replaying.
---

# Bundle



## Import

```ts
import type { Bundle, BundleOptions, BundleRecorder } from "vgpu";
```

## Signature

```ts
import type { Draw, DrawCallOptions, Effect, Target, TargetSignature } from "vgpu";

interface BundleOptions {
  readonly target: Target | TargetSignature;
  readonly label?: string;
}

interface BundleRecorder {
  draw(drawable: Draw | Effect, opts?: DrawCallOptions): void;
}

interface Bundle {
  readonly id: string;
  readonly gpu: GPURenderBundle;
}
```

## Parameters

| Param                     | Type                                 | Required | Default            | Notes                                                                                                                                                                                                           |
| ------------------------- | ------------------------------------ | -------: | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bundle.opts               | `BundleOptions`                      |        ✔ | —                  | Recording options.                                                                                                                                                                                              |
| opts.target               | `Target \| TargetSignature`          |        ✔ | —                  | Formats, depth format, and sample count are recorded. Signature form is `{ colors: [...], depth?, sampleCount? }`; `colors` is required.                                                                        |
| opts.label                | `string`                             |        ✖ | `` `bundle${n}` `` | Bundle id and GPU label. Auto id increments from `bundle1`.                                                                                                                                                     |
| bundle.cb                 | `(recorder: BundleRecorder) => void` |        ✔ | —                  | Called immediately to encode commands.                                                                                                                                                                          |
| recorder.draw\.drawable   | `Draw \| Effect`                     |        ✔ | —                  | Draw or fullscreen effect to encode into the bundle.                                                                                                                                                            |
| recorder.draw\.opts       | `DrawCallOptions`                    |        ✖ | `{}`               | Counts and offsets captured in the recorded commands. `indirect` records fine — render bundle encoders support `drawIndirect`/`drawIndexedIndirect` — and the GPU re-reads the argument buffer on every replay. |
| framePass.bundles.bundles | `readonly Bundle[]`                  |        ✔ | —                  | Replayed bundles; must be created by `bundle()`.                                                                                                                                                                |

**Returns:** `bundle(gpu)` returns `Bundle` with `id` and native `gpu` render bundle; `BundleRecorder.draw()` returns `void`; `FramePass.bundles()` returns `void`.

**Throws:** `VGPU-R3-BUNDLE-STALE` when replay target formats/depth/sample count differ from the recorded signature or when a recorded draw's bound resource identity / claimed group changed after recording; `VGPU-R3-BUNDLE-INVALID` when replay receives an object not created by `bundle()`; `VGPU-BUNDLE-BLEND-CONSTANT` when recording a draw with `blendConstant` (the blend constant is render-pass state that render bundle encoders cannot set; encode such draws in a frame pass instead); `VGPU-BUNDLE-STENCIL-REF` when recording a draw whose `stencil` has `ref` (the stencil reference is likewise render-pass state; stencil state without `ref` records fine); `VGPU-SURFACE-DISPOSED` when replaying against a disposed surface; draw binding errors such as `VGPU-R1-BINDING-NEVER-SET` can throw during recording. Signature mismatch messages print both recorded and actual signature keys.

## Examples

```ts
import { init, bundle, draw, frame, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64] });
const drawable = draw(gpu, { shader: `
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1, 1, 0, 1); }
` });

const statics = bundle(gpu, { target: colorTarget, label: "static" }, (recorded) => {
  recorded.draw(drawable);
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: colorTarget }, (pass) => pass.bundles(statics));
});
```

```ts
import { init, bundle, effect, frame, surface } from "vgpu/mock";

const gpu = await init();
const canvasSurface = surface(gpu, mockCanvas());
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
let statics = bundle(gpu, { target: canvasSurface, label: "surfaceStatics" }, (recorded) => recorded.draw(shader));

canvasSurface.onResize(() => {
  statics = bundle(gpu, { target: canvasSurface, label: "surfaceStatics" }, (recorded) => recorded.draw(shader));
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: canvasSurface }, (p) => p.bundles(statics));
});

function mockCanvas(): HTMLCanvasElement {
  return {
    width: 10,
    height: 10,
    clientWidth: 10,
    clientHeight: 10,
    getContext() { return { configure() {}, unconfigure() {}, getCurrentTexture() { return { createView: () => ({}) }; } }; },
  } as unknown as HTMLCanvasElement;
}
```

```ts
import { init, bundle, clock, effect, frame, pingPong } from "vgpu/mock";

const gpu = await init();
const ping = pingPong(gpu, 32, 32);
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
const even = bundle(gpu, { target: ping.write }, (b) => b.draw(shader));
ping.swap();
const odd = bundle(gpu, { target: ping.write }, (b) => b.draw(shader));
ping.swap();

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: ping.write }, (p) => p.bundles(clock(gpu).frameCount % 2 ? odd : even));
});
```

## Signature-arm recording

`bundle(gpu, { target: { colors: ["bgra8unorm"], depth: "depth24plus", sampleCount: 4 } }, cb)` records before a target exists. This relaxes only the replay target: any resources sampled by draws still need to be set before recording. Cold signature recording creates missing pipelines synchronously, which can jank; pre-warm first with `await draw.compile(signature)` or `await effect.compile(signature)`.

For future canvas surfaces, use `navigator.gpu.getPreferredCanvasFormat()` when building the signature. A bundle recorded for `bgra8unorm` will not replay on an `rgba8unorm` surface, and the stale error prints both keys.

## Notes

* Bundles match replay targets by render signature, not size. They survive resizing the target they draw onto.
* Re-record when the bundle samples a resized target; vgpu detects the changed texture identity and reports `VGPU-R3-BUNDLE-STALE`.
* `surface.onResize(...)` fires immediately, so the same re-recording callback can initialize and refresh bundles that sample resized resources.
* Bundles freeze bind group identities, not buffer contents. Updating JS-owned packed values in-place is safe; rebinding a different texture/buffer/sampler stales the bundle.
* Draws with `blendConstant` cannot be recorded: render bundle encoders have no way to set the pass blend constant. Recording throws `VGPU-BUNDLE-BLEND-CONSTANT`; use `FramePass.draw` for those draws.
* Draws whose `stencil` has `ref` cannot be recorded either: render bundle encoders have no way to set the pass stencil reference. Recording throws `VGPU-BUNDLE-STENCIL-REF`; stencil pipeline state without `ref` records fine.
* **See also:** `FramePass.bundles`, `Draw`, `Effect`, `Surface`, `Target`, `createRenderBundle`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: clock
description: The frame clock of a gpu: elapsed time, last delta, frame count, and the manual `advance()` that lets an external ticker own the clock. Use it whenever a shader, a camera or a simulation needs time; use `advance()` when something else already owns the timeline (GSAP/Motion, an XR frame callback, a fixed-timestep loop, a deterministic replay).
---

# clock



## Import

```ts
import { clock } from "vgpu";
```

## Signature

```ts
import type { Gpu } from "vgpu";

interface Clock {
  readonly time: number;
  readonly deltaTime: number;
  readonly frameCount: number;
  advance(dtSeconds: number): void;
}

declare function clock(gpu: Gpu): Clock;
```

## Parameters

| Param             | Type     | Required | Default | Notes                                                                                                                                            |
| ----------------- | -------- | -------: | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| gpu               | `Gpu`    |        ✔ | —       | The context returned by `init()`. One clock per gpu: `clock(gpu) === clock(gpu)`.                                                                |
| advance.dtSeconds | `number` |        ✔ | —       | Seconds to move the clock forward, finite and `>= 0`. Scale it for a timescale (`dt * 0.5`), or pass a constant for a fixed timestep (`1 / 60`). |

| Property   | Type     | Default | Notes                                                                                                                                |
| ---------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| time       | `number` | `0`     | Seconds since the first frame. Advances once per frame: with the wall-clock delta, or with the value the last `advance()` was given. |
| deltaTime  | `number` | `0`     | Seconds between the last two ticks. Exactly the argument of the last `advance()` when driving the clock manually.                    |
| frameCount | `number` | `0`     | Frames opened by `frame(gpu)` / `frameLoop(gpu)`. `advance()` never counts a frame.                                                  |

**Returns:** `Clock` — a live view of this gpu's frame clock. It reads through to the clock, so a single instance can be captured outside the loop and read inside it.

**Throws:** `VGPU-GPU-DISPOSED` when `clock(gpu)` runs after `gpu.dispose()` — read the clock before disposing, or `init()` a new gpu; `VGPU-GPU-FOREIGN` when the argument was not created by `init()`; `VGPU-CLOCK-DELTA-INVALID` when `advance()` receives a value that is not a finite, non-negative number — pass elapsed seconds, e.g. `advance(1 / 60)`.

## Examples

```ts
import { init, clock, effect, frameLoop, surface } from "vgpu";

declare const canvas: HTMLCanvasElement;

const gpu = await init();
const canvasSurface = surface(gpu, canvas);
const wave = effect(gpu, `
  struct Params { time: f32 }
  @group(0) @binding(0) var<uniform> params: Params;
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0 } } });

// Automatic: every frame advances the clock with the wall-clock delta.
const time = clock(gpu);
frameLoop(gpu, (frame) => {
  wave.set({ params: { time: time.time } });
  frame.pass(canvasSurface, wave);
});
```

```ts
import { init, clock, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [64, 64] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

// Manual: a fixed timestep makes the render deterministic, run after run.
const time = clock(gpu);
for (let step = 0; step < 120; step++) {
  time.advance(1 / 60);
  frame(gpu, (currentFrame) => currentFrame.pass(scene, shader));
}
console.log(time.time, time.frameCount); // 2, 120
```

```ts
import { init, clock, frameLoop } from "vgpu/mock";

const gpu = await init();
const time = clock(gpu);

// Timescale: the same wall clock, played at half speed. Slow motion is one multiplication.
let previousMs = performance.now();
const timescale = 0.5;
frameLoop(gpu, () => {
  const nowMs = performance.now();
  time.advance(((nowMs - previousMs) / 1000) * timescale);
  previousMs = nowMs;
});
```

## Notes

* One tick per frame, manual first: `frame()` advances the clock with wall-clock time **unless** `advance()` already ran since the last frame. Calling `advance(dt)` and then `frame()` in the same tick advances exactly once, by `dt`.
* `advance()` moves `time` and `deltaTime` immediately, before any frame opens — so code that reads the clock outside a frame (input smoothing, physics substeps) sees the value it is about to render with.
* Mixing is allowed and useful: drive the clock manually while an external ticker runs, then drop back to plain `frame(gpu)` calls and the wall clock takes over again, measured from the last tick.
* The clock is not a global. It belongs to the gpu, and it is created lazily: a program that never opens a frame and never calls `clock(gpu)` never allocates it.
* There is no clock on `Frame`. Pass `clock(gpu)` (or the numbers you read from it) into render helpers instead of reaching for frame state.
* **See also:** `frame`, `frameLoop`, `Gpu`, and the guide *Driving vgpu with an external ticker*.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Compute
description: Compute pipeline created by `compute(gpu)`. It uses the same WGSL reflection and `set()` ownership rules as render draws, then `dispatch(x, y?, z?)` — or `dispatch({ indirect })` for GPU-driven counts — encodes and submits one compute pass.
---

# Compute



## Import

```ts
import type { Compute, ComputeOptions, DispatchOptions, StorageAccess, StorageBuffer, StorageOptions } from "vgpu";
```

## Signature

```ts
interface ComputeOptions {
  readonly label?: string;
  readonly set?: Record<string, unknown>;
  readonly constants?: Readonly<Record<string, number | boolean>>;
  readonly entry?: string;
}

interface DispatchOptions {
  readonly indirect: StorageBuffer | { readonly buffer: StorageBuffer; readonly offset?: number };
}

interface Compute {
  set(values: Record<string, unknown>): this;
  dispatch(x: number, y?: number, z?: number): void;
  dispatch(opts: DispatchOptions): void;
}

type StorageAccess = "read" | "read-write";

interface StorageOptions {
  readonly access?: StorageAccess;
  readonly indirect?: boolean;
}

interface StorageBuffer {
  readonly size: number;
  readonly access: StorageAccess;
  read(): Promise<ArrayBuffer>;
  write(data: BufferSource): void;
}
```

## Parameters

| Param                          | Type                                          |          Required | Default                      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------ | --------------------------------------------- | ----------------: | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| compute.source                 | `string \| ShaderSource`                      |                 ✔ | —                            | WGSL string or `ShaderSource`. Must include at least one `@compute` entry point.                                                                                                                                                                                                                                                                                                                                                                                                  |
| compute.opts                   | `ComputeOptions`                              |                 ✖ | `{}`                         | Initial compute options.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| opts.label                     | `string`                                      |                 ✖ | `"compute"`                  | Used in shader reflection, GPU labels, and error `where` fields.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| opts.set                       | `Record<string, unknown>`                     |                 ✖ | `undefined`                  | Initial `.set()` call.                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| opts.constants                 | `Readonly<Record<string, number \| boolean>>` |                 ✖ | WGSL defaults                | Constructor-only values for WGSL `override` constants, applied to the compute stage. Use them to tune workgroup size per device or workload at pipeline creation: `@workgroup_size(WG)` with `override WG: u32`. Keying (`@id(N)` → decimal string of `N`) and number/boolean conversion match `DrawOptions.constants`.                                                                                                                                                           |
| opts.entry                     | `string`                                      |                 ✖ | first `@compute` entry point | Constructor-only entry point selection when one WGSL module packs several `@compute` kernels sharing structs and bindings (e.g. emit/simulate/compact). The name must exist in the shader with the `@compute` stage. Binding visibility, bind group layouts, and the storage-aliasing preflight follow the selected entry.                                                                                                                                                        |
| compute.set.values             | `Record<string, unknown>`                     |                 ✔ | —                            | Binding values by WGSL variable name. JS values are packed; buffers/resources are bound by identity.                                                                                                                                                                                                                                                                                                                                                                              |
| compute.dispatch.x             | `number`                                      |                 ✔ | —                            | Workgroup count X passed to `dispatchWorkgroups`.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| compute.dispatch.y             | `number`                                      |                 ✖ | `1`                          | Workgroup count Y.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| compute.dispatch.z             | `number`                                      |                 ✖ | `1`                          | Workgroup count Z.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| compute.dispatch.opts.indirect | `StorageBuffer \| { buffer, offset? }`        | ✔ in the overload | —                            | GPU-driven dispatch via `dispatchWorkgroupsIndirect`: the GPU reads `[x, y, z]` workgroup counts (3 tightly packed u32, 12 bytes) from the buffer at the byte `offset` (default `0`). Use it when an earlier pass decides how much work exists — variable particle populations, stream compaction. Requires a buffer created with `storage(gpu, bytes, { indirect: true })`; `offset` must be a multiple of 4 and `offset + 12 <= size`. Cannot be combined with explicit counts. |
| storage.bytes                  | `number`                                      |                 ✔ | —                            | Byte size for a main API (`vgpu`) storage buffer.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| storage.access                 | `StorageAccess \| StorageOptions`             |                 ✖ | `"read-write"`               | Access string, or a `StorageOptions` bag with `access` and `indirect`. Stored on the resource facade and used by binding normalization.                                                                                                                                                                                                                                                                                                                                           |
| storage.access.indirect        | `boolean`                                     |                 ✖ | `false`                      | Appends the `"indirect"` buffer usage so the buffer can supply GPU-read draw/dispatch arguments.                                                                                                                                                                                                                                                                                                                                                                                  |
| storage.write.data             | `BufferSource`                                |                 ✔ | —                            | `ArrayBuffer` or `ArrayBufferView`; writes at offset `0` in the public main API (`vgpu`) type.                                                                                                                                                                                                                                                                                                                                                                                    |

**Returns:** `compute(gpu)` returns `Compute`; `set()` returns the same `Compute`; `dispatch()` returns `void` after submitting; `storage(gpu)` returns a main API (`vgpu`) `StorageBuffer`; `StorageBuffer.read()` resolves an `ArrayBuffer` copy.

**Throws:** `VGPU-RING1-UNSUPPORTED` when the shader has no `@compute` entry point; `VGPU-INDIRECT-INVALID` at dispatch time for a malformed `indirect` (neither a `StorageBuffer` nor `{ buffer, offset? }`), a buffer created without the indirect flag (use `storage(gpu, bytes, { indirect: true })`), an `offset` that is not a non-negative integer multiple of 4, counts that do not fit the buffer (`offset + 12 > size`), or `indirect` combined with explicit workgroup counts in the same call; `VGPU-CONSTANTS-INVALID` for a malformed `constants` option (non-object value, a key that matches no override in the shader — the message lists the available overrides — or a value that is neither a finite number nor a boolean), and for an override declared without a default that `constants` does not provide; `VGPU-ENTRY-INVALID` for a non-string `entry`, a name that matches no entry point in the shader, or a name whose entry point is not `@compute` — the message lists the shader's available entry points with their stages; `VGPU-R1-STORAGE-ALIASING` when the same storage buffer is bound more than once and at least one reflected binding is writable; `VGPU-R1-BINDING-NEVER-SET`, `VGPU-R1-OWNERSHIP-FLIP`, and `VGPU-R1-BINDING-INCOMPATIBLE-RESOURCE` for binding errors; `VGPU-SHADER-SOURCE-INVALID` for malformed `ShaderSource`; `TypeError` if `StorageBuffer.write()` receives a non-buffer source.

## Examples

```ts
import { init, compute, storage } from "vgpu/mock";

const gpu = await init();
const bytes = 4 * 16;
const src = storage(gpu, bytes, "read");
const dst = storage(gpu, bytes, "read-write");
src.write(new Float32Array(16));

const sim = compute(gpu, `
  @group(0) @binding(0) var<storage, read> src: array<vec4f>;
  @group(0) @binding(1) var<storage, read_write> dst: array<vec4f>;
  @compute @workgroup_size(1)
  fn cs_main(@builtin(global_invocation_id) id: vec3u) {
    dst[id.x] = src[id.x] + vec4f(1.0, 0.0, 0.0, 0.0);
  }
`, { label: "sim", set: { src, dst } });

sim.dispatch(4);
```

```ts
import { init, compute, pingPongStorage } from "vgpu/mock";

const gpu = await init();
const particles = pingPongStorage(gpu, 1024);
const step = compute(gpu, `
  @group(0) @binding(0) var<storage, read> src: array<u32>;
  @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
  @compute @workgroup_size(64)
  fn cs_main(@builtin(global_invocation_id) id: vec3u) { dst[id.x] = src[id.x]; }
`);

step.set({ src: particles.read, dst: particles.write });
step.dispatch(Math.ceil(256 / 64));
particles.swap();
```

```ts
import { init, compute, storage } from "vgpu/mock";

const gpu = await init();
const wg = 64; // tune per device or workload without editing WGSL
const data = storage(gpu, 4 * 256);
const scale = compute(gpu, `
  override WG: u32 = 64;
  @group(0) @binding(0) var<storage, read_write> data: array<f32>;
  @compute @workgroup_size(WG)
  fn cs_main(@builtin(global_invocation_id) id: vec3u) { data[id.x] = data[id.x] * 2.0; }
`, { constants: { WG: wg }, set: { data } });

scale.dispatch(Math.ceil(256 / wg));
```

One JS constant drives both the pipeline's workgroup size and the dispatch math, so retuning `wg` cannot desynchronize them.

```ts
import { init, compute, storage } from "vgpu/mock";

const gpu = await init();
const alive = storage(gpu, 4, "read");             // live-particle count, e.g. from emission/compaction
const args = storage(gpu, 12, { indirect: true }); // [x, y, z] workgroup counts
const particles = storage(gpu, 4 * 1024);

const prepare = compute(gpu, `
  @group(0) @binding(0) var<storage, read> alive: u32;
  @group(0) @binding(1) var<storage, read_write> args: array<u32, 3>;
  @compute @workgroup_size(1) fn cs_main() {
    args[0] = (alive + 63u) / 64u; args[1] = 1u; args[2] = 1u; // one workgroup per 64 live particles
  }
`, { set: { alive, args } });

const step = compute(gpu, `
  @group(0) @binding(0) var<storage, read_write> particles: array<f32>;
  @compute @workgroup_size(64)
  fn cs_main(@builtin(global_invocation_id) id: vec3u) { particles[id.x] = particles[id.x] + 0.016; }
`, { set: { particles } });

prepare.dispatch(1);               // GPU computes how much work exists
step.dispatch({ indirect: args }); // GPU reads the counts; JS never sees them
```

GPU-driven dispatch: the first pass writes the workgroup counts from GPU-side state, so the population can vary every frame without a readback stall.

## Notes

* Use explicit `dispatch(x, y, z)` when the CPU already knows stable workgroup counts. Use `dispatch({ indirect })` when a preceding GPU pass decides the count (compaction, particles), so no CPU readback is needed.
* Declare storage `read` for source-only buffers and `read-write` for state that a kernel updates. For iterative simulation, bind `pingPongStorage(gpu, ...)` read/write pairs and swap after each step instead of aliasing one writable buffer.
* `StorageBuffer.read()` is for tests, snapshots, and diagnostics; avoid awaiting it in a hot loop unless CPU synchronization is intentional.
* Use `pingPongStorage(gpu, bytes)` when a compute step reads previous state and writes next state; binding the same writable storage identity twice is rejected before dispatch.
* Bindings use compute visibility only when statically reachable from the selected compute entry point; unused declarations stay in the layout with visibility `0`.
* `constants` maps to `GPUProgrammableStage.constants` of the compute stage; the pipeline is created inside `compute(gpu)`, so recreate the compute to change them.
* Dispatch counts are forwarded to WebGPU; validate domain-specific bounds in your app.
* Write indirect counts from another compute pass (bind the same buffer as storage) or from JS via `write()`. The same option shape drives GPU-driven draws via `DrawCallOptions.indirect`.
* `storage(gpu)` creates storage buffers with `copy_src` and `copy_dst`, so they can be read back and rewritten from JS.
* **See also:** `compute`, `Draw.set`, `SharedUniforms`, `Target`, `StorageBuffer` from `vgpu/core`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Draw
description: Target-agnostic renderable shader unit created by `draw(gpu)`. It reflects WGSL bindings, caches pipelines per target format/depth/sample count, and supports geometries, explicit vertex counts, instancing, and raw group claims.
---

# Draw



## Import

```ts
import type { DepthOptions, Draw, DrawOptions, DrawCallOptions, DrawLayoutOptions, GeometryLike, StencilFaceOptions, StencilOptions } from "vgpu";
```

## Signature

```ts
import type { ShaderSource, StorageBuffer, Target, TargetSignature } from "vgpu";

type SetBag = Record<string, unknown>;

type BlendPreset = "alpha" | "additive" | "premultiplied";
interface BlendComponentOptions { readonly src: GPUBlendFactor; readonly dst: GPUBlendFactor; readonly op?: GPUBlendOperation; }
interface BlendOptions { readonly color: BlendComponentOptions; readonly alpha?: BlendComponentOptions; }

interface DepthOptions {
  readonly write?: boolean;
  readonly compare?: GPUCompareFunction;
  readonly bias?: number;
  readonly biasSlopeScale?: number;
  readonly biasClamp?: number;
}

interface StencilFaceOptions {
  readonly compare?: GPUCompareFunction;
  readonly fail?: GPUStencilOperation;
  readonly depthFail?: GPUStencilOperation;
  readonly pass?: GPUStencilOperation;
}

interface StencilOptions {
  readonly front?: StencilFaceOptions;
  readonly back?: StencilFaceOptions;
  readonly readMask?: number;
  readonly writeMask?: number;
  readonly ref?: number;
}

interface DrawOptions {
  readonly shader: string | ShaderSource;
  readonly geometry?: GeometryLike;
  readonly set?: SetBag;
  readonly label?: string;
  readonly targets?: readonly Target[];
  readonly instances?: number;
  readonly vertices?: number;
  readonly firstInstance?: number;
  readonly blend?: BlendPreset | BlendOptions;
  readonly blendConstant?: readonly [number, number, number, number];
  readonly writeMask?: readonly ("r" | "g" | "b" | "a")[];
  readonly colors?: readonly ({ readonly blend?: BlendPreset | BlendOptions; readonly writeMask?: readonly ("r" | "g" | "b" | "a")[] } | null)[];
  readonly cull?: "none" | "front" | "back";
  readonly frontFace?: "ccw" | "cw";
  readonly unclippedDepth?: boolean;
  readonly depth?: false | DepthOptions;
  readonly stencil?: StencilOptions;
  readonly multisample?: { readonly alphaToCoverage?: boolean; readonly mask?: number };
  readonly constants?: Readonly<Record<string, number | boolean>>;
  readonly entry?: { readonly vertex?: string; readonly fragment?: string };
}

interface DrawCallOptions {
  readonly target?: Target;
  readonly offsets?: readonly number[] | Partial<Record<number, readonly number[]>>;
  readonly instances?: number;
  readonly vertices?: number;
  readonly indices?: number;
  readonly firstVertex?: number;
  readonly firstIndex?: number;
  readonly baseVertex?: number;
  readonly firstInstance?: number;
  readonly indirect?: StorageBuffer | { readonly buffer: StorageBuffer; readonly offset?: number };
}

interface DrawLayoutOptions { readonly dynamicOffsets?: boolean; }

interface GeometryLike {
  readonly vertexCount?: number;
  readonly indexCount?: number;
  readonly instanceCount?: number;
  readonly vertexBuffers?: readonly GPUBuffer[];
  readonly indexBuffer?: GPUBuffer;
  readonly indexFormat?: GPUIndexFormat;
  readonly vertexBufferLayouts?: readonly GPUVertexBufferLayout[];
  readonly topology?: GPUPrimitiveTopology;
  readonly stripIndexFormat?: GPUIndexFormat;
  readonly firstVertex?: number;
  readonly firstIndex?: number;
  readonly baseVertex?: number;
}

interface Draw {
  readonly gpu: GPURenderPipeline | undefined;
  readonly targets: readonly Target[] | undefined;
  set(values: SetBag): this;
  group(n: number, bindGroup: GPUBindGroup): this;
  layout(n: number, opts?: DrawLayoutOptions): GPUBindGroupLayout;
  draw(target?: Target | DrawCallOptions): void;
  compile(target?: Target | TargetSignature): Promise<this>;
  compileSync(target?: Target | TargetSignature): this;
}
```

## Parameters

| Param                            | Type                                                              | Required | Default                                                   | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------------------- | ----------------------------------------------------------------- | -------: | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| opts.shader                      | `string \| ShaderSource`                                          |        ✔ | —                                                         | WGSL string or loader-produced `ShaderSource`. Must contain compatible vertex/fragment entry points; default names are `vs_main` and `fs_main` if reflection does not find them.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| opts.geometry                    | `GeometryLike`                                                    |        ✖ | `undefined`                                               | Supplies vertex/index buffers and layouts. Omit for generated vertex-index drawing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| opts.set                         | `Record<string, unknown>`                                         |        ✖ | `undefined`                                               | Initial `.set()` call.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| opts.label                       | `string`                                                          |        ✖ | `"draw"`                                                  | Debug/error label.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| opts.targets                     | `readonly Target[]`                                               |        ✖ | `undefined`                                               | Synchronous pre-warm sugar for the listed target signatures. In browser load paths, prefer `await draw.compile(target)`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| opts.instances                   | `number`                                                          |        ✖ | `1`                                                       | Default instance count. Integer `>= 0`; per-call `instances` overrides.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| opts.vertices                    | `number`                                                          |        ✖ | `3` for non-indexed, unless `geometry.vertexCount` exists | Default non-indexed vertex count. Ignored by indexed geometries. Integer `>= 0`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| opts.firstInstance               | `number`                                                          |        ✖ | `0`                                                       | Default first instance. Integer `>= 0`; per-call `firstInstance` overrides.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| opts.blend                       | `"alpha" \| "additive" \| "premultiplied" \| BlendOptions`        |        ✖ | `undefined`                                               | Constructor-only blend state applied uniformly to every color target. Presets resolve at construction; explicit components use `src`/`dst` and optional `op` (`"add"` default). Omitted `alpha` copies `color`.                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| opts.blendConstant               | `readonly [number, number, number, number]`                       |        ✖ | whatever the pass holds — `(0, 0, 0, 0)` at pass start    | Scales `"constant"`/`"one-minus-constant"` blend factors. Use it to fade or crossfade a whole layer per draw without touching per-vertex alpha. Encoder state, not pipeline state (see Notes). Components must be finite; values outside `[0, 1]` are legal. When omitted, no `setBlendConstant` is emitted for this draw, so constant factors read the current pass value: the `(0, 0, 0, 0)` default at the start of the pass, or the value an earlier draw in the same pass set, which persists until the next set. At least one color target's effective blend (`colors[i].blend` when that target has one, else the top-level `blend`) must use a constant factor. |
| opts.writeMask                   | `readonly ("r" \| "g" \| "b" \| "a")[]`                           |        ✖ | all channels                                              | Constructor-only color channel mask applied uniformly to every color target. Omit to write RGBA; `[]` writes no channels; `["r","g","b"]` skips alpha.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| opts.colors                      | `readonly ({ blend?, writeMask? } \| null)[]`                     |        ✖ | `undefined`                                               | Per-attachment blend/writeMask overrides for MRT — the deferred-shading case, where one draw writes a G-buffer (`target(gpu, { colors: [...] })`) and each attachment needs different state. One entry per color attachment, aligned by index. Inheritance is per field: `null` entries and omitted fields fall back to the top-level `blend`/`writeMask`; `{ writeMask: [] }` leaves that attachment untouched.                                                                                                                                                                                                                                                        |
| opts.cull                        | `"none" \| "front" \| "back"`                                     |        ✖ | `"none"`                                                  | Skips rasterizing triangles that face away from the chosen side. `"back"`: culls faces pointing away from the viewer; on a closed geometry they are never visible, so the GPU skips roughly half the fragment work. `"front"`: culls faces toward the viewer; used when rendering shadow maps to reduce peter panning. When omitted, no triangles are culled — required for open geometry such as foliage cards.                                                                                                                                                                                                                                                        |
| opts.frontFace                   | `"ccw" \| "cw"`                                                   |        ✖ | `"ccw"`                                                   | Winding order that counts as front-facing — the reference `cull` works against. Set `"cw"` for geometry authored clockwise, or for draws with a negative (mirrored) scale, which flips the on-screen winding. When omitted, counter-clockwise triangles are front.                                                                                                                                                                                                                                                                                                                                                                                                      |
| opts.unclippedDepth              | `boolean`                                                         |        ✖ | `false`                                                   | Disables depth clipping so geometry outside `[near, far]` rasterizes instead of vanishing. Use it for shadow-map pancaking: casters behind the light's near plane flatten onto it instead of being clipped out of the map. Requires the `"depth-clip-control"` device feature. When omitted or `false`, standard clipping applies.                                                                                                                                                                                                                                                                                                                                      |
| opts.depth                       | `false \| DepthOptions`                                           |        ✖ | `{ write: true, compare: "less-equal" }`                  | Depth test/write state for targets with a depth attachment; fields are in the `DepthOptions` table below. `false` disables depth testing for overlays and gizmos that must draw over the scene regardless of distance. When omitted, nearer fragments win and coplanar re-draws still pass; ignored when the target has no depth.                                                                                                                                                                                                                                                                                                                                       |
| opts.stencil                     | `StencilOptions`                                                  |        ✖ | WebGPU pass-through defaults                              | Masks draws to marked screen regions using the stencil aspect of the depth attachment — portals, mirrors, object outlines, masked UI. Requires a target depth format with a stencil aspect (`depth: "depth24plus-stencil8"`). Fields are in the `StencilOptions` table below.                                                                                                                                                                                                                                                                                                                                                                                           |
| opts.multisample                 | `{ alphaToCoverage?, mask? }`                                     |        ✖ | `{ alphaToCoverage: false, mask: 0xFFFFFFFF }`            | MSAA state. `alphaToCoverage`: turns fragment alpha into a per-sample coverage mask, so alpha-tested foliage antialiases in any draw order — no blending, no transparency sorting; requires an `msaa: true` target. `mask`: bitmask of samples the draw may write — a niche debugging tool most draws never set. Only the low `sampleCount` bits matter; higher bits are legal and ignored.                                                                                                                                                                                                                                                                             |
| opts.constants                   | `Readonly<Record<string, number \| boolean>>`                     |        ✖ | the WGSL defaults                                         | Values for WGSL `override` constants, fixed at pipeline creation. Use them to specialize one shader — quality tiers, feature toggles, workgroup-size tuning — without string-templating the WGSL. Key by override name, or by the decimal string of `N` when the declaration has `@id(N)` (the name is not usable then). Booleans become `1`/`0`; every override declared without a default must be provided.                                                                                                                                                                                                                                                           |
| opts.entry                       | `{ vertex?: string; fragment?: string }`                          |        ✖ | first entry point of each stage                           | Selects which `@vertex`/`@fragment` functions to compile when one WGSL module declares several — variants of one technique sharing helpers, such as depth-only and shaded passes from the same source. Names must exist in the shader with the matching stage. Omitted fields keep the first entry point of that stage.                                                                                                                                                                                                                                                                                                                                                 |
| draw\.set.values                 | `Record<string, unknown>`                                         |        ✔ | —                                                         | Values keyed by WGSL binding variable name. JS objects/numbers are packed; resources are bound by identity.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| draw\.group.n                    | `number`                                                          |        ✔ | —                                                         | Bind group index to claim for manual bind-group binding (`group(n, bindGroup)`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| draw\.group.bindGroup            | `GPUBindGroup`                                                    |        ✔ | —                                                         | Must be compatible with `draw.layout(n)` or `draw.layout(n, { dynamicOffsets: true })`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| draw\.layout.n                   | `number`                                                          |        ✔ | —                                                         | Reflected bind group index.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| draw\.layout.opts.dynamicOffsets | `boolean`                                                         |        ✖ | `false`                                                   | When `true`, returns/reuses a layout whose buffer entries have `hasDynamicOffset: true` and clears cached pipelines.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| draw\.draw\.target               | `Target \| DrawCallOptions`                                       |        ✖ | `{}`                                                      | One-shot draw options. Pass a bare target for the common case, or an options bag when setting counts or offsets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| opts.target                      | `Target`                                                          |        ✖ | —                                                         | Required at runtime when an options bag is used. Use a `Surface` or an offscreen `Target`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| opts.offsets                     | `readonly number[] \| Partial<Record<number, readonly number[]>>` |        ✖ | Reflected/claimed fallback offsets                        | Dynamic offsets for claimed/dynamic groups. Array applies to every group; object keys by group.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| opts.instances                   | `number`                                                          |        ✖ | `DrawOptions.instances ?? geometry.instanceCount ?? 1`    | Per-call instance count; integer `>= 0`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| opts.vertices                    | `number`                                                          |        ✖ | `geometry.vertexCount ?? DrawOptions.vertices ?? 3`       | Per-call non-indexed vertex count; indexed geometries use `geometry.indexCount`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| opts.firstVertex                 | `number`                                                          |        ✖ | `0`                                                       | Non-indexed first vertex; indexed geometries use firstIndex/baseVertex `0`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| opts.firstInstance               | `number`                                                          |        ✖ | `DrawOptions.firstInstance ?? 0`                          | Per-call first instance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| opts.indirect                    | `StorageBuffer \| { buffer, offset? }`                            |        ✖ | `undefined`                                               | GPU-driven draw: the GPU reads the draw arguments from the buffer at byte `offset` (default `0`) instead of CPU-side counts. Use it when a culling compute pass decides what to draw — the arguments are written on the GPU and the CPU never round-trips. Create the buffer with `storage(gpu, bytes, { indirect: true })`; argument layouts are in Notes.                                                                                                                                                                                                                                                                                                             |

**`DepthOptions`** — the object form of `opts.depth`:

| Field          | Type                 | Required | Default        | Notes                                                                                                                                                                                                                                                            |
| -------------- | -------------------- | -------: | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| write          | `boolean`            |        ✖ | `true`         | `false` keeps testing against the depth buffer without writing it. Use it for blended transparents and decals, which must hide behind opaque geometry but not occlude what draws after them.                                                                     |
| compare        | `GPUCompareFunction` |        ✖ | `"less-equal"` | Comparison a fragment must pass against the stored depth. `"greater"` plus `clearDepth: 0` on the pass gives reversed-Z, which spreads floating-point precision evenly across the view distance.                                                                 |
| bias           | `number`             |        ✖ | `0`            | Constant offset added to each fragment's depth. Must be an integer (WebGPU `depthBias` is `i32`). A small positive bias while rendering the shadow map removes shadow acne; a small negative bias lets coplanar decals win the depth test instead of z-fighting. |
| biasSlopeScale | `number`             |        ✖ | `0`            | Extra bias proportional to the polygon's depth slope. Surfaces at glancing angles need more offset than facing ones — pair it with `bias` to remove acne on sloped ground.                                                                                       |
| biasClamp      | `number`             |        ✖ | `0` (no clamp) | Upper bound on the total bias. Caps runaway slope-scaled bias on near-edge-on triangles, which otherwise detaches shadows from their casters (peter panning).                                                                                                    |

**`StencilOptions`** — the fields of `opts.stencil`:

| Field     | Type                 | Required | Default                                              | Notes                                                                                                                                                                                                                        |
| --------- | -------------------- | -------: | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| front     | `StencilFaceOptions` |        ✖ | `{ compare: "always", fail/depthFail/pass: "keep" }` | Test and operations for front-facing triangles; fields are in the `StencilFaceOptions` table below.                                                                                                                          |
| back      | `StencilFaceOptions` |        ✖ | mirrors the normalized `front`                       | Give it explicitly when the two sides must differ — incrementing on front faces and decrementing on back faces to count volume crossings. With `back` given and `front` omitted, the front keeps the WebGPU face defaults.   |
| readMask  | `number`             |        ✖ | `0xFFFFFFFF`                                         | Bits of the stored stencil value visible to `compare`. Integer in `[0, 0xFFFFFFFF]`.                                                                                                                                         |
| writeMask | `number`             |        ✖ | `0xFFFFFFFF`                                         | Bits the face operations may change. Integer in `[0, 0xFFFFFFFF]`. Disjoint masks let several effects share one stencil buffer.                                                                                              |
| ref       | `number`             |        ✖ | pass default `0`                                     | Value `compare` tests against and `"replace"` writes. Integer in `[0, 0xFFFFFFFF]`. Encoder state, not pipeline state (see Notes); an explicit `0` still emits, restoring the pass default after an earlier draw changed it. |

**`StencilFaceOptions`** — the `front` / `back` faces:

| Field     | Type                  | Required | Default    | Notes                                                                                                                                                    |
| --------- | --------------------- | -------: | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| compare   | `GPUCompareFunction`  |        ✖ | `"always"` | Comparison between the masked `ref` and the masked stored value. `"equal"` draws only inside a previously marked region — the portal or mirror interior. |
| fail      | `GPUStencilOperation` |        ✖ | `"keep"`   | Operation when the stencil comparison fails.                                                                                                             |
| depthFail | `GPUStencilOperation` |        ✖ | `"keep"`   | Operation when the stencil comparison passes but the depth test fails.                                                                                   |
| pass      | `GPUStencilOperation` |        ✖ | `"keep"`   | Operation when both comparisons pass. `"replace"` writes `ref`, marking the region for later draws.                                                      |

**Returns:** `draw(gpu)` returns `Draw`; `set()`, `group()`, and `compileSync()` return the same `Draw`; `layout()` returns a `GPUBindGroupLayout`; one-shot `draw()` returns `void`; `compile()` returns `Promise<this>`.

**Throws:** one `VGPU-*` code per condition below. Checks marked † resolve against the target signature at compile/draw time; with `targets: [...]` they surface from `draw(gpu)` itself, because that option compiles at construction.

* `VGPU-LIMIT-STORAGE-VERTEX` / `VGPU-LIMIT-STORAGE-FRAGMENT` — static storage-buffer use by a selected entry point exceeds the granted stage limit. Request the supported `requiredLimits` value, or reduce/move the storage data.
* `VGPU-TARGET-REQUIRED` — `draw.draw()` was called without a target and the draw has none to fall back on. Pass a `Target`, or an options bag with `target`.
* `VGPU-BLEND-INVALID` — unknown blend preset or malformed blend object. Use `"alpha"`, `"additive"`, `"premultiplied"`, or `{ color: { src, dst, op? }, alpha? }`.
* `VGPU-BLEND-CONSTANT-INVALID` — `blendConstant` is not exactly four finite numbers, or no color target's effective blend uses a `"constant"`/`"one-minus-constant"` factor (the value could never apply). The effective blend of a target is its `colors[i].blend` when it has one, else the top-level `blend` — so a top-level constant factor overridden on *every* target is still dead, while a constant factor reached only through `colors[i].blend` is live. Fix the tuple, or add a constant factor to a blend that survives the per-target overrides.
* `VGPU-WRITEMASK-INVALID` — `writeMask` is not an array, or contains a channel outside `"r"`/`"g"`/`"b"`/`"a"`.
* `VGPU-COLORS-INVALID` — `colors` is not an array; an entry is neither `null` nor `{ blend?, writeMask? }`; or † its length differs from the target's color attachment count (both counts are in the message). Give one entry per attachment.
* `VGPU-CULL-INVALID` — `cull` is outside `"none"`/`"front"`/`"back"`.
* `VGPU-FRONTFACE-INVALID` — `frontFace` is outside `"ccw"`/`"cw"`.
* `VGPU-UNCLIPPED-DEPTH-INVALID` — `unclippedDepth` is not a boolean, or is `true` on a device whose `features` lacks `"depth-clip-control"`. Request the feature with `init({ requiredFeatures: ["depth-clip-control"] })` on an adapter that supports it.
* `VGPU-DEPTH-INVALID` — non-boolean `write`; unknown `compare`; non-integer `bias`; non-finite bias values; a nonzero bias value with a `line-*`/`point-*` topology (depth bias is only defined for triangles); or a nonzero `biasClamp` on a compatibility-mode device. Zero the offending field.
* `VGPU-STENCIL-INVALID` — malformed `stencil` (non-object value, malformed `front`/`back` face, unknown `compare` or `fail`/`depthFail`/`pass` operation, or `readMask`/`writeMask`/`ref` outside integer `[0, 0xFFFFFFFF]`); or † any stencil state against a depth format without a stencil aspect. Create the target with `depth: "depth24plus-stencil8"`.
* `VGPU-MULTISAMPLE-INVALID` — malformed `multisample` (non-object value, non-boolean `alphaToCoverage`, or a `mask` outside integer `[0, 0xFFFFFFFF]`); or † `alphaToCoverage: true` against a non-MSAA signature. Create the target with `msaa: true`.
* `VGPU-CONSTANTS-INVALID` — non-object `constants`; a key that matches no override in the shader (the message lists the available ones); a value that is neither a finite number nor a boolean; or an override declared without a default that `constants` does not provide. Add `constants: { "<nameOrId>": value }`.
* `VGPU-ENTRY-INVALID` — non-object `entry` or a non-string `vertex`/`fragment` field; a name that matches no entry point; or a name whose entry point has the wrong stage. The message lists the shader's entry points with their stages.
* `VGPU-R1-DRAW-COUNT` — a count field is not an integer `>= 0`. Use `0` only for a deliberate no-op draw.
* `VGPU-INDIRECT-INVALID` — at call time: `indirect` is neither a `StorageBuffer` nor `{ buffer, offset? }`; the buffer was created without the indirect flag (use `storage(gpu, bytes, { indirect: true })`); `offset` is not a non-negative integer multiple of 4; the arguments overrun the buffer (the message shows the byte math); or `indirect` is combined with `vertices`/`indices`/`instances`/`firstVertex`/`firstIndex`/`baseVertex`/`firstInstance` — the GPU reads those from the buffer, so drop the CPU-side value.
* `VGPU-R1-BINDING-NEVER-SET` — a reflected binding was never provided before drawing. `set()` the named binding, or claim its group with `group(n, bindGroup)`.
* `VGPU-R1-OWNERSHIP-FLIP` — a binding switched between JS-value ownership and resource ownership across `set()` calls. Keep passing the kind its first `set()` used.
* `VGPU-R1-BINDING-INCOMPATIBLE-RESOURCE` — a `set()` value does not satisfy the binding; the message names the binding and what it needs.
* `VGPU-SET-TEXTURE-FILTERABILITY` — a facade texture format cannot satisfy an ordinarily sampled `float` binding (detail identifies the format, texture, and paired sampler). Use a filterable format, request `float32-filterable`, or rewrite to `textureLoad`.
* `VGPU-R4-GROUP-CLAIMED` — `set()` tried to update a claimed group. Call `set()` before claiming, or keep updating the group yourself from `draw.layout(n)`.
* `VGPU-R4-GROUP-INCOMPATIBLE` — a claimed bind group does not match the draw's layout. Build it from `draw.layout(n, { dynamicOffsets? })` before calling `group(n, bindGroup)`.
* `VGPU-R4-GROUP-VALIDATION` — WebGPU rejected a claimed group at draw time; delivered asynchronously through `gpu.onError`. Build the group from `draw.layout(n)` and pass offsets via the draw call.
* `VGPU-SHADER-SOURCE-INVALID` — malformed `ShaderSource`. Pass WGSL text or a loader-produced `{ version, wgsl }` object.

## Examples

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64] });
const tri = draw(gpu, {
  label: "tri",
  targets: [colorTarget],
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0, 1, 0, 1); }
  `,
});

tri.draw({ target: colorTarget, vertices: 3, instances: 1 });
```

Backface culling for a closed imported geometry:

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
const statue = { vertexCount: 36 }; // closed geometry; vertex data omitted for brevity
const opaque = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi % 3u], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.8, 0.8, 0.7, 1); }
  `,
  geometry: statue,
  cull: "back",    // closed geometry: faces pointing away are never visible
  frontFace: "cw", // the importer produced clockwise triangles
});
opaque.draw(scene);
```

Culling back faces skips roughly half the fragment work on the closed statue, and `frontFace: "cw"` keeps the imported clockwise winding — or a negative-scale mirror — counting as front-facing.

Shadow map with depth bias and pancaking:

```ts
import { init, createMockAdapter, draw, target } from "vgpu/mock";

const gpu = await init({
  adapter: createMockAdapter({ features: ["depth-clip-control"] }),
  requiredFeatures: ["depth-clip-control"],
});
const shadowMap = target(gpu, { size: [1024, 1024], depth: true });
const casters = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0); }
  `,
  // Nudge stored depth away from the light to stop shadow acne.
  depth: { bias: 2, biasSlopeScale: 2 },
  // Pancake casters behind the light's near plane instead of clipping them away.
  unclippedDepth: true,
});
casters.draw(shadowMap);
```

The bias pair keeps lit surfaces acne-free, and `unclippedDepth` flattens casters between the light and its near plane onto the map instead of losing their shadows.

MRT decal into a G-buffer:

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
// Deferred-shading G-buffer: albedo + world-space normals.
const gbuffer = target(gpu, {
  size: [512, 512],
  colors: [{ format: "rgba8unorm" }, { format: "rgba16float" }],
  depth: true,
});
const decal = draw(gpu, {
  shader: `
    struct Frag { @location(0) albedo: vec4f, @location(1) normal: vec4f }
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> Frag { return Frag(vec4f(0.6, 0.1, 0.1, 0.8), vec4f(0, 1, 0, 0)); }
  `,
  colors: [
    { blend: "alpha" },   // blend the decal into the albedo
    { writeMask: [] },    // leave the normals untouched
  ],
  depth: { write: false }, // the decal sits on existing geometry
});
decal.draw(gbuffer);
```

One draw blends the decal into `gbuffer.colors[0]` while `{ writeMask: [] }` leaves the normals exactly as the opaque pass wrote them — the fragment still outputs `@location(1)`, the mask only blocks the write.

Alpha-tested foliage without transparency sorting:

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true, msaa: true });
const foliage = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    // In a real scene, alpha comes from the leaf texture.
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.1, 0.5, 0.1, 0.4); }
  `,
  multisample: { alphaToCoverage: true },
});
foliage.draw(scene);
```

Fragment alpha becomes per-sample coverage, so leaf edges antialias in any draw order — no blending, no transparency sorting.

Stencil-masked portal:

```ts
import { init, draw, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: "depth24plus-stencil8" });
const SHADER = `
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.2, 1, 1); }
`;
// Mark the portal's pixels with stencil value 1; write no color, no depth.
const portalMask = draw(gpu, { shader: SHADER, writeMask: [], depth: false, stencil: { front: { pass: "replace" }, ref: 1 } });
// Draw the far world only where the mask matches.
const otherWorld = draw(gpu, { shader: SHADER, stencil: { front: { compare: "equal" }, ref: 1 } });

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene }, (pass) => {
    pass.draw(portalMask);
    pass.draw(otherWorld);
  });
});
```

The first draw marks the portal region in the stencil buffer; the second renders only where the stored value equals `ref`, inside one pass so the mask survives.

Per-draw layer fade with the blend constant:

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [128, 128] });
const overlay = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1, 0.5, 0, 1); }
  `,
  // Weight the whole layer by the blend constant, not per-vertex alpha.
  blend: { color: { src: "constant", dst: "one-minus-constant" } },
  blendConstant: [0.25, 0.25, 0.25, 0.25], // the layer shows at 25%
});
overlay.draw(colorTarget);
```

The whole overlay fades with one per-draw value — no per-vertex alpha rewrite, and no extra pipeline, because the constant is encoder state.

Pipeline specialization with `constants` and `entry`:

```ts
import { init, draw, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [128, 128] });
// One module, two fragment variants sharing the vertex stage and helpers.
const SOURCE = `
  override STEPS: u32 = 8;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_shaded() -> @location(0) vec4f { return vec4f(f32(STEPS) / 64.0, 0, 0, 1); }
  @fragment fn fs_flat() -> @location(0) vec4f { return vec4f(0.5, 0.5, 0.5, 1); }
`;
const hero = draw(gpu, { shader: SOURCE, constants: { STEPS: 64 } });        // high quality tier
const backdrop = draw(gpu, { shader: SOURCE, entry: { fragment: "fs_flat" } }); // cheap variant

hero.draw(colorTarget);
backdrop.draw(colorTarget);
```

Both draws compile from the same module: `constants` specializes the shaded variant at pipeline creation instead of string-templating WGSL, and `entry` picks the flat fragment for the backdrop.

GPU-driven draw with `indirect`:

```ts
import { init, compute, draw, storage, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
// drawIndirect arguments: vertexCount, instanceCount, firstVertex, firstInstance.
const args = storage(gpu, 16, { indirect: true });
const cullPass = compute(gpu, `
  @group(0) @binding(0) var<storage, read_write> args: array<u32, 4>;
  @compute @workgroup_size(1) fn cs_main() {
    args = array<u32, 4>(3u, 1u, 0u, 0u); // survivors of the culling test
  }
`);
cullPass.set({ args });
const grass = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0, 0.6, 0, 1); }
  `,
});
cullPass.dispatch(1);                          // the GPU decides the counts
grass.draw({ target: scene, indirect: args }); // the CPU never reads them back
```

The compute pass writes the draw arguments and the draw consumes them on the GPU — the culling result never round-trips through the CPU.

## Pipeline pre-warm

`draw.compile(target)` asynchronously prepares one target signature and resolves to the same draw. `draw.compileSync(target)` prepares the same signature synchronously; if an async compile for that signature is still pending, the synchronous result wins the race and unblocks later draws. Both methods also accept a target signature object such as `{ colors: ["bgra8unorm"], depth: "depth24plus", sampleCount: 4 }`; `colors` is required and bare strings are rejected.

Each color/depth/sample-count variant is a different pipeline. A missed variant sync-compiles on first use, which can jank; fire-and-forget pre-warms should always use `.catch(...)` or `gpu.onError`/`gpu.settled()` will not observe the returned promise rejection. `targets: [target]` is kept as creation-time `compileSync()` sugar for non-browser hot paths.

## Notes

* Choose blend by use case: omit it for opaque geometry; use `"alpha"`/`"premultiplied"` for ordinary composition and `"additive"` for glow. Reserve explicit equations for special effects. `blendConstant` is persistent pass state (not pipeline state and not bundle state), so set it when fading or crossfading a layer.
* For MRT, use `colors[i]` to inherit or override blend/write masks per attachment. Use `cull: "back"` on closed geometries, `"none"` on foliage/cards, and `"front"` for shadow passes; pair negative scales with `frontFace: "cw"`. Disable depth for overlays and depth writes for transparent/decals. Stencil needs a depth-stencil target; `multisample.alphaToCoverage` is for alpha-tested foliage with MSAA, not general blending.
* Use indirect draws when compute produces arguments in storage buffers marked `{ indirect: true }`; this avoids CPU readback and keeps culling GPU-driven.
* Count precedence is per-call option, then draw option, then geometry/default. `instances: 0` and `vertices: 0` are valid no-op draws.
* Blend, write masks, `colors`, `cull`, `frontFace`, `unclippedDepth`, `depth`, `stencil` (all but `ref`), `multisample`, `constants`, and `entry` are immutable pipeline state, fixed at `draw(gpu)`; draws that differ in any of them compile distinct pipelines. Absent options and their explicit no-op spellings — `unclippedDepth: false`, `multisample: {}`, `constants: {}`, an all-defaults `stencil`, `entry` naming the first-of-stage functions — keep byte-identical descriptors and cache keys.
* WebGPU mapping: `cull`/`frontFace`/`unclippedDepth` → `GPUPrimitiveState`; `depth` → `GPUDepthStencilState` (`write` → `depthWriteEnabled`, `compare` → `depthCompare`, the bias family → `depthBias`/`depthBiasSlopeScale`/`depthBiasClamp`); `stencil` → its stencil members (`fail` → `failOp`, `depthFail` → `depthFailOp`, `pass` → `passOp`); `multisample` → `GPUMultisampleState`, with the sample `count` always taken from the target's `sampleCount`; `constants` → `GPUProgrammableStage.constants` on both stages.
* `depth: false` compiles `{ depthWriteEnabled: false, depthCompare: "always" }` because WebGPU cannot omit depth state when the pass has a depth attachment. `stencil` merges into the same depth-stencil state; stencil without a `depth` option keeps the depth defaults.
* `unclippedDepth` disables clipping only; fragment depth is still clamped to the viewport `[minDepth, maxDepth]` range at output.
* With `alphaToCoverage` on, WebGPU additionally requires the first color target to be blendable with an alpha channel and forbids a fragment `sample_mask` output; native validation reports those.
* WebGPU matches `constants` keys against the module's override declarations, not per entry point, so one record serves both stages even when an override is referenced by only one of them.
* `entry` selection happens at construction: binding visibility, bind group layouts, vertex input layouts (the selected vertex entry's inputs drive geometry attribute matching), and storage-stage limit checks all reflect the chosen variant. Unused declarations keep visibility `0` in reflected layouts, so build claimed bind groups from `draw.layout(n)` rather than guessing a raw layout.
* `blendConstant` and `stencil.ref` are encoder state: emitted as `setBlendConstant`/`setStencilReference` after `setPipeline` and before the draw, so draws that differ only in them share pipelines. Both are *pass* state and persist: a value set by one draw stays in effect for every later draw in the same pass that does not set its own. The `(0, 0, 0, 0)` blend-constant default therefore only holds until the first draw in the pass sets one — pass `blendConstant` explicitly on any draw in a pass that must not inherit a previous draw's value (`stencil.ref` behaves the same, and an explicit `0` re-emits). Render bundle encoders cannot set render-pass state, so `bundle()` rejects such draws with `VGPU-BUNDLE-BLEND-CONSTANT`/`VGPU-BUNDLE-STENCIL-REF`; encode them in a frame pass instead.
* Blend presets: `"alpha"` uses source alpha over, `"premultiplied"` uses premultiplied source over, and `"additive"` uses one-plus-one additive blending for color and alpha. In explicit blends, `op` defaults to `"add"` and omitted `alpha` copies `color`.
* `indirect` argument layouts: a non-indexed geometry (or no geometry) encodes `drawIndirect` — 4 u32 values, `vertexCount, instanceCount, firstVertex, firstInstance` (16 bytes); an indexed geometry still sets its index buffer and encodes `drawIndexedIndirect` — 5 32-bit values, `indexCount, instanceCount, firstIndex, baseVertex (signed), firstInstance` (20 bytes). Write them from a compute shader (bind the same buffer as storage) or from JS via `write()`. Indirect draws record fine into `bundle()`: `drawIndirect`/`drawIndexedIndirect` exist on render bundle encoders.
* A non-zero `firstInstance` inside the buffered indirect arguments silently turns the draw into a no-op unless the device has the `"indirect-first-instance"` feature. The value lives on the GPU, so vgpu cannot validate it — request the feature with `init({ requiredFeatures: ["indirect-first-instance"] })` when you need it.
* One-shot `draw.draw()` has no implicit target and returns `void`; raw claimed-group validation errors are delivered through `gpu.onError`, and tests can `await gpu.settled()`.
* Changing resource identity after a draw is recorded in a `Bundle` marks that bundle stale; changing JS values in-place does not.
* **See also:** `Effect`, `FramePass.draw`, `Bundle`, `Surface`, `Target`, `SharedUniforms`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Effect
description: Fullscreen-fragment render unit created by `effect(gpu)`. Use it for post-processing, gradients, blurs, and screen/target copies; use `draw(gpu)` for meshes, vertex buffers, instancing, or explicit vertex counts.
---

# Effect



## Import

```ts
import type { Effect, EffectOptions } from "vgpu";
```

## Signature

```ts
import type { DrawCallOptions, Target, TargetSignature } from "vgpu";

type SetBag = Record<string, unknown>;

type BlendPreset = "alpha" | "additive" | "premultiplied";
interface BlendComponentOptions { readonly src: GPUBlendFactor; readonly dst: GPUBlendFactor; readonly op?: GPUBlendOperation; }
interface BlendOptions { readonly color: BlendComponentOptions; readonly alpha?: BlendComponentOptions; }

interface EffectOptions {
  readonly set?: SetBag;
  readonly label?: string;
  readonly blend?: BlendPreset | BlendOptions;
  readonly writeMask?: readonly ("r" | "g" | "b" | "a")[];
}

interface Effect {
  readonly gpu: GPURenderPipeline | undefined;
  set(values: SetBag): this;
  draw(target?: Target | DrawCallOptions): void;
  compile(target?: Target | TargetSignature): Promise<this>;
  compileSync(target?: Target | TargetSignature): this;
}
```

## Parameters

| Param               | Type                                                       | Required | Default      | Notes                                                                                                                                                                                      |
| ------------------- | ---------------------------------------------------------- | -------: | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| effect.source       | `string \| ShaderSource`                                   |        ✔ | —            | WGSL string or `ShaderSource`. If no `@vertex` entry exists, vgpu injects a fullscreen triangle vertex stage and provides `@location(0) uv`.                                               |
| effect.opts         | `EffectOptions`                                            |        ✖ | `{}`         | Initial options. Passing a `mesh` property is rejected; effects have no vertex buffers.                                                                                                    |
| opts.set            | `Record<string, unknown>`                                  |        ✖ | `undefined`  | Same as one initial `.set(opts.set)` call: establishes first-set binding ownership and validates reflected bindings.                                                                       |
| opts.label          | `string`                                                   |        ✖ | `"effect"`   | Used in shader reflection labels, GPU object labels, and `VGPU-*` error `where` fields.                                                                                                    |
| opts.blend          | `"alpha" \| "additive" \| "premultiplied" \| BlendOptions` |        ✖ | `undefined`  | Constructor-only blend state passed through to the fullscreen draw. Presets and defaults match `DrawOptions.blend`; omitted explicit `alpha` copies `color`, and `op` defaults to `"add"`. |
| opts.writeMask      | `readonly ("r" \| "g" \| "b" \| "a")[]`                    |        ✖ | all channels | Constructor-only color channel mask. Omit for RGBA; `[]` writes no channels; `["r","g","b"]` skips alpha.                                                                                  |
| effect.set.values   | `Record<string, unknown>`                                  |        ✔ | —            | Binding values by WGSL variable name. JS values are lib-owned; resources are user-owned.                                                                                                   |
| effect.draw\.target | `Target \| DrawCallOptions`                                |        ✖ | `{}`         | One-shot render pass. Pass a bare target for the common case, or an options bag when setting per-call draw options.                                                                        |
| opts.target         | `Target`                                                   |        ✖ | —            | Required at runtime when an options bag is used. Use a `Surface` or an offscreen `Target`.                                                                                                 |

The `uv` varying that `effect(gpu)` injects is top-origin: `(0, 0)` is the
top-left corner and `v` grows downward — the same convention as WebGPU texture
coordinates, `@builtin(position)`, and `target.read()`. Sampling any texture
with this `uv` needs no flip: a pass that samples `src` at `uv` reproduces the
image exactly. If you are porting a WebGL or Shadertoy shader that assumes
`v` grows upward, invert once at the boundary (`1.0 - uv.y`) and keep
everything else flip-free.

**Returns:** `effect(gpu)` returns `Effect`; `effect.set()` and `effect.compileSync()` return the same `Effect`; `effect.compile()` returns `Promise<this>`; `effect.draw()` returns `void` after starting a one-shot draw path.

**Throws:** `VGPU-TARGET-REQUIRED` when `effect.draw()` or compile pre-warm is called without `target`; `VGPU-BLEND-INVALID` for an unknown blend preset or malformed blend object; `VGPU-WRITEMASK-INVALID` for a non-array or unknown write mask channel; `VGPU-RING1-UNSUPPORTED` when `effect(gpu)` receives mesh/vertex data; `VGPU-SHADER-SOURCE-INVALID` for malformed `ShaderSource`; `VGPU-R1-BINDING-NEVER-SET` when a reflected binding has no value at draw time; `VGPU-R1-OWNERSHIP-FLIP` when a binding switches between JS-value and resource ownership; `VGPU-SET-TEXTURE-FILTERABILITY` when an ordinarily sampled facade texture is not filterable (structured detail names its format/binding and paired sampler; use a filterable format, request `float32-filterable`, or use `textureLoad` without a sampler). Asynchronous draw validation errors are delivered through `gpu.onError`; tests can `await gpu.settled()`.

## Examples

```ts
import { init, clock, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64] });
const shader = effect(gpu, `
  struct Params { time: f32, speed: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time * params.speed) * 0.5 + 0.5, 1);
  }
`, { label: "wave", set: { params: { time: 0, speed: 2 } } });

shader.set({ params: { time: clock(gpu).time, speed: 2 } });
frame(gpu, (currentFrame) => currentFrame.pass(colorTarget, shader));
```

```ts
import { init, effect, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [32, 32] });
const copy = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv.x, uv.y, 0.0, 1.0);
  }
`);
copy.draw(colorTarget);
```

## Pipeline pre-warm

Effects compile lazily for the target signature they draw into. Use `await effect.compile(target)` during loading to pre-warm without blocking, or `effect.compileSync(target)` when synchronous creation is acceptable. Signature objects follow the same shape as draws: `{ colors: ["bgra8unorm"], depth?, sampleCount? }`.

## Notes

* A fragment-only effect is internally implemented as a `Draw` with an injected fullscreen triangle. Fragment-only resources receive fragment visibility only, so storage does not consume `maxStorageBuffersInVertexStage`.
* `blend` and `writeMask` are immutable pipeline state, fixed at `effect(gpu)` construction, and apply uniformly to every color target. Use them for overlays, glow, UI, and other loaded-pass compositing. For explicit blends, `op` defaults to `"add"` and omitted `alpha` copies `color`.
* One-shot `effect.draw()` does not join a surrounding frame. Inside `frame(gpu)`, draw through `frame.pass()`.
* There is no implicit screen target. Browser code should create a `Surface` and pass it as `target`.
* Do not rely on implicit uniforms like time or resolution; pass `clock(gpu).time`, `target.size`, or `target.texelSize` explicitly through `set()`.
* **See also:** `effect`, `Draw`, `FramePass.draw`, `Surface`, `Target`, `SharedUniforms`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Frame
description: `frame()` is both a callable one-frame submit helper and a `FrameRunner`. It creates one command encoder, lets you encode any number of explicit-target render passes, then submits once.
---

# Frame



## Import

```ts
import type { Frame, FramePass, FramePassOptions, FrameLoopHandle, FrameRunner } from "vgpu";
```

## Signature

```ts
import type { Bundle, ClearColor, Draw, DrawCallOptions, Effect, Target, TimerSpan, Visibility, VisibilityQuery } from "vgpu";

interface FramePassOptions {
  readonly target: Target;
  readonly clear?: boolean | ClearColor;
  readonly clearDepth?: number;
  readonly clearStencil?: number;
  readonly depthReadOnly?: boolean;
  readonly viewport?: {
    readonly x?: number;
    readonly y?: number;
    readonly width: number;
    readonly height: number;
    readonly minDepth?: number;
    readonly maxDepth?: number;
  };
  readonly scissor?: readonly [number, number, number, number];
  readonly timer?: TimerSpan;
  readonly visibility?: Visibility;
}

interface FrameLoopHandle { stop(): void; }
interface FrameLoopOptions { readonly fps?: number; }
type FrameLoopCallback = (frame: Frame) => void;

declare class Frame {
  done: Promise<void>;
  pass(target: Target, body: Effect | Draw | ((pass: FramePass) => void)): void;
  pass(options: FramePassOptions, body: Effect | Draw | ((pass: FramePass) => void)): void;
  submit(): void;
  cancel(): void;
}

declare class FramePass {
  readonly target: Target;
  draw(drawable: Draw | Effect, opts?: DrawCallOptions): void;
  occlusion(query: VisibilityQuery, body: Draw | Effect | (() => void)): void;
  bundles(...bundles: readonly Bundle[]): void;
}

declare class FrameRunner {
  frame(cb?: (frame: Frame) => void): Frame;
  loop(cb: FrameLoopCallback, opts?: FrameLoopOptions): FrameLoopHandle;
}
```

## Parameters

| Param                | Type                                              | Required | Default        | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------- | ------------------------------------------------- | -------: | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| frame.cb             | `(frame: Frame) => void`                          |        ✖ | `undefined`    | If supplied, called and then `frame.submit()` runs in `finally`. If omitted, submit manually.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| target.clearColor    | `ClearColor`                                      |        ✖ | `[0, 0, 0, 1]` | Writable default clear color of the pass target, used when pass `clear` is omitted or `true`. Set it at creation (`surface(gpu, canvas, { clearColor })`, `target(gpu, { size, clearColor })`) or assign it later. Assign a `GPUColor` object or `[r, g, b, a]`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| frame.pass.target    | `Target \| FramePassOptions`                      |        ✔ | —              | Pass a bare target for the allocation-free common case, or an options bag when customizing clear/preserve behavior.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| opts.target          | `Target`                                          |        ✔ | —              | Required inside `FramePassOptions`. Use a `Surface` from `surface(gpu, canvas)` or an offscreen `Target` from `target(gpu, { size })`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| opts.clear           | `boolean \| ClearColor`                           |        ✖ | `true`         | Omitted or `true` clears with `target.clearColor`; `false` preserves existing color and depth with load ops; a color clears this pass with that color.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| opts.clearDepth      | `number`                                          |        ✖ | `1`            | Depth clear value used when the pass clears, in `[0, 1]`. Clear to `0` and give draws `depth: { compare: "greater" }` for reversed-Z, which evens out float depth precision and cuts distant z-fighting. Invalid alongside `clear: false`, which preserves depth, and on a target without depth (the value would have nowhere to land).                                                                                                                                                                                                                                                                                                                                                                                                            |
| opts.clearStencil    | `number`                                          |        ✖ | `0`            | Stencil clear value used when the pass clears; integer in `[0, 0xFFFFFFFF]`, masked to the stencil aspect's bit width by taking the LSBs (values above `0xFF` are legal on 8-bit aspects). Clear to `0` before stencil-masking draws mark pixels via `DrawOptions.stencil` — portals, mirrors, UI cutouts. Requires a target depth format with a stencil aspect (e.g. `depth: "depth24plus-stencil8"`). Invalid alongside `clear: false`, which preserves stencil.                                                                                                                                                                                                                                                                                 |
| opts.depthReadOnly   | `boolean`                                         |        ✖ | `false`        | Opens the pass with a read-only depth attachment: draws depth-test against it and may sample `target.depth` in the same pass — the soft-particles/SSAO setup. Every draw in the pass needs `depth: { write: false }` (or `depth: false`); the default depth state writes and throws `VGPU-PASS-DEPTH-READONLY` at encode. Effects always keep the writing default, so an `Effect` cannot run in a depthReadOnly pass on a depth target. Combined depth-stencil formats mark the stencil aspect read-only too. Requires a target with depth; invalid alongside `clearDepth`/`clearStencil` (color `clear` still applies), and invalid on MSAA targets, whose depth aspect is stored with `storeOp: "discard"` — there is no retained depth to read. |
| opts.viewport        | `{ x?, y?, width, height, minDepth?, maxDepth? }` |        ✖ | full target    | Restricts rasterization to a sub-rectangle for every draw in this pass. Use it for split-screen views, minimaps, and picture-in-picture insets. Floats (fractional values allowed); defaults `x`/`y` `0`, `minDepth` `0`, `maxDepth` `1`. May extend past the target — validated at pass open against device limits, not the attachment.                                                                                                                                                                                                                                                                                                                                                                                                           |
| opts.scissor         | `readonly [number, number, number, number]`       |        ✖ | full target    | Clips every draw in this pass to `[x, y, width, height]`. Use it for UI clipping and partial redraw of damaged regions. Non-negative integers; `x + width` and `y + height` must fit the target's **current** pixel size at pass open (targets are resizable). Never affects the clear — `loadOp: "clear"` fills the whole attachment; to clear a sub-rectangle, draw it inside a scissored pass.                                                                                                                                                                                                                                                                                                                                                  |
| opts.timer           | `TimerSpan`                                       |        ✖ | `undefined`    | Times this pass on the GPU: pass `timer.span(name)` from a `timer(gpu)` (needs the `"timestamp-query"` device feature). See `Timer` for results, capacity, and feature gating.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| opts.visibility      | `Visibility`                                      |        ✖ | `undefined`    | Enables occlusion queries in this pass: pass a `visibility(gpu)` instance, then wrap proxy draws in `pass.occlusion(handle, body)`. Requires a target with a depth attachment. See `Visibility` for handle semantics and capacity.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| frame.pass.body      | `Effect \| Draw \| ((pass: FramePass) => void)`   |        ✔ | —              | Pass a drawable directly for a single draw, or a callback to encode multiple draw and bundle commands.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| pass.draw\.drawable  | `Draw \| Effect`                                  |        ✔ | —              | A main API (`vgpu`) draw or fullscreen effect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| pass.draw\.opts      | `DrawCallOptions`                                 |        ✖ | `{}`           | Per-call counts and dynamic offsets. Target is the frame pass target.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| pass.occlusion.query | `VisibilityQuery`                                 |        ✔ | —              | Stable handle from `vis.query(label)` of the same visibility instance the pass was opened with. One use per handle per frame, across passes too.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| pass.occlusion.body  | `Draw \| Effect \| (() => void)`                  |        ✔ | —              | Wrapped in `beginOcclusionQuery`/`endOcclusionQuery`. The body ALWAYS executes — it is the proxy the GPU measures; condition your real draws on `q.hidden` outside the scope.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| pass.bundles.bundles | `readonly Bundle[]`                               |        ✔ | —              | Bundles recorded by `bundle(gpu, { target }, cb)`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| runner.loop.cb       | `(frame: Frame) => void`                          |        ✔ | —              | Called on each scheduled frame; frame is submitted in `finally`. Surface auto-resize runs before this callback.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| runner.loop.opts.fps | `number`                                          |        ✖ | `0` (uncapped) | Positive values cap by minimum frame interval `1000 / fps`; omitted or non-positive uses every rAF/timer tick.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

**Returns:** `frame(gpu)` / `FrameRunner.frame()` return `Frame`; `Frame.pass()`, `Frame.submit()`, and `Frame.cancel()` return `void`; `FramePass.draw()`, `.occlusion()`, and `.bundles()` return `void`; `loop()` returns `FrameLoopHandle` with `stop()`.

**Throws:**

* `VGPU-TARGET-REQUIRED` — a runtime JS call omitted the frame pass target. Name a `Surface` or offscreen `Target` in every `frame.pass`.
* `VGPU-CLEAR-COLOR-INVALID` — `target.clearColor` / `surface.clearColor` (at creation or on assignment) or a pass clear color is not four finite numbers. Assign `[r, g, b, a]` or a `GPUColor` object.
* `VGPU-PASS-PRESERVE-MSAA` — `clear: false` on an MSAA target; multisample attachments use `storeOp: "discard"`, so there is nothing to preserve. Render accumulation/preserve passes into a non-MSAA target.
* `VGPU-PASS-CLEARDEPTH-INVALID` — `clearDepth` is not a number in `[0, 1]`, or the target has no depth attachment (the option would have no effect). Create the target with `depth: true`, or drop `clearDepth`.
* `VGPU-PASS-PRESERVE-CLEARDEPTH` — `clearDepth` combined with `clear: false`; preserved depth is never cleared. Drop one of the two.
* `VGPU-PASS-CLEARSTENCIL-INVALID` — `clearStencil` is not an integer in `[0, 0xFFFFFFFF]`, or the target's depth format has no stencil aspect (the option would have no effect). Create the target with `depth: "depth24plus-stencil8"`.
* `VGPU-PASS-PRESERVE-CLEARSTENCIL` — `clearStencil` combined with `clear: false`; preserved stencil is never cleared. Drop one of the two.
* `VGPU-PASS-DEPTH-READONLY` — `depthReadOnly` is not a boolean, is set on a target without depth, or is combined with `clearDepth`/`clearStencil` (read-only aspects omit their load/store ops and are never cleared). Also thrown at encode when a draw writes depth — add `depth: { write: false }` — or writes stencil (a draw writes stencil when any stencil op on an unculled face is not `"keep"` and its stencil `writeMask` is nonzero — use `"keep"` ops or `writeMask: 0`); and when `pass.bundles(...)` is called in such a pass — `bundle()` records writable depth/stencil, and WebGPU only executes read-only-recorded bundles there, so encode those draws with `pass.draw(...)` instead.
* `VGPU-PASS-DEPTH-READONLY-MSAA` — `depthReadOnly` on an MSAA target; multisampled depth is stored with `storeOp: "discard"`, so a read-only pass would depth-test against discarded contents instead of failing loudly. Use a non-MSAA target for read-only depth.
* `VGPU-PASS-VIEWPORT-INVALID` — `viewport` is malformed or outside device limits: `width`/`height` in `[0, maxTextureDimension2D]`, `x`/`y` at least `-2 × maxTextureDimension2D` with `x + width`/`y + height` at most `2 × maxTextureDimension2D − 1`, `minDepth`/`maxDepth` in `[0, 1]` with `minDepth <= maxDepth`. The message names the offending field.
* `VGPU-PASS-SCISSOR-INVALID` — `scissor` is malformed or exceeds the target's current pixel size. Shrink the rectangle, or resize it from `surface.onResize(...)`.
* `VGPU-TIMER-INVALID` — `timer` is not a `TimerSpan` from `timer.span(name)`, repeats a span name within one frame, or belongs to a different gpu's or disposed timer.
* `VGPU-TIMER-CAPACITY` — one frame timed more spans than the per-frame limit (see `Timer`).
* `VGPU-VIS-INVALID` — `visibility` is not a `Visibility` from this gpu's `visibility(gpu)`, or `occlusion()` received a non-`VisibilityQuery` value or a handle of a different visibility instance.
* `VGPU-VIS-NO-DEPTH` — `visibility` on a pass whose target has no depth attachment; nothing is depth-tested, so every query would report visible. Create the target with `depth: true`.
* `VGPU-VIS-DISPOSED` — the visibility instance or the handle is disposed.
* `VGPU-VIS-CAPACITY` — the `occlusion()` call exceeded the declared capacity. Raise `VisibilityOptions.capacity`.
* `VGPU-QUERY-NO-VISIBILITY` — `occlusion()` in a pass opened without `visibility`. Pass the instance in `FramePassOptions.visibility`.
* `VGPU-QUERY-NESTED` — `occlusion()` inside an active `occlusion()` body; one scope at a time. Close the outer scope first.
* `VGPU-QUERY-DUPLICATE` — a handle used twice within one frame, across passes too (see `Visibility`). Create one handle per queried object.
* `VGPU-FRAME-REENTRANT` — a frame started from another frame or from a surface resize callback. Encode everything in one frame callback.
* `VGPU-FRAME-CANCELED` — a `frame.pass(...)`, or a retained `FramePass` operation, on a frame closed by `cancel()`; its command encoder was dropped, so the work would never run. Open a new `frame(gpu)`.
* `VGPU-FRAME-PASS-ACTIVE` — `frame.cancel()` from inside that frame's active pass callback. Return from `frame.pass(...)` first, then cancel, so resources referenced by the native pass descriptor stay alive until the pass closes.
* `VGPU-FRAME-SUBMITTED` — `frame.cancel()` on a frame that was already submitted; queued GPU work cannot be taken back, and the frame needs no cleanup. Cancel only frames you decided not to submit.
* `VGPU-R3-BUNDLE-STALE` / `VGPU-R3-BUNDLE-INVALID` — replaying a bundle whose recorded resources changed identity, or a value not created by `bundle()`. Re-record the bundle.
* Binding errors such as `VGPU-R1-BINDING-NEVER-SET` propagate during encoding. Raw claimed-group validation is delivered asynchronously through `gpu.onError`.

## Examples

```ts
import { init, draw, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [64, 64], format: "rgba8unorm", clearColor: [0.02, 0.02, 0.04, 1] });
scene.clearColor = [0.02, 0.02, 0.04, 1]; // and it stays writable at runtime
const drawable = draw(gpu, { shader: `
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.4, 1.0, 1.0); }
` });

frame(gpu, (currentFrame) => {
  currentFrame.pass(scene, (pass) => pass.draw(drawable)); // clears with scene.clearColor
});
```

```ts
import { init, effect, frameLoop, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [16, 16] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
const handle = frameLoop(gpu, (frame) => {
  frame.pass({ target: colorTarget, clear: false }, shader); // preserve color and depth
}, { fps: 30 });
handle.stop();
```

```ts
import { init, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const screen = target(gpu, { size: [640, 360] });
const p1View = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.1, 0.3, 0.6, 1); }`);
const p2View = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.6, 0.3, 0.1, 1); }`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: screen, viewport: { width: 320, height: 360 } }, p1View); // player 1, left half
  currentFrame.pass({ target: screen, clear: false, viewport: { x: 320, width: 320, height: 360 } }, p2View); // player 2, right half
});
```

Split-screen: the first pass clears the whole target and rasterizes one camera into the left viewport; the second preserves it (`clear: false`) and rasterizes the other camera into the right viewport.

```ts
import { init, draw, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
const opaque = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.5, 0.2, 1); }`);
const particles = draw(gpu, {
  shader: `
    @group(0) @binding(0) var sceneDepth: texture_depth_2d;
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0.4, 1);
    }
    @fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
      let d = textureLoad(sceneDepth, vec2i(pos.xy), 0);
      let fade = clamp((d - pos.z) * 32.0, 0.0, 1.0); // fade out where the particle nears geometry
      return vec4f(0.9, 0.6, 0.3, 1.0) * fade;
    }
  `,
  depth: { write: false }, // required: the pass depth is read-only
  blend: "additive",
  set: { sceneDepth: scene.depth },
});

frame(gpu, (currentFrame) => {
  currentFrame.pass(scene, opaque); // pass 1: opaque geometry writes depth
  currentFrame.pass({ target: scene, clear: false, depthReadOnly: true }, particles); // pass 2: test + sample that depth
});
```

Soft particles: pass 2 depth-tests against the opaque depth while sampling the same `scene.depth` texture, which WebGPU only allows because the attachment is read-only.

```ts
import { init, effect, frame, target, visibility } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [64, 64], depth: true });
const vis = visibility(gpu);
const proxy = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

const currentFrame = frame(gpu); // manual frame: nothing submits it for you
currentFrame.pass({ target: scene, visibility: vis }, (pass) => pass.occlusion(vis.query("statue"), proxy));

if (sceneChangedUnderneath()) {
  currentFrame.cancel(); // no GPU work runs, and the occlusion query set stops being retained for this frame
  vis.dispose(); // released now instead of at gpu.dispose()
} else {
  currentFrame.submit();
}

function sceneChangedUnderneath(): boolean { return true; }
```

Cancelling a manual frame: `cancel()` drops the encoder and releases the telemetry retains the frame took, so a `timer(gpu)` / `visibility(gpu)` can be disposed for good without submitting work you no longer want.

## Notes

* `Frame`, `FramePass`, and `FrameRunner` are type-only public exports. Create frames through `frame()`, not `new Frame(...)`.
* There is no default target and no implicit canvas target; every `frame.pass` names its target.
* Omitted `clear` and `clear: true` clear with `target.clearColor`. Pass a color to clear one pass with that color without changing the target default: pass color > `target.clearColor` > the built-in `[0, 0, 0, 1]`.
* Draws replayed via `pass.bundles(...)` inside an occlusion scope count toward the active query — render bundles have no query methods, but their draws execute inside the scope.
* `viewport` and `scissor` are set once right after the pass opens and apply to every draw in the pass, including replayed bundles. Both are in physical pixels: surfaces size their textures by `devicePixelRatio`, so a CSS-pixel rectangle must be scaled by dpr.
* `clear: false` preserves color and depth contents within the same target. On `Surface`, repeated passes in one frame layer onto the same current texture; the first preserved surface pass of a new browser frame reads the swapchain's fresh contents, not the previous frame's image.
* **Hot loops:** options bags and pass callbacks are read synchronously, so you can hoist and reuse them. For zero-per-frame-JS-cost replay, record stable work with `bundle()` and replay the bundle.
* `frame.cancel()` discards a frame you decided not to submit: its command encoder is dropped, so nothing it encoded ever runs, and every `timer(gpu)` / `visibility(gpu)` attached to it releases the query set it was holding for that frame — no result, no phantom timing, no phantom `"hidden"`. It is the explicit way out of the retain a manual `frame(gpu)` otherwise keeps until `gpu.dispose()`: a frame is never assumed abandoned, because an old frame can still be submitted. Frames run by `frame(gpu, cb)` / `frameLoop(gpu, cb)` submit themselves and need no cancel.
* Cancelling is idempotent, like submitting: a second `cancel()` does nothing, and `submit()` after `cancel()` is a no-op — so calling `cancel()` in a `frame(gpu, cb)` callback after its `frame.pass(...)` calls have returned is safe, the runner's submit in `finally` simply finds a closed frame. `cancel()` from inside an active pass callback throws `VGPU-FRAME-PASS-ACTIVE`, because the native pass descriptor still references its telemetry resources; return from `frame.pass(...)` before canceling. The reverse is also an error: `cancel()` after `submit()` throws `VGPU-FRAME-SUBMITTED` (the work is already on the queue and cannot be taken back), and `pass()` or a retained `FramePass` operation after `cancel()` throws `VGPU-FRAME-CANCELED` (it would encode into a dropped encoder and silently never run).
* `frame.done` is resolve-only. Await it as a completion/timing signal for readbacks, benchmarks, deterministic tests, or teardown; use `gpu.onError` plus `await gpu.settled()` for asynchronous errors.
* Do not `await frame.done` inside a RAF/frame loop. Schedule the next frame as soon as `frame(gpu)` returns, or you serialize CPU and GPU work.
* **See also:** `frame`, `Surface`, `Effect`, `Draw`, `Bundle`, `Target`, `Timer`, `Visibility`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: geometry()
description: Creates an immutable vertex/index layout plus mutable GPU buffers for `draw(gpu)`. The v2 descriptor form accepts named attributes, multiple vertex streams, instancing streams, indices, topology, writes, and slices. Existing scene geometry descriptors such as `box()` continue to work as primitive sugar.
---

# geometry()



## Import

```ts
import type { Geometry, GeometryOptions, GeometryBufferOptions, GeometrySliceOptions } from "vgpu";
import { box } from "vgpu/scene";
```

## Signature

```ts
interface Gpu {
  geometry(geometry: import("vgpu/scene").SceneGeometry): Geometry;
  geometry(options: GeometryOptions): Geometry;
}

type GeometryData = ArrayBuffer | ArrayBufferView;
type GeometryAttributes = {
  readonly [name: string]: GPUVertexFormat | GeometryAttributeOverride;
};

interface GeometryAttributeOverride {
  readonly format: GPUVertexFormat;
  readonly offset?: number;
  readonly location?: number;
}

interface GeometryBufferOptions {
  readonly attributes: GeometryAttributes;
  readonly data?: GeometryData;
  readonly buffer?: GPUBuffer;
  readonly stride?: number;
  readonly stepMode?: GPUVertexStepMode;
  readonly label?: string;
}

interface GeometryOptions {
  readonly buffers: readonly GeometryBufferOptions[];
  readonly vertexCount?: number;
  readonly instanceCount?: number;
  readonly indices?: Uint16Array | Uint32Array | readonly number[];
  readonly indexBuffer?: GPUBuffer;
  readonly indexFormat?: GPUIndexFormat;
  readonly indexCount?: number;
  readonly topology?: GPUPrimitiveTopology;
  readonly label?: string;
}

interface GeometryBuffer {
  readonly gpu: GPUBuffer;
  readonly stride: number;
  readonly stepMode: GPUVertexStepMode;
  write(data: GeometryData, byteOffset?: number): void;
}

interface GeometrySliceOptions {
  readonly firstIndex?: number;
  readonly indexCount?: number;
  readonly baseVertex?: number;
  readonly firstVertex?: number;
  readonly vertexCount?: number;
  readonly instanceCount?: number;
  readonly label?: string;
}

interface Geometry {
  readonly vertexCount?: number;
  readonly indexCount?: number;
  readonly instanceCount?: number;
  readonly vertexBuffers?: readonly GPUBuffer[];
  readonly indexBuffer?: GPUBuffer;
  readonly indexFormat?: GPUIndexFormat;
  readonly vertexBufferLayouts?: readonly GPUVertexBufferLayout[];
  readonly topology: GPUPrimitiveTopology;
  readonly buffers: readonly GeometryBuffer[];
  slice(opts?: GeometrySliceOptions): GeometrySlice;
  write(data: GeometryData, byteOffset?: number): void;
  writeIndices(data: Uint16Array | Uint32Array, byteOffset?: number): void;
  destroy(): void;
}

interface GeometrySlice extends GeometryLike {
  readonly geometry: Geometry;
  readonly firstIndex?: number;
  readonly indexCount?: number;
  readonly baseVertex?: number;
  readonly firstVertex?: number;
  readonly vertexCount?: number;
  readonly instanceCount?: number;
}

interface DrawCallOptions {
  /** Non-indexed count. Precedence: call > slice > geometry > DrawOptions > 3. */
  readonly vertices?: number;
  /** Non-indexed start. Precedence: call > slice > 0. */
  readonly firstVertex?: number;
  /** Indexed count. Precedence: call > slice > geometry. */
  readonly indices?: number;
  /** Indexed start. Precedence: call > slice > 0. */
  readonly firstIndex?: number;
  /** Indexed base vertex. Precedence: call > slice > 0. */
  readonly baseVertex?: number;
}
```

## Parameters

| Field                      | Type                                              | Required | Default                       | Notes                                                                                                                           |
| -------------------------- | ------------------------------------------------- | -------: | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| geometry                   | `SceneGeometry`                                   |        ✔ | —                             | v1 primitive sugar path. Emits pinned locations: `position` → `@location(0)`, `normal` → `@location(1)`, `uv` → `@location(2)`. |
| options.buffers            | `readonly GeometryBufferOptions[]`                |        ✔ | —                             | Vertex buffer streams. Maximum 8.                                                                                               |
| buffer.attributes          | `GeometryAttributes`                              |        ✔ | —                             | Record form only. Key is the WGSL vertex input name unless `location` is specified. Maximum 16 attributes total.                |
| attribute format           | `GPUVertexFormat`                                 |        ✔ | —                             | Shorthand value: `{ position: "float32x3" }`.                                                                                   |
| attribute.offset           | `number`                                          |        ✖ | tight-packed order            | Byte offset within the stream. Integer-like attribute keys are rejected to avoid JavaScript key reordering.                     |
| attribute.location         | `number`                                          |        ✖ | shader name match             | Explicit shader location. When present, the record key is only a label.                                                         |
| buffer.data                | `GeometryData`                                    |        ✖ | —                             | Creates an owned `["vertex", "copy_dst"]` buffer and uploads the initial data. Mutually exclusive with `buffer`.                |
| buffer.buffer              | `GPUBuffer`                                       |        ✖ | —                             | Caller-owned escape hatch. Mutually exclusive with `data`; not destroyed by `geometry.destroy()`.                               |
| buffer.stride              | `number`                                          |        ✖ | `roundUp4(sum(format sizes))` | Explicit stride for padded/interleaved data. Must be valid for WebGPU vertex buffers.                                           |
| buffer.stepMode            | `"vertex" \| "instance"`                          |        ✖ | `"vertex"`                    | Instance streams derive `geometry.instanceCount` from the first instance buffer with data.                                      |
| options.vertexCount        | `number`                                          |        ✖ | derived                       | Derived from the first vertex-step buffer with data.                                                                            |
| options.instanceCount      | `number`                                          |        ✖ | derived                       | Draw default after `DrawCallOptions.instances` and `DrawOptions.instances`.                                                     |
| options.indices            | `Uint16Array \| Uint32Array \| readonly number[]` |        ✖ | —                             | Creates an owned `["index", "copy_dst"]` index buffer. `Uint16Array` infers `"uint16"`; otherwise `"uint32"`.                   |
| options.indexBuffer        | `GPUBuffer`                                       |        ✖ | —                             | Caller-owned index buffer escape hatch. Pair with `indexFormat` and `indexCount`.                                               |
| options.topology           | `GPUPrimitiveTopology`                            |        ✖ | `"triangle-list"`             | Pipeline-affecting geometry topology. Strip topologies derive `stripIndexFormat` from `indexFormat`.                            |
| geometry.slice.opts        | `GeometrySliceOptions`                            |        ✖ | full range                    | Frozen range view sharing buffers and layout identity with the parent geometry.                                                 |
| geometry.write.data        | `GeometryData`                                    |        ✔ | —                             | Writes to buffer 0 using `queue.writeBuffer`. No resize.                                                                        |
| geometry.writeIndices.data | `Uint16Array \| Uint32Array`                      |        ✔ | —                             | Writes to an index buffer owned from `options.indices`. Write caller-owned `indexBuffer` objects directly. No resize.           |

**Returns:** `geometry(gpu)` returns `Geometry`; `geometry.slice()` returns `GeometrySlice`; `write()`, `writeIndices()`, and `destroy()` return `void`.

## Error codes

| Code                            | When                                                                                                                      | Fix                                                                                |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `VGPU-MESH-LAYOUT-INVALID`      | Invalid stride/offset/format, both `data` and `buffer`, or integer-like attribute key.                                    | Use record keys that are names, align offsets/strides, and choose one data source. |
| `VGPU-MESH-LIMIT-EXCEEDED`      | More than 8 buffers or 16 attributes.                                                                                     | Split draws or reduce streams/attributes.                                          |
| `VGPU-MESH-LOCATION-CONFLICT`   | Duplicate explicit `location` values.                                                                                     | Give each explicit shader location once.                                           |
| `VGPU-MESH-DATA-MISALIGNED`     | Data byte length is not divisible by stride, or index bytes do not match format.                                          | Repack data or pass an explicit `stride`.                                          |
| `VGPU-MESH-RANGE-INVALID`       | Slice or draw-time range is negative, non-integer, outside parent counts, or uses index ranges on non-indexed geometries. | Clamp ranges and use indexed fields only with indexed geometries.                  |
| `VGPU-MESH-WRITE-RANGE`         | `write()` or `writeIndices()` would overflow the fixed buffer.                                                            | Create a larger geometry; writes do not resize buffers.                            |
| `VGPU-MESH-ATTRIBUTE-UNMATCHED` | Named geometry attribute has no vertex-stage shader input.                                                                | Rename the attribute or specify `location`.                                        |
| `VGPU-MESH-INPUT-MISSING`       | Shader declares an uncovered `@location` input.                                                                           | Add the geometry attribute or remove the shader input.                             |
| `VGPU-MESH-FORMAT-MISMATCH`     | Vertex format base type does not match the WGSL input base type.                                                          | Use a compatible `GPUVertexFormat`; width differences are allowed by WebGPU.       |

## Examples

```ts
import { init, geometry } from "vgpu/mock";

const gpu = await init();
const positions = new Float32Array([
  -1, -1, 0,
   1, -1, 0,
   0,  1, 0,
]);

const triangle = geometry(gpu, {
  buffers: [{
    data: positions,
    attributes: { position: "float32x3" },
  }],
});
```

```ts
import { init, geometry } from "vgpu/mock";

const gpu = await init();
const ledVertices = new Float32Array(6 * 6);
const ledGeometry = geometry(gpu, {
  label: "triangle-led-front-led-emitters",
  buffers: [{
    data: ledVertices,
    stride: 24,
    attributes: {
      position: "float32x2",
      local: "float32x2",
      led_index: "float32",
    },
  }],
});
```

```ts
import { init, geometry } from "vgpu/mock";

const gpu = await init();
const quadCorners = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
const instanceData = new Float32Array(4 * 10);
const particles = geometry(gpu, {
  topology: "triangle-strip",
  buffers: [
    { data: quadCorners, attributes: { corner: "float32x2" } },
    { stepMode: "instance", data: instanceData, attributes: {
      i_pos: "float32x3",
      i_color: { format: "unorm8x4", location: 5 },
    } },
  ],
});
```

```ts
import { init, draw, geometry } from "vgpu/mock";

const gpu = await init();
const vertexData = new Float32Array(3 * 4500);
const allIndices = new Uint32Array(4500);
const gltfGeometry = geometry(gpu, {
  buffers: [{ data: vertexData, attributes: { position: "float32x3" } }],
  indices: allIndices,
});
const hull = gltfGeometry.slice({ firstIndex: 0, indexCount: 3600 });
const glass = gltfGeometry.slice({ firstIndex: 3600, indexCount: 900, label: "glass" });
const pbrWgsl = "@vertex fn vs_main(@location(0) position: vec3f) -> @builtin(position) vec4f { return vec4f(position, 1); }";

draw(gpu, { shader: pbrWgsl, geometry: hull });
draw(gpu, { shader: pbrWgsl, geometry: glass, blend: "alpha" });
```

```ts
import { init, draw, geometry, target } from "vgpu/mock";

const gpu = await init();
const glyphQuads = new Float32Array(4 * 4);
const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
const text = geometry(gpu, {
  buffers: [{ data: glyphQuads, attributes: { pos: "float32x2", uv: "float32x2" } }],
  indices: quadIndices,
});
const sdfTextWgsl = "@vertex fn vs_main(@location(0) pos: vec2f, @location(1) uv: vec2f) -> @builtin(position) vec4f { return vec4f(pos, 0, 1); }";
const textDraw = draw(gpu, { shader: sdfTextWgsl, geometry: text });
const colorTarget = target(gpu, { size: [640, 480] });

text.write(new Float32Array(4 * 4));
text.writeIndices(new Uint16Array([0, 1, 2, 2, 1, 3]));
textDraw.draw({ target: colorTarget, indices: 6 });
```

```ts
import { init, geometry } from "vgpu/mock";
import { box } from "vgpu/scene";

const gpu = await init();
const cube = geometry(gpu, box({ size: 2 }));
```

## Notes

* Layout is immutable; data is mutable. Changing formats, strides, topology, index format, or buffer identities requires a new geometry and a new draw.
* Auto stride is tight-packed and rounded up to 4 bytes. It never guesses padded data; pass `stride` when a writer emits padding.
* Slices share parent buffers and the same `vertexBufferLayouts` array identity so pipelines are shared.
* Draw-time range overrides use `DrawCallOptions.indices`, `firstIndex`, and `baseVertex`; non-indexed draws use existing `vertices` and `firstVertex`.
* Bundles bake counts and ranges at record time. Dynamic per-frame ranges need direct draws or bundle re-recording.
* `destroy()` only destroys buffers owned from `data`/`indices`; caller-owned `buffer` and `indexBuffer` remain caller-owned.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Gpu
description: The main API (`vgpu`) context returned by `init()`. It owns device lifetime and the frame clock; every resource — canvas surfaces, offscreen targets, render, compute, storage, uniforms, samplers, and bundles — is created by a free function that takes the `Gpu` as its first argument.
---

# Gpu



## Import

```ts
import type { Gpu } from "vgpu";
import { init } from "vgpu/mock";
```

## Signature

```ts
import type { Bundle, BundleOptions, BundleRecorder, Clock, Compute, ComputeOptions, Draw, DrawOptions, Effect, EffectOptions, Frame, FrameLoopHandle, FrameLoopOptions, Geometry, GeometryOptions, GeometryRecipe, GpuErrorListener, PingPongStorage, PingPongTargets, SharedUniforms, StorageAccess, StorageBuffer, StorageOptions, Surface, SurfaceOptions, Target, TargetOptions, TargetTextureOptions, Timer, Visibility, VisibilityOptions } from "vgpu";
import type { Device } from "vgpu/core";
import type { ShaderSource } from "vgpu";

interface Gpu {
  readonly device: Device;
  readonly gpu: GPUDevice;
  /** True once `dispose()` ran. Reads stay legal; new work does not. */
  readonly disposed: boolean;
  dispose(): void;
  onError(cb: GpuErrorListener): () => void;
  settled(): Promise<void>;
}

// The creation API: named exports of `vgpu`, `vgpu/node` and `vgpu/mock`, all gpu-first.
declare function surface(gpu: Gpu, canvas: HTMLCanvasElement | OffscreenCanvas, opts?: SurfaceOptions): Surface;
declare function effect(gpu: Gpu, source: string | ShaderSource, opts?: EffectOptions): Effect;
declare function draw(gpu: Gpu, opts: DrawOptions): Draw;
declare function target(gpu: Gpu, opts: TargetOptions): Target;
declare function frame(gpu: Gpu, cb?: (frame: Frame) => void): Frame;
declare function frameLoop(gpu: Gpu, cb: (frame: Frame) => void, opts?: FrameLoopOptions): FrameLoopHandle;
declare function sampler(gpu: Gpu, desc?: GPUSamplerDescriptor): GPUSampler;
declare function geometry(gpu: Gpu, input: GeometryOptions | GeometryRecipe): Geometry;
declare function compute(gpu: Gpu, source: string | ShaderSource, opts?: ComputeOptions): Compute;
declare function storage(gpu: Gpu, bytes: number, access?: StorageAccess | StorageOptions): StorageBuffer;
declare function timer(gpu: Gpu): Timer;
declare function visibility(gpu: Gpu, options?: VisibilityOptions): Visibility;
declare function pingPong(gpu: Gpu, width: number, height: number, opts?: TargetTextureOptions): PingPongTargets;
declare function pingPongStorage(gpu: Gpu, bytes: number): PingPongStorage;
declare function uniforms<T extends Record<string, unknown>>(gpu: Gpu, values: T): SharedUniforms<T>;
declare function bundle(gpu: Gpu, opts: BundleOptions, record: (recorder: BundleRecorder) => void): Bundle;
declare function clock(gpu: Gpu): Clock;
```

## Parameters

`Gpu` is an object, not a callable constructor: it carries no creation methods. Every factory below takes it as `gpu`, its first argument.

| Param                 | Type                                   |         Required | Default        | Notes                                                                                                                                                                                 |                                                                                 |
| --------------------- | -------------------------------------- | ---------------: | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| surface.canvas        | `HTMLCanvasElement \| OffscreenCanvas` |                ✔ | —              | Canvas-like object with a `webgpu` context. A canvas may have one live `Surface`.                                                                                                     |                                                                                 |
| surface.opts          | `SurfaceOptions`                       |                ✖ | `{}`           | Per-surface canvas format, size, DPR, and auto-resize behavior.                                                                                                                       |                                                                                 |
| effect.source         | `string \| ShaderSource`               |                ✔ | —              | WGSL string or loader-produced `ShaderSource { version: 1, wgsl }`.                                                                                                                   |                                                                                 |
| effect.opts           | `EffectOptions`                        |                ✖ | `{}`           | `label` defaults to `"effect"`; `set` defaults to no initial bindings.                                                                                                                |                                                                                 |
| draw\.opts            | `DrawOptions`                          |                ✔ | —              | Includes required `shader`; see `DrawOptions`.                                                                                                                                        |                                                                                 |
| target.opts           | `TargetOptions`                        |                ✔ | —              | Offscreen target options. `size` is required.                                                                                                                                         |                                                                                 |
| frame.cb              | `(frame: Frame) => void`               |                ✖ | `undefined`    | If provided, submits automatically in `finally`; if omitted, caller must call `frame.submit()`.                                                                                       |                                                                                 |
| sampler.desc          | `GPUSamplerDescriptor`                 |                ✖ | `undefined`    | Cached by descriptor. `sampler(gpu)` is the canonical default sampler.                                                                                                                |                                                                                 |
| geometry.input        | \`GeometryOptions \\                   | GeometryRecipe\` | ✔              | —                                                                                                                                                                                     | A raw buffer descriptor, or a `vgpu/scene` recipe such as `box()` or `plane()`. |
| compute.source        | `string \| ShaderSource`               |                ✔ | —              | WGSL string or `ShaderSource`. Must contain a `@compute` entry point.                                                                                                                 |                                                                                 |
| compute.opts          | `ComputeOptions`                       |                ✖ | `{}`           | `label` defaults to `"compute"`; `set` defaults to no initial bindings.                                                                                                               |                                                                                 |
| storage.bytes         | `number`                               |                ✔ | —              | Byte size for a main API (`vgpu`) storage buffer.                                                                                                                                     |                                                                                 |
| storage.access        | `StorageAccess \| StorageOptions`      |                ✖ | `"read-write"` | Access string, or a `StorageOptions` bag `{ access?, indirect? }`. See `Compute` for storage buffer semantics, including `{ indirect: true }` for GPU-driven draw/dispatch arguments. |                                                                                 |
| timer                 | —                                      |                — | —              | No parameters. GPU pass timing; needs the `"timestamp-query"` device feature. See `Timer` for feature gating, spans, and result delivery.                                             |                                                                                 |
| visibility.options    | `VisibilityOptions`                    |                ✖ | `{}`           | Occlusion queries for visibility culling — core WebGPU, no device feature required. See `Visibility` for capacity and handle semantics.                                               |                                                                                 |
| pingPong.width        | `number`                               |                ✔ | —              | Floored and clamped to at least `1`.                                                                                                                                                  |                                                                                 |
| pingPong.height       | `number`                               |                ✔ | —              | Floored and clamped to at least `1`.                                                                                                                                                  |                                                                                 |
| pingPong.opts         | `TargetTextureOptions`                 |                ✖ | `{}`           | Texture/attachment options only; size comes from positional width/height.                                                                                                             |                                                                                 |
| pingPongStorage.bytes | `number`                               |                ✔ | —              | Creates two `"read-write"` storage buffers.                                                                                                                                           |                                                                                 |
| uniforms.values       | `Record<string, unknown>`              |                ✔ | —              | Cloned initial JS values; WGSL layout is adopted when first bound.                                                                                                                    |                                                                                 |
| bundle.opts           | `BundleOptions`                        |                ✔ | —              | Requires a `target` or target signature.                                                                                                                                              |                                                                                 |
| bundle.cb             | `(recorder: BundleRecorder) => void`   |                ✔ | —              | Records bundle commands immediately.                                                                                                                                                  |                                                                                 |
| onError.cb            | `GpuErrorListener`                     |                ✔ | —              | Receives asynchronous vgpu errors; returns an unsubscribe function.                                                                                                                   |                                                                                 |
| clock                 | —                                      |                — | —              | No parameters. The frame clock of this gpu: `{ time, deltaTime, frameCount, advance(dtSeconds) }`, one instance per gpu. See `Clock`.                                                 |                                                                                 |

**Returns:** each factory returns the resource named in its signature. `dispose()` and frame/pass callbacks return `void`.

**Throws:** `VGPU-GPU-DISPOSED` when any factory (or `clock(gpu)`) runs after `gpu.dispose()` — the device and everything it owned are gone, so the handle it would return could only fail later; create resources before disposing, or `init()` a new gpu; `VGPU-GPU-FOREIGN` when the first argument was not created by `init()` (a plain object, a `GPUDevice`, a gpu from another library): it carries no vgpu kernel, so pass the object returned by `init()` from `vgpu`, `vgpu/node` or `vgpu/mock`; `VGPU-LIMIT-STORAGE-VERTEX` / `VGPU-LIMIT-STORAGE-FRAGMENT` when a selected render entry exceeds its granted storage-buffer limit. The structured detail reports `stage`, `entryPoint`, `count`, `limit`, and each counted binding's `name`, `group`, and `binding`; request a supported limit or reduce/move the data; `VGPU-SHADER-SOURCE-INVALID` for malformed `ShaderSource`; `VGPU-SET-TEXTURE-FILTERABILITY` when a known facade texture format cannot satisfy an ordinarily sampled float binding (detail reports format, texture binding/name/label, and paired sampler identity); `VGPU-RING1-UNSUPPORTED` for unsupported effect/compute/target cases; `VGPU-TARGET-REQUIRED` when one-shot drawing needs an explicit target; `VGPU-TARGET-SIZE-REQUIRED` for runtime JS calls to `target(gpu)` without `size`; `VGPU-SURFACE-*` errors from `surface()`, surface resize, surface readback, or using disposed surfaces; plus method-specific `VGPU-R1-*`, `VGPU-R3-*`, and `VGPU-R4-*` errors documented on `Effect`, `Draw`, `Compute`, `Frame`, `Bundle`, `Target`, and `SharedUniforms`.

## Examples

```ts
import { init, draw, frame, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [128, 128], depth: true });
const drawable = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1, 0, 1, 1); }
  `,
  // optional sync pre-warm; `await draw.compile(target)` is preferred during browser load
  targets: [colorTarget],
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: colorTarget, clear: [0, 0, 0, 1] }, (pass) => pass.draw(drawable));
});
```

```ts
import { init, effect, frameLoop, surface } from "vgpu";

declare const canvas: HTMLCanvasElement;

const gpu = await init();
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const wave = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.4, 1.0, 1.0); }`);

frameLoop(gpu, (frame) => {
  frame.pass({ target: canvasSurface }, (pass) => pass.draw(wave));
});
```

## Error delivery

`gpu.onError(cb)` subscribes to asynchronous vgpu errors and returns an unsubscribe function. Listeners run in subscription order; removing one stops future deliveries; a throwing listener is reported to `console.error` without stopping the rest. If no listener is registered, vgpu reports the error to `console.error` by default.

`gpu.settled()` resolves after the current snapshot of pending error deliveries and in-flight pipeline work settles. It never rejects, so it is safe for deterministic tests and teardown.

## Notes

* There is no implicit screen property and no implicit default target. Pass `target` explicitly to frame passes and one-shot draws.
* Canvas-specific `size`, `dpr`, and `autoResize` live on `surface(gpu, canvas, opts)`, not on `init()`.
* Time is explicit JS state, and it lives on the clock, not on the context: read `clock(gpu).time` / `.deltaTime` / `.frameCount` and pass them through `set()` or `SharedUniforms` when shaders need them.
* Every factory rejects a disposed gpu with `VGPU-GPU-DISPOSED`, and an object vgpu did not create with `VGPU-GPU-FOREIGN`. Both are thrown synchronously, from the call that made the mistake.
* **See also:** `init`, `Clock`, `Surface`, `Effect`, `Draw`, `Compute`, `Frame`, `Target`, `Bundle`, `SharedUniforms`, `Timer`, `Visibility`.

## Sampled float texture layouts

vgpu infers sampled-texture layouts per selected WGSL entry point. A non-multisampled `texture_*<f32>` used by `textureSample*` or `textureGather*` with an ordinary sampler receives WebGPU `sampleType: "float"`; a texture used only by `textureLoad` remains `"unfilterable-float"`. Calls through helper functions are included.

The WGSL `f32` scalar type does not make every concrete texture format filterable. In particular, `r32float`, `rg32float`, and `rgba32float` require the device's `float32-filterable` feature for ordinary sampling. Use a filterable format, request that feature when supported, or use `textureLoad` without a sampler.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: init
description: Creates the public `Gpu` context. `init()` creates the device only; canvas-backed rendering is explicit through `surface(gpu, canvas, opts)`.
---

# init



## Import

```ts
import { init } from "vgpu";
```

Browser code imports from `vgpu`; Node GPU tests import from `vgpu/node`; deterministic unit tests import from `vgpu/mock`.

## Signature

```ts
import type { Gpu } from "vgpu";
import type { RequiredDeviceLimits, VGPUAdapter } from "vgpu/core";

declare function init(options?: InitOptions): Promise<Gpu>;

interface InitOptions {
  readonly adapter?: VGPUAdapter;
  readonly powerPreference?: GPUPowerPreference;
  readonly requiredFeatures?: readonly GPUFeatureName[];
  readonly requiredLimits?: RequiredDeviceLimits;
  readonly label?: string;
}
```

## Parameters

| Param                    | Type                        | Required | Default     | Notes                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------ | --------------------------- | -------: | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options                  | `InitOptions`               |        ✖ | `{}`        | Device creation options only. Canvas size, DPR, and auto-resize belong to `surface(gpu, canvas, opts)`.                                                                                                                                                                                                                                                        |
| options.adapter          | `VGPUAdapter`               |        ✖ | `undefined` | Explicit adapter. If omitted in `vgpu`, `navigator.gpu.requestAdapter()` is used; `vgpu/node` and `vgpu/mock` provide adapter factories.                                                                                                                                                                                                                       |
| options.powerPreference  | `GPUPowerPreference`        |        ✖ | `undefined` | Forwarded to `navigator.gpu.requestAdapter({ powerPreference })`.                                                                                                                                                                                                                                                                                              |
| options.requiredFeatures | `readonly GPUFeatureName[]` |        ✖ | `undefined` | Optional device features to enable, forwarded to `adapter.requestDevice` (e.g. `"depth-clip-control"` for `DrawOptions.unclippedDepth`). Checked against the adapter's supported features first: a name the adapter lacks fails init with `VGPU-FEATURE-UNSUPPORTED` instead of a native rejection. `device.features` reflects exactly the requested features. |
| options.requiredLimits   | `RequiredDeviceLimits`      |        ✖ | `undefined` | Forwarded unchanged to `adapter.requestDevice`. Unsupported names/values reject device creation.                                                                                                                                                                                                                                                               |
| options.label            | `string`                    |        ✖ | `undefined` | Reserved public option; current main API (`vgpu`) device creation does not use it as a debug label.                                                                                                                                                                                                                                                            |

**Returns:** `Promise<Gpu>` — the context every factory takes first: `surface(gpu, ...)`,
`target(gpu, ...)`, `draw(gpu, ...)`, `effect(gpu, ...)`, `compute(gpu, ...)`, `geometry(gpu, ...)`,
`frame(gpu, cb)` / `frameLoop(gpu, cb)`, `storage(gpu, ...)`, `uniforms(gpu, ...)`,
`sampler(gpu, ...)`, and `bundle(gpu, ...)`.

**Throws:** `VGPU-RING1-UNSUPPORTED` when WebGPU is unavailable, adapter request returns `null`, or an entrypoint lacks an adapter factory — use `vgpu/mock` in tests, `vgpu/node` in Node, or pass a valid adapter; `VGPU-FEATURE-UNSUPPORTED` when `requiredFeatures` names a feature the adapter does not support — remove the unsupported name(s) or run on an adapter that supports them.

## Examples

```ts
import { init, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64], format: "rgba8unorm" });
const shader = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.0, 1.0);
  }
`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: colorTarget }, (p) => p.draw(shader));
});
```

```ts
import { init, effect, frame, surface } from "vgpu";

declare const canvas: HTMLCanvasElement;

const gpu = await init({
  // Request only when a vertex entry actually reads storage and the adapter supports it.
  requiredLimits: { maxStorageBuffersInVertexStage: 1 },
});
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: canvasSurface }, (p) => p.draw(shader));
});
```

```ts
import { init, createMockAdapter, timer } from "vgpu/mock";

// Feature-gated device: GPU pass timing needs "timestamp-query".
const gpu = await init({
  adapter: createMockAdapter({ features: ["timestamp-query"] }),
  requiredFeatures: ["timestamp-query"],
});
const gpuTimer = timer(gpu);
gpuTimer.onResults((spans) => console.table(spans));
```

The granted feature makes `timer(gpu)` succeed. In a browser, drop the `adapter` option — `requiredFeatures` is forwarded to the real adapter the same way.

## Notes

* Choose the entrypoint for the runtime: use `vgpu` in browsers with WebGPU; use `vgpu/node` for headless Node rendering with a real adapter; use `vgpu/mock` for deterministic tests that do not require a GPU. Keep application code on the same `init(options?)` shape so the switch is local.
* Request optional features only when a code path uses them: `"timestamp-query"` enables `timer(gpu)`, `"depth-clip-control"` enables `DrawOptions.unclippedDepth`, and `"indirect-first-instance"` enables indirect draws that provide a non-zero first instance. Do not request features speculatively; unsupported names fail `init`.
* In tests, declare and request a mock feature explicitly: `init({ adapter: createMockAdapter({ features: ["timestamp-query"] }), requiredFeatures: ["timestamp-query"] })` lets you exercise feature gates instead of silently relying on defaults.
* `init(canvas)` is intentionally not supported. Create surfaces explicitly with `surface(gpu, canvas)`.
* `size`, `dpr`, and `autoResize` are `SurfaceOptions`, not `InitOptions`.
* The browser, node, and mock entrypoints all use the same `init(options?)` shape.
* In `vgpu/mock`, the default adapter declares no optional features. Pass `adapter: createMockAdapter({ features: [...] })` to test feature-gated paths deterministically; `requiredFeatures` outside that set fails with `VGPU-FEATURE-UNSUPPORTED`, and granted features appear on `gpu.device.features`.
* **See also:** `Gpu`, `Surface`, `Target`, `FrameRunner`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Surface
description: Canvas-backed render target created by `surface(gpu, canvas, opts)`. Use it for browser canvases, `OffscreenCanvas`, multi-canvas rendering, and resize-driven derived targets.
---

# Surface



## Import

```ts
import type { Surface, SurfaceOptions, SurfaceResizeEvent } from "vgpu";
```

## Signature

```ts
import type { Target } from "vgpu";

interface SurfaceOptions {
  readonly autoResize?: boolean;
  readonly dpr?: number | readonly [number, number];
  readonly size?: readonly [number, number];
  readonly format?: GPUTextureFormat;
  readonly alphaMode?: GPUCanvasAlphaMode;
  readonly colorSpace?: PredefinedColorSpace;
  readonly label?: string;
}

interface SurfaceResizeEvent {
  readonly width: number;
  readonly height: number;
  readonly dpr: number;
  readonly surface: Surface;
}

interface Surface extends Target {
  readonly canvas: HTMLCanvasElement | OffscreenCanvas;
  readonly context: GPUCanvasContext;
  readonly autoResize: boolean;
  readonly layoutBacked: boolean;
  readonly dpr: number;
  readonly disposed: boolean;
  onResize(cb: (event: SurfaceResizeEvent) => void): () => void;
  dispose(): void;
}
```

## Parameters

| Param               | Type                                   | Required | Default                                                                                                            | Notes                                                                                                                                                                                                                       |
| ------------------- | -------------------------------------- | -------: | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| surface.canvas      | `HTMLCanvasElement \| OffscreenCanvas` |        ✔ | —                                                                                                                  | Must return a `GPUCanvasContext` from `getContext("webgpu")`.                                                                                                                                                               |
| surface.opts        | `SurfaceOptions`                       |        ✖ | `{}`                                                                                                               | Canvas configuration and resize behavior.                                                                                                                                                                                   |
| opts.autoResize     | `boolean`                              |        ✖ | `true` for layout-backed canvases, `false` when `size` is provided or when the canvas has no numeric `clientWidth` | Auto-resize is checked at the frame boundary before user frame callbacks. Explicit `true` on buffer-only canvases throws.                                                                                                   |
| opts.dpr            | `number \| readonly [number, number]`  |        ✖ | `globalThis.devicePixelRatio ?? 1`                                                                                 | Number fixes DPR. Tuple clamps runtime DPR to `[min, max]`; layout-backed surfaces re-read DPR each frame.                                                                                                                  |
| opts.size           | `readonly [number, number]`            |        ✖ | Layout-backed: `clientWidth/clientHeight × dpr`; buffer-only: existing `canvas.width/height`                       | Physical pixel size. When provided, initial canvas buffer is set and `autoResize` defaults to `false`.                                                                                                                      |
| opts.format         | `GPUTextureFormat`                     |        ✖ | `navigator.gpu.getPreferredCanvasFormat() ?? "bgra8unorm"`                                                         | Canvas swapchain format.                                                                                                                                                                                                    |
| opts.alphaMode      | `GPUCanvasAlphaMode`                   |        ✖ | `"premultiplied"`                                                                                                  | Passed to `GPUCanvasContext.configure`.                                                                                                                                                                                     |
| opts.colorSpace     | `PredefinedColorSpace`                 |        ✖ | `"srgb"`                                                                                                           | Passed to `GPUCanvasContext.configure`.                                                                                                                                                                                     |
| opts.clearColor     | `ClearColor`                           |        ✖ | `[0, 0, 0, 1]`                                                                                                     | Default clear color of this surface, used by passes that clear without naming one. Writable at runtime as `surface.clearColor`; a pass `clear` color still wins for that pass. Four finite numbers, or a `GPUColor` object. |
| opts.label          | `string`                               |        ✖ | `undefined`                                                                                                        | Used in error messages and texture labels.                                                                                                                                                                                  |
| onResize.cb         | `(event: SurfaceResizeEvent) => void`  |        ✔ | —                                                                                                                  | Called synchronously immediately on subscription and after future size changes.                                                                                                                                             |
| event.width         | `number`                               |        ✔ | —                                                                                                                  | Physical pixel width, equal to `surface.size[0]` and `canvas.width`.                                                                                                                                                        |
| event.height        | `number`                               |        ✔ | —                                                                                                                  | Physical pixel height, equal to `surface.size[1]` and `canvas.height`.                                                                                                                                                      |
| event.dpr           | `number`                               |        ✔ | —                                                                                                                  | Effective DPR used for the current size.                                                                                                                                                                                    |
| event.surface       | `Surface`                              |        ✔ | —                                                                                                                  | Surface that resized, useful for shared handlers.                                                                                                                                                                           |
| surface.resize.size | `readonly [number, number]`            |        ✔ | —                                                                                                                  | Manual physical pixel size. Values are floored and clamped to at least `1`.                                                                                                                                                 |

**Returns:** `surface(gpu)` returns `Surface`; `onResize()` returns an unsubscribe function; `dispose()` returns `void`.

**Throws:** `VGPU-SURFACE-CONTEXT` when `getContext("webgpu")` returns `null`; `VGPU-SURFACE-DUPLICATE` when a live surface already owns the canvas; `VGPU-SURFACE-AUTORESIZE-UNSUPPORTED` for explicit `autoResize: true` on buffer-only canvases; `VGPU-SURFACE-DISPOSED` when using a disposed surface; `VGPU-SURFACE-RESIZE-REENTRANT` when resizing the same surface from its own resize callback; `VGPU-FRAME-REENTRANT` when `frame(gpu)` is called from any `onResize` callback. The immediate `onResize` fire on subscription also counts as being inside an `onResize` callback, so call `frame(gpu)` before subscribing or from code outside the callback.

## Examples

```ts
import { init, effect, frame, surface } from "vgpu";

declare const canvas: HTMLCanvasElement;

const gpu = await init();
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const wave = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.2, 0.6, 1, 1); }`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: canvasSurface }, (pass) => pass.draw(wave));
});
```

```ts
import { init, effect, frame, surface, target } from "vgpu/mock";

const gpu = await init();
declare const canvas: HTMLCanvasElement;
const canvasSurface = surface(gpu, canvas);

const bloomSize = (w: number, h: number): [number, number] => [w / 2, h / 2];
const bloom = target(gpu, { size: bloomSize(canvasSurface.size[0], canvasSurface.size[1]) });
const brightPass = effect(gpu, `
  struct Params { resolution: vec2f }
  @group(0) @binding(0) var<uniform> params: Params;
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
`, { set: { params: { resolution: bloom.size } } });
const composite = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

canvasSurface.onResize(({ width, height }) => {
  bloom.resize(bloomSize(width, height));
  brightPass.set({ params: { resolution: bloom.size } });
});

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: bloom }, (pass) => pass.draw(brightPass));
  currentFrame.pass({ target: canvasSurface }, (pass) => pass.draw(composite));
});
```

```ts
import { init, effect, frame, surface } from "vgpu";

declare const canvasA: HTMLCanvasElement;
declare const canvasB: HTMLCanvasElement;

const gpu = await init();
const main = surface(gpu, canvasA);
const preview = surface(gpu, canvasB, { autoResize: false, size: [320, 180] });
const shader = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: main }, (p) => p.draw(shader));
  currentFrame.pass({ target: preview }, (p) => p.draw(shader));
});
```

```ts
import { init, surface, target } from "vgpu";

declare const offscreen: OffscreenCanvas;
declare function postMessage(message: unknown): void;

const gpu = await init();
const canvasSurface = surface(gpu, offscreen);
const half = target(gpu, { size: [Math.max(1, canvasSurface.size[0] / 2), Math.max(1, canvasSurface.size[1] / 2)] });

canvasSurface.onResize(({ width, height }) => {
  half.resize([width / 2, height / 2]);
  postMessage({ type: "resized", width, height });
});

canvasSurface.resize([640, 360]);
```

```ts
import { init, bundle, effect, frame, surface } from "vgpu/mock";

declare const canvas: HTMLCanvasElement;

const gpu = await init();
const canvasSurface = surface(gpu, canvas);
const draw = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
let statics = bundle(gpu, { target: canvasSurface }, (recorded) => recorded.draw(draw));

// Drawing onto a resized surface keeps the same bundle valid as long as the render signature matches.
frame(gpu, (currentFrame) => currentFrame.pass({ target: canvasSurface }, (pass) => pass.bundles(statics)));
```

## Notes

* Use a `Surface` for the swapchain/backbuffer: it is an ephemeral current-frame render target, not a stable reusable or ping-pong intermediate. Use `target(gpu, ...)` for intermediate, reusable, sampleable/readable images; see `Target` for the contrast.
* A surface pass may be the final presentation pass; do not use a surface as a ping-pong resource. For post-processing, render into a `Target`, then sample it in a draw or effect targeting the surface in the same frame.
* Layout-backed detection is structural: `typeof canvas.clientWidth === "number"`; it does not use `instanceof`.
* Resize callbacks run in surface creation order at the frame boundary, before the user frame callback.
* Manual `surface.resize()` fires callbacks synchronously at the call site and works for `OffscreenCanvas`.
* `surface.read()` returns RGBA bytes. Canvas formats `bgra8unorm` and `bgra8unorm-srgb` are supported and swizzled to RGBA, which matters on platforms where `navigator.gpu.getPreferredCanvasFormat()` returns BGRA.
* `surface.readFloats()` returns the same pixels decoded to a `Float32Array` of components (`unorm8` canvas formats normalized to `[0, 1]`); it is the readback to use if a surface is ever configured with a float format.
* A canvas can have only one live surface. Call `surface.dispose()` before creating another one for the same canvas.
* **See also:** `init`, `surface`, `Target`, `Frame`, `Bundle`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Target
description: Offscreen render target abstraction used by passes, draws, bundles, and ping-pong resources. Targets own size, color formats, optional depth, MSAA resolve textures, and readback. Canvas-backed targets are `Surface` instances created with `surface(gpu, canvas)`.
---

# Target



## Import

```ts
import type { Target, TargetOptions, TargetTextureOptions, PingPongTargets, PingPongStorage } from "vgpu";
```

## Signature

```ts
import type { ClearColor } from "vgpu";
import type { ResourceDestroyCallback, ResourceIdentity, Texture, UnsubscribeResourceDestroy } from "vgpu/core";

interface TargetTextureOptions {
  readonly format?: GPUTextureFormat;
  readonly colors?: readonly { readonly format: GPUTextureFormat }[];
  readonly depth?: boolean | GPUTextureFormat;
  readonly msaa?: boolean | 4;
  readonly label?: string;
}

interface TargetOptions extends TargetTextureOptions {
  readonly size: readonly [number, number];
}

interface Target {
  readonly gpu: unknown;
  readonly size: readonly [number, number];
  readonly texelSize: readonly [number, number];
  readonly color: Texture;
  readonly colors: readonly [Texture, ...Texture[]];
  readonly depth?: Texture;
  readonly format: GPUTextureFormat;
  readonly sampleCount: 1 | 4;
  readonly resourceIdentity: ResourceIdentity;
  resize(size: readonly [number, number]): void;
  read(): Promise<Uint8Array>;
  readFloats(): Promise<Float32Array>;
  onDestroy(cb: ResourceDestroyCallback<Target>): UnsubscribeResourceDestroy;
  renderPassDescriptor(opts?: {
    readonly clear?: ClearColor;
    readonly preserve?: boolean;
    readonly clearDepth?: number;
    readonly clearStencil?: number;
    readonly depthReadOnly?: boolean;
  }): GPURenderPassDescriptor;
}

interface PingPongTargets { readonly read: Target; readonly write: Target; swap(): void; }
interface PingPongStorage { readonly read: import("vgpu").StorageBuffer; readonly write: import("vgpu").StorageBuffer; swap(): void; }
```

## Parameters

| Param                                     | Type                                      | Required | Default                                     | Notes                                                                                                                                                                                                                                                                                                          |
| ----------------------------------------- | ----------------------------------------- | -------: | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| target.opts                               | `TargetOptions`                           |        ✔ | —                                           | Creates an offscreen target. `size` is mandatory.                                                                                                                                                                                                                                                              |
| opts.size                                 | `readonly [number, number]`               |        ✔ | —                                           | Initial offscreen texture size in physical pixels.                                                                                                                                                                                                                                                             |
| opts.format                               | `GPUTextureFormat`                        |        ✖ | `"rgba8unorm"`                              | Used for single-color targets when `colors` is omitted.                                                                                                                                                                                                                                                        |
| opts.colors                               | `readonly { format: GPUTextureFormat }[]` |        ✖ | `[{ format: opts.format ?? "rgba8unorm" }]` | Multiple render targets (MRT): one attachment per entry, all written by one pass — the G-buffer layout for deferred shading. `target.color` is `colors[0]`.                                                                                                                                                    |
| opts.depth                                | `boolean \| GPUTextureFormat`             |        ✖ | `undefined`                                 | `true` means `"depth24plus"`; a string uses that depth format; omitted means no depth. Combined depth-stencil formats such as `"depth24plus-stencil8"` are supported; stencil-only `"stencil8"` is rejected.                                                                                                   |
| opts.msaa                                 | `boolean \| 4`                            |        ✖ | `false` / sample count `1`                  | Only `true` or `4` enables MSAA, creating color/depth attachments with sample count `4` and resolving to sampleable `.color(s)`.                                                                                                                                                                               |
| opts.clearColor                           | `ClearColor`                              |        ✖ | `[0, 0, 0, 1]`                              | Default clear color of this target, used by passes that clear without naming one. Writable at runtime as `target.clearColor`; a pass `clear` color still wins for that pass. Four finite numbers, or a `GPUColor` object.                                                                                      |
| opts.label                                | `string`                                  |        ✖ | `undefined`                                 | Prefix for created texture labels.                                                                                                                                                                                                                                                                             |
| target.resize.size                        | `readonly [number, number]`               |        ✔ | —                                           | Recreates offscreen textures unless size is unchanged.                                                                                                                                                                                                                                                         |
| target.read                               | —                                         |        — | —                                           | No parameters; reads `target.color` and returns its raw unpadded texel bytes (4 per texel for `rgba8unorm`, 8 for `rgba16float`, 16 for `rgba32float`). `bgra8unorm` / `bgra8unorm-srgb` are supported and swizzled to RGBA, matching canvas preferred formats on platforms such as macOS.                     |
| target.readFloats                         | —                                         |        — | —                                           | No parameters; reads `target.color` and decodes it to one f32 per component — the HDR readback for `rgba16float` / `rgba32float` targets. `unorm8` formats decode to `[0, 1]`.                                                                                                                                 |
| target.onDestroy.cb                       | `ResourceDestroyCallback<Target>`         |        ✔ | —                                           | Subscribes to target destruction.                                                                                                                                                                                                                                                                              |
| target.renderPassDescriptor.clear         | `ClearColor`                              |        ✖ | `[0, 0, 0, 1]`                              | Clear color for all color attachments unless `preserve` is true. `Frame.pass` supplies `target.clearColor` for omitted/`true` clears and a per-pass color when provided.                                                                                                                                       |
| target.renderPassDescriptor.preserve      | `boolean`                                 |        ✖ | `false`                                     | Optional implementer hook used by `Frame.pass({ clear: false })`; when true, color and depth attachments should load existing contents and omit clear values.                                                                                                                                                  |
| target.renderPassDescriptor.clearDepth    | `number`                                  |        ✖ | `1`                                         | Depth clear value used when the pass clears. `Frame.pass` supplies `FramePassOptions.clearDepth`; ignored while preserving and on targets without depth.                                                                                                                                                       |
| target.renderPassDescriptor.clearStencil  | `number`                                  |        ✖ | `0`                                         | Stencil clear value used when the pass clears. `Frame.pass` supplies `FramePassOptions.clearStencil`; ignored while preserving and on targets whose depth format has no stencil aspect.                                                                                                                        |
| target.renderPassDescriptor.depthReadOnly | `boolean`                                 |        ✖ | `false`                                     | When true, the depth-stencil attachment is built with `depthReadOnly: true` and omits `depthLoadOp`/`depthStoreOp`, as WebGPU requires for read-only aspects; formats with a stencil aspect also set `stencilReadOnly: true` and omit the stencil ops. `Frame.pass` supplies `FramePassOptions.depthReadOnly`. |
| pingPong.width                            | `number`                                  |        ✔ | —                                           | Floored and clamped to at least `1`.                                                                                                                                                                                                                                                                           |
| pingPong.height                           | `number`                                  |        ✔ | —                                           | Floored and clamped to at least `1`.                                                                                                                                                                                                                                                                           |
| pingPong.opts                             | `TargetTextureOptions`                    |        ✖ | `{}`                                        | Texture options for both targets. Size is intentionally not accepted; positional width/height win.                                                                                                                                                                                                             |
| pingPongStorage.bytes                     | `number`                                  |        ✔ | —                                           | Creates two `"read-write"` storage buffers.                                                                                                                                                                                                                                                                    |

**Returns:** `target(gpu)` returns `Target`; `resize()` returns `void`; `read()` returns `Promise<Uint8Array>`; `readFloats()` returns `Promise<Float32Array>`; `renderPassDescriptor(opts?)` returns a WebGPU render pass descriptor; `pingPong(gpu)` returns `PingPongTargets`; `pingPongStorage(gpu)` returns `PingPongStorage`.

**Throws:** `VGPU-CORE-UNSUPPORTED-FORMAT` when `read()` / `readFloats()` runs on a color format outside the readback table (see `Texture`); `VGPU-TARGET-SIZE-REQUIRED` when runtime JS calls `target(gpu)` without `size`; `VGPU-TARGET-MSAA-INVALID` when runtime JS passes an unsupported `msaa` value (only `true` / `4` are accepted); `VGPU-TARGET-DEPTH-STENCIL-ONLY` when `depth` receives the stencil-only `"stencil8"` format (stencil-only depth targets are not supported yet); `VGPU-RING1-UNSUPPORTED` when `msaa: true` / `4` with `rgba16float` is used on a Dawn compatibility-mode device; underlying core texture/readback operations can throw native WebGPU validation errors.

## Examples

```ts
import { init, effect, frame, target } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [128, 128], format: "rgba16float", depth: true, msaa: true });
const post = effect(gpu, `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f { return vec4f(uv, 0, 1); }
`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: scene, clear: [0, 0, 0, 1] }, (pass) => pass.draw(post));
});
```

```ts
import { init, draw, frame, target } from "vgpu/mock";

const gpu = await init();
// G-buffer for deferred shading: albedo, normals, material parameters.
const gbuffer = target(gpu, {
  size: [512, 512],
  colors: [{ format: "rgba8unorm" }, { format: "rgba16float" }, { format: "rgba8unorm" }],
  depth: true,
});
const fill = draw(gpu, {
  shader: `
    @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
      var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
      return vec4f(p[vi], 0, 1);
    }
    struct GBuffer { @location(0) albedo: vec4f, @location(1) normal: vec4f, @location(2) material: vec4f }
    @fragment fn fs_main() -> GBuffer {
      return GBuffer(vec4f(0.8, 0.2, 0.2, 1), vec4f(0, 0, 1, 0), vec4f(0.5, 0.1, 0, 0));
    }
  `,
});

frame(gpu, (currentFrame) => {
  currentFrame.pass(gbuffer, fill); // one draw fills all three attachments
});
```

One geometry pass fills every G-buffer attachment; a later lighting effect samples them as `gbuffer.colors[0]`–`[2]`. Per-attachment blend/write-mask overrides for MRT draws live on `DrawOptions.colors`.

```ts
import { init, effect, surface, target } from "vgpu/mock";

const gpu = await init();
const canvasSurface = surface(gpu, mockCanvas());
const bloomSize = (w: number, h: number): [number, number] => [w / 2, h / 2];
const bloom = target(gpu, { size: bloomSize(canvasSurface.size[0], canvasSurface.size[1]) });
const bright = effect(gpu, `
  struct Params { resolution: vec2f }
  @group(0) @binding(0) var<uniform> params: Params;
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
`, { set: { params: { resolution: bloom.size } } });

canvasSurface.onResize(({ width, height }) => {
  bloom.resize(bloomSize(width, height));
  bright.set({ params: { resolution: bloom.size } });
});

function mockCanvas(): HTMLCanvasElement {
  return {
    width: 10,
    height: 10,
    clientWidth: 10,
    clientHeight: 10,
    getContext() { return { configure() {}, unconfigure() {}, getCurrentTexture() { return { createView: () => ({}) }; } }; },
  } as unknown as HTMLCanvasElement;
}
```

```ts
import { init, effect, frame, target } from "vgpu/mock";

const gpu = await init();
// HDR target: readFloats() decodes the half-float texels, read() would hand back raw bytes.
const hdr = target(gpu, { size: [64, 64], format: "rgba16float" });
const bloom = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(4.0, 2.0, 1.0, 1.0); }`);

frame(gpu, (currentFrame) => currentFrame.pass(hdr, bloom));

const floats = await hdr.readFloats(); // Float32Array, 64 * 64 * 4 components
console.log(floats[0]); // 4 — values above 1 survive the readback
```

```ts
import { init, effect, frame, pingPong } from "vgpu/mock";

const gpu = await init();
const pair = pingPong(gpu, 32.9, 32.1, { format: "rgba8unorm" });
const blur = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

frame(gpu, (currentFrame) => {
  currentFrame.pass({ target: pair.write, clear: false }, (pass) => pass.draw(blur));
});
pair.swap();
```

## Notes

* Choose `Target` for offscreen intermediates that must be reused, sampled, read back, or ping-ponged; choose `Surface` only for the canvas swapchain (see `Surface`). A target can be rendered in multiple passes and sampled by later effects.
* Use simple `format` for one color attachment. Use `colors` when a pass writes multiple attachments (MRT/G-buffer), then consume `target.colors[i]` in later lighting/post passes.
* Set `depth: true` for ordinary z-testing; choose `depth: "depth24plus-stencil8"` when stencil masking is required. Enable `msaa: true`/`4` for anti-aliased 3D geometry, but do not combine MSAA with `clear: false` preservation or `depthReadOnly`: the internal multisample render attachments (including depth) are discarded, while the resolved `.color(s)` remain sampleable/readable.
* `target.read()` / `target.readFloats()` are intended for tests, snapshots, and diagnostics—not a per-frame hot path. For iterative simulation or post-processing, use `pingPong(gpu, ...)` and swap targets instead of readback.
* There is no global resolution binding. Pass `target.size` or `target.texelSize` explicitly to shaders.
* `Surface.color` wraps the canvas current texture; offscreen target colors are stable until resize/destroy.
* `target.read()` and `surface.read()` return raw texel bytes in the target's own color format, with row padding removed and BGRA canvas formats swizzled to RGBA. For `rgba8unorm` targets that is exactly the previous RGBA byte layout.
* Float targets (`rgba16float`, `rgba32float`, `r16float`, `r32float`, `rg16float`, `rg32float`) read back through `target.readFloats()`, which decodes half/float texels into a `Float32Array` of components — HDR values above `1` and negatives are preserved. `readFloats()` also works on `unorm8` targets (normalized to `[0, 1]`), so tooling can stay format-agnostic.
* Custom `Target` implementers must provide `readFloats()`; delegating to `this.color.readFloats()` (as `target(gpu)` and `surface(gpu)` do) is enough.
* Size-dependent targets derived from a surface should be created from the real initial `surface.size` and resized from `surface.onResize(...)`.
* Custom `Target` implementers should honor the optional `renderPassDescriptor(opts?)` options-bag fields to participate in `Frame.pass({ clear: false })`, `FramePassOptions.clearDepth`, `FramePassOptions.clearStencil`, and `FramePassOptions.depthReadOnly`; implementations that ignore a field will clear (with depth `1`, stencil `0`) instead.
* Depth formats with a stencil aspect (`"depth24plus-stencil8"`, `"depth32float-stencil8"`) emit `stencilLoadOp`/`stencilStoreOp` on the pass depth-stencil attachment, mirroring the depth load/store behavior with `stencilClearValue` from `FramePassOptions.clearStencil` (default `0`), as WebGPU requires when the stencil aspect is writable.
* `target.depth` is created with `texture_binding` usage in addition to `render_attachment`, so it can be bound with `set()` as a sampled depth texture — including inside the same pass when it opened with `FramePassOptions.depthReadOnly`.
* **See also:** `Surface`, `FramePassOptions`, `Effect`, `Draw`, `Bundle`, `Compute` storage ping-pong.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Timer
description: GPU pass timer created by `timer(gpu)`; requires the `"timestamp-query"` device feature. Use it to find the expensive pass before optimizing: encoders only record, so CPU timing says nothing about GPU cost. Mark a pass with `FramePassOptions.timer` and the GPU brackets it with a begin/end timestamp pair. Decoded durations land in `timer.onResults`, in milliseconds, 1–2 frames after submit.
---

# Timer



## Import

```ts
import type { Timer, TimerSpan } from "vgpu";
```

## Signature

```ts
interface TimerSpan {
  readonly name: string;
}

interface Timer {
  span(name: string): TimerSpan;
  onResults(cb: (spans: Readonly<Record<string, number>>) => void): () => void;
  dispose(): void;
}
```

## Parameters

| Param              | Type                                                | Required | Default | Notes                                                                                                          |
| ------------------ | --------------------------------------------------- | -------: | ------- | -------------------------------------------------------------------------------------------------------------- |
| timer()            | —                                                   |        — | —       | No parameters.                                                                                                 |
| timer.span.name    | `string`                                            |        ✔ | —       | Non-empty result key. Spans are memoized per name, so `timer.span("shadows")` is allocation-free in hot loops. |
| timer.onResults.cb | `(spans: Readonly<Record<string, number>>) => void` |        ✔ | —       | Receives one frozen `name → milliseconds` record per timed frame.                                              |

**Returns:** `timer(gpu)` returns `Timer`; `span()` returns a `TimerSpan` to pass as `FramePassOptions.timer`; `onResults()` returns an unsubscribe function; `dispose()` returns `void`.

**Throws:**

* `VGPU-TIMER-INVALID` when `timer(gpu)` runs on a device without `"timestamp-query"` — request it: `init({ requiredFeatures: ["timestamp-query"] })`.
* `VGPU-TIMER-INVALID` for an empty or non-string span name — name each timed pass, e.g. `timer.span("shadows")`.
* `VGPU-TIMER-INVALID` for a span name reused within one frame (each name holds one begin/end pair per frame) — give the second pass its own span.
* `VGPU-TIMER-INVALID` for a non-`TimerSpan` `FramePassOptions.timer` value, or a span used with another gpu's frames — pass only `timer.span(name)` results, one timer per gpu.
* `VGPU-TIMER-INVALID` for any use of a disposed timer or its spans — create a new timer with `timer(gpu)`.
* `VGPU-TIMER-CAPACITY` when one frame times more than 2048 spans; a timer owns one timestamp query set and WebGPU `createQuerySet` caps `count` at 4096 (2 queries per span) — time fewer passes, or spread timing across frames.

## Examples

```ts
import { init, createMockAdapter, effect, frameLoop, target, timer } from "vgpu/mock";

const gpu = await init({ adapter: createMockAdapter({ features: ["timestamp-query"] }), requiredFeatures: ["timestamp-query"] });
const shadowMap = target(gpu, { size: [512, 512], depth: true });
const scene = target(gpu, { size: [256, 256], depth: true });
const casters = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0); }`);
const world = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);

const gpuTimer = timer(gpu);
gpuTimer.onResults((spans) => {
  console.log(`shadows ${spans.shadows}ms, main ${spans.main}ms`);
});

const loop = frameLoop(gpu, (f) => {
  f.pass({ target: shadowMap, timer: gpuTimer.span("shadows") }, (p) => p.draw(casters));
  f.pass({ target: scene, timer: gpuTimer.span("main") }, (p) => p.draw(world));
});
loop.stop();
```

```ts
import { init, timer } from "vgpu";

// Optional timing: only request the feature when the adapter has it.
const gpu = await init({ requiredFeatures: ["timestamp-query"] });
const gpuTimer = gpu.device.features.has("timestamp-query") ? timer(gpu) : undefined;
gpuTimer?.onResults((spans) => console.table(spans));
```

## Notes

* Start with a `Timer` before optimizing a suspected bottleneck: CPU wall-clock timings measure submission/encoding, not GPU execution. Results are asynchronous and normally arrive one to two frames after submit, so use them as a rolling signal rather than an immediate branch condition.
* Results are GPU durations decoded from timestamp pairs: `(end - begin)` nanosecond ticks converted to milliseconds. Timestamp values are implementation-defined, and WebGPU notes the counter "may reset ... which can result in unexpected values such as negative deltas"; vgpu clamps negative deltas to `0` instead of reporting garbage.
* Readback never blocks a frame: results resolve through rotated staging buffers, and when readbacks lag more frames than the ring holds, that frame's results are dropped rather than awaited. A dropped frame is dropped **whole** — no resolve is encoded at all, `onResults` simply does not fire for it, and nothing is merged or partially applied; the next frame that finds a free staging buffer reports again. `await gpu.settled()` covers pending readbacks for deterministic tests and teardown.
* One timer can be used from several frames that are open at the same time, but results stay scoped to the newest one: opening a frame retargets the timer's per-frame bookkeeping, so an older frame submitted afterwards encodes no resolve and reports nothing (its spans are dropped, never merged into the newer frame's results). Submitting it is always safe, however long it stayed open — the query set it referenced is kept alive for it.
* A manual `frame(gpu)` that attached a span holds the timer's query set until you `submit()` or `cancel()` it (a failed frame releases it too). Dropping such a frame without either leaks those resources for the lifetime of the gpu — the same leak as a native `GPUCommandEncoder` you never `finish()` — because a frame is never assumed abandoned: it could still be submitted. Always close the frames you open — `submit()` them, or `frame.cancel()` the ones you decided not to submit, which releases the query set without encoding a resolve or reporting a result — or let `frame(gpu, cb)` do it for you; `gpu.dispose()` (or device loss) is the backstop.
* Results apply in submission order; a stale readback that lands after a newer one is discarded **entirely** — the whole set of spans from that frame is thrown away, never merged into the newer results. A readback that fails outright (device lost while mapping) is discarded the same way and reported on `gpu.onError` as `VGPU-QUERY-READBACK`; it never rejects a frame or `gpu.settled()`.
* Capacity starts at 32 spans per frame and grows only at frame boundaries — a pass that overflows the current query set goes untimed for that frame, and the next frame's larger set covers it. For the hard per-frame limit, see `VGPU-TIMER-CAPACITY` above.
* `dispose()` releases the timer's query set and resolve/staging buffers after in-flight readbacks settle. Calling it mid-frame is safe: every frame that attached a span still references the query set from its pass descriptors, so destruction is deferred until each of those frames reports back — submitted, failed or abandoned — including when several manual `frame(gpu)`s are open at once, each of which holds its own reference. In-flight readbacks still apply, so results already submitted are not lost. `gpu.dispose()` disposes the timers that gpu created. Create the timer once and reuse its spans.
* Timing granularity is the render pass: the pair lands in the pass descriptor's `timestampWrites` (`beginningOfPassWriteIndex`/`endOfPassWriteIndex`), so a span measures the whole pass, not individual draws.
* **See also:** `timer`, `Frame`, `FramePassOptions.timer`, `init`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Uniform
description: Low-level user-owned uniform buffer with a stable bind group at binding `0`. Prefer main API (`vgpu`) `set({ params: ... })` for ordinary values; use `Uniform` when you need byte-level writes or one buffer shared by many draws.
---

# Uniform



## Import

```ts
import { Uniform } from "vgpu/core";
import type { UniformOptions } from "vgpu/core";
```

## Signature

```ts
import type { Device } from "vgpu/core";

declare interface UniformOptions {
  readonly size: number;
  readonly label?: string;
  readonly visibility?: GPUShaderStageFlags;
  readonly bindGroupLayout?: GPUBindGroupLayout;
}

declare class Uniform {
  readonly device: Device;
  readonly size: number;
  readonly buffer: import("vgpu/core").Buffer;
  readonly bindGroupLayout: GPUBindGroupLayout;
  readonly bindGroup: GPUBindGroup;
  constructor(device: Device, opts: UniformOptions);
  get gpu(): GPUBuffer;
  write(data: BufferSource, offset?: number): void;
  destroy(): void;
  dispose(): void;
}
```

## Parameters

| Param                | Type                  | Required | Default                                                                           | Notes                                                                                                   |
| -------------------- | --------------------- | -------: | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| device               | `Device`              |        ✔ | —                                                                                 | Core device, usually `gpu.device` from the main API (`vgpu`) `init()`.                                  |
| opts                 | `UniformOptions`      |        ✔ | —                                                                                 | Buffer/layout options.                                                                                  |
| opts.size            | `number`              |        ✔ | —                                                                                 | Byte size. Used for `device.createBuffer({ usage: ["uniform", "copy_dst"] })` and `minBindingSize`.     |
| opts.label           | `string`              |        ✖ | `undefined`                                                                       | Forwarded to buffer label; layout label becomes `${label}.bgl`; bind group label becomes `${label}.bg`. |
| opts.visibility      | `GPUShaderStageFlags` |        ✖ | `GPUShaderStage.VERTEX \| GPUShaderStage.FRAGMENT` with numeric fallback `1 \| 2` | Ignored when `opts.bindGroupLayout` is supplied.                                                        |
| opts.bindGroupLayout | `GPUBindGroupLayout`  |        ✖ | A new binding-0 uniform layout                                                    | Reuse a pipeline/draw-owned layout. Binding `0` must be a compatible uniform buffer.                    |
| uniform.write.data   | `BufferSource`        |        ✔ | —                                                                                 | Bytes uploaded with `queue.writeBuffer`.                                                                |
| uniform.write.offset | `number`              |        ✖ | `0`                                                                               | Destination byte offset in the buffer.                                                                  |

**Returns:** Constructor returns `Uniform`; `gpu` returns the underlying `GPUBuffer`; `write()`, `destroy()`, and `dispose()` return `void`.

**Throws:** No main API (`vgpu`) `VGPU-*` errors are thrown directly by `Uniform`; invalid sizes, incompatible reused layouts, out-of-range writes, or destroyed-buffer usage can surface as core/native WebGPU validation errors. Binding a `Uniform` through main API (`vgpu`) can still trigger `VGPU-R1-OWNERSHIP-FLIP` if the same shader binding was first set with JS values.

## Examples

```ts
import { init, draw } from "vgpu/mock";
import { Uniform } from "vgpu/core";

const gpu = await init();
const camera = new Uniform(gpu.device, { size: 64, label: "camera" });
camera.write(new Float32Array(16));

const drawable = draw(gpu, { shader: `
  struct Camera { viewProjection: mat4x4f }
  @group(0) @binding(0) var<uniform> camera: Camera;
  @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
    var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
    return camera.viewProjection * vec4f(p[vi], 0, 1);
  }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }
` });
drawable.set({ camera });
```

```ts
import { init, draw } from "vgpu/mock";
import { Uniform } from "vgpu/core";

const gpu = await init();
const drawable = draw(gpu, { shader: `
  struct Params { value: f32 }
  @group(0) @binding(0) var<uniform> params: Params;
  @vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); }
  @fragment fn fs_main() -> @location(0) vec4f { return vec4f(params.value); }
` });
const params = new Uniform(gpu.device, { size: 16, bindGroupLayout: drawable.layout(0) });
params.write(new Float32Array([1, 0, 0, 0]));
drawable.set({ params });
```

## Notes

* `Uniform` is user-owned from the first `draw.set({ name: uniform })`; vgpu binds its identity and never packs JS values into it.
* It creates a non-dynamic bind group. For many per-object uniforms with dynamic offsets, use `UniformPool` instead.
* Call `destroy()` / `dispose()` when the buffer lifetime ends.
* **See also:** `SharedUniforms`, `StructuredUniform`, `UniformPool`, `Draw.set`, `Effect.set`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: SharedUniforms
description: Values-first shared uniform/storage object created by `uniforms(gpu, values)`. It adopts binary WGSL layout lazily from the first compatible shader binding and reuses one stable buffer across shaders.
---

# SharedUniforms



## Import

```ts
import type { SharedUniforms } from "vgpu";
```

## Signature

```ts
interface SharedUniforms<T extends Record<string, unknown> = Record<string, unknown>> {
  set(values: Partial<T>): void;
}
```

## Parameters

| Param             | Type                                | Required | Default | Notes                                                                                                                   |
| ----------------- | ----------------------------------- | -------: | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| uniforms.values   | `T extends Record<string, unknown>` |        ✔ | —       | Initial values are cloned. Layout and buffer are not created until first binding to a reflected uniform/storage buffer. |
| shared.set.values | `Partial<T>`                        |        ✔ | —       | Deep-merges plain objects and clones arrays/typed arrays before writing current values to the adopted layout.           |

**Returns:** `uniforms(gpu)` returns `SharedUniforms<T>`; `shared.set()` returns `void`.

**Throws:** `VGPU-R1-SHARED-UNIFORMS-LAYOUT-MISMATCH` when a later shader declares a structurally different layout for the same shared object; `VGPU-RING1-UNSUPPORTED` when address spaces differ, the binding is not a buffer, the binding has no host-shareable layout, or the layout is runtime-sized; packing may throw core validation errors for values that do not match the adopted WGSL layout.

## Examples

```ts
import { init, clock, effect, frame, target, uniforms } from "vgpu/mock";

const gpu = await init();
const colorTarget = target(gpu, { size: [64, 64] });
const globals = uniforms(gpu, { time: 0, mouse: [0, 0] });
const wave = effect(gpu, `
  struct Globals { time: f32, mouse: vec2f }
  @group(0) @binding(0) var<uniform> globals: Globals;
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(globals.time) * 0.5 + 0.5, 1);
  }
`, { set: { globals } });

globals.set({ time: clock(gpu).time });
frame(gpu, (currentFrame) => currentFrame.pass({ target: colorTarget }, (pass) => pass.draw(wave)));
```

```ts
import { init, uniforms } from "vgpu/mock";

const gpu = await init();
const globals = uniforms(gpu, { exposure: 1, tint: [1, 1, 1] });
globals.set({ exposure: 1.25 });
```

## Notes

* The first shader to bind the object chooses the WGSL layout. Keep struct member names/types/order aligned for every later shader that reuses it.
* Use shared uniforms for values like time, mouse, camera, exposure, and viewport data consumed by many passes.
* If one shader needs a different layout, create a second `uniforms(gpu)` object rather than mutating the first layout.
* **See also:** `uniforms`, `Effect.set`, `Draw.set`, `Uniform`, `StructuredUniform`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Visibility
description: GPU occlusion query handles created by `visibility(gpu)`; core WebGPU, no device feature required. Use them for occlusion culling: skipping expensive draws behind occluders in urban streets, interiors, and dense foliage. The pattern is two-phase culling. This frame, draw a cheap proxy under a query; next frame, skip the real object once confirmed hidden. Open the pass with `FramePassOptions.visibility`, wrap proxies in `pass.occlusion(handle, body)`, and gate real draws on `handle.hidden`. Results latch into the handle 1–2 frames after submit.
---

# Visibility



## Import

```ts
import type { Visibility, VisibilityOptions, VisibilityQuery } from "vgpu";
```

## Signature

```ts
interface VisibilityOptions {
  readonly capacity?: number;
}

interface Visibility {
  query(label: string): VisibilityQuery;
  reset(): void;
  dispose(): void;
}

interface VisibilityQuery {
  readonly label: string;
  readonly hidden: boolean;
  readonly state: "visible" | "hidden" | "unknown";
  readonly age: number;
  reset(): void;
  dispose(): void;
}
```

## Parameters

| Param              | Type                                 | Required | Default     | Notes                                                                                                                                                                                                                                          |
| ------------------ | ------------------------------------ | -------: | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| visibility.options | `VisibilityOptions`                  |        ✖ | `{}`        | Optional; `visibility(gpu)` equals `visibility(gpu, {})`.                                                                                                                                                                                      |
| options.capacity   | `number`                             |        ✖ | `64`        | Query slots per frame — the size of the one occlusion query set this instance owns. It never grows: the set is bound to pass descriptors mid-frame, so capacity is a declared contract. Size it to the number of handles queried in one frame. |
| vis.query.label    | `string`                             |        ✔ | —           | Non-empty result key. Handles are stable — create them once outside the loop. A label stays claimed until its handle is disposed.                                                                                                              |
| query.hidden       | `boolean`                            |        — | `false`     | `true` only when a completed query confirmed zero passing samples (and no reset since). `"unknown"` and `"visible"` read as `false`: the safe default is to draw.                                                                              |
| query.state        | `"visible" \| "hidden" \| "unknown"` |        — | `"unknown"` | Latched result. `"visible"`: the last completed query saw at least one passing sample. `"unknown"`: no result since creation or the last reset.                                                                                                |
| query.age          | `number`                             |        — | `Infinity`  | Frames since the last applied result; `Infinity` before the first. Use it to distrust stale answers after the camera moved.                                                                                                                    |

**Returns:** `visibility(gpu)` returns `Visibility`; `query()` returns a stable `VisibilityQuery` handle; `reset()` and `dispose()` return `void`.

**Throws:**

* `VGPU-VIS-CAPACITY-LIMIT` when `capacity` is not an integer in `[1, 4096]` (WebGPU `createQuerySet` caps `count` at 4096) — lower it, or create several visibility instances.
* `VGPU-VIS-LABEL-DUPLICATE` when `vis.query()` receives a label that is already live — reuse the existing handle, or `dispose()` the old one first.
* `VGPU-VIS-DISPOSED` for any use of a disposed handle or instance — create a fresh one with `vis.query(label)` / `visibility(gpu)`.
* `VGPU-VIS-INVALID` for an empty or non-string label — label each queried object, e.g. `vis.query("statue")`.
* `VGPU-VIS-INVALID` for mismatched plumbing: a non-`Visibility` pass option, a non-`VisibilityQuery` `occlusion()` argument, a handle from another instance, or an instance used with another gpu's frames — keep one instance per gpu and pass only its own handles.
* Pass-side errors (`VGPU-VIS-NO-DEPTH`, `VGPU-VIS-CAPACITY`, `VGPU-QUERY-NO-VISIBILITY`, `VGPU-QUERY-NESTED`, `VGPU-QUERY-DUPLICATE`) are documented on `Frame`.

## Examples

```ts
import { init, draw, effect, frameLoop, target, visibility } from "vgpu/mock";

const gpu = await init();
const scene = target(gpu, { size: [256, 256], depth: true });
const world = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
const statue = draw(gpu, { shader: `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.5); }` });
const statueProxy = draw(gpu, {
  shader: `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0); }`,
  writeMask: [],           // write no color channels
  depth: { write: false }, // test depth, never write it
});

const vis = visibility(gpu, { capacity: 8 });
const qStatue = vis.query("statue");

const loop = frameLoop(gpu, (f) => {
  f.pass({ target: scene, visibility: vis }, (p) => {
    p.draw(world);                       // occluders fill depth first
    p.occlusion(qStatue, statueProxy);   // bounding proxy under the query
    if (!qStatue.hidden) p.draw(statue); // skip the real draw once confirmed hidden
  });
});
loop.stop();
```

The proxy must test against the scene without touching it: `writeMask: []` writes no color and `depth: { write: false }` tests but never writes depth — a writing proxy would stamp its pixels into the image. The query counts samples that pass the tests, so a non-writing draw still measures visibility; once a readback confirms zero passing samples, `qStatue.hidden` flips and the statue is skipped.

```ts
import { init, visibility } from "vgpu/mock";

// Camera cut / teleport: last frame's occlusion answers are meaningless — reset to "unknown"
// so everything draws until fresh results land.
const gpu = await init();
const vis = visibility(gpu);
const q = vis.query("statue");

function onCameraTeleport(): void {
  vis.reset(); // every handle: state "unknown", hidden false, age Infinity
}
onCameraTeleport();
console.log(q.state, q.hidden, q.age); // "unknown" false Infinity
```

## Notes

* Use visibility for scenes with many expensive objects and large occluders (interiors, city blocks, dense foliage). It is a cheap proxy, not a universal culling system: small/overlapping occluders can produce popping, and every query requires a depth-enabled target.
* Results are **zero vs non-zero only**, mirroring WebGPU occlusion query semantics: a resolved value of `0` means no samples passed depth testing inside the scope; any non-zero value is unspecified. vgpu decodes that to `"hidden"` / `"visible"` and never exposes a sample count.
* The occlusion scope body **always executes** — the proxy draw is what the GPU measures, so it cannot be skipped. Cull the real draws outside the scope by checking `q.hidden`.
* Latch contract: handle state changes only between frames (when a readback applies) and through `reset()`, so `hidden`/`state`/`age` are stable while a frame callback runs. Expect one frame of popping when an object comes back into view; oversized proxies soften popping at the cost of overdraw — the looser the proxy, the more the real object draws.
* `reset()` (per handle or whole instance) discards readbacks from pre-reset frames completely — every pre-reset result is dropped, not just downgraded, so a stale in-flight result can never resurrect after a camera cut.
* Slots are allocated per frame in `occlusion()` call order and resolved as one contiguous range appended to the frame encoder before submit; readback never blocks a frame: when readbacks lag more frames than the staging ring holds, that frame's resolve is skipped and its results are dropped **whole** — no handle is updated, all of them keep their previous state, and nothing is partially applied. A readback that lands stale (after a newer one already applied) or fails outright (device lost while mapping) is discarded the same way; failures are reported on `gpu.onError` as `VGPU-QUERY-READBACK` and never reject a frame or `gpu.settled()`, and a dropped result never degrades a handle to `"hidden"`. `await gpu.settled()` covers pending readbacks for deterministic tests and teardown.
* One instance can be used from several frames open at the same time, but results stay scoped to the newest one: opening a frame restarts slot allocation, so an older frame submitted afterwards encodes no resolve and updates no handle — its results are dropped whole rather than latching a stale `"hidden"`. Submitting it is always safe, however long it stayed open — the query set it referenced is kept alive for it.
* A manual `frame(gpu)` that opened a visibility pass holds the query set until you `submit()` or `cancel()` it (a failed frame releases it too). Dropping such a frame without either leaks those resources for the lifetime of the gpu — the same leak as a native `GPUCommandEncoder` you never `finish()` — because a frame is never assumed abandoned: it could still be submitted. Always close the frames you open — `submit()` them, or `frame.cancel()` the ones you decided not to submit, which releases the query set without encoding a resolve or latching any handle — or let `frame(gpu, cb)` do it for you; `gpu.dispose()` (or device loss) is the backstop.
* `handle.dispose()` frees the label for reuse immediately. This is safe because in-flight readbacks resolve to handle object references captured per frame — never by label — and a disposed handle discards late results, so a new same-label handle can never observe the old handle's stale state.
* The pass target needs a depth attachment: without depth testing every rasterized sample passes and each query would report `"visible"`, useless for culling (`VGPU-VIS-NO-DEPTH`). MSAA targets and `depthReadOnly` passes both work — read-only depth still tests.
* One visibility instance owns one occlusion query set plus resolve/staging buffers; `dispose()` releases them once in-flight readbacks settle. Calling it mid-frame is safe: every frame that opened a visibility pass still references the query set from its pass descriptors, so destruction is deferred until each of those frames reports back — submitted, failed or abandoned — including when several manual `frame(gpu)`s are open at once, each of which holds its own reference. `gpu.dispose()` disposes the visibility instances that gpu created. Create the instance once and reuse it across frames.
* **See also:** `visibility`, `Frame`, `FramePassOptions.visibility`, `FramePass.occlusion`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/color
description: Pure WGSL color utilities for shaders resolved by `@vgpu/wgsl`. Import these functions from WGSL modules when you need sRGB transfer, luminance, exposure, tone mapping, or bloom threshold helpers without declaring any resources.
---

# @vgpu/wgsl-std/color



## Import

```wgsl
import { applyExposure, luminance, luminanceThreshold, tonemapAces, tonemapReinhard } from "@vgpu/wgsl-std/color";
```

## Signature

```wgsl
export fn luminance(value: vec3f) -> f32;
export fn applyExposure(value: vec3f, exposure: f32) -> vec3f;
export fn srgbToLinear(value: f32) -> f32;
export fn srgbToLinear3(value: vec3f) -> vec3f;
export fn srgbToLinear4(value: vec4f) -> vec4f;
export fn linearToSrgb(value: f32) -> f32;
export fn linearToSrgb3(value: vec3f) -> vec3f;
export fn linearToSrgb4(value: vec4f) -> vec4f;
export fn tonemapAces(value: vec3f) -> vec3f;
export fn tonemapReinhard(value: vec3f) -> vec3f;
export fn luminanceThreshold(value: vec3f, threshold: f32, softKnee: f32) -> vec3f;
```

## Parameters

| Param     | Type    | Required | Default | Notes                                                                                                                                                              |
| --------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| value     | `f32`   | ✔        | —       | Scalar color channel for `srgbToLinear` or `linearToSrgb`. Expected range is usually `[0.0, 1.0]`; helpers do not clamp scalar transfer inputs.                    |
| value     | `vec3f` | ✔        | —       | RGB/linear-HDR color for `luminance`, `applyExposure`, `tonemap*`, `luminanceThreshold`, `srgbToLinear3`, and `linearToSrgb3`.                                     |
| value     | `vec4f` | ✔        | —       | RGBA color for `srgbToLinear4` and `linearToSrgb4`; RGB is converted and alpha is preserved unchanged.                                                             |
| exposure  | `f32`   | ✔        | —       | Exposure in stops/EV for `applyExposure`; `1.0` doubles, `0.0` leaves unchanged, `-2.0` multiplies by `0.25`.                                                      |
| threshold | `f32`   | ✔        | —       | Linear luminance threshold for `luminanceThreshold`.                                                                                                               |
| softKnee  | `f32`   | ✔        | —       | Width of bright-pass transition. Internally clamped with `max(softKnee, 0.000001)`, so `0.0` behaves as an extremely hard edge without invalid `smoothstep` edges. |

**Returns:** WGSL functions return `f32`, `vec3f`, or `vec4f` as declared. Transfer helpers return converted color, `luminance` returns Rec.709/sRGB relative luminance, exposure/tonemap/threshold helpers return linear color values.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for misspelled imports, `VGPU-WGSL-PKG-NOTFOUND` if the package import cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if caller WGSL is invalid.

## Examples

```ts
const shaderWgsl = `
import { applyExposure, linearToSrgb3, srgbToLinear3, tonemapAces } from "@vgpu/wgsl-std/color";

fn grade(baseColorSrgb: vec3f, exposureStops: f32) -> vec3f {
  let linear = srgbToLinear3(baseColorSrgb);
  let exposed = applyExposure(linear, exposureStops);
  return linearToSrgb3(tonemapAces(exposed));
}
`;

console.log(shaderWgsl.includes("tonemapAces"));
```

```ts
const bloomWgsl = `
import { luminanceThreshold } from "@vgpu/wgsl-std/color";

fn bloomExtract(hdrColor: vec3f) -> vec3f {
  return luminanceThreshold(hdrColor, 1.0, 0.25);
}
`;

console.log(bloomWgsl.length > 0);
```

## Notes

* This module is pure WGSL: it declares no `@group`, no `@binding`, no overrides, no hidden state, and no entry points. It is safe to import into resolver graphs.
* `luminance` uses `dot(value, vec3f(0.2126, 0.7152, 0.0722))`; pass linear-light colors, not encoded sRGB.
* `tonemapAces` implements the Narkowicz ACES fit and clamps to `[0.0, 1.0]`; `tonemapReinhard` uses `value / (1.0 + luminance(value))` and does not explicitly clamp.
* sRGB transfer helpers implement the standard piecewise formulas and do not clamp. Clamp explicitly if your target requires saturated display-range values.
* This module intentionally does not include PBR, BRDF, Fresnel, IBL, Hable/Filament tone mappers, or a default display pipeline.
* **See also:** `@vgpu/wgsl-std/hash`, `@vgpu/wgsl-std/noise`, `resolveShader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/fullscreen
description: Pure WGSL fullscreen-triangle helpers for resolver-managed shaders. Import them when a vertex shader should draw a full-screen pass with three vertices and no vertex buffer.
---

# @vgpu/wgsl-std/fullscreen



## Import

```wgsl
import { fullscreenTriangleClip, fullscreenTriangleUv } from "@vgpu/wgsl-std/fullscreen";
```

## Signature

```wgsl
export fn fullscreenTriangleClip(index: u32) -> vec4f;
export fn fullscreenTriangleUv(clipXy: vec2f) -> vec2f;
```

## Parameters

| Param  | Type    | Required | Default | Notes                                                                                                                                                                   |
| ------ | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| index  | `u32`   | ✔        | —       | Vertex index for `fullscreenTriangleClip`. Use `@builtin(vertex_index)` and draw exactly 3 vertices. Values `0`, `1`, and `2` map to the oversized fullscreen triangle. |
| clipXy | `vec2f` | ✔        | —       | Clip-space XY position, normally `fullscreenTriangleClip(index).xy`, for UV conversion with a render-target-friendly Y flip.                                            |

**Returns:** `fullscreenTriangleClip` returns clip-space `vec4f`; `fullscreenTriangleUv` returns `vec2f` UVs where clip-space top-left `(-1.0, 1.0)` maps to `(0.0, 0.0)`.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for misspelled imports, `VGPU-WGSL-PKG-NOTFOUND` if the package import cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if caller WGSL is invalid.

## Examples

```ts
const fullscreenVertexWgsl = `
import { fullscreenTriangleClip, fullscreenTriangleUv } from "@vgpu/wgsl-std/fullscreen";

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
}

@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
  var out: VertexOutput;
  out.position = fullscreenTriangleClip(index);
  out.uv = fullscreenTriangleUv(out.position.xy);
  return out;
}
`;

console.log(fullscreenVertexWgsl.includes("vs_main"));
```

```ts
const postProcessWgsl = `
import { fullscreenTriangleClip } from "@vgpu/wgsl-std/fullscreen";

@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
  return fullscreenTriangleClip(index);
}
`;

console.log(postProcessWgsl.length > 0);
```

## Notes

* This module is pure WGSL: it declares no `@group`, no `@binding`, no overrides, no hidden state, and no entry points.
* Clip outputs are `0 -> (-1, -3)`, `1 -> (-1, 1)`, `2 -> (3, 1)`, covering the viewport as one oversized triangle.
* `fullscreenTriangleUv` applies `clipXy * vec2f(0.5, -0.5) + vec2f(0.5)`, matching top-left texture coordinate convention for fullscreen passes.
* The helpers do not choose varying locations, fragment outputs, bind groups, or samplers; those remain in your entry shader.
* **See also:** `@vgpu/wgsl-std/color`, `resolveShader`, `wgslVitePlugin`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/hash
description: Pure WGSL hash utilities for deterministic shader randomness. Import them for integer hashing, multi-output PCG-style lattice hashes, and stable conversion from `u32` hashes to unit floats.
---

# @vgpu/wgsl-std/hash



## Import

```wgsl
import { hash1, hash2, hash3, hashU32, pcg2d, pcg3d, unitFloat } from "@vgpu/wgsl-std/hash";
```

## Signature

```wgsl
export fn hashU32(value: u32) -> u32;
export fn pcg2d(value: vec2u) -> vec2u;
export fn pcg3d(value: vec3u) -> vec3u;
export fn unitFloat(hash: u32) -> f32;
export fn hash1(seed: f32) -> f32;
export fn hash2(seed: vec2f) -> vec2f;
export fn hash3(seed: vec3f) -> vec3f;
```

## Parameters

| Param | Type    | Required | Default | Notes                                                                                                    |
| ----- | ------- | -------- | ------- | -------------------------------------------------------------------------------------------------------- |
| value | `u32`   | ✔        | —       | Input integer for `hashU32`, implemented with Wellons lowbias32 constants.                               |
| value | `vec2u` | ✔        | —       | Two-dimensional unsigned seed for `pcg2d`; returns two decorrelated unsigned outputs.                    |
| value | `vec3u` | ✔        | —       | Three-dimensional unsigned seed for `pcg3d`; returns three decorrelated unsigned outputs.                |
| hash  | `u32`   | ✔        | —       | Hash bits passed to `unitFloat`. The low 8 bits are discarded, then the top 24 bits map to `[0.0, 1.0)`. |
| seed  | `f32`   | ✔        | —       | Float seed for `hash1`; bitcast to `u32` before hashing. `-0.0` and `0.0` hash differently.              |
| seed  | `vec2f` | ✔        | —       | Float vector seed for `hash2`; bitcast to `vec2u`, hashed with `pcg2d`, converted with `unitFloat`.      |
| seed  | `vec3f` | ✔        | —       | Float vector seed for `hash3`; bitcast to `vec3u`, hashed with `pcg3d`, converted with `unitFloat`.      |

**Returns:** `hashU32` returns `u32`; `pcg2d`/`pcg3d` return unsigned vectors; `unitFloat` and `hash1` return `f32` in `[0.0, 1.0)`; `hash2`/`hash3` return float vectors with each component in `[0.0, 1.0)`.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for misspelled imports, `VGPU-WGSL-PKG-NOTFOUND` if the package import cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if caller WGSL is invalid.

## Examples

```ts
const hashWgsl = `
import { pcg3d, unitFloat } from "@vgpu/wgsl-std/hash";

fn cellRandom(cell: vec3i) -> f32 {
  return unitFloat(pcg3d(bitcast<vec3u>(cell)).x);
}
`;

console.log(hashWgsl.includes("cellRandom"));
```

```ts
const jitterWgsl = `
import { hash2 } from "@vgpu/wgsl-std/hash";

fn jitter(pixel: vec2f) -> vec2f {
  return hash2(pixel) - vec2f(0.5);
}
`;

console.log(jitterWgsl.length > 0);
```

## Notes

* This module is pure WGSL: it declares no `@group`, no `@binding`, no overrides, no hidden state, and no entry points.
* `hashU32` is Wellons lowbias32, not PCG. `pcg2d` and `pcg3d` are multi-output PCG-style vector hashes for lattice coordinates.
* `unitFloat(hash)` computes `f32(hash >> 8u) * (1.0 / 16777216.0)`, so it never returns `1.0`.
* Float wrappers hash raw IEEE bit patterns. Normalize NaNs and signed zero yourself if those values should collide.
* For lattice noise, prefer integer cell coordinates (`vec*i` bitcast to `vec*u`) over sine/fract-style float hashes.
* **See also:** `@vgpu/wgsl-std/noise`, `@vgpu/wgsl-std/color`, `resolveShader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/noise
description: Pure WGSL Voronoi noise primitives for resolver-managed shaders. Import these when you need nearest and second-nearest jittered feature-cell distances without adding bindings or policy-specific styling. For smooth, non-cellular fields and fBM, use @vgpu/wgsl-std/noise/perlin or @vgpu/wgsl-std/noise/simplex instead.
---

# @vgpu/wgsl-std/noise



> **Want fBM, clouds, terrain, fog or plasma? You are on the wrong page.** This module is Voronoi
> (cellular) noise and ships **no fBM helper** — do not hand-roll octaves on top of it. Use
> `@vgpu/wgsl-std/noise/perlin` (`fbmPerlin2d`/`fbmPerlin3d`) or `@vgpu/wgsl-std/noise/simplex`
> (`fbmSimplex2d`/`fbmSimplex3d`): separate subpaths, amplitude-normalized, guaranteed `(-1, 1)`.
> `npx vgpu docs cat /@vgpu/wgsl-std/noise/perlin/index.docs.md`. Stay here only for cells, edges
> (`f2 - f1`) and per-cell IDs (`cell`).

## Import

```wgsl
import { VoronoiSample2, VoronoiSample3, voronoi2d, voronoi3d } from "@vgpu/wgsl-std/noise";
```

## Signature

```wgsl
export struct VoronoiSample2 {
  f1: f32,
  f2: f32,
  cell: vec2i,
}

export struct VoronoiSample3 {
  f1: f32,
  f2: f32,
  cell: vec3i,
}

export fn voronoi2d(position: vec2f) -> VoronoiSample2;
export fn voronoi3d(position: vec3f) -> VoronoiSample3;
```

## Parameters

| Param    | Type    | Required | Default | Notes                                                                                                     |
| -------- | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- |
| position | `vec2f` | ✔        | —       | 2D sample position for `voronoi2d`. The function searches the `3 x 3` cells around `floor(position)`.     |
| position | `vec3f` | ✔        | —       | 3D sample position for `voronoi3d`. The function searches the `3 x 3 x 3` cells around `floor(position)`. |

`VoronoiSample2` fields:

| Param | Type    | Required | Default | Notes                                                                                                       |
| ----- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| f1    | `f32`   | ✔        | —       | Euclidean distance to the nearest jittered feature point.                                                   |
| f2    | `f32`   | ✔        | —       | Euclidean distance to the second-nearest jittered feature point. `f1 <= f2`.                                |
| cell  | `vec2i` | ✔        | —       | Integer lattice cell containing the nearest feature point. Use it for stable per-cell randomization or IDs. |

`VoronoiSample3` fields:

| Param | Type    | Required | Default | Notes                                                                                                       |
| ----- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| f1    | `f32`   | ✔        | —       | Euclidean distance to the nearest jittered feature point.                                                   |
| f2    | `f32`   | ✔        | —       | Euclidean distance to the second-nearest jittered feature point. `f1 <= f2`.                                |
| cell  | `vec3i` | ✔        | —       | Integer lattice cell containing the nearest feature point. Use it for stable per-cell randomization or IDs. |

**Returns:** `voronoi2d` returns `VoronoiSample2`; `voronoi3d` returns `VoronoiSample3`. The returned sample contains nearest distance `f1`, second-nearest distance `f2`, and the winning feature cell.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for misspelled imports, `VGPU-WGSL-PKG-NOTFOUND` if the package import cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if caller WGSL is invalid. Because this module imports `@vgpu/wgsl-std/hash`, package resolution must also be able to resolve that package export.

## Examples

```ts
const voronoiWgsl = `
import { voronoi2d } from "@vgpu/wgsl-std/noise";

fn edgeMask(position: vec2f) -> f32 {
  let sample = voronoi2d(position);
  return sample.f2 - sample.f1;
}
`;

console.log(voronoiWgsl.includes("edgeMask"));
```

```ts
const styledVoronoiWgsl = `
import { pcg3d, unitFloat } from "@vgpu/wgsl-std/hash";
import { voronoi3d } from "@vgpu/wgsl-std/noise";

fn animatedCell(position: vec2f, time: f32) -> f32 {
  let sample = voronoi3d(vec3f(position, time));
  return unitFloat(pcg3d(bitcast<vec3u>(sample.cell)).x);
}
`;

console.log(styledVoronoiWgsl.length > 0);
```

```ts
const cloudWgsl = `
import { voronoi3d } from "@vgpu/wgsl-std/noise";

// Prefer fbmPerlin3d/fbmSimplex3d for this look; only stack Voronoi octaves when
// you want the cellular structure to survive. Voronoi has no built-in fBM helper,
// so build the octaves yourself ("@vgpu/wgsl-std/noise/perlin" and
// "@vgpu/wgsl-std/noise/simplex" ship their own amplitude-normalized
// fbmPerlin*/fbmSimplex* instead).
fn cloudFbm(position: vec3f) -> f32 {
  var value = 0.0;
  var amplitude = 0.5;
  var frequency = 1.0;
  for (var octave = 0u; octave < 4u; octave = octave + 1u) {
    let sample = voronoi3d(position * frequency);
    value = value + amplitude * (1.0 - sample.f1);
    frequency = frequency * 2.03;
    amplitude = amplitude * 0.5;
  }
  return value;
}

// Animate by feeding time as the third dimension, and soften the cell edges by
// warping the domain with a lower-frequency sample before the octave loop.
fn clouds(uv: vec2f, time: f32) -> f32 {
  let warp = voronoi3d(vec3f(uv * 0.5, time * 0.1)).f1;
  return cloudFbm(vec3f(uv + vec2f(warp), time * 0.2));
}
`;

console.log(cloudWgsl.includes("cloudFbm"));
```

## Notes

* This module is pure WGSL: it declares no `@group`, no `@binding`, no overrides, no hidden state, and no entry points. It imports only pure hash helpers.
* `@vgpu/wgsl-std` is a dependency of the `vgpu` package, so `import ... from "@vgpu/wgsl-std/noise";` resolves in any project that installed `vgpu` — no separate install. A `VGPU-WGSL-PKG-NOTFOUND` for this package means it is genuinely absent from `node_modules`.
* **This module is Voronoi (cellular) noise only** — it looks like cells, not smooth clouds. For a cloud/plasma/fog look built purely from Voronoi, sum octaves of `1.0 - sample.f1` and warp the domain, as the third example above shows; for an animated version pass time as the `vec3f` Z coordinate. For a smooth, non-cellular gradient noise, use `@vgpu/wgsl-std/noise/perlin` (`perlin2d`/`perlin3d`) or `@vgpu/wgsl-std/noise/simplex` (`simplex2d`/`simplex3d`) instead — both ship their own amplitude-normalized `fbmPerlin*`/`fbmSimplex*` and are separate subpaths so importing this module never pulls them in. Validate the result with pixels (`npx vgpu check`, then a headless render), not by eye.
* Each lattice cell owns one fully jittered feature point in `[0.0, 1.0)^n`, generated by `pcg2d` or `pcg3d` and converted with `unitFloat`.
* The module returns geometric data only. It does not choose colors, edge widths, jitter strength, smoothing curves, animation, or material policy.
* Use `sample.cell` for stable per-cell styling; use `sample.f2 - sample.f1` for Voronoi edge distance patterns.
* **See also:** `@vgpu/wgsl-std/hash`, `@vgpu/wgsl-std/color`, `resolveShader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/noise/perlin
description: Improved Perlin gradient noise (2D/3D) plus amplitude-normalized fBM, for clouds, terrain, water and plasma. Smooth, band-limited, and **guaranteed to stay inside `(-1, 1)`** — unlike the widespread `2.2 *` normalization, nothing here ever needs clamping. Import this when you want a smooth field; import `@vgpu/wgsl-std/noise` (Voronoi) when you want cells.
---

# @vgpu/wgsl-std/noise/perlin



## Import

```wgsl
import { fbmPerlin2d, fbmPerlin3d, perlin2d, perlin3d } from "@vgpu/wgsl-std/noise/perlin";
```

## Signature

```wgsl
export fn perlin2d(position: vec2f) -> f32;
export fn perlin3d(position: vec3f) -> f32;
export fn fbmPerlin2d(position: vec2f, octaves: i32, lacunarity: f32, gain: f32) -> f32;
export fn fbmPerlin3d(position: vec3f, octaves: i32, lacunarity: f32, gain: f32) -> f32;
```

## Parameters

| Param      | Type              | Required | Default | Notes                                                                                                                                                                                                                                       |
| ---------- | ----------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| position   | `vec2f` / `vec3f` | ✔        | —       | Sample position in noise space. One unit is one lattice cell, so scale the input to choose the feature size (`perlin3d(worldPosition * 0.35)`). There is no seed parameter — offset `position` instead (see Notes).                         |
| octaves    | `i32`             | ✔        | —       | Number of fBM octaves. **Silently clamped to `[1, 16]`**: `0` and negatives behave as `1`, values above `16` as `16`. The clamp bounds shader cost and keeps a garbage value (from a uniform, say) from hanging the GPU.                    |
| lacunarity | `f32`             | ✔        | —       | Frequency multiplier per octave; `2.0` is the usual choice. Not clamped. Irrational-ish values (`2.17`) hide the lattice better than exact powers of two. Effective coordinate of the last octave is `position * lacunarity^(octaves - 1)`. |
| gain       | `f32`             | ✔        | —       | Amplitude multiplier per octave; `0.5` is the usual choice. **Silently clamped to `[0, 1]`**, because a negative gain would break the range proof. `gain = 0` reduces fBM to a single octave.                                               |

**Returns:** `f32` in the open interval `(-1, 1)` for every finite input, for all four functions. This is a proof, not an observation: the quintic blend is a convex combination of the corner gradient dot products, and each normalizer is strictly below `1 / sup`. fBM divides by the sum of its amplitudes, so `|sum| <= weight` keeps the same bound across octaves.

Measured over 1e6 random samples (`packages/wgsl-std/tests/perlin.test.ts` pins these):

| fn         | max \|value\|          | typical σ | max slope | corners hashed |
| ---------- | ---------------------- | --------- | --------- | -------------- |
| `perlin2d` | 0.9937 (bound 0.99996) | 0.305     | ≈2.74     | 4 × `pcg2d`    |
| `perlin3d` | 0.9560 (bound 0.99956) | 0.260     | ≈2.45     | 8 × `pcg3d`    |

`perlin2d` and `perlin3d` are **exactly `0.0` at every integer lattice point** (`perlin3d(vec3f(3.0, -1.0, 8.0)) == 0.0`). That is inherent to gradient noise, not a bug: if your field looks like a zero grid, you are sampling integers — offset by a fraction of a cell.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for a misspelled import (note this module is `@vgpu/wgsl-std/noise/perlin`, *not* `@vgpu/wgsl-std/noise`), `VGPU-WGSL-PKG-NOTFOUND` if the package cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if caller WGSL is invalid. This module also reaches `@vgpu/wgsl-std/hash`, so that package export must resolve too.

## Examples

```ts
const remapWgsl = `
import { perlin3d } from "@vgpu/wgsl-std/noise/perlin";
import { remap, saturate } from "@vgpu/wgsl-std/math";

// The field is (-1, 1) with sigma ~= 0.26, so remap the band you actually care
// about instead of saturating: saturate() would throw away half the signal and
// flatten everything above 1.0 that the field never reaches anyway.
fn fogDensity(worldPosition: vec3f, time: f32) -> f32 {
  let field = perlin3d(worldPosition * 0.35 + vec3f(0.0, 0.0, time * 0.15));
  return saturate(remap(-0.4, 0.6, 0.0, 1.0, field));
}
`;

console.log(remapWgsl.includes("fogDensity"));
```

```ts
const cloudsWgsl = `
import { fbmPerlin3d } from "@vgpu/wgsl-std/noise/perlin";
import { remap, saturate } from "@vgpu/wgsl-std/math";

// Animated clouds: FBM over a domain-warped field.
// Offsets are >= 2 units apart so the three warp components are decorrelated.
fn cloudCoverage(position: vec3f, time: f32) -> f32 {
  let drift = vec3f(position.xy + vec2f(time * 0.02, 0.0), position.z + time * 0.05);

  // Domain warp: displace the sample point with a low-frequency field.
  let warp = vec3f(
    fbmPerlin3d(drift, 3, 2.0, 0.5),
    fbmPerlin3d(drift + vec3f(37.2, 11.7, 5.3), 3, 2.0, 0.5),
    fbmPerlin3d(drift + vec3f(-19.4, 42.1, 23.8), 3, 2.0, 0.5),
  );

  // Detail octaves on the warped domain, then shape coverage.
  let field = fbmPerlin3d(drift + warp * 0.45, 5, 2.0, 0.5);
  let coverage = saturate(remap(-0.15, 0.65, 0.0, 1.0, field));
  return coverage * coverage * (3.0 - 2.0 * coverage);
}
`;

console.log(cloudsWgsl.includes("cloudCoverage"));
```

```ts
const shapingWgsl = `
import { perlin3d } from "@vgpu/wgsl-std/noise/perlin";

// Turbulence (billowy): sum |noise| per octave -> [0, 1).
// Must be per-octave; abs(fbmPerlin3d(...)) is NOT the same thing.
fn turbulence3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0; var amplitude = 1.0; var weight = 0.0; var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    sum = sum + amplitude * abs(perlin3d(sample));
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}

// Ridged (terrain): replace abs(n) with (1.0 - abs(n)) squared, per octave.
fn ridged3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0; var amplitude = 1.0; var weight = 0.0; var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    let ridge = 1.0 - abs(perlin3d(sample));
    sum = sum + amplitude * ridge * ridge;
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}

// Normals: finite differences on the final field are adequate; analytic
// derivatives are not part of this release.
`;

console.log(shapingWgsl.includes("turbulence3d"));
```

## Notes

* **Range and shaping.** Output is `(-1, 1)`, never clipped, but `σ ≈ 0.305` (2D) / `0.260` (3D) means extremes are rare: over 1e6 samples the largest magnitudes seen were 0.9937 and 0.9560. Use `remap` from `@vgpu/wgsl-std/math` to stretch the band you care about; reaching for `saturate` alone discards the entire negative half of the field.
* **Zero on the lattice.** `perlin2d`/`perlin3d` return exactly `0.0` at integer coordinates. Sample at cell fractions, and remember that `fbmPerlin*` inherits this at octave 1 only (later octaves land off-lattice).
* **Seeding is an input offset — there is no `seed` argument.** Measured Pearson correlation of `perlin3d(p)` against `perlin3d(p + offset)` over 2e5 samples:

  | offset         | correlation |
  | -------------- | ----------- |
  | `(0.5, 0, 0)`  | 0.400       |
  | `(1, 0, 0)`    | −0.043      |
  | `(2, 0, 0)`    | 0.0017      |
  | `(17, 0, 0)`   | −0.0004     |
  | `(101, 53, 7)` | 0.0035      |

  So **any offset of ≥ 2.0 units on some axis decorrelates**; sub-cell offsets do not (`+0.5` still correlates at 0.40). Keep offsets in the tens–thousands, not 1e6, because of the f32 mantissa.
* **Period and f32 domain.** The gradient hash covers 2³² cells, so there is no visible tiling (compare the period-289 float `permute` hashes found in GLSL ports). The practical limit is f32 precision, not the period: `position - floor(position)` is quantized to \~2⁻²⁴·|p|, so detail bands visibly past `|p| ≈ 1e4` and vanishes entirely at `|p| ≥ 2²³` (the field is then exactly 0, because every sample lands on a lattice point). fBM multiplies the effective coordinate by `lacunarity^(octaves - 1)` — 6 octaves at `lacunarity = 2.0` reaches 32× your input.
* **Animating with time.** Feeding an unbounded `time` into the third axis works, but it walks the coordinate toward the f32 limit above, so a long-running session eventually looks blocky. Wrap or scale time (`time * 0.05`, reset periodically), or animate a 2D field's offset instead.
* **Cost model.** `perlin2d` = 4 `pcg2d` hashes, `perlin3d` = 8 `pcg3d` hashes. A 6-octave `fbmPerlin3d` is 48 hashes per sample, and the domain-warped clouds recipe above is ≈4× that (three warp calls plus the detail call). Prefer `fbmPerlin2d` when the third axis only carries animation, and measure before shipping a fullscreen 6-octave 3D field.
* **Determinism.** Gradient selection goes through the integer `pcg2d`/`pcg3d` hashes and the arithmetic uses no `sin`/`cos`/`sqrt`/`inverseSqrt`/`pow`, whose accuracy is implementation-defined. Which gradient a cell gets is therefore bit-identical on every backend; only the final interpolation may differ by a few ulp. The test suite compares real GPU output against an f32-exact CPU reference at 1e-5.
* **Purity.** This module declares no `@group`, no `@binding`, no overrides, no entry points and no hidden state. It imports pure hash helpers plus a private gradient core shared with the simplex module.
* **Credit.** Algorithmic reference: Ken Perlin, *Improving Noise* (SIGGRAPH 2002) — quintic fade and the 12 cube-edge gradient set. No code was copied from any implementation; the gradients are re-derived on this package's own `pcg2d`/`pcg3d` hashes.
* **See also:** `@vgpu/wgsl-std/noise/simplex` (same shape, \~2.5× the slope and \~1.7× the σ at the same input scale — scale `position` by \~0.4–0.5 when migrating, and it hashes 4 corners instead of 8 in 3D), `@vgpu/wgsl-std/noise` (Voronoi/cellular, for cells rather than smooth fields), `@vgpu/wgsl-std/math` (`remap`, `saturate`), `@vgpu/wgsl-std/hash`, `resolveShader`.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: @vgpu/wgsl-std/noise/simplex
description: Pure WGSL simplex noise (`simplex2d`, `simplex3d`) and amplitude-normalized fBM (`fbmSimplex2d`, `fbmSimplex3d`) for resolver-managed shaders. Import these for smooth gradient noise — clouds, terrain, water, plasma — when you want the cheaper, higher-frequency sibling of `@vgpu/wgsl-std/noise/perlin`. For cell/edge patterns use `@vgpu/wgsl-std/noise` (Voronoi) instead.
---

# @vgpu/wgsl-std/noise/simplex



## Import

```wgsl
import { simplex2d, simplex3d, fbmSimplex2d, fbmSimplex3d } from "@vgpu/wgsl-std/noise/simplex";
```

## Signature

```wgsl
export fn simplex2d(position: vec2f) -> f32;
export fn simplex3d(position: vec3f) -> f32;
export fn fbmSimplex2d(position: vec2f, octaves: i32, lacunarity: f32, gain: f32) -> f32;
export fn fbmSimplex3d(position: vec3f, octaves: i32, lacunarity: f32, gain: f32) -> f32;
```

## Parameters

| Param      | Type    | Required | Default | Notes                                                                                                                                                                                                       |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| position   | `vec2f` | ✔        | —       | 2D sample position for `simplex2d` / `fbmSimplex2d`. One unit is one lattice cell; scale the input to set the feature size.                                                                                 |
| position   | `vec3f` | ✔        | —       | 3D sample position for `simplex3d` / `fbmSimplex3d`. Passing time as Z animates a 2D field, but see the note on unbounded time below.                                                                       |
| octaves    | `i32`   | ✔        | —       | Number of fBM octaves. **Silently clamped to `[1, 16]`**: an unbounded dynamic loop count is a GPU-hang risk. `1` returns exactly the base noise.                                                           |
| lacunarity | `f32`   | ✔        | —       | Per-octave frequency multiplier. Not clamped. `2.0` is the usual choice; a slightly irrational value such as `2.17` avoids octaves lining up on the lattice.                                                |
| gain       | `f32`   | ✔        | —       | Per-octave amplitude multiplier. **Silently clamped to `[0, 1]`**: a negative gain would break the amplitude sum the range guarantee rests on. `0.5` is the usual choice; `0` collapses to a single octave. |

**Returns:** `f32` in the open interval `(-1, 1)` — never clipped, for every finite input, including all fBM parameter combinations. The bound is a proof, not an observation: the normalizers (`98.0` for 2D, `76.0` for 3D) sit just below `1 / sup` of the raw kernel sum (`0.0100802047` and `0.0130071572`), so `abs(simplex2d) <= 0.98786` and `abs(simplex3d) <= 0.98854`; fBM divides by the sum of the amplitudes, so the bound survives octaves.

**Throws:** These WGSL declarations do not throw. `resolveShader()` can still throw `VGPU-WGSL-SYM-NOEXPORT` for a misspelled import (note the module is `@vgpu/wgsl-std/noise/simplex`, not `@vgpu/wgsl-std/noise`), `VGPU-WGSL-PKG-NOTFOUND` if the package import cannot be resolved, or validation errors such as `VGPU-WGSL-NAGA-UNKNOWN` if the calling WGSL is invalid. This module reaches `@vgpu/wgsl-std/hash` through a private gradient core, so package resolution must be able to resolve that package export too.

## Measured field properties

| fn          | guaranteed range | max after normalization | observed max | sigma     | max slope | hashes per call |
| ----------- | ---------------- | ----------------------- | ------------ | --------- | --------- | --------------- |
| `simplex2d` | `(-1, 1)`        | 0.98786                 | 0.9878       | **0.533** | ≈6.95     | 3 × `pcg2d`     |
| `simplex3d` | `(-1, 1)`        | 0.98854                 | 0.9884       | **0.388** | ≈6.53     | 4 × `pcg3d`     |

Extremes are rare: with sigma ≈ 0.39–0.53 most samples sit well inside the range. **Remap, do not saturate** — `saturate(simplex3d(p))` throws away the whole negative half of the field. Use `remap(-0.6, 0.6, 0.0, 1.0, value)` from `@vgpu/wgsl-std/math` and clamp afterwards if you need `[0, 1]`.

## Examples

```ts
const simplexWgsl = `
import { simplex3d } from "@vgpu/wgsl-std/noise/simplex";
import { remap, saturate } from "@vgpu/wgsl-std/math";

// A single octave, remapped into [0, 1] for use as a mask.
fn smoke(position: vec2f, time: f32) -> f32 {
  let value = simplex3d(vec3f(position * 3.0, time * 0.2));
  return saturate(remap(-0.6, 0.6, 0.0, 1.0, value));
}
`;

console.log(simplexWgsl.includes("smoke"));
```

```ts
const cloudWgsl = `
import { fbmSimplex3d } from "@vgpu/wgsl-std/noise/simplex";
import { remap, saturate } from "@vgpu/wgsl-std/math";

// Animated clouds: fBM over a domain-warped field.
// Offsets are >= 2 units apart so the three warp components are decorrelated.
fn cloudCoverage(position: vec3f, time: f32) -> f32 {
  let drift = vec3f(position.xy + vec2f(time * 0.02, 0.0), position.z + time * 0.05);

  // Domain warp: displace the sample point with a low-frequency field.
  let warp = vec3f(
    fbmSimplex3d(drift, 3, 2.0, 0.5),
    fbmSimplex3d(drift + vec3f(37.2, 11.7, 5.3), 3, 2.0, 0.5),
    fbmSimplex3d(drift + vec3f(-19.4, 42.1, 23.8), 3, 2.0, 0.5),
  );

  // Detail octaves on the warped domain, then shape coverage.
  let field = fbmSimplex3d(drift + warp * 0.45, 5, 2.0, 0.5);
  let coverage = saturate(remap(-0.15, 0.65, 0.0, 1.0, field));
  return coverage * coverage * (3.0 - 2.0 * coverage);
}
`;

console.log(cloudWgsl.includes("cloudCoverage"));
```

```ts
const shapedWgsl = `
import { simplex3d } from "@vgpu/wgsl-std/noise/simplex";

// Turbulence (billowy): sum abs(noise) per octave -> [0, 1).
// Must be per-octave; abs(fbmSimplex3d(...)) is NOT the same thing.
fn turbulenceSimplex3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0;
  var amplitude = 1.0;
  var weight = 0.0;
  var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    sum = sum + amplitude * abs(simplex3d(sample));
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}

// Ridged (terrain): replace abs(n) with a squared (1.0 - abs(n)), per octave.
fn ridgedSimplex3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0;
  var amplitude = 1.0;
  var weight = 0.0;
  var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    let ridge = 1.0 - abs(simplex3d(sample));
    sum = sum + amplitude * ridge * ridge;
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}
`;

console.log(shapedWgsl.includes("ridgedSimplex3d"));
```

```ts
const seededWgsl = `
import { simplex2d } from "@vgpu/wgsl-std/noise/simplex";

// There is no seed parameter: offset the input instead, by >= 2.0 units.
fn twoIndependentFields(position: vec2f) -> vec2f {
  return vec2f(simplex2d(position), simplex2d(position + vec2f(37.0, -19.0)));
}
`;

console.log(seededWgsl.includes("twoIndependentFields"));
```

## Notes

* **Kernel radius² is `0.5`, not the widespread `0.6`.** The canonical Gustavson/webgl-noise value has a support radius of 0.775, which exceeds the reach of the 3-corner (2D) / 4-corner (3D) traversal, so corners that still carry a non-zero contribution get dropped at a simplex face: a genuine C0 crack. Measured with a dense line scan (step `1e-6`): max `|Δv|` is `9.5e-5` / `4.6e-5` (raw field) at `0.6` versus `4.8e-8` / `2.9e-8` at `0.5`, i.e. \~1000× worse, \~0.8% of the normalized range. It is invisible in a flat colour ramp and obvious as a seam in normals or high-contrast ramps. `0.5` is not merely "smaller and safer": it is exactly the largest radius² for which every dropped corner is already outside the kernel (equality is attained, in 2D, at `d = (G2 - 0.5, 0.5)`). **Do not "fix" this back to `0.6`** — `tests/simplex.test.ts` fails loudly, by design, if you do.
* **Simplex vs Perlin at the same input scale:** \~2.5× the slope and \~1.7× the sigma. `simplex3d(p)` is *not* "`perlin3d(p)` but faster" — it is a higher-frequency, higher-contrast field. When migrating, **scale `p` by \~0.4–0.5** (`simplex3d(p * 0.45)`) to get a comparable look.
* **Cost:** `simplex3d` costs 4 `pcg3d` hashes against `perlin3d`'s 8, so it is roughly half the price per call — but a fair comparison has to account for the frequency difference above: matching Perlin's look means sampling a scaled-down domain, not a cheaper field. A 6-octave `fbmSimplex3d` is 24 `pcg3d` hashes; the cloud recipe above (3 warp fields at 3 octaves plus 5 detail octaves) is \~14 `simplex3d` calls, i.e. \~56 hashes per pixel. Prefer `simplex2d` where the third axis is only animation.
* **Seeding is by input offset; there is no `seed` parameter** (same convention as `voronoi2d`/`voronoi3d`). Measured Pearson correlation between `p` and `p + offset`: `(0.5,0,0)` → 0.400, `(1,0,0)` → −0.043, `(2,0,0)` → 0.0017, `(17,0,0)` → −0.0004, `(101,53,7)` → 0.0035. **Any offset of ≥ 2.0 units on some axis decorrelates; sub-cell offsets do not.** Keep offsets in the tens–thousands, not `1e6`, because of the f32 mantissa.
* **Period is 2³² cells** — the gradient index comes from `pcg2d`/`pcg3d` over the integer cell, not from a float `mod 289` permutation, so there is no visible tiling.
* **f32 domain:** the fractional part `p - floor(p)` is quantized to \~2⁻²⁴·|p|, so detail visibly bands beyond `|p| ≈ 1e4` and vanishes entirely at `|p| >= 2²³`. fBM multiplies the effective coordinate by `lacunarity^(octaves - 1)` (6 octaves at λ=2 → 32×), so it reaches that limit 32× sooner. **An animation that feeds unbounded `time` into the third axis degrades after long sessions** — wrap or scale time (`fract(time * 0.05) * 20.0`, or reset a session clock), do not let it grow without bound.
* Unlike Perlin, simplex has **no zero on the integer grid**: the simplex lattice is sheared, so integer coordinates are generically interior points. The field *is* exactly 0 at each lattice vertex of the sheared grid (e.g. `simplex3d(vec3f(0.5))` is `0.0`), because there the gradient offset is the zero vector and every other corner lies outside the kernel. Do not build a lattice-zero assumption on top of `simplex2d`/`simplex3d`.
* This module is pure WGSL: no `@group`, no `@binding`, no overrides, no entry points, no hidden state. It contains no lookup table (no `array<...>`) and no `sin`/`cos`/`sqrt`/`inverseSqrt`/`pow`: those have implementation-defined accuracy, so an angle-based gradient would drift per driver. Gradient *selection* is therefore bit-identical on every backend; only the final interpolation may differ by a few ulp.
* The module returns a scalar field only. It does not choose colours, coverage curves, warp strength, animation speed, or material policy — the recipes above are examples to copy and tune, not policy the module enforces.
* Algorithmic references (no code copied, no third-party licence obligation incurred): Perlin 2001, *Hardware-accelerated procedural texturing* / simplex construction; Stefan Gustavson, *Simplex noise demystified*. The US patent on simplex noise (US 6,867,776 B2) expired in 2022.
* **See also:** `@vgpu/wgsl-std/noise/perlin` (`perlin2d`/`perlin3d`/`fbmPerlin2d`/`fbmPerlin3d` — smoother, exactly 0 on the integer lattice, \~2.5× less slope), `@vgpu/wgsl-std/noise` (Voronoi cells), `@vgpu/wgsl-std/math` (`remap`, `saturate`), `@vgpu/wgsl-std/hash` (`pcg2d`, `pcg3d`).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: createNodeAdapter
description: `createNodeAdapter()` returns a Dawn-backed `VGPUAdapter` using the `webgpu@0.4.0` API. The native loader resolves Dawn in this order:
---

# createNodeAdapter



1. the file named by `VGPU_DAWN_BINARY`;
2. a verified vgpu prebuild in the versioned user cache;
3. the stock `webgpu` package;
4. on Linux, a lazy download of the vgpu prebuild if stock Dawn cannot load.

A best-effort postinstall download normally fills the cache. It never fails npm
or pnpm installation, and the runtime retry covers `--ignore-scripts` and pnpm
configurations that block lifecycle scripts. Downloads come from the pinned
`vercel-labs/vgpu` GitHub Release and are accepted only when their SHA-256 equals
the hash pinned in `@vgpu/adapter-node`. Private-repository and draft-release
access honors `GH_TOKEN` or `GITHUB_TOKEN`; published public assets are fetched
without a token. Override the cache root with `VGPU_CACHE_DIR` (or
`XDG_CACHE_HOME`). Cache entries must be regular, non-symlink files. Before
native loading, the verified bytes are copied through one open file descriptor
into a private per-process directory and that exact private copy is loaded,
preventing cache replacement between verification and `require()`.

The portable prebuild currently supports Linux arm64 with glibc 2.31 or newer.
Linux musl, other Linux CPUs, and other operating systems continue to try stock
`webgpu`; errors identify an unsupported platform, musl, an offline/blocked
download, or the exact required and detected glibc versions. Manual and CI
installation is available with:

```bash
pnpm exec vgpu install-dawn
# or
npx vgpu install-dawn
```

`VGPU-NODE-PREBUILD-MISSING` includes the failed reason and those remediation
commands. `VGPU-NODE-PREBUILD-CHECKSUM` refuses a corrupt or substituted asset.
`VGPU-NODE-GLIBC-MISMATCH` names both versions when a selected native binary
cannot load. Do not upgrade a host's glibc in place; install the portable
prebuild or provide an audited binary with `VGPU_DAWN_BINARY`.

On Linux the adapter keeps the established OpenGL compatibility backend when
`DISPLAY` or `WAYLAND_DISPLAY` is configured. Without a display server it lets
Dawn discover the available backend, enabling display-free Vulkan without
application configuration. It keeps a process-wide singleton GPU instance;
conflicting later flags are warned
and ignored because Dawn re-init can SIGSEGV. For deterministic headless Vulkan
CI, install the distro's Mesa/lavapipe packages and select its ICD with
`VK_ICD_FILENAMES`; `VGPU_DAWN_FLAGS=backend=vulkan` remains available when
strict backend selection is required. Mesa and GPU drivers remain host-managed
and are never downloaded by vgpu. `VGPU_DAWN_FLAGS` may contain any
space-separated Dawn flags and replaces automatic backend discovery.

A CPU Vulkan device is not necessarily a WebGPU fallback adapter. In current
Dawn builds lavapipe is acquired by the normal request; forcing
`forceFallbackAdapter` can exclude it. If no adapter is found after the retry
window, `VGPU-NODE-NO-ADAPTER` reports the request options and Dawn flags and
points to the Mesa version, Vulkan ICD, and display environment to inspect.

For agentic headless snapshot tests, pair this adapter with an explicit
offscreen `rgba8unorm` render target that includes `"copy_src"`, submit the
render commands, `await device.queue.flush()`, then read pixels through
`Texture.read()`. Keep PNG encoding and pixel comparison in project test tooling
(such as `pngjs`, `pixelmatch`, or existing snapshots), not in the VGPU API. See
`createNodeDevice` for a full native-before/VGPU-after guide.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: createNodeDevice
description: `createNodeDevice(opts?)` is a mechanical convenience that requests a `Device` from the Node adapter directly. Use `createNodeDevice()` for focused scripts, server-side renders, and agentic headless snapshot tests that need one explicit WebGPU device. Application code can also use `init()` from `vgpu/node` for the main API (`vgpu`) facade.
---

# createNodeDevice



## Agentic headless snapshot workflow

A snapshot agent should render a deterministic, named scene into an offscreen
texture, wait for GPU work to finish, read the texture bytes, and hand those
bytes to project-owned PNG or pixel-diff tooling. VGPU does not provide an
image-diff framework and does not drive browser screenshots; it keeps the
WebGPU render target and readback steps small and explicit.

Name the inputs that control the frame (camera, time, seed, viewport, material
fixtures) and keep them stable between runs. Name the render target as well so
native WebGPU validation output points back to the snapshot under test.

### Native WebGPU readback boilerplate

In raw WebGPU, reading an offscreen render target is a separate copy-to-buffer
operation. You must create the target with `COPY_SRC`, align rows to 256 bytes,
copy the texture into a mappable buffer, submit the copy, wait for submitted work,
map the buffer, then strip row padding before comparing pixels:

```text
const width = 256;
const height = 256;
const bytesPerPixel = 4;
const unpaddedBytesPerRow = width * bytesPerPixel;
const bytesPerRow = Math.ceil(unpaddedBytesPerRow / 256) * 256;

const target = gpuDevice.createTexture({
  label: "snapshot.hero.native.target",
  size: { width, height },
  format: "rgba8unorm",
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});

// Render the deterministic frame into `target` here.

const readback = gpuDevice.createBuffer({
  label: "snapshot.hero.native.readback",
  size: bytesPerRow * height,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});

const encoder = gpuDevice.createCommandEncoder({ label: "snapshot.hero.native.copy" });
encoder.copyTextureToBuffer(
  { texture: target },
  { buffer: readback, bytesPerRow, rowsPerImage: height },
  { width, height, depthOrArrayLayers: 1 },
);
gpuDevice.queue.submit([encoder.finish()]);
await gpuDevice.queue.onSubmittedWorkDone();

await readback.mapAsync(GPUMapMode.READ);
const mapped = new Uint8Array(readback.getMappedRange());
const rgba = new Uint8Array(unpaddedBytesPerRow * height);
for (let y = 0; y < height; y += 1) {
  rgba.set(
    mapped.subarray(y * bytesPerRow, y * bytesPerRow + unpaddedBytesPerRow),
    y * unpaddedBytesPerRow,
  );
}
readback.unmap();
```

`Texture.read()` wraps that readback path for VGPU textures, including the
padding removal, while preserving normal WebGPU rendering and command submission.

### With VGPU in Node

Use the Node adapter, create an explicit offscreen render target, submit your
render commands, flush the queue, then read RGBA bytes. The example uses raw
WebGPU pipeline creation through `.gpu` because VGPU intentionally keeps that
escape hatch available for native interop; wrapper lifecycle methods should still
own teardown.

```text
import { createNodeDevice } from "@vgpu/adapter-node";

const width = 256;
const height = 256;
const format: GPUTextureFormat = "rgba8unorm";

const scene = {
  name: "hero-triangle",
  seed: 7,
  timeSeconds: 0,
  camera: "orthographic-front",
};

const device = await createNodeDevice({ label: `snapshot.${scene.name}.device` });

try {
  const target = device.createTexture({
    label: `snapshot.${scene.name}.target`,
    size: [width, height],
    format,
    usage: ["render_attachment", "copy_src"],
  });

  const shader = device.gpu.createShaderModule({
    label: `snapshot.${scene.name}.shader`,
    code: /* wgsl */ `
      @vertex
      fn vs(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f {
        let positions = array<vec2f, 3>(
          vec2f(-0.75, -0.75),
          vec2f( 0.75, -0.75),
          vec2f( 0.00,  0.75),
        );
        return vec4f(positions[vertexIndex], 0.0, 1.0);
      }

      @fragment
      fn fs(@builtin(position) position: vec4f) -> @location(0) vec4f {
        let uv = position.xy / vec2f(${width}.0, ${height}.0);
        return vec4f(uv.x, uv.y, 0.25, 1.0);
      }
    `,
  });

  const pipeline = device.gpu.createRenderPipeline({
    label: `snapshot.${scene.name}.pipeline`,
    layout: "auto",
    vertex: { module: shader, entryPoint: "vs" },
    fragment: { module: shader, entryPoint: "fs", targets: [{ format }] },
    primitive: { topology: "triangle-list" },
  });

  const encoder = device.gpu.createCommandEncoder({ label: `snapshot.${scene.name}.frame` });
  const pass = encoder.beginRenderPass({
    label: `snapshot.${scene.name}.pass`,
    colorAttachments: [{
      view: target.createView(),
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
      loadOp: "clear",
      storeOp: "store",
    }],
  });
  pass.setPipeline(pipeline);
  pass.draw(3);
  pass.end();

  device.queue.gpu.submit([encoder.finish()]);
  await device.queue.flush();

  const rgba = await target.read();
  // Pass `rgba` to project-owned PNG or pixel-diff tooling.

  target.destroy();
} finally {
  device.destroy();
}
```

### Test harness sketch

Keep PNG encoding and image comparison outside the VGPU API. A Vitest/Jest-style
harness can use packages such as `pngjs` and `pixelmatch`, or your repository's
existing snapshot writer:

```text
import { readFileSync, writeFileSync } from "node:fs";
import pixelmatch from "pixelmatch";
import { PNG } from "pngjs";

const actualPng = new PNG({ width, height });
actualPng.data.set(rgba);

if (process.env.VGPU_WRITE_SNAPSHOTS === "1") {
  writeFileSync("snapshots/hero-triangle.png", PNG.sync.write(actualPng));
} else {
  const expected = PNG.sync.read(readFileSync("snapshots/hero-triangle.png"));
  const diff = new PNG({ width, height });
  const mismatched = pixelmatch(
    expected.data,
    actualPng.data,
    diff.data,
    width,
    height,
    { threshold: 0.02 },
  );
  if (mismatched > 0) {
    writeFileSync("snapshots/hero-triangle.diff.png", PNG.sync.write(diff));
    throw new Error(`Snapshot mismatch: ${mismatched} pixels differ`);
  }
}
```

## CI, Docker, and GLIBC notes

On Linux, the stock Dawn binary shipped by `webgpu@0.4.0` targets recent glibc
(2.38 on arm64). vgpu no longer treats that as a hard requirement: when the
stock binary fails to load, the adapter falls back to vgpu's portable Dawn
prebuild — built against GLIBC 2.30, downloaded from GitHub Releases, verified
against a pinned SHA-256, and cached per version. The fallback also runs at
`postinstall` (best-effort, never fails your install) and can be invoked
manually with `npx vgpu install-dawn`. `VGPU_DAWN_BINARY` overrides resolution
entirely for air-gapped hosts. Load failures surface as structured
`VGPU-NODE-*` errors that name the platform, the reason, and the fix.

Docker remains the recommended path when you want a fully pinned rendering
environment — identical Mesa/driver stack for bit-exact snapshots — rather
than as a GLIBC workaround:

```bash
pnpm test:docker
```

The Docker image uses Node 22 on Debian trixie with Mesa/EGL/GL and Xvfb for
the OpenGL software stack. It also sets the headless defaults used by the
adapter: `LIBGL_ALWAYS_SOFTWARE=1`, `DISPLAY=:99`, and
`XDG_RUNTIME_DIR=/tmp/xdg-runtime`. To update project snapshots in that
environment, use a project convention such as:

```bash
VGPU_WRITE_SNAPSHOTS=1 pnpm test:docker
```

## Shutting down cleanly

Dawn keeps polling for device events while the device is alive, which keeps
the Node process running after your last render. Dispose the context when you
are done:

```text
try {
  // render, read pixels, write files
  await gpu.settled();
} finally {
  gpu.dispose();
}
```

`gpu.dispose()` destroys the device and stops event polling, letting the
process exit on its own — no `process.exit()` needed.

## Headless rendering without a GPU

vgpu can render on CPU anywhere. Check the machine first:

```bash
npx vgpu doctor
```

If no usable Vulkan driver exists, the fix is one command:

```bash
npx vgpu install-software-renderer
```

This downloads vgpu's portable lavapipe build (Mesa 25, \~20 MB, sha256-verified,
cached next to the Dawn binary) — no root, no system packages. Once cached,
`init()` uses it automatically whenever no other adapter exists. The system
needs a handful of tiny libraries (Vulkan loader, libdrm, zlib, zstd, and
libudev) — doctor checks and prescribes them in one command.

### Reading the startup noise on a machine without a GPU

Dawn, the Vulkan loader and Mesa write their own startup diagnostics straight to
stderr from native code before any JavaScript runs, and they look alarming even
when nothing is wrong:

```text
error: XDG_RUNTIME_DIR is invalid or not set in the environment.
error: XDG_RUNTIME_DIR is invalid or not set in the environment.
Warning: Vulkan shaderUniform*ArrayDynamicIndexing required.
```

Those lines come from the driver stack, not from vgpu; the prebuilt Dawn binding
exposes no logging hook, so vgpu cannot capture or relabel them. Instead, when a
run ends up on the CPU renderer, vgpu prints one labelled notice on stderr —
once per process, after the adapter is known, so it lands *below* the native
lines it explains:

```text
vgpu: notice — no hardware GPU adapter is available; using CPU software renderer (llvmpipe (LLVM 19.1.7, 128 bits)). This is expected on a machine without a usable GPU, and rendering continues normally.
vgpu: notice — Vulkan/XDG_RUNTIME_DIR "error" and "Warning" lines printed above come from the GPU driver stack, not from vgpu, and are harmless. Run `npx vgpu doctor` for details.
```

Explicit `init({ adapter: "software" })` stays silent: choosing the CPU renderer
on purpose needs no explanation.

Prefer your distribution's driver when it is recent (Mesa >= 23 with
`mesa-vulkan-drivers`); the portable build exists for hosts where that is not
an option.

## Choosing an adapter

```ts
import { init } from "vgpu/node";

{ const gpu = await init(); }                        // auto: hardware first, cached software renderer as last resort
{ const gpu = await init({ adapter: "hardware" }); } // require a real GPU — fails loud, never falls back
{ const gpu = await init({ adapter: "software" }); } // force the portable renderer — deterministic pixels on any machine
```

`gpu.adapter` reports what was selected: `{ name, type: "gpu" | "cpu" }`.
The `VGPU_ADAPTER` environment variable overrides the code-level choice and
announces itself on stderr — handy for forcing a mode in CI without editing
code.

## `.gpu` lifecycle guidance

`.gpu` is a raw WebGPU escape hatch. Use it for operations VGPU does not wrap,
such as custom pipelines, render passes, query sets, or native interop. Prefer
VGPU wrapper lifecycle methods for resources VGPU owns: call `texture.destroy()`,
`buffer.destroy()`, and `device.destroy()` rather than `texture.gpu.destroy()` or
`buffer.gpu.destroy()`. Direct raw destruction is reserved for deliberate
escape-hatch/native interop cases where you also own the consequences.

## Troubleshooting snapshot tests

* **Unsupported format**: `Texture.read()` supports the formats documented by
  `Texture` readback, including `rgba8unorm`, `rgba8unorm-srgb`, and the float
  formats `rgba16float` / `rgba32float` (plus their `r`/`rg` variants). Prefer
  `rgba8unorm` for deterministic PNG snapshots; for HDR targets read components
  with `Texture.readFloats()` (a `Float32Array`) instead of decoding the raw
  half/float bytes yourself. Formats outside that table (depth/stencil, packed,
  snorm/uint/sint, compressed) throw `VGPU-CORE-UNSUPPORTED-FORMAT`.
* **Missing `copy_src` usage**: the render target must include `"copy_src"` in
  addition to `"render_attachment"`; otherwise readback copy validation fails.
* **Unflushed queue**: submit the render commands and `await device.queue.flush()`
  before `await target.read()` so the readback observes the completed frame.
* **Non-deterministic inputs**: freeze clock, random seeds, camera, viewport,
  device options, and fixture data. Include those names in labels and snapshot
  filenames.
* **Native environment failures**: Xvfb or Mesa setup problems are
  host/container issues — re-run in `pnpm test:docker`. Dawn load errors are
  handled by the adapter's portable-prebuild fallback; if one still surfaces,
  the structured `VGPU-NODE-*` error names the fix (typically
  `npx vgpu install-dawn` or `VGPU_DAWN_BINARY`).

## Related work

This workflow builds on the core readback and Node adapter/Docker work tracked in
\#81 and #82, and it relates to the explicit frame-helper work referenced by #83
when a test wants a higher-level frame abstraction instead of raw command
encoders. It is documentation only and leaves future API changes to separate
issues.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)