---
title: Determinism
description: What a frame may depend on, what quietly breaks that, and how values from the host get in safely.
---

`at(frame)` depends on three things and nothing else:

1. the frame number;
2. the values of the [channels](/docs/reactivity/channel) it reads;
3. its option, `viewport`.

Seeking, export, caching and Remotion's parallel rendering all rely on this. Frame 200
resolved first, or in another process, or after frame 900, is the same frame 200.

## What breaks it

Each of these compiles and looks right when the scene plays from the start:

```ts twoslash
import { derived, LayoutActor, signal } from "@motionactor/core";

let seen = 0;

class Broken extends LayoutActor {
  startedAt = signal(0);

  // A different answer on every resolution.
  elapsed = derived(() => Date.now() - this.startedAt());

  // The same frame drawn differently each time.
  jitter = derived(() => Math.random() * 4);

  // The answer depends on how many frames came before.
  index = derived(() => seen++);
}
```

The bug shows up later, away from its cause: a wrong frame after a seek, a cached frame that
disagrees with a fresh one, or a video whose frames come from parallel renders that each
saw something different. Nothing throws.

| Instead of | Use |
| --- | --- |
| `Date.now()` | The frame, through `runtime.frame()`, or a [`ClockActor`](/docs/actors/clock-actor) |
| `Math.random()` | A seeded generator whose seed is a signal |
| A measured DOM box | A [channel](/docs/reactivity/channel) with `record: true` |
| A fetched value | A [scene input](/docs/runtime/scene-orchestration#inputs-and-layers): a new value is a new `build` |

## Values from the host

Some values exist only where the scene is drawn: the size of a box after layout, the width a
font set a line at. They enter through channels, and a channel's value is one of the three
inputs above.

A plain channel holds its latest value, which is wrong for anything that changes over time.
Measure a box while playing forward to frame 200, seek back to frame 10, and a latest-wins
read gives frame 200's box. A channel declared with `record: true` keeps every value with
the frame it was published for. A read at frame N returns the value from the greatest
recorded frame at or before N, so a seek back replays what was measured then.
`LayoutActor` records its `measuredBox` this way.

A renderer that measured a host value for one frame publishes it with that frame, through the
resolved actor's handle:

```ts twoslash
import { channel, createScene, LayoutActor } from "@motionactor/core";

class Card extends LayoutActor {
  available = channel({ default: 0, record: true });
}

const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  yield* ctx.wait(1);
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

declare const frame: number;
declare const width: number;
// ---cut---
compiled.at(frame).scene.card?.channels.available.write(width, frame);
const state = compiled.at(frame);
```

The same frame with the same publications gives the same state however many times React
renders it.

## Testing it

A scene that holds to this can be checked with no renderer at all. Resolve a frame in a test
and assert on its state; see [Testing](/docs/guides/testing).
