Skip to content

RUNE-305: Compositing Proposed

Updated2026-08-23

Summary

Rune composes child snapshots at CMD_DRAW_CHILD. Ordinary children use isolated group opacity when needed. Active motion-blur and Layer 3D children are prepared as command-list images, then drawn once in painter order. Snapshot and shell bounds remain conservative for capture and transient layer allocation; the audited Direct2D backend does not use them as damage rectangles because it renders by whole-target clear plus full-root replay.

Motivation

Commit publishes immutable compositing intent but cannot know presentation-time animated values. Replay resolves opacity, motion, and Layer 3D matrices at the requested frame time. Group opacity must isolate overlapping descendants; projective transforms must flatten the complete subtree to preserve overlap, effects, clips, text, SVG, image, and video behavior.

Design

Frame Cycle

For each renderable frame, the current backend:

  1. loads the latest submitted root;
  2. checks settled-root skip conditions;
  3. resolves animated values needed by transforms, geometry, paint, group opacity, motion blur, and Layer 3D;
  4. clears the full target;
  5. replays the complete root in painter order;
  6. stores successful frame bookkeeping and presents.

There is no current-frame damage production, dirty-rect clear, or bounds-based replay culling in the audited Direct2D implementation.

Structural Bounds

Dirty nodes compute local/transformed/projected snapshot bounds from commands and child snapshots. A clean parent on a dirty-child path receives a shell snapshot that shares command/data blocks but recomputes structural bounds and aggregate settle time from replacement children.

Static Layer 3D metadata projects four plane corners and adds one DIP of AA/filter padding. Animated matrix values or statically singular projection publish conservative infinite projected bounds. Replay performs the stricter frame-local renderability test.

Isolated Group Opacity

The child boundary resolves:

text
childOpacity = clamp(child.nodeOpacity * resolve(child.groupOpacity))

For an ordinary child below one, replay pushes one D2D layer using finite child bounds when available, replays the subtree with base opacity one, then pops the layer. Opacity one stays on the direct path.

The root snapshot has no parent child-boundary. Root replay resolves snapshot groupOpacity directly: zero skips the root, a value below one wraps the complete root in one opacity layer, and one replays directly.

Motion Blur

For active ordinary or root blur, Rune captures the snapshot subtree once into a command list. Replay resolves the same snapshot at t and t - exposure, derives a displacement vector, and applies centered CLSID_D2D1DirectionalBlur when that vector is usable. Nested ordinary blurs compound in painter order.

For active 3D blur, replay projects all four plane corners for both samples, maps each displacement through ancestor affine state, and uses the greatest output-space length as the single blur vector. The vector is capped to 64 DIPs in output space and to Direct2D's effect-space limits.

Layer 3D Capture

An active 3D child is prepared at its CMD_DRAW_CHILD boundary:

  1. resolve plane bounds and the final row-vector 4x4 matrix
  2. validate projected corners, W, winding, and backface policy
  3. capture the subtree once into a command list in local coordinates under the plane clip
  4. apply CLSID_D2D13DTransform
  5. optionally apply post-projection directional blur
  6. draw the final image once at the boundary

Multiple active siblings retain painter order. There is no depth buffer or Z sorting.

Fallbacks and Restrictions

  • a failed current 3D projection sample skips that child
  • a failed historical blur sample draws the current projection unblurred
  • zero or negligible blur reuses the ordinary direct path
  • an active 3D layer cannot contain another active 3D descendant or an ordinary descendant with active motion blur; native replay skips invalid outer planes defensively and Story rejects the topology earlier

API Surface

c
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);

group_opacity, motion-blur values, and all nine Layer 3D RuneValues participate in node-local ownership validation, liveness, settle aggregation, snapshot copying, and shell publication.

Examples

Corrected Layer Opacity

A layer contains two overlapping red rectangles and has opacity 0.5. Replay composes the two rectangles first, then applies 50% opacity once. The overlap has the same layer alpha as the non-overlap region.

Projected Card

A card layer is captured once into a command list, projected with a Y rotation and perspective depth, then drawn between its siblings at the original child command position. Text, image, SVG, effects, and local clips remain part of the flattened source.

Performance

  • opacity one avoids PushLayer
  • finite bounds constrain transient opacity layers and effect allocation even though they do not drive partial redraw
  • active planes are captured at their draw boundary, preserving current ancestor state without switching the main target
  • no cache invalidation model is required in V1 because command lists/effects are frame-local

Alternatives Considered

ApproachRejected because
descendant CmdSetOpacity for a Story layeroverlapping children blend opacity more than once
per-primitive 4x4 transformsbreaks subtree overlap, effects, local clips, and group opacity
current backend damage trackingnot part of the audited implementation baseline
cross-frame layer cacherequires subtree generations and dynamic image/effect invalidation

Dependencies

  • RUNE-303 defines immutable snapshots, metadata ABI, liveness, settle time, and shell recomputation.
  • RUNE-304 defines CMD_DRAW_CHILD and replayable child command streams.

Test Strategy

  1. verify shell snapshots recompute bounds and settle time after child-only updates
  2. verify ordinary and projected group opacity applies once to overlapping content
  3. verify nested groups and legacy node opacity multiply once per child boundary
  4. verify ordinary/root motion blur and projected 3D blur honor the documented preparation and fallback rules
  5. verify active siblings preserve painter order and invalid active-root/nested-plane topologies are rejected or skipped
  6. verify the current backend clears and replays the full target instead of producing dirty-rect compositing

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