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:
- the frame number;
- the values of the channels it reads;
- 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: DateConstructorEnables basic storage and retrieval of dates and times.Date.DateConstructor.now(): numberReturns 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: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): numberReturns 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): voidPublish `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.