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" />
<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" />
<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()
    /
    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 visualisation, infrequent updates.

manual

Renders only after an explicit

advance()
call.

<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

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} />
<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 disabled
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 disabled

Profiling (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)
clears timing state, and
applySchedulerPreset(...)
keeps diagnostics/profiling toggles aligned.