Skip to content

RUNE-308: Resource Model Proposed

Updated2026-08-23

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 RuneNode state
  • 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_time and latest_settle_time
  • child_slots[] and resource_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:

HandlePurposeBacking dataRealization
ImageRaster image resourceCPU pixel buffer or exported frame snapshot datalazy bitmap/texture objects
FontTypeface selectionfont identity and DirectWrite metadatalazy font-face use
NineSliceNine-slice source geometryretained image, source rect, insets, interpolationsamples backing image during replay
ShapedTextShaped glyph dataimmutable shaping resultnative glyph storage consumed at replay
ShaderProgramDevice-independent shader metadata and runtime bytecodecompiled program + diagnostics + dependency pathsslot/effect users realize backend state later

ResourceSlot Resources

ResourceSlot is the single public slot handle. Replay chooses the backend subtype stored in slot->resource:

  • D2DBrushResource for solid, gradient, image, and shader/effect brushes
  • D2DPathResource for ID2D1PathGeometry

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

KindExamplesLifetime ownerDestruction path
Persistent node staterecorder, live edges, working RuneAnimDesc[]RuneNode on UI threadfreed with node
Global resourceImage, Font, NineSlice, ShapedText, ShaderProgramcaller + snapshot resource_refs[]release / deferred cleanup as applicable
ResourceSlot resourceResourceSlot with brush/path realizationcaller + snapshot resource_refs[]; render thread owns realization fieldsdeferred cleanup when refcount reaches zero
Snapshot-ownedcopied anims, typed data, child_slots[], resource_refs[], structural metadataRenderNodeSnapshotreleased 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

ApproachRejected because
store every asset inline in snapshotsduplicates large shared data and defeats retained realizations
infer referenced handles by scanning command/data buffers at retirementcouples lifetime to replay formats and adds overhead compared with resource_refs[]
per-resource redraw invalidationthe 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 ShapedText as a global resource.

Test Strategy

  1. verify dirty-node commit copies only snapshot-owned publication state and keeps mutable authoring state on RuneNode
  2. verify resource_refs[] addrefs every referenced RuneObject* and releases those references when the snapshot is destroyed
  3. verify ResourceSlot identity remains stable while compatible brush/path realizations are reused across snapshots
  4. verify ShaderProgram, Image, Font, NineSlice, and ShapedText behave as independent global resources
  5. verify redraw is driven by submitted snapshot changes and settle state, not by a separate per-resource dirty channel

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