Getting Started

Concepts & Architecture

Architectural decisions, runtime lifecycle, and the mental model behind Motion GPU.


This page explains the architectural decisions behind Motion GPU, how the runtime pieces fit together, and what happens on every frame. Understanding this foundation makes the rest of the documentation much easier to navigate.

Design goals

Motion GPU is intentionally strict in input contracts and explicit in scheduling. These four goals drive every design decision:

Goal What it means in practice
Deterministic pipeline rebuilds Renderer recreation is keyed off a stable signature derived from compiled shader source, uniform layout, texture bindings/config (including fragmentVisible), and storage resources — not from live values. This makes rebuild triggers predictable.
Predictable frame flow The scheduler owns all invalidation and render-mode gating. There is no hidden “auto-render on change” behavior except what you explicitly opt into via useFrame invalidation policies.
Minimal hidden magic Runtime state changes go through explicit calls (setUniform, setTexture, invalidate). No proxy traps, no implicit reactivity inside the render loop.
Recoverable failure UX Every error — from missing WebGPU support to WGSL syntax mistakes — is normalized into a structured report with stable code/severity/recoverability metadata, user hint, and optional source/context snippets. The default overlay is opt-out and can be replaced with a custom renderer.

Package layout

packages/motion-gpu/src/lib/
├── index.ts                        # Root framework-agnostic core entrypoint
├── advanced.ts                     # Root framework-agnostic advanced core entrypoint
├── svelte/
│   ├── index.ts                    # Explicit Svelte adapter exports
│   ├── advanced.ts                 # Explicit Svelte advanced exports
│   ├── FragCanvas.svelte           # Svelte host component
│   ├── frame-context.ts            # Svelte useFrame adapter wrapper
│   ├── motiongpu-context.ts        # Svelte useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # Svelte normalized pointer hook
│   ├── use-texture.ts              # Svelte lifecycle-aware texture hook
│   ├── use-motiongpu-user-context.ts # Svelte advanced user-context hook
│   ├── Portal.svelte               # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.svelte # Default error-overlay UI component
├── react/
│   ├── index.ts                    # Explicit React adapter exports
│   ├── advanced.ts                 # Explicit React advanced exports
│   ├── FragCanvas.tsx              # React host component
│   ├── frame-context.ts            # React useFrame adapter wrapper
│   ├── motiongpu-context.ts        # React useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # React normalized pointer hook
│   ├── use-texture.ts              # React lifecycle-aware texture hook
│   ├── use-motiongpu-user-context.ts # React advanced user-context hook
│   ├── Portal.tsx                  # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.tsx   # Default error-overlay UI component
├── vue/
│   ├── index.ts                    # Explicit Vue adapter exports
│   ├── advanced.ts                 # Explicit Vue advanced exports
│   ├── FragCanvas.vue              # Vue host component
│   ├── frame-context.ts            # Vue useFrame adapter wrapper
│   ├── motiongpu-context.ts        # Vue useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # Vue normalized pointer composable
│   ├── use-texture.ts              # Vue lifecycle-aware texture composable
│   ├── use-motiongpu-user-context.ts # Vue advanced user-context composable
│   ├── Portal.vue                  # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.vue   # Default error-overlay UI component
└── core/
    ├── index.ts                    # Framework-agnostic core entrypoint
    ├── advanced.ts                 # Framework-agnostic advanced core entrypoint
    ├── current-value.ts            # Reactive primitive independent from any framework
    ├── frame-registry.ts           # Scheduler DAG + invalidation engine
    ├── runtime-loop.ts             # Renderer lifecycle + requestAnimationFrame loop
    ├── scheduler-helpers.ts        # Presets/debug snapshot utilities
    ├── types.ts                    # Shared type definitions
    ├── material.ts                 # defineMaterial + resolveMaterial
    ├── material-preprocess.ts      # #include / defines expansion + line mapping
    ├── renderer.ts                 # WebGPU renderer creation + frame execution
    ├── shader.ts                   # WGSL code generation (fragment)
    ├── compute-shader.ts           # Compute shader WGSL generation + validation
    ├── storage-buffers.ts          # Storage buffer validation + normalization
    ├── uniforms.ts                 # Type inference, layout, packing
    ├── textures.ts                 # Texture normalization + helpers
    ├── texture-loader.ts           # URL fetch, decode, blob cache
    ├── render-graph.ts             # Pass execution planner
    ├── render-targets.ts           # Render target resolution
    ├── recompile-policy.ts         # Pipeline signature builder
    ├── error-report.ts             # Error normalization + classification
    └── error-diagnostics.ts        # Shader compile diagnostics payload
