Skip to main content

frust_engine/compile/
mod.rs

1//! Scene compilation: a `frust_scene::Scene` becomes sparse strips plus draws.
2//!
3//! [`SceneCompiler`] is a stateless walk over an already-recorded display list.
4//! Unlike an immediate-mode scene recorder, there is no render state to save
5//! and restore and no transform stack to unwind — `frust_scene::SceneBuilder`
6//! has already composed every command's transform, so each command carries the
7//! only transform it needs and the walk composes it with the frame's root once.
8//!
9//! The compiler owns the retained scratch a
10//! [`StripGenerator`] needs (line buffer, tiles, flatten/stroke context) so a
11//! steady-state frame reuses those allocations, and the two pieces of state
12//! that genuinely span frames — the [`ImageResidency`] that keeps an image's
13//! atlas rectangle alive for as long as the scene keeps drawing it, and the
14//! [`GlyphPrepCache`] that keeps a glyph's fetched outline and its font's
15//! hinting instance alive on the same terms. Everything else is per-frame:
16//! [`compile`](SceneCompiler::compile) returns what a frame produced in one
17//! [`CompiledFrame`] and keeps nothing of it.
18//!
19//! Compiled here: the geometry primitives — axis-aligned and rounded
20//! rectangles, lines, arbitrary filled/stroked paths — the clip bracket around
21//! them, which lowers to a scissor rectangle or a coverage mask and never to an
22//! intermediate texture (see [`clip`]), the opacity-layer and retained brackets
23//! that group them (see [`layers`]), the hole punch that erases what they
24//! painted (see [`clear`]), images, whose destination rectangle is
25//! rasterized like any other fill and painted by an atlas-backed image paint
26//! (see [`paint`] and [`crate::cache::images`]), and blurred rounded
27//! rectangles, whose padded bounding rectangle is rasterized the same way and
28//! painted by a gaussian-falloff paint the fragment shader evaluates per pixel
29//! (see [`blur_rrect`]), and glyph runs, which take one of two routes decided
30//! per run by [`crate::text::atlas_policy`] before the walk begins: a settled
31//! run is resolved through the glyph atlas and each glyph drawn as one image
32//! paint over its slot, while an animating, oversized or refused one has its
33//! outlines fetched and scaled by `glifo` and rasterized as any other filled
34//! path would be, painted by the run's own brush (see [`crate::text`]).
35//! Shader quads are recognised
36//! and skipped — the engine grows them in a later pass, and skipping is the
37//! conservative behaviour (a frame draws less, never wrong). A backdrop blur
38//! is a layer of its own holding one draw — its rectangle's coverage under
39//! whatever clip is open, painted by nothing the compiler resolves — listed in
40//! [`CompiledFrame::backdrops`] for the scheduler and the renderer to cut the
41//! frame at and to paint from the blurred page (see
42//! [`crate::filters`]' *The backdrop blur*).
43
44pub mod blur_rrect;
45
46pub mod clear;
47
48pub mod clip;
49
50pub(crate) mod cull;
51
52pub mod layers;
53
54pub mod paint;
55
56pub mod draw;
57
58pub mod external;
59
60pub(crate) mod parallel;
61
62pub use clear::ClearPunch;
63pub use clip::ClipStack;
64pub use draw::{DepthCounter, EngineDraw};
65pub use external::{ExternalExtents, ExternalSkip};
66pub use layers::{
67    GroupStack, LayerLowering, RetainedBracket, RetainedKind, RetainedPlacement, RetainedStack,
68};
69pub use parallel::{DEFAULT_PARALLEL_MIN_COST, DEFAULT_PARALLEL_WORKERS, MAX_PARALLEL_WORKERS};
70
71/// The most glyph runs a compiler's resident-run cache remembers at once (see
72/// [`SceneCompiler::resident_glyph_run_count`]). A byte budget bounds the same
73/// records alongside it, so a scene of very long runs is held to fewer.
74pub const MAX_RESIDENT_GLYPH_RUNS: usize = crate::text::atlas_policy::MAX_RESIDENT_RUNS;
75
76use std::sync::Once;
77use std::time::Duration;
78
79use kurbo::{Affine, BezPath, Cap, Join, PathEl, Rect, RoundedRectRadii, Shape, Stroke};
80use peniko::{Brush, Color, Fill, ImageData};
81
82use frust_gpu::{SceneTextureId, TierCaps};
83use frust_scene::{Command, CornerRadii, DashPattern, GlyphRun, PathStyle, Scene};
84
85use glifo::{GlyphAtlas, GlyphPrepCache, PendingClearRect};
86
87use vello_common::encode::EncodedPaint;
88use vello_common::fearless_simd::Level;
89use vello_common::geometry::RectU16;
90use vello_common::paint::ImageId;
91use vello_common::record::CommandRecorder;
92use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage};
93use vello_common::tile::Tile;
94use vello_common::util::is_axis_aligned;
95
96use crate::cache::images::{
97    AtlasBudget, AtlasRegion, ImageResidency, ImageSkip, ImageUpload, is_mobile_tier,
98};
99use crate::compile::blur_rrect::encode_blurred_rounded_rect;
100use crate::compile::clear::StagedPunch;
101use crate::compile::external::encode_scene_texture;
102use crate::compile::paint::{LutRequest, encode_brush, encode_image_brush, encode_image_command};
103use crate::compile::parallel::{Claim, Geometry, Merge, Pool};
104use crate::config;
105use crate::error::EngineError;
106use crate::filters::{Backdrop, MAX_BACKDROPS_PER_FRAME, backdrop_region, backdrop_sigma};
107use crate::text::{
108    AtlasPolicy, FontFacts, GlyphQuadScratch, GlyphQuadTargets, GlyphRunRouting, GlyphRunTargets,
109    RunKey, RunRoute, context_paint, glyph_atlas_policy, lower_glyph_run,
110};
111
112/// Curve-flattening tolerance, in device pixels.
113///
114/// The value `vello_hybrid`'s own scene recorder flattens at; keeping it
115/// identical is what lets the two rasterizers be compared strip-for-strip.
116pub(crate) const FLATTEN_TOLERANCE: f64 = 0.1;
117
118/// Raised the first time an image is refused residency, so a scene that draws
119/// an unsupported image says so at least once at warning level without the
120/// per-frame repetition a per-skip warning would produce.
121static IMAGE_SKIP_WARNING: Once = Once::new();
122
123/// Raised the first time a glyph run is refused for an unreadable font, on the
124/// same once-per-process terms as [`IMAGE_SKIP_WARNING`].
125static FONT_SKIP_WARNING: Once = Once::new();
126
127/// Raised the first time the compiler drops a [`Command::ShaderQuad`] whose
128/// program has no rendered target, on the same once-per-process terms as
129/// [`IMAGE_SKIP_WARNING`].
130static SHADER_QUAD_SKIP_WARNING: Once = Once::new();
131
132/// Raised the first time a [`Command::ShaderQuad`] is dropped because the
133/// shader-effect kill switch is set, on the same once-per-process terms as
134/// [`IMAGE_SKIP_WARNING`]. Separate from [`SHADER_QUAD_SKIP_WARNING`] because
135/// it reports a deliberate configuration rather than a missing pre-pass, and
136/// conflating the two would tell an operator who set the switch that something
137/// went wrong.
138static SHADER_EFFECTS_DISABLED_WARNING: Once = Once::new();
139
140/// A stopwatch for the CPU phases one frame's encode splits into, compiled
141/// away entirely without `perf-trace`.
142///
143/// The engine's own CPU profile is measured by lapping this once per phase
144/// rather than by sampling: a phase is tens to hundreds of microseconds and no
145/// sampling profiler rides along on a phone under a benchmark harness, while a
146/// lap is two clock reads. Under `perf-trace` a lap reads
147/// [`std::time::Instant`]; without it the type is zero-sized, [`Self::lap`]
148/// answers [`Duration::ZERO`] and no clock is read at all — the "zero clock
149/// reads in a disabled build" terms `docs/RENDER_DEVELOPMENT.md`'s perf-trace
150/// convention and `docs/PERFORMANCE_BASELINES.md`'s Release-lean note set for an
151/// FFI-sensitive path, met at compile time rather than by a runtime branch.
152///
153/// Laps are cumulative by construction: each one both reports the span since
154/// the previous lap and opens the next, so a phase can never be double-counted
155/// or silently skipped the way two independent `Instant` pairs could.
156#[derive(Debug, Clone, Copy)]
157pub(crate) struct PhaseClock {
158    /// When the phase now being timed began.
159    #[cfg(feature = "perf-trace")]
160    last: std::time::Instant,
161}
162
163impl PhaseClock {
164    /// Opens the first phase at "now".
165    #[must_use]
166    pub(crate) fn start() -> Self {
167        Self {
168            #[cfg(feature = "perf-trace")]
169            last: std::time::Instant::now(),
170        }
171    }
172
173    /// Closes the phase in flight, answering what it cost, and opens the next.
174    #[must_use]
175    pub(crate) fn lap(&mut self) -> Duration {
176        #[cfg(feature = "perf-trace")]
177        {
178            let now = std::time::Instant::now();
179            // Saturating rather than `-`: a clock that went backwards across a
180            // lap is a measurement artefact, and a zero span reports it far
181            // better than a panicked frame would (E17).
182            let span = now.saturating_duration_since(self.last);
183            self.last = now;
184            span
185        }
186        #[cfg(not(feature = "perf-trace"))]
187        Duration::ZERO
188    }
189}
190
191/// What one [`SceneCompiler::compile`] call spent, phase by phase.
192///
193/// Zero across the board in a build without `perf-trace` — see [`PhaseClock`].
194/// The phases partition the call in the order they run, so their sum is the
195/// whole compile minus call overhead. Two edges are worth naming rather than
196/// leaving to be inferred: the frame record and its depth counter are built
197/// after the `admit` lap, so `walk` spans their construction as well as the
198/// command walk itself; and the `frust-perf img` line a `perf-trace` build
199/// emits is written *after* the last lap, so no phase is charged the cost of
200/// reporting on one.
201///
202/// 1. [`validate`](Self::validate) — the up-front finiteness and geometry
203///    sweep over every command. A frame is refused whole or not at all, so
204///    this sweep runs before the walk records anything and is paid on every
205///    frame in the command count.
206/// 2. [`prepare`](Self::prepare) — resetting the per-frame scratch (strip
207///    generator, clip/group/retained stacks, punches) and ageing the two
208///    caches that span frames (image residency, `glifo`'s prep cache).
209/// 3. [`classify`](Self::classify) — routing every glyph run through
210///    [`crate::text::atlas_policy`] before any of them is drawn.
211/// 4. [`admit`](Self::admit) — closing the glyph atlas's own frame, which is
212///    where admission packs what the routing pass asked for.
213/// 5. [`walk`](Self::walk) — building the frame record the walk fills, then
214///    the command walk itself: strip generation and paint encoding, and on a
215///    text-heavy scene the bulk of the call. The part of it spent inside glyph
216///    runs is reported separately by [`glyphs`](Self::glyphs), which is a
217///    subset of this rather than a phase of its own. It also carries the
218///    parallel walk's overhead: the planning pass that picks the shape
219///    commands helper threads generate, spawning the helpers and joining them
220///    (see the `parallel` module).
221/// 6. [`finish`](Self::finish) — closing open groups, generating the hole
222///    punches, ageing the glyph atlas and taking the frame's image plan.
223///
224/// Observational only: nothing downstream branches on any of it.
225#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
226pub struct CompileSpans {
227    /// The up-front finiteness and geometry sweep over every command.
228    pub validate: Duration,
229    /// Per-frame scratch resets and the two cross-frame caches' ageing.
230    pub prepare: Duration,
231    /// Glyph-run routing, ahead of the walk.
232    pub classify: Duration,
233    /// Glyph atlas admission, closing the routing pass.
234    pub admit: Duration,
235    /// The command walk: strip generation and paint encoding, with the
236    /// parallel walk's planning, spawn and join.
237    pub walk: Duration,
238    /// The part of [`walk`](Self::walk) spent inside glyph runs.
239    ///
240    /// A **subset** of `walk`, not a seventh phase beside it, and so
241    /// deliberately excluded from [`Self::total`]: adding it would count the
242    /// glyph work twice. It exists because "the walk dominates" is not on its
243    /// own an actionable measurement on a text-heavy scene — whether the cost
244    /// is the text or everything drawn around it is the question that decides
245    /// where a lever could go.
246    ///
247    /// Accumulated per [`Command::GlyphRun`] rather than per glyph: a run is
248    /// the unit the cache and the atlas policy both work in, and two clock
249    /// reads a glyph would cost more than the phase being measured.
250    pub glyphs: Duration,
251    /// Closing groups, punch generation, atlas ageing, the image plan.
252    pub finish: Duration,
253}
254
255impl CompileSpans {
256    /// What the six phases sum to.
257    ///
258    /// Saturating rather than `+`: a sum is only ever read by a diagnostic
259    /// line, and overflowing one must not take the frame with it (E17).
260    #[must_use]
261    pub fn total(&self) -> Duration {
262        [
263            self.prepare,
264            self.classify,
265            self.admit,
266            self.walk,
267            self.finish,
268        ]
269        .iter()
270        .fold(self.validate, |acc, span| acc.saturating_add(*span))
271    }
272}
273
274/// Where one glyph an atlas-routed draw sampled lives in the atlas array.
275///
276/// The image half of residency travels as an [`ImageUpload`], carrying pixels;
277/// a glyph's pixels are produced *on the GPU* by the replay pass, so nothing
278/// travels here but the rectangle — which the renderer still needs, because a
279/// glyph paint names its slot by [`ImageId`] and only the sink that drew it was
280/// ever handed the slot itself.
281///
282/// Reported per draw rather than per allocation, so a recycled handle can never
283/// be resolved against a previous occupant's rectangle: `glifo` returns an
284/// evicted slot's id to the shared allocator, and whatever takes it next — a
285/// glyph or an image — reports its own rectangle on the frame it is drawn.
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub struct GlyphSlot {
288    /// The handle the draw's image paint names this slot by.
289    pub id: ImageId,
290    /// The slot's own rectangle, padding excluded.
291    pub region: AtlasRegion,
292    /// Transparent padding texels `glifo` keeps around `region`.
293    pub padding: u32,
294}
295
296/// One glyph drawn as a *glyph quad*: the whole-pixel rectangle its single
297/// rect instance covers, and the atlas texel under that rectangle's top-left
298/// corner (see [`crate::text`]'s backend for when a glyph is one).
299///
300/// The draw it belongs to is an [`EngineDraw`] with the run's shared record as
301/// its paint, found by its painter-order depth, which no two draws of a frame
302/// share. Its strip range is empty unless the glyph was drawn inside an
303/// isolated layer, where it carries the rectangle's own coverage so the
304/// recorder can fold its bounds into the layer's: the renderer draws a quad
305/// from this alone — one rect instance, no edge coverage (the rectangle lands
306/// on whole pixels), its payload this atlas texel — and never from strips.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub struct GlyphQuad {
309    /// The depth of the draw this quad is.
310    pub depth: u32,
311    /// Left edge in device pixels, already trimmed to the viewport and the
312    /// clip stack's scissor.
313    pub x: u16,
314    /// Top edge in device pixels, trimmed likewise.
315    pub y: u16,
316    /// Width in device pixels; never zero.
317    pub width: u16,
318    /// Height in device pixels; never zero.
319    pub height: u16,
320    /// Atlas texel column under the rectangle's left edge.
321    pub atlas_x: u16,
322    /// Atlas texel row under the rectangle's top edge.
323    pub atlas_y: u16,
324}
325
326/// Everything one compiled frame produced.
327///
328/// The strips and their alpha coverage share one
329/// [`StripStorage`]: `strips.strips` is the frame's whole strip buffer (each
330/// [`EngineDraw::strip_range`] indexes into it) and `strips.alphas` the alpha
331/// runs those strips reference. They are kept together because a strip's
332/// packed alpha index is only meaningful against the alpha buffer generated
333/// alongside it.
334///
335/// The draws themselves live in `recorder.draws` rather than in a field of
336/// their own: [`CommandRecorder`] already owns that vector, and its node
337/// ranges index into it, so a second parallel copy could only drift out of
338/// agreement with the recording. Read them through [`CompiledFrame::draws`].
339#[derive(Debug)]
340pub struct CompiledFrame {
341    /// The frame's strips and the alpha coverage they index.
342    pub strips: StripStorage,
343    /// The recorded render graph, owning the frame's draws.
344    pub recorder: CommandRecorder<EngineDraw>,
345    /// Paints too complex to inline into a draw, indexed by
346    /// [`Paint::Indexed`](vello_common::paint::Paint::Indexed).
347    pub encoded_paints: Vec<EncodedPaint>,
348    /// The frame's hole punches, hoisted to the root and issued at the
349    /// punch's own painter-order position (see [`clear`]).
350    ///
351    /// Deliberately not draws: a punch erases rather than paints, and keeping
352    /// it out of the recording is what lets a target that disregards alpha
353    /// drop the whole pass and read the frame unchanged.
354    pub clears: Vec<ClearPunch>,
355    /// The colour ramps `encoded_paints` needs made resident before the frame
356    /// is drawn, one per gradient entry.
357    ///
358    /// Deliberately not serviced here: the compiler holds no gradient cache,
359    /// so ramp residency is decided once per frame by the renderer rather than
360    /// per draw by the walk (see [`paint`]).
361    pub lut_requests: Vec<LutRequest>,
362    /// How many of this frame's draws wrote their strip coverage directly as a
363    /// rectangle, bypassing flattening and tiling (see [`fast_rect`]).
364    ///
365    /// Purely observational — nothing downstream branches on it. It exists so
366    /// the fast path's admission rule is measurable from outside the compiler
367    /// rather than inferred from a strip count that both paths can produce.
368    pub fast_rect_draws: u32,
369    /// How many of this frame's clips lowered to a scissor rectangle, costing
370    /// no rasterization at all (see [`clip`]).
371    pub scissor_clips: u32,
372    /// How many of this frame's clips lowered to a coverage mask.
373    pub mask_clips: u32,
374    /// Atlas regions whose texels must be cleared before this frame draws,
375    /// freed by the residency reap at the head of the frame — less every
376    /// rectangle an upload of the same plan rewrites in full (see
377    /// [`image_forgets`](Self::image_forgets) for the unfiltered set).
378    ///
379    /// Serviced **before** [`image_uploads`](Self::image_uploads): a rectangle
380    /// freed this frame can be re-allocated in the same frame, so clearing
381    /// after writing would erase the image that just moved in.
382    pub image_evictions: Vec<AtlasRegion>,
383    /// Every atlas rectangle whose resident image was reaped, for the
384    /// consumer's registry to forget.
385    ///
386    /// A superset of [`image_evictions`](Self::image_evictions): a clear is
387    /// skipped when an upload in the same plan rewrites the identical
388    /// rectangle, but the evicted tenant is gone either way and must stop
389    /// resolving. Same pending-until-acknowledged lifetime.
390    pub image_forgets: Vec<AtlasRegion>,
391    /// Atlas regions whose texels must be written before this frame draws, one
392    /// per image that became resident during it.
393    ///
394    /// Empty in the steady state: an image drawn on a thousand consecutive
395    /// frames appears here exactly once, on the first.
396    pub image_uploads: Vec<ImageUpload>,
397    /// The atlas array depth this frame's paints address — the layer count the
398    /// array texture must have grown to before the uploads are written.
399    pub atlas_layers: u32,
400    /// How many of this frame's draws painted with an atlas-backed image.
401    pub image_draws: u32,
402    /// How many image draws were dropped because the image could not be made
403    /// resident (unsupported format, oversized, malformed, atlas full, the
404    /// same blob already resolved this frame at another extent, or the atlas
405    /// disabled outright).
406    ///
407    /// Observational, and the counter that makes "an image the engine cannot
408    /// hold is a skipped draw, not a panicked frame" measurable rather than
409    /// asserted.
410    pub skipped_images: u32,
411    /// How many of this frame's draws painted with an externally bound
412    /// texture.
413    pub external_draws: u32,
414    /// How many external-texture draws were dropped — an id nothing is
415    /// registered under, or a destination the texture cannot be mapped onto.
416    ///
417    /// Observational, and the external counterpart of
418    /// [`skipped_images`](Self::skipped_images): "a texture the engine cannot
419    /// resolve is a skipped draw, not a wrongly-sampled one", measured rather
420    /// than asserted.
421    pub skipped_externals: u32,
422    /// How many strips this frame's coverage masks cost.
423    ///
424    /// Observational, and the counter the clip lowering's whole claim rests on:
425    /// a frame whose clips all scissored reports zero here, which is what
426    /// "a rectangular clip is free" means measured rather than asserted.
427    pub clip_mask_strips: usize,
428    /// How many of this frame's draws painted one glyph outline.
429    ///
430    /// A glyph run costs one draw per glyph that produced coverage, so this is
431    /// bounded by — and usually below — the run's own glyph count: a glyph
432    /// clipped away or carrying no ink (a space) records nothing.
433    pub glyph_draws: u32,
434    /// How many glyphs were dropped because the engine has no way to paint
435    /// them on this path — a colour (COLR) glyph, a bitmap-strike glyph, or a
436    /// stroked outline.
437    ///
438    /// Observational, and the counter that makes "a glyph the engine cannot
439    /// paint goes missing rather than landing wrong" measurable rather than
440    /// asserted, the same way [`skipped_images`](Self::skipped_images) does
441    /// for images.
442    pub skipped_glyphs: u32,
443    /// How many of [`glyph_draws`](Self::glyph_draws) sampled the glyph atlas
444    /// rather than rasterizing an outline.
445    ///
446    /// The measure of what the policy is actually buying: a page of settled
447    /// text reads all-atlas, an animating size reads zero, and the difference
448    /// between them is the frame's rasterization work.
449    ///
450    /// Observational only. It is **not** the signal for whether the atlas has
451    /// pixel work outstanding — a run whose draws were all culled still
452    /// inserted entries and dirtied a page while reporting zero here. That
453    /// question is [`SceneCompiler::glyph_replay_pending`]'s.
454    pub atlas_glyph_draws: u32,
455    /// Where each of this frame's atlas-sampled glyphs lives, one entry per
456    /// atlas draw (see [`GlyphSlot`]).
457    pub glyph_slots: Vec<GlyphSlot>,
458    /// The frame's glyph quads, one per draw a fully resident glyph run drew
459    /// as one, in depth order (see [`GlyphQuad`]). A quad's draw is the one
460    /// kind of draw whose strip range may be empty: outside an isolated layer
461    /// nothing reads its coverage, so none is generated.
462    ///
463    /// A subset of [`atlas_glyph_draws`](Self::atlas_glyph_draws), and empty
464    /// whenever quads are off (`FRUST_ENGINE_NO_GLYPH_QUADS`,
465    /// [`SceneCompiler::set_glyph_quads`]).
466    pub glyph_quads: Vec<GlyphQuad>,
467    /// The indices into [`encoded_paints`](Self::encoded_paints) of the
468    /// records glyph quads are painted with — one per glyph run and atlas
469    /// layer — in ascending order.
470    ///
471    /// Each is an image record naming one of the run's slots, read for its
472    /// atlas layer and tint alone; a draw painted with one is drawn from its
473    /// [`GlyphQuad`], never from the record's own placement.
474    pub glyph_run_paints: Vec<usize>,
475    /// How many glyph runs this frame drew out of the resident-run cache —
476    /// every glyph resolved from slots remembered on an earlier frame, without
477    /// the run reaching `glifo` (see [`crate::text::ResidentPlan`]).
478    ///
479    /// Observational: a resident run draws exactly the quads, records and
480    /// slots `glifo`'s own fully resident run would, so nothing downstream
481    /// tells the two apart. Zero whenever the cache is off
482    /// (`FRUST_ENGINE_NO_GLYPH_RUN_CACHE`, [`SceneCompiler::set_glyph_run_cache`])
483    /// or quads are.
484    pub resident_glyph_runs: u32,
485    /// How many of this frame's draws took strips a helper thread generated
486    /// rather than strips the walk generated itself (see the `parallel` module).
487    ///
488    /// Observational: a helper's run is byte-identical to the one the walk
489    /// would have generated, so nothing downstream tells the two apart. Zero
490    /// whenever the frame spawned no helper — one worker
491    /// (`FRUST_ENGINE_NO_PARALLEL`, [`SceneCompiler::set_parallel_workers`]),
492    /// planned work below [`SceneCompiler::parallel_min_cost`], or a walk that
493    /// reached every planned command before a helper did.
494    pub parallel_draws: u32,
495    /// Atlas rectangles freed by the *previous* frame's glyph eviction, to be
496    /// zeroed before this frame writes anything into the array.
497    ///
498    /// Carried a frame late deliberately: `glifo` evicts at the end of a frame,
499    /// and a rectangle it frees can be handed straight back out on the next
500    /// one, so clearing it after that frame's uploads and replay would erase
501    /// whatever just moved in. Same ordering, same reason, as
502    /// [`image_evictions`](Self::image_evictions).
503    ///
504    /// Reported rather than consumed, on the same terms as
505    /// [`image_evictions`](Self::image_evictions): the same rectangles appear
506    /// on every later frame until a caller that really wrote them calls
507    /// [`SceneCompiler::acknowledge_glyph_clears`]. A frame compiled and then
508    /// refused takes none of them with it.
509    pub glyph_clears: Vec<PendingClearRect>,
510    /// The retained brackets this frame recorded, in recording order — empty
511    /// unless retention is on ([`SceneCompiler::set_retained_layers`]).
512    ///
513    /// Each names the recorded layer standing for it; the renderer's
514    /// retained-page store (`crate::cache::retained`) resolves every one to a
515    /// page, a composite or the inline lowering before the frame is scheduled.
516    pub retained: Vec<RetainedBracket>,
517    /// The keys of every `PushRetained` this frame carried that is *not* in
518    /// [`retained`](Self::retained), in recording order — empty unless
519    /// retention is on.
520    ///
521    /// Only the outermost bracket is recorded; one nested inside it (or one
522    /// whose transform left the device grid) is painted as part of the
523    /// enclosing body and its own key's page receives nothing this frame. The
524    /// retained-page store evicts every key listed here, so the ledger never
525    /// names a page older than the frame's own recording of the body (see
526    /// `crate::cache::retained`'s *A page holds this frame's recording, or
527    /// goes*).
528    pub retained_superseded: Vec<u64>,
529    /// The frame's backdrop blurs, in recording order — and so in ascending
530    /// [`Backdrop::depth`] — one per `Command::BackdropBlur` whose rectangle
531    /// produced coverage (see [`crate::filters`]' *The backdrop blur*).
532    ///
533    /// Empty on every frame that records none, and then nothing downstream
534    /// plans, cuts or draws anything differently.
535    pub backdrops: Vec<Backdrop>,
536    /// What compiling this frame cost, phase by phase — all zero without
537    /// `perf-trace` (see [`CompileSpans`]).
538    ///
539    /// Carried on the frame rather than kept on the compiler because the
540    /// consumer is the renderer's own encode-phase accounting, which already
541    /// holds the frame and would otherwise have to reach back into the
542    /// compiler for a number belonging to this frame alone.
543    pub compile_spans: CompileSpans,
544}
545
546impl CompiledFrame {
547    /// The frame's draws, in paint order (back-most first).
548    pub fn draws(&self) -> &[EngineDraw] {
549        &self.recorder.draws
550    }
551
552    /// The frame's whole strip buffer; a draw's `strip_range` indexes into it.
553    pub fn strip_buf(&self) -> &[vello_common::strip::Strip] {
554        &self.strips.strips
555    }
556
557    /// The alpha coverage the frame's strips reference.
558    pub fn alphas(&self) -> &[u8] {
559        &self.strips.alphas
560    }
561}
562
563/// Compiles a `frust_scene::Scene` into strips and draws.
564///
565/// Create one per surface and reuse it across frames — the retained
566/// [`StripGenerator`] is the point.
567#[derive(Debug)]
568pub struct SceneCompiler {
569    generator: StripGenerator,
570    /// The SIMD level the walk's generator was built at, so a helper's
571    /// generator is built at the same one (see [`parallel`]).
572    level: Level,
573    /// How many threads generate a frame's strips, the calling thread
574    /// included; one is the sequential walk. Starts at
575    /// [`parallel::default_workers`]; [`Self::set_parallel_workers`] decides
576    /// it per compiler.
577    parallel_workers: usize,
578    /// The estimated cost a frame's planned helper work must reach before a
579    /// helper is spawned for it (see [`DEFAULT_PARALLEL_MIN_COST`]). Starts at
580    /// that default, or `FRUST_ENGINE_PARALLEL_MIN_COST`;
581    /// [`Self::set_parallel_min_cost`] decides it per compiler.
582    parallel_min_cost: u64,
583    /// The helpers' generators and the per-job slots their runs wait in,
584    /// retained across frames for their allocations alone.
585    pool: Pool,
586    clips: ClipStack,
587    groups: GroupStack,
588    retained_stack: RetainedStack,
589    /// Whether a `PushRetained` bracket is recorded for the retained-page
590    /// store and a `DrawRetained` lowered rather than refused (see
591    /// [`Self::set_retained_layers`]). Off for a compiler built on its own.
592    retain: bool,
593    /// The [`CompiledFrame::retained`] entry of the outermost `PushRetained`
594    /// body the walk is inside, while it is inside one — what a hole punch or
595    /// an external texture drawn in the body marks unretainable.
596    retained_open: Option<usize>,
597    punches: Vec<StagedPunch>,
598    images: ImageResidency,
599    glyphs: GlyphPrepCache,
600    /// Which glyphs earn an atlas slot, and the entry map they live in.
601    ///
602    /// A sibling field of [`Self::images`] rather than a member of it: the two
603    /// share one allocator but decide different things, and every call that
604    /// needs both takes them as disjoint borrows of this struct (see
605    /// [`crate::text::atlas_policy`] for why the allocator is the residency's).
606    glyph_atlas: AtlasPolicy,
607    /// This frame's per-run routing decisions, in the order the scene records
608    /// its glyph runs.
609    ///
610    /// Filled by the collect walk at the head of [`Self::compile`] and consumed
611    /// by the draw walk one run at a time. Retained across frames only for its
612    /// allocation.
613    run_routes: Vec<RunRoute>,
614    /// How many of [`Self::run_routes`] the draw walk has consumed.
615    next_run: usize,
616    /// Scratch for the collect walk's distinct-glyph count, retained across
617    /// runs and frames for its allocation alone. A run's admission is charged
618    /// against the atlas budget at that count (see
619    /// [`crate::text::RunKey::distinct_glyphs`]), counted by sorting the run's
620    /// ids and dropping repeats; one buffer owned here is one not allocated
621    /// per run. It carries nothing between calls — `RunKey::for_run` empties it
622    /// before it counts.
623    run_glyph_ids: Vec<u32>,
624    /// Whether a glyph run resolved entirely out of the glyph atlas is drawn
625    /// as glyph quads (see [`crate::text`]). On unless
626    /// `FRUST_ENGINE_NO_GLYPH_QUADS` is set; [`Self::set_glyph_quads`] decides
627    /// it per compiler.
628    glyph_quads: bool,
629    /// Where a run's glyph quads wait for the run to finish, retained for its
630    /// allocations alone.
631    glyph_quad_scratch: GlyphQuadScratch,
632    /// Whether a run drawn as glyph quads may be drawn out of the glyph-atlas
633    /// policy's resident-run cache rather than through `glifo` (see
634    /// [`crate::text::ResidentPlan`]). On unless
635    /// `FRUST_ENGINE_NO_GLYPH_RUN_CACHE` is set; [`Self::set_glyph_run_cache`]
636    /// decides it per compiler.
637    glyph_run_cache: bool,
638    /// The font gate's answers for the faces drawn most recently, so a frame
639    /// of many short runs reads each face's table directory once (see
640    /// [`crate::text::FontFacts`]).
641    font_facts: FontFacts,
642    /// Whether a glyph run's outline is hinted before it is rasterized (see
643    /// [`crate::text`]'s module doc for the split this half of the policy
644    /// answers). Mobile-safe by default — `false`, the same "known nothing
645    /// about the device yet" reasoning [`Self::new`] gives
646    /// [`AtlasBudget::MOBILE`] — and set from the adapter's own class by
647    /// [`Self::for_caps`], or directly by [`Self::set_hint_text`] for a test
648    /// that wants either answer without a `TierCaps` in hand.
649    hint_text: bool,
650    /// The texel extent of every externally bound texture, so a
651    /// [`Command::SceneTexture`] can be lowered without this crate's compile
652    /// half knowing anything about `wgpu` (see
653    /// [`crate::compile::external`]). Written through
654    /// [`Self::bind_external_texture`]/[`Self::unbind_external_texture`],
655    /// which the renderer calls alongside its own view registry so the two
656    /// halves are always registered together.
657    externals: ExternalExtents,
658    /// The frame's own target extent — the `(width, height)` most recently
659    /// passed to [`Self::compile`] (or, before the first call, this
660    /// compiler's own construction size). Kept only so the `ShaderQuad` arm
661    /// can tell a quad deliberately culled by the shader-quad pre-pass's own
662    /// target-extent check (`crate::effects::shader_quad`) apart from one
663    /// whose pre-pass genuinely never ran — see [`shader_quad_is_culled`] and
664    /// [`note_shader_quad_unrendered`]/[`note_shader_quad_culled`].
665    frame_extent: (u16, u16),
666}
667
668impl SceneCompiler {
669    /// A compiler sized for a `width` x `height` viewport.
670    ///
671    /// The size is re-asserted on every [`compile`](Self::compile) call, so
672    /// this is only the initial allocation hint; pass the surface's current
673    /// size to avoid an immediate resize.
674    ///
675    /// Image residency starts on [`AtlasBudget::MOBILE`], the smaller of the
676    /// two tiers. A compiler built without an adapter in hand knows nothing
677    /// about the device it will end up on, and over-budgeting a phone costs
678    /// real memory while under-budgeting a desktop costs only an extra atlas
679    /// layer — call [`for_caps`](Self::for_caps) or
680    /// [`set_atlas_budget`](Self::set_atlas_budget) once the adapter is known.
681    pub fn new(width: u16, height: u16) -> Self {
682        Self::with_atlas_budget(width, height, AtlasBudget::MOBILE)
683    }
684
685    /// A compiler sized for a `width` x `height` viewport, with image
686    /// residency budgeted for `caps`' adapter and glyph hinting decided by
687    /// `caps`' device class.
688    ///
689    /// Hinting is turned on for a desktop-class adapter and left off for a
690    /// mobile one — the same `!`[`is_mobile_tier`] split
691    /// [`AtlasBudget::for_caps`] draws its own tier from, so a caller with an
692    /// adapter in hand only ever answers the mobile-or-desktop question once.
693    /// See [`crate::text`]'s module doc for why hinting defaults off and what
694    /// the other half of the policy — the transform predicate `glifo` applies
695    /// on top of this — is not this crate's to make.
696    pub fn for_caps(width: u16, height: u16, caps: &TierCaps) -> Self {
697        let mut compiler = Self::with_atlas_budget(width, height, AtlasBudget::for_caps(caps));
698        compiler.hint_text = !is_mobile_tier(caps);
699        compiler
700    }
701
702    /// Set whether a glyph run's outline is hinted before it is rasterized,
703    /// bypassing [`Self::for_caps`]' `TierCaps` reading.
704    ///
705    /// For a test that wants a chosen answer without building a `TierCaps` —
706    /// [`Self::new`] and [`Self::with_atlas_budget`] already default to the
707    /// mobile-safe `false`, so this is also how a caller that built one of
708    /// those turns hinting on.
709    pub fn set_hint_text(&mut self, hint_text: bool) {
710        self.hint_text = hint_text;
711    }
712
713    /// Set whether a glyph run resolved entirely out of the glyph atlas is
714    /// drawn as glyph quads — one rect instance per glyph, one record per run
715    /// and atlas layer — or the general way, one image record and one
716    /// generated rectangle per glyph (see [`crate::text`]).
717    ///
718    /// The per-compiler counterpart of `FRUST_ENGINE_NO_GLYPH_QUADS`, which
719    /// only sets the value a compiler starts with: a caller comparing the two
720    /// lowerings in one process sets it here rather than through a
721    /// process-global environment variable. Takes effect from the next run.
722    pub fn set_glyph_quads(&mut self, enabled: bool) {
723        self.glyph_quads = enabled;
724    }
725
726    /// Whether resident glyph runs are drawn as glyph quads.
727    #[must_use]
728    pub fn glyph_quads_enabled(&self) -> bool {
729        self.glyph_quads
730    }
731
732    /// Set whether a glyph run drawn as glyph quads may be drawn out of the
733    /// resident-run cache — its glyphs' slots remembered from an earlier
734    /// frame — instead of being resolved glyph by glyph through `glifo` (see
735    /// [`crate::text::ResidentPlan`]).
736    ///
737    /// The per-compiler counterpart of `FRUST_ENGINE_NO_GLYPH_RUN_CACHE`, on
738    /// the same terms as [`Self::set_glyph_quads`]: the variable sets the
739    /// value a compiler starts with, this decides it for one compiler. Takes
740    /// effect from the next run. The cache only ever serves quads, so turning
741    /// quads off turns it off too.
742    pub fn set_glyph_run_cache(&mut self, enabled: bool) {
743        self.glyph_run_cache = enabled;
744    }
745
746    /// Whether glyph runs may be drawn out of the resident-run cache.
747    #[must_use]
748    pub fn glyph_run_cache_enabled(&self) -> bool {
749        self.glyph_run_cache
750    }
751
752    /// Set whether retained brackets are recorded for a retained-page store.
753    ///
754    /// Off by default: a compiler built on its own has no store behind it, so
755    /// it lowers every `PushRetained` inline exactly as `frust_scene` tells a
756    /// renderer that does not retain to, and refuses a `DrawRetained` with
757    /// [`EngineError::RetainedKeyMissing`]. The renderer turns it on for the
758    /// compiler it owns (unless `FRUST_ENGINE_NO_LAYERS` is set): each
759    /// outermost bracket's body then becomes a recorded layer whatever its
760    /// alpha and is listed in [`CompiledFrame::retained`], and a `DrawRetained`
761    /// becomes an empty layer at its painter-order position (see
762    /// [`layers`]). Takes effect from the next frame.
763    pub fn set_retained_layers(&mut self, enabled: bool) {
764        self.retain = enabled;
765    }
766
767    /// Whether retained brackets are recorded for a retained-page store.
768    #[must_use]
769    pub fn retained_layers_enabled(&self) -> bool {
770        self.retain
771    }
772
773    /// How many glyph runs the resident-run cache currently remembers.
774    ///
775    /// Bounded by [`MAX_RESIDENT_GLYPH_RUNS`] records (and a byte budget
776    /// beside it), however many distinct runs have been drawn.
777    #[must_use]
778    pub fn resident_glyph_run_count(&self) -> usize {
779        self.glyph_atlas.resident_runs()
780    }
781
782    /// Set how many threads generate a frame's strips, the calling thread
783    /// included (see the `parallel` module). One — or zero, read as one — is the
784    /// sequential walk: no planning pass, no helper thread.
785    ///
786    /// The per-compiler counterpart of `FRUST_ENGINE_NO_PARALLEL`, on the same
787    /// terms as [`Self::set_glyph_quads`]: the variable sets the value a
788    /// compiler starts with ([`DEFAULT_PARALLEL_WORKERS`] capped by the
789    /// machine, or one with it set), this decides it for one compiler. Takes
790    /// effect from the next frame. The frame a compiler produces is the same
791    /// at every setting. Capped at [`MAX_PARALLEL_WORKERS`] — every
792    /// helper is a thread spawned per frame and a strip generator kept across
793    /// them, so a count far past any machine would only pay for threads that
794    /// cannot run; the variable's path is capped by the machine's own thread
795    /// count instead, so that a test can ask for the helper count it means to
796    /// exercise on any host.
797    pub fn set_parallel_workers(&mut self, workers: usize) {
798        self.parallel_workers = workers.clamp(1, parallel::MAX_PARALLEL_WORKERS);
799    }
800
801    /// How many threads generate a frame's strips, the calling thread
802    /// included.
803    #[must_use]
804    pub fn parallel_workers(&self) -> usize {
805        self.parallel_workers
806    }
807
808    /// Set the estimated cost a frame's planned helper work must reach before
809    /// a helper is spawned for it (see [`DEFAULT_PARALLEL_MIN_COST`] for the
810    /// unit). Zero spawns for any frame with a single planned command, which
811    /// is how a test drives the helpers over a scene too small to earn them.
812    pub fn set_parallel_min_cost(&mut self, cost: u64) {
813        self.parallel_min_cost = cost;
814    }
815
816    /// The estimated cost a frame's planned work must reach before a helper
817    /// is spawned.
818    #[must_use]
819    pub fn parallel_min_cost(&self) -> u64 {
820        self.parallel_min_cost
821    }
822
823    /// A compiler sized for a `width` x `height` viewport, with image
824    /// residency budgeted explicitly.
825    pub fn with_atlas_budget(width: u16, height: u16, budget: AtlasBudget) -> Self {
826        let level = Level::try_detect().unwrap_or(Level::baseline());
827        let images = ImageResidency::new(budget);
828        Self {
829            generator: StripGenerator::new(width, height, level),
830            level,
831            parallel_workers: parallel::default_workers(),
832            parallel_min_cost: config::parallel_min_cost().unwrap_or(DEFAULT_PARALLEL_MIN_COST),
833            pool: Pool::default(),
834            clips: ClipStack::new(),
835            groups: GroupStack::new(),
836            retained_stack: RetainedStack::new(),
837            retain: false,
838            retained_open: None,
839            punches: Vec::new(),
840            // Built from the residency, so the policy's page geometry is read
841            // off the allocator it will pack into rather than derived a second
842            // time from the same budget.
843            glyph_atlas: glyph_atlas_policy(&images),
844            images,
845            glyphs: GlyphPrepCache::default(),
846            run_routes: Vec::new(),
847            next_run: 0,
848            run_glyph_ids: Vec::new(),
849            glyph_quads: !config::glyph_quads_disabled(),
850            glyph_quad_scratch: GlyphQuadScratch::default(),
851            glyph_run_cache: !config::glyph_run_cache_disabled(),
852            font_facts: FontFacts::default(),
853            hint_text: false,
854            externals: ExternalExtents::new(),
855            frame_extent: (width, height),
856        }
857    }
858
859    /// Records an externally owned texture as bound under `id` at `size`
860    /// texels, answering whether the extent is one a paint can be composed
861    /// against at all (see [`ExternalExtents::bind`]).
862    ///
863    /// Only the extent: the view the frame's passes sample is the renderer's
864    /// (see [`crate::gpu::bindings`]). A caller that registers one half without
865    /// the other gets a texture that draws nothing, which is why the renderer's
866    /// own `bind_texture` writes both.
867    pub fn bind_external_texture(&mut self, id: u64, size: (u32, u32)) -> bool {
868        self.externals.bind(id, size)
869    }
870
871    /// Forgets the extent recorded for `id`, so a `SceneTexture` naming it
872    /// draws nothing again.
873    pub fn unbind_external_texture(&mut self, id: u64) {
874        self.externals.unbind(id);
875    }
876
877    /// The externally bound extents this compiler resolves against.
878    #[must_use]
879    pub fn externals(&self) -> &ExternalExtents {
880        &self.externals
881    }
882
883    /// The glyph entry map, for the caller that has to drain the pages this
884    /// compiler's last frame dirtied.
885    ///
886    /// The engine produces no glyph pixels itself: `glifo` records the fills
887    /// that rasterize a newly cached glyph into a per-page recorder, and
888    /// [`crate::gpu::atlas::AtlasRenderer::render_pending`] replays them into
889    /// the atlas array before the frame's scene pass. That replay needs the map
890    /// itself, which is what this hands over.
891    pub fn glyph_atlas_mut(&mut self) -> &mut GlyphAtlas {
892        self.glyph_atlas.atlas_mut()
893    }
894
895    /// How many glyphs this compiler currently holds resident in the atlas.
896    ///
897    /// Observational, and the counter the policy's whole claim rests on: a page
898    /// of static text reaches a fixed number here and stays there, while an
899    /// animating size never contributes at all.
900    #[must_use]
901    pub fn glyph_atlas_entries(&self) -> usize {
902        self.glyph_atlas.entry_count()
903    }
904
905    /// Whether any glyph may be cached at all — `false` under
906    /// `FRUST_ENGINE_NO_ATLAS`.
907    #[must_use]
908    pub fn glyph_atlas_enabled(&self) -> bool {
909        self.glyph_atlas.is_enabled()
910    }
911
912    /// The glyph-atlas policy's counters for the last compiled frame — what
913    /// the `frust-perf enc` line's miss, eviction and run-key counts read.
914    #[cfg(feature = "perf-trace")]
915    pub(crate) fn glyph_atlas_trace(&self) -> &crate::text::atlas_policy::AtlasTrace {
916        self.glyph_atlas.trace()
917    }
918
919    /// The images this compiler currently holds resident.
920    pub fn images(&self) -> &ImageResidency {
921        &self.images
922    }
923
924    /// Record that a compiled frame's
925    /// [`image_evictions`](CompiledFrame::image_evictions) and
926    /// [`image_uploads`](CompiledFrame::image_uploads) have been serviced
927    /// against a live atlas array.
928    ///
929    /// The other half of the plan seam: [`compile`](Self::compile) reports the
930    /// plan without consuming it, and it goes on being reported — identically,
931    /// never duplicated — until this is called. Call it only once the regions
932    /// have really been written, so a frame refused after compiling keeps its
933    /// uploads for the next frame that is not (see [`crate::cache::images`]'s
934    /// module doc).
935    pub fn acknowledge_image_plan(&mut self) {
936        self.images.acknowledge_plan();
937    }
938
939    /// Whether `glifo` still holds recorded page commands, bitmap uploads or
940    /// freed rectangles that have not reached the atlas array.
941    ///
942    /// The gate a caller drives
943    /// [`crate::gpu::atlas::AtlasRenderer::render_pending`] from. Deliberately
944    /// *not* [`CompiledFrame::atlas_glyph_draws`]: `glifo` dirties a page when
945    /// it inserts an entry, not when a draw survives, so a run scrolled behind
946    /// a clip inserts entries and records fills while contributing no draw at
947    /// all. Gating on draws leaves those commands recorded — and a recorded
948    /// command outliving the slot it names is old ink replayed into whichever
949    /// glyph was let that rectangle next.
950    ///
951    /// Stays `true` across a frame the caller refuses, exactly as the image
952    /// plan does, until [`acknowledge_glyph_replay`](Self::acknowledge_glyph_replay).
953    #[must_use]
954    pub fn glyph_replay_pending(&self) -> bool {
955        self.glyph_atlas.replay_pending()
956    }
957
958    /// Record that the recorded page commands were replayed into the atlas
959    /// array.
960    ///
961    /// Also what lets `glifo`'s eviction pass resume: while a replay is
962    /// outstanding the policy defers ageing, so that no rectangle a recorded
963    /// command still names can be freed and re-let underneath it (see
964    /// [`crate::text::atlas_policy`]).
965    pub fn acknowledge_glyph_replay(&mut self) {
966        self.glyph_atlas.acknowledge_replay();
967    }
968
969    /// Whether any rectangle freed by glyph eviction is still waiting to be
970    /// zeroed.
971    #[must_use]
972    pub fn glyph_clears_pending(&self) -> bool {
973        self.glyph_atlas.has_pending_clears()
974    }
975
976    /// Record that this frame's [`CompiledFrame::glyph_clears`] were written to
977    /// the atlas array.
978    ///
979    /// The glyph half of the same re-offer contract
980    /// [`acknowledge_image_plan`](Self::acknowledge_image_plan) closes for
981    /// images: [`compile`](Self::compile) reports the clears without consuming
982    /// them, and goes on reporting the same ones, until a caller that really
983    /// issued the writes says so. A frame compiled and then dropped therefore
984    /// leaves no rectangle holding an evicted glyph's pixels.
985    pub fn acknowledge_glyph_clears(&mut self) {
986        self.glyph_atlas.acknowledge_clears();
987    }
988
989    /// Re-budget image residency, dropping every image currently resident.
990    ///
991    /// The atlas geometry is what an allocation's coordinates mean, so a change
992    /// to it invalidates every rectangle already handed out: residency starts
993    /// over and each image re-uploads on the next frame that draws it. A caller
994    /// that owns the atlas texture must recreate it at the new extent in the
995    /// same step — this is an adapter-change or start-up operation, never a
996    /// per-frame one.
997    pub fn set_atlas_budget(&mut self, budget: AtlasBudget) {
998        self.set_image_residency(ImageResidency::new(budget));
999    }
1000
1001    /// Replace this compiler's image residency wholesale, dropping every image
1002    /// currently resident.
1003    ///
1004    /// The same invalidation [`set_atlas_budget`](Self::set_atlas_budget)
1005    /// carries, exposed for the residencies a budget alone cannot express — a
1006    /// deliberately [disabled](ImageResidency::disabled) one, or one a caller
1007    /// built against an adapter's own capabilities.
1008    ///
1009    /// The glyph policy is rebuilt alongside it, and for the same reason: its
1010    /// slots came out of the allocator being replaced, so every one of them
1011    /// names a rectangle of a geometry that no longer exists. Text re-caches on
1012    /// the next frame that draws it, exactly as an image re-uploads.
1013    pub fn set_image_residency(&mut self, images: ImageResidency) {
1014        self.images = images;
1015        self.glyph_atlas = glyph_atlas_policy(&self.images);
1016    }
1017
1018    /// Compile `scene` for a `size` viewport, with `root` applied ahead of
1019    /// every command's own transform.
1020    ///
1021    /// # Errors
1022    ///
1023    /// [`EngineError::TargetTooLarge`] when `size` cannot be rounded up to
1024    /// whole tiles inside `u16`; [`EngineError::InvalidTransform`] when a
1025    /// composed transform is non-finite and so maps geometry to coordinates no
1026    /// `u16` pixel can hold; and [`EngineError::InvalidGeometry`] when a
1027    /// command the compiler lowers carries non-finite geometry of its own (see
1028    /// [`check_geometry`]). All three are refused before any strip is
1029    /// generated — the frame path returns errors and never panics.
1030    pub fn compile(
1031        &mut self,
1032        scene: &Scene,
1033        root: Affine,
1034        size: (u16, u16),
1035    ) -> Result<CompiledFrame, EngineError> {
1036        // The compiler's half of the encode's CPU accounting; the renderer
1037        // laps the rest of the call around it (see [`CompileSpans`]). Free
1038        // without `perf-trace`.
1039        let mut clock = PhaseClock::start();
1040        let (width, height) = size;
1041        check_tile_addressable(width, height)?;
1042        check_finite(root)?;
1043
1044        // The whole scene is refused up front rather than mid-walk, so a
1045        // rejected frame never leaves half its draws recorded.
1046        let mut backdrops = 0_usize;
1047        for command in scene.commands() {
1048            if matches!(command, Command::BackdropBlur { .. }) {
1049                backdrops = backdrops.saturating_add(1);
1050            }
1051            if let Some(transform) = command_transform(command) {
1052                check_finite(root * transform)?;
1053            }
1054            if let Command::DrawRetained { key, .. } = command
1055                && !self.retain
1056            {
1057                // This compiler retains nothing, so no ledger can have
1058                // declared a key resident and a `DrawRetained` here is a
1059                // recorder error. Refused with the whole frame, before any
1060                // strip is generated; the caller logs the reason and skips
1061                // the frame. With retention on, whether the key is resident is
1062                // the renderer's store's to answer, after the walk.
1063                return Err(EngineError::RetainedKeyMissing { key: *key });
1064            }
1065            check_geometry(command)?;
1066        }
1067        // Counted over the commands the scene records, not over the ones that
1068        // end up drawing: the bound is a statement about the display list a
1069        // widget tree produced, refused whole before a strip is generated.
1070        if backdrops > MAX_BACKDROPS_PER_FRAME {
1071            return Err(EngineError::TooManyBackdrops {
1072                recorded: backdrops,
1073                max: MAX_BACKDROPS_PER_FRAME,
1074            });
1075        }
1076
1077        let validate = clock.lap();
1078
1079        self.generator.reset(width, height);
1080        self.frame_extent = (width, height);
1081        self.clips.reset();
1082        self.groups.reset();
1083        self.retained_stack.reset();
1084        self.retained_open = None;
1085        self.punches.clear();
1086        // Ahead of the walk, so a rectangle this frame's reap frees is
1087        // available to this frame's own allocations and its clear is ordered
1088        // ahead of their uploads.
1089        self.images.begin_frame();
1090        // Once per compiled frame, which is the cadence `glifo` ages its
1091        // outline entries by. Ahead of the walk rather than after it for the
1092        // same reason as the reap above: the glyphs this frame is about to
1093        // draw should be stamped as used *after* the ageing pass, not before
1094        // it.
1095        self.glyphs.maintain();
1096        let prepare = clock.lap();
1097
1098        // Phase one of the frame: every glyph run is *routed* before any of
1099        // them is drawn. Opened here, beside the residency's own frame, because
1100        // the two age against the same clock.
1101        // Nothing is rasterized in that phase and no slot is allocated — the
1102        // walk only asks the policy which runs may be cached, which is what
1103        // records their sizes against the animation guard before a single glyph
1104        // reaches `glifo`. Closing the phase hands back the rectangles last
1105        // frame's eviction freed, to be zeroed ahead of anything this frame
1106        // writes (see [`CompiledFrame::glyph_clears`]).
1107        self.glyph_atlas.begin_frame();
1108        self.classify_runs(scene, root);
1109        let classify = clock.lap();
1110
1111        let glyph_clears = self
1112            .glyph_atlas
1113            .build(self.images.allocator_mut(), |_| {
1114                // Unreachable: the collect walk claims no glyph, because the
1115                // allocation and the rasterization of a cached glyph are
1116                // `glifo`'s own — it keys, packs and records every one of them
1117                // itself once a run reaches it with the cacher enabled. So the
1118                // pass this closes carries clears and nothing else.
1119                None
1120            })
1121            .clears;
1122        let admit = clock.lap();
1123
1124        let mut frame = CompiledFrame {
1125            strips: StripStorage::new(GenerationMode::Append),
1126            recorder: CommandRecorder::new(width, height),
1127            encoded_paints: Vec::new(),
1128            clears: Vec::new(),
1129            lut_requests: Vec::new(),
1130            fast_rect_draws: 0,
1131            scissor_clips: 0,
1132            mask_clips: 0,
1133            clip_mask_strips: 0,
1134            image_evictions: Vec::new(),
1135            image_forgets: Vec::new(),
1136            image_uploads: Vec::new(),
1137            atlas_layers: 0,
1138            image_draws: 0,
1139            skipped_images: 0,
1140            external_draws: 0,
1141            skipped_externals: 0,
1142            glyph_draws: 0,
1143            skipped_glyphs: 0,
1144            atlas_glyph_draws: 0,
1145            glyph_slots: Vec::new(),
1146            glyph_quads: Vec::new(),
1147            glyph_run_paints: Vec::new(),
1148            resident_glyph_runs: 0,
1149            parallel_draws: 0,
1150            glyph_clears,
1151            retained: Vec::new(),
1152            retained_superseded: Vec::new(),
1153            backdrops: Vec::new(),
1154            compile_spans: CompileSpans::default(),
1155        };
1156        let mut depth = DepthCounter::new();
1157
1158        // The walk proper, with its shape commands' strips generated by
1159        // helper threads behind it whenever the plan earns one (see
1160        // [`parallel`]). Planned and spawned inside the walk's lap, and the
1161        // helpers joined before it closes, so the parallel walk's overhead is
1162        // charged to the phase it shortens.
1163        let helpers = self.parallel_workers.saturating_sub(1);
1164        let plan = if helpers > 0 {
1165            parallel::plan_with(
1166                scene,
1167                root,
1168                Rect::new(0.0, 0.0, f64::from(width), f64::from(height)),
1169                self.retain,
1170            )
1171        } else {
1172            parallel::Plan::default()
1173        };
1174        if helpers > 0 && !plan.is_empty() && plan.cost() >= self.parallel_min_cost {
1175            // Moved out for the walk's duration: the helpers borrow it while
1176            // the walk borrows the compiler. It is put back on the normal
1177            // return only: a helper panic propagates out of the scope, which
1178            // aborts a release build (`panic = "abort"` in the release profile)
1179            // and, in a dev, test or downstream build that unwinds, leaves the
1180            // compiler with an empty pool — the sequential walk plus a fresh
1181            // pool's allocations on the next frame, slower and never wrong.
1182            let mut pool = std::mem::take(&mut self.pool);
1183            let level = self.level;
1184            pool.run(&plan, helpers, level, (width, height), |merge| {
1185                for (index, command) in scene.commands().iter().enumerate() {
1186                    self.compile_command(index, command, root, &mut frame, &mut depth, Some(merge));
1187                }
1188            });
1189            self.pool = pool;
1190        } else {
1191            for (index, command) in scene.commands().iter().enumerate() {
1192                self.compile_command(index, command, root, &mut frame, &mut depth, None);
1193            }
1194        }
1195        let walk = clock.lap();
1196
1197        self.close_open_groups(&mut frame);
1198        self.generate_punches(&mut frame);
1199
1200        // Closes the frame the policy opened: ages `glifo`'s entry map, frees
1201        // whatever aged out back to the shared allocator, and takes the clear
1202        // rects that eviction produced — which belong to the *next* frame's
1203        // pass, not this one's.
1204        self.glyph_atlas.end_frame(self.images.allocator_mut());
1205
1206        frame.scissor_clips = self.clips.scissor_clips();
1207        frame.mask_clips = self.clips.mask_clips();
1208        frame.clip_mask_strips = self.clips.mask_strips();
1209        // Copied rather than drained. Compiling is not the moment residency
1210        // becomes true — this frame can still be refused by the caller after it
1211        // returns, and a refused frame never reaches the atlas. The plan stays
1212        // pending in the residency, re-offered on every later frame, until the
1213        // consumer that actually wrote the regions acknowledges it through
1214        // [`acknowledge_image_plan`](SceneCompiler::acknowledge_image_plan).
1215        let (evictions, uploads) = self.images.plan();
1216        frame.image_evictions = evictions;
1217        frame.image_forgets = self.images.evictions().to_vec();
1218        frame.image_uploads = uploads;
1219        frame.atlas_layers = self.images.layers();
1220
1221        // Field by field rather than as a whole struct, so the glyph subset
1222        // the walk accumulated into `frame` survives. Last, so `finish` covers
1223        // every phase above it and the six partition the call rather than
1224        // sampling parts of it.
1225        frame.compile_spans.validate = validate;
1226        frame.compile_spans.prepare = prepare;
1227        frame.compile_spans.classify = classify;
1228        frame.compile_spans.admit = admit;
1229        frame.compile_spans.walk = walk;
1230        frame.compile_spans.finish = clock.lap();
1231
1232        // After the last lap, deliberately: the line reports this frame's
1233        // residency, and a phase that included the cost of reporting on itself
1234        // would be measuring the instrumentation rather than the compile.
1235        #[cfg(feature = "perf-trace")]
1236        note_image_pressure(&frame, &self.images);
1237
1238        Ok(frame)
1239    }
1240
1241    fn compile_command(
1242        &mut self,
1243        index: usize,
1244        command: &Command,
1245        root: Affine,
1246        frame: &mut CompiledFrame,
1247        depth: &mut DepthCounter,
1248        merge: Option<&Merge<'_, '_>>,
1249    ) {
1250        // The frame root with any open retained bracket's presentation scale
1251        // composed ahead of it (see [`layers`]). The identity outside a
1252        // bracket, so this is the plain frame root for every frame that
1253        // records none.
1254        let combined = root * self.retained_stack.correction();
1255
1256        // Claimed for every command, drawn or not, ahead of every early return
1257        // below: claiming is what moves the walk's position past a planned
1258        // command, which is what stops a helper from starting work the walk
1259        // has already gone by (see [`parallel`]).
1260        let claim = merge.and_then(|merge| merge.claim(index, combined));
1261
1262        // Taken here rather than inside the glyph arm, and taken for every
1263        // glyph run whether or not it goes on to be drawn: the collect walk
1264        // classified one run per `Command::GlyphRun` in this same order, so
1265        // consuming one per `Command::GlyphRun` is what keeps the two walks in
1266        // step through every early return below.
1267        let route = match command {
1268            Command::GlyphRun(_) => self.take_run_route(),
1269            _ => None,
1270        };
1271
1272        // A correction composes a transform the up-front walk never saw, and
1273        // the product can leave the finite device grid even though both
1274        // factors are on it. Such a command draws nothing rather than refusing
1275        // the frame: the refusal is the up-front walk's to make over the
1276        // numbers a scene actually carries, and a bracket's presentation scale
1277        // is not one of them. Only a command *inside* a retained bracket can
1278        // land here at all — outside one the composition is the frame root's,
1279        // which that walk already checked.
1280        //
1281        // Drawing nothing is not the same as doing nothing: a command that
1282        // opens a bracket still has to open one, or its pop would close the
1283        // bracket around it instead. So a bracket lands blocked rather than
1284        // absent, which draws nothing inside it and balances its own pop.
1285        let on_grid = command_on_grid(command, combined);
1286        if !on_grid {
1287            match command {
1288                Command::PushClip { .. }
1289                | Command::PushClipRounded { .. }
1290                | Command::PushClipPath { .. }
1291                | Command::PushLayer { .. } => self.open_blocked_group(),
1292                // The correction is the outermost bracket's, so a bracket
1293                // reaching here is a nested one, whose presentation is ignored
1294                // anyway; only its depth has to be counted. The substitution is
1295                // still made through [`retained_entry`] rather than inline,
1296                // because it is the collect walk's to make identically (see
1297                // [`Self::classify_runs`]).
1298                Command::PushRetained {
1299                    key,
1300                    rect,
1301                    scale,
1302                    transform,
1303                    ..
1304                } => {
1305                    let (scale, transform) = retained_entry(on_grid, *scale, *transform);
1306                    self.retained_stack.enter(*rect, scale, transform);
1307                    self.supersede_retained(frame, *key);
1308                }
1309                _ => {}
1310            }
1311            return;
1312        }
1313
1314        match command {
1315            // A shape is its geometry's coverage under its brush; the
1316            // geometry is resolved by the one function a helper resolves it by
1317            // too, so a helper's run and the walk's own can never differ.
1318            Command::FillRect {
1319                brush, transform, ..
1320            }
1321            | Command::RoundedRect {
1322                brush, transform, ..
1323            }
1324            | Command::Line {
1325                brush, transform, ..
1326            }
1327            | Command::Path {
1328                brush, transform, ..
1329            } => {
1330                if let Some(geometry) = Geometry::of(command, combined) {
1331                    self.draw_shape(
1332                        frame,
1333                        depth,
1334                        PaintSource::Brush(brush),
1335                        combined * *transform,
1336                        geometry,
1337                        claim,
1338                    );
1339                }
1340            }
1341            Command::PushClip { rect, transform } => {
1342                let transform = combined * *transform;
1343                self.clips.push_rect(*rect, transform, &mut self.generator);
1344                self.groups.push_clip(transform.transform_rect_bbox(*rect));
1345            }
1346            Command::PushClipRounded {
1347                rect,
1348                radii,
1349                transform,
1350            } => {
1351                let transform = combined * *transform;
1352                self.clips.push_rounded(
1353                    *rect,
1354                    rounded_rect_radii(*radii),
1355                    transform,
1356                    &mut self.generator,
1357                );
1358                self.groups.push_clip(transform.transform_rect_bbox(*rect));
1359            }
1360            // An outline is always a coverage mask, never a layer page — so a
1361            // `PushRetained` body opened under it finds the mask open and is
1362            // marked unretainable on exactly a rounded clip's terms (see
1363            // [`Self::open_retained`]). The group is confined to the outline's
1364            // device bounding box, which is what a clear hoisted past it is
1365            // limited to.
1366            Command::PushClipPath { path, transform } => {
1367                let transform = combined * *transform;
1368                self.clips.push_path(path, transform, &mut self.generator);
1369                self.groups
1370                    .push_clip(transform.transform_rect_bbox(path.bounding_box()));
1371            }
1372            Command::PushLayer {
1373                rect,
1374                alpha,
1375                transform,
1376            } => {
1377                self.open_layer(frame, *rect, *alpha, combined * *transform);
1378            }
1379            // One bracket stack serves all three kinds, so whichever pop
1380            // arrives closes the innermost open bracket (see [`layers`]). A pop
1381            // with nothing open is ignored: an unbalanced widget tree must not
1382            // be able to lift a bracket a sibling still relies on.
1383            Command::PopClip | Command::PopLayer => self.close_group(frame),
1384            Command::ClearRect { rect, transform } => {
1385                // A punch is hoisted out of every bracket to the frame root, so
1386                // a page could never carry it: a body that punches is drawn
1387                // inline every frame.
1388                self.mark_retained_body_unretainable(frame);
1389                let transform = combined * *transform;
1390                // Hoisted here rather than at the end of the frame because
1391                // this is the only point the brackets confining it are still
1392                // open; its coverage is generated once the frame's draws are
1393                // done (see [`clear`]).
1394                let punch = clear::punch_rect(*rect, transform, self.groups.bounds());
1395                if let Some(device) = punch {
1396                    self.punches.push(StagedPunch {
1397                        device,
1398                        depth: depth.advance(),
1399                    });
1400                }
1401            }
1402            Command::PushRetained {
1403                key,
1404                rect,
1405                alpha,
1406                scale,
1407                transform,
1408            } => {
1409                let mut listed = false;
1410                if self.retained_stack.enter(*rect, *scale, *transform) {
1411                    // The bracket's own correction is the one that applies to
1412                    // the layer it opens, so the transform is recomposed here
1413                    // rather than reusing `combined` from before the entry.
1414                    let corrected = root * self.retained_stack.correction() * *transform;
1415                    if check_finite(corrected).is_ok() {
1416                        if self.retain {
1417                            self.open_retained(frame, *key, *rect, *alpha, corrected);
1418                            listed = true;
1419                        } else if *alpha < 1.0 {
1420                            self.open_layer(frame, *rect, *alpha, corrected);
1421                            self.retained_stack.record_layer(self.groups.depth());
1422                        }
1423                    }
1424                }
1425                // A nested bracket is painted as part of the outermost body,
1426                // so its own key's page receives nothing this frame.
1427                if !listed {
1428                    self.supersede_retained(frame, *key);
1429                }
1430            }
1431            Command::PopRetained => {
1432                let closes = self.retained_stack.leave(self.groups.depth());
1433                if closes {
1434                    self.close_group(frame);
1435                }
1436                // The outermost bracket just ended. A body whose own group a
1437                // stray pop inside it already closed drew its tail outside the
1438                // layer, so the layer is not the body and is never retained.
1439                if !self.retained_stack.is_open()
1440                    && let Some(index) = self.retained_open.take()
1441                    && !closes
1442                {
1443                    mark_unretainable(frame, index);
1444                }
1445            }
1446            // Refused by the up-front walk in [`Self::compile`] when retention
1447            // is off, so it reaches the lowering only with a store behind it.
1448            Command::DrawRetained {
1449                key,
1450                rect,
1451                transform,
1452                alpha,
1453                scale,
1454            } => {
1455                if self.retain {
1456                    let mapped = combined * *transform * Affine::scale_about(*scale, rect.center());
1457                    self.record_draw_retained(frame, depth, *key, *rect, *alpha, mapped);
1458                }
1459            }
1460            // An image is its destination rectangle's coverage under an image
1461            // paint — the same two rectangle paths a solid fill takes, so a
1462            // pixel-aligned image costs no flattening either.
1463            Command::Image {
1464                data,
1465                dest,
1466                transform,
1467            } => {
1468                if let Some(geometry) = Geometry::of(command, combined) {
1469                    let source = PaintSource::Image { data, dest: *dest };
1470                    self.draw_shape(frame, depth, source, combined * *transform, geometry, claim);
1471                }
1472            }
1473            // The strip generator rasterizes the padded bounding rectangle,
1474            // not `rect` itself and not a rounded shape — see [`blur_rrect`]'s
1475            // module doc for why. It takes the same fast rectangle path a fill
1476            // or an image does whenever that padded rectangle lands
1477            // pixel-aligned under the composed transform.
1478            Command::BlurredRoundedRect {
1479                rect,
1480                radii,
1481                std_dev,
1482                color,
1483                transform,
1484            } => {
1485                if let Some(geometry) = Geometry::of(command, combined) {
1486                    let source = PaintSource::BlurredRect {
1487                        rect: *rect,
1488                        radii: *radii,
1489                        std_dev: *std_dev,
1490                        color: *color,
1491                    };
1492                    self.draw_shape(frame, depth, source, combined * *transform, geometry, claim);
1493                }
1494            }
1495            Command::GlyphRun(run) => {
1496                // Two clock reads a run, free without `perf-trace` (see
1497                // [`PhaseClock`]); the accumulated total is a subset of the
1498                // walk this arm runs inside.
1499                let mut clock = PhaseClock::start();
1500                self.compile_glyph_run(run, combined * run.transform, route, frame, depth);
1501                frame.compile_spans.glyphs = frame.compile_spans.glyphs.saturating_add(clock.lap());
1502            }
1503            // A fragment program's own pixels were produced before the frame
1504            // was compiled, into a texture registered under an id derived from
1505            // the program alone (`crate::effects::shader_quad`). From here on
1506            // the quad is an external texture like any other — the pre-pass is
1507            // the only thing that distinguishes it.
1508            Command::ShaderQuad {
1509                program,
1510                dest,
1511                transform,
1512                ..
1513            } => {
1514                // A fragment program's pixels change with no command changing,
1515                // so a body drawing one is never retained.
1516                self.mark_retained_body_unretainable(frame);
1517                if config::shader_effects_disabled() {
1518                    note_shader_effects_disabled();
1519                    return;
1520                }
1521                let id = shader_quad_texture_id(program.id());
1522                if self.externals.get(id).is_none() {
1523                    // No pre-pass ran for this program, its shader failed to
1524                    // compile, or this exact quad was deliberately culled by
1525                    // the pre-pass's own target-extent check (see
1526                    // `shader_quad_is_culled`) — an expected, routine outcome
1527                    // told apart from the other two so it is never reported
1528                    // through the missing-pre-pass warning below.
1529                    if shader_quad_is_culled(*dest, combined * *transform, self.frame_extent) {
1530                        note_shader_quad_culled(program.id());
1531                    } else {
1532                        // Reported in its own words rather than as an
1533                        // unregistered scene texture, whose id would name
1534                        // nothing a reader could look up.
1535                        note_shader_quad_unrendered(program.id());
1536                    }
1537                    frame.skipped_externals = frame.skipped_externals.saturating_add(1);
1538                    return;
1539                }
1540                self.draw_external_texture(id, *dest, combined * *transform, frame, depth);
1541            }
1542            Command::SceneTexture {
1543                id,
1544                dest,
1545                transform,
1546            } => {
1547                // An externally bound texture's pixels change with no command
1548                // changing (a camera, a video, a 3D pass), so a body drawing
1549                // one is never retained.
1550                self.mark_retained_body_unretainable(frame);
1551                self.draw_external_texture(*id, *dest, combined * *transform, frame, depth);
1552            }
1553            Command::BackdropBlur {
1554                rect,
1555                radius,
1556                transform,
1557            } => {
1558                // A page is rasterized offscreen, where there is no frame
1559                // beneath the body to sample, so a body blurring its backdrop
1560                // is drawn inline every frame — on exactly a punch's terms.
1561                self.mark_retained_body_unretainable(frame);
1562                self.record_backdrop(frame, depth, *rect, *radius, combined * *transform);
1563            }
1564        }
1565    }
1566
1567    /// Record a `Command::BackdropBlur`: a layer of its own holding the
1568    /// rectangle's coverage, listed in [`CompiledFrame::backdrops`].
1569    ///
1570    /// The layer is what places the backdrop at its painter-order position in
1571    /// the recording — the node the scheduler cuts the frame's surface round
1572    /// at — and it opens no clip and composites at full opacity, so a
1573    /// scheduler that served it as an ordinary layer would inline it. Its one
1574    /// draw is the rectangle generated under the clip stack exactly like a
1575    /// fill's, so a scissor trims it and a coverage mask (a rounded or path
1576    /// clip) masks it; it is painted transparent, because what it really
1577    /// paints — the blurred page — is the renderer's to bind once the page
1578    /// exists. A rectangle producing no coverage records the empty layer and
1579    /// no backdrop, which then costs nothing anywhere.
1580    fn record_backdrop(
1581        &mut self,
1582        frame: &mut CompiledFrame,
1583        depth: &mut DepthCounter,
1584        rect: Rect,
1585        radius: f64,
1586        transform: Affine,
1587    ) {
1588        let Some(sigma) = backdrop_sigma(radius, transform) else {
1589            return;
1590        };
1591        let (width, height) = self.frame_extent;
1592        let Some(region) =
1593            backdrop_region(transform.transform_rect_bbox(rect), sigma, (width, height))
1594        else {
1595            return;
1596        };
1597
1598        let layer = u32::try_from(frame.recorder.layers.len()).unwrap_or(u32::MAX);
1599        frame.recorder.push_layer(layers::layer_props(1.0), None);
1600        let transparent = Brush::Solid(Color::TRANSPARENT);
1601        let recorded = self.record(
1602            frame,
1603            depth,
1604            PaintSource::Brush(&transparent),
1605            transform,
1606            Geometry::rect(rect, transform),
1607            None,
1608        );
1609        frame.recorder.pop_layer();
1610
1611        if recorded && let Some(draw) = frame.draws().last() {
1612            frame.backdrops.push(Backdrop {
1613                layer,
1614                depth: draw.depth,
1615                region,
1616                sigma,
1617            });
1618        }
1619    }
1620
1621    /// Record the texture bound under `id` scaled to fill `dest` under
1622    /// `transform` — the body both [`Command::SceneTexture`] and
1623    /// [`Command::ShaderQuad`] lower to, since the only thing separating them
1624    /// is where the texels came from.
1625    ///
1626    /// An externally owned texture is its destination rectangle's coverage
1627    /// under an image paint that samples the caller's texture rather than the
1628    /// atlas — the same two rectangle paths [`Command::Image`] takes, so a
1629    /// pixel-aligned one costs no flattening either.
1630    fn draw_external_texture(
1631        &mut self,
1632        id: u64,
1633        dest: Rect,
1634        transform: Affine,
1635        frame: &mut CompiledFrame,
1636        depth: &mut DepthCounter,
1637    ) {
1638        let source = PaintSource::SceneTexture { id, dest };
1639        let geometry = Geometry::rect(dest, transform);
1640        self.draw_shape(frame, depth, source, transform, geometry, None);
1641    }
1642
1643    /// Route every glyph run the scene records, in recording order.
1644    ///
1645    /// The whole of the frame's collect phase. It is a walk of its own rather
1646    /// than a question asked inside the draw walk because the answer for one
1647    /// run depends on what the *font* has been drawn at recently, and a policy
1648    /// that learned a size only as it drew it would route the first run of a
1649    /// changing frame as settled and the second as animating — the two halves
1650    /// of one line of text taking different paths.
1651    ///
1652    /// Every run is classified, including ones the draw walk will refuse: the
1653    /// refusals it makes (an empty run, a blocked clip, an unreadable face)
1654    /// are not size observations, and a size drawn on a frame is a size drawn
1655    /// on that frame whatever else happens to it.
1656    ///
1657    /// The walk carries a [`RetainedStack`] of its own for one reason: a run's
1658    /// route depends on the scale in its *device* transform (see
1659    /// [`crate::text::atlas_policy`]), and inside a `PushRetained` bracket that
1660    /// transform carries the bracket's presentation scale as well as the frame
1661    /// root. Classifying against `root * run.transform` alone would answer for
1662    /// a size the run is not drawn at. Only the correction is tracked here —
1663    /// the bracket's *layers* are the draw walk's to open, and this walk opens
1664    /// nothing.
1665    ///
1666    /// A bracket is entered on the draw walk's exact terms, through the same
1667    /// [`command_on_grid`] check and the same [`retained_entry`] substitution
1668    /// it uses, so the correction the two walks carry is one decision made
1669    /// twice rather than two decisions that happen to agree.
1670    fn classify_runs(&mut self, scene: &Scene, root: Affine) {
1671        self.run_routes.clear();
1672        self.next_run = 0;
1673
1674        let mut retained_stack = RetainedStack::new();
1675        for command in scene.commands() {
1676            match command {
1677                Command::PushRetained {
1678                    rect,
1679                    scale,
1680                    transform,
1681                    ..
1682                } => {
1683                    // The draw walk's own entry, made here on exactly its
1684                    // terms: [`command_on_grid`] is the check it asks and
1685                    // [`retained_entry`] is the substitution it makes. A
1686                    // bracket it enters neutrally installs no correction, so a
1687                    // walk that entered it with the recorded pair would
1688                    // classify every run inside against a device transform
1689                    // nothing is ever drawn through — a route decided for one
1690                    // magnitude and a draw made at another.
1691                    let combined = root * retained_stack.correction();
1692                    let on_grid = command_on_grid(command, combined);
1693                    let (scale, transform) = retained_entry(on_grid, *scale, *transform);
1694                    retained_stack.enter(*rect, scale, transform);
1695                }
1696                Command::PopRetained => {
1697                    // The group depth a real close would be tested against is
1698                    // the draw walk's; nothing here closes a group, so zero is
1699                    // the honest answer and the return value is unused.
1700                    retained_stack.leave(0);
1701                }
1702                Command::GlyphRun(run) => {
1703                    // The context colour `glifo` would resolve a COLR layer
1704                    // against — the run's own brush when it is solid, black
1705                    // otherwise, which is the same answer
1706                    // `EngineGlyphSink::get_context_color` gives it.
1707                    let context_color = match context_paint(&run.brush) {
1708                        vello_common::paint::PaintType::Solid(color) => color,
1709                        _ => peniko::color::palette::css::BLACK,
1710                    };
1711                    let key = RunKey::for_run(
1712                        run,
1713                        root * retained_stack.correction() * run.transform,
1714                        self.hint_text,
1715                        self.font_facts.of(run.font.font()).color,
1716                        context_color,
1717                        &mut self.run_glyph_ids,
1718                    );
1719                    let route = self.glyph_atlas.classify_run(&key);
1720                    self.run_routes.push(route);
1721                }
1722                _ => {}
1723            }
1724        }
1725    }
1726
1727    /// The next run's route, or `None` once the collect walk's answers are
1728    /// exhausted.
1729    ///
1730    /// `None` is the conservative answer rather than an error: a run with no
1731    /// recorded route is drawn as outlines, which is correct pixels by the path
1732    /// the engine has always used.
1733    ///
1734    /// The route is re-tested against *live* glyph residency on the way out
1735    /// (see [`crate::text::atlas_policy::AtlasPolicy::admit_run`]). The collect
1736    /// walk answered every run of this frame from the population the frame
1737    /// opened with, because `glifo` inserts nothing until the draw walk reaches
1738    /// the run; without this second test a frame one entry below the budget
1739    /// would admit every run it carries and overshoot by as much as one frame's
1740    /// whole text. Here the population is the real one — every earlier run of
1741    /// this same frame has already inserted, and the re-test charges those
1742    /// insertions before it answers — so the bound holds within a frame and not
1743    /// merely across frames. It can only ever *narrow* an answer, which is the
1744    /// outline path: correct pixels, and the only direction that is safe to
1745    /// decide late.
1746    fn take_run_route(&mut self) -> Option<RunRoute> {
1747        let route = self.run_routes.get(self.next_run).copied();
1748        self.next_run = self.next_run.saturating_add(1);
1749        route.map(|route| self.glyph_atlas.admit_run(route))
1750    }
1751
1752    /// Draw one glyph run: its brush encoded once, then every glyph's outline
1753    /// rasterized under the active clip (see [`crate::text`]).
1754    ///
1755    /// `transform` is the run's own transform composed with the frame root.
1756    /// The brush is encoded against it once for the whole run rather than once
1757    /// per glyph, because that transform *is* the paint's placement — a glyph
1758    /// moves the outline, never the paint behind it — so a gradient-brushed
1759    /// line of text costs one encoded entry and one colour ramp.
1760    ///
1761    /// A run whose brush cannot be encoded draws nothing, on the same terms an
1762    /// image draw the atlas refuses does: the refusal is already counted in
1763    /// [`CompiledFrame::skipped_images`] by the encoding, and every glyph in
1764    /// the run simply goes missing rather than being painted with a
1765    /// substitute. An image-brushed run is likewise counted as the one image
1766    /// draw its single encoding is, not as one per glyph.
1767    ///
1768    /// A run whose font cannot be read is refused the same way and for a
1769    /// harder reason: the text backend's font gate is what keeps a blob that
1770    /// is not a font off the frame path at all (see [`crate::text`]).
1771    fn compile_glyph_run(
1772        &mut self,
1773        run: &GlyphRun,
1774        transform: Affine,
1775        route: Option<RunRoute>,
1776        frame: &mut CompiledFrame,
1777        depth: &mut DepthCounter,
1778    ) {
1779        // All three checked before the brush is encoded, so a run that can
1780        // draw nothing leaves no orphan entry in the frame's encoded-paint
1781        // table and no ramp request for a gradient nothing paints with — the
1782        // same rule [`record`](Self::record) keeps for a shape.
1783        if run.glyphs.is_empty() || self.clips.blocks_everything() {
1784            return;
1785        }
1786        if !self.font_facts.of(run.font.font()).readable {
1787            note_font_skip();
1788            let glyphs = u32::try_from(run.glyphs.len()).unwrap_or(u32::MAX);
1789            frame.skipped_glyphs = frame.skipped_glyphs.saturating_add(glyphs);
1790            return;
1791        }
1792
1793        let Some(paint) = self.encode_paint(PaintSource::Brush(&run.brush), transform, frame)
1794        else {
1795            return;
1796        };
1797
1798        // Read out ahead of the destructure below: `hint_text` is `Copy`, and
1799        // reading it through `self` after the destructure moved out its other
1800        // fields would fight the borrow checker for no reason.
1801        let hint_text = self.hint_text;
1802        let glyph_run_cache = self.glyph_run_cache;
1803        // Destructured rather than passed as `self`, because the sink borrows
1804        // the generator and the clip stack mutably while the glyph caches, the
1805        // entry map and the shared allocator are borrowed mutably alongside
1806        // them — five disjoint fields of one struct.
1807        let Self {
1808            generator,
1809            clips,
1810            groups,
1811            glyphs,
1812            images,
1813            glyph_atlas,
1814            glyph_quads,
1815            glyph_quad_scratch,
1816            ..
1817        } = self;
1818        // The policy's decision, turned into the borrow `glifo` caches
1819        // through. A run it refused reaches `glifo` with no cache at all, which
1820        // is the outline path unchanged rather than a cache that declines every
1821        // lookup — those are the same pixels but not the same work.
1822        let atlas = match route {
1823            Some(RunRoute::Atlas(_)) => Some((glyph_atlas, images.allocator_mut())),
1824            Some(RunRoute::Outline(_)) | None => None,
1825        };
1826        // Quads only for a run the atlas serves, and never under a coverage
1827        // mask: a quad's rectangle can be trimmed to the scissor but cannot
1828        // carry a mask (see [`crate::text`]'s backend).
1829        let quads =
1830            (*glyph_quads && atlas.is_some() && clips.mask().is_none()).then(|| GlyphQuadTargets {
1831                scratch: glyph_quad_scratch,
1832                window: quad_window(generator, groups),
1833            });
1834        let outcome = lower_glyph_run(
1835            run,
1836            transform,
1837            paint,
1838            &run.brush,
1839            hint_text,
1840            GlyphRunRouting {
1841                atlas,
1842                resident: glyph_run_cache,
1843                quads,
1844            },
1845            GlyphRunTargets {
1846                generator,
1847                clips,
1848                prep: glyphs,
1849                frame,
1850                depth,
1851            },
1852        );
1853
1854        frame.glyph_draws = frame.glyph_draws.saturating_add(outcome.drawn);
1855        frame.skipped_glyphs = frame.skipped_glyphs.saturating_add(outcome.skipped);
1856        if outcome.resident {
1857            frame.resident_glyph_runs = frame.resident_glyph_runs.saturating_add(1);
1858        }
1859    }
1860
1861    /// Open a layer bracket: its rectangle's clip, and — below full opacity —
1862    /// a recorded layer for the scheduler to give a page of its own.
1863    ///
1864    /// The clip and the bracket are pushed together and unconditionally, which
1865    /// is what keeps the two stacks in step for [`close_group`](Self::close_group).
1866    fn open_layer(&mut self, frame: &mut CompiledFrame, rect: Rect, alpha: f32, transform: Affine) {
1867        self.clips.push_rect(rect, transform, &mut self.generator);
1868
1869        let isolated = layers::lower_layer(alpha) == LayerLowering::Isolated;
1870        if isolated {
1871            frame.recorder.push_layer(layers::layer_props(alpha), None);
1872        }
1873        self.groups
1874            .push_layer(transform.transform_rect_bbox(rect), isolated);
1875    }
1876
1877    /// Open the outermost `PushRetained` bracket of a compiler with retention
1878    /// on: the same lowering the inline emulation gives it, recorded as a
1879    /// layer whatever its alpha, and listed for the retained-page store.
1880    ///
1881    /// Below full opacity that is exactly [`Self::open_layer`]. At full opacity
1882    /// the inline emulation opens no layer and clips nothing, so the layer
1883    /// opened here carries no clip either ([`GroupStack::push_retained`]) — the
1884    /// body's draws are the inline lowering's draws, gathered — and the store
1885    /// that refuses it leaves the scheduler a full-opacity layer it inlines.
1886    ///
1887    /// Whether the body can be retained at all is measured here, before the
1888    /// bracket's own group narrows anything: a page can only hold what the
1889    /// body really drew, so a body reaching past the viewport or a scissor
1890    /// around it, or under a coverage mask, would be retained missing what was
1891    /// clipped away — and composited somewhere else on a later frame, the loss
1892    /// would show.
1893    fn open_retained(
1894        &mut self,
1895        frame: &mut CompiledFrame,
1896        key: u64,
1897        rect: Rect,
1898        alpha: f32,
1899        transform: Affine,
1900    ) {
1901        let window = quad_window(&self.generator, &self.groups);
1902        let device = transform.transform_rect_bbox(rect);
1903        let retainable = self.clips.mask().is_none()
1904            && !self.clips.blocks_everything()
1905            && rect_within(device, window);
1906        // The id `push_layer` is about to hand the layer, in either branch.
1907        let layer = u32::try_from(frame.recorder.layers.len()).unwrap_or(u32::MAX);
1908
1909        if alpha < 1.0 {
1910            self.open_layer(frame, rect, alpha, transform);
1911        } else {
1912            frame.recorder.push_layer(layers::layer_props(alpha), None);
1913            self.groups.push_retained();
1914        }
1915        self.retained_stack.record_layer(self.groups.depth());
1916
1917        let placement = RetainedPlacement::of(transform);
1918        frame.retained.push(RetainedBracket {
1919            key,
1920            layer,
1921            opacity: alpha,
1922            placement,
1923            extent_px: placement.extent(rect),
1924            kind: RetainedKind::Record {
1925                rect: tile_bounds(device),
1926                retainable,
1927            },
1928        });
1929        self.retained_open = Some(frame.retained.len() - 1);
1930    }
1931
1932    /// Record a `DrawRetained` of a compiler with retention on: an empty layer
1933    /// at the command's painter-order position, standing for the composite the
1934    /// retained-page store resolves it to, plus what the store needs to place
1935    /// that composite.
1936    ///
1937    /// `mapped` is the body's local space mapped onto the device grid with the
1938    /// presentation scale folded in — the same composition a `PushRetained`
1939    /// bracket's correction produces for its body. A non-finite one draws
1940    /// nothing, the same answer every off-grid command gets.
1941    fn record_draw_retained(
1942        &mut self,
1943        frame: &mut CompiledFrame,
1944        depth: &mut DepthCounter,
1945        key: u64,
1946        rect: Rect,
1947        alpha: f32,
1948        mapped: Affine,
1949    ) {
1950        if check_finite(mapped).is_err() {
1951            return;
1952        }
1953        let window = quad_window(&self.generator, &self.groups);
1954        let masked = self.clips.mask().is_some();
1955        let layer = u32::try_from(frame.recorder.layers.len()).unwrap_or(u32::MAX);
1956        // Pushed and popped at once: a layer with no draws of its own, whose
1957        // node is what puts the composite at this point of its stream.
1958        frame.recorder.push_layer(layers::layer_props(alpha), None);
1959        frame.recorder.pop_layer();
1960
1961        let placement = RetainedPlacement::of(mapped);
1962        frame.retained.push(RetainedBracket {
1963            key,
1964            layer,
1965            opacity: alpha,
1966            placement,
1967            extent_px: placement.extent(rect),
1968            kind: RetainedKind::Draw {
1969                depth: depth.advance(),
1970                window: if self.clips.blocks_everything() {
1971                    RectU16::new(0, 0, 0, 0)
1972                } else {
1973                    window
1974                },
1975                masked,
1976            },
1977        });
1978    }
1979
1980    /// List `key`, a `PushRetained` this frame carries but records no bracket
1981    /// for, in [`CompiledFrame::retained_superseded`] — with retention on;
1982    /// without it there is no store to evict from.
1983    fn supersede_retained(&self, frame: &mut CompiledFrame, key: u64) {
1984        if self.retain {
1985            frame.retained_superseded.push(key);
1986        }
1987    }
1988
1989    /// Mark the retained body the walk is inside, if any, as one no page may
1990    /// hold.
1991    fn mark_retained_body_unretainable(&mut self, frame: &mut CompiledFrame) {
1992        if let Some(index) = self.retained_open {
1993            mark_unretainable(frame, index);
1994        }
1995    }
1996
1997    /// Open a bracket that admits nothing, for a push whose transform does not
1998    /// land on the device grid.
1999    ///
2000    /// A degenerate rectangle under the identity, rather than the push's own
2001    /// geometry under its own transform: the point is to reach an empty
2002    /// scissor without handing the flattener a transform it cannot subdivide
2003    /// against, which is the very thing that made this push unusable.
2004    fn open_blocked_group(&mut self) {
2005        self.clips
2006            .push_rect(Rect::ZERO, Affine::IDENTITY, &mut self.generator);
2007        self.groups.push_clip(Rect::ZERO);
2008    }
2009
2010    /// Close the innermost open bracket, undoing exactly what opened it.
2011    ///
2012    /// The recording is only popped when this bracket is the one that pushed
2013    /// it *and* the recording agrees a layer is open — the recorder's own pop
2014    /// panics on an empty layer stack, and the frame path returns errors
2015    /// rather than panicking (E17).
2016    fn close_group(&mut self, frame: &mut CompiledFrame) {
2017        let Some(group) = self.groups.pop() else {
2018            return;
2019        };
2020        if group.closes_clip() {
2021            self.clips.pop();
2022        }
2023        if group.closes_layer() && frame.recorder.has_layers() {
2024            frame.recorder.pop_layer();
2025        }
2026    }
2027
2028    /// Close every bracket the display list left open at the end of the frame.
2029    ///
2030    /// A recorded layer that is never popped has no bounds — the recorder
2031    /// computes them at the pop — so an unbalanced push would otherwise leave
2032    /// the scheduler a layer it cannot place. Closing here is the same policy
2033    /// an unbalanced pop gets, applied at the other end.
2034    fn close_open_groups(&mut self, frame: &mut CompiledFrame) {
2035        while !self.groups.is_empty() {
2036            self.close_group(frame);
2037        }
2038        self.retained_stack.reset();
2039        // A body still open at the end of the frame never reached its own
2040        // `PopRetained`, so where it would have ended is unknown.
2041        if let Some(index) = self.retained_open.take() {
2042            mark_unretainable(frame, index);
2043        }
2044    }
2045
2046    /// Generate the coverage for every punch the frame hoisted.
2047    ///
2048    /// Runs after the walk, so the strips land past every draw's own range and
2049    /// no draw references them. The punch is rasterized at the frame root
2050    /// under no clip at all — being hoisted out of its brackets is exactly
2051    /// what the confinement in [`clear::punch_rect`] already accounted for —
2052    /// and takes the fast rectangle path whenever its edges fall on whole
2053    /// pixels, which is what makes a pixel-aligned punch pixel-exact however
2054    /// its edges fall inside a tile.
2055    fn generate_punches(&mut self, frame: &mut CompiledFrame) {
2056        for index in 0..self.punches.len() {
2057            let Some(punch) = self.punches.get(index).copied() else {
2058                continue;
2059            };
2060
2061            let start = frame.strips.strips.len();
2062            match fast_rect(punch.device, Affine::IDENTITY) {
2063                Some(device) => {
2064                    self.generator
2065                        .generate_filled_rect_fast(&device, &mut frame.strips, None);
2066                }
2067                None => {
2068                    self.generator.generate_filled_path(
2069                        punch.device.path_elements(FLATTEN_TOLERANCE),
2070                        Fill::NonZero,
2071                        Affine::IDENTITY,
2072                        None,
2073                        &mut frame.strips,
2074                        None,
2075                    );
2076                }
2077            }
2078
2079            let strip_range = start..frame.strips.strips.len();
2080            if strip_range.is_empty() {
2081                continue;
2082            }
2083
2084            frame.clears.push(ClearPunch {
2085                strip_range,
2086                bounds: clear::device_bounds(punch.device),
2087                depth: punch.depth,
2088            });
2089        }
2090    }
2091
2092    /// [`record`](Self::record) `geometry`, counting the draw as a fast
2093    /// rectangle when it took that path (see [`fast_rect`]).
2094    fn draw_shape(
2095        &mut self,
2096        frame: &mut CompiledFrame,
2097        depth: &mut DepthCounter,
2098        source: PaintSource<'_>,
2099        transform: Affine,
2100        geometry: Geometry<'_>,
2101        claim: Option<Claim<'_>>,
2102    ) {
2103        let fast = geometry.is_fast_rect();
2104        if self.record(frame, depth, source, transform, geometry, claim) && fast {
2105            frame.fast_rect_draws = frame.fast_rect_draws.saturating_add(1);
2106        }
2107    }
2108
2109    /// Generate `geometry` under the active clip, then record whatever strips
2110    /// survived as one draw painted from `source` under `transform`.
2111    ///
2112    /// The clip stack's coverage mask is handed to the strip generator, which
2113    /// is what intersects a mask clip while the draw's own coverage is
2114    /// produced; the scissor is applied afterwards, to the run the generator
2115    /// appended. A scissor admitting nothing skips generation entirely rather
2116    /// than generating coverage to throw away.
2117    ///
2118    /// `claim` is the command's helper-generated run, when one was planned
2119    /// (see [`parallel`]). It stands in for generating only while no mask is
2120    /// open — a helper generates unclipped — and only when a helper actually
2121    /// got to it; otherwise the run is generated here exactly as it would be
2122    /// without one. Everything after generation is the same either way.
2123    ///
2124    /// Returns whether a draw was recorded. A generator call that produced no
2125    /// strips (fully culled, clipped away, degenerate, or empty geometry)
2126    /// records nothing and consumes no depth, so a frame's depths stay dense
2127    /// over the draws that actually exist.
2128    ///
2129    /// The paint is encoded only once the strips are known to be non-empty, so
2130    /// a culled draw leaves no orphan entry in the frame's encoded-paint table
2131    /// and no ramp request for a gradient nothing paints with. An image the
2132    /// atlas refuses arrives *after* that point, so its coverage is rolled back
2133    /// to where the generator started rather than left behind as strips no draw
2134    /// references.
2135    fn record(
2136        &mut self,
2137        frame: &mut CompiledFrame,
2138        depth: &mut DepthCounter,
2139        source: PaintSource<'_>,
2140        transform: Affine,
2141        geometry: Geometry<'_>,
2142        claim: Option<Claim<'_>>,
2143    ) -> bool {
2144        if self.clips.blocks_everything() {
2145            return false;
2146        }
2147
2148        let start = frame.strips.strips.len();
2149        let alpha_start = frame.strips.alphas.len();
2150        let mask = self.clips.mask();
2151        let merged =
2152            mask.is_none() && claim.is_some_and(|claim| claim.append_to(&mut frame.strips));
2153        if !merged {
2154            geometry.generate(&mut self.generator, &mut frame.strips, mask);
2155        }
2156        self.clips.clip_run(&mut frame.strips, start, alpha_start);
2157        let strip_range = start..frame.strips.strips.len();
2158
2159        // A run is only a draw when it carries content. Under a mask clip
2160        // `vello_common::clip::intersect_impl` gates its trailing sentinel on
2161        // the *whole* target buffer being non-empty, not on what this call
2162        // added — and `frame.strips` is one `Append`-mode buffer shared by
2163        // every draw of the frame, so a masked path that contributed no rows
2164        // (zero-area geometry, coverage the mask removed entirely) after an
2165        // earlier draw's content still gets a sentinel: a *lone* sentinel.
2166        // The renderer's pairwise walk reads each span's extent off the strip
2167        // after it, so that is not a run at all — roll it back exactly like an
2168        // empty one. Every generation path pairs a content strip with its own
2169        // sentinel in the same call, so a one-strip run can only be that
2170        // sentinel; the assertion keeps a future generator change from being
2171        // swallowed here as "nothing to draw".
2172        if strip_range.len() < 2 {
2173            debug_assert!(
2174                strip_range.is_empty() || frame.strips.strips[start].is_sentinel(),
2175                "a one-strip run must be a lone sentinel, not an unterminated content strip"
2176            );
2177            frame.strips.strips.truncate(start);
2178            frame.strips.alphas.truncate(alpha_start);
2179            return false;
2180        }
2181
2182        let Some(paint) = self.encode_paint(source, transform, frame) else {
2183            frame.strips.strips.truncate(start);
2184            frame.strips.alphas.truncate(alpha_start);
2185            return false;
2186        };
2187
2188        let draw = EngineDraw::new(paint, depth.advance(), strip_range.clone());
2189        frame
2190            .recorder
2191            .push_draw(draw, &frame.strips.strips[strip_range]);
2192        if merged {
2193            frame.parallel_draws = frame.parallel_draws.saturating_add(1);
2194        }
2195        true
2196    }
2197
2198    /// Encode `source` into the paint a draw carries, or `None` when the paint
2199    /// cannot be resolved and the draw is to be dropped.
2200    ///
2201    /// A solid and a gradient are always encodable (a degenerate gradient falls
2202    /// back to a solid). The two that can answer `None` are an image, which
2203    /// needs atlas space the residency may refuse, and an externally bound
2204    /// texture, whose id may name nothing registered.
2205    fn encode_paint(
2206        &mut self,
2207        source: PaintSource<'_>,
2208        transform: Affine,
2209        frame: &mut CompiledFrame,
2210    ) -> Option<vello_common::paint::Paint> {
2211        let encoded = match source {
2212            PaintSource::Brush(Brush::Image(brush)) => encode_image_brush(
2213                brush,
2214                transform,
2215                &mut frame.encoded_paints,
2216                &mut self.images,
2217            ),
2218            PaintSource::Brush(brush) => {
2219                let encoding = encode_brush(brush, transform, &mut frame.encoded_paints);
2220                frame.lut_requests.extend(encoding.lut_request);
2221                return Some(encoding.paint);
2222            }
2223            PaintSource::Image { data, dest } => encode_image_command(
2224                data,
2225                dest,
2226                transform,
2227                &mut frame.encoded_paints,
2228                &mut self.images,
2229            ),
2230            PaintSource::SceneTexture { id, dest } => {
2231                // Its own error type and its own counters, so it returns here
2232                // rather than joining the atlas-residency match below: nothing
2233                // is made resident and nothing is uploaded — the texels are
2234                // the caller's and are already on the device.
2235                return match encode_scene_texture(
2236                    id,
2237                    dest,
2238                    transform,
2239                    &mut self.externals,
2240                    &mut frame.encoded_paints,
2241                ) {
2242                    Ok(encoding) => {
2243                        frame.external_draws = frame.external_draws.saturating_add(1);
2244                        Some(encoding.paint)
2245                    }
2246                    Err(_) => {
2247                        frame.skipped_externals = frame.skipped_externals.saturating_add(1);
2248                        None
2249                    }
2250                };
2251            }
2252            PaintSource::BlurredRect {
2253                rect,
2254                radii,
2255                std_dev,
2256                color,
2257            } => {
2258                // Unlike an image, this can never be refused (see
2259                // [`encode_blurred_rounded_rect`]'s doc), so it returns
2260                // straight away rather than joining the fallible match below.
2261                let paint = encode_blurred_rounded_rect(
2262                    rect,
2263                    radii,
2264                    std_dev,
2265                    color,
2266                    transform,
2267                    &mut frame.encoded_paints,
2268                );
2269                return Some(paint);
2270            }
2271        };
2272
2273        match encoded {
2274            Ok(encoding) => {
2275                frame.image_draws = frame.image_draws.saturating_add(1);
2276                Some(encoding.paint)
2277            }
2278            Err(skip) => {
2279                frame.skipped_images = frame.skipped_images.saturating_add(1);
2280                note_image_skip(skip);
2281                None
2282            }
2283        }
2284    }
2285}
2286
2287/// What a draw is painted with, as the walk hands it to
2288/// [`SceneCompiler::record`].
2289///
2290/// An image is not a [`Brush`] in the display list — [`Command::Image`] carries
2291/// its pixels and a destination rectangle directly — so the two arrive by
2292/// different routes and are distinguished here rather than by forcing one into
2293/// the shape of the other.
2294enum PaintSource<'a> {
2295    /// A solid, gradient or image brush recorded on a shape command.
2296    Brush(&'a Brush),
2297    /// A [`Command::Image`]'s pixels scaled to fill `dest`.
2298    Image {
2299        /// The decoded image to make resident.
2300        data: &'a ImageData,
2301        /// The destination rectangle, in the command's own coordinate space.
2302        dest: Rect,
2303    },
2304    /// A [`Command::SceneTexture`]'s externally bound texture scaled to fill
2305    /// `dest`.
2306    SceneTexture {
2307        /// The opaque id the display list names the texture by.
2308        id: u64,
2309        /// The destination rectangle, in the command's own coordinate space.
2310        dest: Rect,
2311    },
2312    /// A [`Command::BlurredRoundedRect`]'s shadow parameters, in the
2313    /// command's own (pre-transform) coordinate space.
2314    BlurredRect {
2315        /// The un-padded rectangle the shadow is cast from.
2316        rect: Rect,
2317        /// Per-corner radii, collapsed to their largest at encode time (see
2318        /// [`blur_rrect`]).
2319        radii: CornerRadii,
2320        /// The blur's standard deviation.
2321        std_dev: f64,
2322        /// The shadow's base colour.
2323        color: Color,
2324    },
2325}
2326
2327/// Report an image the atlas refused.
2328///
2329/// The first refusal in a process is a warning, because a blank image where one
2330/// was expected is otherwise invisible; the rest are debug, because a scene
2331/// that keeps drawing a refused image would repeat the message every frame.
2332fn note_image_skip(skip: ImageSkip) {
2333    IMAGE_SKIP_WARNING.call_once(|| {
2334        log::warn!("image draw skipped: {skip} (further skips are logged at debug level)");
2335    });
2336    log::debug!("image draw skipped: {skip}");
2337}
2338
2339/// Report what this frame's image residency cost, on a frame where it cost
2340/// anything.
2341///
2342/// The counterpart to [`note_image_skip`]'s once-per-process warning, which
2343/// says *that* an image was refused and then goes quiet: this says how many
2344/// draws a given frame lost and how hard the atlas is being churned to avoid
2345/// losing more. `frust-perf`-prefixed and at info level, which is what carries
2346/// it into a benchmark capture — the harness keeps every line with that prefix
2347/// and drops the rest, so a run's own log answers "did the atlas hold this
2348/// scene?" without a parallel system-log capture beside it.
2349///
2350/// Silent on a frame that skipped nothing and evicted nothing, which is every
2351/// frame of a steady scene: the common path pays two comparisons and writes no
2352/// line. `skipped` is the frame's own count of image draws that painted
2353/// nothing, so it includes the few refusals decided before residency is even
2354/// consulted (a singular paint transform, a destination with no area) as well
2355/// as the atlas's own — every one of them is a draw the display list asked for
2356/// and the frame did not paint, which is the question the line answers.
2357///
2358/// `perf-trace`-only, like every other `frust-perf` line in this workspace
2359/// (`frust_render::context::log_render_path`, [`PhaseClock`], the encode
2360/// window): the release-lean gate asserts a shipping binary contains no
2361/// `frust-perf` bytes at all, and a `#[cfg]` is what makes that the compiler's
2362/// answer rather than a hope about the optimizer.
2363#[cfg(feature = "perf-trace")]
2364fn note_image_pressure(frame: &CompiledFrame, images: &ImageResidency) {
2365    if !image_pressure_reported(frame, images) {
2366        return;
2367    }
2368    // The text is built *inside* the macro's argument list, so the log ceiling
2369    // covers the `format!` and not merely the emission. `evicted>0` is the
2370    // expected steady state of a working set larger than the atlas, and a
2371    // build whose ceiling drops info lines must not pay a `String` per frame
2372    // for one it will never record.
2373    log::info!("{}", image_pressure_line(frame, images));
2374}
2375
2376/// Whether `frame` has any image-residency cost to report against `images`.
2377///
2378/// Split from the line itself so the text can be built where the log macro can
2379/// elide it — see [`note_image_pressure`] — and so "a frame the atlas held
2380/// reports nothing at all" stays a property a test can name.
2381#[cfg(feature = "perf-trace")]
2382#[must_use]
2383pub fn image_pressure_reported(frame: &CompiledFrame, images: &ImageResidency) -> bool {
2384    frame.skipped_images != 0 || images.frame_pressure_evictions() != 0
2385}
2386
2387/// The `frust-perf img` line `frame` reports against `images`.
2388///
2389/// Separate from the logging above because the *text* is a contract: a
2390/// benchmark capture is graded by grepping these fields, so the field order and
2391/// the names are pinned by a test rather than only by this module. Ask
2392/// [`image_pressure_reported`] first — on a quiet frame this still formats a
2393/// line, it is simply one nothing asks for.
2394#[cfg(feature = "perf-trace")]
2395#[must_use]
2396pub fn image_pressure_line(frame: &CompiledFrame, images: &ImageResidency) -> String {
2397    let budget = images.budget();
2398    format!(
2399        "frust-perf img skipped={} evicted={} resident={} budget={}x{}x{}",
2400        frame.skipped_images,
2401        images.frame_pressure_evictions(),
2402        images.entry_count(),
2403        budget.atlas_size.0,
2404        budget.atlas_size.1,
2405        budget.max_atlases,
2406    )
2407}
2408
2409/// Report a glyph run whose font could not be read.
2410///
2411/// The same once-warning-then-debug shape [`note_image_skip`] uses, and for
2412/// the same reason: a missing line of text is otherwise invisible, while a
2413/// scene that keeps drawing against an unloaded font would repeat the message
2414/// every frame.
2415fn note_font_skip() {
2416    FONT_SKIP_WARNING.call_once(|| {
2417        log::warn!(
2418            "glyph run skipped: its font blob is not a readable face \
2419             (further skips are logged at debug level)"
2420        );
2421    });
2422    log::debug!("glyph run skipped: its font blob is not a readable face");
2423}
2424
2425/// The id the offscreen target of fragment program `program_id` is registered
2426/// under — the compiler's half of the agreement
2427/// [`crate::effects::shader_quad`] makes with the pre-pass that renders it.
2428///
2429/// A pure function of the program id, computed independently on both sides
2430/// rather than exchanged through a side table, because the walk holds nothing
2431/// else: a [`Command::ShaderQuad`] carries its `ShaderProgram` and no texture
2432/// handle. `SceneTextureId::for_shader_program` is what keeps the derived
2433/// value out of the range host textures mint from.
2434#[must_use]
2435pub fn shader_quad_texture_id(program_id: u64) -> u64 {
2436    SceneTextureId::for_shader_program(program_id).get()
2437}
2438
2439/// Report a [`Command::ShaderQuad`] dropped for want of a rendered target.
2440///
2441/// Latched to once per process rather than following [`note_image_skip`] and
2442/// [`note_font_skip`]'s warn-then-debug shape: a compiler driven without the
2443/// pre-pass (a host encoding frames straight through
2444/// [`crate::EngineRenderer`], or a program whose shader failed to compile)
2445/// produces this on every frame forever, and the second report says nothing
2446/// the first did not. Never raised for a quad [`shader_quad_is_culled`]
2447/// reports deliberately culled — that case is [`note_shader_quad_culled`]'s.
2448fn note_shader_quad_unrendered(program_id: u64) {
2449    SHADER_QUAD_SKIP_WARNING.call_once(|| {
2450        log::warn!(
2451            "ShaderQuad draws nothing: fragment program {program_id} has no rendered target \
2452             — either the frame's shader pre-pass did not run before this compile, or the \
2453             program failed to compile (logged once per process)"
2454        );
2455    });
2456}
2457
2458/// Whether a [`Command::ShaderQuad`] at `dest` under `transform` (the
2459/// command's own transform composed with the frame's) is entirely outside
2460/// `frame_extent` — the same target-extent overlap test
2461/// `crate::effects::shader_quad::frame_demands`/`culled_program_ids` apply to
2462/// decide whether the pre-pass renders this program's quad at all this frame,
2463/// restated independently here (the compiler and the pre-pass share no
2464/// channel for "this id was culled, not missing") so an id nothing is
2465/// registered under can be told apart from one whose pre-pass genuinely never
2466/// ran — see [`note_shader_quad_unrendered`]/[`note_shader_quad_culled`].
2467///
2468/// Conservative on the same terms as the pre-pass's own check: a non-finite
2469/// `bbox` answers `false` (never treated as culled — some other refusal
2470/// accounts for it), and a shared edge counts as overlapping via
2471/// [`kurbo::Rect::overlaps`]. Unlike the pre-pass's walk, this has no
2472/// [`Command::PushRetained`]-bracket exemption to make: it is asked only
2473/// about the one quad instance actually being compiled right now, under its
2474/// own already-composed `transform` (which, inside a bracket, already
2475/// includes the enclosing retained bracket's own presentation correction — see
2476/// `compile_command`'s `combined`) — there is no separate "the bracket might
2477/// still move it" case to guard against here, only the geometry this exact
2478/// draw is about to be attempted at.
2479fn shader_quad_is_culled(dest: Rect, transform: Affine, frame_extent: (u16, u16)) -> bool {
2480    let frame_rect = Rect::new(
2481        0.0,
2482        0.0,
2483        f64::from(frame_extent.0),
2484        f64::from(frame_extent.1),
2485    );
2486    let bbox = transform.transform_rect_bbox(dest);
2487    bbox.is_finite() && !bbox.overlaps(frame_rect)
2488}
2489
2490/// Report a [`Command::ShaderQuad`] dropped because the shader-quad pre-pass
2491/// (`crate::effects::shader_quad`) deliberately culled every one of the
2492/// program's quads this frame — its device rectangle does not intersect the
2493/// frame's own target at all — rather than because no pre-pass ran for it at
2494/// all.
2495///
2496/// Debug-level and unlatched (unlike [`note_shader_quad_unrendered`]): a
2497/// culled quad is an expected, routine outcome of a scene drawing an
2498/// off-screen or fully clipped program, not a signal that something is
2499/// missing, so it does not need the once-per-process rate limit a genuine
2500/// "nothing rendered this" warning does.
2501fn note_shader_quad_culled(program_id: u64) {
2502    log::debug!(
2503        "ShaderQuad draws nothing: fragment program {program_id} was culled by the \
2504         shader-quad pre-pass — its destination does not intersect this frame's target \
2505         (expected; not a missing pre-pass)"
2506    );
2507}
2508
2509/// Report a [`Command::ShaderQuad`] dropped because
2510/// `FRUST_ENGINE_NO_SHADER_EFFECTS` is set.
2511///
2512/// Once per process, like the sighting above: the switch is read once and
2513/// cannot change under a running process, so the fact is stated once.
2514fn note_shader_effects_disabled() {
2515    SHADER_EFFECTS_DISABLED_WARNING.call_once(|| {
2516        log::warn!(
2517            "ShaderQuad commands draw nothing: FRUST_ENGINE_NO_SHADER_EFFECTS is set \
2518             (logged once per process)"
2519        );
2520    });
2521}
2522
2523/// The transform a command carries, or `None` for one that carries none.
2524fn command_transform(command: &Command) -> Option<Affine> {
2525    match command {
2526        Command::FillRect { transform, .. }
2527        | Command::RoundedRect { transform, .. }
2528        | Command::Line { transform, .. }
2529        | Command::PushClip { transform, .. }
2530        | Command::PushClipRounded { transform, .. }
2531        | Command::PushClipPath { transform, .. }
2532        | Command::Image { transform, .. }
2533        | Command::BlurredRoundedRect { transform, .. }
2534        | Command::PushLayer { transform, .. }
2535        | Command::ClearRect { transform, .. }
2536        | Command::Path { transform, .. }
2537        | Command::ShaderQuad { transform, .. }
2538        | Command::SceneTexture { transform, .. }
2539        | Command::PushRetained { transform, .. }
2540        | Command::DrawRetained { transform, .. }
2541        | Command::BackdropBlur { transform, .. } => Some(*transform),
2542        Command::GlyphRun(run) => Some(run.transform),
2543        Command::PopClip | Command::PopLayer | Command::PopRetained => None,
2544    }
2545}
2546
2547/// Whether `command`'s own transform still lands on the finite device grid once
2548/// `combined` — the frame root with any open retained bracket's correction — is
2549/// composed ahead of it.
2550///
2551/// Asked by both of the frame's walks, from one place, because they have to ask
2552/// it the same way. The draw walk draws nothing for a command that answers
2553/// `false` and enters a `PushRetained` neutrally instead (see
2554/// [`retained_entry`]); the collect walk classifies against the transform that
2555/// entry implies. Two copies of this expression could drift a coefficient
2556/// apart and route a run for a device size it is never drawn at.
2557///
2558/// A command carrying no transform of its own is on the grid trivially: there
2559/// is nothing to compose.
2560fn command_on_grid(command: &Command, combined: Affine) -> bool {
2561    command_transform(command).is_none_or(|transform| check_finite(combined * transform).is_ok())
2562}
2563
2564/// The presentation scale and transform a `PushRetained` bracket is entered
2565/// with: the recorded pair on the grid, and the neutral pair off it.
2566///
2567/// The neutral pair is what makes an off-grid bracket *inert* rather than
2568/// absent — it still has a depth to count and a pop to balance, but it installs
2569/// no correction, so nothing inside it is drawn through a transform the frame
2570/// refused. Both walks substitute through this one function so that the route
2571/// a run is given and the transform it is drawn through can never be decided
2572/// from different magnitudes.
2573fn retained_entry(on_grid: bool, scale: f64, transform: Affine) -> (f64, Affine) {
2574    if on_grid {
2575        (scale, transform)
2576    } else {
2577        (1.0, Affine::IDENTITY)
2578    }
2579}
2580
2581/// Refuse a viewport whose tile-snapped extent would not fit in `u16`.
2582///
2583/// The recorder snaps the scene size up to whole tiles, and that rounding is
2584/// checked arithmetic upstream — an extent within three pixels of `u16::MAX`
2585/// has no representable tile-aligned bound. Refusing it here is what keeps the
2586/// frame path free of that panic.
2587fn check_tile_addressable(width: u16, height: u16) -> Result<(), EngineError> {
2588    let addressable = width.checked_next_multiple_of(Tile::WIDTH).is_some()
2589        && height.checked_next_multiple_of(Tile::HEIGHT).is_some();
2590
2591    if addressable {
2592        Ok(())
2593    } else {
2594        Err(EngineError::TargetTooLarge)
2595    }
2596}
2597
2598/// Refuse a transform that maps geometry off the finite device grid.
2599///
2600/// A non-finite coefficient (`NaN` from a degenerate inverse, an infinity from
2601/// an overflowed scale) sends every coordinate it touches outside the `u16`
2602/// pixel range the strip pipeline addresses, so the frame is refused rather
2603/// than rasterized into whatever the downstream float-to-integer conversions
2604/// happen to saturate to.
2605fn check_finite(transform: Affine) -> Result<(), EngineError> {
2606    if transform.as_coeffs().iter().all(|c| c.is_finite()) {
2607        Ok(())
2608    } else {
2609        Err(EngineError::InvalidTransform)
2610    }
2611}
2612
2613/// Refuse a command whose own geometry is non-finite.
2614///
2615/// A finite transform is not enough on its own: a `NaN` corner radius, an
2616/// infinite rectangle extent, a `NaN` control point or stroke width all reach
2617/// the flattener and the stroker as they were recorded, and neither of those
2618/// bails on a non-finite number. They subdivide against it — a rounded rect of
2619/// unbounded extent with a `NaN` radius never finishes at all, and a `NaN`
2620/// stroke width buys hundreds of milliseconds and megabytes of scratch to emit
2621/// no coverage whatsoever. Refusing here, in the same up-front walk the
2622/// transforms are checked in, is what bounds the frame path's work by the
2623/// scene rather than by the arithmetic.
2624///
2625/// Only the commands the compiler actually lowers are checked. A command it
2626/// recognises and skips contributes no geometry to the frame, so refusing the
2627/// whole frame over one would draw *nothing* where skipping draws less — the
2628/// weaker outcome. A clip is checked because it *is* lowered: its rectangle and
2629/// radii decide whether the clip scissors or masks and where its edges land, so
2630/// a non-finite one is refused on the same terms as a fill's, matching the
2631/// refusal its transform already drew. A layer and a retained bracket are
2632/// checked on those same terms, and for the same reason: each lowers its
2633/// rectangle through the clip stack, so a non-finite one reaches the flattener
2634/// exactly as a clip's would. Their `alpha` and `scale` are checked alongside
2635/// it because neither is decoration — an alpha decides whether the layer
2636/// isolates, and a scale composes a transform every command inside the bracket
2637/// is drawn under. A clear is checked because its rectangle *is* the coverage
2638/// it erases with. An image's destination rectangle is checked on those same
2639/// terms — it is both the coverage the image paints through and the scale its
2640/// natural-to-destination transform is derived from, so a non-finite one would
2641/// reach the flattener and the paint encoding alike. A blurred rounded
2642/// rectangle's rectangle, corner radii and standard deviation are checked on
2643/// those same terms — together they decide the padded rectangle the strip
2644/// generator rasterizes ([`blur_rrect::inflated_bounds`]) and the falloff the
2645/// fragment shader evaluates from the encoded paint
2646/// ([`blur_rrect::encode_blurred_rounded_rect`]), so a non-finite one would
2647/// reach the flattener and the paint encoding exactly as a non-finite rounded
2648/// rect's radii already do. A lowered command carrying no geometry at all
2649/// ([`Command::PopClip`] and its two siblings) has nothing to check and sits
2650/// with the skipped group. A glyph run's font size and per-glyph positions are
2651/// checked for the same reason a stroke width is: they are not decoration
2652/// either, but the numbers every glyph's own draw transform is derived from
2653/// ([`crate::text`]), so a non-finite one reaches the flattener as a transform
2654/// no subdivision converges against. The scan is per glyph and therefore the
2655/// one check here whose cost grows with a command's contents — bounded by the
2656/// glyph count the run already carries, and paid once per frame rather than
2657/// once per glyph drawn.
2658///
2659/// The match is exhaustive over every [`Command`] variant, the same as
2660/// [`SceneCompiler::compile_command`]'s: a variant added to the enum fails to
2661/// compile here until it is placed in the checked group or the unchecked one.
2662/// Moving a variant *between* those two groups is not itself compiler-enforced
2663/// — the match stays exhaustive either way — so that half of the discipline
2664/// still has to be kept by hand alongside `compile_command`.
2665fn check_geometry(command: &Command) -> Result<(), EngineError> {
2666    let finite = match command {
2667        Command::FillRect { rect, .. } => rect.is_finite(),
2668        Command::RoundedRect { rect, radii, .. } => rect.is_finite() && radii_are_finite(*radii),
2669        Command::Line { p0, p1, width, .. } => {
2670            p0.is_finite() && p1.is_finite() && width.is_finite()
2671        }
2672        Command::Path { path, style, .. } => path.is_finite() && style_is_finite(style),
2673        Command::PushClip { rect, .. } => rect.is_finite(),
2674        Command::PushClipRounded { rect, radii, .. } => {
2675            rect.is_finite() && radii_are_finite(*radii)
2676        }
2677        // An outline is the mask's geometry outright, so a non-finite point
2678        // reaches the flattener exactly as a filled path's would.
2679        Command::PushClipPath { path, .. } => path.is_finite(),
2680        Command::PushLayer { rect, alpha, .. } => rect.is_finite() && alpha.is_finite(),
2681        Command::ClearRect { rect, .. } => rect.is_finite(),
2682        Command::PushRetained {
2683            rect, alpha, scale, ..
2684        }
2685        | Command::DrawRetained {
2686            rect, alpha, scale, ..
2687        } => rect.is_finite() && alpha.is_finite() && scale.is_finite(),
2688        Command::Image { dest, .. } => dest.is_finite(),
2689        Command::SceneTexture { dest, .. } => dest.is_finite(),
2690        // Carries a destination rectangle like the two above, and lowers
2691        // through the same external-texture path, so the same check applies.
2692        Command::ShaderQuad { dest, .. } => dest.is_finite(),
2693        Command::BlurredRoundedRect {
2694            rect,
2695            radii,
2696            std_dev,
2697            ..
2698        } => rect.is_finite() && radii_are_finite(*radii) && std_dev.is_finite(),
2699        Command::GlyphRun(run) => glyph_run_is_finite(run),
2700        // Its rectangle is the coverage the blurred backdrop is drawn through
2701        // and the region the blur reads is grown from it, and its radius is
2702        // the σ the planner's decimation loop runs over — a non-finite one of
2703        // either reaches the flattener or that loop as it was recorded.
2704        Command::BackdropBlur { rect, radius, .. } => rect.is_finite() && radius.is_finite(),
2705        // Carrying no geometry of their own — see above.
2706        Command::PopClip | Command::PopLayer | Command::PopRetained => true,
2707    };
2708
2709    if finite {
2710        Ok(())
2711    } else {
2712        Err(EngineError::InvalidGeometry)
2713    }
2714}
2715
2716/// Whether a glyph run's own numbers are finite.
2717///
2718/// The font size and every glyph position, because those are exactly the run's
2719/// numbers that end up inside a transform: `glifo` absorbs the font size into
2720/// each glyph's draw transform and translates that transform by the glyph's
2721/// position, so either one non-finite produces a transform the flattener
2722/// subdivides against forever. The font itself is not checked — a malformed or
2723/// unreadable face yields no outline and draws nothing, which is a missing
2724/// glyph rather than an unbounded loop.
2725fn glyph_run_is_finite(run: &GlyphRun) -> bool {
2726    run.font_size.is_finite()
2727        && run
2728            .glyphs
2729            .iter()
2730            .all(|glyph| glyph.x.is_finite() && glyph.y.is_finite())
2731}
2732
2733/// Whether every corner radius is finite.
2734fn radii_are_finite(radii: CornerRadii) -> bool {
2735    radii.top_left.is_finite()
2736        && radii.top_right.is_finite()
2737        && radii.bottom_right.is_finite()
2738        && radii.bottom_left.is_finite()
2739}
2740
2741/// Whether a path style's own numbers are finite.
2742///
2743/// The dash lengths and phase are checked even though
2744/// [`DashPattern::is_effective`] would strike a non-finite pattern out and
2745/// stroke solid: a frame the compiler refuses for a `NaN` stroke width would
2746/// otherwise be accepted for a `NaN` dash phase, and one contract over every
2747/// number a *lowered* command carries is the one a caller can hold in their
2748/// head — not a claim about a command [`check_geometry`] skips rather than
2749/// lowers, whose numbers this function never sees.
2750///
2751/// An effective dash pattern is checked further, past its own fields: see
2752/// [`dash_cycle_is_normalizable`].
2753fn style_is_finite(style: &PathStyle) -> bool {
2754    match style {
2755        PathStyle::Fill => true,
2756        PathStyle::Stroke { width, dash } => {
2757            let dash_finite = match dash {
2758                Some(dash) => {
2759                    dash.on.is_finite()
2760                        && dash.off.is_finite()
2761                        && dash.phase.is_finite()
2762                        && dash_cycle_is_normalizable(dash)
2763                }
2764                None => true,
2765            };
2766            width.is_finite() && dash_finite
2767        }
2768    }
2769}
2770
2771/// Whether a dash pattern's derived cycle survives kurbo's own normalization
2772/// arithmetic, so `kurbo::dash` terminates instead of spinning forever.
2773///
2774/// [`DashPattern::is_effective`] already screens out a non-positive or
2775/// sub-epsilon pattern in favour of a solid stroke, but its own period check —
2776/// `on + off >= DASH_PERIOD_EPSILON` — can itself be fooled: two individually
2777/// finite lengths can sum past `f64::MAX` into `+inf`, and `+inf >=
2778/// DASH_PERIOD_EPSILON` still reads as effective. kurbo doubles this crate's
2779/// on/off pair into its own length-2 dash array, so the period it derives is
2780/// always `on + off`; once that overflows, `phase.rem_euclid(period)`
2781/// overflows with it, and the catch-up loop `kurbo::dash` runs before it ever
2782/// pulls a `PathEl` adds an infinite step to a value that never converges —
2783/// `on = off = f64::MAX`, `phase = -1.0` hangs this way. Refusing here, where
2784/// `on`, `off` and `phase` already passed their own finiteness checks, is what
2785/// keeps that unbounded loop out of the frame path.
2786///
2787/// Public for the same reason [`dash_path`] and [`well_formed`] are: the CPU
2788/// oracle carries an identical copy, and a cross-crate test pins the two
2789/// against each other so they cannot drift apart silently.
2790pub fn dash_cycle_is_normalizable(dash: &DashPattern) -> bool {
2791    if !dash.is_effective() {
2792        // A degenerate pattern never reaches `dash_path`: `is_effective` is
2793        // what routes it to a solid stroke instead, so its derived period is
2794        // moot here.
2795        return true;
2796    }
2797    let period = dash.on + dash.off;
2798    period.is_finite() && period > 0.0 && dash.phase.rem_euclid(period).is_finite()
2799}
2800
2801/// The device-space rectangle to hand the fast rectangle path, or `None` when
2802/// this rectangle has to go through full path processing.
2803///
2804/// The fast path writes strip coverage for a rectangle directly, skipping
2805/// flattening and tiling entirely, and is taken only when the result is
2806/// indistinguishable from the general path: the composed transform must keep
2807/// the rectangle axis-aligned (no rotation or skew), and the transformed
2808/// rectangle must land on whole pixels, so no edge needs partial coverage.
2809///
2810/// [`clip`] admits a rectangular clip to its scissor path by the same rule and
2811/// through this same function: a rectangle whose coverage can be written
2812/// exactly is a rectangle whose *clip* can be applied exactly, so the two share
2813/// one admission rule rather than two that could drift apart.
2814fn fast_rect(rect: Rect, transform: Affine) -> Option<Rect> {
2815    if !is_axis_aligned(&transform) {
2816        return None;
2817    }
2818
2819    let device = transform.transform_rect_bbox(rect);
2820    is_pixel_aligned(device).then_some(device)
2821}
2822
2823/// The device window a glyph quad drawn now is trimmed to: the generator's
2824/// viewport intersected with the scissor every open bracket's clip applies.
2825///
2826/// Only asked while no coverage mask is on the clip stack, and then every open
2827/// bracket's clip is a scissor whose rectangle is exactly the bracket's
2828/// recorded bounds: a bracket and its clip are pushed together from one
2829/// rectangle under one transform ([`GroupStack`]'s invariant), and a clip
2830/// scissors only when [`fast_rect`] admits that rectangle — the same
2831/// `transform_rect_bbox` the bounds are recorded from, on whole pixels — clamped
2832/// onto the `u16` grid the same way. So this is the rectangle the general
2833/// draw's coverage is culled and scissored to, without a second clip stack.
2834fn quad_window(generator: &StripGenerator, groups: &GroupStack) -> RectU16 {
2835    let coordinate = |value: f64| value.clamp(0.0, f64::from(u16::MAX)) as u16;
2836    groups.bounds().fold(
2837        RectU16::new(0, 0, generator.width(), generator.height()),
2838        |window, bounds| {
2839            window.intersect(RectU16::new(
2840                coordinate(bounds.x0),
2841                coordinate(bounds.y0),
2842                coordinate(bounds.x1),
2843                coordinate(bounds.y1),
2844            ))
2845        },
2846    )
2847}
2848
2849/// Mark `frame`'s retained bracket `index`, a `PushRetained` body, as one no
2850/// page may hold.
2851fn mark_unretainable(frame: &mut CompiledFrame, index: usize) {
2852    if let Some(RetainedBracket {
2853        kind: RetainedKind::Record { retainable, .. },
2854        ..
2855    }) = frame.retained.get_mut(index)
2856    {
2857        *retainable = false;
2858    }
2859}
2860
2861/// Whether `device` lies wholly inside `window`.
2862fn rect_within(device: Rect, window: RectU16) -> bool {
2863    device.is_finite()
2864        && !window.is_empty()
2865        && device.x0 >= f64::from(window.x0)
2866        && device.y0 >= f64::from(window.y0)
2867        && device.x1 <= f64::from(window.x1)
2868        && device.y1 <= f64::from(window.y1)
2869}
2870
2871/// `device` rounded out to whole tiles and clamped onto the `u16` grid — the
2872/// alignment a recorded layer's own bounds take.
2873fn tile_bounds(device: Rect) -> RectU16 {
2874    let snap = |value: f64, step: u16, up: bool| {
2875        let step = f64::from(step);
2876        let snapped = if up {
2877            (value / step).ceil() * step
2878        } else {
2879            (value / step).floor() * step
2880        };
2881        if snapped.is_finite() {
2882            snapped.clamp(0.0, f64::from(u16::MAX)) as u16
2883        } else {
2884            0
2885        }
2886    };
2887    RectU16::new(
2888        snap(device.x0, Tile::WIDTH, false),
2889        snap(device.y0, Tile::HEIGHT, false),
2890        snap(device.x1, Tile::WIDTH, true),
2891        snap(device.y1, Tile::HEIGHT, true),
2892    )
2893}
2894
2895/// Whether every edge of `rect` falls on a whole pixel.
2896fn is_pixel_aligned(rect: Rect) -> bool {
2897    [rect.x0, rect.y0, rect.x1, rect.y1]
2898        .iter()
2899        .all(|v| v.is_finite() && v.fract() == 0.0)
2900}
2901
2902/// A stroke of `width` with round caps and joins — the only stroke style the
2903/// display list can express.
2904fn round_stroke(width: f64) -> Stroke {
2905    Stroke::new(width)
2906        .with_caps(Cap::Round)
2907        .with_join(Join::Round)
2908}
2909
2910/// `path` expanded into the sub-paths `dash` breaks it into.
2911///
2912/// Both ends of the expansion go through [`well_formed`]. The input needs it
2913/// because `kurbo::dash` mishandles a subpath that closes without ever
2914/// producing a segment: it emits that subpath's closing element ahead of the
2915/// `MoveTo` meant to open the output, so a path whose *first* subpath is a
2916/// zero-length closed one (a dashed arc at zero sweep records exactly that)
2917/// dashes to a sequence beginning with `ClosePath`. Such a sequence is not a
2918/// path any consumer can read — `BezPath`'s own "begins with `MoveTo`"
2919/// invariant is asserted in a debug build and silently strokes malformed
2920/// geometry in a release one. Normalizing those subpaths away first removes
2921/// the input the iterator gets wrong; normalizing the result as well makes the
2922/// well-formedness of what this returns a property of this function rather
2923/// than of the dash iterator's internal states.
2924///
2925/// Callers inside this crate only ever reach `dash` here once
2926/// [`dash_cycle_is_normalizable`] has passed it, since `kurbo::dash` itself
2927/// does not bound its catch-up loop against a non-normalizable cycle; a caller
2928/// outside the up-front walk carries that same obligation. Made `pub` (rather
2929/// than `pub(crate)`) so `frust-testing`'s CPU oracle, which keeps its own
2930/// independent copy of this lowering (see that crate's `oracle_cpu` module
2931/// docs for why), can pin its output against this one directly rather than
2932/// only through a rendered image.
2933pub fn dash_path(path: &BezPath, dash: DashPattern) -> BezPath {
2934    let source = well_formed(path.iter());
2935    well_formed(kurbo::dash(source.iter(), dash.phase, &[dash.on, dash.off]))
2936}
2937
2938/// `elements` as a path every consumer can read: opened by a `MoveTo`, and
2939/// carrying no `ClosePath` that closes a subpath with no segments in it.
2940///
2941/// Both rules drop elements that describe no geometry — an element before the
2942/// first `MoveTo` has no start point to be drawn from, and closing a subpath
2943/// that never left its start point adds no segment — so a well-formed path in
2944/// yields itself back unchanged.
2945///
2946/// `pub` for the same cross-crate-parity reason as [`dash_path`].
2947pub fn well_formed(elements: impl Iterator<Item = PathEl>) -> BezPath {
2948    let mut out = BezPath::new();
2949    // Tracked rather than read back off `out`: `BezPath::is_empty` asks whether
2950    // a path holds any SEGMENT, which a path holding only its opening `MoveTo`
2951    // does not.
2952    let mut opened = false;
2953    let mut segments_in_subpath = 0_usize;
2954
2955    for element in elements {
2956        match element {
2957            PathEl::MoveTo(_) => {
2958                opened = true;
2959                segments_in_subpath = 0;
2960                out.push(element);
2961            }
2962            PathEl::ClosePath => {
2963                if segments_in_subpath > 0 {
2964                    segments_in_subpath = 0;
2965                    out.push(element);
2966                }
2967            }
2968            PathEl::LineTo(_) | PathEl::QuadTo(..) | PathEl::CurveTo(..) => {
2969                if opened {
2970                    segments_in_subpath += 1;
2971                    out.push(element);
2972                }
2973            }
2974        }
2975    }
2976    out
2977}
2978
2979/// The display list's per-corner radii as kurbo's, in its clockwise-from-top-left
2980/// argument order.
2981fn rounded_rect_radii(radii: CornerRadii) -> RoundedRectRadii {
2982    RoundedRectRadii::new(
2983        radii.top_left,
2984        radii.top_right,
2985        radii.bottom_right,
2986        radii.bottom_left,
2987    )
2988}
2989
2990#[cfg(test)]
2991mod tests {
2992    use super::*;
2993
2994    #[test]
2995    fn a_viewport_within_three_pixels_of_the_u16_ceiling_is_refused() {
2996        assert!(check_tile_addressable(65532, 65532).is_ok());
2997        assert!(matches!(
2998            check_tile_addressable(65533, 16),
2999            Err(EngineError::TargetTooLarge)
3000        ));
3001        assert!(matches!(
3002            check_tile_addressable(16, u16::MAX),
3003            Err(EngineError::TargetTooLarge)
3004        ));
3005    }
3006
3007    /// The substitution both walks make for an off-grid `PushRetained`, and why
3008    /// making it in only one of them would matter.
3009    ///
3010    /// A bracket entered with the recorded presentation installs a correction;
3011    /// one entered neutrally installs none. That correction is precisely the
3012    /// affine a run inside the bracket is *classified* against, so a walk that
3013    /// substituted and a walk that did not would decide a run's route from one
3014    /// magnitude and draw it at another — which is the atlas route handed to a
3015    /// transform `glifo` will not absorb.
3016    #[test]
3017    fn an_off_grid_retained_bracket_is_entered_neutrally() {
3018        // A frame root already carrying an outer bracket's correction, and an
3019        // inner bracket whose own transform overflows against it. Both factors
3020        // are finite; only the composition is not.
3021        let combined = Affine::scale(1e200);
3022        let transform = Affine::scale(1e200);
3023        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
3024        let command = Command::PushRetained {
3025            key: 0,
3026            rect,
3027            alpha: 1.0,
3028            scale: 2.0,
3029            transform,
3030        };
3031
3032        assert!(!command_on_grid(&command, combined));
3033        assert!(command_on_grid(&command, Affine::IDENTITY));
3034
3035        let (scale, entered) = retained_entry(false, 2.0, transform);
3036        assert_eq!(scale, 1.0);
3037        assert_eq!(entered.as_coeffs(), Affine::IDENTITY.as_coeffs());
3038        let (scale, entered) = retained_entry(true, 2.0, transform);
3039        assert_eq!(scale, 2.0);
3040        assert_eq!(entered.as_coeffs(), transform.as_coeffs());
3041
3042        // And the difference reaches the quantity that is classified: an
3043        // outermost bracket entered with the recorded pair corrects, one
3044        // entered neutrally does not.
3045        let mut recorded = RetainedStack::new();
3046        recorded.enter(rect, 2.0, transform);
3047        assert_ne!(
3048            recorded.correction().as_coeffs(),
3049            Affine::IDENTITY.as_coeffs()
3050        );
3051
3052        let mut neutral = RetainedStack::new();
3053        let (scale, entered) = retained_entry(false, 2.0, transform);
3054        neutral.enter(rect, scale, entered);
3055        assert_eq!(
3056            neutral.correction().as_coeffs(),
3057            Affine::IDENTITY.as_coeffs()
3058        );
3059    }
3060
3061    #[test]
3062    fn pixel_alignment_rejects_fractional_edges() {
3063        assert!(is_pixel_aligned(Rect::new(0.0, 0.0, 4.0, 4.0)));
3064        assert!(!is_pixel_aligned(Rect::new(0.0, 0.5, 4.0, 4.0)));
3065        assert!(!is_pixel_aligned(Rect::new(0.0, 0.0, 4.0, f64::INFINITY)));
3066    }
3067
3068    /// [`SHADER_QUAD_SKIP_WARNING`] is a process-global [`Once`], so this
3069    /// proves the half of "exactly once" a test can still observe once
3070    /// another test in the same binary may already have tripped it: the
3071    /// latch never un-completes, whatever else in this binary called
3072    /// [`note_shader_quad_unrendered`] first. `Once::call_once` itself is the
3073    /// standard-library guarantee behind the other half — that the closure
3074    /// inside it runs at most once ever — so calling the reporting function
3075    /// twice here and observing the latch hold is a structural stand-in for
3076    /// capturing and counting the actual log line.
3077    #[test]
3078    fn an_unrendered_shader_quad_is_latched_to_once_per_process() {
3079        note_shader_quad_unrendered(1);
3080        assert!(SHADER_QUAD_SKIP_WARNING.is_completed());
3081        note_shader_quad_unrendered(1);
3082        assert!(SHADER_QUAD_SKIP_WARNING.is_completed());
3083    }
3084
3085    /// The kill switch's own report latches independently of the one above —
3086    /// two distinct facts about why a quad drew nothing, each stated once.
3087    #[test]
3088    fn a_disabled_shader_quad_is_latched_to_once_per_process() {
3089        note_shader_effects_disabled();
3090        assert!(SHADER_EFFECTS_DISABLED_WARNING.is_completed());
3091        note_shader_effects_disabled();
3092        assert!(SHADER_EFFECTS_DISABLED_WARNING.is_completed());
3093    }
3094
3095    /// A shader quad's target id is derived, never minted, and lands in the
3096    /// half of the id space `SceneTextureId::mint` cannot reach — the whole
3097    /// reason the compiler can name a texture it never saw registered.
3098    #[test]
3099    fn a_shader_quad_target_id_is_derived_from_its_program() {
3100        assert_eq!(shader_quad_texture_id(7), shader_quad_texture_id(7));
3101        assert_ne!(shader_quad_texture_id(7), shader_quad_texture_id(8));
3102        assert_ne!(shader_quad_texture_id(7), 7);
3103        assert_eq!(
3104            shader_quad_texture_id(7),
3105            SceneTextureId::for_shader_program(7).get()
3106        );
3107    }
3108
3109    #[test]
3110    fn shader_quad_is_culled_true_when_entirely_outside_the_frame() {
3111        assert!(shader_quad_is_culled(
3112            Rect::new(1000.0, 1000.0, 1008.0, 1008.0),
3113            Affine::IDENTITY,
3114            (64, 64)
3115        ));
3116    }
3117
3118    #[test]
3119    fn shader_quad_is_culled_false_when_overlapping_the_frame() {
3120        assert!(!shader_quad_is_culled(
3121            Rect::new(0.0, 0.0, 8.0, 8.0),
3122            Affine::IDENTITY,
3123            (64, 64)
3124        ));
3125    }
3126
3127    #[test]
3128    fn shader_quad_is_culled_false_touching_the_frame_edge() {
3129        // A shared edge counts as overlapping (kurbo::Rect::overlaps), so a
3130        // quad exactly abutting the frame boundary is never wrongly reported
3131        // culled.
3132        assert!(!shader_quad_is_culled(
3133            Rect::new(64.0, 0.0, 80.0, 16.0),
3134            Affine::IDENTITY,
3135            (64, 64)
3136        ));
3137    }
3138
3139    #[test]
3140    fn shader_quad_is_culled_false_for_a_non_finite_bbox() {
3141        assert!(!shader_quad_is_culled(
3142            Rect::new(0.0, 0.0, f64::NAN, 8.0),
3143            Affine::IDENTITY,
3144            (64, 64)
3145        ));
3146    }
3147
3148    #[test]
3149    fn a_culled_shader_quad_is_reported_at_debug_level_not_through_the_unrendered_warning() {
3150        // `note_shader_quad_culled` carries no process-latch to observe the
3151        // way `SHADER_QUAD_SKIP_WARNING` does above — it is meant to fire
3152        // every time, unlike the once-per-process missing-pre-pass report.
3153        // What is asserted here is the compile-time distinction itself: a
3154        // quad `shader_quad_is_culled` reports true for must never also read
3155        // as "un-rendered" by the same geometry.
3156        let frame_extent = (64, 64);
3157        let culled_dest = Rect::new(1000.0, 1000.0, 1008.0, 1008.0);
3158        let unrendered_dest = Rect::new(0.0, 0.0, 8.0, 8.0);
3159
3160        assert!(shader_quad_is_culled(
3161            culled_dest,
3162            Affine::IDENTITY,
3163            frame_extent
3164        ));
3165        assert!(!shader_quad_is_culled(
3166            unrendered_dest,
3167            Affine::IDENTITY,
3168            frame_extent
3169        ));
3170    }
3171}