Skip to content

RUNE-303: RuneNode Proposed

Updated2026-08-23

Summary

RuneNode is mutable UI-thread authoring state, while RenderNodeSnapshot is the immutable payload published to the render thread for one node and one commit. In addition to commands, typed data, and copied animations, snapshots carry immutable child_slots[], resource_refs[], node-level compositing metadata, and conservative structural bounds. Clean-parent shell snapshots share command/data blocks while replacing child references and recomputing structural metadata.

Motivation

Rune needs coherent cross-thread rendering without locking readers or rebuilding whole parents every time a child changes. A mutable node graph is convenient for UI-thread editing, but the render thread needs a self-consistent immutable view of commands, animations, bounds, and retained resources.

Design

Core Structures

A snapshot carries:

  • shared CmdBufferBlock + size
  • shared DataBufferBlock + size
  • copied RuneAnimDesc[] plus per-frame resolved[] storage
  • local/layout/transformed/projected bounds and validity flags
  • own_latest_settle_time and aggregate latest_settle_time
  • immutable child_slots[] and resource_refs[]
  • opacity, blend_mode, cache_hint, group_opacity, layer_composite_3d, and layer_motion_blur
  • stable node_id, hit_test_id, and expr_data_offset

RuneNode retains mutable authoring state such as the recorder, persistent animation working set, live child/referrer edges, and the latest published snapshot pointer.

Child Model

Child draws are encoded as CMD_DRAW_CHILD commands in cmd_buffer:

cpp
struct CmdDrawChild {
    CmdHeader header;
    uint16_t child_slot;
};

child_slot indexes the owning snapshot's child_slots[] array.

  • child content changed -> commit may create a new parent shell snapshot that shares command/data blocks and replaces child_slots[]
  • child added, removed, or reordered -> parent must rerecord because CMD_DRAW_CHILD order changed
  • parent snapshots retain addref'd child snapshots through child_slots[]

Commit and Publication

rune_node_commit() is the public handoff from mutable authoring state to the immutable render tree. For each dirty node, it runs the internal recording lifecycle (begin_update() -> on_render() -> end_update()), finalizes bounds and settle metadata, and publishes fresh or shell snapshots bottom-up.

text
UI thread:     mutable RuneNode + persistent RuneAnimDesc state
Render thread: immutable RenderNodeSnapshot tree + snapshot-local RuneAnimDesc copies + immutable child/resource refs

Bottom-up publication works as follows:

  1. dirty descendants commit first
  2. a dirty node rerecords and finalizes a fresh snapshot
  3. a clean parent on a dirty path may allocate a shell snapshot that shares command/data blocks, preserves metadata, copies resource_refs[], replaces child_slots[], and recomputes structural bounds and aggregate settle time
  4. unrelated clean subtrees reuse the previous snapshot pointer unchanged

Publication never exposes mixed tree versions because replay only sees immutable snapshots.

Structural Bounds and Metadata

Snapshot bounds are conservative commit-time metadata, not per-frame damage history. They support capture, projection fallback, and transient layer/effect allocation. Active 3D metadata, group opacity, and motion-blur descriptors participate in liveness, ownership validation, settle aggregation, and shell publication even when no ordinary draw command references them.

Relationship to Traversal

The render thread consumes snapshots submitted through rune_context_submit(). The current D2D backend may skip a settled unchanged submitted root, but any rendered frame clears the full target and replays the full root. CMD_DRAW_CHILD selects child replay through the immutable slot array; traversal never reads mutable node state.

API Surface

c
RuneNode* rune_node_create(RuneEngine* engine,
                           void(*on_render)(RuneNode* node, void* user_data),
                           void* user_data);
RuneSnapshot* rune_node_commit(RuneNode* node);
void rune_node_mark_dirty(RuneNode* node);
void rune_node_begin_record(RuneNode* node);
RuneSnapshot* rune_node_finish_snapshot(RuneNode* node);
void rune_node_cmd_draw_node(RuneNode* node, RuneNode* child_node);
void rune_node_cmd_draw_snapshot(RuneNode* node, RuneSnapshot* snapshot);
void rune_node_set_group_opacity(RuneNode* node, RuneValue opacity);
RuneStatus rune_node_set_layer_composite_3d(
    RuneNode* node,
    const RuneLayerComposite3D* descriptor);
RuneStatus rune_node_set_layer_motion_blur(
    RuneNode* node,
    const RuneLayerMotionBlur* descriptor);

Examples

Dirty Child, Clean Parent

A button icon changes color inside a stable container. The icon node records a new snapshot; commit publishes a new container shell snapshot with shared command/data blocks and an updated child_slots[iconSlot] pointer. The container keeps its existing command buffer because its own draw order did not change.

Child Reorder

A list item moves ahead of a sibling. The parent must rerecord because the CMD_DRAW_CHILD sequence changed, even though the child snapshots themselves may be reusable.

Performance

  • dirty-child commits publish shallow shell snapshots without rebuilding parent command buffers
  • commit cost scales with dirty paths, not whole-tree size
  • readers stay lock-free because traversal sees only immutable snapshots
  • shared command/data blocks keep child-only publication cheap

Alternatives Considered

ApproachRejected because
snapshot-owned mutable child treesreplay would race authoring state or force parent rebuilds
per-node incremental publication visible to replaytraversal could observe mixed tree versions
embedding child snapshot pointers directly in cmd_bufferparent buffers would become unstable whenever a child republished
current-frame world-bounds bookkeeping in snapshotsthe audited backend does not store that state

Dependencies

  • RUNE-302 defines the animation descriptors copied into each snapshot.
  • RUNE-304 defines command encoding, including CmdHeader and CMD_DRAW_CHILD.
  • RUNE-305 defines the replay/compositing policy that consumes submitted snapshots.

Test Strategy

  1. verify rune_node_mark_dirty() causes rune_node_commit() to invoke on_render for dirty nodes and finalize new snapshots
  2. verify child-content-only updates publish shell snapshots with replacement child_slots[] and preserved shared command/data blocks
  3. verify child add/remove/reorder operations force parent rerecording
  4. verify snapshots retain immutable child_slots[], resource_refs[], and copied animation data
  5. verify the render thread reads only submitted snapshot data and never dereferences mutable node authoring state

Rune Project brings Rune Story authoring together with the Rune Engine rendering foundation.