Rendering

Render Modes

Controlling when the canvas re-renders always, on-demand, or manual.


This page covers the three render modes (always, on-demand, manual), the stage API for organizing task execution, dependency ordering, and the profiling/diagnostics system.

For the useFrame API, see Frame Scheduler.

Render modes

renderMode determines when the canvas re-renders. It is set as a FragCanvas prop and can be changed at runtime via useMotionGPU().renderMode.set(...).

always (default)

Renders every frame via requestAnimationFrame, as long as autoRender is true.

<FragCanvas {material} renderMode="always" />

Best for: continuous animations, simulations, video textures.

on-demand

Renders only when something triggers an invalidation or advance() is called.

<FragCanvas {material} renderMode="on-demand" />
<script lang="ts"> import { useMotionGPU } from '@motion-core/motion-gpu/svelte'; const gpu = useMotionGPU(); function handlePointerMove() { gpu.invalidate(); } </script>

Invalidation sources:

  • gpu.invalidate() / state.invalidate() from user code
  • useFrame tasks with autoInvalidate: true (default)
  • useFrame tasks with invalidation: { mode: 'on-change', token: ... } when token changes
  • Switching to on-demand mode auto-injects one initial invalidation

Best for: interactive UIs, data visualization, infrequent updates.

manual

Renders only after an explicit advance() call.

<script lang="ts"> import { useMotionGPU } from '@motion-core/motion-gpu/svelte'; const gpu = useMotionGPU(); function captureFrame() { gpu.advance(); } </script> <button onclick={captureFrame}>Render one frame</button>

Best for: frame-by-frame capture, testing, server-side rendering scenarios.

Runtime mode switching

const gpu = useMotionGPU();

// Switch to on-demand
gpu.renderMode.set('on-demand');

// Switch to manual
gpu.renderMode.set('manual');

// Back to always
gpu.renderMode.set('always');
const gpu = useMotionGPU();

// Switch to on-demand
gpu.renderMode.set('on-demand');

// Switch to manual
gpu.renderMode.set('manual');

// Back to always
gpu.renderMode.set('always');

autoRender gate

autoRender is a boolean that acts as a hard override. When false, no rendering occurs regardless of render mode:

gpu.autoRender.set(false); // Stops all rendering
gpu.autoRender.set(true);  // Resumes
gpu.autoRender.set(false); // Stops all rendering
gpu.autoRender.set(true);  // Resumes

Stages API

Stages group tasks into execution phases. By default, all tasks run in a single main stage. You can create custom stages for explicit ordering:

Creating stages

const gpu = useMotionGPU();

const physicsStage = gpu.scheduler.createStage('physics');
const postStage = gpu.scheduler.createStage('post-process', {
  after: 'physics'
});
const gpu = useMotionGPU();

const physicsStage = gpu.scheduler.createStage('physics');
const postStage = gpu.scheduler.createStage('post-process', {
  after: 'physics'
});

Assigning tasks to stages

useFrame('simulate', (state) => {
  // Physics simulation
}, { stage: physicsStage });

useFrame('render-update', (state) => {
  // Update uniforms from simulation results
}, { stage: physicsStage });

useFrame('post-effects', (state) => {
  // Post-process adjustments
}, { stage: postStage });
useFrame('simulate', (state) => {
  // Physics simulation
}, { stage: physicsStage });

useFrame('render-update', (state) => {
  // Update uniforms from simulation results
}, { stage: physicsStage });

useFrame('post-effects', (state) => {
  // Post-process adjustments
}, { stage: postStage });

The default stage uses an internal symbol key, so referencing it by string (for example 'main') is unsupported.

Stage options

Option Type Description
before stage key or list This stage runs before the specified stage(s)
after stage key or list This stage runs after the specified stage(s)
callback (state, runTasks) => void Wrapper function for gating or batching task execution

Stage callback

The callback option lets you wrap task execution:

gpu.scheduler.createStage('guarded', {
  callback: (state, runTasks) => {
    if (shouldProcessThisFrame) {
      runTasks(); // Execute all tasks in this stage
    }
    // If runTasks() is not called, all tasks in this stage are skipped
  }
});
gpu.scheduler.createStage('guarded', {
  callback: (state, runTasks) => {
    if (shouldProcessThisFrame) {
      runTasks(); // Execute all tasks in this stage
    }
    // If runTasks() is not called, all tasks in this stage are skipped
  }
});

