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//! The surface's clear and the frame's opaque strips are not passes of their
25//! own: the renderer draws them inside the pass of the first surface round that
26//! draws, which clears the surface instead of loading it (see
27//! [`crate::renderer`]'s *The frame's passes*), so a frame with no isolated
28//! layer and no hole punch is one render pass in all.
29//!
30//! ## What this scheduler serves, and what it refuses
31//!
32//! Frust's layer needs are narrow. Clips lower to a scissor rectangle or a
33//! coverage mask and never to an intermediate texture (see
34//! [`compile::clip`](crate::compile::clip)), so the overwhelming majority of
35//! frames contain no layer at all and schedule to a single round. Opacity
36//! layers exist but are rare and shallow.
37//!
38//! The scheduler serves every layer tree [`MAX_LIVE_PAGES`] pooled pages are
39//! enough for:
40//!
41//! - isolated layers nested at most [`MAX_CHAIN_DEPTH`] deep;
42//! - isolated layers *beside* each other under one parent, a fan of any width —
43//!   two widgets fading at once is the commonest sibling shape a frust screen
44//!   records, and a staggered list entrance fading five rows at once is served
45//!   on the same two pages;
46//! - a chain hanging off an isolated ancestor's later child, which is what a
47//!   navigation transition records for the whole of a scrub: a full-screen page
48//!   layer holding a chip that carries a translucent chip of its own. This is
49//!   the shape the [spill page](PageParity::Spill) exists for — see *The spill
50//!   page* below;
51//! - any mixture of the above that stays inside [`MAX_LIVE_PAGES`] live pages;
52//! - a Gaussian-blur or shadow-only drop-shadow [filter](crate::filters) layer
53//!   recorded directly under the frame's own surface, which takes *both*
54//!   ping-pong groups for the length of its pass sequence (see *Filter rounds*
55//!   below).
56//!
57//! A fan of any width fits because a parent does not have to composite *all* of
58//! its isolated children in one pass. When both groups are taken, the parent's
59//! round is [cut](cut_at): the ops accumulated so far are emitted as a round of
60//! their own, the pages that batch composites go back to the pool, and the next
61//! sibling is rendered into one of them. What a cut cannot pay for is a layer
62//! whose own round samples a live page while an isolated ancestor is holding
63//! another — three pages really are live at that point — and that is where the
64//! spill page comes in.
65//!
66//! Everything else — a layer that finds no page even after a cut and the spill,
67//! a deeper chain, a filter this engine does not render, a filter layer nested
68//! inside another recorded layer, a non-default blend mode, a layer mask or
69//! layer clip path — is refused with [`EngineError::SchedulerEscalation`]
70//! carrying a reason that names what was found.
71//!
72//! ## A refusal is a skipped frame, not a fallback
73//!
74//! Refusing is a return value and never a panic (E17), but it is not a route to
75//! a second renderer: the engine tier carries none, and which renderer draws a
76//! surface is decided once when the surface is configured, not per frame. What
77//! becomes of a refused frame is the caller's own contract. `frust-render`'s
78//! engine arm drops it — the acquired texture is released unpresented, the
79//! refusal is counted, and the reason is logged rate-limited — so nothing new
80//! reaches the screen and whatever was presented last stays there.
81//!
82//! A shape that refuses on *every* frame therefore freezes a surface rather
83//! than degrading it, which is the reason the refused set is kept to shapes
84//! frust's own widget tree does not record, and the reason a shape it does
85//! record (two fading siblings) is served here instead of escalated. Porting the
86//! reference renderer's general scheduler, which serves any scene at the cost of
87//! roughly ten times this module's code, is what the
88//! [`full-scheduler`](full) feature marks the site for.
89//!
90//! ## Bottom-up traversal and the two-page ping-pong
91//!
92//! Rounds are emitted contents-before-composite: a layer's contents must exist
93//! before the pass that composites them can run, so the deepest layer is
94//! rendered first and the surface's last round is last. A layer takes the group
95//! its depth's parity names when that group is free, and the other group when it
96//! is not (see [`PageParity`]).
97//!
98//! Parity alone is what bounds a *chain* to two live pages however deep it runs:
99//! while a parent renders into its own page it samples the child's, and the
100//! child's page returns to the pool the moment the parent's pass ends, so the
101//! grandchild reuses it. Siblings are what makes the fallback to the other group
102//! necessary — two layers at one depth share a parity, and a second page in one
103//! group would overwrite the first before the parent ever sampled it. So the
104//! second sibling takes the group the first left free, and a third finds both
105//! taken. That is what [`cut_at`] answers: the parent's round is closed early,
106//! its two composites are done, both pages come back, and the third sibling
107//! takes the group the first one had. A fan of any width costs one extra pass
108//! per pair rather than a refused frame.
109//!
110//! Pages are named lazily, by the round that renders into them, rather than
111//! reserved on the way down. An outer layer that reserved its page before the
112//! descent would hold one group for the whole traversal and defeat the
113//! ping-pong. The one exception is a layer whose round has to be cut: a cut
114//! renders into the layer's own page, so that layer takes its group at its first
115//! cut and keeps it until its last round. That is what makes a cut ancestor
116//! expensive for everything below it — from its first cut onwards it is holding
117//! one of the two groups, so a chain hanging off its later child has only one
118//! group left to ping-pong in.
119//!
120//! ## The spill page
121//!
122//! One page beside the pair ([`PageParity::Spill`]), taken only where the walk
123//! would otherwise refuse the frame: after [`make_room`] has cut everything it
124//! could and both groups are still holding pages later rounds composite, a
125//! *regular* layer takes the spill page instead of escalating. It is acquired
126//! and released exactly as a parity page is — it goes back to the pool the
127//! moment the round whose composite sampled it ends — so it is one more live
128//! intermediate at a peak, not a page held for the frame.
129//!
130//! It exists for one shape, and that shape is a navigation transition: a
131//! full-screen layer at a fractional opacity (the page being scrubbed) holding
132//! a chip beside a chip that carries a translucent chip of its own. The outer
133//! layer takes a group at the cut its first chip forces, the nested chip takes
134//! the other, and the chip hosting it — whose own round has to sample the
135//! nested page while the outer page is still owed upwards — has none. Because
136//! the opacity is fractional for the whole gesture, refusing it refuses *every*
137//! frame of the gesture: the surface holds its last presented image until the
138//! transition ends, which is a frozen scrub rather than a dropped frame (see *A
139//! refusal is a skipped frame* above). The identical content at the root
140//! schedules on two pages, which is what made the shape a transition-only
141//! defect.
142//!
143//! Bounded at one, deliberately. A second layer wanting the spill while it is
144//! held is refused exactly as before, which is what keeps a frame's live
145//! intermediates at [`MAX_LIVE_PAGES`] and keeps this a narrowing of the
146//! refused set rather than a step towards the general scheduler. Two callers do
147//! not reach for it at all: a [cut](cut_at) never takes it (a cut that cannot
148//! find a group is skipped, and the walk carries on to the acquire that can
149//! spill), and neither does a [filter layer](filter_rounds), whose passes
150//! ping-pong between two groups of their own and so are held to the pair.
151//!
152//! Everything the pair already served schedules exactly as it did:
153//! [`LivePages::free`] counts only the two groups, so the cutting decisions
154//! that serve fans and chains are made against the state they were tuned
155//! against, and the spill is reached only at the site that used to escalate.
156//!
157//! ## Inlined and dropped layers
158//!
159//! A layer only needs isolating when compositing it differs from drawing its
160//! contents directly. A recorded layer at full opacity with no blend, mask or
161//! clip composites source-over at alpha 1, which is precisely what drawing its
162//! contents into the parent does, so its stream is spliced into the parent's
163//! round and it costs no page and no pass. At the other end, a layer at zero
164//! opacity contributes nothing at all and is dropped along with everything
165//! nested inside it, and so does a layer whose contents cover no pixel at all —
166//! neither its own round nor any round its descendants would have rendered is
167//! emitted, because nothing would ever sample the pages they wrote.
168//!
169//! Depth is therefore counted over *isolated* layers only. Counting recorded
170//! depth instead would let an inlined layer push its isolated descendant onto
171//! the same parity as an isolated ancestor, putting two live pages in one group.
172//!
173//! ## Filter rounds
174//!
175//! A [filter](crate::filters) layer is an isolated layer whose page is not
176//! composited straight away: a sequence of filter passes runs over it first,
177//! and the parent composites what the last of them wrote. A pass reads a whole
178//! image and writes a whole image, and one render pass cannot do both to one
179//! texture, so each pass is a round of its own writing the group it did not
180//! read — the same ping-pong the chain uses, run between two pages of one
181//! layer instead of two layers. The sequence is arranged to be even in length
182//! (see [`crate::filters::blur::blur_passes`] and
183//! [`crate::filters::drop_shadow::drop_shadow_passes`]), so the result lands
184//! back in the page the layer's own contents were rendered into and the
185//! composite is the ordinary one.
186//!
187//! Every filter round clears its destination rather than loading it. A pass
188//! writes only the region its step names — a decimated one writes a quarter of
189//! the texels the pass before it did — and the kernels sample bilinearly with
190//! no bounds checks, so whatever surrounds the written region has to be
191//! transparent rather than a previous holder's pixels.
192//!
193//! Two consequences bound what filter shapes are served. The layer holds *both*
194//! groups from its first filter round to its last, so a filter layer is refused
195//! whenever the second group cannot be handed back to it; and it is refused
196//! outright when it is recorded inside another layer, because `vello_common`
197//! places a filter layer in its parent by undoing a source shift the reference
198//! renderer applies to a filter layer's contents and frust's compiler does not,
199//! which would size the parent's page from bounds short of the filter's own
200//! spread on two sides. Neither is a shape frust records: a backdrop blur is a
201//! layer under the surface.
202//!
203//! ## Retained pages
204//!
205//! A layer the renderer's retained-page store admitted
206//! ([`crate::cache::retained`]) is handed to [`Schedule::build_retained`] with
207//! a [`RetainedRole`]. A *recorded* body is walked like an isolated layer — its
208//! own nested translucent layers take pooled pages exactly as they would inside
209//! any layer — but its rounds target the store's page
210//! ([`RoundTarget::Retained`]) rather than a pooled one, it is never inlined at
211//! full opacity or dropped at zero (the page is wanted for later frames
212//! whatever this one composites), and its composite samples that page
213//! ([`Composite::retained`]). A *drawn* one is an empty layer standing at its
214//! painter-order position, which becomes the composite alone. Neither takes a
215//! ping-pong group or the spill page, and neither counts toward
216//! [`MAX_LIVE_PAGES`] or [`MAX_CHAIN_DEPTH`]: the store's page lives outside the
217//! pool for the store's own lifetime, so nothing about it competes for the
218//! frame's live intermediates. A retained round is cut like an isolated
219//! layer's when an ancestor needs room, and continues its own page after the
220//! cut.
221//!
222//! ## Backdrop rounds
223//!
224//! A backdrop blur ([`crate::filters`]' *The backdrop blur*) is handed to
225//! [`Schedule::build_frame`] as a recorded layer holding one draw. Where the
226//! walk is drawing into the frame's own target — the root, or layers inlined
227//! into it — it costs, in order: a **cut** of the surface round open so far
228//! (unconditional, so every op recorded before the backdrop lands before
229//! anything reads the target back, including the composite of a retained page
230//! drawn ahead of it), a **region copy** round ([`RegionCopy`]) seeding a page
231//! with the backdrop's region of the target, the blur's **pass rounds** over
232//! that page exactly as a filter layer's run, and then the backdrop's draw
233//! spliced into the root's next surface round, which samples the blurred page
234//! ([`BackdropSample`]) and releases it when it ends. The page is held across
235//! later ops like a pending composite is: a page-hungry layer recorded after
236//! the backdrop cuts that surface round to get it back.
237//!
238//! Anywhere else — inside an isolated layer, a retained body, or a layer
239//! covering nothing — there is no frame beneath the backdrop to sample, so its
240//! layer is dropped: no round, and no backdrop draw in any page round. A frame
241//! carrying no backdrop schedules exactly as [`Schedule::build_retained`] does.
242
243pub mod pages;
244
245pub use pages::{PageConfig, PageParity, PageSize, filter_page_size, page_ceiling, page_size};
246
247use core::ops::Range;
248
249use frust_gpu::TierCaps;
250use vello_common::geometry::{RectU16, SizeU16};
251use vello_common::peniko::BlendMode;
252use vello_common::record::{CommandRecorder, Node, RecordedLayer, RecordedLayerKind};
253
254use crate::compile::EngineDraw;
255use crate::error::EngineError;
256use crate::filters::{Backdrop, FilterStep, ServedFilter, blur, drop_shadow, served_filter};
257
258/// The deepest chain of nested isolated layers this scheduler serves.
259///
260/// Four levels covers every layer shape frust's widget set records; a deeper
261/// one is escalated rather than served — the frame is skipped, and shapes past
262/// this bound are rare enough that skipping them beats the cost and complexity
263/// of growing this module to serve them.
264pub const MAX_CHAIN_DEPTH: usize = 4;
265
266/// The page groups the depth-parity ping-pong alternates between.
267///
268/// Two is what the even/odd alternation guarantees for a chain of any depth,
269/// and what a fan of any width is served on by cutting its parent's round. It
270/// is the *pair*, not the frame's page budget — see [`MAX_LIVE_PAGES`].
271pub const PING_PONG_GROUPS: usize = 2;
272
273/// The most intermediate pages this scheduler keeps live at one time.
274///
275/// The [two ping-pong groups](PING_PONG_GROUPS) plus one: a single spill page
276/// ([`PageParity::Spill`]) for the shape that needs a third live page and
277/// cannot be cut into needing fewer. A shape needing a *fourth* — after cutting
278/// an open round has been tried and could not hand one back — is a shape this
279/// scheduler does not serve.
280pub const MAX_LIVE_PAGES: usize = PING_PONG_GROUPS + 1;
281
282/// Where the reference renderer's general scheduler is ported.
283///
284/// Deliberately empty. The scheduler this module implements covers the layer
285/// shapes frust actually records and escalates the rest; the general algorithm
286/// — an atlas per parity group, batching independent layers into shared rounds,
287/// filter pass plans, non-default blend through a scratch texture — is an order
288/// of magnitude more code and lands here, behind this feature, if and when a
289/// frame shape needs it. The feature exists now so the module path is settled
290/// and the escalation route above has a named destination.
291#[cfg(feature = "full-scheduler")]
292pub mod full {}
293
294/// One pooled intermediate page a round renders into.
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct PageTarget {
297    /// The layer this page holds, indexed into
298    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
299    pub layer: u32,
300    /// The layer's depth counted over isolated layers only, one-based.
301    pub depth: usize,
302    /// The group the page comes from — [`depth`](Self::depth)'s parity when
303    /// either of the pair was free, the other of the pair when it was not, and
304    /// the [spill page](PageParity::Spill) when neither was.
305    pub parity: PageParity,
306    /// The extent to acquire the page at.
307    pub size: PageSize,
308    /// The tile-aligned device-space bounds this page holds — the layer's own
309    /// for a layer that fits one page, and one column [band](pages::page_bands)
310    /// of them for a layer wider than any page is.
311    ///
312    /// The contents are rendered at the page's origin, so every strip drawn
313    /// into this round is offset by `-(bounds.x0, bounds.y0)` *and clipped to
314    /// this rectangle*: a banded layer's rounds are all handed the same ops, so
315    /// this is what selects the part of them each band actually holds. The
316    /// renderer's `PageWindow` is where both halves are applied.
317    pub bounds: RectU16,
318    /// Whether an earlier round of this same layer already rendered into this
319    /// page, so this one loads its contents instead of clearing them.
320    ///
321    /// A pooled page holds whatever its last holder left there, which is why a
322    /// layer's *first* round always clears it. A layer whose round was
323    /// [cut](cut_at) keeps the page across the cut — clearing again would wipe
324    /// the half already drawn — and no other layer can have taken the group in
325    /// between, because the cut is what made this layer the group's holder.
326    pub continued: bool,
327}
328
329/// What a round renders into.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub enum RoundTarget {
332    /// The frame's own surface. Draws land at their recorded coordinates.
333    Root,
334    /// A pooled intermediate page holding one isolated layer.
335    Page(PageTarget),
336    /// A retained page holding one recorded body — the store's own texture,
337    /// never a pooled one (see the module header's *Retained pages*).
338    Retained(RetainedTarget),
339}
340
341/// Compositing a finished page onto the round's target.
342///
343/// One instanced quad through the intermediate-strip pipeline: it samples
344/// `source()` of the page in [`parity`](Self::parity) and writes
345/// [`bounds`](Self::bounds), modulated by [`opacity`](Self::opacity). The page
346/// is free for reuse once the round holding this composite ends.
347#[derive(Debug, Clone, PartialEq)]
348pub struct Composite {
349    /// The layer being composited, indexed into
350    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
351    pub layer: u32,
352    /// The group holding the finished page.
353    pub parity: PageParity,
354    /// Where the layer lands in the round's target, in that target's own
355    /// coordinates.
356    pub bounds: RectU16,
357    /// Constant alpha applied while compositing, strictly between 0 and 1 for
358    /// a pooled page; a retained page's is the bracket's own alpha, which may
359    /// be 1.
360    pub opacity: f32,
361    /// The retained page this composite samples instead of a pooled one, or
362    /// `None` for a pooled page. When set, [`parity`](Self::parity) names no
363    /// page and nothing is released for it.
364    pub retained: Option<RetainedSource>,
365}
366
367impl Composite {
368    /// The region of the page to sample, in page texels.
369    ///
370    /// The layer was rendered at the page's origin, so this is
371    /// [`bounds`](Self::bounds) moved there — or, for a retained page drawn
372    /// trimmed to its window, moved to the texel its trimmed corner samples.
373    #[must_use]
374    pub fn source(&self) -> RectU16 {
375        let (x, y) = self.retained.map_or((0, 0), |source| source.origin);
376        RectU16::new(
377            x,
378            y,
379            x.saturating_add(self.bounds.width()),
380            y.saturating_add(self.bounds.height()),
381        )
382    }
383}
384
385/// Where a retained composite's page comes from.
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub struct RetainedSource {
388    /// The retained bracket the page belongs to, indexed into the frame's
389    /// [`CompiledFrame::retained`](crate::compile::CompiledFrame::retained) —
390    /// the renderer resolves it to the store's texture.
391    pub bracket: u32,
392    /// The page texel under the composite's top-left corner.
393    pub origin: (u16, u16),
394}
395
396/// A retained page a round renders a recorded body into (see the module
397/// header's *Retained pages*).
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub struct RetainedTarget {
400    /// The body's layer, indexed into
401    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
402    pub layer: u32,
403    /// The retained bracket the page belongs to (see
404    /// [`RetainedSource::bracket`]).
405    pub bracket: u32,
406    /// The body's tile-aligned device bounds, held at the page's origin.
407    pub bounds: RectU16,
408    /// Whether an earlier round of this body already rendered into the page
409    /// this frame, so this one loads it instead of clearing it.
410    pub continued: bool,
411}
412
413/// How the retained-page store resolved one recorded layer standing for a
414/// retained bracket.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum RetainedRole {
417    /// A body rendered into its retained page and composited from it.
418    Record {
419        /// The retained bracket (see [`RetainedSource::bracket`]).
420        bracket: u32,
421    },
422    /// A `DrawRetained` composited from a resident page.
423    Draw {
424        /// The retained bracket (see [`RetainedSource::bracket`]).
425        bracket: u32,
426        /// Where the composite lands.
427        dest: RectU16,
428        /// The page texel under `dest`'s top-left corner.
429        origin: (u16, u16),
430    },
431    /// A `DrawRetained` whose composite lands nowhere visible: nothing at all.
432    Hidden,
433}
434
435/// The [`RetainedRole`] of every recorded layer the retained-page store
436/// resolved, indexed by layer id; a layer it did not is scheduled like any
437/// other.
438#[derive(Debug, Clone, Default, PartialEq, Eq)]
439pub struct RetainedLayers {
440    roles: Vec<Option<RetainedRole>>,
441}
442
443impl RetainedLayers {
444    /// No layer resolved: [`Schedule::build_retained`] schedules exactly as
445    /// [`Schedule::build`] does.
446    #[must_use]
447    pub fn new() -> Self {
448        Self::default()
449    }
450
451    /// Forget every role, keeping the allocation.
452    pub fn clear(&mut self) {
453        self.roles.clear();
454    }
455
456    /// Give layer `layer` its role.
457    pub fn set(&mut self, layer: u32, role: RetainedRole) {
458        let index = layer as usize;
459        if self.roles.len() <= index {
460            self.roles.resize(index.saturating_add(1), None);
461        }
462        if let Some(slot) = self.roles.get_mut(index) {
463            *slot = Some(role);
464        }
465    }
466
467    /// Layer `layer`'s role, or `None` for an ordinary layer.
468    #[must_use]
469    pub fn role(&self, layer: u32) -> Option<RetainedRole> {
470        self.roles.get(layer as usize).copied().flatten()
471    }
472}
473
474/// One pass of a filter layer's sequence, reading one page and writing the
475/// other.
476///
477/// One instanced quad through the filter pipeline. The page it writes is the
478/// round's own target, so only the page it reads is named here; the extents it
479/// reads and writes are [`step`](Self::step)'s, both taken at their page's
480/// origin because a layer is always rendered there.
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub struct FilterPass {
483    /// The layer being filtered, indexed into
484    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
485    pub layer: u32,
486    /// Which pass of the filter's sequence this is, and at what extents.
487    pub step: FilterStep,
488    /// The group holding the page this pass reads.
489    pub source: PageParity,
490}
491
492/// One unit of work inside a round, executed in list order.
493#[derive(Debug, Clone, PartialEq)]
494pub enum RoundOp {
495    /// Draws to issue into the round's target, indexed into
496    /// [`CommandRecorder::draws`](vello_common::record::CommandRecorder::draws).
497    ///
498    /// Never empty: a node that recorded no draw contributes no op.
499    Draws(Range<u32>),
500    /// A finished page composited onto the round's target.
501    Composite(Composite),
502}
503
504/// One render pass over one target.
505///
506/// A target is not a round: the surface, and a layer's page, can each take
507/// several, and the rounds of one target run in the order they are listed with
508/// every one after the first loading what the one before it left. The
509/// surface's first round that draws is the one that clears it — the renderer
510/// folds the frame's clear and its opaque strips into that round's pass.
511#[derive(Debug, Clone, PartialEq)]
512pub struct Round {
513    /// What this pass renders into.
514    pub target: RoundTarget,
515    /// The pass's work, in execution order. Empty on a filter round, which
516    /// issues no draw and composites nothing.
517    pub ops: Vec<RoundOp>,
518    /// Pages whose contents this round consumed; they return to the pool once
519    /// it completes.
520    pub released: Vec<PageParity>,
521    /// The filter pass this round runs, on a filter round.
522    ///
523    /// A field rather than a [`RoundOp`] variant, and it stays one now that the
524    /// renderer executes filter rounds: a filter round is *only* a filter pass
525    /// — it draws nothing and composites nothing, and it is drawn through a
526    /// different pipeline, off a different instance buffer, with no viewport
527    /// uniform and no strip bind groups. Naming it here is what lets the frame
528    /// path branch on the round before it starts issuing a list, and keeps
529    /// [`RoundOp`] exactly the set of things one strip pass can interleave.
530    pub filter: Option<FilterPass>,
531    /// The region of the frame's own target this round seeds its page with,
532    /// on a region-copy round (see the module header's *Backdrop rounds*).
533    ///
534    /// A field rather than a [`RoundOp`] for the reason [`Self::filter`] is
535    /// one: a copy round is *only* a copy — it draws nothing, composites
536    /// nothing, and is recorded through no strip pipeline — and its `ops` are
537    /// empty.
538    pub copy: Option<RegionCopy>,
539    /// The pages this round's draws *sample* rather than composite: one per
540    /// backdrop blur whose rectangle this round draws, each holding the
541    /// blurred region (see the module header's *Backdrop rounds*). Every page
542    /// named here is also in [`Self::released`]. Empty on every round of a
543    /// frame with no backdrop, and on every round but a surface one.
544    pub samples: Vec<BackdropSample>,
545}
546
547/// A region of the frame's own target, copied into a round's page at the
548/// page's origin before anything else reads the page.
549///
550/// The reusable half of a backdrop blur: whatever needs to read what the frame
551/// has already painted — a blur of it, and next a blend composite against it —
552/// is handed a page holding exactly that region, made by cutting the surface
553/// round so everything before it in painter order has landed, then copying.
554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub struct RegionCopy {
556    /// The device-space region copied, which is also the page's own
557    /// [`PageTarget::bounds`]: the page holds it at its origin.
558    pub source: RectU16,
559}
560
561/// A finished page a surface round's draws sample at their own position — the
562/// blurred backdrop a backdrop blur's rectangle is painted from.
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub struct BackdropSample {
565    /// The backdrop's recorded layer, indexed into
566    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
567    pub layer: u32,
568    /// The group holding the page.
569    pub parity: PageParity,
570}
571
572impl Round {
573    /// The pooled page this round renders into, or `None` for the surface
574    /// and for a retained page.
575    #[must_use]
576    pub fn page(&self) -> Option<&PageTarget> {
577        match &self.target {
578            RoundTarget::Root | RoundTarget::Retained(_) => None,
579            RoundTarget::Page(page) => Some(page),
580        }
581    }
582
583    /// The retained page this round renders a body into, if it does.
584    #[must_use]
585    pub fn retained(&self) -> Option<&RetainedTarget> {
586        match &self.target {
587            RoundTarget::Retained(target) => Some(target),
588            RoundTarget::Root | RoundTarget::Page(_) => None,
589        }
590    }
591
592    /// The filter pass this round runs, or `None` on an ordinary round.
593    #[must_use]
594    pub fn filter_pass(&self) -> Option<&FilterPass> {
595        self.filter.as_ref()
596    }
597
598    /// The region this round copies into its page, or `None` on every round
599    /// but a region-copy one.
600    #[must_use]
601    pub fn region_copy(&self) -> Option<&RegionCopy> {
602        self.copy.as_ref()
603    }
604
605    /// Whether this round renders into the frame's own surface.
606    #[must_use]
607    pub fn is_root(&self) -> bool {
608        matches!(self.target, RoundTarget::Root)
609    }
610
611    /// The composites this round performs, in execution order.
612    pub fn composites(&self) -> impl Iterator<Item = &Composite> {
613        self.ops.iter().filter_map(|op| match op {
614            RoundOp::Composite(composite) => Some(composite),
615            RoundOp::Draws(_) => None,
616        })
617    }
618
619    /// How many recorded draws this round issues.
620    #[must_use]
621    pub fn draw_count(&self) -> u32 {
622        self.ops
623            .iter()
624            .map(|op| match op {
625                RoundOp::Draws(range) => range.end.saturating_sub(range.start),
626                RoundOp::Composite(_) => 0,
627            })
628            .sum()
629    }
630}
631
632/// Turns a recording into the rounds that render it.
633#[derive(Debug, Clone, Copy, PartialEq, Eq)]
634pub struct Schedule;
635
636impl Schedule {
637    /// The rounds `recorder` renders as: every layer's rounds before the round
638    /// that composites it, and the surface's last round last.
639    ///
640    /// Always at least one round: a recording with no layers, and one whose
641    /// layers all inline or drop away, both schedule to the single root round
642    /// that clears and paints the surface.
643    ///
644    /// # Errors
645    ///
646    /// [`EngineError::SchedulerEscalation`] for a layer shape outside what this
647    /// scheduler serves (see the module header), with a reason naming what was
648    /// found, and [`EngineError::IntermediateTextureTooLarge`] for a layer
649    /// larger than a page can be sized to. Neither is a panic, and neither
650    /// routes the frame anywhere: both hand the caller a frame it is expected
651    /// to skip.
652    ///
653    /// A Gaussian-blur or shadow-only drop-shadow filter layer is *planned*
654    /// here: its contents' round, then one round per pass of its filter's
655    /// sequence, then the composite of whichever page the last pass wrote.
656    /// Every other filter is refused by name (see
657    /// [`crate::filters::served_filter`]), and so are the two shapes a served
658    /// filter layer still cannot take — one recorded inside another layer, and
659    /// one that cannot be handed the second page group its passes ping-pong
660    /// into.
661    ///
662    /// One entry point, not two. While the renderer had no filter pipeline,
663    /// this call refused every filter and a second one planned them, so the
664    /// frame path could not reach a round nothing could execute; the renderer
665    /// runs them now ([`crate::renderer::FilterResources`]), and the split went
666    /// with the reason for it.
667    pub fn build(
668        recorder: &CommandRecorder<EngineDraw>,
669        caps: &TierCaps,
670        config: &PageConfig,
671    ) -> Result<Vec<Round>, EngineError> {
672        Self::build_retained(recorder, caps, config, &RetainedLayers::new())
673    }
674
675    /// [`Self::build`], with the layers the retained-page store resolved
676    /// served as `retained` says (see the module header's *Retained pages*).
677    ///
678    /// # Errors
679    ///
680    /// Exactly [`Self::build`]'s. A retained body is held to the same layer
681    /// properties every layer is (no blend mode, mask or clip path, a finite
682    /// opacity) and its nested layers to every bound; the body itself takes no
683    /// page group, so it can never be the layer a shortage refuses.
684    pub fn build_retained(
685        recorder: &CommandRecorder<EngineDraw>,
686        caps: &TierCaps,
687        config: &PageConfig,
688        retained: &RetainedLayers,
689    ) -> Result<Vec<Round>, EngineError> {
690        Self::build_frame(recorder, caps, config, retained, &[])
691    }
692
693    /// [`Self::build_retained`], with the frame's backdrop blurs served as the
694    /// module header's *Backdrop rounds* describes. With no backdrop this is
695    /// exactly [`Self::build_retained`]: nothing is cut, copied or sampled.
696    ///
697    /// # Errors
698    ///
699    /// Exactly [`Self::build`]'s. A backdrop's page is sized by
700    /// [`filter_page_size`] like a filter layer's, so a region larger than a
701    /// page can be is refused with
702    /// [`EngineError::IntermediateTextureTooLarge`] on the same terms.
703    pub fn build_frame(
704        recorder: &CommandRecorder<EngineDraw>,
705        caps: &TierCaps,
706        config: &PageConfig,
707        retained: &RetainedLayers,
708        backdrops: &[Backdrop],
709    ) -> Result<Vec<Round>, EngineError> {
710        let mut rounds: Vec<Round> = Vec::new();
711        let mut pages = LivePages::default();
712        // A child layer is always recorded after its parent, so a well-formed
713        // recording enters each layer exactly once. Marking them is what keeps
714        // a recording whose nodes re-enter one from being walked forever, and
715        // it bounds the stack below to one entry per recorded layer.
716        let mut entered = vec![false; recorder.layers.len()];
717        let mut stack = vec![Stream::root(&recorder.nodes)];
718
719        while !stack.is_empty() {
720            // Taking one node off the innermost open stream is a borrow of the
721            // stack that has to end before the step below can push or pop it.
722            let step = match stack.last_mut() {
723                None => break,
724                Some(stream) => match stream.nodes.get(stream.index) {
725                    None => Step::Done,
726                    Some(node) => {
727                        stream.index = stream.index.saturating_add(1);
728                        if node.draws.start < node.draws.end {
729                            stream.ops.push(RoundOp::Draws(node.draws.clone()));
730                        }
731                        match node.layer {
732                            None => Step::Next,
733                            Some(id) => Step::Enter {
734                                id,
735                                depth: stream.depth,
736                            },
737                        }
738                    }
739                },
740            };
741
742            match step {
743                Step::Next => {}
744                Step::Done => {
745                    if let Some(stream) = stack.pop() {
746                        let carry =
747                            finish(stream, &mut stack, &mut rounds, &mut pages, caps, config)?;
748                        if let Some(parent) = stack.last_mut() {
749                            parent.ops.extend(carry);
750                        }
751                    }
752                }
753                Step::Enter { id, depth } => {
754                    let index = id as usize;
755                    let (Some(layer), Some(seen)) =
756                        (recorder.layers.get(index), entered.get_mut(index))
757                    else {
758                        return Err(escalate(format!(
759                            "a node enters layer {id}, which the recording does not hold"
760                        )));
761                    };
762                    if *seen {
763                        return Err(escalate(format!(
764                            "layer {id} is entered a second time; the simple scheduler walks \
765                             each recorded layer once, and a recording whose nodes re-enter one \
766                             describes no tree it could render"
767                        )));
768                    }
769                    *seen = true;
770
771                    // Suppression is inherited rather than re-derived: a layer
772                    // inside one covering no pixels covers none either, however
773                    // its own bounds read.
774                    let inherited = stack.last().is_some_and(|stream| stream.suppressed);
775
776                    if let Some(backdrop) = backdrops.iter().find(|backdrop| backdrop.layer == id) {
777                        // The layer properties every layer is held to; a
778                        // backdrop's own are always the inlining ones.
779                        layer_role(id, layer)?;
780                        // Served only where the frame's own target is what the
781                        // walk is drawing into — the root, or layers inlined
782                        // into it. Inside an isolated layer (or a retained
783                        // body, or one covering nothing) there is no frame
784                        // beneath to sample: the backdrop and its rectangle
785                        // draw nothing, and never land in a page round.
786                        let at_root = stack
787                            .iter()
788                            .rev()
789                            .find(|stream| !matches!(stream.owner, StreamOwner::Inline))
790                            .is_some_and(|stream| matches!(stream.owner, StreamOwner::Root));
791                        if at_root && !inherited && !layer.bbox.is_empty() {
792                            backdrop_rounds(
793                                backdrop,
794                                &mut stack,
795                                &mut rounds,
796                                &mut pages,
797                                caps,
798                                config,
799                            )?;
800                            stack.push(Stream::inline(layer, depth, false));
801                        }
802                        continue;
803                    }
804
805                    if let Some(role) = retained.role(id) {
806                        // The layer properties every layer is held to; the
807                        // isolation answer is the store's, not opacity's.
808                        layer_role(id, layer)?;
809                        match role {
810                            RetainedRole::Record { bracket } => {
811                                stack.push(Stream::retained(id, layer, bracket, depth, inherited))
812                            }
813                            RetainedRole::Draw {
814                                bracket,
815                                dest,
816                                origin,
817                            } => {
818                                if !inherited
819                                    && layer.props.opacity > 0.0
820                                    && let Some(stream) = stack.last_mut()
821                                {
822                                    stream.ops.push(RoundOp::Composite(Composite {
823                                        layer: id,
824                                        parity: PageParity::Even,
825                                        bounds: dest,
826                                        opacity: layer.props.opacity,
827                                        retained: Some(RetainedSource { bracket, origin }),
828                                    }));
829                                }
830                            }
831                            RetainedRole::Hidden => {}
832                        }
833                        continue;
834                    }
835
836                    // Nothing nested inside a dropped layer is rendered, so
837                    // nothing inside one is validated either — the walk does
838                    // not enter it.
839                    let role = layer_role(id, layer)?;
840                    // A filter layer's placement in its parent is computed by
841                    // undoing a shift the reference renderer applies to a
842                    // filter layer's contents and frust's compiler does not, so
843                    // the bounds it hands its parent fall short of the filter's
844                    // own spread on two sides. Refused rather than rendered
845                    // from bounds that would clip it; a backdrop blur is
846                    // recorded under the surface, where nothing reads that
847                    // placement.
848                    if matches!(role, LayerRole::Filtered) && stack.len() > 1 {
849                        return Err(escalate(format!(
850                            "filter layer {id} is recorded inside another layer, whose own bounds \
851                             would be taken from a placement that undoes a source shift this \
852                             engine's compiler never applied; a filter layer is served directly \
853                             under the frame's own surface"
854                        )));
855                    }
856
857                    match role {
858                        LayerRole::Dropped => {}
859                        LayerRole::Inline => stack.push(Stream::inline(layer, depth, inherited)),
860                        LayerRole::Isolated | LayerRole::Filtered => {
861                            let depth = depth.saturating_add(1);
862                            if depth > MAX_CHAIN_DEPTH {
863                                return Err(escalate(format!(
864                                    "{depth} nested isolated layers, deeper than the \
865                                     {MAX_CHAIN_DEPTH}-deep chain the simple scheduler serves"
866                                )));
867                            }
868                            // A layer whose contents cover nothing composites
869                            // nothing, so it renders no round — and neither do
870                            // its descendants, whose pages nothing would ever
871                            // sample. The walk still enters it, so a shape this
872                            // scheduler refuses is refused wherever it is
873                            // recorded rather than only where it is visible.
874                            let suppressed = inherited || layer.bbox.is_empty();
875                            stack.push(Stream::isolated(id, layer, depth, suppressed));
876                        }
877                    }
878                }
879            }
880        }
881
882        Ok(rounds)
883    }
884}
885
886/// Emits the last round a finished stream renders as, and returns what its
887/// parent splices in at the node that entered it: one composite for an isolated
888/// layer, the layer's own ops for an inlined one, nothing for a layer that
889/// composites nothing. The frame root pushes the last round and carries nothing.
890///
891/// `stack` is the walk's remaining streams — this stream's ancestors — because
892/// a layer short of a page makes room by [cutting](cut_at) one of their rounds.
893fn finish<'a>(
894    stream: Stream<'a>,
895    stack: &mut [Stream<'a>],
896    rounds: &mut Vec<Round>,
897    pages: &mut LivePages,
898    caps: &TierCaps,
899    config: &PageConfig,
900) -> Result<Vec<RoundOp>, EngineError> {
901    let (id, layer) = match stream.owner {
902        StreamOwner::Inline => return Ok(stream.ops),
903        StreamOwner::Root => {
904            let mut released = released_pages(&stream.ops);
905            release_samples(&stream.samples, &mut released);
906            for parity in &released {
907                pages.release(*parity);
908            }
909            // An empty trailing round is worth a pass only when it is the
910            // frame's only one: `build` always returns at least one round, but
911            // a surface already written by an earlier cut needs no empty pass
912            // behind it.
913            if !stream.ops.is_empty() || !stream.emitted {
914                rounds.push(Round {
915                    target: RoundTarget::Root,
916                    ops: stream.ops,
917                    released,
918                    filter: None,
919                    copy: None,
920                    samples: stream.samples,
921                });
922            }
923            return Ok(Vec::new());
924        }
925        StreamOwner::Retained { id, layer, bracket } => {
926            return Ok(finish_retained(stream, id, layer, bracket, rounds, pages));
927        }
928        StreamOwner::Isolated { id, layer } => (id, layer),
929    };
930
931    // A layer covering no pixels, or one nested inside such a layer, renders
932    // nothing: no round of its own was cut, none of its descendants' was
933    // emitted, and no page was ever taken for any of them, so there is nothing
934    // to release and nothing for the parent to composite.
935    if stream.suppressed {
936        return Ok(Vec::new());
937    }
938
939    let bounds = layer.bbox;
940    let depth = stream.depth;
941
942    // A regular layer wider than any single page is banded into column
943    // pages (E14) rather than refused — but only while nothing has claimed
944    // its page ahead of time (a cut ancestor reserves one this way, see
945    // `cut_at`) and its own accumulated ops hold no composite of a nested
946    // isolated child: a band's ops are replayed once per band, and
947    // replaying a child's composite would read a page a later band has
948    // already reused. Both are shapes banding does not reach, and a layer
949    // whose height alone exceeds the ceiling is still refused by `page_size`
950    // below exactly as it always was — bands are columns, so height has no
951    // split to be served by.
952    if matches!(layer.kind, RecordedLayerKind::Regular)
953        && stream.page.is_none()
954        && u32::from(bounds.width()) > page_ceiling(config, caps)
955        && !holds_composite(&stream.ops)
956    {
957        return band_rounds(
958            BandedLayer {
959                id,
960                layer,
961                bounds,
962                depth,
963            },
964            stream.ops,
965            stack,
966            rounds,
967            pages,
968            caps,
969            config,
970        );
971    }
972
973    // A filter layer's page is sized with the extra atlas margin
974    // (`filter_page_size`) its kernel taps can reach past: both this
975    // contents round and every filter-pass round the layer costs share this
976    // one `size` (see `filter_rounds`'s own `filtered.size`), so the margin
977    // has to be reserved here, before the first byte of the layer is ever
978    // rendered — sizing only the pass rounds would leave the contents
979    // themselves on an unpadded page while the first pass reads past its
980    // edge. A regular layer never filters its contents, so it keeps the
981    // plain `page_size` this always used.
982    let size = match &layer.kind {
983        RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
984        _ => filter_page_size(bounds, config, caps)?,
985    };
986    let page = |parity, continued| {
987        RoundTarget::Page(PageTarget {
988            layer: id,
989            depth,
990            parity,
991            size,
992            bounds,
993            continued,
994        })
995    };
996
997    // The group holding the layer's own contents once its last content round
998    // has been emitted.
999    let contents = match stream.page {
1000        // The layer's round was cut earlier this frame, so its page is already
1001        // this stream's and this round loads what the cut left in it.
1002        Some(parity) => {
1003            if !stream.ops.is_empty() {
1004                let released = released_pages(&stream.ops);
1005                for parity in &released {
1006                    pages.release(*parity);
1007                }
1008                rounds.push(Round {
1009                    target: page(parity, true),
1010                    ops: stream.ops,
1011                    released,
1012                    filter: None,
1013                    copy: None,
1014                    samples: Vec::new(),
1015                });
1016            }
1017            parity
1018        }
1019        None => {
1020            make_room(stack, rounds, pages, caps, config)?;
1021            // Acquired before the children's pages are freed, never after: a
1022            // child's page is live for the whole of the pass that samples it,
1023            // so the group this round renders into can never be one of theirs.
1024            // With both groups still held after `make_room` has cut everything
1025            // it could, a regular layer takes the one spill page rather than
1026            // refusing the frame — see the module header's *The spill page*.
1027            let spills = matches!(layer.kind, RecordedLayerKind::Regular);
1028            let parity = pages
1029                .acquire(PageParity::from_depth(depth))
1030                .or_else(|| spills.then(|| pages.acquire_spill()).flatten())
1031                .ok_or_else(|| escalate(out_of_pages(id, spills)))?;
1032            let released = released_pages(&stream.ops);
1033            for parity in &released {
1034                pages.release(*parity);
1035            }
1036            rounds.push(Round {
1037                target: page(parity, false),
1038                ops: stream.ops,
1039                released,
1040                filter: None,
1041                copy: None,
1042                samples: Vec::new(),
1043            });
1044            parity
1045        }
1046    };
1047
1048    // A filter layer's contents are not what the parent composites: the filter
1049    // passes run over them first, and the parent samples whatever the last of
1050    // them wrote.
1051    let composited = match &layer.kind {
1052        RecordedLayerKind::Regular => contents,
1053        kind => filter_rounds(
1054            &FilterLayer {
1055                id,
1056                kind,
1057                depth,
1058                bounds,
1059                size,
1060                contents,
1061            },
1062            stack,
1063            rounds,
1064            pages,
1065            caps,
1066            config,
1067        )?,
1068    };
1069
1070    Ok(vec![RoundOp::Composite(Composite {
1071        layer: id,
1072        parity: composited,
1073        bounds,
1074        opacity: layer.props.opacity,
1075        retained: None,
1076    })])
1077}
1078
1079/// Emits the last round a retained body renders as — into the store's page,
1080/// never a pooled one — and returns the composite of that page for the parent
1081/// to splice in (nothing at zero opacity: the page is still wanted, but this
1082/// frame shows none of it).
1083///
1084/// A body that renders anything renders at least one round even when it is
1085/// empty of ops: that round is what clears the page, and a page the store
1086/// believes rendered must hold this frame's body rather than an earlier one.
1087/// A body nested inside a layer covering nothing renders no round at all, and
1088/// the store, seeing none, leaves its key as it was.
1089fn finish_retained(
1090    stream: Stream<'_>,
1091    id: u32,
1092    layer: &RecordedLayer,
1093    bracket: u32,
1094    rounds: &mut Vec<Round>,
1095    pages: &mut LivePages,
1096) -> Vec<RoundOp> {
1097    if stream.suppressed {
1098        return Vec::new();
1099    }
1100    let released = released_pages(&stream.ops);
1101    for parity in &released {
1102        pages.release(*parity);
1103    }
1104    if !stream.ops.is_empty() || !stream.emitted {
1105        rounds.push(Round {
1106            target: RoundTarget::Retained(RetainedTarget {
1107                layer: id,
1108                bracket,
1109                bounds: layer.bbox,
1110                continued: stream.emitted,
1111            }),
1112            ops: stream.ops,
1113            released,
1114            filter: None,
1115            copy: None,
1116            samples: Vec::new(),
1117        });
1118    }
1119
1120    let opacity = layer.props.opacity;
1121    if opacity > 0.0 {
1122        vec![RoundOp::Composite(Composite {
1123            layer: id,
1124            parity: PageParity::Even,
1125            bounds: layer.bbox,
1126            opacity,
1127            retained: Some(RetainedSource {
1128                bracket,
1129                origin: (0, 0),
1130            }),
1131        })]
1132    } else {
1133        Vec::new()
1134    }
1135}
1136
1137/// A filter layer whose contents are rendered, ready for its passes to be
1138/// planned.
1139struct FilterLayer<'a> {
1140    /// The layer, indexed into
1141    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1142    id: u32,
1143    /// The recorded filter, re-read here rather than carried down from
1144    /// [`layer_role`]: preparing a blur's kernel is a handful of arithmetic
1145    /// over a fixed-size array, and threading it through the walk's stack would
1146    /// put a filter's state in every stream that has none.
1147    kind: &'a RecordedLayerKind,
1148    /// The layer's depth counted over isolated layers only, one-based.
1149    depth: usize,
1150    /// The layer's tile-aligned device-space bounds, already grown by the
1151    /// filter's own spread (see [`crate::filters::LayerFilter::filter_data`]).
1152    bounds: RectU16,
1153    /// The extent both of the layer's pages are acquired at.
1154    size: PageSize,
1155    /// The group the layer's own contents were rendered into.
1156    contents: PageParity,
1157}
1158
1159/// Emits the rounds a filter layer's pass sequence renders as, and answers the
1160/// group holding the filtered result for its parent to composite.
1161///
1162/// One round per pass, each writing the group it did not read. The sequence is
1163/// even in length, so the result lands back in the page the contents were
1164/// rendered into; [`blur::blur_passes`] is what keeps that true.
1165fn filter_rounds(
1166    filtered: &FilterLayer<'_>,
1167    stack: &mut [Stream<'_>],
1168    rounds: &mut Vec<Round>,
1169    pages: &mut LivePages,
1170    caps: &TierCaps,
1171    config: &PageConfig,
1172) -> Result<PageParity, EngineError> {
1173    let id = filtered.id;
1174    // The one dispatch every filter-recognising site in this crate shares
1175    // (`renderer::filter_block`, `layer_role` below): the recorded primitive
1176    // names which filter it is, and the reason a refusal carries is that
1177    // primitive's own.
1178    let steps = match served_filter(id, filtered.kind).map_err(escalate)? {
1179        ServedFilter::Blur(blur) => blur::blur_passes(&blur, SizeU16::from(filtered.bounds)),
1180        ServedFilter::DropShadow(shadow) => {
1181            drop_shadow::drop_shadow_passes(&shadow, SizeU16::from(filtered.bounds))
1182        }
1183    };
1184    pass_rounds(filtered, &steps, stack, rounds, pages, caps, config)
1185}
1186
1187/// Emits one round per pass of `steps` over `filtered`'s pages, and answers
1188/// the group holding the result — [`filter_rounds`]' own sequence, shared with
1189/// a backdrop blur's ([`backdrop_rounds`]), whose page is seeded by a region
1190/// copy instead of by rendered contents.
1191fn pass_rounds(
1192    filtered: &FilterLayer<'_>,
1193    steps: &[FilterStep],
1194    stack: &mut [Stream<'_>],
1195    rounds: &mut Vec<Round>,
1196    pages: &mut LivePages,
1197    caps: &TierCaps,
1198    config: &PageConfig,
1199) -> Result<PageParity, EngineError> {
1200    let id = filtered.id;
1201    let Some(last) = steps.len().checked_sub(1) else {
1202        return Ok(filtered.contents);
1203    };
1204
1205    // A filter layer holds both groups from its first pass to its last, so an
1206    // open round still holding the other one is cut to hand it back — innermost
1207    // first, and only until it comes back. A cut cannot take the group instead:
1208    // with both held there is none for it to acquire.
1209    let scratch = filtered.contents.opposite();
1210    for index in (0..stack.len()).rev() {
1211        if !pages.holds(scratch) {
1212            break;
1213        }
1214        cut_at(stack, index, rounds, pages, caps, config)?;
1215    }
1216    let scratch = pages.acquire(scratch).ok_or_else(|| {
1217        escalate(format!(
1218            "filter layer {id} would need both of the {PING_PONG_GROUPS} page groups at once — \
1219             one for its contents and one for its passes to write — and the second is holding a \
1220             page a later round composites that no open round could be cut to hand back"
1221        ))
1222    })?;
1223
1224    let mut source = filtered.contents;
1225    let mut dest = scratch;
1226    for (index, step) in steps.iter().enumerate() {
1227        // The page a pass reads is the page the pass before it wrote, so both
1228        // stay live for the whole sequence. The scratch one goes back to the
1229        // pool as the last pass ends; the one holding the result goes back when
1230        // the parent's composite has sampled it.
1231        let released = if index == last {
1232            vec![source]
1233        } else {
1234            Vec::new()
1235        };
1236        for parity in &released {
1237            pages.release(*parity);
1238        }
1239
1240        rounds.push(Round {
1241            // Cleared, never continued: a pass writes only the region its step
1242            // names — a decimated one a quarter of the texels the pass before
1243            // it did — and the kernels sample past that region without bounds
1244            // checks, so what surrounds it has to be transparent rather than a
1245            // previous holder's pixels.
1246            target: RoundTarget::Page(PageTarget {
1247                layer: id,
1248                depth: filtered.depth,
1249                parity: dest,
1250                size: filtered.size,
1251                bounds: filtered.bounds,
1252                continued: false,
1253            }),
1254            ops: Vec::new(),
1255            released,
1256            filter: Some(FilterPass {
1257                layer: id,
1258                step: *step,
1259                source,
1260            }),
1261            copy: None,
1262            samples: Vec::new(),
1263        });
1264
1265        if index < last {
1266            core::mem::swap(&mut source, &mut dest);
1267        }
1268    }
1269
1270    Ok(dest)
1271}
1272
1273/// A regular layer wider than [`page_ceiling`] can size a single page to,
1274/// ready for its column bands to be planned.
1275struct BandedLayer<'a> {
1276    /// The layer, indexed into
1277    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1278    id: u32,
1279    /// The recorded layer, for its opacity.
1280    layer: &'a RecordedLayer,
1281    /// The layer's tile-aligned device-space bounds — wider than
1282    /// [`page_ceiling`], which is why it is here rather than sized by
1283    /// [`page_size`] like every other regular layer.
1284    bounds: RectU16,
1285    /// The layer's depth counted over isolated layers only, one-based.
1286    depth: usize,
1287}
1288
1289/// Emits the rounds a banded layer's column pages render as: one page and
1290/// one composite per [band](pages::page_bands), the layer's own draws
1291/// replayed into each.
1292///
1293/// A band is rendered and composited one at a time rather than the whole split
1294/// acquired up front, because a live page is scarce here: each band's composite
1295/// is spliced straight onto `stack`'s own innermost accumulator (exactly where
1296/// an ordinary single-page layer's composite would land), `make_room` is asked
1297/// again before the next band's page is acquired, and the same cutting it
1298/// already does for a wide sibling fan is what hands a finished band's page
1299/// back — a banded layer is, from the pool's point of view, a fan of same-depth
1300/// siblings that happen to share one layer id. See the [`pages`] module
1301/// header's *A fourth decision* section for why this is a page decision rather
1302/// than a new kind of target.
1303///
1304/// `ops` is `banded`'s stream's own accumulated ops — verified by the caller
1305/// to hold no [`RoundOp::Composite`] — replayed unchanged into every band's
1306/// content round; only the page each round targets and the rectangle its
1307/// composite lands at differ band to band.
1308///
1309/// Replayed unchanged, and *clipped* on the way to the GPU: every band is
1310/// offered the whole layer's draws, and each band's own
1311/// [`PageTarget::bounds`] is what selects the strips that belong to it — a
1312/// strip left of the band's column contributes no instance to it, and one
1313/// straddling the column's edge is advanced into it rather than clamped onto
1314/// it. That is what makes compositing the bands in order paint exactly what one
1315/// whole-layer page would have; the clip itself is the renderer's, at instance
1316/// emission, since the ops here are draw ranges rather than geometry.
1317///
1318/// A band takes a ping-pong group or nothing: the [spill
1319/// page](PageParity::Spill) is deliberately not offered here. It exists for a
1320/// layer whose own round samples a live page while an ancestor holds another,
1321/// and a banded layer is reached only when its ops hold no composite at all —
1322/// so the previous band's composite is always sitting in an open round
1323/// `make_room` can cut, and a third page is never what a band is short of.
1324///
1325/// # Errors
1326///
1327/// Whatever [`pages::page_bands`] itself refuses `banded.bounds` with: taller
1328/// than [`page_ceiling`] (bands are columns, so height has no split to be
1329/// served by), or wider than [`pages::MAX_PAGE_BANDS`] bands can cover. Both
1330/// are the refusal a single page gives an over-ceiling layer, unchanged by
1331/// banding. [`EngineError::SchedulerEscalation`] when a band's own page
1332/// cannot be found even one at a time.
1333fn band_rounds(
1334    banded: BandedLayer<'_>,
1335    ops: Vec<RoundOp>,
1336    stack: &mut [Stream<'_>],
1337    rounds: &mut Vec<Round>,
1338    pages: &mut LivePages,
1339    caps: &TierCaps,
1340    config: &PageConfig,
1341) -> Result<Vec<RoundOp>, EngineError> {
1342    let bands = pages::page_bands(banded.bounds, config, caps)?;
1343
1344    for band in bands {
1345        make_room(stack, rounds, pages, caps, config)?;
1346        // Acquired before anything of this band is released, never after —
1347        // the same ordering a single-page layer's own acquisition keeps.
1348        let parity = pages
1349            .acquire(PageParity::from_depth(banded.depth))
1350            .ok_or_else(|| {
1351                escalate(format!(
1352                    "layer {} would need a third live intermediate page to render one of its own \
1353                     column bands: both of the {PING_PONG_GROUPS} groups the simple scheduler \
1354                     ping-pongs between are already holding a page a later round composites, and \
1355                     no open round could be cut to hand one back",
1356                    banded.id
1357                ))
1358            })?;
1359
1360        rounds.push(Round {
1361            target: RoundTarget::Page(PageTarget {
1362                layer: banded.id,
1363                depth: banded.depth,
1364                parity,
1365                size: band.size,
1366                bounds: band.bounds,
1367                // Every band is a page of its own rather than a continuation
1368                // of the one before it: two bands never share a texture at
1369                // once, so there is nothing here for a later band to load.
1370                continued: false,
1371            }),
1372            ops: ops.clone(),
1373            released: Vec::new(),
1374            filter: None,
1375            copy: None,
1376            samples: Vec::new(),
1377        });
1378
1379        // Spliced onto the enclosing stream's own accumulator directly,
1380        // rather than returned for the caller to append: a later band still
1381        // has to acquire a group, and `make_room`'s own cutting is what finds
1382        // this composite and hands the group back — exactly the mechanism a
1383        // page-hungry sibling already relies on, so nothing here needs to
1384        // wait for this function to return before a round can use it.
1385        if let Some(parent) = stack.last_mut() {
1386            parent.ops.push(RoundOp::Composite(Composite {
1387                layer: banded.id,
1388                parity,
1389                bounds: band.bounds,
1390                opacity: banded.layer.props.opacity,
1391                retained: None,
1392            }));
1393        }
1394    }
1395
1396    Ok(Vec::new())
1397}
1398
1399/// Frees a page group for a layer about to take one, by cutting an ancestor's
1400/// round short, when the walk is running out of groups.
1401///
1402/// Two moves, in this order.
1403///
1404/// - **Ahead of the shortage.** An isolated ancestor that has not taken a page
1405///   yet is going to need one, and a layer taking the last free group would
1406///   leave it none — which is what used to limit a parent to two isolated
1407///   children. Cutting such an ancestor now gives it its page *and* hands back
1408///   every page that cut composites, so the group this layer is about to take
1409///   comes straight back and the count is no worse than it started. Outermost
1410///   first: an outer ancestor's cut is what frees the group an inner one takes.
1411/// - **At the shortage.** With both groups gone, the innermost open round that
1412///   composites anything is cut, releasing its pages a pass early. This is what
1413///   serves a fan of any width under the surface: the siblings already
1414///   composited go back to the pool before the next one is rendered.
1415///
1416/// A cut that cannot help is skipped rather than forced, and running out of
1417/// ancestors to cut is not itself an error — the acquire this precedes
1418/// escalates if it still finds no group, which keeps one refusal site and one
1419/// reason.
1420fn make_room(
1421    stack: &mut [Stream<'_>],
1422    rounds: &mut Vec<Round>,
1423    pages: &mut LivePages,
1424    caps: &TierCaps,
1425    config: &PageConfig,
1426) -> Result<(), EngineError> {
1427    if pages.free() > 1 {
1428        return Ok(());
1429    }
1430
1431    for index in 0..stack.len() {
1432        if pages.free() == 0 {
1433            break;
1434        }
1435        if stack[index].wants_a_page() {
1436            cut_at(stack, index, rounds, pages, caps, config)?;
1437        }
1438    }
1439
1440    if pages.free() == 0 {
1441        for index in (0..stack.len()).rev() {
1442            if cut_at(stack, index, rounds, pages, caps, config)? {
1443                break;
1444            }
1445        }
1446    }
1447
1448    Ok(())
1449}
1450
1451/// Emits what `stack[index]` has accumulated as a round of its own, so the pages
1452/// that batch composites return to the pool before the walk renders anything
1453/// else. Answers whether a round was emitted.
1454///
1455/// The round carries that stream's ops followed by those of every inlined stream
1456/// open above it: an inlined layer draws into its host's round, so its ops
1457/// belong to this cut, in exactly the painter order the recording laid them in.
1458/// Whatever any of them records after the cut becomes the host's next round,
1459/// which loads rather than clears (see [`PageTarget::continued`]) — so a cut
1460/// changes when a target's pixels are written, never which ones or in what
1461/// order.
1462///
1463/// `false` when there is nothing to gain or no way to emit: a batch holding no
1464/// composite frees no page by being cut, a suppressed stream renders nothing at
1465/// all, an inlined one has no target of its own, and an isolated one that has
1466/// not taken a page cannot take one with both groups held.
1467fn cut_at(
1468    stack: &mut [Stream<'_>],
1469    index: usize,
1470    rounds: &mut Vec<Round>,
1471    pages: &mut LivePages,
1472    caps: &TierCaps,
1473    config: &PageConfig,
1474) -> Result<bool, EngineError> {
1475    let Some(stream) = stack.get(index) else {
1476        return Ok(false);
1477    };
1478    if stream.suppressed || matches!(stream.owner, StreamOwner::Inline) {
1479        return Ok(false);
1480    }
1481
1482    // Every stream above this one up to the next stream with a target of its own
1483    // is inlined into this round.
1484    let end = stack
1485        .iter()
1486        .enumerate()
1487        .skip(index.saturating_add(1))
1488        .find(|(_, stream)| !matches!(stream.owner, StreamOwner::Inline))
1489        .map_or(stack.len(), |(at, _)| at);
1490    let batch = stack.get(index..end).unwrap_or(&[]);
1491    if !batch
1492        .iter()
1493        .any(|stream| holds_composite(&stream.ops) || !stream.samples.is_empty())
1494    {
1495        return Ok(false);
1496    }
1497
1498    let continued = stack[index].emitted;
1499    let target = match stack[index].owner {
1500        // Both refused above; repeated here because the target is what the
1501        // round is built from.
1502        StreamOwner::Inline => return Ok(false),
1503        StreamOwner::Root => RoundTarget::Root,
1504        // A retained body's page is the store's, so cutting it takes no group:
1505        // the cut renders what the body has so far, and its next round loads
1506        // it.
1507        StreamOwner::Retained { id, layer, bracket } => RoundTarget::Retained(RetainedTarget {
1508            layer: id,
1509            bracket,
1510            bounds: layer.bbox,
1511            continued,
1512        }),
1513        StreamOwner::Isolated { id, layer } => {
1514            let bounds = layer.bbox;
1515            // A filter layer's stream can itself be cut mid-frame — nothing
1516            // stops a regular/opacity layer from being recorded nested inside
1517            // one, and `make_room` treats every isolated stream on the stack
1518            // alike — so this has to size the page on the same terms `finish`
1519            // does above, or a cut filter layer's contents would land on an
1520            // unpadded page its own first pass then reads past the edge of.
1521            let size = match &layer.kind {
1522                RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
1523                _ => filter_page_size(bounds, config, caps)?,
1524            };
1525            let parity = match stack[index].page {
1526                Some(parity) => parity,
1527                None => {
1528                    let Some(parity) = pages.acquire(PageParity::from_depth(stack[index].depth))
1529                    else {
1530                        return Ok(false);
1531                    };
1532                    stack[index].page = Some(parity);
1533                    parity
1534                }
1535            };
1536            RoundTarget::Page(PageTarget {
1537                layer: id,
1538                depth: stack[index].depth,
1539                parity,
1540                size,
1541                bounds,
1542                continued,
1543            })
1544        }
1545    };
1546
1547    let mut ops: Vec<RoundOp> = Vec::new();
1548    let mut samples: Vec<BackdropSample> = Vec::new();
1549    for stream in stack.get_mut(index..end).unwrap_or(&mut []) {
1550        ops.append(&mut stream.ops);
1551        samples.append(&mut stream.samples);
1552    }
1553    let mut released = released_pages(&ops);
1554    release_samples(&samples, &mut released);
1555    for parity in &released {
1556        pages.release(*parity);
1557    }
1558    stack[index].emitted = true;
1559    rounds.push(Round {
1560        target,
1561        ops,
1562        released,
1563        filter: None,
1564        copy: None,
1565        samples,
1566    });
1567
1568    Ok(true)
1569}
1570
1571/// Emits the rounds a backdrop blur at the frame root costs, leaving the page
1572/// holding the blurred region sampled by the root stream (see the module
1573/// header's *Backdrop rounds*).
1574///
1575/// Three steps, in order: the surface round open so far is emitted whatever it
1576/// holds, so everything painted before the backdrop has landed before anything
1577/// reads it back; a [region-copy](RegionCopy) round seeds a page with the
1578/// backdrop's region of the frame's target; and the blur's own pass sequence
1579/// runs over that page exactly as a filter layer's does. The result stays held
1580/// until the surface round that draws the backdrop's rectangle ends.
1581///
1582/// `stack` holds the frame root and nothing but inlined layers above it — the
1583/// caller serves a backdrop nowhere else — so cutting it leaves no page held
1584/// and both groups are free for the copy and the passes.
1585fn backdrop_rounds(
1586    backdrop: &Backdrop,
1587    stack: &mut [Stream<'_>],
1588    rounds: &mut Vec<Round>,
1589    pages: &mut LivePages,
1590    caps: &TierCaps,
1591    config: &PageConfig,
1592) -> Result<(), EngineError> {
1593    flush_surface(stack, rounds, pages);
1594
1595    let id = backdrop.layer;
1596    let region = backdrop.region;
1597    let size = filter_page_size(region, config, caps)?;
1598    let contents = pages.acquire(PageParity::Even).ok_or_else(|| {
1599        escalate(format!(
1600            "backdrop layer {id} found no free page group for its region copy after the surface \
1601             round ahead of it was cut"
1602        ))
1603    })?;
1604    rounds.push(Round {
1605        // Cleared and then written by the copy: the page's previous holder
1606        // must not show past the copied region, where the kernels read.
1607        target: RoundTarget::Page(PageTarget {
1608            layer: id,
1609            depth: 1,
1610            parity: contents,
1611            size,
1612            bounds: region,
1613            continued: false,
1614        }),
1615        ops: Vec::new(),
1616        released: Vec::new(),
1617        filter: None,
1618        copy: Some(RegionCopy { source: region }),
1619        samples: Vec::new(),
1620    });
1621
1622    let steps = blur::blur_passes(&backdrop.blur(), SizeU16::from(region));
1623    let regular = RecordedLayerKind::Regular;
1624    let blurred = pass_rounds(
1625        &FilterLayer {
1626            id,
1627            kind: &regular,
1628            depth: 1,
1629            bounds: region,
1630            size,
1631            contents,
1632        },
1633        &steps,
1634        stack,
1635        rounds,
1636        pages,
1637        caps,
1638        config,
1639    )?;
1640
1641    if let Some(root) = stack.first_mut() {
1642        root.samples.push(BackdropSample {
1643            layer: id,
1644            parity: blurred,
1645        });
1646    }
1647    Ok(())
1648}
1649
1650/// Emits everything the frame root and the inlined layers above it have
1651/// accumulated as a surface round of its own — unconditionally, unlike
1652/// [`cut_at`], which cuts only to hand a page back — or nothing when nothing
1653/// has accumulated since the last one.
1654fn flush_surface(stack: &mut [Stream<'_>], rounds: &mut Vec<Round>, pages: &mut LivePages) {
1655    let mut ops: Vec<RoundOp> = Vec::new();
1656    let mut samples: Vec<BackdropSample> = Vec::new();
1657    for stream in stack.iter_mut() {
1658        ops.append(&mut stream.ops);
1659        samples.append(&mut stream.samples);
1660    }
1661    if ops.is_empty() && samples.is_empty() {
1662        return;
1663    }
1664    let mut released = released_pages(&ops);
1665    release_samples(&samples, &mut released);
1666    for parity in &released {
1667        pages.release(*parity);
1668    }
1669    if let Some(root) = stack.first_mut() {
1670        root.emitted = true;
1671    }
1672    rounds.push(Round {
1673        target: RoundTarget::Root,
1674        ops,
1675        released,
1676        filter: None,
1677        copy: None,
1678        samples,
1679    });
1680}
1681
1682/// Adds the pages `samples` holds to `released`, each listed once.
1683fn release_samples(samples: &[BackdropSample], released: &mut Vec<PageParity>) {
1684    for sample in samples {
1685        if !released.contains(&sample.parity) {
1686            released.push(sample.parity);
1687        }
1688    }
1689}
1690
1691/// How a recorded layer is served.
1692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1693enum LayerRole {
1694    /// Composites identically to drawing its contents directly, so its stream
1695    /// is spliced into its parent's round.
1696    Inline,
1697    /// Needs a page and a pass of its own.
1698    Isolated,
1699    /// Needs a page and a pass of its own, plus a second page and a pass per
1700    /// step of its filter's sequence.
1701    ///
1702    /// Never inlined however it composites: filtering a layer's contents is not
1703    /// what drawing them into the parent does.
1704    Filtered,
1705    /// Contributes nothing, along with everything nested inside it.
1706    Dropped,
1707}
1708
1709/// What taking one node off the innermost open stream left the walk to do.
1710///
1711/// The step is decided under a borrow of the stack and acted on after it, since
1712/// entering a layer pushes a stream and finishing one pops it.
1713enum Step {
1714    /// The node entered a layer.
1715    Enter {
1716        /// The layer's index into
1717        /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1718        id: u32,
1719        /// The isolated nesting depth of the stream that entered it.
1720        depth: usize,
1721    },
1722    /// The node entered nothing; the stream continues.
1723    Next,
1724    /// The stream is exhausted and renders as its round now.
1725    Done,
1726}
1727
1728/// One command stream the walk has open: the frame root's own nodes, or one
1729/// recorded layer's.
1730///
1731/// The walk keeps these on an explicit stack rather than recursing, so a
1732/// recording nesting layers far past anything a widget tree records costs heap
1733/// rather than call frames. The stack is bounded by the recording's layer count,
1734/// because a layer is entered at most once.
1735struct Stream<'a> {
1736    /// Whose stream this is, and how it is served.
1737    owner: StreamOwner<'a>,
1738    /// The stream's nodes, in recording order.
1739    nodes: &'a [Node],
1740    /// The next node to visit.
1741    index: usize,
1742    /// The isolated nesting depth of this stream's contents, one-based; zero at
1743    /// the frame root.
1744    depth: usize,
1745    /// The ops accumulated since this stream's last round, in execution order.
1746    ops: Vec<RoundOp>,
1747    /// Whether a round of this stream's has been emitted already, which is what
1748    /// makes the next one a continuation of the same target.
1749    emitted: bool,
1750    /// The group this stream's page came from, taken at its first
1751    /// [cut](cut_at) and held until its last round. `None` while the stream is
1752    /// still free to have its page named by the round that finishes it, which
1753    /// is the common case — only a cut stream reserves ahead.
1754    page: Option<PageParity>,
1755    /// Whether this stream renders nothing at all: it is a layer covering no
1756    /// pixels, or nested inside one.
1757    suppressed: bool,
1758    /// The blurred backdrop pages the ops accumulated since this stream's last
1759    /// round sample — only ever the frame root's, since a backdrop is served
1760    /// nowhere else. They travel with the ops into whichever round emits them,
1761    /// which releases them.
1762    samples: Vec<BackdropSample>,
1763}
1764
1765impl<'a> Stream<'a> {
1766    /// The frame root's stream, which renders into no page.
1767    fn root(nodes: &'a [Node]) -> Self {
1768        Self {
1769            owner: StreamOwner::Root,
1770            nodes,
1771            index: 0,
1772            depth: 0,
1773            ops: Vec::new(),
1774            emitted: false,
1775            page: None,
1776            suppressed: false,
1777            samples: Vec::new(),
1778        }
1779    }
1780
1781    /// An inlined layer's stream, spliced into its parent's round at the depth
1782    /// its parent already occupies — an inlined layer takes no page, so it
1783    /// consumes no level (see the module header).
1784    fn inline(layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
1785        Self {
1786            owner: StreamOwner::Inline,
1787            nodes: &layer.nodes,
1788            index: 0,
1789            depth,
1790            ops: Vec::new(),
1791            emitted: false,
1792            page: None,
1793            suppressed,
1794            samples: Vec::new(),
1795        }
1796    }
1797
1798    /// An isolated layer's stream, one level deeper than its parent.
1799    fn isolated(id: u32, layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
1800        Self {
1801            owner: StreamOwner::Isolated { id, layer },
1802            nodes: &layer.nodes,
1803            index: 0,
1804            depth,
1805            ops: Vec::new(),
1806            emitted: false,
1807            page: None,
1808            suppressed,
1809            samples: Vec::new(),
1810        }
1811    }
1812
1813    /// A retained body's stream, at its parent's depth: the body renders into
1814    /// the store's page, not a pooled one, so it consumes no level of the
1815    /// chain its nested layers alternate along.
1816    fn retained(
1817        id: u32,
1818        layer: &'a RecordedLayer,
1819        bracket: u32,
1820        depth: usize,
1821        suppressed: bool,
1822    ) -> Self {
1823        Self {
1824            owner: StreamOwner::Retained { id, layer, bracket },
1825            nodes: &layer.nodes,
1826            index: 0,
1827            depth,
1828            ops: Vec::new(),
1829            emitted: false,
1830            page: None,
1831            suppressed,
1832            samples: Vec::new(),
1833        }
1834    }
1835
1836    /// Whether this stream is one a page group is still owed to.
1837    ///
1838    /// True only for an isolated layer that will really render — a suppressed
1839    /// one renders nothing — and only until its first cut names its group.
1840    fn wants_a_page(&self) -> bool {
1841        matches!(self.owner, StreamOwner::Isolated { .. })
1842            && !self.suppressed
1843            && self.page.is_none()
1844    }
1845}
1846
1847/// Whose stream a [`Stream`] walks, and so what finishing it produces.
1848enum StreamOwner<'a> {
1849    /// The frame's own surface. It occupies no page, which is what leaves both
1850    /// groups free for the layers directly under it.
1851    Root,
1852    /// A layer whose ops are spliced into its parent's round.
1853    Inline,
1854    /// A layer with a page and a round of its own.
1855    Isolated {
1856        /// The layer's index into
1857        /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1858        id: u32,
1859        /// The recorded layer, for its bounds and its opacity.
1860        layer: &'a RecordedLayer,
1861    },
1862    /// A retained body, with rounds of its own into the store's page.
1863    Retained {
1864        /// The layer's index into
1865        /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
1866        id: u32,
1867        /// The recorded layer, for its bounds and its opacity.
1868        layer: &'a RecordedLayer,
1869        /// The retained bracket the page belongs to.
1870        bracket: u32,
1871    },
1872}
1873
1874/// Which groups are holding a finished page.
1875///
1876/// A group holds one page at a time, so three booleans are this scheduler's
1877/// whole page allocator: acquiring is finding a group no round still needs,
1878/// releasing is the round that sampled a page ending. Nothing here allocates a
1879/// texture — the pool does that at execute time, keyed on the extent the round
1880/// carries.
1881///
1882/// The [ping-pong pair](PING_PONG_GROUPS) and the [spill page](Self::spill) are
1883/// kept apart deliberately. [`free`](Self::free) counts only the pair, so
1884/// [`make_room`]'s decisions — when to cut ahead of a shortage, when to cut at
1885/// one — are made against exactly the state they were tuned against and every
1886/// shape this scheduler already served schedules unchanged. The spill is only
1887/// ever reached through [`acquire_spill`](Self::acquire_spill), at the one site
1888/// that would otherwise refuse the frame.
1889#[derive(Debug, Default)]
1890struct LivePages {
1891    held: [bool; PING_PONG_GROUPS],
1892    /// Whether the one spill page is holding a page a later round composites.
1893    spill: bool,
1894}
1895
1896impl LivePages {
1897    /// The ping-pong group to render into, preferring `preferred` and falling
1898    /// back to the other, or `None` when both are holding a page a later round
1899    /// still composites.
1900    ///
1901    /// The preference is what keeps a chain alternating exactly as its depths'
1902    /// parities say; the fallback is what lets a second sibling, which shares
1903    /// its predecessor's depth and so its preference, take the free group
1904    /// instead of overwriting the page beside it. Never answers the spill page:
1905    /// that is [`acquire_spill`](Self::acquire_spill)'s, so the fallback to a
1906    /// third page is a decision a caller makes rather than one this hides.
1907    fn acquire(&mut self, preferred: PageParity) -> Option<PageParity> {
1908        let parity = if !self.holds(preferred) {
1909            preferred
1910        } else if !self.holds(preferred.opposite()) {
1911            preferred.opposite()
1912        } else {
1913            return None;
1914        };
1915
1916        self.set(parity, true);
1917        Some(parity)
1918    }
1919
1920    /// The spill page, or `None` when it is already holding one.
1921    ///
1922    /// The whole of the bound: one page, so a second layer wanting it while it
1923    /// is held finds nothing and the frame is refused exactly as it was before
1924    /// the spill existed.
1925    fn acquire_spill(&mut self) -> Option<PageParity> {
1926        if self.spill {
1927            return None;
1928        }
1929        self.set(PageParity::Spill, true);
1930        Some(PageParity::Spill)
1931    }
1932
1933    /// Hand a group's page back, once the round that sampled it has ended.
1934    fn release(&mut self, parity: PageParity) {
1935        self.set(parity, false);
1936    }
1937
1938    /// How many of the ping-pong groups are holding no page.
1939    ///
1940    /// The spill page is deliberately not counted: it is a last resort rather
1941    /// than a group in the rotation, and counting it would make [`make_room`]
1942    /// stop cutting one shortage early — which would change how every already
1943    /// served shape schedules.
1944    fn free(&self) -> usize {
1945        self.held.iter().filter(|held| !**held).count()
1946    }
1947
1948    /// Whether `parity`'s group is holding a page.
1949    fn holds(&self, parity: PageParity) -> bool {
1950        match parity {
1951            PageParity::Spill => self.spill,
1952            group => self.held.get(group.index()).copied().unwrap_or(false),
1953        }
1954    }
1955
1956    fn set(&mut self, parity: PageParity, held: bool) {
1957        match parity {
1958            PageParity::Spill => self.spill = held,
1959            group => {
1960                if let Some(slot) = self.held.get_mut(group.index()) {
1961                    *slot = held;
1962                }
1963            }
1964        }
1965    }
1966}
1967
1968/// How `layer` is served, refusing every property this scheduler cannot honour.
1969fn layer_role(id: u32, layer: &RecordedLayer) -> Result<LayerRole, EngineError> {
1970    if layer.props.blend_mode != BlendMode::default() {
1971        return Err(escalate(format!(
1972            "layer {id} composites with a non-default blend mode ({:?}), which has to read the \
1973             target it blends into",
1974            layer.props.blend_mode
1975        )));
1976    }
1977    if layer.props.mask.is_some() {
1978        return Err(escalate(format!(
1979            "layer {id} carries a layer mask, which needs an intermediate of its own"
1980        )));
1981    }
1982    if layer.props.clip_path.is_some() {
1983        return Err(escalate(format!(
1984            "layer {id} carries a layer clip path; frust lowers clips through the clip stack, not \
1985             through a layer"
1986        )));
1987    }
1988
1989    let opacity = layer.props.opacity;
1990    if !opacity.is_finite() {
1991        return Err(escalate(format!(
1992            "layer {id} carries a non-finite opacity ({opacity})"
1993        )));
1994    }
1995
1996    if opacity <= 0.0 {
1997        return Ok(LayerRole::Dropped);
1998    }
1999
2000    match &layer.kind {
2001        RecordedLayerKind::Regular => Ok(if opacity >= 1.0 {
2002            LayerRole::Inline
2003        } else {
2004            LayerRole::Isolated
2005        }),
2006        // The two filters the engine renders are a Gaussian blur and a
2007        // shadow-only drop shadow; every other filter a recording can carry is
2008        // refused here, by name, and the frame is skipped. A filtered layer is
2009        // never inlined, whatever its opacity.
2010        kind => {
2011            // Recognised here rather than at the filter round, so a refusal
2012            // names what was found before any round has been emitted and a
2013            // filter shape this engine cannot render is refused wherever it is
2014            // recorded — including inside a layer whose contents cover nothing.
2015            served_filter(id, kind).map_err(escalate)?;
2016            Ok(LayerRole::Filtered)
2017        }
2018    }
2019}
2020
2021/// Whether `ops` composites anything, and so whether emitting them as a round
2022/// would hand a page back to the pool.
2023fn holds_composite(ops: &[RoundOp]) -> bool {
2024    ops.iter().any(|op| matches!(op, RoundOp::Composite(_)))
2025}
2026
2027/// The pages `ops` samples, each listed once.
2028fn released_pages(ops: &[RoundOp]) -> Vec<PageParity> {
2029    let mut pages: Vec<PageParity> = Vec::new();
2030
2031    for op in ops {
2032        if let RoundOp::Composite(composite) = op
2033            && composite.retained.is_none()
2034            && !pages.contains(&composite.parity)
2035        {
2036            pages.push(composite.parity);
2037        }
2038    }
2039
2040    pages
2041}
2042
2043/// Why layer `id` is refused when every page it could have rendered into is
2044/// holding one a later round still composites.
2045///
2046/// Two reasons, because two different bounds are reached. `spills` is whether
2047/// the layer was offered the [spill page](PageParity::Spill) — every regular
2048/// layer is — and so whether what ran out was all three of this scheduler's
2049/// live pages or only the ping-pong pair a filter layer is held to.
2050fn out_of_pages(id: u32, spills: bool) -> String {
2051    if spills {
2052        format!(
2053            "layer {id} would need a fourth live intermediate page: both of the \
2054             {PING_PONG_GROUPS} groups the simple scheduler ping-pongs between and the one spill \
2055             page beside them are already holding a page a later round composites, and no open \
2056             round could be cut to hand one back. A nested chain of any depth fits, a fan of \
2057             siblings of any width fits, and so does a chain hanging off an isolated ancestor's \
2058             later child; what does not is a layer whose own round samples two live pages while a \
2059             third is still owed to a round above it"
2060        )
2061    } else {
2062        format!(
2063            "filter layer {id} would need a third live intermediate page for its contents: both \
2064             of the {PING_PONG_GROUPS} groups the simple scheduler ping-pongs between are already \
2065             holding a page a later round composites, and no open round could be cut to hand one \
2066             back. The spill page a regular layer falls back on is not offered here — a filter \
2067             layer's passes ping-pong between the two groups themselves, so it is held to them"
2068        )
2069    }
2070}
2071
2072/// The escalation error carrying `reason`.
2073fn escalate(reason: impl Into<String>) -> EngineError {
2074    EngineError::SchedulerEscalation {
2075        reason: reason.into(),
2076    }
2077}