---
title: Migrating to 0.6.0
description: Upgrade from 0.5.0 or earlier 0.6.0 candidates to prepared shaders, explicit scene state and validated resource lifetimes.
---

# Migrating to 0.6.0



This guide targets stable `0.6.0`. Update `vgpu` and any
directly installed packages in its fixed release group to the same version:
`@vgpu/core`, `@vgpu/wgsl`, `@vgpu/wgsl-std`, `@vgpu/adapter-node`,
`@vgpu/adapter-mock`, `@vgpu/render` and the optional `@vgpu/native` companion.

```bash
npm install --save-exact vgpu@0.6.0
# If the app imports WGSL tooling, runtime preparation or WGSL types directly:
npm install --save-exact @vgpu/wgsl@0.6.0
# Only if the project already uses native generation:
npm install --save-dev --save-exact @vgpu/native@0.6.0
```

The native companion remains beta with the same toolchain and platform qualifications as
`0.5.0`. Regenerate and verify native output with the matching version. No new native
configuration or generated Swift API migration is required by this release.

Choose the path matching the version your project actually uses:

| Starting version             | Path to `0.6.0`                                                                                                              |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `0.5.0`                      | Follow the applicable stable-origin steps below. Start with shaders before adapting scene or frame code.                     |
| `0.6.0-rc.0`                 | Follow steps 1 and 6, then the RC verification notes. Keep the other adaptations already completed.                          |
| `0.6.0-rc.1`                 | Update matching packages and rebuild. No additional API migration; verify the encode and index-upload fixes described below. |
| `0.6.0-rc.2`                 | Update matching packages and rebuild. No runtime API or behavior changes since this candidate.                               |
| Earlier stable or `0.5.0` RC | Complete the applicable [0.5.0 migration](/docs/migrations/0.5.0), then follow the `0.5.0` path here.                        |

## From the previous stable release

For projects on `0.5.0`, apply the steps relevant to the APIs they use. Projects on an
older stable first apply the [0.5.0 migration](/docs/migrations/0.5.0), then this guide.
This is a pre-1.0 release with compatibility changes; it is not a drop-in update for
every existing scene or frame callback.

Use this checklist to find the steps that affect your application:

| Existing usage                                                                            | Required review                                                |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `draw`, `effect` or `compute` shader inputs; WGSL loaders or cached assets                | Step 1: prepared shaders and a matching loader.                |
| Scene meshes/materials/lights, camera nodes, controls or object `orbit()`                 | Step 2: explicit scene and camera state.                       |
| Multiple entries in a shader, or a `Surface` used as an input binding                     | Step 3: entry selection and offscreen inputs.                  |
| Compute preparation, async frame callbacks or uniforms changed between commands           | Step 4: compilation, synchronous encoding and captured values. |
| Recovery through global frame-loop errors or direct device disposal                       | Step 5: `gpu.lost` and wrapper teardown.                       |
| Binding error handlers, buffer ranges, storage aliases or consumers shared across bundles | Step 6: validation and bundle ownership.                       |
| `canvasMouseTracker`                                                                      | Step 7: explicit coordinate spaces.                            |

The new instance bridge, Surface depth/MSAA and explicit consumer disposal are optional.
Adopt them only where needed; the relevant steps distinguish additions from required changes.

### 1. Prepare shaders before creating renderer consumers

Both `0.5.0` and `0.6.0-rc.0` accepted WGSL strings and v1 loader artifacts.
`draw()`, `effect()` and `compute()` now require prepared `ShaderSource` v2 objects.
Upgrade the loader and renderer together, and declare `@vgpu/wgsl@0.6.0` directly
in any app that imports its tooling or runtime helpers. Isolated package managers do not
make transitive tooling imports available. Build environments must install that dependency.

| Shader source or bundler                                | Upgrade action                                                                                              |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Static `.wgsl` through Vite                             | Keep `wgslVitePlugin` and rebuild with the upgraded packages. Pass the import itself, not `.wgsl`.          |
| Static `.wgsl` through webpack                          | Keep `@vgpu/wgsl/loader-webpack` and rebuild with the upgraded packages.                                    |
| Next.js with Turbopack                                  | Replace the handwritten `"*.wgsl"` rule with `wgslTurbopackRule()` from `@vgpu/wgsl/next`, then rebuild.    |
| Prebuilt/committed/third-party JS or JSON shader assets | Regenerate at their source; application loaders do not process assets that bypass them.                     |
| WGSL generated or loaded at runtime                     | Call `prepareShader()` once per source revision, before constructing consumers and outside frame callbacks. |

#### Rebuild static shaders and preserve valid caches

For Turbopack, after installing the upgraded tooling, replace only the WGSL rule and keep
other settings and wrappers. Move existing `minify` options into the helper argument:

```ts
import { wgslTurbopackRule } from "@vgpu/wgsl/next";

export default {
  turbopack: {
    rules: { "*.wgsl": wgslTurbopackRule() },
  },
};
```

