Rendering

Frame Scheduler

Managing per-frame logic, task ordering, and invalidation with the useFrame API.


The frame scheduler manages all per-frame logic in MotionGPU. Every

useFrame
callback is registered as a task with ordering constraints and an invalidation policy. This page covers the
useFrame
API in depth.

For render modes, stages, and profiling, see Render Modes and Scheduling.

Basic usage

<script lang="ts">
  import { useFrame } from '@motion-core/motion-gpu';

  useFrame((state) => {
    state.setUniform('uTime', state.time);
  });
</script>
<script lang="ts">
  import { useFrame } from '@motion-core/motion-gpu';

  useFrame((state) => {
    state.setUniform('uTime', state.time);
  });
</script>

useFrame
must be called inside a
<FragCanvas>
component tree. Calling it outside throws.

Overloads

Signature Description
useFrame(callback, options?)
Auto-generated task key
useFrame(key, callback, options?)
Explicit stable key for ordering and debugging
// Auto key
useFrame((state) => { ... });

// Explicit key
useFrame('camera-update', (state) => { ... });

// With options
useFrame('particles', (state) => { ... }, {
  stage: particleStage,
  autoInvalidate: false
});
// Auto key
useFrame((state) => { ... });

// Explicit key
useFrame('camera-update', (state) => { ... });

// With options
useFrame('particles', (state) => { ... }, {
  stage: particleStage,
  autoInvalidate: false
});

Return value

useFrame
returns a handle for controlling the task:

Field Type Description
task
{ key: FrameKey; stage: FrameKey }
Task and stage identifiers
start()
() => void
Enables the task (resumes execution)
stop()
() => void
Disables the task (paused, not removed)
started
Readable<boolean>
Reactive store for effective running state
const handle = useFrame('animation', (state) => {
  state.setUniform('uTime', state.time);
});

// Pause animation
handle.stop();

// Resume
handle.start();
const handle = useFrame('animation', (state) => {
  state.setUniform('uTime', state.time);
});

// Pause animation
handle.stop();

// Resume
handle.start();

FrameState callback API

The

state
parameter passed to your callback provides everything you need for per-frame logic:

Field Type Description
time
number
requestAnimationFrame
timestamp in seconds (monotonic clock)
delta
number
Time since last frame in seconds (clamped by
maxDelta
)
canvas
HTMLCanvasElement
Active canvas element
renderMode
RenderMode
Current render mode
autoRender
boolean
Current auto-render state
setUniform(name, value)
function Set a runtime uniform override
setTexture(name, value)
function Set a runtime texture source
invalidate(token?)
function Trigger invalidation for on-demand mode
advance()
function Request one frame in manual mode

Example: complete runtime component

<script lang="ts">
  import { useFrame, useMotionGPU } from '@motion-core/motion-gpu';

  const gpu = useMotionGPU();
  let mouseX = 0.5;
  let mouseY = 0.5;

  $effect(() => {
    const canvas = gpu.canvas;
    if (!canvas) return;

    const onMove = (e: PointerEvent) => {
      const rect = canvas.getBoundingClientRect();
      mouseX = (e.clientX - rect.left) / rect.width;
      mouseY = 1.0 - (e.clientY - rect.top) / rect.height;
    };

    canvas.addEventListener('pointermove', onMove);
    return () => canvas.removeEventListener('pointermove', onMove);
  });

  useFrame((state) => {
    state.setUniform('uTime', state.time);
    state.setUniform('uMouse', [mouseX, mouseY]);
  });
</script>
<script lang="ts">
  import { useFrame, useMotionGPU } from '@motion-core/motion-gpu';

  const gpu = useMotionGPU();
  let mouseX = 0.5;
  let mouseY = 0.5;

  $effect(() => {
    const canvas = gpu.canvas;
    if (!canvas) return;

    const onMove = (e: PointerEvent) => {
      const rect = canvas.getBoundingClientRect();
      mouseX = (e.clientX - rect.left) / rect.width;
      mouseY = 1.0 - (e.clientY - rect.top) / rect.height;
    };

    canvas.addEventListener('pointermove', onMove);
    return () => canvas.removeEventListener('pointermove', onMove);
  });

  useFrame((state) => {
    state.setUniform('uTime', state.time);
    state.setUniform('uMouse', [mouseX, mouseY]);
  });
</script>

UseFrameOptions

Option Type Default Description
autoStart
boolean
true
Whether the task starts enabled
autoInvalidate
boolean
true
Auto-invalidate every frame (for on-demand mode)
invalidation
FrameTaskInvalidation
Implicit Explicit invalidation policy override
stage
FrameKey
FrameStage
Main stage Which stage to assign the task to
before
task ref or list
undefined
This task must run before the specified task(s)
after
task ref or list
undefined
This task must run after the specified task(s)
running
() => boolean
undefined
Dynamic gate — task is skipped when this returns
false

Invalidation policies

The invalidation policy controls whether a task’s execution triggers a re-render in

on-demand
mode:

Simple policies

Value Behaviour
'never'
Task never triggers invalidation
'always'
Task always triggers invalidation

Object policies

// Always invalidate with a token
{ mode: 'always', token: 'my-task' }

// Never invalidate
{ mode: 'never' }

// On-change: invalidate only when token value changes
{ mode: 'on-change', token: () => someReactiveValue }
// Always invalidate with a token
{ mode: 'always', token: 'my-task' }

// Never invalidate
{ mode: 'never' }

// On-change: invalidate only when token value changes
{ mode: 'on-change', token: () => someReactiveValue }

on-change
semantics

With

on-change
, the scheduler compares the current resolved token against the previous frame’s token. Invalidation fires only when they differ. This is ideal for state-driven updates:

useFrame('background-inset', (state) => {
  state.setUniform('uColor', currentColor);
}, {
  autoInvalidate: false,
  invalidation: {
    mode: 'on-change',
    token: () => currentColor.join(',')
  }
});
useFrame('background-inset', (state) => {
  state.setUniform('uColor', currentColor);
}, {
  autoInvalidate: false,
  invalidation: {
    mode: 'on-change',
    token: () => currentColor.join(',')
  }
});

Task ordering with dependencies

Use

before
and
after
to control execution order within a stage:

const physics = useFrame('physics', (state) => {
  // Update physics simulation
});

const render = useFrame('render-update', (state) => {
  // Read physics results
}, {
  after: physics.task
});
const physics = useFrame('physics', (state) => {
  // Update physics simulation
});

const render = useFrame('render-update', (state) => {
  // Read physics results
}, {
  after: physics.task
});

Dependencies are resolved via topological sort.

  • Circular dependencies throw a deterministic error.
  • Missing
    before
    /
    after
    references throw a deterministic error.

Lifecycle

  • useFrame
    automatically unregisters the task when the component is destroyed (via Svelte’s
    onDestroy
    ).
  • The
    stop()
    /
    start()
    methods pause/resume without unregistering — the task remains in the schedule but is skipped.
  • The
    running
    gate is checked each frame — a stopped task or a task with
    running: () => false
    is simply skipped, not removed.