Expand description
Auto-promotion of UI subtrees to composited layers.
A node whose style makes it a layer root (opacity present on a node
with children unless groupAlpha: false, a non-empty filter chain, or
cache: "always") has its
whole subtree captured into an offscreen atlas by a custom render pass and
drawn back as one quad — so opacity fades the subtree as a group (web
semantics) instead of folding into each node’s own colors (which shows
overlapping children through each other).
Captures are cached: a clean layer (no content dirt this frame — see
LayerContentDirt, resolve_layer_repaints) skips its capture pass
entirely and composites last frame’s texture. The layer root’s own
translation and group alpha are composite-time parameters, so
translate/opacity animation of a promoted subtree costs no re-capture —
promotion is the will-change pattern, an optimization rather than a tax.
Captures are also clip-independent: ancestor clipping (a scroll
container or the viewport) never reaches the captured pixels — members
are captured under interior clips only (the cascade restarted at the
layer root, clip) and the ancestor clip clamps the composite quad
at draw time instead (web semantics: overflow clips the filtered
result). This is what makes the translation-invariance above actually
hold under scroll: without it, a capture taken while clipped would be
served stale after scrolling into view. An offscreen layer still captures
when dirty (its quad just draws nothing) — the accepted cost is invisible
re-captures for continuously-animated offscreen subtrees.
Promotion is a render-side concern: the subtree stays in the main UI tree
(layout, picking, refs, animations untouched); promoting inserts the
PromotedLayer marker on the existing entity and demoting removes it.
The render half lives in render and works entirely through public
bevy_ui_render seams — stock extraction/queue/prepare run untouched; a
post-queue system moves the subtree’s already-queued phase items into a
per-layer synthetic view rendered to the atlas. See render for details.
Extensibility contract: each future promotion rule (transform3d,
backdrop) is one evaluator producing one PromotionReasons flag plus
composite parameters the render pass forwards without interpreting.
Promotion is !reasons.is_empty(); demotion is the flags emptying.
Modules§
- clip
- Interior clips — the main-world half of clip-independent layer capture.
- pick3d
- Transformed picking for
transform3dlayers. - render
- Render-world half of layer compositing — a custom pass over stock
bevy_ui_render, public API only (no fork). Mechanism per frame: - transform3d
- The composite-time 3D transform on a promoted layer (
transform3dstyle).
Structs§
- Layer
Capture Rect - The layer’s capture rectangle in physical pixels, in the same
screen space as
UiGlobalTransform— the node’s border box, inflated on every side by the node’s quantized filter outset (blur reads/writes beyond the border box; seecrate::filters::quantize_outset). Recomputed every frame after layout bysync_layer_geometry; consumed by extraction. v1 clips the capture to this box (web opacity does not clip — known divergence, diag-warned). - Layer
Content Dirt - Frame-scoped content-dirt inbox for the layer capture cache. Every site
that mutates a node’s rendered appearance pushes the entity here (see
mark_content_dirty);resolve_layer_repaintsdrains it inPostUpdate, onceLayerMembershipis this frame’s — the taps themselves can’t resolve node → layer, membership isn’t valid yet. - Layer
Group Alpha - The composite-time alpha of a promoted subtree (applied once to the whole
captured group). Separate from
PromotedLayerso per-frame writes from the animation/transition paths don’t look like promotion-state changes. - Layer
Membership - Which layer root each node under a promoted subtree belongs to
(ancestor-or-self, nearest wins — so nested layers map their interior,
including the inner root’s own paint, to the inner root). Rebuilt every
frame after layout; extracted to the render world to route stolen phase
items. A layer root’s composite quad is the one thing that routes by
Self::enclosinginstead (it draws inside the parent layer’s capture, or the screen when there is none). - Layer
Meta - Per-layer observability row in
LayersRegistry. Identity fields are written byevaluate_layer_promotions; geometry fields (capture_rect/depth) and the livegroup_alphaare refreshed bysync_layer_geometry; cache stats byresolve_layer_repaints. - Layer
Repaint State - Per-layer repaint decisions, rebuilt from scratch every frame by
resolve_layer_repaints— nothing persists to be cleared, so render extraction (which runs at the sync point after the whole main frame) always reads the same frame’s state. - Layers
Registry - Public registry of currently promoted layers — the observability surface auto-promotion comes with (promotion cost is invisible in JSX; this is where to see what promoted and why). Tests assert on it; a future devtools “layers” tab is a pure consumer.
- Promoted
Layer - Marker on a promoted layer root. Insert = promote, remove = demote — nothing else about the entity changes.
- Promotion
Reasons - Why a node is promoted — one bit per rule, OR’d together. A node is promoted iff any bit is set; it demotes when the set empties.
Functions§
- evaluate_
layer_ promotions - The single writer of promotion state. Drains the bridge’s dirty set (fed
by the op-apply hooks), re-evaluates
promotion_reasonsper node, and flips thePromotedLayer/LayerGroupAlphamarkers + the registry +bridge.promoted_layers. Ordered afterapply_js_opsand before the interaction/transition/animation appliers so every later alpha writer this frame sees the final promotion state. - fold_
member_ geometry - Fold one member’s root-relative geometry into a layer’s content hash:
translation relative to the layer root (so moving the whole layer cancels
exactly and never re-captures), the affine’s linear part (member scale /
rotation), and the laid-out size. Quantized (1/64 px positions, 1/1024
matrix entries) so float ulp noise —
(a+d)-(b+d)isn’t bit-exact — can’t flap the hash. Visit order is encoded implicitly by the fold sequence, so reorders change the hash too. - mark_
content_ dirty - Tap helper for
EntityCommandscall sites (style apply, op arms): queue a push of this entity intoLayerContentDirt. Queued (not immediate) because the op-apply sites only holdEntityCommands; the push lands at the next command flush, well before thePostUpdateresolver. A missing resource (external app without the plugin) degrades to a no-op. - promotion_
reasons - The promotion rule set — pure, one flag per rule (see the module doc’s
extensibility contract).
PromotionReasons::FORCEDiscache: "always"in the base style, gated only on element eligibility (no visual semantics of its own — no child orgroupAlphagate).PromotionReasons::FILTERis a non-emptyfilterchain in the base style OR any hover/press/focus variant, gated the same way (the effect is subtree-wide by definition, and a filtered leaf is valid). Like opacity the union is presence-based: interaction must never flip promotion, so a hover-only filter promotes eagerly — the layer (and its capture) exists before the first hover.PromotionReasons::OPACITY: - resolve_
layer_ repaints - Turn this frame’s dirt into per-layer repaint decisions. Runs in
PostUpdateaftersync_layer_geometry(membership + geometry hashes are this frame’s) and afterbevy_ui’s text systems (Changed<TextLayoutInfo>must see this frame’s reshapes). Render extraction reads the result at the sync point; the state is rebuilt from scratch next frame, so nothing needs clearing across frames. - sync_
layer_ geometry - Recomputes each promoted layer’s capture rect (inflated by the node’s
quantized filter outset — blur reads/writes beyond the border box, and the
composite quad, texture allocation, and synthetic-view ortho all derive
from this rect), the subtree membership map, and each layer’s
content-geometry hash (see
fold_member_geometry). Also warns (filterBleed) when a nested filtered layer’s inflated rect escapes its enclosing layer’s capture — v1 clips there, losing part of the bleed. Runs inPostUpdateafterbevy_uilayout soComputedNode/UiGlobalTransformare this frame’s values. - watch_
layer_ image_ assets <image>textures arrive asynchronously — no op, no bevy-react write site — so watch the asset events and dirty the owning layer of any node using a touched image. (One frame late for loads, which are async anyway; canvas uploads are alsoModifiedhere, double-covering their direct tap.)