The helper fingerprints the installed compiler in loader options, so a restored `.next/cache`
can remain. Restart `next dev` after compiler upgrades or rebuilds. If retaining the old bare-string
Turbopack rule, clear that app's `.next/cache` once after upgrading instead. The helper configures
Turbopack only: preserve the webpack hook for webpack mode (`next build --webpack` on Next 16,
or omit `--turbopack` on Next 15.5). Stable Next 15.5 does not support Turbopack persistent caching.

Webpack and Vite need no routine cache deletion. Clear the affected cache only if stale v1 output
persists; webpack also needs a one-time clear for an installed managed package replaced in place
at the same version and resolved loader path. Rebuilding the application does not upgrade
prebuilt shader assets that bypass these loaders.

Leaf WGSL files now undergo reflection during the build. Fix reported parse/layout errors,
including host-unshareable `bool` uniform members. Loaders still do not validate against a
GPU device; keep `vgpu check --require-validation` in your validation workflow.

#### Prepare runtime and legacy inputs explicitly

Before:

```ts illustrative
const post = effect(gpu, sourceText);
const scene = draw(gpu, { shader: resolved.wgsl });
```

After:

```ts illustrative
import { prepareShader } from "@vgpu/wgsl/prepare";
import postShader from "./post.wgsl";

const post = effect(gpu, postShader); // prepared by the build-time loader
const dynamic = effect(gpu, prepareShader(sourceText, "dynamic.wgsl"));
const scene = draw(gpu, { shader: prepareShader(resolved, "scene.wgsl") });
const legacy = effect(gpu, prepareShader(legacyAsset, "legacy.wgsl"));
```

Pass the complete `resolveShader()`/`compile()` result or legacy object to preserve
`functionExports`. `compile()` and `reflectSource()` retain their string contracts; their
outputs are not automatically renderer inputs. Native `createShaderModule({ code })` and
`vgpu/three` keep their existing string/legacy contracts.

Runtime preparation includes the parser in that consumer and reparses on every call. Prefer
static `.wgsl` imports for static shaders; they need no browser `prepareShader()` call or
manual metadata decoder configuration. Compact loader output preserves the same v2 contract.

Prepared loader/helper artifacts are deeply frozen. Keep application tags/caches in separate
objects or a `WeakMap`; modifying the artifact throws in strict mode. Rebuild or prepare a new
artifact when source changes, and never hand-edit reflection or checksums. Deeply immutable
artifacts share validation snapshots across consumers/devices. Mutable clones remain accepted
but are revalidated per creation; a shallow spread still shares frozen nested objects.
The checksum detects accidental WGSL replacement, not authenticity or reflection correctness.

Raw strings/v1 artifacts throw `VGPU-SHADER-SOURCE-UNPREPARED`; other integer versions throw
`VGPU-SHADER-SOURCE-VERSION`; malformed or inconsistent data throws `VGPU-SHADER-SOURCE-INVALID`.
Fix the producing toolchain or prepare the original source instead of editing the version field.

### 2. Replace scene object and camera authoring

Geometry recipes, `geometries`, `degToRad`, `srgb`, geometry types, `group` and
`SceneNode` remain. Scene composition now supplies transforms, camera matrices and
instance data to application-owned shaders and draws.

| Previous export or usage                                                                                                                           | Replacement                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `scene()`                                                                                                                                          | `group()` as the root.                                                                                                                        |
| `mesh()`, `MeshNode`                                                                                                                               | A `group` or external world matrix, plus explicit `geometry()` and `draw()`. Use `instances()` and `instanceGeometry()` for repeated objects. |
| `SceneMaterial`, `UnlitMaterial`, `LambertMaterial`, `NormalMaterial`, `ShaderMaterial`, their lowercase factories and material option/value types | Application WGSL, pipeline options and named bindings. There is no replacement material object.                                               |
| `AmbientLight`, `DirectionalLight`, their lowercase factories and option/value types                                                               | Application light data passed to the shader.                                                                                                  |
| `PerspectiveCamera`, `perspectiveCamera` and their option/value types                                                                              | `Lens`, `Pose`, `perspective()` and `viewMatrices()`.                                                                                         |
| `OrthographicCamera`, `orthographicCamera` and their option/value types                                                                            | `orthographic(bounds, out)` and `viewMatrices()`.                                                                                             |
| `Camera`, `SceneCamera`, `CameraVec3`                                                                                                              | `Pose`, `Lens`, `CameraMatrices`, and `Vec3Like` or `ArrayLike<number>`.                                                                      |
| `OrbitControls`, `orbitControls` and their element/option/value types                                                                              | Application input listeners, `orbitRig()`, `orbit()`, `pan()`, `dolly()`, `smoothRig()` and `rigPose()`.                                      |
| `orbit(time, options)`, `OrbitOptions`                                                                                                             | `composeMatrix()` for object animation. The new `orbit(rig, deltaYaw, deltaPitch, limits?)` changes a camera rig.                             |
| Other `SceneNodeKind` values                                                                                                                       | Only `"group"` remains; keep application classifications separately.                                                                          |

