This page covers the three render modes (
alwayson-demandmanualFor the
useFrameRender modes
renderModeFragCanvasuseMotionGPU().renderMode.set(...)always (default)
alwaysRenders every frame via
requestAnimationFrameautoRendertrue<FragCanvas {material} renderMode="always" /><FragCanvas {material} renderMode="always" />Best for: continuous animations, simulations, video textures.
on-demand
on-demandRenders only when something triggers an invalidation or
advance()<FragCanvas {material} renderMode="on-demand" /><FragCanvas {material} renderMode="on-demand" /><script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
function handlePointerMove() {
gpu.invalidate();
}
</script><script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
function handlePointerMove() {
gpu.invalidate();
}
</script>Invalidation sources:
- /
gpu.invalidate()from user codestate.invalidate() - tasks with
useFrame(default)autoInvalidate: true - tasks with
useFramewhen token changesinvalidation: { mode: 'on-change', token: ... } - Switching to mode auto-injects one initial invalidation
on-demand
Best for: interactive UIs, data visualisation, infrequent updates.
manual
manualRenders only after an explicit
advance()<script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
function captureFrame() {
gpu.advance();
}
</script>
<button onclick={captureFrame}>Render one frame</button><script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
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
autoRenderautoRenderfalsegpu.autoRender.set(false); // Stops all rendering
gpu.autoRender.set(true); // Resumesgpu.autoRender.set(false); // Stops all rendering
gpu.autoRender.set(true); // ResumesStages 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'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
callbackgpu.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
beforeafter- 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
maxDeltastate.delta- Default: seconds
0.1 - Must be: finite and
> 0 - Configurable via: prop or
FragCanvasgpu.maxDelta.set(value)
<FragCanvas {material} maxDelta={0.05} /><FragCanvas {material} maxDelta={0.05} />Diagnostics and profiling
Diagnostics (single-frame timing)
const scheduler = gpu.scheduler;
// Enable timing collection
scheduler.setDiagnosticsEnabled(true);
// After a frame, read the last run timings
const timings = scheduler.getLastRunTimings();
// { total: 0.42, stages: [...] } or null if disabledconst scheduler = gpu.scheduler;
// Enable timing collection
scheduler.setDiagnosticsEnabled(true);
// After a frame, read the last run timings
const timings = scheduler.getLastRunTimings();
// { total: 0.42, stages: [...] } or null if disabledProfiling (rolling history)
// Enable rolling profiling
scheduler.setProfilingEnabled(true);
scheduler.setProfilingWindow(120); // Track last 120 frames
// Read aggregate stats
const snapshot = scheduler.getProfilingSnapshot();
// {
// window: 120,
// frameCount: 120,
// total: { last, avg, min, max, count },
// stages: { ... }
// }
// Reset history
scheduler.resetProfiling();// Enable rolling profiling
scheduler.setProfilingEnabled(true);
scheduler.setProfilingWindow(120); // Track last 120 frames
// Read aggregate stats
const snapshot = scheduler.getProfilingSnapshot();
// {
// window: 120,
// frameCount: 120,
// total: { last, avg, min, max, count },
// stages: { ... }
// }
// Reset history
scheduler.resetProfiling();Advanced scheduler helpers
For common tuning profiles and one-call debug capture, use the advanced entrypoint:
import { applySchedulerPreset, captureSchedulerDebugSnapshot } from '@motion-core/motion-gpu/advanced';
const scheduler = gpu.scheduler;
// Presets: 'performance' | 'balanced' | 'debug'
applySchedulerPreset(scheduler, 'balanced');
applySchedulerPreset(scheduler, 'debug', { profilingWindow: 300 });
const debugSnapshot = captureSchedulerDebugSnapshot(scheduler);
// {
// diagnosticsEnabled,
// profilingEnabled,
// profilingWindow,
// schedule,
// lastRunTimings,
// profilingSnapshot
// }import { applySchedulerPreset, captureSchedulerDebugSnapshot } from '@motion-core/motion-gpu/advanced';
const scheduler = gpu.scheduler;
// Presets: 'performance' | 'balanced' | 'debug'
applySchedulerPreset(scheduler, 'balanced');
applySchedulerPreset(scheduler, 'debug', { profilingWindow: 300 });
const debugSnapshot = captureSchedulerDebugSnapshot(scheduler);
// {
// diagnosticsEnabled,
// profilingEnabled,
// profilingWindow,
// schedule,
// lastRunTimings,
// profilingSnapshot
// }Both diagnostics and profiling share the same underlying timing measurement in the current runtime implementation.
setDiagnosticsEnabled(false)applySchedulerPreset(...)