Snapshots, commit, and replay
Rune Engine keeps mutable authoring state and immutable render state alive at the same time. That is the core reason commit is fast, replay is race-free, and capture/export can reuse old frames safely.
Mental model
text
mutable RuneNode tree
-- commit dirty branches -->
immutable Snapshot tree
-- submit -->
context committed_root
-- render -->
resolved values + full replayResponsibilities and boundaries
- RuneNode owns the recorder, live child edges, and persistent animation working set.
- Snapshot owns immutable command/data buffers, copied animation descriptors, child slots, resource references, and commit-time metadata such as bounds and latest settle time.
- Context swaps one committed root for another and keeps the last rendered root alive until the renderer no longer needs it.
- Replay reads only snapshot-local state. It never follows mutable node pointers.
Stable current behavior
node.Commit()walks dirty branches bottom-up. Clean subtrees return their existing active snapshot in constant time.- A dirty child can publish a fresh snapshot without forcing a clean parent to re-record every command. Commit builds a shell snapshot that shares the parent's immutable command/data blocks and replaces only
child_slots[], bounds, and aggregate settle metadata. draw_node(child)records a live child edge;draw_snapshot(snapshot)records a frozen edge. Both become the sameCMD_DRAW_CHILDboundary in the published snapshot.context.Submit(snapshot)addrefs the new root, releases the old one, and wakes the render thread.- Before replay, the renderer resolves ordinary animations first and then runs the ordered derived-value program for expression and path-sampled outputs.
RequestRender()exists for out-of-band content changes, such as a dynamic image receiving a new decoded frame while the submitted snapshot pointer stays the same.
Current limitations
- Snapshot bounds are conservative capture and allocation metadata. They are not current-frame damage rectangles or partial-replay inputs.
- The current backend replays the whole submitted root when it renders. There is no documented incremental redraw path today.
- Imperative
BeginRecord()→FinishSnapshot()produces a reusable frozen snapshot, not a live node that can keep participating in later dirty-tree updates.
How the pieces connect
Commit makes rendering deterministic by freezing exactly one tree version before submission. Replay then consumes that one immutable tree at one frame time. Old snapshots can remain alive for capture, child reuse, or the previous rendered root while newer snapshots are already being authored and submitted.
That model is what lets Story use the same playback core for preview, still capture, and export: publish immutable state once, then render or seek it as many times as needed.