#### Keep transforms independent of geometry and materials

Before:

```ts illustrative
const root = scene();
root.add(mesh(box(), normalMaterial(), { position: [1, 0, 0] }));
const model = orbit(time, { radius: 2, height: 0.5, speed: 0.3 });
```

After:

```ts
import { composeMatrix, group, instances } from "vgpu/scene";

const root = group();
const object = group({ position: [1, 0, 0] });
root.add(object);
const objects = instances({ capacity: 100 });
const id = objects.add();
objects.bindWorld(id, () => object.worldMatrix);
object.set({ position: [2, 0, 0] });
objects.syncWorlds();

const model = new Float32Array(16);
function animateObject(time: number) {
  const angle = time * 0.3;
  composeMatrix({
    position: [Math.cos(angle) * 2, 0.5, Math.sin(angle) * 2],
    rotation: [0, angle, 0],
  }, model);
}
```

`composeMatrix()` writes into reusable output; it does not allocate the old orbit matrix.
Keep radius, height and speed in application state, including a zero radius when intended.
Change borrowed node vectors through `.set()` rather than mutating them. Reparenting preserves
the local transform. `visible` remains metadata and does not suppress a draw or instance.
Nonzero quaternions are normalized; zero quaternions and non-finite node values throw
`VGPU-SCENE-VALUE`.

Instance capacity is fixed. Growing it requires a new collection and GPU bridge, copied
application data, restored world bindings and replacement handles/draws. Handles remain stable
only during an instance's lifetime; removal can move dense slots. Keep picking mappings in
application state and do not serialize `InstanceId` values.

External hierarchies can use `hierarchyOrder()` and `evaluateHierarchy()` with finite affine
local matrices. Rebuild the order after changing parents and fully evaluate after topology or
world-output replacement/reset. Dirty flags reduce matrix recomputation, not traversal, and
remain caller-owned. Physics with final world matrices can call `setWorlds()` directly.
If a bound `syncWorlds()` source throws, earlier results may already be copied: abort the frame
and repair/retry or unbind those sources before publishing.

#### Own camera state and repack camera uniforms

Before, a camera object carried projection and view state:

```ts illustrative
const camera = perspectiveCamera({ fov: 45, aspect, position: [2, 2, 3] });
camera.set({ aspect });
drawable.set({ camera: { viewProjection: camera.viewProjection } });
```

After, supply the lens, world pose and output matrices explicitly:

```ts
import { group, perspective, viewMatrices } from "vgpu/scene";

const eye = group({ position: [2, 2, 3] });
const lens = { fov: 45, near: 0.1, far: 100 };
const pose = { position: new Float32Array(3), quaternion: new Float32Array(4) };
const projection = new Float32Array(16);
const matrices = { view: new Float32Array(16), viewProjection: new Float32Array(16) };

function updateCamera(aspect: number) {
  eye.lookAt([0, 0, 0], [0, 1, 0]);
  pose.position.set(eye.worldPosition);
  pose.quaternion.set(eye.quaternion); // eye is unparented: its local pose is world space
  perspective(lens, aspect, projection);
  viewMatrices(pose, projection, matrices);
}
updateCamera(16 / 9);
```

Repeat `drawable.set({ camera: { viewProjection: matrices.viewProjection } })` after every
matrix update. Uniform `.set()` packs immediately; previous calls do not retain live matrix
arrays. The shader owns the resource name and binding location; scene reserves no bind group.

For a parented camera, preserve the full old world transform, including parent scale, by
inverting `cameraNode.worldMatrix` with `invertAffine()` and multiplying projection by view
with `multiplyMatrices()`. A `Pose` contains world position/quaternion only; copying a
parented node's local quaternion loses the parent transform. Zero scale is not invertible
and throws `VGPU-SPATIAL-SINGULAR`.

Choose the old defaults (`near: 0.1`, `far: 100`, `aspect: 1`) explicitly if required.
Perspective requires finite `0 < near < far`, positive finite aspect and FOV in degrees
strictly between 0 and 180. Replace infinite far planes with a finite distance. Orthographic
bounds require finite `left < right`, `bottom < top`, and `0 <= near < far`; formerly accepted
mirrored bounds now throw `VGPU-CAMERA-VALUE`. Supply your own projection to `viewMatrices()`
when a mirrored projection is intentional. Both helpers use right-handed −Z view space and
WebGPU depth 0..1.

