RUNE-305: Compositing Proposed
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:
- loads the latest submitted root;
- checks settled-root skip conditions;
- resolves animated values needed by transforms, geometry, paint, group opacity, motion blur, and Layer 3D;
- clears the full target;
- replays the complete root in painter order;
- 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:
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:
- resolve plane bounds and the final row-vector 4x4 matrix
- validate projected corners, W, winding, and backface policy
- capture the subtree once into a command list in local coordinates under the plane clip
- apply
CLSID_D2D13DTransform - optionally apply post-projection directional blur
- 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
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
| Approach | Rejected because |
|---|---|
descendant CmdSetOpacity for a Story layer | overlapping children blend opacity more than once |
| per-primitive 4x4 transforms | breaks subtree overlap, effects, local clips, and group opacity |
| current backend damage tracking | not part of the audited implementation baseline |
| cross-frame layer cache | requires 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_CHILDand replayable child command streams.
Test Strategy
- verify shell snapshots recompute bounds and settle time after child-only updates
- verify ordinary and projected group opacity applies once to overlapping content
- verify nested groups and legacy node opacity multiply once per child boundary
- verify ordinary/root motion blur and projected 3D blur honor the documented preparation and fallback rules
- verify active siblings preserve painter order and invalid active-root/nested-plane topologies are rejected or skipped
- verify the current backend clears and replays the full target instead of producing dirty-rect compositing