RUNE-303: RuneNode Proposed
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-frameresolved[]storage - local/layout/transformed/projected bounds and validity flags
own_latest_settle_timeand aggregatelatest_settle_time- immutable
child_slots[]andresource_refs[] opacity,blend_mode,cache_hint,group_opacity,layer_composite_3d, andlayer_motion_blur- stable
node_id,hit_test_id, andexpr_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:
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_CHILDorder 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.
UI thread: mutable RuneNode + persistent RuneAnimDesc state
Render thread: immutable RenderNodeSnapshot tree + snapshot-local RuneAnimDesc copies + immutable child/resource refsBottom-up publication works as follows:
- dirty descendants commit first
- a dirty node rerecords and finalizes a fresh snapshot
- a clean parent on a dirty path may allocate a shell snapshot that shares command/data blocks, preserves metadata, copies
resource_refs[], replaceschild_slots[], and recomputes structural bounds and aggregate settle time - 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
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
| Approach | Rejected because |
|---|---|
| snapshot-owned mutable child trees | replay would race authoring state or force parent rebuilds |
| per-node incremental publication visible to replay | traversal could observe mixed tree versions |
embedding child snapshot pointers directly in cmd_buffer | parent buffers would become unstable whenever a child republished |
| current-frame world-bounds bookkeeping in snapshots | the audited backend does not store that state |
Dependencies
- RUNE-302 defines the animation descriptors copied into each snapshot.
- RUNE-304 defines command encoding, including
CmdHeaderandCMD_DRAW_CHILD. - RUNE-305 defines the replay/compositing policy that consumes submitted snapshots.
Test Strategy
- verify
rune_node_mark_dirty()causesrune_node_commit()to invokeon_renderfor dirty nodes and finalize new snapshots - verify child-content-only updates publish shell snapshots with replacement
child_slots[]and preserved shared command/data blocks - verify child add/remove/reorder operations force parent rerecording
- verify snapshots retain immutable
child_slots[],resource_refs[], and copied animation data - verify the render thread reads only submitted snapshot data and never dereferences mutable node authoring state