For input, create independent goal/current rigs. Apply events to the goal; call
`smoothRig(current, goal, dt, { timeConstant })` with finite nonnegative seconds, then
`rigPose()`. Remove input listeners on teardown. Smoothing uses logarithmic distance,
returns the current rig, and has no epsilon snap or changed boolean. It no longer substitutes
1/60 for missing/non-finite `dt`; implement your own settling policy. A zero time constant
snaps even at zero `dt`; otherwise zero `dt` leaves the current state unchanged. The default
pitch range is ±(π/2−1e−4); pass explicit limits to retain the old ±(π/2−0.01) range.
`dolly()` changes distance; `zoom()` changes the lens.

#### Publish instance data before encoding its consumers

Import `instanceGeometry` from `vgpu/scene/gpu`; it borrows a live, non-instanced mesh from
the same GPU. Declare four world columns and all custom instance attributes in each consuming
vertex entry. A world matrix costs four attributes and 64 bytes per record, in addition to
custom 32-bit attributes. Respect device limits, the eight-buffer limit and 2048-byte stride;
split batches or reduce attributes/capacity on `VGPU-INSTANCE-LAYOUT`.

```ts illustrative
objects.syncWorlds(); // or objects.setWorlds(ids, finalWorlds)
const count = bridge.publish();
drawable.set({ camera: { viewProjection: matrices.viewProjection } });
frame(gpu, f => f.pass(screen, pass => pass.draw(drawable, { instances: count })));
```

`publish()` does not update camera/world sources and returns an explicit count, including zero.
Unchanged publication uploads no instance bytes; separate bridges track uploads independently.
Record the count explicitly in bundles and re-record when it changes. Publish before all passes
that consume those bytes. Do not rewrite a buffer while earlier consumers remain unsubmitted;
distinct per-pass contents require separate buffers. Later queue-ordered frames need no idle wait.

Destroying a bridge destroys its added buffer and composed wrapper, not the borrowed mesh.
Destroying the mesh invalidates dependent draws and bundles. Recreate the bridge/draw and
re-record bundles; raw buffer destruction outside the geometry lifecycle is not tracked.
Ordinary destroyed geometry now fails synchronously with `VGPU-MESH-LAYOUT-INVALID`; bridges
use `VGPU-INSTANCE-DESTROYED`.

WGSL can import `instanceWorldMatrix`, `transformPosition`, `transformDirection` and
`transformNormal` from `@vgpu/wgsl-std/scene`. They introduce no bindings. Use the complete
world/deformation transform: position uses w=1, direction w=0, and normals use normalized
inverse-transpose with reflection sign. Extreme ill-conditioned float32 inputs or backend
subnormal handling can lose direction/sign accuracy; the zero fallback has no epsilon or
symbolic-rank guarantee. See [Scene composition](/docs/guides/scene-composition) for complete setups.

### 3. Choose shader entries and distinguish inputs from presentation

#### Pin shader entries when preserving the previous choice

With multiple entries of one stage, automatic selection now prefers `vs_main`, `fs_main`
or `cs_main`. A single entry can have any name; without the conventional name the first
entry still wins. If `0.5.0` selected an earlier nonconventional entry, name it explicitly:

```ts illustrative
draw(gpu, { shader, entry: { vertex: "previous_vertex", fragment: "previous_fragment" } });
compute(gpu, computeSource, { entry: "previous_kernel" });
effect(gpu, fragmentSource, { entry: { fragment: "previous_fragment" } });
```

Override only affected stages. Selection determines resources and vertex input layouts as well
as execution; compare pixels/readbacks and bind everything the chosen entry uses. Invalid names
or wrong stages throw `VGPU-ENTRY-INVALID`. Effects expose fragment selection only. For an effect
with custom vertices that must avoid `vs_main`, rename the unselected conventional function or
use `draw()` with explicit selections. Fragment-only effects retain the generated fullscreen vertex.

Runtime WGSL string compilation now discovers attributed compute entries, Unicode names and
WGSL whitespace correctly while ignoring comments. Its signature and shader passthrough are
unchanged; corrected metadata requires no new call shape.

#### Replace Surface input bindings with offscreen resources

Passing a `Surface` to a Draw, Effect or Compute binding now throws
`VGPU-SURFACE-NOT-BINDABLE`, both in constructor `set` options and later `.set()` calls.
The surface remains a render destination. Replace a sampled presentation workflow:

```ts illustrative
frame(gpu, f => {
  f.pass(screen, producer);
  consumer.set({ source: screen });
  f.pass(preview, consumer);
});
```

with an offscreen producer and final presentation:

```ts
import { effect, frame, init, surface, target } from "vgpu";
import { prepareShader } from "@vgpu/wgsl/prepare";

const gpu = await init();
const screen = surface(gpu, document.querySelector("canvas")!);
const intermediate = target(gpu, { size: screen.size });
const producer = effect(gpu, prepareShader(`
  @fragment fn main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 0.0, 1.0);
  }
`));
const consumer = effect(gpu, prepareShader(`
  @group(0) @binding(0) var source: texture_2d<f32>;
  @fragment fn main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return textureLoad(source, vec2i(uv * vec2f(textureDimensions(source))), 0);
  }