packages/motion-gpu/src/lib/
├── index.ts                        # Root framework-agnostic core entrypoint
├── advanced.ts                     # Root framework-agnostic advanced core entrypoint
├── svelte/
│   ├── index.ts                    # Explicit Svelte adapter exports
│   ├── advanced.ts                 # Explicit Svelte advanced exports
│   ├── FragCanvas.svelte           # Svelte host component
│   ├── frame-context.ts            # Svelte useFrame adapter wrapper
│   ├── motiongpu-context.ts        # Svelte useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # Svelte normalized pointer hook
│   ├── use-texture.ts              # Svelte lifecycle-aware texture hook
│   ├── use-motiongpu-user-context.ts # Svelte advanced user-context hook
│   ├── Portal.svelte               # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.svelte # Default error-overlay UI component
├── react/
│   ├── index.ts                    # Explicit React adapter exports
│   ├── advanced.ts                 # Explicit React advanced exports
│   ├── FragCanvas.tsx              # React host component
│   ├── frame-context.ts            # React useFrame adapter wrapper
│   ├── motiongpu-context.ts        # React useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # React normalized pointer hook
│   ├── use-texture.ts              # React lifecycle-aware texture hook
│   ├── use-motiongpu-user-context.ts # React advanced user-context hook
│   ├── Portal.tsx                  # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.tsx   # Default error-overlay UI component
├── vue/
│   ├── index.ts                    # Explicit Vue adapter exports
│   ├── advanced.ts                 # Explicit Vue advanced exports
│   ├── FragCanvas.vue              # Vue host component
│   ├── frame-context.ts            # Vue useFrame adapter wrapper
│   ├── motiongpu-context.ts        # Vue useMotionGPU adapter wrapper
│   ├── use-pointer.ts              # Vue normalized pointer composable
│   ├── use-texture.ts              # Vue lifecycle-aware texture composable
│   ├── use-motiongpu-user-context.ts # Vue advanced user-context composable
│   ├── Portal.vue                  # DOM portal utility for overlay rendering
│   └── MotionGPUErrorOverlay.vue   # Default error-overlay UI component
└── core/
    ├── index.ts                    # Framework-agnostic core entrypoint
    ├── advanced.ts                 # Framework-agnostic advanced core entrypoint
    ├── current-value.ts            # Reactive primitive independent from any framework
    ├── frame-registry.ts           # Scheduler DAG + invalidation engine
    ├── runtime-loop.ts             # Renderer lifecycle + requestAnimationFrame loop
    ├── scheduler-helpers.ts        # Presets/debug snapshot utilities
    ├── types.ts                    # Shared type definitions
    ├── material.ts                 # defineMaterial + resolveMaterial
    ├── material-preprocess.ts      # #include / defines expansion + line mapping
    ├── renderer.ts                 # WebGPU renderer creation + frame execution
    ├── shader.ts                   # WGSL code generation (fragment)
    ├── compute-shader.ts           # Compute shader WGSL generation + validation
    ├── storage-buffers.ts          # Storage buffer validation + normalization
    ├── uniforms.ts                 # Type inference, layout, packing
    ├── textures.ts                 # Texture normalization + helpers
    ├── texture-loader.ts           # URL fetch, decode, blob cache
    ├── render-graph.ts             # Pass execution planner
    ├── render-targets.ts           # Render target resolution
    ├── recompile-policy.ts         # Pipeline signature builder
    ├── error-report.ts             # Error normalization + classification
    └── error-diagnostics.ts        # Shader compile diagnostics payload

FragCanvas runtime lifecycle

FragCanvas is the single entrypoint that ties everything together. Here is what happens from mount to destroy:

Initialization (mount)

  1. Create frame registry — instantiates the scheduler with default stage, timing, and profiling state.
  2. Set up adapter context — provides MotionGPUContext and frame registry context so useMotionGPU(), useFrame(), usePointer(), etc. work inside child components.
  3. Start core runtime loopcreateMotionGPURuntimeLoop(...) receives adapter getters and owns material resolution, renderer rebuild policy, retries, and render scheduling.
  4. Resolve material (core) — calls resolveMaterial(material) to produce preprocessed WGSL, uniform layout, texture keys, storage buffer keys, and deterministic signature.
  5. Create renderer (core) — when needed, createRenderer(...) requests adapter/device, compiles WGSL, allocates storage buffers, and builds bind groups + pipelines (render and compute).

