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