`), { set: { source: intermediate } });
screen.onResize(({ width, height }) => intermediate.resize([width, height]));
await producer.compile(intermediate);
await consumer.compile(screen);
frame(gpu, f => {
  f.pass(intermediate, producer);
  f.pass(screen, consumer);
});
```

A bound `Target` follows attachment replacement after resize. An explicit `Texture` keeps that
identity: rebind it after replacement. Read intermediate results with
`intermediate.color.read({ mipLevel: 0, region: "all" })`. `surface.color` remains the current
presentation texture; retaining it does not extend its lifetime beyond presentation.

#### Opt into direct depth-tested canvas rendering when useful

Existing surfaces keep no depth attachment and one sample. Existing two-pass offscreen rendering
still works. For direct presentation, `surface(gpu, canvas, { depth: true, msaa: 4 })` adds
`depth24plus` and four samples. `depth` also accepts supported explicit depth-aspect formats;
`msaa: true` also means four samples. No `sampleCount` option is added.

`compile(surface)`, `compileSync(surface)`, Draw constructor `targets`, and bundle recording
can run outside frames using the configured color/depth/sample signature without acquiring a
canvas texture. They honor opt-in depth/MSAA; they are not limited to one sample or no depth.
When supplying an explicit signature, match the actual surface format and attachments; query
the preferred canvas format before creating a surface rather than assuming BGRA/RGBA. Draw
and replay canvas work inside `frame()` or `frameLoop()`.

`surface.depth` is owned and stable until replacement/disposal. MSAA color storage is private
and resolves into the single-sample `surface.color`. To sample depth, bind `surface.depth`
explicitly on a surface without `msaa` and rebind it in `onResize` after replacement. Resize
outside the frame, before encoding attachment consumers. Explicit and automatic layout/DPR
resizes notify listeners after committing replacements;
a listener failure does not undo the resize or prevent other notifications/old-attachment cleanup.
Synchronous allocation failure preserves the previous surface size and attachments. Recursive
resize during replacement notification throws `VGPU-SURFACE-RESIZE-REENTRANT`.

Direct canvas dimension writes between frames reconcile owned attachments before the next frame
callback without public resize notifications. The pass descriptor also reconciles later writes,
but mid-frame changes can invalidate attachments already encoded. Use explicit resize and rebind
when consumers retain depth. Offscreen targets remain useful for sampling, postprocessing,
history and readback; these additions require no rewrite of an existing offscreen workflow.

### 4. Prepare asynchronously, then encode frames synchronously

`compute()` now constructs a lazy pipeline. Call `await simulation.compile()` before rendering
when preparation must finish in advance; the promise rejects compilation failures and resolves
to the same object. `compileSync()` or first dispatch compiles synchronously. Native synchronous
creation failures now throw, and automatic reuse of a failed pipeline throws rather than silently
skipping work. An explicit compile retries. `compile()` after `compileSync()` waits for native
validation, including synchronous takeover of pending asynchronous compilation.

Move awaits out of `frame`, `frameLoop`, equivalent `FrameRunner` methods and compute-pass
callbacks. For example:

```ts illustrative
// Before
frame(gpu, async f => {
  await loadAssets();
  simulation.dispatch(64);
  f.pass(output, display);
});

// After
await loadAssets();
await simulation.compile();
frame(gpu, f => {
  f.computePass(pass => pass.dispatch(simulation, 64));
  f.pass(output, display);
});
```

`simulation.dispatch()` still submits independently, even inside a frame. Use `computePass()`
for ordered render/compute work in one frame submission and cancellation. Passes cannot nest;
do not retain the pass after its callback or submit/cancel before it ends. Dispatch counts must
be finite nonnegative integers within the granted device limit, and workgroup dimensions/product
must fit device limits. Request supported larger limits through `init({ requiredLimits })` or
reduce workgroups; defaults did not increase.

Inferred Promise/PromiseLike returns, unions containing them, and unresolved generic callback
returns fail typechecking. Concrete synchronous returns remain accepted and ignored. For a
forwarder whose contract ignores the result, accept `FrameLoopCallback` or `(frame: Frame) => void`
and pass the callback directly. Do not erase an async continuation with casts or wrappers.
Callbacks typed `void`, `unknown` or `any` can pass types but still throw
`VGPU-ASYNC-FRAME-CALLBACK` if they return a thenable. The open frame is canceled before implicit
submission, and a failing loop stops. Already submitted work and CPU-side continuations are not
rolled back; later encoding through that canceled frame fails with `VGPU-FRAME-CANCELED`.

#### Set each direct operation's uniform values before encoding it

Direct frame draws and dispatches now capture managed uniform values per operation:

```ts illustrative
frame(gpu, f => f.computePass(pass => {
  simulation.set({ dt: 0.01 });
  pass.dispatch(simulation, 64); // captures 0.01
  simulation.set({ dt: 0.02 });
  pass.dispatch(simulation, 64); // captures 0.02
}));
```

