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