Per-frame loop (requestAnimationFrame)

Each frame follows this exact sequence:

  1. Compute timingtime accumulates, delta is clamped to maxDelta.
  2. Update size — reads canvas.getBoundingClientRect() and applies DPR.
  3. Run scheduler — executes all registered useFrame tasks in topologically sorted stage/task order.
  4. Check render gateshouldRender() evaluates render mode + invalidation + advance flags.
  5. Render — if gate passes:
    • Resolves effective uniform/texture values (material defaults + runtime overrides) into reusable render payloads,
    • Flushes pending storage buffer writes to GPU,
    • Uploads changed textures,
    • Writes dirty uniform ranges to GPU buffer,
    • Dispatches all enabled compute passes in declaration order (pre-scene workgroup execution on storage buffers/textures),
    • Executes the base fullscreen pass,
    • Executes post-process render passes through the slot graph,
    • Presents the final output to the canvas.
  6. End frame — clears one-frame invalidation and advance flags.

Teardown (destroy)

  1. Cancel the requestAnimationFrame loop.
  2. Destroy the renderer (releases all GPU resources).
  3. Clear the scheduler registry.

Rebuild and retry policy

Not every change triggers a full renderer rebuild. This table clarifies what does and what does not:

Change Triggers rebuild? What happens instead
Material signature change (shader, uniform layout, texture bindings, storage buffers) Yes Full renderer recreation
Color pipeline change (outputEncoding, toneMapping, dynamicRange, canvasColorSpace, workingFormat) Yes Full renderer recreation
Runtime uniform value change No Dirty-range buffer write only
Runtime texture source change No Texture re-upload only
Runtime texture fragmentVisible change Yes Full renderer recreation
Storage buffer writeStorageBuffer No Pending write flushed next frame
Storage buffer readStorageBuffer No Async staging buffer copy
Canvas resize No Render target resize, re-render
Clear color change No Applied next frame

When renderer creation fails, FragCanvas retries with exponential backoff (250ms500ms1000ms → … → 8000ms cap). The backoff resets when the pipeline signature changes.

Data flow: uniforms and textures

Data flows through three layers, from compile-time defaults to per-frame overrides:

Stage Uniforms Textures
Material definition Static defaults in defineMaterial({ uniforms }) Static TextureDefinition map in defineMaterial({ textures }). Storage buffers declared in defineMaterial({ storageBuffers }).
Frame runtime state.setUniform(name, value) in useFrame callbacks state.setTexture(name, value) in useFrame callbacks. state.writeStorageBuffer(name, data) / state.readStorageBuffer(name) for storage buffers.
Render submit Effective values are resolved from defaults + runtime overrides into a reusable frame payload map (runtime wins on conflicts). Material definitions + runtime source overrides resolved into reusable texture payloads (runtime wins on conflicts).

Setting an unknown uniform or texture name throws immediately — there is no silent fallback.

Scheduling architecture

The scheduler is a DAG-based execution engine:

Concept Description
Task A useFrame callback with a key, stage assignment, invalidation policy, and dependency edges.
Stage An ordered group of tasks. Stages can have their own before / after dependencies and optional wrapper callbacks.
Dependencies before / after on both tasks and stages. Resolved via topological sort — cycles and missing references throw.
Render modes always (continuous), on-demand (invalidation-driven), manual (explicit advance() only).

The scheduler exposes its resolved execution order via getSchedule() for debugging. The advanced entrypoint helper captureSchedulerDebugSnapshot(...) bundles schedule, last-run timings, and profiling snapshot into one payload for debug tooling.

Render graph architecture

Post-processing uses a slot graph with built-in ping-pong slots plus optional named render-target slots:

Slot Purpose
source Current scene/result surface
target Ping-pong companion surface (allocated when needed)
canvas Final output slot. Usually the visible canvas; with a final color pipeline it is a logical internal target presented to the physical canvas afterwards.
<targetName> Named off-screen surface resolved from renderTargets[targetName]

Without post-process render passes and without a final color pipeline, the base shader renders directly to canvas. Compute-only pipelines keep that direct path: compute dispatches happen before the scene, then the scene renders straight to canvas with no source/target ping-pong allocation and no final presentation pass.