If the old application expected the last update to affect every operation, set that common value
before encoding them. Capture covers JS-owned uniform bindings and `uniforms()` used as uniforms.
Storage, shared uniforms adopted as storage, raw buffers, claimed groups and render bundles retain
live buffer contents. Use separate resources for distinct bundle/raw-buffer values in one
submission. Host writes and CPU mutations are outside frame cancellation.

Upload deferral preserves these semantics: `.set()` still validates and packs immediately,
one-shot work uploads pending values when needed, and exposed stable raw buffers are flushed
before exposure and continue receiving immediate updates. No consumer flush call is added.
Identical managed packed bytes now reuse the same frame snapshot, and frame pages/bind groups are pooled. Redundant `.set()` calls remain valid;
storage, bundles and exposed/raw buffers keep live write semantics. No migration is required
for this reuse optimization.

#### Keep completion and error handling separate

Register `gpu.onError` for asynchronous validation/execution failures. `frame.done` and
`gpu.settled()` remain resolve-only completion signals; neither they nor a later storage read
prove that the producer succeeded. `settled()` now also waits for queue work already submitted
when called, so it can take longer; later submissions are outside that snapshot. Calls after
observed loss/disposal retain tracked waits without creating a new queue fence.

For a nonconforming queue whose `onSubmittedWorkDone()` throws synchronously, an already-submitted
standalone dispatch now reports one `VGPU-COMPUTE-VALIDATION` through `gpu.onError` at
`<label>.completion` instead of throwing after submission. Conforming queues require no adaptation.

### 5. Handle device loss explicitly and preserve normal teardown

Use the stable readonly `gpu.lost` promise to detect native loss observed while the wrapper is
active. Every running frame loop stops before its handlers run. The next tick no longer emits
an uncaught loss error, so replace global-error-based recovery. Loss does not dispose resources,
notify `gpu.onError` or automatically recreate the device:

```ts illustrative
void gpu.lost.then(() => {
  gpu.dispose();
  return start(); // init(), recreate resources and restart the loop
}).catch((error: unknown) => console.error("GPU restart failed", error));
```

Dispose the old surface/Gpu before configuring its replacement on the same canvas.
`gpu.lost` never rejects, but recovery code can. Factories, `clock()`, `frame()` and `frameLoop()`
now throw `VGPU-DEVICE-LOST` synchronously after observed loss. An already-open manual frame
remains open; `submit()` throws until normal disposal cancels it. Explicit submit error handling
does not change.

Use `gpu.dispose()` for teardown, including inside a frame callback. Direct core
`gpu.device.dispose()`/`destroy()` during a callback now allows implicit-submit errors to escape
instead of swallowing `VGPU-DEVICE-DISPOSED`; a failing loop stops and rethrows.

Normal unmount remains: stop the loop, await `gpu.settled()` if submitted work/deliveries must
finish, then `gpu.dispose()`. Never await `gpu.lost` for teardown. Disposal before vgpu observes
native loss leaves it pending; disposal after observation preserves its resolved value. Disposing
a wrapper from `initFromDevice()` never destroys the borrowed native device. If its owner destroys
that device while the wrapper is active, loss resolves with `reason: "destroyed"`.

Bundles now release abandoned facade retention automatically. Optional, synchronous, idempotent
`bundle.dispose()` drops references immediately without destroying borrowed resources or revoking
a previously saved native bundle handle. Existing recording/replay calls require no migration.
Continue re-recording when captured resource identities, draw counts or render signatures change;
automatic cleanup does not rebuild stale bundles or promise driver-memory release timing.

### 6. Validate bindings and keep separately replayable bundles independent

Binding `set()` calls on Draw, Effect and Compute now report `VGPU-R1-BINDING-DESTROYED`
for an already-destroyed tracked Buffer, Uniform-like value or provider. Update handlers
that matched `VGPU-BUFFER-DISPOSED` at this boundary; direct Buffer `read()`/`write()`
continue using `VGPU-BUFFER-DISPOSED`.

Raw `GPUBuffer` and `{ buffer, offset?, size? }` bindings are live resources. Their descriptors
are snapshotted by `set()`: call `set()` again after changing a range. Only `undefined` selects
defaults (`offset: 0`, `size: buffer.size - offset`); `null` and numeric strings are invalid.
Use the reflected uniform/storage usage, a nonnegative safe-integer offset aligned to the
granted device limit, and a positive in-bounds size between the reflected minimum and granted
maximum. Storage sizes must be multiples of four. Tracked bindings receive the same checkable
range validation and retain synchronous usage checks.

