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