RUNE-308: Resource Model Proposed
Summary
Rune separates render data into persistent RuneNode authoring state, snapshot-owned publication state, and two categories of shareable RuneObject resources: global resources and slot resources. Global resources include Image, Font, ShapedText, NineSlice, and device-independent ShaderProgram. ResourceSlot resources hold retained brush or path realizations. Snapshots publish immutable cmd_buffer + data_buffer payloads plus addref'd child_slots[] and resource_refs[], so render-thread replay never races UI-thread state and never needs to scan command bytes to keep referenced resources alive.
Motivation
The render thread needs one immutable package per node and must never race mutable UI-thread state. At the same time, Rune cannot afford to duplicate heavyweight shared assets or recreate D2D brush/path realizations every time a node records a new snapshot. The model therefore keeps authoring state on RuneNode, publishes immutable snapshot data at commit, and gives every native handle a unified RuneObject lifetime with snapshot-held references and render-thread-aware cleanup.
Design
Classification Rule
Use the following ownership rule:
- immutable authoring data with independent lifetime -> global resource
- render-thread-managed reusable D2D realization -> slot resource
- mutable authoring state needed for UI-thread recording -> persistent
RuneNodestate - recording-local payload or copied animation/metadata state -> snapshot-owned
If data must survive across snapshots as a first-class handle, make it a RuneObject. If it is only meaningful inside one publication pass, keep it on the snapshot or the owning RuneNode.
RuneObject Base
All native handles inherit from RuneObject with an obj_type, runtime*, and atomic ref_count. The ABI exposes one refcount surface for all native handles:
rune_addref(void* handle)rune_release(void* handle)
When ref_count reaches zero, RuneEngine is the special shutdown case. Other runtime-bound objects enter deferred cleanup so the render thread can release backend state safely.
Persistent RuneNode State
UI-thread RuneNode objects own mutable authoring state such as the recorder, persistent RuneAnimDesc[] working set, live child/referrer edges, node-level compositing properties, and the latest active snapshot.
rune_node_animate() allocates or retargets slots on that node-local working set. CreateExpression() and SamplePath() append ordered derived operations to the recorder's expression builder. The render thread must not read this mutable node state directly.
Snapshot-Owned Data
At commit, the node's live recording state is copied into a new immutable snapshot. Snapshot-owned fields include:
- shared command/data buffer block references and sizes
- copied
RuneAnimDesc[]plus replay-time resolved storage - local/layout/transformed/projected bounds and validity flags
own_latest_settle_timeandlatest_settle_timechild_slots[]andresource_refs[]expr_data_offset- node-level metadata copied for replay (
opacity,blend_mode,cache_hint,group_opacity,layer_composite_3d,layer_motion_blur,hit_test_id)
DATA_PATH and DATA_EXPRESSION records remain snapshot-owned because RunePath and expr_data_offset both address that snapshot's immutable typed arena.
Global Resources
These resources are independent RuneObject handles with lifecycle separate from any one node or snapshot:
| Handle | Purpose | Backing data | Realization |
|---|---|---|---|
Image | Raster image resource | CPU pixel buffer or exported frame snapshot data | lazy bitmap/texture objects |
Font | Typeface selection | font identity and DirectWrite metadata | lazy font-face use |
NineSlice | Nine-slice source geometry | retained image, source rect, insets, interpolation | samples backing image during replay |
ShapedText | Shaped glyph data | immutable shaping result | native glyph storage consumed at replay |
ShaderProgram | Device-independent shader metadata and runtime bytecode | compiled program + diagnostics + dependency paths | slot/effect users realize backend state later |
ResourceSlot Resources
ResourceSlot is the single public slot handle. Replay chooses the backend subtype stored in slot->resource:
D2DBrushResourcefor solid, gradient, image, and shader/effect brushesD2DPathResourceforID2D1PathGeometry
ISlotResource::config_hash lets replay reuse compatible realizations across snapshots and frames instead of recreating them from scratch.
Lifetime and Redraw Boundaries
Snapshots retain every referenced child snapshot or RuneObject through child_slots[] and resource_refs[]. Retired snapshots release those references when the render thread no longer needs them.
Resources do not have an independent dirty channel. Redraw is driven by submitted snapshot pointers plus latest_settle_time; in the audited backend that results in settled-root skipping or full-target replay, not resource-local damage tracking.
API Surface
Ownership Model
| Kind | Examples | Lifetime owner | Destruction path |
|---|---|---|---|
| Persistent node state | recorder, live edges, working RuneAnimDesc[] | RuneNode on UI thread | freed with node |
| Global resource | Image, Font, NineSlice, ShapedText, ShaderProgram | caller + snapshot resource_refs[] | release / deferred cleanup as applicable |
| ResourceSlot resource | ResourceSlot with brush/path realization | caller + snapshot resource_refs[]; render thread owns realization fields | deferred cleanup when refcount reaches zero |
| Snapshot-owned | copied anims, typed data, child_slots[], resource_refs[], structural metadata | RenderNodeSnapshot | released when snapshot retires |
Examples
Stable Image Across Snapshots
A caller creates a pixel-backed Image once, records it through brush CMD_DATA that targets a ResourceSlot, and draws it from many snapshots. Each referencing snapshot addrefs both the slot and the image through resource_refs[], so the CPU data and cached realization stay valid until the last snapshot retires.
Child-Only Update
A parent snapshot records CMD_DRAW_CHILD child_slot=3. If the child publishes a new snapshot while the parent's commands stay unchanged, commit publishes a new parent shell with a copied child_slots[] array whose slot 3 points at the new child snapshot. Any slot/global resources remain retained through the shell's copied resource_refs[].
Alternatives Considered
| Approach | Rejected because |
|---|---|
| store every asset inline in snapshots | duplicates large shared data and defeats retained realizations |
| infer referenced handles by scanning command/data buffers at retirement | couples lifetime to replay formats and adds overhead compared with resource_refs[] |
| per-resource redraw invalidation | the audited backend redraw boundary is snapshot submission, not resource mutation |
Dependencies
- RUNE-300 defines the public handle and ownership surface.
- RUNE-303 defines snapshot publication, shell snapshots, and immutable child/resource retention.
- RUNE-306 defines
ShapedTextas a global resource.
Test Strategy
- verify dirty-node commit copies only snapshot-owned publication state and keeps mutable authoring state on
RuneNode - verify
resource_refs[]addrefs every referencedRuneObject*and releases those references when the snapshot is destroyed - verify
ResourceSlotidentity remains stable while compatible brush/path realizations are reused across snapshots - verify
ShaderProgram,Image,Font,NineSlice, andShapedTextbehave as independent global resources - verify redraw is driven by submitted snapshot changes and settle state, not by a separate per-resource dirty channel