Invalid usage/ranges now throw `VGPU-R1-BINDING-INCOMPATIBLE-RESOURCE` at `set()` instead
of waiting for native bind-group validation. The rejected binding retains its previous value.
This guarantee applies to that binding, not the whole `set({ a, b })` call: earlier valid
entries may already have been applied when a later entry throws.
Ordinary struct values with numeric fields named `buffer`, `offset` or `size` remain valid.
For compute storage, use separate allocations whenever either binding is writable, including
when one binding is tracked and the other raw. Even disjoint ranges of one allocation produce
`VGPU-R1-STORAGE-ALIASING`; use two allocations or `pingPongStorage()`.

Rebinding a Draw/Effect while recording bundle B now makes an earlier completed bundle A stale.
For separately replayable bundles with different resource identities or claimed groups, keep a
stable consumer per bundle (two consumers for ping-pong directions), or re-record and replace
the old bundle after rebinding. Rebinding between draws within one recording remains supported.

**Known limitation in 0.6.0:** after a partially failed multi-binding `Draw.set({ a, b })`,
a previously recorded bundle may still replay the old `a` reference. Repair the bindings and
explicitly re-record the bundle before replaying it. Automatic retention cleanup does not fix
this pre-existing error-path gap.

Before, a shared consumer could leave an earlier bundle replayable after a later recording:

```ts illustrative
const forward = bundle(gpu, { target: output }, encoder => encoder.draw(shared));
const backward = bundle(gpu, { target: output }, encoder => {
  shared.set(reverseBindings);
  encoder.draw(shared);
});
// Managed replay of forward now throws VGPU-R3-BUNDLE-STALE.
```

After, create two consumers with stable resource identities:

```ts illustrative
const forwardDraw = draw(gpu, { shader, set: forwardBindings });
const backwardDraw = draw(gpu, { shader, set: reverseBindings });
const forward = bundle(gpu, { target: output }, encoder => encoder.draw(forwardDraw));
const backward = bundle(gpu, { target: output }, encoder => encoder.draw(backwardDraw));
```

Optional, synchronous, idempotent `dispose()` on Draw, Effect and Compute releases consumer
state without destroying borrowed resources. Later calls fail with `VGPU-DRAW-DISPOSED`
(Draw/Effect) or `VGPU-COMPUTE-DISPOSED`. Disposing a recording's consumer invalidates its
managed bundles; replace the consumer and re-record before replay. Already encoded commands
and saved native bundle handles are not revoked. Collection without explicit disposal leaves
retained bundles valid through independent resource snapshots; collection timing is unspecified.
Generated compatible texture views are reused, while explicit `Texture.createView()` stays fresh.

Unchanged draws, effects and computes now reuse passing binding checks and static bind-group
keys; draws and effects also reuse derived pipeline keys. No migration is required: binding
changes and tracked resource destruction still invalidate that work, pipeline failures still
surface, and uniform capture, live buffer writes and bundle staleness keep their semantics.

`geometry(gpu, { indices })` now accepts odd-length `Uint16Array` inputs by padding only the
initial upload. Existing valid calls need no changes. The logical index count and writable byte
range exclude padding, and `writeIndices` still requires four-byte alignment: it cannot rewrite
the last index of an odd-length array. If every drawn index must be rewritable, supply an
even-length array and set the intended logical `indexCount` explicitly.

### 7. Select the intended mouse coordinate space

`canvasMouseTracker` from `@vgpu/render/utils` no longer accepts `normalize`, and `position` is no
longer a tuple. Remove the option and choose `position.normalized` or `position.canvasPixels`:

```ts
import { canvasMouseTracker } from "@vgpu/render/utils";

const canvas = document.querySelector("canvas")!;
const pointer = canvasMouseTracker({ canvas, flipY: true });
const [u, v] = pointer.position.normalized;
const [x, y] = pointer.position.canvasPixels;
pointer.dispose();
```

The former default tuple held CSS-offset pixels; `canvasPixels` holds drawing-buffer pixels.
For CSS/DOM positions, multiply `normalized` by `canvas.clientWidth`/`canvas.clientHeight`
and keep the default `flipY: false` for a top-left origin. Keep `flipY: true` for
a bottom-left origin. When interacting with a lower-resolution offscreen target, convert
normalized coordinates to that target's dimensions rather than reusing canvas pixels.

### 8. Keep optional documentation integrations optional

Scene references, numerical-library examples and Blender skill resources add discovery and
authoring guidance without new runtime dependencies or required application changes. Keep your
project's chosen math library; install an optional one only for your own numerical needs.
Preferred-format examples now describe the existing format contract correctly. The functional
Surface preparation/depth changes are consolidated in step 3; no intermediate migration is needed.

## From release candidates

### From `0.6.0-rc.2`

Update the fixed packages together to `0.6.0` and rebuild. No additional API or behavior
migration is required: the stable release retains this candidate's runtime implementation.
Keep its prepared shaders, loader configuration, scene state, frame callbacks and binding
ownership. The multi-binding `Draw.set()` limitation in step 6 still applies.

### From `0.6.0-rc.1`

