Skip to main content

frust_engine/schedule/
mod.rs

1//! Scheduling a recording into render passes.
2//!
3//! A recorded frame is a tree: a root command stream that may enter a layer,
4//! whose own stream may enter another. Draws inside a layer that composites
5//! with anything other than plain source-over at full opacity cannot be written
6//! straight to the surface — the layer has to be rendered in isolation first
7//! and then composited as a whole. A GPU render pass writes one target, so each
8//! isolated layer costs a pass of its own. This module decides how many passes
9//! a frame needs, what each one targets, and in what order they run; it
10//! allocates nothing and touches no device.
11//!
12//! [`Schedule::build`] returns the frame's [`Round`]s in execution order. A
13//! round is one render pass over either the frame's own surface or one pooled
14//! intermediate [page](pages). Its [`ops`](Round::ops) run in the order they
15//! are listed: a batch of draws, then the composite of a finished page onto the
16//! round's target, then more draws, and so on, exactly as the recording
17//! interleaved them.
18//!
19//! One target can take several rounds. The surface, and a layer's page, are
20//! written by as many passes as the frame needs; each after the first loads what
21//! the one before it left rather than clearing (see [`PageTarget::continued`]),
22//! so the sequence reads as one painter's-order walk however it was cut up.
23//!
24//! ## What this scheduler serves, and what it refuses
25//!
26//! Frust's layer needs are narrow. Clips lower to a scissor rectangle or a
27//! coverage mask and never to an intermediate texture (see
28//! [`compile::clip`](crate::compile::clip)), so the overwhelming majority of
29//! frames contain no layer at all and schedule to a single round. Opacity
30//! layers exist but are rare and shallow.
31//!
32//! The scheduler serves every layer tree [`MAX_LIVE_PAGES`] pooled pages are
33//! enough for:
34//!
35//! - isolated layers nested at most [`MAX_CHAIN_DEPTH`] deep;
36//! - isolated layers *beside* each other under one parent, a fan of any width —
37//!   two widgets fading at once is the commonest sibling shape a frust screen
38//!   records, and a staggered list entrance fading five rows at once is served
39//!   on the same two pages;
40//! - a chain hanging off an isolated ancestor's later child, which is what a
41//!   navigation transition records for the whole of a scrub: a full-screen page
42//!   layer holding a chip that carries a translucent chip of its own. This is
43//!   the shape the [spill page](PageParity::Spill) exists for — see *The spill
44//!   page* below;
45//! - any mixture of the above that stays inside [`MAX_LIVE_PAGES`] live pages;
46//! - a Gaussian-blur or shadow-only drop-shadow [filter](crate::filters) layer
47//!   recorded directly under the frame's own surface, which takes *both*
48//!   ping-pong groups for the length of its pass sequence (see *Filter rounds*
49//!   below).
50//!
51//! A fan of any width fits because a parent does not have to composite *all* of
52//! its isolated children in one pass. When both groups are taken, the parent's
53//! round is [cut](cut_at): the ops accumulated so far are emitted as a round of
54//! their own, the pages that batch composites go back to the pool, and the next
55//! sibling is rendered into one of them. What a cut cannot pay for is a layer
56//! whose own round samples a live page while an isolated ancestor is holding
57//! another — three pages really are live at that point — and that is where the
58//! spill page comes in.
59//!
60//! Everything else — a layer that finds no page even after a cut and the spill,
61//! a deeper chain, a filter this engine does not render, a filter layer nested
62//! inside another recorded layer, a non-default blend mode, a layer mask or
63//! layer clip path — is refused with [`EngineError::SchedulerEscalation`]
64//! carrying a reason that names what was found.
65//!
66//! ## A refusal is a skipped frame, not a fallback
67//!
68//! Refusing is a return value and never a panic (E17), but it is not a route to
69//! a second renderer: the engine tier carries none, and which renderer draws a
70//! surface is decided once when the surface is configured, not per frame. What
71//! becomes of a refused frame is the caller's own contract. `frust-render`'s
72//! engine arm drops it — the acquired texture is released unpresented, the
73//! refusal is counted, and the reason is logged rate-limited — so nothing new
74//! reaches the screen and whatever was presented last stays there.
75//!
76//! A shape that refuses on *every* frame therefore freezes a surface rather
77//! than degrading it, which is the reason the refused set is kept to shapes
78//! frust's own widget tree does not record, and the reason a shape it does
79//! record (two fading siblings) is served here instead of escalated. Porting the
80//! reference renderer's general scheduler, which serves any scene at the cost of
81//! roughly ten times this module's code, is what the
82//! [`full-scheduler`](full) feature marks the site for.
83//!
84//! ## Bottom-up traversal and the two-page ping-pong
85//!
86//! Rounds are emitted contents-before-composite: a layer's contents must exist
87//! before the pass that composites them can run, so the deepest layer is
88//! rendered first and the surface's last round is last. A layer takes the group
89//! its depth's parity names when that group is free, and the other group when it
90//! is not (see [`PageParity`]).
91//!
92//! Parity alone is what bounds a *chain* to two live pages however deep it runs:
93//! while a parent renders into its own page it samples the child's, and the
94//! child's page returns to the pool the moment the parent's pass ends, so the
95//! grandchild reuses it. Siblings are what makes the fallback to the other group
96//! necessary — two layers at one depth share a parity, and a second page in one
97//! group would overwrite the first before the parent ever sampled it. So the
98//! second sibling takes the group the first left free, and a third finds both
99//! taken. That is what [`cut_at`] answers: the parent's round is closed early,
100//! its two composites are done, both pages come back, and the third sibling
101//! takes the group the first one had. A fan of any width costs one extra pass
102//! per pair rather than a refused frame.
103//!
104//! Pages are named lazily, by the round that renders into them, rather than
105//! reserved on the way down. An outer layer that reserved its page before the
106//! descent would hold one group for the whole traversal and defeat the
107//! ping-pong. The one exception is a layer whose round has to be cut: a cut
108//! renders into the layer's own page, so that layer takes its group at its first
109//! cut and keeps it until its last round. That is what makes a cut ancestor
110//! expensive for everything below it — from its first cut onwards it is holding
111//! one of the two groups, so a chain hanging off its later child has only one
112//! group left to ping-pong in.
113//!
114//! ## The spill page
115//!
116//! One page beside the pair ([`PageParity::Spill`]), taken only where the walk
117//! would otherwise refuse the frame: after [`make_room`] has cut everything it
118//! could and both groups are still holding pages later rounds composite, a
119//! *regular* layer takes the spill page instead of escalating. It is acquired
120//! and released exactly as a parity page is — it goes back to the pool the
121//! moment the round whose composite sampled it ends — so it is one more live
122//! intermediate at a peak, not a page held for the frame.
123//!
124//! It exists for one shape, and that shape is a navigation transition: a
125//! full-screen layer at a fractional opacity (the page being scrubbed) holding
126//! a chip beside a chip that carries a translucent chip of its own. The outer
127//! layer takes a group at the cut its first chip forces, the nested chip takes
128//! the other, and the chip hosting it — whose own round has to sample the
129//! nested page while the outer page is still owed upwards — has none. Because
130//! the opacity is fractional for the whole gesture, refusing it refuses *every*
131//! frame of the gesture: the surface holds its last presented image until the
132//! transition ends, which is a frozen scrub rather than a dropped frame (see *A
133//! refusal is a skipped frame* above). The identical content at the root
134//! schedules on two pages, which is what made the shape a transition-only
135//! defect.
136//!
137//! Bounded at one, deliberately. A second layer wanting the spill while it is
138//! held is refused exactly as before, which is what keeps a frame's live
139//! intermediates at [`MAX_LIVE_PAGES`] and keeps this a narrowing of the
140//! refused set rather than a step towards the general scheduler. Two callers do
141//! not reach for it at all: a [cut](cut_at) never takes it (a cut that cannot
142//! find a group is skipped, and the walk carries on to the acquire that can
143//! spill), and neither does a [filter layer](filter_rounds), whose passes
144//! ping-pong between two groups of their own and so are held to the pair.
145//!
146//! Everything the pair already served schedules exactly as it did:
147//! [`LivePages::free`] counts only the two groups, so the cutting decisions
148//! that serve fans and chains are made against the state they were tuned
149//! against, and the spill is reached only at the site that used to escalate.
150//!
151//! ## Inlined and dropped layers
152//!
153//! A layer only needs isolating when compositing it differs from drawing its
154//! contents directly. A recorded layer at full opacity with no blend, mask or
155//! clip composites source-over at alpha 1, which is precisely what drawing its
156//! contents into the parent does, so its stream is spliced into the parent's
157//! round and it costs no page and no pass. At the other end, a layer at zero
158//! opacity contributes nothing at all and is dropped along with everything
159//! nested inside it, and so does a layer whose contents cover no pixel at all —
160//! neither its own round nor any round its descendants would have rendered is
161//! emitted, because nothing would ever sample the pages they wrote.
162//!
163//! Depth is therefore counted over *isolated* layers only. Counting recorded
164//! depth instead would let an inlined layer push its isolated descendant onto
165//! the same parity as an isolated ancestor, putting two live pages in one group.
166//!
167//! ## Filter rounds
168//!
169//! A [filter](crate::filters) layer is an isolated layer whose page is not
170//! composited straight away: a sequence of filter passes runs over it first,
171//! and the parent composites what the last of them wrote. A pass reads a whole
172//! image and writes a whole image, and one render pass cannot do both to one
173//! texture, so each pass is a round of its own writing the group it did not
174//! read — the same ping-pong the chain uses, run between two pages of one
175//! layer instead of two layers. The sequence is arranged to be even in length
176//! (see [`crate::filters::blur::blur_passes`] and
177//! [`crate::filters::drop_shadow::drop_shadow_passes`]), so the result lands
178//! back in the page the layer's own contents were rendered into and the
179//! composite is the ordinary one.
180//!
181//! Every filter round clears its destination rather than loading it. A pass
182//! writes only the region its step names — a decimated one writes a quarter of
183//! the texels the pass before it did — and the kernels sample bilinearly with
184//! no bounds checks, so whatever surrounds the written region has to be
185//! transparent rather than a previous holder's pixels.
186//!
187//! Two consequences bound what filter shapes are served. The layer holds *both*
188//! groups from its first filter round to its last, so a filter layer is refused
189//! whenever the second group cannot be handed back to it; and it is refused
190//! outright when it is recorded inside another layer, because `vello_common`
191//! places a filter layer in its parent by undoing a source shift the reference
192//! renderer applies to a filter layer's contents and frust's compiler does not,
193//! which would size the parent's page from bounds short of the filter's own
194//! spread on two sides. Neither is a shape frust records: a backdrop blur is a
195//! layer under the surface.
196
197pub mod pages;
198
199pub use pages::{PageConfig, PageParity, PageSize, filter_page_size, page_ceiling, page_size};
200
201use core::ops::Range;
202
203use frust_gpu::TierCaps;
204use vello_common::geometry::{RectU16, SizeU16};
205use vello_common::peniko::BlendMode;
206use vello_common::record::{CommandRecorder, Node, RecordedLayer, RecordedLayerKind};
207
208use crate::compile::EngineDraw;
209use crate::error::EngineError;
210use crate::filters::{FilterStep, ServedFilter, blur, drop_shadow, served_filter};
211
212/// The deepest chain of nested isolated layers this scheduler serves.
213///
214/// Four levels covers every layer shape frust's widget set records; a deeper
215/// one is escalated rather than served — the frame is skipped, and shapes past
216/// this bound are rare enough that skipping them beats the cost and complexity
217/// of growing this module to serve them.
218pub const MAX_CHAIN_DEPTH: usize = 4;
219
220/// The page groups the depth-parity ping-pong alternates between.
221///
222/// Two is what the even/odd alternation guarantees for a chain of any depth,
223/// and what a fan of any width is served on by cutting its parent's round. It
224/// is the *pair*, not the frame's page budget — see [`MAX_LIVE_PAGES`].
225pub const PING_PONG_GROUPS: usize = 2;
226
227/// The most intermediate pages this scheduler keeps live at one time.
228///
229/// The [two ping-pong groups](PING_PONG_GROUPS) plus one: a single spill page
230/// ([`PageParity::Spill`]) for the shape that needs a third live page and
231/// cannot be cut into needing fewer. A shape needing a *fourth* — after cutting
232/// an open round has been tried and could not hand one back — is a shape this
233/// scheduler does not serve.
234pub const MAX_LIVE_PAGES: usize = PING_PONG_GROUPS + 1;
235
236/// Where the reference renderer's general scheduler is ported.
237///
238/// Deliberately empty. The scheduler this module implements covers the layer
239/// shapes frust actually records and escalates the rest; the general algorithm
240/// — an atlas per parity group, batching independent layers into shared rounds,
241/// filter pass plans, non-default blend through a scratch texture — is an order
242/// of magnitude more code and lands here, behind this feature, if and when a
243/// frame shape needs it. The feature exists now so the module path is settled
244/// and the escalation route above has a named destination.
245#[cfg(feature = "full-scheduler")]
246pub mod full {}
247
248/// One pooled intermediate page a round renders into.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct PageTarget {
251    /// The layer this page holds, indexed into
252    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
253    pub layer: u32,
254    /// The layer's depth counted over isolated layers only, one-based.
255    pub depth: usize,
256    /// The group the page comes from — [`depth`](Self::depth)'s parity when
257    /// either of the pair was free, the other of the pair when it was not, and
258    /// the [spill page](PageParity::Spill) when neither was.
259    pub parity: PageParity,
260    /// The extent to acquire the page at.
261    pub size: PageSize,
262    /// The tile-aligned device-space bounds this page holds — the layer's own
263    /// for a layer that fits one page, and one column [band](pages::page_bands)
264    /// of them for a layer wider than any page is.
265    ///
266    /// The contents are rendered at the page's origin, so every strip drawn
267    /// into this round is offset by `-(bounds.x0, bounds.y0)` *and clipped to
268    /// this rectangle*: a banded layer's rounds are all handed the same ops, so
269    /// this is what selects the part of them each band actually holds. The
270    /// renderer's `PageWindow` is where both halves are applied.
271    pub bounds: RectU16,
272    /// Whether an earlier round of this same layer already rendered into this
273    /// page, so this one loads its contents instead of clearing them.
274    ///
275    /// A pooled page holds whatever its last holder left there, which is why a
276    /// layer's *first* round always clears it. A layer whose round was
277    /// [cut](cut_at) keeps the page across the cut — clearing again would wipe
278    /// the half already drawn — and no other layer can have taken the group in
279    /// between, because the cut is what made this layer the group's holder.
280    pub continued: bool,
281}
282
283/// What a round renders into.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum RoundTarget {
286    /// The frame's own surface. Draws land at their recorded coordinates.
287    Root,
288    /// A pooled intermediate page holding one isolated layer.
289    Page(PageTarget),
290}
291
292/// Compositing a finished page onto the round's target.
293///
294/// One instanced quad through the intermediate-strip pipeline: it samples
295/// `source()` of the page in [`parity`](Self::parity) and writes
296/// [`bounds`](Self::bounds), modulated by [`opacity`](Self::opacity). The page
297/// is free for reuse once the round holding this composite ends.
298#[derive(Debug, Clone, PartialEq)]
299pub struct Composite {
300    /// The layer being composited, indexed into
301    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
302    pub layer: u32,
303    /// The group holding the finished page.
304    pub parity: PageParity,
305    /// Where the layer lands in the round's target, in that target's own
306    /// coordinates.
307    pub bounds: RectU16,
308    /// Constant alpha applied while compositing, strictly between 0 and 1.
309    pub opacity: f32,
310}
311
312impl Composite {
313    /// The region of the page to sample, in page texels.
314    ///
315    /// The layer was rendered at the page's origin, so this is
316    /// [`bounds`](Self::bounds) moved there.
317    #[must_use]
318    pub fn source(&self) -> RectU16 {
319        RectU16::new(0, 0, self.bounds.width(), self.bounds.height())
320    }
321}
322
323/// One pass of a filter layer's sequence, reading one page and writing the
324/// other.
325///
326/// One instanced quad through the filter pipeline. The page it writes is the
327/// round's own target, so only the page it reads is named here; the extents it
328/// reads and writes are [`step`](Self::step)'s, both taken at their page's
329/// origin because a layer is always rendered there.
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct FilterPass {
332    /// The layer being filtered, indexed into
333    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
334    pub layer: u32,
335    /// Which pass of the filter's sequence this is, and at what extents.
336    pub step: FilterStep,
337    /// The group holding the page this pass reads.
338    pub source: PageParity,
339}
340
341/// One unit of work inside a round, executed in list order.
342#[derive(Debug, Clone, PartialEq)]
343pub enum RoundOp {
344    /// Draws to issue into the round's target, indexed into
345    /// [`CommandRecorder::draws`](vello_common::record::CommandRecorder::draws).
346    ///
347    /// Never empty: a node that recorded no draw contributes no op.
348    Draws(Range<u32>),
349    /// A finished page composited onto the round's target.
350    Composite(Composite),
351}
352
353/// One render pass over one target.
354///
355/// A target is not a round: the surface, and a layer's page, can each take
356/// several, and the rounds of one target run in the order they are listed with
357/// every one after the first loading what the one before it left.
358#[derive(Debug, Clone, PartialEq)]
359pub struct Round {
360    /// What this pass renders into.
361    pub target: RoundTarget,
362    /// The pass's work, in execution order. Empty on a filter round, which
363    /// issues no draw and composites nothing.
364    pub ops: Vec<RoundOp>,
365    /// Pages whose contents this round consumed; they return to the pool once
366    /// it completes.
367    pub released: Vec<PageParity>,
368    /// The filter pass this round runs, on a filter round.
369    ///
370    /// A field rather than a [`RoundOp`] variant, and it stays one now that the
371    /// renderer executes filter rounds: a filter round is *only* a filter pass
372    /// — it draws nothing and composites nothing, and it is drawn through a
373    /// different pipeline, off a different instance buffer, with no viewport
374    /// uniform and no strip bind groups. Naming it here is what lets the frame
375    /// path branch on the round before it starts issuing a list, and keeps
376    /// [`RoundOp`] exactly the set of things one strip pass can interleave.
377    pub filter: Option<FilterPass>,
378}
379
380impl Round {
381    /// The page this round renders into, or `None` for the surface.
382    #[must_use]
383    pub fn page(&self) -> Option<&PageTarget> {
384        match &self.target {
385            RoundTarget::Root => None,
386            RoundTarget::Page(page) => Some(page),
387        }
388    }
389
390    /// The filter pass this round runs, or `None` on an ordinary round.
391    #[must_use]
392    pub fn filter_pass(&self) -> Option<&FilterPass> {
393        self.filter.as_ref()
394    }
395
396    /// Whether this round renders into the frame's own surface.
397    #[must_use]
398    pub fn is_root(&self) -> bool {
399        matches!(self.target, RoundTarget::Root)
400    }
401
402    /// The composites this round performs, in execution order.
403    pub fn composites(&self) -> impl Iterator<Item = &Composite> {
404        self.ops.iter().filter_map(|op| match op {
405            RoundOp::Composite(composite) => Some(composite),
406            RoundOp::Draws(_) => None,
407        })
408    }
409
410    /// How many recorded draws this round issues.
411    #[must_use]
412    pub fn draw_count(&self) -> u32 {
413        self.ops
414            .iter()
415            .map(|op| match op {
416                RoundOp::Draws(range) => range.end.saturating_sub(range.start),
417                RoundOp::Composite(_) => 0,
418            })
419            .sum()
420    }
421}
422
423/// Turns a recording into the rounds that render it.
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub struct Schedule;
426
427impl Schedule {
428    /// The rounds `recorder` renders as: every layer's rounds before the round
429    /// that composites it, and the surface's last round last.
430    ///
431    /// Always at least one round: a recording with no layers, and one whose
432    /// layers all inline or drop away, both schedule to the single root round
433    /// that clears and paints the surface.
434    ///
435    /// # Errors
436    ///
437    /// [`EngineError::SchedulerEscalation`] for a layer shape outside what this
438    /// scheduler serves (see the module header), with a reason naming what was
439    /// found, and [`EngineError::IntermediateTextureTooLarge`] for a layer
440    /// larger than a page can be sized to. Neither is a panic, and neither
441    /// routes the frame anywhere: both hand the caller a frame it is expected
442    /// to skip.
443    ///
444    /// A Gaussian-blur or shadow-only drop-shadow filter layer is *planned*
445    /// here: its contents' round, then one round per pass of its filter's
446    /// sequence, then the composite of whichever page the last pass wrote.
447    /// Every other filter is refused by name (see
448    /// [`crate::filters::served_filter`]), and so are the two shapes a served
449    /// filter layer still cannot take — one recorded inside another layer, and
450    /// one that cannot be handed the second page group its passes ping-pong
451    /// into.
452    ///
453    /// One entry point, not two. While the renderer had no filter pipeline,
454    /// this call refused every filter and a second one planned them, so the
455    /// frame path could not reach a round nothing could execute; the renderer
456    /// runs them now ([`crate::renderer::FilterResources`]), and the split went
457    /// with the reason for it.
458    pub fn build(
459        recorder: &CommandRecorder<EngineDraw>,
460        caps: &TierCaps,
461        config: &PageConfig,
462    ) -> Result<Vec<Round>, EngineError> {
463        let mut rounds: Vec<Round> = Vec::new();
464        let mut pages = LivePages::default();
465        // A child layer is always recorded after its parent, so a well-formed
466        // recording enters each layer exactly once. Marking them is what keeps
467        // a recording whose nodes re-enter one from being walked forever, and
468        // it bounds the stack below to one entry per recorded layer.
469        let mut entered = vec![false; recorder.layers.len()];
470        let mut stack = vec![Stream::root(&recorder.nodes)];
471
472        while !stack.is_empty() {
473            // Taking one node off the innermost open stream is a borrow of the
474            // stack that has to end before the step below can push or pop it.
475            let step = match stack.last_mut() {
476                None => break,
477                Some(stream) => match stream.nodes.get(stream.index) {
478                    None => Step::Done,
479                    Some(node) => {
480                        stream.index = stream.index.saturating_add(1);
481                        if node.draws.start < node.draws.end {
482                            stream.ops.push(RoundOp::Draws(node.draws.clone()));
483                        }
484                        match node.layer {
485                            None => Step::Next,
486                            Some(id) => Step::Enter {
487                                id,
488                                depth: stream.depth,
489                            },
490                        }
491                    }
492                },
493            };
494
495            match step {
496                Step::Next => {}
497                Step::Done => {
498                    if let Some(stream) = stack.pop() {
499                        let carry =
500                            finish(stream, &mut stack, &mut rounds, &mut pages, caps, config)?;
501                        if let Some(parent) = stack.last_mut() {
502                            parent.ops.extend(carry);
503                        }
504                    }
505                }
506                Step::Enter { id, depth } => {
507                    let index = id as usize;
508                    let (Some(layer), Some(seen)) =
509                        (recorder.layers.get(index), entered.get_mut(index))
510                    else {
511                        return Err(escalate(format!(
512                            "a node enters layer {id}, which the recording does not hold"
513                        )));
514                    };
515                    if *seen {
516                        return Err(escalate(format!(
517                            "layer {id} is entered a second time; the simple scheduler walks \
518                             each recorded layer once, and a recording whose nodes re-enter one \
519                             describes no tree it could render"
520                        )));
521                    }
522                    *seen = true;
523
524                    // Suppression is inherited rather than re-derived: a layer
525                    // inside one covering no pixels covers none either, however
526                    // its own bounds read.
527                    let inherited = stack.last().is_some_and(|stream| stream.suppressed);
528
529                    // Nothing nested inside a dropped layer is rendered, so
530                    // nothing inside one is validated either — the walk does
531                    // not enter it.
532                    let role = layer_role(id, layer)?;
533                    // A filter layer's placement in its parent is computed by
534                    // undoing a shift the reference renderer applies to a
535                    // filter layer's contents and frust's compiler does not, so
536                    // the bounds it hands its parent fall short of the filter's
537                    // own spread on two sides. Refused rather than rendered
538                    // from bounds that would clip it; a backdrop blur is
539                    // recorded under the surface, where nothing reads that
540                    // placement.
541                    if matches!(role, LayerRole::Filtered) && stack.len() > 1 {
542                        return Err(escalate(format!(
543                            "filter layer {id} is recorded inside another layer, whose own bounds \
544                             would be taken from a placement that undoes a source shift this \
545                             engine's compiler never applied; a filter layer is served directly \
546                             under the frame's own surface"
547                        )));
548                    }
549
550                    match role {
551                        LayerRole::Dropped => {}
552                        LayerRole::Inline => stack.push(Stream::inline(layer, depth, inherited)),
553                        LayerRole::Isolated | LayerRole::Filtered => {
554                            let depth = depth.saturating_add(1);
555                            if depth > MAX_CHAIN_DEPTH {
556                                return Err(escalate(format!(
557                                    "{depth} nested isolated layers, deeper than the \
558                                     {MAX_CHAIN_DEPTH}-deep chain the simple scheduler serves"
559                                )));
560                            }
561                            // A layer whose contents cover nothing composites
562                            // nothing, so it renders no round — and neither do
563                            // its descendants, whose pages nothing would ever
564                            // sample. The walk still enters it, so a shape this
565                            // scheduler refuses is refused wherever it is
566                            // recorded rather than only where it is visible.
567                            let suppressed = inherited || layer.bbox.is_empty();
568                            stack.push(Stream::isolated(id, layer, depth, suppressed));
569                        }
570                    }
571                }
572            }
573        }
574
575        Ok(rounds)
576    }
577}
578
579/// Emits the last round a finished stream renders as, and returns what its
580/// parent splices in at the node that entered it: one composite for an isolated
581/// layer, the layer's own ops for an inlined one, nothing for a layer that
582/// composites nothing. The frame root pushes the last round and carries nothing.
583///
584/// `stack` is the walk's remaining streams — this stream's ancestors — because
585/// a layer short of a page makes room by [cutting](cut_at) one of their rounds.
586fn finish<'a>(
587    stream: Stream<'a>,
588    stack: &mut [Stream<'a>],
589    rounds: &mut Vec<Round>,
590    pages: &mut LivePages,
591    caps: &TierCaps,
592    config: &PageConfig,
593) -> Result<Vec<RoundOp>, EngineError> {
594    let (id, layer) = match stream.owner {
595        StreamOwner::Inline => return Ok(stream.ops),
596        StreamOwner::Root => {
597            let released = released_pages(&stream.ops);
598            for parity in &released {
599                pages.release(*parity);
600            }
601            // An empty trailing round is worth a pass only when it is the
602            // frame's only one: `build` always returns at least one round, but
603            // a surface already written by an earlier cut needs no empty pass
604            // behind it.
605            if !stream.ops.is_empty() || !stream.emitted {
606                rounds.push(Round {
607                    target: RoundTarget::Root,
608                    ops: stream.ops,
609                    released,
610                    filter: None,
611                });
612            }
613            return Ok(Vec::new());
614        }
615        StreamOwner::Isolated { id, layer } => (id, layer),
616    };
617
618    // A layer covering no pixels, or one nested inside such a layer, renders
619    // nothing: no round of its own was cut, none of its descendants' was
620    // emitted, and no page was ever taken for any of them, so there is nothing
621    // to release and nothing for the parent to composite.
622    if stream.suppressed {
623        return Ok(Vec::new());
624    }
625
626    let bounds = layer.bbox;
627    let depth = stream.depth;
628
629    // A regular layer wider than any single page is banded into column
630    // pages (E14) rather than refused — but only while nothing has claimed
631    // its page ahead of time (a cut ancestor reserves one this way, see
632    // `cut_at`) and its own accumulated ops hold no composite of a nested
633    // isolated child: a band's ops are replayed once per band, and
634    // replaying a child's composite would read a page a later band has
635    // already reused. Both are shapes banding does not reach, and a layer
636    // whose height alone exceeds the ceiling is still refused by `page_size`
637    // below exactly as it always was — bands are columns, so height has no
638    // split to be served by.
639    if matches!(layer.kind, RecordedLayerKind::Regular)
640        && stream.page.is_none()
641        && u32::from(bounds.width()) > page_ceiling(config, caps)
642        && !holds_composite(&stream.ops)
643    {
644        return band_rounds(
645            BandedLayer {
646                id,
647                layer,
648                bounds,
649                depth,
650            },
651            stream.ops,
652            stack,
653            rounds,
654            pages,
655            caps,
656            config,
657        );
658    }
659
660    // A filter layer's page is sized with the extra atlas margin
661    // (`filter_page_size`) its kernel taps can reach past: both this
662    // contents round and every filter-pass round the layer costs share this
663    // one `size` (see `filter_rounds`'s own `filtered.size`), so the margin
664    // has to be reserved here, before the first byte of the layer is ever
665    // rendered — sizing only the pass rounds would leave the contents
666    // themselves on an unpadded page while the first pass reads past its
667    // edge. A regular layer never filters its contents, so it keeps the
668    // plain `page_size` this always used.
669    let size = match &layer.kind {
670        RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
671        _ => filter_page_size(bounds, config, caps)?,
672    };
673    let page = |parity, continued| {
674        RoundTarget::Page(PageTarget {
675            layer: id,
676            depth,
677            parity,
678            size,
679            bounds,
680            continued,
681        })
682    };
683
684    // The group holding the layer's own contents once its last content round
685    // has been emitted.
686    let contents = match stream.page {
687        // The layer's round was cut earlier this frame, so its page is already
688        // this stream's and this round loads what the cut left in it.
689        Some(parity) => {
690            if !stream.ops.is_empty() {
691                let released = released_pages(&stream.ops);
692                for parity in &released {
693                    pages.release(*parity);
694                }
695                rounds.push(Round {
696                    target: page(parity, true),
697                    ops: stream.ops,
698                    released,
699                    filter: None,
700                });
701            }
702            parity
703        }
704        None => {
705            make_room(stack, rounds, pages, caps, config)?;
706            // Acquired before the children's pages are freed, never after: a
707            // child's page is live for the whole of the pass that samples it,
708            // so the group this round renders into can never be one of theirs.
709            // With both groups still held after `make_room` has cut everything
710            // it could, a regular layer takes the one spill page rather than
711            // refusing the frame — see the module header's *The spill page*.
712            let spills = matches!(layer.kind, RecordedLayerKind::Regular);
713            let parity = pages
714                .acquire(PageParity::from_depth(depth))
715                .or_else(|| spills.then(|| pages.acquire_spill()).flatten())
716                .ok_or_else(|| escalate(out_of_pages(id, spills)))?;
717            let released = released_pages(&stream.ops);
718            for parity in &released {
719                pages.release(*parity);
720            }
721            rounds.push(Round {
722                target: page(parity, false),
723                ops: stream.ops,
724                released,
725                filter: None,
726            });
727            parity
728        }
729    };
730
731    // A filter layer's contents are not what the parent composites: the filter
732    // passes run over them first, and the parent samples whatever the last of
733    // them wrote.
734    let composited = match &layer.kind {
735        RecordedLayerKind::Regular => contents,
736        kind => filter_rounds(
737            &FilterLayer {
738                id,
739                kind,
740                depth,
741                bounds,
742                size,
743                contents,
744            },
745            stack,
746            rounds,
747            pages,
748            caps,
749            config,
750        )?,
751    };
752
753    Ok(vec![RoundOp::Composite(Composite {
754        layer: id,
755        parity: composited,
756        bounds,
757        opacity: layer.props.opacity,
758    })])
759}
760
761/// A filter layer whose contents are rendered, ready for its passes to be
762/// planned.
763struct FilterLayer<'a> {
764    /// The layer, indexed into
765    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
766    id: u32,
767    /// The recorded filter, re-read here rather than carried down from
768    /// [`layer_role`]: preparing a blur's kernel is a handful of arithmetic
769    /// over a fixed-size array, and threading it through the walk's stack would
770    /// put a filter's state in every stream that has none.
771    kind: &'a RecordedLayerKind,
772    /// The layer's depth counted over isolated layers only, one-based.
773    depth: usize,
774    /// The layer's tile-aligned device-space bounds, already grown by the
775    /// filter's own spread (see [`crate::filters::LayerFilter::filter_data`]).
776    bounds: RectU16,
777    /// The extent both of the layer's pages are acquired at.
778    size: PageSize,
779    /// The group the layer's own contents were rendered into.
780    contents: PageParity,
781}
782
783/// Emits the rounds a filter layer's pass sequence renders as, and answers the
784/// group holding the filtered result for its parent to composite.
785///
786/// One round per pass, each writing the group it did not read. The sequence is
787/// even in length, so the result lands back in the page the contents were
788/// rendered into; [`blur::blur_passes`] is what keeps that true.
789fn filter_rounds(
790    filtered: &FilterLayer<'_>,
791    stack: &mut [Stream<'_>],
792    rounds: &mut Vec<Round>,
793    pages: &mut LivePages,
794    caps: &TierCaps,
795    config: &PageConfig,
796) -> Result<PageParity, EngineError> {
797    let id = filtered.id;
798    // The one dispatch every filter-recognising site in this crate shares
799    // (`renderer::filter_block`, `layer_role` below): the recorded primitive
800    // names which filter it is, and the reason a refusal carries is that
801    // primitive's own.
802    let steps = match served_filter(id, filtered.kind).map_err(escalate)? {
803        ServedFilter::Blur(blur) => blur::blur_passes(&blur, SizeU16::from(filtered.bounds)),
804        ServedFilter::DropShadow(shadow) => {
805            drop_shadow::drop_shadow_passes(&shadow, SizeU16::from(filtered.bounds))
806        }
807    };
808    let Some(last) = steps.len().checked_sub(1) else {
809        return Ok(filtered.contents);
810    };
811
812    // A filter layer holds both groups from its first pass to its last, so an
813    // open round still holding the other one is cut to hand it back — innermost
814    // first, and only until it comes back. A cut cannot take the group instead:
815    // with both held there is none for it to acquire.
816    let scratch = filtered.contents.opposite();
817    for index in (0..stack.len()).rev() {
818        if !pages.holds(scratch) {
819            break;
820        }
821        cut_at(stack, index, rounds, pages, caps, config)?;
822    }
823    let scratch = pages.acquire(scratch).ok_or_else(|| {
824        escalate(format!(
825            "filter layer {id} would need both of the {PING_PONG_GROUPS} page groups at once — \
826             one for its contents and one for its passes to write — and the second is holding a \
827             page a later round composites that no open round could be cut to hand back"
828        ))
829    })?;
830
831    let mut source = filtered.contents;
832    let mut dest = scratch;
833    for (index, step) in steps.iter().enumerate() {
834        // The page a pass reads is the page the pass before it wrote, so both
835        // stay live for the whole sequence. The scratch one goes back to the
836        // pool as the last pass ends; the one holding the result goes back when
837        // the parent's composite has sampled it.
838        let released = if index == last {
839            vec![source]
840        } else {
841            Vec::new()
842        };
843        for parity in &released {
844            pages.release(*parity);
845        }
846
847        rounds.push(Round {
848            // Cleared, never continued: a pass writes only the region its step
849            // names — a decimated one a quarter of the texels the pass before
850            // it did — and the kernels sample past that region without bounds
851            // checks, so what surrounds it has to be transparent rather than a
852            // previous holder's pixels.
853            target: RoundTarget::Page(PageTarget {
854                layer: id,
855                depth: filtered.depth,
856                parity: dest,
857                size: filtered.size,
858                bounds: filtered.bounds,
859                continued: false,
860            }),
861            ops: Vec::new(),
862            released,
863            filter: Some(FilterPass {
864                layer: id,
865                step: *step,
866                source,
867            }),
868        });
869
870        if index < last {
871            core::mem::swap(&mut source, &mut dest);
872        }
873    }
874
875    Ok(dest)
876}
877
878/// A regular layer wider than [`page_ceiling`] can size a single page to,
879/// ready for its column bands to be planned.
880struct BandedLayer<'a> {
881    /// The layer, indexed into
882    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
883    id: u32,
884    /// The recorded layer, for its opacity.
885    layer: &'a RecordedLayer,
886    /// The layer's tile-aligned device-space bounds — wider than
887    /// [`page_ceiling`], which is why it is here rather than sized by
888    /// [`page_size`] like every other regular layer.
889    bounds: RectU16,
890    /// The layer's depth counted over isolated layers only, one-based.
891    depth: usize,
892}
893
894/// Emits the rounds a banded layer's column pages render as: one page and
895/// one composite per [band](pages::page_bands), the layer's own draws
896/// replayed into each.
897///
898/// A band is rendered and composited one at a time rather than the whole split
899/// acquired up front, because a live page is scarce here: each band's composite
900/// is spliced straight onto `stack`'s own innermost accumulator (exactly where
901/// an ordinary single-page layer's composite would land), `make_room` is asked
902/// again before the next band's page is acquired, and the same cutting it
903/// already does for a wide sibling fan is what hands a finished band's page
904/// back — a banded layer is, from the pool's point of view, a fan of same-depth
905/// siblings that happen to share one layer id. See the [`pages`] module
906/// header's *A fourth decision* section for why this is a page decision rather
907/// than a new kind of target.
908///
909/// `ops` is `banded`'s stream's own accumulated ops — verified by the caller
910/// to hold no [`RoundOp::Composite`] — replayed unchanged into every band's
911/// content round; only the page each round targets and the rectangle its
912/// composite lands at differ band to band.
913///
914/// Replayed unchanged, and *clipped* on the way to the GPU: every band is
915/// offered the whole layer's draws, and each band's own
916/// [`PageTarget::bounds`] is what selects the strips that belong to it — a
917/// strip left of the band's column contributes no instance to it, and one
918/// straddling the column's edge is advanced into it rather than clamped onto
919/// it. That is what makes compositing the bands in order paint exactly what one
920/// whole-layer page would have; the clip itself is the renderer's, at instance
921/// emission, since the ops here are draw ranges rather than geometry.
922///
923/// A band takes a ping-pong group or nothing: the [spill
924/// page](PageParity::Spill) is deliberately not offered here. It exists for a
925/// layer whose own round samples a live page while an ancestor holds another,
926/// and a banded layer is reached only when its ops hold no composite at all —
927/// so the previous band's composite is always sitting in an open round
928/// `make_room` can cut, and a third page is never what a band is short of.
929///
930/// # Errors
931///
932/// Whatever [`pages::page_bands`] itself refuses `banded.bounds` with: taller
933/// than [`page_ceiling`] (bands are columns, so height has no split to be
934/// served by), or wider than [`pages::MAX_PAGE_BANDS`] bands can cover. Both
935/// are the refusal a single page gives an over-ceiling layer, unchanged by
936/// banding. [`EngineError::SchedulerEscalation`] when a band's own page
937/// cannot be found even one at a time.
938fn band_rounds(
939    banded: BandedLayer<'_>,
940    ops: Vec<RoundOp>,
941    stack: &mut [Stream<'_>],
942    rounds: &mut Vec<Round>,
943    pages: &mut LivePages,
944    caps: &TierCaps,
945    config: &PageConfig,
946) -> Result<Vec<RoundOp>, EngineError> {
947    let bands = pages::page_bands(banded.bounds, config, caps)?;
948
949    for band in bands {
950        make_room(stack, rounds, pages, caps, config)?;
951        // Acquired before anything of this band is released, never after —
952        // the same ordering a single-page layer's own acquisition keeps.
953        let parity = pages
954            .acquire(PageParity::from_depth(banded.depth))
955            .ok_or_else(|| {
956                escalate(format!(
957                    "layer {} would need a third live intermediate page to render one of its own \
958                     column bands: both of the {PING_PONG_GROUPS} groups the simple scheduler \
959                     ping-pongs between are already holding a page a later round composites, and \
960                     no open round could be cut to hand one back",
961                    banded.id
962                ))
963            })?;
964
965        rounds.push(Round {
966            target: RoundTarget::Page(PageTarget {
967                layer: banded.id,
968                depth: banded.depth,
969                parity,
970                size: band.size,
971                bounds: band.bounds,
972                // Every band is a page of its own rather than a continuation
973                // of the one before it: two bands never share a texture at
974                // once, so there is nothing here for a later band to load.
975                continued: false,
976            }),
977            ops: ops.clone(),
978            released: Vec::new(),
979            filter: None,
980        });
981
982        // Spliced onto the enclosing stream's own accumulator directly,
983        // rather than returned for the caller to append: a later band still
984        // has to acquire a group, and `make_room`'s own cutting is what finds
985        // this composite and hands the group back — exactly the mechanism a
986        // page-hungry sibling already relies on, so nothing here needs to
987        // wait for this function to return before a round can use it.
988        if let Some(parent) = stack.last_mut() {
989            parent.ops.push(RoundOp::Composite(Composite {
990                layer: banded.id,
991                parity,
992                bounds: band.bounds,
993                opacity: banded.layer.props.opacity,
994            }));
995        }
996    }
997
998    Ok(Vec::new())
999}
1000
1001/// Frees a page group for a layer about to take one, by cutting an ancestor's
1002/// round short, when the walk is running out of groups.
1003///
1004/// Two moves, in this order.
1005///
1006/// - **Ahead of the shortage.** An isolated ancestor that has not taken a page
1007///   yet is going to need one, and a layer taking the last free group would
1008///   leave it none — which is what used to limit a parent to two isolated
1009///   children. Cutting such an ancestor now gives it its page *and* hands back
1010///   every page that cut composites, so the group this layer is about to take
1011///   comes straight back and the count is no worse than it started. Outermost
1012///   first: an outer ancestor's cut is what frees the group an inner one takes.
1013/// - **At the shortage.** With both groups gone, the innermost open round that
1014///   composites anything is cut, releasing its pages a pass early. This is what
1015///   serves a fan of any width under the surface: the siblings already
1016///   composited go back to the pool before the next one is rendered.
1017///
1018/// A cut that cannot help is skipped rather than forced, and running out of
1019/// ancestors to cut is not itself an error — the acquire this precedes
1020/// escalates if it still finds no group, which keeps one refusal site and one
1021/// reason.
1022fn make_room(
1023    stack: &mut [Stream<'_>],
1024    rounds: &mut Vec<Round>,
1025    pages: &mut LivePages,
1026    caps: &TierCaps,
1027    config: &PageConfig,
1028) -> Result<(), EngineError> {
1029    if pages.free() > 1 {
1030        return Ok(());
1031    }
1032
1033    for index in 0..stack.len() {
1034        if pages.free() == 0 {
1035            break;
1036        }
1037        if stack[index].wants_a_page() {
1038            cut_at(stack, index, rounds, pages, caps, config)?;
1039        }
1040    }
1041
1042    if pages.free() == 0 {
1043        for index in (0..stack.len()).rev() {
1044            if cut_at(stack, index, rounds, pages, caps, config)? {
1045                break;
1046            }
1047        }
1048    }
1049
1050    Ok(())
1051}
1052
1053/// Emits what `stack[index]` has accumulated as a round of its own, so the pages
1054/// that batch composites return to the pool before the walk renders anything
1055/// else. Answers whether a round was emitted.
1056///
1057/// The round carries that stream's ops followed by those of every inlined stream
1058/// open above it: an inlined layer draws into its host's round, so its ops
1059/// belong to this cut, in exactly the painter order the recording laid them in.
1060/// Whatever any of them records after the cut becomes the host's next round,
1061/// which loads rather than clears (see [`PageTarget::continued`]) — so a cut
1062/// changes when a target's pixels are written, never which ones or in what
1063/// order.
1064///
1065/// `false` when there is nothing to gain or no way to emit: a batch holding no
1066/// composite frees no page by being cut, a suppressed stream renders nothing at
1067/// all, an inlined one has no target of its own, and an isolated one that has
1068/// not taken a page cannot take one with both groups held.
1069fn cut_at(
1070    stack: &mut [Stream<'_>],
1071    index: usize,
1072    rounds: &mut Vec<Round>,
1073    pages: &mut LivePages,
1074    caps: &TierCaps,
1075    config: &PageConfig,
1076) -> Result<bool, EngineError> {
1077    let Some(stream) = stack.get(index) else {
1078        return Ok(false);
1079    };
1080    if stream.suppressed || matches!(stream.owner, StreamOwner::Inline) {
1081        return Ok(false);
1082    }
1083
1084    // Every stream above this one up to the next stream with a target of its own
1085    // is inlined into this round.
1086    let end = stack
1087        .iter()
1088        .enumerate()
1089        .skip(index.saturating_add(1))
1090        .find(|(_, stream)| !matches!(stream.owner, StreamOwner::Inline))
1091        .map_or(stack.len(), |(at, _)| at);
1092    let batch = stack.get(index..end).unwrap_or(&[]);
1093    if !batch.iter().any(|stream| holds_composite(&stream.ops)) {
1094        return Ok(false);
1095    }
1096
1097    let continued = stack[index].emitted;
1098    let target = match stack[index].owner {
1099        // Both refused above; repeated here because the target is what the
1100        // round is built from.
1101        StreamOwner::Inline => return Ok(false),
1102        StreamOwner::Root => RoundTarget::Root,
1103        StreamOwner::Isolated { id, layer } => {
1104            let bounds = layer.bbox;
1105            // A filter layer's stream can itself be cut mid-frame — nothing
1106            // stops a regular/opacity layer from being recorded nested inside
1107            // one, and `make_room` treats every isolated stream on the stack
1108            // alike — so this has to size the page on the same terms `finish`
1109            // does above, or a cut filter layer's contents would land on an
1110            // unpadded page its own first pass then reads past the edge of.
1111            let size = match &layer.kind {
1112                RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
1113                _ => filter_page_size(bounds, config, caps)?,
1114            };
1115            let parity = match stack[index].page {
1116                Some(parity) => parity,
1117                None => {
1118                    let Some(parity) = pages.acquire(PageParity::from_depth(stack[index].depth))
1119                    else {
1120                        return Ok(false);
1121                    };
1122                    stack[index].page = Some(parity);
1123                    parity
1124                }
1125            };
1126            RoundTarget::Page(PageTarget {
1127                layer: id,
1128                depth: stack[index].depth,
1129                parity,
1130                size,
1131                bounds,
1132                continued,
1133            })
1134        }
1135    };
1136
1137    let mut ops: Vec<RoundOp> = Vec::new();
1138    for stream in stack.get_mut(index..end).unwrap_or(&mut []) {
1139        ops.append(&mut stream.ops);
1140    }
1141    let released = released_pages(&ops);
1142    for parity in &released {
1143        pages.release(*parity);
1144    }
1145    stack[index].emitted = true;
1146    rounds.push(Round {
1147        target,
1148        ops,
1149        released,
1150        filter: None,
1151    });
1152
1153    Ok(true)
1154}
1155
1156/// How a recorded layer is served.
1157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1158enum LayerRole {
1159    /// Composites identically to drawing its contents directly, so its stream
1160    /// is spliced into its parent's round.
1161    Inline,
1162    /// Needs a page and a pass of its own.
1163    Isolated,
1164    /// Needs a page and a pass of its own, plus a second page and a pass per
1165    /// step of its filter's sequence.
1166    ///
1167    /// Never inlined however it composites: filtering a layer's contents is not
1168    /// what drawing them into the parent does.
1169    Filtered,
1170    /// Contributes nothing, along with everything nested inside it.
1171    Dropped,
1172}
1173
1174/// What taking one node off the innermost open stream left the walk to do.
1175///
1176/// The step is decided under a borrow of the stack and acted on after it, since
1177/// entering a layer pushes a stream and finishing one pops it.
1178enum Step {
1179    /// The node entered a layer.
1180    Enter {
1181        /// The layer's index into
1182        /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1183        id: u32,
1184        /// The isolated nesting depth of the stream that entered it.
1185        depth: usize,
1186    },
1187    /// The node entered nothing; the stream continues.
1188    Next,
1189    /// The stream is exhausted and renders as its round now.
1190    Done,
1191}
1192
1193/// One command stream the walk has open: the frame root's own nodes, or one
1194/// recorded layer's.
1195///
1196/// The walk keeps these on an explicit stack rather than recursing, so a
1197/// recording nesting layers far past anything a widget tree records costs heap
1198/// rather than call frames. The stack is bounded by the recording's layer count,
1199/// because a layer is entered at most once.
1200struct Stream<'a> {
1201    /// Whose stream this is, and how it is served.
1202    owner: StreamOwner<'a>,
1203    /// The stream's nodes, in recording order.
1204    nodes: &'a [Node],
1205    /// The next node to visit.
1206    index: usize,
1207    /// The isolated nesting depth of this stream's contents, one-based; zero at
1208    /// the frame root.
1209    depth: usize,
1210    /// The ops accumulated since this stream's last round, in execution order.
1211    ops: Vec<RoundOp>,
1212    /// Whether a round of this stream's has been emitted already, which is what
1213    /// makes the next one a continuation of the same target.
1214    emitted: bool,
1215    /// The group this stream's page came from, taken at its first
1216    /// [cut](cut_at) and held until its last round. `None` while the stream is
1217    /// still free to have its page named by the round that finishes it, which
1218    /// is the common case — only a cut stream reserves ahead.
1219    page: Option<PageParity>,
1220    /// Whether this stream renders nothing at all: it is a layer covering no
1221    /// pixels, or nested inside one.
1222    suppressed: bool,
1223}
1224
1225impl<'a> Stream<'a> {
1226    /// The frame root's stream, which renders into no page.
1227    fn root(nodes: &'a [Node]) -> Self {
1228        Self {
1229            owner: StreamOwner::Root,
1230            nodes,
1231            index: 0,
1232            depth: 0,
1233            ops: Vec::new(),
1234            emitted: false,
1235            page: None,
1236            suppressed: false,
1237        }
1238    }
1239
1240    /// An inlined layer's stream, spliced into its parent's round at the depth
1241    /// its parent already occupies — an inlined layer takes no page, so it
1242    /// consumes no level (see the module header).
1243    fn inline(layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
1244        Self {
1245            owner: StreamOwner::Inline,
1246            nodes: &layer.nodes,
1247            index: 0,
1248            depth,
1249            ops: Vec::new(),
1250            emitted: false,
1251            page: None,
1252            suppressed,
1253        }
1254    }
1255
1256    /// An isolated layer's stream, one level deeper than its parent.
1257    fn isolated(id: u32, layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
1258        Self {
1259            owner: StreamOwner::Isolated { id, layer },
1260            nodes: &layer.nodes,
1261            index: 0,
1262            depth,
1263            ops: Vec::new(),
1264            emitted: false,
1265            page: None,
1266            suppressed,
1267        }
1268    }
1269
1270    /// Whether this stream is one a page group is still owed to.
1271    ///
1272    /// True only for an isolated layer that will really render — a suppressed
1273    /// one renders nothing — and only until its first cut names its group.
1274    fn wants_a_page(&self) -> bool {
1275        matches!(self.owner, StreamOwner::Isolated { .. })
1276            && !self.suppressed
1277            && self.page.is_none()
1278    }
1279}
1280
1281/// Whose stream a [`Stream`] walks, and so what finishing it produces.
1282enum StreamOwner<'a> {
1283    /// The frame's own surface. It occupies no page, which is what leaves both
1284    /// groups free for the layers directly under it.
1285    Root,
1286    /// A layer whose ops are spliced into its parent's round.
1287    Inline,
1288    /// A layer with a page and a round of its own.
1289    Isolated {
1290        /// The layer's index into
1291        /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1292        id: u32,
1293        /// The recorded layer, for its bounds and its opacity.
1294        layer: &'a RecordedLayer,
1295    },
1296}
1297
1298/// Which groups are holding a finished page.
1299///
1300/// A group holds one page at a time, so three booleans are this scheduler's
1301/// whole page allocator: acquiring is finding a group no round still needs,
1302/// releasing is the round that sampled a page ending. Nothing here allocates a
1303/// texture — the pool does that at execute time, keyed on the extent the round
1304/// carries.
1305///
1306/// The [ping-pong pair](PING_PONG_GROUPS) and the [spill page](Self::spill) are
1307/// kept apart deliberately. [`free`](Self::free) counts only the pair, so
1308/// [`make_room`]'s decisions — when to cut ahead of a shortage, when to cut at
1309/// one — are made against exactly the state they were tuned against and every
1310/// shape this scheduler already served schedules unchanged. The spill is only
1311/// ever reached through [`acquire_spill`](Self::acquire_spill), at the one site
1312/// that would otherwise refuse the frame.
1313#[derive(Debug, Default)]
1314struct LivePages {
1315    held: [bool; PING_PONG_GROUPS],
1316    /// Whether the one spill page is holding a page a later round composites.
1317    spill: bool,
1318}
1319
1320impl LivePages {
1321    /// The ping-pong group to render into, preferring `preferred` and falling
1322    /// back to the other, or `None` when both are holding a page a later round
1323    /// still composites.
1324    ///
1325    /// The preference is what keeps a chain alternating exactly as its depths'
1326    /// parities say; the fallback is what lets a second sibling, which shares
1327    /// its predecessor's depth and so its preference, take the free group
1328    /// instead of overwriting the page beside it. Never answers the spill page:
1329    /// that is [`acquire_spill`](Self::acquire_spill)'s, so the fallback to a
1330    /// third page is a decision a caller makes rather than one this hides.
1331    fn acquire(&mut self, preferred: PageParity) -> Option<PageParity> {
1332        let parity = if !self.holds(preferred) {
1333            preferred
1334        } else if !self.holds(preferred.opposite()) {
1335            preferred.opposite()
1336        } else {
1337            return None;
1338        };
1339
1340        self.set(parity, true);
1341        Some(parity)
1342    }
1343
1344    /// The spill page, or `None` when it is already holding one.
1345    ///
1346    /// The whole of the bound: one page, so a second layer wanting it while it
1347    /// is held finds nothing and the frame is refused exactly as it was before
1348    /// the spill existed.
1349    fn acquire_spill(&mut self) -> Option<PageParity> {
1350        if self.spill {
1351            return None;
1352        }
1353        self.set(PageParity::Spill, true);
1354        Some(PageParity::Spill)
1355    }
1356
1357    /// Hand a group's page back, once the round that sampled it has ended.
1358    fn release(&mut self, parity: PageParity) {
1359        self.set(parity, false);
1360    }
1361
1362    /// How many of the ping-pong groups are holding no page.
1363    ///
1364    /// The spill page is deliberately not counted: it is a last resort rather
1365    /// than a group in the rotation, and counting it would make [`make_room`]
1366    /// stop cutting one shortage early — which would change how every already
1367    /// served shape schedules.
1368    fn free(&self) -> usize {
1369        self.held.iter().filter(|held| !**held).count()
1370    }
1371
1372    /// Whether `parity`'s group is holding a page.
1373    fn holds(&self, parity: PageParity) -> bool {
1374        match parity {
1375            PageParity::Spill => self.spill,
1376            group => self.held.get(group.index()).copied().unwrap_or(false),
1377        }
1378    }
1379
1380    fn set(&mut self, parity: PageParity, held: bool) {
1381        match parity {
1382            PageParity::Spill => self.spill = held,
1383            group => {
1384                if let Some(slot) = self.held.get_mut(group.index()) {
1385                    *slot = held;
1386                }
1387            }
1388        }
1389    }
1390}
1391
1392/// How `layer` is served, refusing every property this scheduler cannot honour.
1393fn layer_role(id: u32, layer: &RecordedLayer) -> Result<LayerRole, EngineError> {
1394    if layer.props.blend_mode != BlendMode::default() {
1395        return Err(escalate(format!(
1396            "layer {id} composites with a non-default blend mode ({:?}), which has to read the \
1397             target it blends into",
1398            layer.props.blend_mode
1399        )));
1400    }
1401    if layer.props.mask.is_some() {
1402        return Err(escalate(format!(
1403            "layer {id} carries a layer mask, which needs an intermediate of its own"
1404        )));
1405    }
1406    if layer.props.clip_path.is_some() {
1407        return Err(escalate(format!(
1408            "layer {id} carries a layer clip path; frust lowers clips through the clip stack, not \
1409             through a layer"
1410        )));
1411    }
1412
1413    let opacity = layer.props.opacity;
1414    if !opacity.is_finite() {
1415        return Err(escalate(format!(
1416            "layer {id} carries a non-finite opacity ({opacity})"
1417        )));
1418    }
1419
1420    if opacity <= 0.0 {
1421        return Ok(LayerRole::Dropped);
1422    }
1423
1424    match &layer.kind {
1425        RecordedLayerKind::Regular => Ok(if opacity >= 1.0 {
1426            LayerRole::Inline
1427        } else {
1428            LayerRole::Isolated
1429        }),
1430        // The two filters the engine renders are a Gaussian blur and a
1431        // shadow-only drop shadow; every other filter a recording can carry is
1432        // refused here, by name, and the frame is skipped. A filtered layer is
1433        // never inlined, whatever its opacity.
1434        kind => {
1435            // Recognised here rather than at the filter round, so a refusal
1436            // names what was found before any round has been emitted and a
1437            // filter shape this engine cannot render is refused wherever it is
1438            // recorded — including inside a layer whose contents cover nothing.
1439            served_filter(id, kind).map_err(escalate)?;
1440            Ok(LayerRole::Filtered)
1441        }
1442    }
1443}
1444
1445/// Whether `ops` composites anything, and so whether emitting them as a round
1446/// would hand a page back to the pool.
1447fn holds_composite(ops: &[RoundOp]) -> bool {
1448    ops.iter().any(|op| matches!(op, RoundOp::Composite(_)))
1449}
1450
1451/// The pages `ops` samples, each listed once.
1452fn released_pages(ops: &[RoundOp]) -> Vec<PageParity> {
1453    let mut pages: Vec<PageParity> = Vec::new();
1454
1455    for op in ops {
1456        if let RoundOp::Composite(composite) = op
1457            && !pages.contains(&composite.parity)
1458        {
1459            pages.push(composite.parity);
1460        }
1461    }
1462
1463    pages
1464}
1465
1466/// Why layer `id` is refused when every page it could have rendered into is
1467/// holding one a later round still composites.
1468///
1469/// Two reasons, because two different bounds are reached. `spills` is whether
1470/// the layer was offered the [spill page](PageParity::Spill) — every regular
1471/// layer is — and so whether what ran out was all three of this scheduler's
1472/// live pages or only the ping-pong pair a filter layer is held to.
1473fn out_of_pages(id: u32, spills: bool) -> String {
1474    if spills {
1475        format!(
1476            "layer {id} would need a fourth live intermediate page: both of the \
1477             {PING_PONG_GROUPS} groups the simple scheduler ping-pongs between and the one spill \
1478             page beside them are already holding a page a later round composites, and no open \
1479             round could be cut to hand one back. A nested chain of any depth fits, a fan of \
1480             siblings of any width fits, and so does a chain hanging off an isolated ancestor's \
1481             later child; what does not is a layer whose own round samples two live pages while a \
1482             third is still owed to a round above it"
1483        )
1484    } else {
1485        format!(
1486            "filter layer {id} would need a third live intermediate page for its contents: both \
1487             of the {PING_PONG_GROUPS} groups the simple scheduler ping-pongs between are already \
1488             holding a page a later round composites, and no open round could be cut to hand one \
1489             back. The spill page a regular layer falls back on is not offered here — a filter \
1490             layer's passes ping-pong between the two groups themselves, so it is held to them"
1491        )
1492    }
1493}
1494
1495/// The escalation error carrying `reason`.
1496fn escalate(reason: impl Into<String>) -> EngineError {
1497    EngineError::SchedulerEscalation {
1498        reason: reason.into(),
1499    }
1500}