Expand description
Scheduling a recording into render passes.
A recorded frame is a tree: a root command stream that may enter a layer, whose own stream may enter another. Draws inside a layer that composites with anything other than plain source-over at full opacity cannot be written straight to the surface — the layer has to be rendered in isolation first and then composited as a whole. A GPU render pass writes one target, so each isolated layer costs a pass of its own. This module decides how many passes a frame needs, what each one targets, and in what order they run; it allocates nothing and touches no device.
Schedule::build returns the frame’s Rounds in execution order. A
round is one render pass over either the frame’s own surface or one pooled
intermediate page. Its ops run in the order they
are listed: a batch of draws, then the composite of a finished page onto the
round’s target, then more draws, and so on, exactly as the recording
interleaved them.
One target can take several rounds. The surface, and a layer’s page, are
written by as many passes as the frame needs; each after the first loads what
the one before it left rather than clearing (see PageTarget::continued),
so the sequence reads as one painter’s-order walk however it was cut up.
§What this scheduler serves, and what it refuses
Frust’s layer needs are narrow. Clips lower to a scissor rectangle or a
coverage mask and never to an intermediate texture (see
compile::clip), so the overwhelming majority of
frames contain no layer at all and schedule to a single round. Opacity
layers exist but are rare and shallow.
The scheduler serves every layer tree MAX_LIVE_PAGES pooled pages are
enough for:
- isolated layers nested at most
MAX_CHAIN_DEPTHdeep; - isolated layers beside each other under one parent, a fan of any width — two widgets fading at once is the commonest sibling shape a frust screen records, and a staggered list entrance fading five rows at once is served on the same two pages;
- a chain hanging off an isolated ancestor’s later child, which is what a navigation transition records for the whole of a scrub: a full-screen page layer holding a chip that carries a translucent chip of its own. This is the shape the spill page exists for — see The spill page below;
- any mixture of the above that stays inside
MAX_LIVE_PAGESlive pages; - a Gaussian-blur or shadow-only drop-shadow filter layer recorded directly under the frame’s own surface, which takes both ping-pong groups for the length of its pass sequence (see Filter rounds below).
A fan of any width fits because a parent does not have to composite all of its isolated children in one pass. When both groups are taken, the parent’s round is cut: the ops accumulated so far are emitted as a round of their own, the pages that batch composites go back to the pool, and the next sibling is rendered into one of them. What a cut cannot pay for is a layer whose own round samples a live page while an isolated ancestor is holding another — three pages really are live at that point — and that is where the spill page comes in.
Everything else — a layer that finds no page even after a cut and the spill,
a deeper chain, a filter this engine does not render, a filter layer nested
inside another recorded layer, a non-default blend mode, a layer mask or
layer clip path — is refused with EngineError::SchedulerEscalation
carrying a reason that names what was found.
§A refusal is a skipped frame, not a fallback
Refusing is a return value and never a panic (E17), but it is not a route to
a second renderer: the engine tier carries none, and which renderer draws a
surface is decided once when the surface is configured, not per frame. What
becomes of a refused frame is the caller’s own contract. frust-render’s
engine arm drops it — the acquired texture is released unpresented, the
refusal is counted, and the reason is logged rate-limited — so nothing new
reaches the screen and whatever was presented last stays there.
A shape that refuses on every frame therefore freezes a surface rather
than degrading it, which is the reason the refused set is kept to shapes
frust’s own widget tree does not record, and the reason a shape it does
record (two fading siblings) is served here instead of escalated. Porting the
reference renderer’s general scheduler, which serves any scene at the cost of
roughly ten times this module’s code, is what the
full-scheduler feature marks the site for.
§Bottom-up traversal and the two-page ping-pong
Rounds are emitted contents-before-composite: a layer’s contents must exist
before the pass that composites them can run, so the deepest layer is
rendered first and the surface’s last round is last. A layer takes the group
its depth’s parity names when that group is free, and the other group when it
is not (see PageParity).
Parity alone is what bounds a chain to two live pages however deep it runs:
while a parent renders into its own page it samples the child’s, and the
child’s page returns to the pool the moment the parent’s pass ends, so the
grandchild reuses it. Siblings are what makes the fallback to the other group
necessary — two layers at one depth share a parity, and a second page in one
group would overwrite the first before the parent ever sampled it. So the
second sibling takes the group the first left free, and a third finds both
taken. That is what [cut_at] answers: the parent’s round is closed early,
its two composites are done, both pages come back, and the third sibling
takes the group the first one had. A fan of any width costs one extra pass
per pair rather than a refused frame.
Pages are named lazily, by the round that renders into them, rather than reserved on the way down. An outer layer that reserved its page before the descent would hold one group for the whole traversal and defeat the ping-pong. The one exception is a layer whose round has to be cut: a cut renders into the layer’s own page, so that layer takes its group at its first cut and keeps it until its last round. That is what makes a cut ancestor expensive for everything below it — from its first cut onwards it is holding one of the two groups, so a chain hanging off its later child has only one group left to ping-pong in.
§The spill page
One page beside the pair (PageParity::Spill), taken only where the walk
would otherwise refuse the frame: after [make_room] has cut everything it
could and both groups are still holding pages later rounds composite, a
regular layer takes the spill page instead of escalating. It is acquired
and released exactly as a parity page is — it goes back to the pool the
moment the round whose composite sampled it ends — so it is one more live
intermediate at a peak, not a page held for the frame.
It exists for one shape, and that shape is a navigation transition: a full-screen layer at a fractional opacity (the page being scrubbed) holding a chip beside a chip that carries a translucent chip of its own. The outer layer takes a group at the cut its first chip forces, the nested chip takes the other, and the chip hosting it — whose own round has to sample the nested page while the outer page is still owed upwards — has none. Because the opacity is fractional for the whole gesture, refusing it refuses every frame of the gesture: the surface holds its last presented image until the transition ends, which is a frozen scrub rather than a dropped frame (see A refusal is a skipped frame above). The identical content at the root schedules on two pages, which is what made the shape a transition-only defect.
Bounded at one, deliberately. A second layer wanting the spill while it is
held is refused exactly as before, which is what keeps a frame’s live
intermediates at MAX_LIVE_PAGES and keeps this a narrowing of the
refused set rather than a step towards the general scheduler. Two callers do
not reach for it at all: a cut never takes it (a cut that cannot
find a group is skipped, and the walk carries on to the acquire that can
spill), and neither does a filter layer, whose passes
ping-pong between two groups of their own and so are held to the pair.
Everything the pair already served schedules exactly as it did:
[LivePages::free] counts only the two groups, so the cutting decisions
that serve fans and chains are made against the state they were tuned
against, and the spill is reached only at the site that used to escalate.
§Inlined and dropped layers
A layer only needs isolating when compositing it differs from drawing its contents directly. A recorded layer at full opacity with no blend, mask or clip composites source-over at alpha 1, which is precisely what drawing its contents into the parent does, so its stream is spliced into the parent’s round and it costs no page and no pass. At the other end, a layer at zero opacity contributes nothing at all and is dropped along with everything nested inside it, and so does a layer whose contents cover no pixel at all — neither its own round nor any round its descendants would have rendered is emitted, because nothing would ever sample the pages they wrote.
Depth is therefore counted over isolated layers only. Counting recorded depth instead would let an inlined layer push its isolated descendant onto the same parity as an isolated ancestor, putting two live pages in one group.
§Filter rounds
A filter layer is an isolated layer whose page is not
composited straight away: a sequence of filter passes runs over it first,
and the parent composites what the last of them wrote. A pass reads a whole
image and writes a whole image, and one render pass cannot do both to one
texture, so each pass is a round of its own writing the group it did not
read — the same ping-pong the chain uses, run between two pages of one
layer instead of two layers. The sequence is arranged to be even in length
(see crate::filters::blur::blur_passes and
crate::filters::drop_shadow::drop_shadow_passes), so the result lands
back in the page the layer’s own contents were rendered into and the
composite is the ordinary one.
Every filter round clears its destination rather than loading it. A pass writes only the region its step names — a decimated one writes a quarter of the texels the pass before it did — and the kernels sample bilinearly with no bounds checks, so whatever surrounds the written region has to be transparent rather than a previous holder’s pixels.
Two consequences bound what filter shapes are served. The layer holds both
groups from its first filter round to its last, so a filter layer is refused
whenever the second group cannot be handed back to it; and it is refused
outright when it is recorded inside another layer, because vello_common
places a filter layer in its parent by undoing a source shift the reference
renderer applies to a filter layer’s contents and frust’s compiler does not,
which would size the parent’s page from bounds short of the filter’s own
spread on two sides. Neither is a shape frust records: a backdrop blur is a
layer under the surface.
Re-exports§
pub use pages::PageConfig;pub use pages::PageParity;pub use pages::PageSize;pub use pages::filter_page_size;pub use pages::page_ceiling;pub use pages::page_size;
Modules§
- pages
- Intermediate pages: which of the two ping-pong groups one comes from, and how large it is asked for.
Structs§
- Composite
- Compositing a finished page onto the round’s target.
- Filter
Pass - One pass of a filter layer’s sequence, reading one page and writing the other.
- Page
Target - One pooled intermediate page a round renders into.
- Round
- One render pass over one target.
- Schedule
- Turns a recording into the rounds that render it.
Enums§
- RoundOp
- One unit of work inside a round, executed in list order.
- Round
Target - What a round renders into.
Constants§
- MAX_
CHAIN_ DEPTH - The deepest chain of nested isolated layers this scheduler serves.
- MAX_
LIVE_ PAGES - The most intermediate pages this scheduler keeps live at one time.
- PING_
PONG_ GROUPS - The page groups the depth-parity ping-pong alternates between.