Update the fixed packages together to `0.6.0` and rebuild. No additional API migration
is required: the encode optimization preserves existing behavior, and the index-upload fix
only accepts inputs that previously failed during creation. Keep the prepared shaders, loader
configuration and binding/lifetime adaptations already completed for `rc.1`. Verify repeated
draws and odd-length indices as described in verification item 9; do not repeat the stable-origin
API rewrites.

### From `0.6.0-rc.0`

For `0.6.0-rc.0`, apply **step 1** (prepared v2 shaders and loader/cache configuration)
and **step 6** (binding errors/ranges, compute aliases and cross-recording bundle ownership).
That published RC emitted v1 artifacts and had no `@vgpu/wgsl/next` helper, so upgrade packages
before importing the helper. Both changes also apply to stable-origin users; no reversal requires
an RC-only repair. The managed-uniform reuse optimization in step 4 requires no adaptation.

Keep the scene, shader-entry selection, Surface, synchronous-frame, device-loss and mouse
adaptations already completed for `rc.0`; do not repeat them or restore removed APIs. Consumer
`dispose()` remains optional. Users of `0.5.0-rc.0` or `0.5.0-rc.1` first complete their applicable
[0.5.0 RC path](/docs/migrations/0.5.0#from-release-candidates), then apply the stable-origin steps
above, retaining already-completed adaptations.

## Verification

1. Typecheck against matching package versions. Rebuild with the restored production cache and
   verify imported shaders have `version: 2`, producer `"@vgpu/wgsl/prepare-v2"`, a string
   `sourceChecksum` and object `reflection`. Render deployed shader consumers without shader-source
   errors or artifact mutation `TypeError`s. Delete/restore a WGSL dependency in watch mode and
   confirm recovery. Keep device validation separate from build-time reflection.
   Find removed scene imports, old `orbit` calls,
   async/unresolved-generic frame callbacks, `normalize` and tuple-valued mouse positions.

2. Exercise object reparenting, explicit camera aspect updates, finite projection bounds, independent
   camera rigs/time steps, zero instances, removals and bridge/mesh destruction. Change a matrix,
   repack its uniform and confirm the next rendered output changes. Verify custom attributes and
   transformed normals on the intended GPU.

3. Compile both automatic and explicitly selected shader entries and compare pixels or compute
   readbacks. Confirm the chosen entry has its inputs and resources. Invalid explicit entries must
   fail with `VGPU-ENTRY-INVALID`.

4. Verify Surface input rejection and an offscreen producer/presentation replacement. Test direct
   surface rendering with default attachments and with depth/MSAA, resize before encoding, rebind
   explicit depth and check stale attachment/bundle handling. Read only current presentation data
   or an owned offscreen texture with explicit mip/region options.

5. On a real GPU, encode multiple operations with distinct managed uniforms and mixed compute/render
   passes; confirm ordering, values and one frame submission. Cancel a frame and confirm its encoded
   work is not submitted. Check live storage, indirect arguments and bundles separately. Await
   compilation; collect `gpu.onError` through completion before assessing execution results.

6. For loss recovery, destroy an externally owned native device wrapped by `initFromDevice()`.
   Confirm `gpu.lost` resolves after loops stop, resources remain undisposed until teardown, and
   recreated resources render on a new GPU. Verify ordinary unmount finishes without awaiting loss.

7. Check pointer alignment when CSS and drawing-buffer dimensions differ, including an upscaled
   offscreen target. For native users, run the installed `vgpu native doctor`, `check`, `build`
   and `verify` and consume the regenerated Swift package on the qualified target platform.

8. Test destroyed-binding diagnostics separately from direct Buffer operations. Reject an invalid
   range and confirm its previous binding remains active; read two valid aligned ranges and compare
   their bytes. Test mixed tracked/raw writable aliases and separate-allocation replacements.
   Record A, rebind during B and verify A becomes stale; verify two stable consumers replay correctly.
   If adopting disposal, check disposed-consumer errors and re-record managed bundles. These checks
   apply to the `rc.0` path as well as stable upgrades.

9. For `rc.0` and `rc.1` upgrades, render repeated unchanged draws, then change a binding, write a live
   buffer, resize a followed target and destroy a tracked resource. Confirm updated pixels and
   the existing destroyed-binding diagnostic. Render an odd-length `Uint16Array` geometry without
   native upload errors, confirm its logical draw count, and check that padding cannot be drawn
   or written through `writeIndices`.

10. For `rc.2` upgrades, verify matching `0.6.0` package versions, rebuild and smoke-test the
    production output. No new migration-specific regression path is introduced. For every origin,
    if a multi-binding `Draw.set()` fails, exercise the repair-and-re-record path from step 6;
    do not assume the call rolled back all bindings or invalidated every affected bundle.

Complete `ts` blocks participate in snippet typechecking. Blocks marked `ts illustrative`
describe historical or partial application code and are not standalone checked programs.


---

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)