Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Determinism

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 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:

import { function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor, 
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
} from "@motionactor/core";
let let seen: numberseen = 0; class class BrokenBroken extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Broken.startedAt: ActorFieldSignal<number>startedAt = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
(0);
// A different answer on every resolution. Broken.elapsed: DerivedDefinition<number, unknown>elapsed = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => var Date: DateConstructor
Enables basic storage and retrieval of dates and times.
Date
.DateConstructor.now(): number
Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).
now
() - this.Broken.startedAt: () => numberstartedAt());
// The same frame drawn differently each time. Broken.jitter: DerivedDefinition<number, unknown>jitter = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.random(): number
Returns a pseudorandom number between 0 and 1.
random
() * 4);
// The answer depends on how many frames came before. Broken.index: DerivedDefinition<number, unknown>index = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => let seen: numberseen++); }

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
Math.random() A seeded generator whose seed is a signal
A measured DOM box A channel with record: true
A fetched value A scene input: 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:

const compiled: CompiledScene<{
    card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ card: Card; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    card: Card;
}>
at
(const frame: numberframe).
FrameState<{ card: Card; }>.scene: {
    card: ResolvedActorOfInstance<Card> | undefined;
}
scene
.card: ResolvedActorOfInstance<Card> | undefinedcard?.channels: ActorClassChannels<Omit<Card, keyof Actor<any>>>channels.available: ResolvedChannelHandle<number>available.ResolvedChannelHandle<number>.write(value: number, frame?: number): void
Publish `value`. `frame` is the frame the producer's scene host committed; a `record: true` channel files the value under it, so a later seek reads the value observed at that frame. Without one the write is live only.
write
(const width: numberwidth, const frame: numberframe);
const
const state: FrameState<{
    card: Card;
}>
state
=
const compiled: CompiledScene<{
    card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ card: Card; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    card: Card;
}>
at
(const frame: numberframe);

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.

Was this page helpful?