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 compute passes (workgroup execution on storage buffers/textures),
    • Executes the base fullscreen pass,
    • Executes post-process render passes through the render 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
Output color-space change ('srgb''linear') 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 Presentation surface (the actual visible canvas)
<targetName> Named off-screen surface resolved from renderTargets[targetName]

Without any passes, the base shader renders directly to canvas. When passes are added, planRenderGraph(...) validates the pass sequence, resolves clear/preserve flags, and produces an immutable execution plan.

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 passes execute, if final output is not canvas, the renderer blits the resolved final surface (source, target, or named target) to canvas.

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 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.