Dependency ordering

Both tasks and stages support before / after dependency edges. Dependencies are resolved via topological sort:

  • Tasks within a stage are sorted by their inter-task dependencies.
  • Stages are sorted by their inter-stage dependencies.
  • Circular dependencies throw an error.
  • Missing dependency references throw an error.

Schedule inspection

You can inspect the resolved execution order at any time:

const schedule = gpu.scheduler.getSchedule();
// {
//   stages: [
//     { key: 'physics', tasks: ['simulate'] },
//     { key: Symbol(motiongpu-main-stage), tasks: ['render-update'] },
//     { key: 'post-process', tasks: ['post-effects'] }
//   ]
// }
const schedule = gpu.scheduler.getSchedule();
// {
//   stages: [
//     { key: 'physics', tasks: ['simulate'] },
//     { key: Symbol(motiongpu-main-stage), tasks: ['render-update'] },
//     { key: 'post-process', tasks: ['post-effects'] }
//   ]
// }

This is useful for debugging task ordering and verifying dependency resolution.

Delta clamping

maxDelta clamps state.delta every frame to prevent large time jumps (e.g., when a tab is background-inseted):

  • Default: 0.1 seconds
  • Must be: finite and > 0
  • Configurable via: FragCanvas prop or gpu.maxDelta.set(value)
<FragCanvas {material} maxDelta={0.05} />

Diagnostics and profiling

Diagnostics (single-frame timing)

const scheduler = gpu.scheduler;

// Enable timing collection
scheduler.setDiagnosticsEnabled(true);

// Read last frame's timings
const timings = scheduler.getLastRunTimings();
// { stages: { ... }, tasks: { ... } }
const scheduler = gpu.scheduler;

// Enable timing collection
scheduler.setDiagnosticsEnabled(true);

// Read last frame's timings
const timings = scheduler.getLastRunTimings();
// { stages: { ... }, tasks: { ... } }

Profiling (rolling window)

// Enable rolling profiling (default window: 60 frames)
scheduler.setProfilingEnabled(true);
scheduler.setProfilingWindow(120); // 2-second window at 60fps

// Read aggregate stats
const snapshot = scheduler.getProfilingSnapshot();
// { stages: { ... }, tasks: { ... } }

// Reset history
scheduler.resetProfiling();
// Enable rolling profiling (default window: 60 frames)
scheduler.setProfilingEnabled(true);
scheduler.setProfilingWindow(120); // 2-second window at 60fps

// Read aggregate stats
const snapshot = scheduler.getProfilingSnapshot();
// { stages: { ... }, tasks: { ... } }

// Reset history
scheduler.resetProfiling();

Advanced preset

For convenience, applySchedulerPreset applies a named configuration:

import { applySchedulerPreset } from '@motion-core/motion-gpu/advanced';

applySchedulerPreset(gpu.scheduler, 'diagnostics');
applySchedulerPreset(gpu.scheduler, 'profiling');
applySchedulerPreset(gpu.scheduler, 'off');
import { applySchedulerPreset } from '@motion-core/motion-gpu/advanced';

applySchedulerPreset(gpu.scheduler, 'diagnostics');
applySchedulerPreset(gpu.scheduler, 'profiling');
applySchedulerPreset(gpu.scheduler, 'off');

Debug snapshot

captureSchedulerDebugSnapshot returns a full serializable snapshot of the scheduler state:

import { captureSchedulerDebugSnapshot } from '@motion-core/motion-gpu/advanced';

const debugSnapshot = captureSchedulerDebugSnapshot(scheduler);
// {
//   schedule: { stages: ... },
//   timings: { ... },
//   profiling: { ... }
// }
import { captureSchedulerDebugSnapshot } from '@motion-core/motion-gpu/advanced';

const debugSnapshot = captureSchedulerDebugSnapshot(scheduler);
// {
//   schedule: { stages: ... },
//   timings: { ... },
//   profiling: { ... }
// }