Skip to main content

Module schedule

Module schedule 

Source
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_DEPTH deep;
  • 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_PAGES live 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.
FilterPass
One pass of a filter layer’s sequence, reading one page and writing the other.
PageTarget
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.
RoundTarget
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.