When FragCanvas receives color.toneMapping, color.dynamicRange, or color.workingFormat, the renderer separates the internal working format from the canvas format. auto working format selects rgba16float for HDR/tone-mapped pipelines, render passes operate before presentation, and the private final presentation pass applies Khronos PBR Neutral, SDR encoding, or HDR canvas output as the last step.

The presentation pass is also where Motion GPU bridges coordinate systems: material shaders keep the documented Y-up uv convention, while internal render-target textures are sampled in framebuffer coordinates before presentation.

When render passes are added, planRenderGraph(...) validates their slot sequence, resolves clear/preserve flags, and produces an immutable execution plan. The plan also records compute steps for diagnostics and pre-scene dispatch, but compute steps do not make render slots available.

Validation includes:

  • needsSwap: true is only valid for source -> target.
  • canvas is output-only.
  • Named slot reads/writes must reference declared renderTargets.
  • Inputs must be written before first read (target and named targets are tracked per frame).

After all post-scene render passes execute, the renderer presents the resolved final surface. In the default path this is a simple blit when final output is offscreen. In HDR/tone-mapped paths it is a color presentation pass into the physical swapchain.

Compute passes in the render graph

Compute passes (ComputePass, PingPongComputePass) coexist in the same pass array as render passes. They have kind: 'compute' and:

  • Do not participate in slot routing (source/target/canvas).
  • Execute before the base scene render, preserving declaration order relative to other compute passes.
  • Do not execute between post-process render passes, even if interleaved with render passes in the array.
  • Execute compute pipelines with configurable workgroup dispatch.
  • Share the same command encoder and submit queue as render passes.
  • PingPongComputePass runs multiple iterations per frame, alternating read/write bindings.
  • Reuse cached compute storage bind-group layouts/bind groups for stable resource topology.
  • Reuse ping-pong A→B / B→A bind groups across iterations.

Storage buffers are allocated with STORAGE | COPY_DST | COPY_SRC usage flags and cleaned up on renderer destroy.

Diagnostics model

All initialization and render failures are normalized into a stable MotionGPUErrorReport shape:

{
  code: MotionGPUErrorCode; // Stable category for telemetry/alerting
  severity: 'error' | 'fatal';
  recoverable: boolean;
  title: string;     // Short category: "WGSL compilation failed", "WebGPU unavailable", etc.
  message: string;   // Primary human-readable error message
  hint: string;      // Suggested fix or next step
  details: string[]; // Additional compiler messages or multi-line info
  stack: string[];   // Stack trace lines
  rawMessage: string; // Original unmodified error message
  phase: 'initialization' | 'render';
  source: {          // Present for shader compile errors
    component: string;
    location: string;
    line: number;
    column?: number;
    snippet: Array<{ number: number; code: string; highlight: boolean }>;
  } | null;
  context: {
    materialSignature?: string;
    passGraph?: {
      passCount: number;
      enabledPassCount: number;
      inputs: string[];
      outputs: string[];
    };
    activeRenderTargets: string[];
  } | null;
}
{
  code: MotionGPUErrorCode; // Stable category for telemetry/alerting
  severity: 'error' | 'fatal';
  recoverable: boolean;
  title: string;     // Short category: "WGSL compilation failed", "WebGPU unavailable", etc.
  message: string;   // Primary human-readable error message
  hint: string;      // Suggested fix or next step
  details: string[]; // Additional compiler messages or multi-line info
  stack: string[];   // Stack trace lines
  rawMessage: string; // Original unmodified error message
  phase: 'initialization' | 'render';
  source: {          // Present for shader compile errors
    component: string;
    location: string;
    line: number;
    column?: number;
    snippet: Array<{ number: number; code: string; highlight: boolean }>;
  } | null;
  context: {
    materialSignature?: string;
    passGraph?: {
      passCount: number;
      enabledPassCount: number;
      inputs: string[];
      outputs: string[];
    };
    activeRenderTargets: string[];
  } | null;
}

The default overlay displays this information automatically. You can:

  • disable all error UI with showErrorOverlay={false},
  • keep UI off-canvas and handle reports only via onError,
  • provide errorRenderer to replace the default MotionGPUErrorOverlay while preserving the same report payload,
  • enable bounded history callbacks with errorHistoryLimit + onErrorHistory.