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: ®ular,
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}