Skip to main content

frust_engine/cache/
images.rs

1//! Image residency: which decoded images own atlas space, and for how long.
2//!
3//! A widget hands the display list the *same* decoded image every frame — the
4//! `peniko::ImageData` in a [`Command::Image`](frust_scene::Command::Image) is a
5//! reference-counted handle cloned per frame, never a re-decode — so the pixels
6//! behind it are stable across frames and belong on the GPU once rather than
7//! per draw. [`ImageResidency`] is what makes that "once" true: it keys an
8//! image on its blob's process-unique id ([`peniko::Blob::id`]) and hands back
9//! the same [`ImageId`] and the same atlas rectangle for every later frame that
10//! draws it.
11//!
12//! Allocation itself is pure — a rectangle packer over
13//! [`vello_common::multi_atlas::MultiAtlasManager`], no device in the loop — so
14//! everything here is host-testable. The two GPU-side halves it *describes*
15//! (writing a newly allocated region's texels, clearing an evicted one's) are
16//! carried out by [`crate::gpu::atlas`] against the plan this module produces:
17//! [`ImageResidency::evictions`] first, [`ImageResidency::uploads`] second. That
18//! order is a contract, not a preference — a rectangle freed this frame can be
19//! re-allocated in the same frame, so clearing after uploading would erase the
20//! image that just moved in.
21//!
22//! ## Residency is committed only once the plan is serviced
23//!
24//! The cache decides residency long before anything reaches a queue: a
25//! compiled frame can still be refused afterwards (a layer shape the scheduler
26//! will not serve, a page past the pool's ceiling, coverage or paints outgrowing
27//! their textures), and a frame refused after compiling is never drawn and never
28//! uploaded. So the plan is *pending* rather than taken: [`ImageResidency::plan`]
29//! hands out a copy, [`begin_frame`](ImageResidency::begin_frame) leaves it
30//! alone, and only [`acknowledge_plan`](ImageResidency::acknowledge_plan) —
31//! called by a consumer that has actually written the regions into a live atlas
32//! — clears it.
33//!
34//! That is what makes the entry map and the atlas's real contents agree. Without
35//! it, a refused frame leaves an image *recorded as resident* whose texels were
36//! never written: every later frame resolves the entry, schedules no upload
37//! (residency is a hit), and either drops the draw for the life of the process
38//! or samples a rectangle nothing ever wrote. Re-emitting the same plan until it
39//! is acknowledged costs a vector copy per frame in the steady state — where the
40//! plan is empty — and is what makes "an image drawn on a thousand frames
41//! uploads once" true of the *atlas* rather than only of the cache.
42//!
43//! ## An image larger than the atlas is minified, not dropped
44//!
45//! A source whose own extent will not fit one atlas layer is downsampled to fit
46//! ([`fit_extent`], [`minify`]) instead of being refused. A photograph decoded
47//! at its capture resolution is routinely several times the atlas budget while
48//! its destination rectangle is a thumbnail, so refusing it drops a draw the
49//! display list plainly describes; and every texel past the destination's own
50//! scale is one the sampler would have thrown away anyway.
51//!
52//! The filter is a plain box average over the premultiplied texels, hand-written
53//! rather than pulled in: the workspace's version pins are law, an image decoder
54//! is not a dependency this crate carries, and a box average is exactly right
55//! for the minification direction (every output texel is the mean of the source
56//! texels it covers). The aspect ratio is preserved, so the paint transform's
57//! two axes stay in step; what changes is the resident rectangle's extent, which
58//! [`ResidentImage::natural`] records alongside it so the consumer can rescale
59//! the encoded paint's own transform into the smaller rectangle.
60//!
61//! ## Two things are refused rather than attempted
62//!
63//! 1. **A format or size `vello_common` panics on.**
64//!    [`vello_common::paint::ImageSource::from_peniko_image_data`] asserts on a
65//!    dimension past `u16::MAX` and `unimplemented!()`s on a format outside
66//!    `Rgba8`/`Bgra8`, and a pixel buffer whose length disagrees with the
67//!    declared extent trips [`Pixmap::from_parts_with_opacity`]'s own assertion.
68//!    All three are checked here, *before* the call, so an oversized or
69//!    malformed image is an [`ImageSkip`] the frame path reports rather than a
70//!    panic it takes (E17). These, plus an atlas with no room left in it, are
71//!    now the only things [`ImageResidency::skipped`] counts: an image that is
72//!    merely *big* is minified and drawn.
73//! 2. **Anything at all, when `FRUST_ENGINE_NO_ATLAS` is set.** The kill switch
74//!    ([`crate::config::atlas_disabled`]) makes every resolution answer
75//!    [`ImageSkip::AtlasDisabled`], so no atlas is allocated, nothing is
76//!    uploaded, and image draws fall back to painting nothing — the switch's
77//!    whole point being to take the atlas out of a frame under diagnosis.
78//!
79//! ## Residency is bounded by age, and by pressure
80//!
81//! An entry unseen for [`MAX_UNSEEN_FRAMES`] consecutive frames is deallocated
82//! and its rectangle reported for clearing — the same age-based reap
83//! `frust-render`'s shader-effect cache uses, and for the same reason: a screen
84//! that stops drawing an image should give its texels back promptly, while an
85//! image drawn every other frame must never be mistaken for gone.
86//!
87//! Age alone is not a bound on *population*, though: it says nothing about how
88//! many distinct images a screen draws inside one age window. A list scrolling
89//! faster than [`MAX_UNSEEN_FRAMES`] streams more of them past the viewport
90//! than the atlas has rectangles for, and every one past the last free
91//! rectangle used to be an [`ImageSkip::NoAtlasSpace`] — a draw that painted
92//! nothing, on a screen whose solid fills all still landed, which reads as a
93//! hole rather than as a cache miss. So an allocation the packer refuses now
94//! *evicts* instead of giving up: [`ImageResidency::resolve`] works out which
95//! least-recently-seen rectangles would make room, frees exactly those, and
96//! each one whose rectangle was taken re-uploads on the next frame that draws
97//! it. A working set larger than the atlas therefore degrades to re-uploads —
98//! bandwidth — rather than to missing content, and
99//! [`ImageResidency::pressure_evictions`] counts how often that trade was made.
100//!
101//! ## An eviction is planned on paper before anything is freed
102//!
103//! An eviction that does not produce the allocation is pure loss. The draw is
104//! skipped anyway, so the frame pays a clear per freed rectangle *and* the
105//! re-upload of every displaced image *and* the hole it was trying to avoid.
106//! And the condition recurs: a request the atlas can never satisfy would strip
107//! and rebuild the residency on every frame that asks for it, which on a mobile
108//! budget is megabytes of texture traffic each way, per frame, buying nothing.
109//!
110//! So the pressure path commits nothing speculatively. `allocate_under_pressure`
111//! walks the candidates in least-recently-seen order over a *model* of the
112//! layers — the rectangles this residency knows are occupied, and each layer's
113//! free area as the packer itself reports it — and stops at the first prefix
114//! that would admit the request. Only then is anything released, and only the
115//! candidates on the layer the fit was found in. A request no bounded set of
116//! candidates can satisfy therefore stays the cheap [`ImageSkip::NoAtlasSpace`]
117//! it was before eviction existed, with the residency untouched.
118//!
119//! The model is deliberately *optimistic*: it can see this residency's own
120//! rectangles but not the glyph pages sharing the same allocator (see below),
121//! so it never refuses a request eviction could have satisfied on a given
122//! layer — a placement that clears the real occupants clears the model's
123//! subset of them too. The reverse can happen: the packer may still refuse a
124//! plan the model approved, which costs the planned evictions and no
125//! allocation. That case is bounded by the same plan budget rather than
126//! open-ended, and it is the one residual the planner cannot rule out without
127//! knowing where every glyph page sits.
128//!
129//! The bounded search itself is scoped *per layer*: each atlas layer gets its
130//! own candidate-count and area allowance rather than the whole residency
131//! sharing one pool, so a request only one layer could ever satisfy is not
132//! starved by candidates on layers that were never going to host it — each
133//! layer is either cleared within its own allowance or ruled out on its own
134//! terms, never on account of what another layer's candidates already cost.
135//!
136//! The one entry never freed on either route is one *this frame* has already
137//! resolved. The plan clears before it uploads, so handing back a rectangle the
138//! frame is about to sample would erase exactly the image the eviction was
139//! meant to make room for. A working set that outgrows the atlas inside a
140//! single frame is therefore still a skip, and honestly so: nothing is
141//! evictable. The same guard covers the *extent-mismatch* release in
142//! [`ImageResidency::resolve`], where a blob redrawn at a new extent gives its
143//! old rectangle back: on a frame that already sampled that rectangle the
144//! second draw is skipped instead, as
145//! [`ImageSkip::SameFrameExtentConflict`] rather than as a capacity refusal —
146//! the atlas may hold nothing at all beside the first extent's own rectangle.
147//!
148//! ## One allocator for images *and* glyphs
149//!
150//! The [`ImageCache`] this module holds is the process's only atlas allocator:
151//! the glyph policy (`crate::text::atlas_policy`) owns no cache of its own and
152//! allocates its slots through [`ImageResidency::allocator_mut`].
153//!
154//! That is a correctness requirement, not tidiness. An [`ImageId`] is a *slot
155//! index into one cache* — `ImageCache::allocate` hands out the next free index
156//! of its own `slots` vector — and the strip shader has exactly one atlas
157//! texture array to sample from. Two caches over the same page geometry would
158//! therefore mint the same small integers for unrelated occupants and pack them
159//! into overlapping rectangles of the same layers, so a glyph and an image would
160//! address each other's texels by construction. Sharing one cache makes both
161//! collisions unrepresentable: one id space, one packer, one set of live
162//! rectangles.
163//!
164//! It is also what upstream does — `vello_hybrid`'s `Resources` holds a single
165//! `image_cache` and hands it to its glyph atlas at every call — and it is what
166//! keeps the budget honest: a glyph page and an image page come out of the same
167//! [`AtlasBudget::max_atlases`] allowance, and [`ImageResidency::layers`] counts
168//! whichever of the two created a layer.
169//!
170//! The one rule a borrower must keep is ownership of its own handles: each side
171//! deallocates only the ids it allocated. Nothing enforces that beyond the two
172//! call sites, and it is the reason [`ImageResidency::allocator_mut`] is
173//! documented as a contract rather than a plain accessor.
174
175use std::collections::{HashMap, HashSet};
176use std::sync::Arc;
177#[cfg(feature = "perf-trace")]
178use std::sync::atomic::{AtomicBool, Ordering};
179
180use frust_gpu::{DownlevelProfile, TierCaps};
181use peniko::color::PremulRgba8;
182use peniko::{ImageData, ImageFormat};
183use thiserror::Error;
184use vello_common::image_cache::ImageCache;
185use vello_common::multi_atlas::{AllocationStrategy, AtlasConfig};
186use vello_common::paint::{ImageId, ImageSource};
187use vello_common::pixmap::Pixmap;
188
189use crate::config;
190
191/// Consecutive frames an image may go undrawn before its atlas rectangle is
192/// reclaimed.
193///
194/// 60 frames is half a second at 120Hz and a whole one at 60Hz: long enough
195/// that an image drawn intermittently (an every-other-frame animation, a brief
196/// scene-diff hiccup) never loses its residency, short enough that navigating
197/// away from an image-heavy screen returns its texels within a frame budget's
198/// worth of frames rather than at surface teardown.
199pub const MAX_UNSEEN_FRAMES: u64 = 60;
200
201/// Transparent padding pixels placed around each image in the atlas.
202///
203/// Zero, deliberately. Padding exists to stop a filtered sample from reading a
204/// neighbour's texels, and the strip shader's atlas samplers cannot do that:
205/// both the bilinear and the bicubic path clamp every tap into
206/// `[offset, offset + size - 1]` — the image's own rectangle — before it
207/// reaches `textureLoad`. Paying a two-pixel border per image would buy nothing
208/// and cost atlas space that a mobile budget does not have.
209pub const ATLAS_PADDING: u16 = 0;
210
211/// The largest image edge that can be made resident at all.
212///
213/// `vello_common`'s image cache addresses an offset and an extent in `u16`, and
214/// its `ImageSource` conversion asserts on anything larger, so this is the
215/// ceiling the pre-check enforces rather than a policy of ours.
216pub const MAX_IMAGE_DIMENSION: u32 = u16::MAX as u32;
217
218/// The most atlas layers the shader's encoded-image record can name.
219///
220/// `GpuEncodedImage::image_params` gives `atlas_index` eight bits, so a layer
221/// past 255 could not be addressed even if the adapter allowed it.
222pub const MAX_ATLAS_LAYERS: usize = 256;
223
224/// Why one image could not be made resident.
225///
226/// Every variant is a *skip*, never a frame failure: the draw paints nothing
227/// and the rest of the frame proceeds. They are distinguished because the
228/// remedies differ — a too-large image is an application decision, an exhausted
229/// atlas is a budget one, and a disabled atlas is the operator's own doing.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
231pub enum ImageSkip {
232    /// `FRUST_ENGINE_NO_ATLAS` is set; the atlas is out of the frame entirely.
233    #[error("image residency is disabled by FRUST_ENGINE_NO_ATLAS")]
234    AtlasDisabled,
235    /// The image declares a format the atlas does not store.
236    #[error("unsupported image format {0:?}: only Rgba8 and Bgra8 are uploaded")]
237    UnsupportedFormat(ImageFormat),
238    /// The image has no area, so there is nothing to make resident.
239    #[error("degenerate image extent {width}x{height}")]
240    DegenerateExtent {
241        /// Declared width in pixels.
242        width: u32,
243        /// Declared height in pixels.
244        height: u32,
245    },
246    /// The image is larger than [`MAX_IMAGE_DIMENSION`] on at least one axis.
247    #[error("image {width}x{height} exceeds the {max}-pixel residency ceiling")]
248    TooLarge {
249        /// Declared width in pixels.
250        width: u32,
251        /// Declared height in pixels.
252        height: u32,
253        /// The per-axis ceiling that refused it.
254        max: u32,
255    },
256    /// The pixel buffer's length disagrees with the declared extent and format.
257    #[error("image pixel buffer is {actual} bytes; {width}x{height} needs exactly {expected}")]
258    MalformedPixels {
259        /// Declared width in pixels.
260        width: u32,
261        /// Declared height in pixels.
262        height: u32,
263        /// Bytes the declared extent and format require.
264        expected: usize,
265        /// Bytes the blob actually holds.
266        actual: usize,
267    },
268    /// The paint transform cannot be inverted, so there is no device-to-image
269    /// mapping for the shader to sample through.
270    #[error("image paint transform is singular or non-finite")]
271    SingularTransform,
272    /// The image fits the ceiling but not the configured atlas budget, and no
273    /// resident image could be evicted to make room for it — either the atlas
274    /// holds nothing but images this same frame drew, or what it holds beside
275    /// them still leaves no rectangle this one fits in.
276    #[error("image {width}x{height} does not fit the {atlas_width}x{atlas_height} atlas budget")]
277    NoAtlasSpace {
278        /// Declared width in pixels.
279        width: u32,
280        /// Declared height in pixels.
281        height: u32,
282        /// Configured atlas width.
283        atlas_width: u32,
284        /// Configured atlas height.
285        atlas_height: u32,
286    },
287    /// The same blob was already resolved earlier in this same frame at a
288    /// different extent.
289    ///
290    /// Not an atlas-capacity refusal: the atlas may be nearly empty. The plan
291    /// clears before it uploads (see the module doc), so releasing this
292    /// blob's rectangle to make room for its own second extent would schedule
293    /// a clear over texels this frame already sampled — the first extent's
294    /// draw keeps its texels and the second is skipped until the next frame
295    /// instead.
296    #[error(
297        "image {width}x{height} already resolved this frame at a different extent; the first \
298         extent's texels are kept until the next frame"
299    )]
300    SameFrameExtentConflict {
301        /// The refused extent's width, after [`fit_extent`] the same way
302        /// [`NoAtlasSpace`](Self::NoAtlasSpace) reports it.
303        width: u32,
304        /// The refused extent's height, after [`fit_extent`].
305        height: u32,
306    },
307}
308
309/// The atlas geometry a tier of adapter is given.
310///
311/// Never `vello_common`'s own [`AtlasConfig`] default: that is 4096 square
312/// across eight layers, 64 MiB of `Rgba8Unorm` per layer and half a gigabyte in
313/// total, which is a desktop-only figure that a phone would pay for the first
314/// image it drew.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct AtlasBudget {
317    /// Extent of each atlas layer, in pixels.
318    pub atlas_size: (u32, u32),
319    /// Maximum number of layers the array may grow to.
320    pub max_atlases: usize,
321}
322
323impl AtlasBudget {
324    /// The budget a tile-based or downlevel-clamped adapter gets: 4 MiB a
325    /// layer, 16 MiB in total.
326    pub const MOBILE: Self = Self {
327        atlas_size: (1024, 1024),
328        max_atlases: 4,
329    };
330
331    /// The budget a full-profile immediate-mode adapter gets: 16 MiB a layer,
332    /// 128 MiB in total.
333    pub const DESKTOP: Self = Self {
334        atlas_size: (2048, 2048),
335        max_atlases: 8,
336    };
337
338    /// The budget for `caps`' adapter, clamped to what it can actually create.
339    ///
340    /// The tier choice is [`is_mobile_tier`]; the clamps that follow are the
341    /// adapter's own texture-edge and array-layer ceilings plus the eight-bit
342    /// [`MAX_ATLAS_LAYERS`] the encoded-image record can address.
343    ///
344    /// `FRUST_ENGINE_ATLAS_SIZE` overrides the tier's extent, and the override
345    /// is **budgeted, not unbudgeted**: it passes through
346    /// [`within_total_bytes`](Self::within_total_bytes) against the tier's own
347    /// [`total_bytes`](Self::total_bytes) before the adapter clamps. So the knob
348    /// redistributes a tier's memory between extent and depth — one large layer
349    /// instead of eight small ones — and cannot spend more of it than the tier
350    /// itself would. Clamped to the adapter last, because an override is a
351    /// request and a request the adapter cannot honour is still narrowed to what
352    /// it can.
353    ///
354    /// The resolved budget is reported once per process through
355    /// `atlas_tier_line`, because which of the two tiers a given phone takes is
356    /// otherwise an inference from behaviour rather than an observation: the
357    /// signals [`is_mobile_tier`] reads are the driver's own answers, and a
358    /// device whose driver answers differently from the one it resembles is
359    /// exactly the case a benchmark capture has to be able to name. That line
360    /// is `perf-trace`-only, like every other `frust-perf` line the engine
361    /// emits — a build without the feature resolves the same budget and writes
362    /// nothing.
363    #[must_use]
364    pub fn for_caps(caps: &TierCaps) -> Self {
365        let tier = if is_mobile_tier(caps) {
366            Self::MOBILE
367        } else {
368            Self::DESKTOP
369        };
370        let resolved = match config::atlas_size() {
371            None => tier.clamped(caps),
372            Some(atlas_size) => Self {
373                atlas_size,
374                max_atlases: tier.max_atlases,
375            }
376            .within_total_bytes(tier.total_bytes())
377            .clamped(caps),
378        };
379
380        #[cfg(feature = "perf-trace")]
381        ATLAS_TIER_LOG.emit(caps, resolved);
382        resolved
383    }
384
385    /// This budget narrowed so a fully grown array costs at most `total_bytes`.
386    ///
387    /// The extent goes first, scaled uniformly until one layer fits inside the
388    /// whole allowance; the layer count then takes whatever multiple of that
389    /// layer is left, never below one. Scaling the extent rather than only the
390    /// depth is what makes the ceiling hold at all: a single 16384-square layer
391    /// is a gigabyte on its own, so a clamp that could only reduce the layer
392    /// *count* would have nothing left to reduce.
393    ///
394    /// Both axes are floored after the uniform scale, which can only take the
395    /// product below the allowance — except where an extreme aspect ratio floors
396    /// one axis to nothing and it is raised back to a texel, so each axis is
397    /// then held against what the other leaves.
398    #[must_use]
399    pub fn within_total_bytes(self, total_bytes: u64) -> Self {
400        // A single texel is the smallest atlas that exists, so an allowance
401        // below one is treated as one rather than producing a zero extent the
402        // packer could not address.
403        let allowance = total_bytes.max(ATLAS_FORMAT_BYTES);
404        let max_texels = allowance / ATLAS_FORMAT_BYTES;
405
406        let (width, height) = self.atlas_size;
407        let texels = u64::from(width).saturating_mul(u64::from(height));
408        let atlas_size = if texels <= max_texels {
409            (width, height)
410        } else {
411            // `as` on a float saturates rather than wrapping, and both factors
412            // are finite and positive, so the narrowing is a plain floor.
413            let scale = (max_texels as f64 / texels.max(1) as f64).sqrt();
414            let scaled_w = ((f64::from(width) * scale) as u32).max(1);
415            let scaled_h = ((f64::from(height) * scale) as u32).max(1);
416            let fitted_w = fit_axis(scaled_w, max_texels / u64::from(scaled_h));
417            let fitted_h = fit_axis(scaled_h, max_texels / u64::from(fitted_w));
418            (fitted_w, fitted_h)
419        };
420
421        let layer_bytes = u64::from(atlas_size.0)
422            .saturating_mul(u64::from(atlas_size.1))
423            .saturating_mul(ATLAS_FORMAT_BYTES)
424            .max(1);
425        let layers = usize::try_from(allowance / layer_bytes).unwrap_or(MAX_ATLAS_LAYERS);
426
427        Self {
428            atlas_size,
429            max_atlases: self.max_atlases.min(layers).max(1),
430        }
431    }
432
433    /// Whether `region` lies inside an atlas array of `layers` layers at this
434    /// budget's per-layer extent.
435    ///
436    /// The pure counterpart of [`crate::gpu::atlas::AtlasArray::contains`],
437    /// answerable with no device: a consumer that records where an image lives
438    /// before the array exists checks the rectangle here, so it can never come
439    /// to name a region the array will refuse to write and then sample it as
440    /// whatever the texture happened to hold.
441    #[must_use]
442    pub fn contains(self, region: AtlasRegion, layers: u32) -> bool {
443        !region.is_empty()
444            && region.layer < layers
445            && region.offset[0].saturating_add(region.size[0]) <= self.atlas_size.0
446            && region.offset[1].saturating_add(region.size[1]) <= self.atlas_size.1
447    }
448
449    /// This budget narrowed to what `caps`' adapter can create.
450    #[must_use]
451    pub fn clamped(self, caps: &TierCaps) -> Self {
452        let edge = caps.max_texture_dimension_2d.max(1);
453        let layers = usize::try_from(caps.max_texture_array_layers).unwrap_or(MAX_ATLAS_LAYERS);
454
455        Self {
456            atlas_size: (
457                self.atlas_size.0.clamp(1, edge),
458                self.atlas_size.1.clamp(1, edge),
459            ),
460            max_atlases: self.max_atlases.clamp(1, layers.min(MAX_ATLAS_LAYERS)),
461        }
462    }
463
464    /// The `vello_common` configuration this budget describes.
465    ///
466    /// `initial_atlas_count` is zero so the first layer is created by the first
467    /// allocation that needs one: an application drawing no images pays for no
468    /// atlas at all, and [`vello_common::multi_atlas::MultiAtlasManager::new`]'s
469    /// own `expect` on eager creation can never be reached.
470    #[must_use]
471    pub fn config(self) -> AtlasConfig {
472        AtlasConfig {
473            initial_atlas_count: 0,
474            max_atlases: self.max_atlases,
475            atlas_size: self.atlas_size,
476            auto_grow: true,
477            allocation_strategy: AllocationStrategy::FirstFit,
478        }
479    }
480
481    /// The bytes one fully populated layer costs at [`ATLAS_FORMAT_BYTES`].
482    #[must_use]
483    pub fn layer_bytes(self) -> u64 {
484        u64::from(self.atlas_size.0)
485            .saturating_mul(u64::from(self.atlas_size.1))
486            .saturating_mul(ATLAS_FORMAT_BYTES)
487    }
488
489    /// The bytes every layer of this budget costs once fully grown.
490    #[must_use]
491    pub fn total_bytes(self) -> u64 {
492        self.layer_bytes()
493            .saturating_mul(self.max_atlases.try_into().unwrap_or(u64::MAX))
494    }
495}
496
497/// Bytes per texel of the atlas array (`Rgba8Unorm`).
498pub const ATLAS_FORMAT_BYTES: u64 = 4;
499
500/// Whether `caps`' adapter takes the mobile atlas budget.
501///
502/// Two signals, either of which is enough:
503///
504/// - its usable limits are clamped to the GLES-3.0/WebGL2 downlevel defaults,
505///   which is the profile every GL-backed target reports; or
506/// - it reports `transient_saves_memory`, wgpu's own answer to "is this a
507///   tile-based renderer keeping attachments on-chip" — true on the mobile
508///   TBDR parts and false on desktop immediate-mode ones.
509///
510/// Neither reads the adapter *name*: a name match is a denylist that ages, and
511/// both signals above are capability answers the driver gives directly.
512#[must_use]
513pub fn is_mobile_tier(caps: &TierCaps) -> bool {
514    caps.downlevel_profile != DownlevelProfile::Full || caps.transient_saves_memory
515}
516
517/// The process's latch for the resolved-tier line, held by
518/// [`AtlasBudget::for_caps`].
519#[cfg(feature = "perf-trace")]
520static ATLAS_TIER_LOG: TierLogOnce = TierLogOnce::new();
521
522/// A one-shot latch for the resolved-tier line.
523///
524/// Once per *process* rather than once per call: a budget is resolved once per
525/// surface, and the question the line answers — which tier this adapter took —
526/// is about the device, so one line per run answers it completely while a line
527/// per surface would repeat it.
528///
529/// A latch value rather than a bare [`std::sync::Once`] because the rule it
530/// keeps is worth testing: [`emit`](Self::emit) answers whether *this* call was
531/// the one that wrote the line, so a test constructs a latch of its own and
532/// pins "the first call writes and no later one does" without installing a
533/// process-global logger.
534///
535/// Compiled only under `perf-trace`, with the line it writes: the release-lean
536/// gate asserts a shipping binary carries no `frust-perf` bytes at all, and a
537/// `#[cfg]` is what makes that the compiler's answer rather than the
538/// optimizer's.
539#[cfg(feature = "perf-trace")]
540#[derive(Debug, Default)]
541pub struct TierLogOnce {
542    emitted: AtomicBool,
543}
544
545#[cfg(feature = "perf-trace")]
546impl TierLogOnce {
547    /// A latch that has not emitted yet.
548    #[must_use]
549    pub const fn new() -> Self {
550        Self {
551            emitted: AtomicBool::new(false),
552        }
553    }
554
555    /// Log [`atlas_tier_line`] for `caps` and `budget` if this latch has not
556    /// fired, answering whether this call was the one that did.
557    pub fn emit(&self, caps: &TierCaps, budget: AtlasBudget) -> bool {
558        if self.emitted.swap(true, Ordering::Relaxed) {
559            return false;
560        }
561
562        log::info!("{}", atlas_tier_line(caps, budget));
563        true
564    }
565}
566
567/// The one line a run records the resolved atlas tier as.
568///
569/// `perf-trace`-only, with [`TierLogOnce`] — see its own doc for why the gate
570/// is a `#[cfg]` rather than a branch.
571///
572/// `frust-perf`-prefixed deliberately: a benchmark capture keeps every line
573/// carrying that prefix and drops the rest, so this is what lets a run's own
574/// log say which budget the device was given rather than leaving it to be
575/// inferred from whether images went missing. The adapter name is last because
576/// it is the one field that can contain spaces, which keeps every `key=value`
577/// field ahead of it parseable by splitting on whitespace.
578#[cfg(feature = "perf-trace")]
579#[must_use]
580pub fn atlas_tier_line(caps: &TierCaps, budget: AtlasBudget) -> String {
581    let tier = if is_mobile_tier(caps) {
582        "mobile"
583    } else {
584        "desktop"
585    };
586
587    format!(
588        "frust-perf atlas tier={tier} budget={}x{}x{} downlevel={:?} \
589         transient_saves_memory={} adapter={}",
590        budget.atlas_size.0,
591        budget.atlas_size.1,
592        budget.max_atlases,
593        caps.downlevel_profile,
594        caps.transient_saves_memory,
595        caps.adapter_name,
596    )
597}
598
599/// Where one image's texels live in the atlas array.
600///
601/// Hashable because the pending-clear list carries a set index beside it: the
602/// same rectangle can reach [`ImageResidency::evictions`] twice before a single
603/// clear has been serviced, and the dedup that keeps it reported once must not
604/// be a linear scan of a list that grows with the churn rate.
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
606pub struct AtlasRegion {
607    /// Array layer holding the region.
608    pub layer: u32,
609    /// Pixel offset of the region's top-left corner within that layer.
610    pub offset: [u32; 2],
611    /// Region extent in pixels.
612    pub size: [u32; 2],
613}
614
615impl AtlasRegion {
616    /// The bytes this region's texels occupy at [`ATLAS_FORMAT_BYTES`].
617    #[must_use]
618    pub fn byte_len(self) -> usize {
619        (self.size[0] as usize)
620            .saturating_mul(self.size[1] as usize)
621            .saturating_mul(ATLAS_FORMAT_BYTES as usize)
622    }
623
624    /// The row stride a texel copy of this region uses.
625    #[must_use]
626    pub fn bytes_per_row(self) -> u32 {
627        self.size[0].saturating_mul(ATLAS_FORMAT_BYTES as u32)
628    }
629
630    /// Whether this region has any texels at all.
631    #[must_use]
632    pub fn is_empty(self) -> bool {
633        self.size[0] == 0 || self.size[1] == 0
634    }
635}
636
637/// A resident image: the handle a paint names it by, and where its texels are.
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639pub struct ResidentImage {
640    /// The handle an [`ImageSource::OpaqueId`] carries.
641    pub id: ImageId,
642    /// The atlas rectangle holding the image's own pixels, padding excluded.
643    ///
644    /// Equal to [`natural`](Self::natural) unless the source was minified to
645    /// fit the atlas, in which case it is the smaller rectangle actually
646    /// uploaded.
647    pub region: AtlasRegion,
648    /// The source's own declared extent, before any minification.
649    ///
650    /// Carried because it is the extent a paint transform was composed against
651    /// — `compile::paint::encode_image_command` derives its natural-to-dest
652    /// mapping from the `ImageData`'s declared size, which it reads before this
653    /// residency is consulted. A consumer that ends up sampling a *minified*
654    /// rectangle has to fold [`minify_scale`](Self::minify_scale) into that
655    /// transform, and this is the half of the ratio the atlas rectangle does
656    /// not carry.
657    pub natural: [u32; 2],
658    /// Transparent padding pixels around `region` in the atlas.
659    pub padding: u32,
660    /// Whether the image's premultiplied pixels include any non-opaque texel.
661    pub may_have_transparency: bool,
662}
663
664impl ResidentImage {
665    /// This image as the paint-side source a `vello_common` encoding carries.
666    #[must_use]
667    pub fn source(&self) -> ImageSource {
668        ImageSource::opaque_id_with_transparency_hint(self.id, self.may_have_transparency)
669    }
670
671    /// The per-axis factor mapping a natural-space texel coordinate onto the
672    /// resident rectangle, or `None` when the source was stored at full size.
673    ///
674    /// `None` rather than `Some((1.0, 1.0))` so a caller can skip the rescale
675    /// entirely on the overwhelmingly common path, and so "was this image
676    /// minified?" is one question rather than a float comparison.
677    #[must_use]
678    pub fn minify_scale(&self) -> Option<(f32, f32)> {
679        if self.region.size == self.natural {
680            return None;
681        }
682        let natural_w = self.natural[0].max(1) as f32;
683        let natural_h = self.natural[1].max(1) as f32;
684        Some((
685            self.region.size[0] as f32 / natural_w,
686            self.region.size[1] as f32 / natural_h,
687        ))
688    }
689}
690
691/// One region's texels, waiting to be written into the atlas array.
692///
693/// The pixels are the premultiplied `Rgba8Unorm` the atlas stores, produced
694/// once at allocation — an image made resident on frame 1 and drawn on frames
695/// 1..1000 is converted exactly once.
696#[derive(Debug, Clone)]
697pub struct ImageUpload {
698    /// The handle the paint naming these pixels carries.
699    ///
700    /// Carried so a consumer rebuilding its own view of residency can key this
701    /// upload directly rather than pairing the frame's uploads positionally
702    /// against its encoded paints. A plan re-emitted until it is acknowledged
703    /// breaks that pairing (an upload can outlive the frame whose draw order
704    /// produced it), and an upload that names itself needs no ordering to be
705    /// read correctly.
706    pub id: ImageId,
707    /// Where the texels go.
708    pub region: AtlasRegion,
709    /// The source's own declared extent, before any minification — see
710    /// [`ResidentImage::natural`], whose value this is.
711    ///
712    /// Carried on the upload because a consumer rebuilding its own view of
713    /// residency from a compiled frame has the upload and nothing else, and
714    /// `region` alone cannot say whether it holds a minified copy.
715    pub natural: [u32; 2],
716    /// The premultiplied pixels, row-major and exactly `region`'s extent.
717    pub pixels: Arc<Pixmap>,
718    /// Whether those pixels include any non-opaque texel — the same hint
719    /// [`ResidentImage::may_have_transparency`] carries, so a consumer can
720    /// rebuild the whole residency record from this upload alone.
721    pub may_have_transparency: bool,
722}
723
724/// One cache entry: the handle, its rectangle, and when it was last drawn.
725#[derive(Debug, Clone, Copy)]
726struct Entry {
727    id: ImageId,
728    region: AtlasRegion,
729    /// The source extent this entry was allocated for, which is what a later
730    /// frame's `ImageData` is matched against — `region` may be the minified
731    /// rectangle and so says nothing about the source.
732    natural: [u32; 2],
733    may_have_transparency: bool,
734    last_seen: u64,
735}
736
737/// The images resident in the atlas array, keyed by blob identity.
738///
739/// One per compiler (and so one per surface). Its frame clock is advanced by
740/// [`begin_frame`](Self::begin_frame), which is also where the age-based reap
741/// runs — before any of the frame's own resolutions, so a rectangle freed this
742/// frame is available to this frame's allocations and its clear is ordered
743/// ahead of their uploads.
744///
745/// It also owns the atlas allocator the *glyph* policy packs into — see this
746/// module's doc and [`allocator_mut`](Self::allocator_mut).
747#[derive(Debug)]
748pub struct ImageResidency {
749    cache: ImageCache,
750    budget: AtlasBudget,
751    entries: HashMap<u64, Entry>,
752    frame: u64,
753    disabled: bool,
754    uploads: Vec<ImageUpload>,
755    evictions: Vec<AtlasRegion>,
756    /// The pending clears' own set index: exactly the rectangles `evictions`
757    /// holds, kept so [`release`](Self::release)'s "report a rectangle once"
758    /// dedup is a hash lookup rather than a scan of a list whose length grows
759    /// with the churn rate.
760    eviction_set: HashSet<AtlasRegion>,
761    skipped: u64,
762    /// Rectangles taken back from a resident image because the packer had no
763    /// room for a new one, over this residency's lifetime.
764    pressure_evictions: u64,
765    /// The same count for the current frame alone, zeroed by
766    /// [`begin_frame`](Self::begin_frame) — what a per-frame report reads, since
767    /// the lifetime total says nothing about whether the atlas is churning
768    /// *now*.
769    frame_pressure_evictions: u64,
770    minified: u64,
771    /// Blob keys whose minification has already been reported, so a source that
772    /// is redrawn — or reaped and made resident again — logs once rather than
773    /// once per residency.
774    reported_minify: HashSet<u64>,
775    /// Scratch for the reap's key list, kept so a frame that evicts allocates
776    /// nothing.
777    reaped: Vec<u64>,
778    /// Scratch for the pressure planner's view of the layers — one rectangle
779    /// per resident entry, grouped by layer — kept for the same reason: the
780    /// frame that has to evict is the one least able to afford an allocation.
781    pressure_rects: Vec<PressureRect>,
782    /// Scratch for the candidate order over [`Self::pressure_rects`], least
783    /// recently seen first. Built and sorted once per refused allocation, never
784    /// once per eviction.
785    pressure_order: Vec<usize>,
786    /// Scratch for each layer's modelled free area, indexed by layer.
787    pressure_free_area: Vec<u64>,
788    /// Scratch for the distinct layers [`Self::pressure_order`] names, in the
789    /// order their stalest candidate appears — the per-layer walk's own visit
790    /// order, kept for the same no-allocation-while-evicting reason as the
791    /// other pressure scratch.
792    pressure_layers: Vec<u32>,
793    /// Scratch for the keys an accepted plan releases.
794    pressure_plan: Vec<u64>,
795}
796
797impl ImageResidency {
798    /// A residency over `budget`'s atlas geometry.
799    ///
800    /// `FRUST_ENGINE_NO_ATLAS` is consulted here, once, rather than per
801    /// resolution: the switch is process-global and cached, so re-reading it on
802    /// a hot path would buy nothing but a lock. A residency constructed with
803    /// the switch set is exactly [`disabled`](Self::disabled).
804    #[must_use]
805    pub fn new(budget: AtlasBudget) -> Self {
806        Self::with_enabled(budget, !config::atlas_disabled())
807    }
808
809    /// A residency that refuses every image, allocating no atlas at all.
810    ///
811    /// What `FRUST_ENGINE_NO_ATLAS` selects, and what a caller wanting the same
812    /// effect programmatically constructs. Every [`resolve`](Self::resolve)
813    /// answers [`ImageSkip::AtlasDisabled`], so image draws are dropped with a
814    /// logged reason rather than painted from an atlas that does not exist.
815    #[must_use]
816    pub fn disabled(budget: AtlasBudget) -> Self {
817        Self::with_enabled(budget, false)
818    }
819
820    fn with_enabled(budget: AtlasBudget, enabled: bool) -> Self {
821        Self {
822            cache: ImageCache::new_with_config(budget.config()),
823            budget,
824            entries: HashMap::new(),
825            frame: 0,
826            disabled: !enabled,
827            uploads: Vec::new(),
828            evictions: Vec::new(),
829            eviction_set: HashSet::new(),
830            skipped: 0,
831            pressure_evictions: 0,
832            frame_pressure_evictions: 0,
833            minified: 0,
834            reported_minify: HashSet::new(),
835            reaped: Vec::new(),
836            pressure_rects: Vec::new(),
837            pressure_order: Vec::new(),
838            pressure_free_area: Vec::new(),
839            pressure_layers: Vec::new(),
840            pressure_plan: Vec::new(),
841        }
842    }
843
844    /// A residency sized for `caps`' adapter.
845    #[must_use]
846    pub fn for_caps(caps: &TierCaps) -> Self {
847        Self::new(AtlasBudget::for_caps(caps))
848    }
849
850    /// The atlas geometry this residency allocates within.
851    #[must_use]
852    pub fn budget(&self) -> AtlasBudget {
853        self.budget
854    }
855
856    /// The single atlas allocator, for reading.
857    ///
858    /// Enough to resolve a handle either side allocated
859    /// ([`ImageCache::get`]) or to count the layers created so far, without
860    /// the mutable borrow allocation needs.
861    #[must_use]
862    pub fn allocator(&self) -> &ImageCache {
863        &self.cache
864    }
865
866    /// The single atlas allocator, for allocating through.
867    ///
868    /// This is the seam the glyph policy takes its slots from — see this
869    /// module's doc for why one cache rather than two is a correctness
870    /// requirement. `ImageId`s and rectangles handed out through here and
871    /// through [`resolve`](Self::resolve) come from the same packer and the
872    /// same id space, so a glyph and an image can never be given the same
873    /// handle or overlapping texels.
874    ///
875    /// **The contract a borrower keeps:** allocate what you like, and
876    /// deallocate *only handles you allocated yourself*. This residency's entry
877    /// map records the rectangles it allocated and nothing re-checks them;
878    /// freeing one of them from outside would leave an entry claiming a
879    /// rectangle the packer has since handed to someone else. Nothing here
880    /// enforces that, because the type the two sides must share is
881    /// `vello_common`'s own and cannot carry an ownership tag — the glyph
882    /// policy deallocates strictly inside `glifo`'s eviction, over the handles
883    /// in its own entry map.
884    #[must_use]
885    pub fn allocator_mut(&mut self) -> &mut ImageCache {
886        &mut self.cache
887    }
888
889    /// Whether this residency refuses every image.
890    #[must_use]
891    pub fn is_disabled(&self) -> bool {
892        self.disabled
893    }
894
895    /// How many images are currently resident.
896    #[must_use]
897    pub fn entry_count(&self) -> usize {
898        self.entries.len()
899    }
900
901    /// The frame clock entries age against.
902    #[must_use]
903    pub fn frame(&self) -> u64 {
904        self.frame
905    }
906
907    /// How many atlas layers have been created so far.
908    ///
909    /// The array texture must be at least this deep before the frame's uploads
910    /// are written.
911    ///
912    /// Read off the shared allocator rather than accumulated from this
913    /// residency's own allocations, so a layer the *glyph* policy created
914    /// counts too: the two classes pack into one array, and an array sized to
915    /// the image half alone would refuse to hold the glyph half's pages. The
916    /// count only ever grows — `MultiAtlasManager` appends layers and never
917    /// drops one — so a depth reported on an earlier frame stays valid.
918    #[must_use]
919    pub fn layers(&self) -> u32 {
920        u32::try_from(self.cache.atlas_count()).unwrap_or(u32::MAX)
921    }
922
923    /// How many resolutions have been refused over this residency's lifetime.
924    ///
925    /// Counts only images that could not be uploaded at all — a format or
926    /// extent the conversion refuses, a disabled atlas, or an atlas with no
927    /// room left *and nothing evictable to make room with*. A source merely
928    /// larger than one atlas layer is minified to fit, and one that displaced
929    /// another image is counted by
930    /// [`pressure_evictions`](Self::pressure_evictions) instead; neither
931    /// reaches this counter.
932    #[must_use]
933    pub fn skipped(&self) -> u64 {
934        self.skipped
935    }
936
937    /// How many resident images have been evicted to make room for another
938    /// over this residency's lifetime.
939    ///
940    /// The price of never refusing a draw the atlas could hold *something*
941    /// for: each one is an image that will re-upload the next frame it is
942    /// drawn on. A steady screen leaves this at zero; a scroll through more
943    /// distinct images than the atlas has rectangles for grows it once per
944    /// displaced image, which is the signal that the budget — not the
945    /// mechanism — is what a frame is paying for.
946    #[must_use]
947    pub fn pressure_evictions(&self) -> u64 {
948        self.pressure_evictions
949    }
950
951    /// How many resident images the *current* frame has evicted to make room.
952    ///
953    /// Zeroed by [`begin_frame`](Self::begin_frame), so a per-frame report reads
954    /// this rather than differencing
955    /// [`pressure_evictions`](Self::pressure_evictions) itself.
956    #[must_use]
957    pub fn frame_pressure_evictions(&self) -> u64 {
958        self.frame_pressure_evictions
959    }
960
961    /// How many sources have been downsampled to fit the atlas over this
962    /// residency's lifetime.
963    ///
964    /// Observational, and the counter that makes "an image too large for the
965    /// atlas is minified, not dropped" measurable rather than asserted.
966    #[must_use]
967    pub fn minified(&self) -> u64 {
968        self.minified
969    }
970
971    /// The regions whose texels must be cleared before the pending uploads.
972    ///
973    /// Pending rather than per-frame: an entry freed on a frame nobody
974    /// serviced is still waiting to be cleared on the next one. Both kinds of
975    /// eviction land here — the age-based reap at the head of a frame and a
976    /// rectangle taken back under pressure mid-frame — so the consumer's
977    /// clear-then-upload contract covers the second without knowing it exists.
978    #[must_use]
979    pub fn evictions(&self) -> &[AtlasRegion] {
980        &self.evictions
981    }
982
983    /// The regions whose texels must be written after the pending evictions.
984    ///
985    /// Pending rather than per-frame: an image made resident on a frame that was
986    /// refused is still waiting to be written on the next one.
987    #[must_use]
988    pub fn uploads(&self) -> &[ImageUpload] {
989        &self.uploads
990    }
991
992    /// A copy of the pending clear-then-write plan, evictions first.
993    ///
994    /// A *copy*, not a drain: the plan stays pending until
995    /// [`acknowledge_plan`](Self::acknowledge_plan) says it was serviced against
996    /// a live atlas, so a frame refused after compiling re-emits the same plan
997    /// on the next frame rather than losing it (see the module doc). The upload
998    /// pixels are behind an `Arc`, so a re-emitted plan copies handles rather
999    /// than texels, and the steady-state plan is empty on both counts.
1000    #[must_use]
1001    pub fn plan(&self) -> (Vec<AtlasRegion>, Vec<ImageUpload>) {
1002        (self.evictions.clone(), self.uploads.clone())
1003    }
1004
1005    /// Whether any part of the pending plan is still unserviced.
1006    #[must_use]
1007    pub fn has_pending_plan(&self) -> bool {
1008        !self.evictions.is_empty() || !self.uploads.is_empty()
1009    }
1010
1011    /// Record that the pending plan reached the atlas array, clearing it.
1012    ///
1013    /// Call this only once every region it names has actually been written or
1014    /// cleared. Calling it on a frame that was refused, or whose writes the
1015    /// array declined, is exactly the defect the pending plan exists to prevent:
1016    /// the entry map would go on claiming an image is resident whose texels are
1017    /// whatever the atlas texture happened to hold.
1018    pub fn acknowledge_plan(&mut self) {
1019        self.evictions.clear();
1020        self.eviction_set.clear();
1021        self.uploads.clear();
1022    }
1023
1024    /// Advance the frame clock and reclaim everything unseen for
1025    /// [`MAX_UNSEEN_FRAMES`].
1026    ///
1027    /// Call once at the head of a frame, before any [`resolve`](Self::resolve).
1028    /// An unacknowledged plan deliberately survives this: the frame that was to
1029    /// service it may have been refused, and dropping it here is what would turn
1030    /// a refused frame into a permanently unwritten atlas region.
1031    pub fn begin_frame(&mut self) {
1032        self.frame = self.frame.saturating_add(1);
1033        self.frame_pressure_evictions = 0;
1034        self.reap();
1035    }
1036
1037    /// The atlas rectangle for `data`'s pixels, allocating and scheduling an
1038    /// upload on the first frame that asks for it.
1039    ///
1040    /// # Errors
1041    ///
1042    /// Returns the [`ImageSkip`] naming why the image was refused. Every
1043    /// refusal happens before any `vello_common` call that could assert on the
1044    /// same condition, so an image the renderer cannot hold is a skipped draw
1045    /// rather than a panicked frame (E17).
1046    pub fn resolve(&mut self, data: &ImageData) -> Result<ResidentImage, ImageSkip> {
1047        let result = self.resolve_inner(data);
1048        if result.is_err() {
1049            self.skipped = self.skipped.saturating_add(1);
1050        }
1051        result
1052    }
1053
1054    fn resolve_inner(&mut self, data: &ImageData) -> Result<ResidentImage, ImageSkip> {
1055        if self.disabled {
1056            return Err(ImageSkip::AtlasDisabled);
1057        }
1058
1059        check_supported(data)?;
1060
1061        let key = data.data.id();
1062        if let Some(entry) = self.entries.get_mut(&key)
1063            && entry.natural == [data.width, data.height]
1064        {
1065            entry.last_seen = self.frame;
1066            return Ok(ResidentImage {
1067                id: entry.id,
1068                region: entry.region,
1069                natural: entry.natural,
1070                padding: u32::from(ATLAS_PADDING),
1071                may_have_transparency: entry.may_have_transparency,
1072            });
1073        }
1074
1075        // A blob id recorded at a different extent is a caller that rebuilt the
1076        // metadata around the same buffer. Its old rectangle describes the
1077        // wrong pixels, so it is released before the new one is taken rather
1078        // than left to age out holding both — unless *this* frame has already
1079        // resolved it at the other extent. The plan clears before it uploads,
1080        // so freeing that rectangle would schedule a clear over texels a draw
1081        // this same frame already recorded, which is exactly what the pressure
1082        // path refuses to do. The second extent is skipped instead, on the same
1083        // terms as a frame whose own working set outgrows the atlas — but this
1084        // is a same-frame ordering conflict on one blob key, not an atlas
1085        // capacity shortage, so it is reported as its own reason rather than
1086        // folded into [`ImageSkip::NoAtlasSpace`].
1087        if self
1088            .entries
1089            .get(&key)
1090            .is_some_and(|entry| entry.last_seen == self.frame)
1091        {
1092            let (width, height) = fit_extent([data.width, data.height], self.budget.atlas_size);
1093            return Err(ImageSkip::SameFrameExtentConflict { width, height });
1094        }
1095        self.release(key);
1096
1097        // Checked above, so this cannot assert; the conversion is also the one
1098        // place the straight-alpha premultiply happens, and it happens once per
1099        // image rather than once per frame.
1100        let ImageSource::Pixmap(pixels) = ImageSource::from_peniko_image_data(data) else {
1101            // `from_peniko_image_data` only ever yields a pixmap; an id-backed
1102            // source would mean a `vello_common` change, which is a skip rather
1103            // than an unreachable.
1104            return Err(ImageSkip::UnsupportedFormat(data.format));
1105        };
1106
1107        let natural = [data.width, data.height];
1108        // Minified before the allocation rather than after it: the packer is
1109        // asked for the rectangle that is actually going to be uploaded, so an
1110        // over-ceiling source never takes (and then has to give back) space at
1111        // its declared extent.
1112        let (width, height) = fit_extent(natural, self.budget.atlas_size);
1113        let pixels = if [width, height] == natural {
1114            pixels
1115        } else {
1116            self.note_minify(key, natural, [width, height]);
1117            Arc::new(minify(&pixels, width, height))
1118        };
1119
1120        let no_space = ImageSkip::NoAtlasSpace {
1121            width,
1122            height,
1123            atlas_width: self.budget.atlas_size.0,
1124            atlas_height: self.budget.atlas_size.1,
1125        };
1126
1127        let id = match self.cache.allocate(width, height, ATLAS_PADDING) {
1128            Ok(id) => id,
1129            // The atlas is full. Take a rectangle back from an image no longer
1130            // on screen rather than dropping this draw — see
1131            // [`allocate_under_pressure`](Self::allocate_under_pressure), and
1132            // the module doc for why a hole is worse than a re-upload.
1133            Err(_) => self
1134                .allocate_under_pressure(width, height)
1135                .ok_or(no_space)?,
1136        };
1137
1138        let Some(resource) = self.cache.get(id) else {
1139            // A successful allocation always populates its slot; refusing here
1140            // keeps the frame path free of the `expect` the reference renderer
1141            // takes at this same point.
1142            return Err(no_space);
1143        };
1144
1145        let region = AtlasRegion {
1146            layer: resource.atlas_id.as_u32(),
1147            offset: resource.offsets(),
1148            size: resource.size(),
1149        };
1150        let may_have_transparency = pixels.may_have_transparency();
1151
1152        self.entries.insert(
1153            key,
1154            Entry {
1155                id,
1156                region,
1157                natural,
1158                may_have_transparency,
1159                last_seen: self.frame,
1160            },
1161        );
1162        self.uploads.push(ImageUpload {
1163            id,
1164            region,
1165            natural,
1166            pixels,
1167            may_have_transparency,
1168        });
1169
1170        Ok(ResidentImage {
1171            id,
1172            region,
1173            natural,
1174            padding: u32::from(ATLAS_PADDING),
1175            may_have_transparency,
1176        })
1177    }
1178
1179    /// Allocate `width` x `height` by taking rectangles back from resident
1180    /// images, least recently seen first, once the packer has refused.
1181    ///
1182    /// Answers the handle that finally fit, or `None` when no bounded set of
1183    /// candidates would have made room — which is what
1184    /// [`ImageSkip::NoAtlasSpace`] then reports, with the residency exactly as
1185    /// it was found.
1186    ///
1187    /// **Nothing is released until a plan that fits has been found.** An
1188    /// eviction that does not produce the allocation buys nothing and costs a
1189    /// clear plus a re-upload per rectangle, every frame the condition recurs
1190    /// (see the module doc). So the search runs over a *model* of the layers
1191    /// and commits only when the model says the request would then be placed:
1192    ///
1193    /// 1. **Candidates.** Only an entry last seen on an earlier frame. The
1194    ///    frame path services [`evictions`](Self::evictions) *before*
1195    ///    [`uploads`](Self::uploads), so freeing a rectangle this frame has
1196    ///    already resolved would schedule a clear over texels the same frame is
1197    ///    about to sample — painting the very hole this eviction exists to
1198    ///    prevent. A frame whose own working set outgrows the atlas therefore
1199    ///    still skips, because everything in the atlas belongs to it.
1200    /// 2. **Order.** `(last_seen, slot, key)`, least recently seen first, built
1201    ///    and sorted *once* per refused allocation rather than once per
1202    ///    eviction. The slot index breaks a tie between two entries last drawn
1203    ///    on the same frame, so which image is displaced is a property of the
1204    ///    residency rather than of the entry map's iteration order. Because the
1205    ///    order is by age, an entry drawn on the *immediately preceding* frame
1206    ///    is reached only after every staler one has already been planned —
1207    ///    which is what keeps a large image redrawn every frame from thrashing
1208    ///    a working set that is merely idle. The candidates are then walked one
1209    ///    *layer* at a time, layers visited in that same staleness order (the
1210    ///    layer whose stalest candidate sorts earliest goes first).
1211    /// 3. **Fit.** After each candidate joins the plan, the layer it sat on is
1212    ///    re-tested: the request must fit inside the freed area that layer now
1213    ///    models, and must clear every rectangle still occupying it
1214    ///    ([`layer_admits`]). The first prefix that passes ends the search.
1215    /// 4. **Budget, per layer.** Each layer's own walk stops after
1216    ///    [`MAX_PRESSURE_EVICTION_CANDIDATES`] of *that layer's* candidates, or
1217    ///    once *that layer's* plan has freed [`PRESSURE_EVICTION_AREA_BUDGET`]
1218    ///    times the requested area — a fresh allowance per layer rather than one
1219    ///    pool the whole residency shares, so candidates on a layer too
1220    ///    fragmented to ever admit the request cannot spend the allowance a
1221    ///    later layer needed to prove it could. A layer that ends on its own
1222    ///    budget contributes nothing and the walk moves to the next layer.
1223    /// 5. **Commit.** Only the planned rectangles *on the layer the fit was
1224    ///    found in* are released — a candidate the walk passed over on another
1225    ///    layer contributed nothing and is left resident.
1226    ///
1227    /// The model is optimistic about occupants it cannot see: the glyph policy
1228    /// packs through the same allocator (see the module doc) and its pages are
1229    /// not in this entry map, so [`layer_admits`] tests a subset of the real
1230    /// obstacles. That direction is the safe one — a placement clearing every
1231    /// real occupant clears the subset too, so a request eviction could satisfy
1232    /// on a *given layer* is never refused for that reason. The converse is
1233    /// possible: the packer may refuse a plan the model approved, which costs
1234    /// the plan's evictions and no allocation. It is bounded by the same
1235    /// per-layer budget as the plan itself, and it is the one residual the
1236    /// planner cannot rule out without knowing where every glyph page sits.
1237    fn allocate_under_pressure(&mut self, width: u32, height: u32) -> Option<ImageId> {
1238        // The overwhelmingly common refusal — a frame that filled the atlas
1239        // with its own draws — costs one pass over the entry map and no model
1240        // at all.
1241        if !self
1242            .entries
1243            .values()
1244            .any(|entry| entry.last_seen < self.frame)
1245        {
1246            return None;
1247        }
1248
1249        // Taken from the residency's own scratch, so the frame least able to
1250        // afford an allocation does not make one to evict with.
1251        let mut rects = std::mem::take(&mut self.pressure_rects);
1252        let mut order = std::mem::take(&mut self.pressure_order);
1253        let mut free_area = std::mem::take(&mut self.pressure_free_area);
1254        let mut layers = std::mem::take(&mut self.pressure_layers);
1255        let mut plan = std::mem::take(&mut self.pressure_plan);
1256
1257        self.model_layers(&mut rects, &mut order, &mut free_area);
1258        let fit = plan_pressure_eviction(
1259            width,
1260            height,
1261            self.budget.atlas_size,
1262            &mut rects,
1263            &order,
1264            &mut free_area,
1265            &mut layers,
1266        );
1267
1268        plan.clear();
1269        if let Some(layer) = fit {
1270            plan.extend(
1271                rects
1272                    .iter()
1273                    .filter(|rect| rect.freed && rect.layer == layer)
1274                    .map(|rect| rect.key),
1275            );
1276        }
1277
1278        let allocated = if plan.is_empty() {
1279            None
1280        } else {
1281            for key in &plan {
1282                self.release(*key);
1283                self.pressure_evictions = self.pressure_evictions.saturating_add(1);
1284                self.frame_pressure_evictions = self.frame_pressure_evictions.saturating_add(1);
1285            }
1286            self.cache.allocate(width, height, ATLAS_PADDING).ok()
1287        };
1288
1289        rects.clear();
1290        order.clear();
1291        free_area.clear();
1292        layers.clear();
1293        plan.clear();
1294        self.pressure_rects = rects;
1295        self.pressure_order = order;
1296        self.pressure_free_area = free_area;
1297        self.pressure_layers = layers;
1298        self.pressure_plan = plan;
1299        allocated
1300    }
1301
1302    /// Fill the planner's scratch with this residency's own view of the atlas.
1303    ///
1304    /// `rects` takes one entry per resident image, grouped by layer so a fit
1305    /// test reads one layer's slice; `order` takes the candidate indices, least
1306    /// recently seen first across the *whole* residency — [`plan_pressure_eviction`]
1307    /// is what regroups it per layer for its own bounded walk; `free_area` takes
1308    /// each layer's free area *as the packer reports it*, which is the one
1309    /// figure that accounts for the glyph pages this map cannot see.
1310    fn model_layers(
1311        &self,
1312        rects: &mut Vec<PressureRect>,
1313        order: &mut Vec<usize>,
1314        free_area: &mut Vec<u64>,
1315    ) {
1316        rects.clear();
1317        rects.extend(self.entries.iter().map(|(key, entry)| {
1318            let region = padded(entry.region, ATLAS_PADDING);
1319            PressureRect {
1320                key: *key,
1321                layer: region.layer,
1322                x0: region.offset[0],
1323                y0: region.offset[1],
1324                x1: region.offset[0].saturating_add(region.size[0]),
1325                y1: region.offset[1].saturating_add(region.size[1]),
1326                area: u64::from(region.size[0]).saturating_mul(u64::from(region.size[1])),
1327                last_seen: entry.last_seen,
1328                slot: entry.id.as_u32(),
1329                freed: false,
1330            }
1331        }));
1332        rects.sort_unstable_by_key(|rect| rect.layer);
1333
1334        order.clear();
1335        order.extend(
1336            rects
1337                .iter()
1338                .enumerate()
1339                .filter_map(|(index, rect)| (rect.last_seen < self.frame).then_some(index)),
1340        );
1341        order.sort_unstable_by_key(|index| {
1342            let rect = &rects[*index];
1343            (rect.last_seen, rect.slot, rect.key)
1344        });
1345
1346        free_area.clear();
1347        free_area.resize(self.cache.atlas_count(), 0);
1348        for (id, stats) in self.cache.atlas_manager().atlas_stats() {
1349            if let Some(free) = free_area.get_mut(id.as_u32() as usize) {
1350                *free = u64::from(stats.total_area.saturating_sub(stats.allocated_area));
1351            }
1352        }
1353    }
1354
1355    /// Count and report one source downsampled to fit the atlas.
1356    ///
1357    /// Once per blob id, at warning level: a minified image is a silent quality
1358    /// decision the application may not have intended (a full-resolution photo
1359    /// where a thumbnail was wanted), so it says so — but a scene redrawing it
1360    /// every frame must not repeat the message.
1361    fn note_minify(&mut self, key: u64, natural: [u32; 2], fitted: [u32; 2]) {
1362        self.minified = self.minified.saturating_add(1);
1363        if !self.reported_minify.insert(key) {
1364            return;
1365        }
1366        log::warn!(
1367            "image {}x{} is larger than the {}x{} atlas budget; uploading a {}x{} box-filtered \
1368             copy instead (reported once per image)",
1369            natural[0],
1370            natural[1],
1371            self.budget.atlas_size.0,
1372            self.budget.atlas_size.1,
1373            fitted[0],
1374            fitted[1],
1375        );
1376    }
1377
1378    /// Deallocate every entry unseen for [`MAX_UNSEEN_FRAMES`], recording each
1379    /// one's rectangle for clearing.
1380    ///
1381    /// The age half of the two bounds; the population half is
1382    /// [`allocate_under_pressure`](Self::allocate_under_pressure), which runs
1383    /// only when an allocation has already failed. Keeping them separate is
1384    /// what makes an idle screen give its texels back promptly whether or not
1385    /// anything is asking for them.
1386    fn reap(&mut self) {
1387        let mut reaped = std::mem::take(&mut self.reaped);
1388        reaped.clear();
1389        reaped.extend(self.entries.iter().filter_map(|(key, entry)| {
1390            (self.frame.saturating_sub(entry.last_seen) > MAX_UNSEEN_FRAMES).then_some(*key)
1391        }));
1392
1393        for key in reaped.drain(..) {
1394            self.release(key);
1395        }
1396        self.reaped = reaped;
1397    }
1398
1399    /// Drop `key`'s entry, freeing its atlas rectangle and recording it for
1400    /// clearing.
1401    ///
1402    /// The padded rectangle is what is cleared: an allocation reserves its
1403    /// padding too, so leaving the border behind would leave stale texels a
1404    /// later allocation could sample through.
1405    ///
1406    /// An upload still pending for that rectangle goes with the entry. It
1407    /// describes pixels no live entry claims any more, and writing them after
1408    /// the clear would put an evicted image back into a rectangle the packer has
1409    /// already handed to something else.
1410    fn release(&mut self, key: u64) {
1411        let Some(entry) = self.entries.remove(&key) else {
1412            return;
1413        };
1414        // The handle came from this cache's own `allocate`, so the atlas it
1415        // names exists and the deallocation cannot fail.
1416        self.cache.deallocate(entry.id);
1417        self.uploads.retain(|upload| upload.id != entry.id);
1418
1419        // The plan survives an unacknowledged frame, so the same rectangle can
1420        // reach here twice before a single clear has been issued for it; one
1421        // clear is what the second one would write anyway.
1422        let region = padded(entry.region, ATLAS_PADDING);
1423        if self.eviction_set.insert(region) {
1424            self.evictions.push(region);
1425        }
1426    }
1427}
1428
1429/// The most rectangles one refused allocation may plan to take back from a
1430/// single atlas layer.
1431///
1432/// A bound on the *search*, not only on the damage: a layer whose walk
1433/// reaches it without finding a fit contributes nothing to the plan, so the
1434/// cost of one hopeless layer stays bounded rather than eating into the
1435/// allowance a later, satisfiable layer needs to prove itself. Sixty-four is
1436/// far past what any satisfiable request has needed — a fit is normally found
1437/// in one, since the rectangle handed back is usually the size of the one
1438/// being asked for — while still being a small fraction of what a
1439/// mobile-tier layer can hold. Applied once per layer a refused allocation
1440/// considers, never once across every layer together — see
1441/// [`plan_pressure_eviction`].
1442const MAX_PRESSURE_EVICTION_CANDIDATES: usize = 64;
1443
1444/// How much area one refused allocation may plan to free from a single atlas
1445/// layer, as a multiple of the area it asked for.
1446///
1447/// The other half of the bound, and the one that scales with the request: a
1448/// layer that has already given back four times the texels the request wants
1449/// and still has nowhere to put them is fighting fragmentation, not scarcity,
1450/// and freeing more of that same layer will not fix it. Applied per layer,
1451/// the same way [`MAX_PRESSURE_EVICTION_CANDIDATES`] is.
1452const PRESSURE_EVICTION_AREA_BUDGET: u64 = 4;
1453
1454/// One resident rectangle as the pressure planner sees it.
1455///
1456/// A flat copy rather than a borrow of the entry map, because the plan is drawn
1457/// up while the map is still intact and the release that follows needs it
1458/// mutably. `x1`/`y1` are exclusive, and every field is the *padded* allocation
1459/// — the rectangle the packer actually holds — rather than the image's own.
1460#[derive(Debug, Clone, Copy)]
1461struct PressureRect {
1462    /// Blob key of the entry holding this rectangle.
1463    key: u64,
1464    /// Array layer the rectangle sits in.
1465    layer: u32,
1466    /// Left edge, inclusive.
1467    x0: u32,
1468    /// Top edge, inclusive.
1469    y0: u32,
1470    /// Right edge, exclusive.
1471    x1: u32,
1472    /// Bottom edge, exclusive.
1473    y1: u32,
1474    /// Texels the rectangle covers.
1475    area: u64,
1476    /// The entry's frame stamp, which is what makes it a candidate or not.
1477    last_seen: u64,
1478    /// The entry's slot index, which breaks a tie between two entries last
1479    /// drawn on the same frame.
1480    slot: u32,
1481    /// Whether the plan under construction has given this rectangle back.
1482    freed: bool,
1483}
1484
1485/// The layer a bounded prefix of `order` would make room in, or `None`.
1486///
1487/// Frees nothing: it marks `rects` and grows `free_area` in the model only, so
1488/// a request no prefix satisfies leaves the residency untouched. `order` is
1489/// least-recently-seen-first across the *whole* residency; this regroups it
1490/// one layer at a time — `layers` scratch, cleared and refilled here, in the
1491/// order each layer's stalest candidate appears in `order` — and gives each
1492/// layer its own [`MAX_PRESSURE_EVICTION_CANDIDATES`]/
1493/// [`PRESSURE_EVICTION_AREA_BUDGET`] allowance rather than pooling one across
1494/// every layer. Without that split, a layer with many candidates that could
1495/// never admit the request (too fragmented, or blocked by an occupant no
1496/// eviction touches) spends the whole budget before a later, satisfiable
1497/// layer is ever tried — the request is then refused even though a bounded
1498/// eviction on that later layer would have placed it. See
1499/// [`ImageResidency::allocate_under_pressure`] for the whole shape and for why
1500/// the answer is a layer rather than a bool — only the planned rectangles on
1501/// the fitting layer are worth releasing.
1502fn plan_pressure_eviction(
1503    width: u32,
1504    height: u32,
1505    atlas_size: (u32, u32),
1506    rects: &mut [PressureRect],
1507    order: &[usize],
1508    free_area: &mut [u64],
1509    layers: &mut Vec<u32>,
1510) -> Option<u32> {
1511    let padding = u32::from(ATLAS_PADDING).saturating_mul(2);
1512    let request_w = width.saturating_add(padding);
1513    let request_h = height.saturating_add(padding);
1514    let request_area = u64::from(request_w).saturating_mul(u64::from(request_h));
1515    let area_budget = request_area.saturating_mul(PRESSURE_EVICTION_AREA_BUDGET);
1516
1517    layers.clear();
1518    for &index in order {
1519        let Some(rect) = rects.get(index) else {
1520            continue;
1521        };
1522        if !layers.contains(&rect.layer) {
1523            layers.push(rect.layer);
1524        }
1525    }
1526
1527    for &layer in layers.iter() {
1528        let mut layer_freed_area = 0_u64;
1529        let mut step = 0_usize;
1530
1531        for &index in order {
1532            let on_this_layer = rects.get(index).is_some_and(|rect| rect.layer == layer);
1533            if !on_this_layer {
1534                continue;
1535            }
1536            if step >= MAX_PRESSURE_EVICTION_CANDIDATES || layer_freed_area > area_budget {
1537                break;
1538            }
1539            step += 1;
1540
1541            let area = {
1542                let Some(rect) = rects.get_mut(index) else {
1543                    continue;
1544                };
1545                rect.freed = true;
1546                rect.area
1547            };
1548            layer_freed_area = layer_freed_area.saturating_add(area);
1549
1550            let Some(free) = free_area.get_mut(layer as usize) else {
1551                continue;
1552            };
1553            *free = free.saturating_add(area);
1554            // Area first because it is exact — it comes off the packer's own
1555            // accounting, so it counts the glyph pages `layer_admits` cannot see —
1556            // and because it is the cheaper of the two questions.
1557            if *free >= request_area && layer_admits(rects, layer, request_w, request_h, atlas_size)
1558            {
1559                return Some(layer);
1560            }
1561        }
1562    }
1563
1564    None
1565}
1566
1567/// Whether a `width` x `height` rectangle could still be placed in `layer` once
1568/// every rectangle the plan has marked freed is out of the way.
1569///
1570/// A *necessary* condition rather than an exact packing answer. Any placement
1571/// has to avoid each occupant whole, so it must lie entirely to one side of it:
1572/// if some occupant leaves less than the requested extent on all four sides,
1573/// there is nowhere for the rectangle to go and the plan cannot succeed. A plan
1574/// this refuses is therefore certainly hopeless; one it admits may still be
1575/// refused by the packer, which is the direction that costs a bounded number of
1576/// evictions rather than a wrong picture (see
1577/// [`ImageResidency::allocate_under_pressure`]).
1578///
1579/// `rects` is grouped by layer, so this reads one layer's slice rather than the
1580/// whole residency.
1581fn layer_admits(
1582    rects: &[PressureRect],
1583    layer: u32,
1584    width: u32,
1585    height: u32,
1586    atlas_size: (u32, u32),
1587) -> bool {
1588    if width > atlas_size.0 || height > atlas_size.1 {
1589        return false;
1590    }
1591
1592    let start = rects.partition_point(|rect| rect.layer < layer);
1593    let end = rects.partition_point(|rect| rect.layer <= layer);
1594    rects[start..end]
1595        .iter()
1596        .filter(|rect| !rect.freed)
1597        .all(|rect| {
1598            width <= rect.x0
1599                || width <= atlas_size.0.saturating_sub(rect.x1)
1600                || height <= rect.y0
1601                || height <= atlas_size.1.saturating_sub(rect.y1)
1602        })
1603}
1604
1605/// The extent `natural` is stored at inside an `atlas`-sized layer: itself when
1606/// it already fits, and otherwise the largest rectangle of the same aspect ratio
1607/// that does.
1608///
1609/// The scale is taken as the smaller of the two axis ratios and applied to both,
1610/// so the two axes of the paint transform stay in step — a per-axis fit would
1611/// stretch the image. Each axis is floored (never rounding *up* past the layer)
1612/// and then held at one texel: a source that scales below a whole texel still
1613/// has to have somewhere to be, and a zero-extent allocation is not a rectangle
1614/// the packer or the shader can address.
1615#[must_use]
1616pub fn fit_extent(natural: [u32; 2], atlas: (u32, u32)) -> (u32, u32) {
1617    let (width, height) = (natural[0], natural[1]);
1618    if width <= atlas.0 && height <= atlas.1 {
1619        return (width, height);
1620    }
1621
1622    let scale = (f64::from(atlas.0) / f64::from(width.max(1)))
1623        .min(f64::from(atlas.1) / f64::from(height.max(1)));
1624    let axis = |value: u32, ceiling: u32| {
1625        let scaled = (f64::from(value) * scale).floor();
1626        // `as` on a non-finite or out-of-range float saturates in Rust, so the
1627        // clamps below are the whole check rather than a second line of defence.
1628        (scaled as u32).clamp(1, ceiling.max(1))
1629    };
1630
1631    (axis(width, atlas.0), axis(height, atlas.1))
1632}
1633
1634/// `source` box-filtered down to `width` x `height`.
1635///
1636/// Every output texel is the unweighted mean of the half-open source rectangle
1637/// it covers, computed on the premultiplied bytes the atlas stores — which is
1638/// the space the samples are composited in, so averaging there is what keeps a
1639/// partially transparent source from bleeding its colour outward.
1640///
1641/// Minification only: a request at or above the source's own extent copies the
1642/// texels straight across rather than inventing any, since a box filter has no
1643/// magnification arm and the caller never asks for one ([`fit_extent`] only ever
1644/// shrinks).
1645#[must_use]
1646pub fn minify(source: &Pixmap, width: u32, height: u32) -> Pixmap {
1647    let source_w = u32::from(source.width());
1648    let source_h = u32::from(source.height());
1649    let width = width.clamp(1, source_w.max(1));
1650    let height = height.clamp(1, source_h.max(1));
1651
1652    let texels = source.data();
1653    let mut out = Vec::with_capacity((width as usize).saturating_mul(height as usize));
1654    let mut may_have_transparency = false;
1655
1656    for y in 0..height {
1657        // Half-open source rows, derived from the output row rather than
1658        // accumulated, so rounding never leaves a gap or an overlap between
1659        // consecutive rows.
1660        let y0 = (u64::from(y) * u64::from(source_h) / u64::from(height)) as u32;
1661        let y1 = ((u64::from(y) + 1) * u64::from(source_h) / u64::from(height)) as u32;
1662        let y1 = y1.max(y0.saturating_add(1)).min(source_h);
1663
1664        for x in 0..width {
1665            let x0 = (u64::from(x) * u64::from(source_w) / u64::from(width)) as u32;
1666            let x1 = ((u64::from(x) + 1) * u64::from(source_w) / u64::from(width)) as u32;
1667            let x1 = x1.max(x0.saturating_add(1)).min(source_w);
1668
1669            let mut sum = [0_u64; 4];
1670            let mut count = 0_u64;
1671            for row in y0..y1 {
1672                let base = (row as usize).saturating_mul(source_w as usize);
1673                for column in x0..x1 {
1674                    let Some(texel) = texels.get(base.saturating_add(column as usize)) else {
1675                        continue;
1676                    };
1677                    sum[0] += u64::from(texel.r);
1678                    sum[1] += u64::from(texel.g);
1679                    sum[2] += u64::from(texel.b);
1680                    sum[3] += u64::from(texel.a);
1681                    count += 1;
1682                }
1683            }
1684
1685            // A count of zero is unreachable — both ranges are non-empty by
1686            // construction — and flooring it at one rather than branching keeps
1687            // the divide-by-zero panic off the frame path (E17) while answering
1688            // a transparent texel if it ever were reachable, since the sums
1689            // would then be zero too.
1690            let divisor = count.max(1);
1691            // Round to nearest rather than truncating: a uniform source has to
1692            // come back bit-identical, which truncating an exact integer mean
1693            // already gives, but a near-uniform one must not drift a level
1694            // darker.
1695            let mean = |channel: usize| {
1696                ((sum[channel] + divisor / 2) / divisor).min(u64::from(u8::MAX)) as u8
1697            };
1698
1699            let texel = PremulRgba8 {
1700                r: mean(0),
1701                g: mean(1),
1702                b: mean(2),
1703                a: mean(3),
1704            };
1705            may_have_transparency |= texel.a != u8::MAX;
1706            out.push(texel);
1707        }
1708    }
1709
1710    // Both extents were clamped into `1..=u16::MAX` above (a source pixmap's own
1711    // extents are `u16`), so neither conversion can lose information.
1712    Pixmap::from_parts_with_opacity(
1713        out,
1714        width.min(u32::from(u16::MAX)) as u16,
1715        height.min(u32::from(u16::MAX)) as u16,
1716        may_have_transparency,
1717    )
1718}
1719
1720/// `axis` held at or below `ceiling`, and never below the one texel an
1721/// addressable rectangle needs.
1722fn fit_axis(axis: u32, ceiling: u64) -> u32 {
1723    axis.min(u32::try_from(ceiling).unwrap_or(u32::MAX)).max(1)
1724}
1725
1726/// `region` grown by `padding` on every side, clamped at the layer origin.
1727fn padded(region: AtlasRegion, padding: u16) -> AtlasRegion {
1728    let padding = u32::from(padding);
1729    let pad_x = padding.min(region.offset[0]);
1730    let pad_y = padding.min(region.offset[1]);
1731
1732    AtlasRegion {
1733        layer: region.layer,
1734        offset: [region.offset[0] - pad_x, region.offset[1] - pad_y],
1735        size: [
1736            region.size[0].saturating_add(pad_x + padding),
1737            region.size[1].saturating_add(pad_y + padding),
1738        ],
1739    }
1740}
1741
1742/// Refuse an image `vello_common`'s own conversion would assert on.
1743///
1744/// The three conditions are exactly the three assertions downstream: the
1745/// format `unimplemented!()`, the `u16::MAX` dimension check, and
1746/// `Pixmap::from_parts_with_opacity`'s length equality. Checking them here is
1747/// what keeps the panic off the frame path.
1748fn check_supported(data: &ImageData) -> Result<(), ImageSkip> {
1749    match data.format {
1750        ImageFormat::Rgba8 | ImageFormat::Bgra8 => {}
1751        format => return Err(ImageSkip::UnsupportedFormat(format)),
1752    }
1753
1754    if data.width == 0 || data.height == 0 {
1755        return Err(ImageSkip::DegenerateExtent {
1756            width: data.width,
1757            height: data.height,
1758        });
1759    }
1760
1761    if data.width > MAX_IMAGE_DIMENSION || data.height > MAX_IMAGE_DIMENSION {
1762        return Err(ImageSkip::TooLarge {
1763            width: data.width,
1764            height: data.height,
1765            max: MAX_IMAGE_DIMENSION,
1766        });
1767    }
1768
1769    let actual = data.data.data().len();
1770    let expected = data
1771        .format
1772        .size_in_bytes(data.width, data.height)
1773        .unwrap_or(usize::MAX);
1774    if actual != expected {
1775        return Err(ImageSkip::MalformedPixels {
1776            width: data.width,
1777            height: data.height,
1778            expected,
1779            actual,
1780        });
1781    }
1782
1783    Ok(())
1784}
1785
1786#[cfg(test)]
1787mod tests {
1788    use super::*;
1789    use peniko::{Blob, ImageAlphaType};
1790
1791    fn image(width: u32, height: u32) -> ImageData {
1792        let len = (width as usize) * (height as usize) * 4;
1793        ImageData {
1794            data: Blob::new(Arc::new(vec![255_u8; len])),
1795            format: ImageFormat::Rgba8,
1796            alpha_type: ImageAlphaType::Alpha,
1797            width,
1798            height,
1799        }
1800    }
1801
1802    fn residency() -> ImageResidency {
1803        ImageResidency::new(AtlasBudget {
1804            atlas_size: (64, 64),
1805            max_atlases: 2,
1806        })
1807    }
1808
1809    #[test]
1810    fn a_mobile_budget_is_never_the_vello_default() {
1811        let default = AtlasConfig::default();
1812        for budget in [AtlasBudget::MOBILE, AtlasBudget::DESKTOP] {
1813            assert_ne!(budget.atlas_size, default.atlas_size);
1814            assert!(budget.total_bytes() < 4096 * 4096 * ATLAS_FORMAT_BYTES * 8);
1815        }
1816        assert_eq!(AtlasBudget::MOBILE.total_bytes(), 16 << 20);
1817        assert_eq!(AtlasBudget::DESKTOP.total_bytes(), 128 << 20);
1818    }
1819
1820    #[test]
1821    fn a_downlevel_adapter_takes_the_mobile_budget() {
1822        let webgl2 = TierCaps::fake(DownlevelProfile::WebGl2);
1823        assert!(is_mobile_tier(&webgl2));
1824
1825        let mut tiler = TierCaps::fake(DownlevelProfile::Full);
1826        assert!(!is_mobile_tier(&tiler));
1827        tiler.transient_saves_memory = true;
1828        assert!(is_mobile_tier(&tiler));
1829    }
1830
1831    #[test]
1832    fn a_budget_is_clamped_to_the_adapters_own_ceilings() {
1833        let mut caps = TierCaps::fake(DownlevelProfile::Full);
1834        caps.max_texture_dimension_2d = 1024;
1835        caps.max_texture_array_layers = 2;
1836
1837        let budget = AtlasBudget::DESKTOP.clamped(&caps);
1838        assert_eq!(budget.atlas_size, (1024, 1024));
1839        assert_eq!(budget.max_atlases, 2);
1840
1841        // The eight-bit `atlas_index` field is a ceiling of its own.
1842        caps.max_texture_array_layers = 4096;
1843        let budget = AtlasBudget {
1844            atlas_size: (256, 256),
1845            max_atlases: 4096,
1846        }
1847        .clamped(&caps);
1848        assert_eq!(budget.max_atlases, MAX_ATLAS_LAYERS);
1849    }
1850
1851    #[test]
1852    fn the_first_layer_is_created_lazily() {
1853        let residency = residency();
1854        assert_eq!(residency.layers(), 0);
1855        assert_eq!(residency.entry_count(), 0);
1856    }
1857
1858    #[test]
1859    fn one_image_over_many_frames_uploads_exactly_once() {
1860        let mut residency = residency();
1861        let data = image(8, 8);
1862        let mut uploads = 0;
1863
1864        for _ in 0..60 {
1865            residency.begin_frame();
1866            residency.resolve(&data).expect("8x8 fits a 64x64 atlas");
1867            uploads += residency.uploads().len();
1868            // Every frame here is a frame that reached the atlas.
1869            residency.acknowledge_plan();
1870        }
1871
1872        assert_eq!(uploads, 1, "residency survives every frame that draws it");
1873        assert_eq!(residency.entry_count(), 1);
1874        assert_eq!(residency.layers(), 1);
1875    }
1876
1877    #[test]
1878    fn an_unseen_image_is_reaped_and_its_padded_region_cleared() {
1879        let mut residency = residency();
1880        let data = image(8, 8);
1881
1882        residency.begin_frame();
1883        let resident = residency.resolve(&data).expect("fits");
1884        residency.acknowledge_plan();
1885
1886        for _ in 0..MAX_UNSEEN_FRAMES {
1887            residency.begin_frame();
1888            assert!(residency.evictions().is_empty(), "still inside the window");
1889        }
1890
1891        residency.begin_frame();
1892        assert_eq!(residency.entry_count(), 0);
1893        assert_eq!(
1894            residency.evictions(),
1895            &[padded(resident.region, ATLAS_PADDING)]
1896        );
1897
1898        // The freed rectangle is available again, and the reallocation
1899        // schedules a fresh upload.
1900        residency.resolve(&data).expect("fits");
1901        assert_eq!(residency.uploads().len(), 1);
1902    }
1903
1904    #[test]
1905    fn an_image_larger_than_the_atlas_is_minified_to_fit_rather_than_skipped() {
1906        let mut residency = residency();
1907        residency.begin_frame();
1908
1909        // Twice the layer's width at a 16:1 aspect ratio: the width sets the
1910        // scale, and the height follows it rather than being fitted on its own.
1911        let resident = residency.resolve(&image(128, 8)).expect("minified to fit");
1912
1913        assert_eq!(resident.region.size, [64, 4]);
1914        assert_eq!(resident.natural, [128, 8]);
1915        assert_eq!(resident.minify_scale(), Some((0.5, 0.5)));
1916        assert_eq!(residency.minified(), 1);
1917        assert_eq!(residency.skipped(), 0, "a big image is not a skipped one");
1918        assert_eq!(residency.uploads().len(), 1);
1919        let upload = &residency.uploads()[0];
1920        assert_eq!(
1921            (upload.pixels.width(), upload.pixels.height()),
1922            (64, 4),
1923            "the upload carries exactly the region's texels"
1924        );
1925    }
1926
1927    #[test]
1928    fn an_atlas_with_no_room_left_is_still_a_skip() {
1929        // Minification fits an image to a LAYER, not to the space left in one:
1930        // a budget already full refuses the next allocation as it always did.
1931        let mut residency = ImageResidency::new(AtlasBudget {
1932            atlas_size: (64, 64),
1933            max_atlases: 1,
1934        });
1935        residency.begin_frame();
1936        residency
1937            .resolve(&image(64, 64))
1938            .expect("fills the one layer");
1939
1940        let skip = residency
1941            .resolve(&image(32, 32))
1942            .expect_err("nothing is left to allocate from");
1943        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
1944        assert_eq!(residency.skipped(), 1);
1945        assert_eq!(residency.minified(), 0);
1946    }
1947
1948    #[test]
1949    fn a_minified_image_stays_resident_across_frames_at_its_declared_extent() {
1950        // Residency is keyed on the SOURCE extent, not the resident one: an
1951        // entry matched against its own minified rectangle would miss every
1952        // frame and re-upload the image on each of them.
1953        let mut residency = residency();
1954        let data = image(128, 8);
1955
1956        for _ in 0..8 {
1957            residency.begin_frame();
1958            let resident = residency.resolve(&data).expect("minified to fit");
1959            assert_eq!(resident.natural, [128, 8]);
1960            residency.acknowledge_plan();
1961        }
1962
1963        assert_eq!(residency.entry_count(), 1);
1964        assert_eq!(
1965            residency.minified(),
1966            1,
1967            "downsampled once, not once a frame"
1968        );
1969        assert!(
1970            residency.uploads().is_empty(),
1971            "no re-upload after the first"
1972        );
1973    }
1974
1975    #[test]
1976    fn a_fit_extent_preserves_the_aspect_ratio_and_never_grows() {
1977        let atlas = (64, 64);
1978        // Already inside the layer: unchanged, both axes.
1979        assert_eq!(fit_extent([64, 64], atlas), (64, 64));
1980        assert_eq!(fit_extent([8, 4], atlas), (8, 4));
1981
1982        // The `adv-huge-image` shape, at this budget's scale.
1983        assert_eq!(fit_extent([5000, 5000], (2048, 2048)), (2048, 2048));
1984
1985        // The tighter axis sets the scale for both.
1986        assert_eq!(fit_extent([128, 8], atlas), (64, 4));
1987        assert_eq!(fit_extent([8, 128], atlas), (4, 64));
1988        assert_eq!(fit_extent([1000, 10], atlas), (64, 1));
1989
1990        // A non-square layer is fitted per axis and still uniformly scaled.
1991        assert_eq!(fit_extent([400, 400], (100, 50)), (50, 50));
1992    }
1993
1994    #[test]
1995    fn an_extreme_aspect_ratio_still_fits_at_one_texel_rather_than_none() {
1996        // 10000:1 into a 64-square layer scales the short axis to 0.0064 texels.
1997        // A zero extent is not a rectangle the packer or the shader can
1998        // address, so it is held at one.
1999        let fitted = fit_extent([10_000, 1], (64, 64));
2000        assert_eq!(fitted, (64, 1));
2001
2002        let mut residency = residency();
2003        residency.begin_frame();
2004        let resident = residency
2005            .resolve(&image(10_000, 1))
2006            .expect("a sliver still becomes a rectangle");
2007        assert_eq!(resident.region.size, [64, 1]);
2008    }
2009
2010    #[test]
2011    fn a_uniform_source_minifies_to_exactly_its_own_colour() {
2012        // What `adv-huge-image` rests on: a uniform source comes back bit
2013        // identical whatever the scale, so the case's `Exact` probe is a
2014        // property of the filter rather than a tolerance.
2015        let texel = PremulRgba8 {
2016            r: 255,
2017            g: 0,
2018            b: 255,
2019            a: 255,
2020        };
2021        let source = Pixmap::from_parts_with_opacity(vec![texel; 100 * 100], 100, 100, false);
2022
2023        let small = minify(&source, 7, 3);
2024        assert_eq!((small.width(), small.height()), (7, 3));
2025        assert!(small.data().iter().all(|out| *out == texel));
2026        assert!(
2027            !small.may_have_transparency(),
2028            "an opaque source stays opaque"
2029        );
2030    }
2031
2032    #[test]
2033    fn a_box_filter_averages_the_source_texels_each_output_covers() {
2034        // A 2x1 source of black and white halves down to one texel, which must
2035        // be their mean rather than either end.
2036        let black = PremulRgba8 {
2037            r: 0,
2038            g: 0,
2039            b: 0,
2040            a: 255,
2041        };
2042        let white = PremulRgba8 {
2043            r: 255,
2044            g: 255,
2045            b: 255,
2046            a: 255,
2047        };
2048        let source = Pixmap::from_parts_with_opacity(vec![black, white], 2, 1, false);
2049
2050        let small = minify(&source, 1, 1);
2051        assert_eq!((small.width(), small.height()), (1, 1));
2052        // 255 / 2 rounded to nearest.
2053        assert_eq!(small.data()[0].r, 128);
2054        assert_eq!(small.data()[0].a, 255);
2055
2056        // Transparency is recomputed from the result, not inherited: averaging
2057        // an opaque and a fully transparent texel produces a partial one.
2058        let clear = PremulRgba8 {
2059            r: 0,
2060            g: 0,
2061            b: 0,
2062            a: 0,
2063        };
2064        let mixed = Pixmap::from_parts_with_opacity(vec![white, clear], 2, 1, true);
2065        let small = minify(&mixed, 1, 1);
2066        assert_eq!(small.data()[0].a, 128);
2067        assert!(small.may_have_transparency());
2068    }
2069
2070    #[test]
2071    fn minify_never_magnifies() {
2072        let texel = PremulRgba8 {
2073            r: 1,
2074            g: 2,
2075            b: 3,
2076            a: 255,
2077        };
2078        let source = Pixmap::from_parts_with_opacity(vec![texel; 4], 2, 2, false);
2079
2080        // A request above the source's own extent is clamped to it rather than
2081        // inventing texels the box filter has no arm for.
2082        let same = minify(&source, 8, 8);
2083        assert_eq!((same.width(), same.height()), (2, 2));
2084        assert!(same.data().iter().all(|out| *out == texel));
2085    }
2086
2087    #[test]
2088    fn a_malformed_or_degenerate_image_is_refused_before_the_conversion() {
2089        let mut short = image(4, 4);
2090        short.data = Blob::new(Arc::new(vec![0_u8; 8]));
2091        assert!(matches!(
2092            check_supported(&short),
2093            Err(ImageSkip::MalformedPixels { .. })
2094        ));
2095
2096        assert!(matches!(
2097            check_supported(&image(0, 4)),
2098            Err(ImageSkip::DegenerateExtent { .. })
2099        ));
2100
2101        let mut huge = image(1, 1);
2102        huge.width = MAX_IMAGE_DIMENSION + 1;
2103        assert!(matches!(
2104            check_supported(&huge),
2105            Err(ImageSkip::TooLarge { .. })
2106        ));
2107    }
2108
2109    #[test]
2110    fn two_images_share_a_layer_and_hold_distinct_rectangles() {
2111        let mut residency = residency();
2112        residency.begin_frame();
2113
2114        let first = residency.resolve(&image(8, 8)).expect("fits");
2115        let second = residency.resolve(&image(16, 16)).expect("fits");
2116
2117        assert_ne!(first.id, second.id);
2118        assert_eq!(first.region.layer, second.region.layer);
2119        assert_ne!(first.region.offset, second.region.offset);
2120        assert_eq!(residency.uploads().len(), 2);
2121        assert_eq!(residency.layers(), 1);
2122    }
2123
2124    #[test]
2125    fn a_second_layer_is_created_when_the_first_is_full() {
2126        let mut residency = residency();
2127        residency.begin_frame();
2128
2129        // Two 64x48 images cannot share one 64x64 layer.
2130        let first = residency.resolve(&image(64, 48)).expect("fits a layer");
2131        let second = residency.resolve(&image(64, 48)).expect("fits the next");
2132
2133        assert_eq!(first.region.layer, 0);
2134        assert_eq!(second.region.layer, 1);
2135        assert_eq!(residency.layers(), 2);
2136    }
2137
2138    #[test]
2139    fn a_slot_taken_through_the_shared_allocator_is_a_distinct_id_on_the_same_budget() {
2140        // The allocator half of the glyph/image seam, stated without the glyph
2141        // policy: whatever else packs into this cache draws from the same id
2142        // space and the same layer allowance, and its pages are visible in the
2143        // depth the array is built to.
2144        let mut residency = residency();
2145        residency.begin_frame();
2146        let resident = residency.resolve(&image(64, 48)).expect("fills a layer");
2147
2148        // Big enough that it cannot share the layer the image took, so it must
2149        // create the second one rather than report the first.
2150        let borrowed = residency
2151            .allocator_mut()
2152            .allocate(64, 48, ATLAS_PADDING)
2153            .expect("the second layer is free");
2154
2155        assert_ne!(
2156            resident.id, borrowed,
2157            "one cache never hands the same slot index to two live occupants"
2158        );
2159        assert_eq!(
2160            residency.layers(),
2161            2,
2162            "a layer created by the other class still has to be in the array"
2163        );
2164        assert_eq!(
2165            residency
2166                .allocator()
2167                .get(resident.id)
2168                .map(|resource| resource.size()),
2169            Some(resident.region.size),
2170            "and neither allocation disturbed the other's rectangle"
2171        );
2172    }
2173
2174    #[test]
2175    fn a_disabled_residency_refuses_every_image_and_touches_no_atlas() {
2176        let mut residency = ImageResidency::disabled(AtlasBudget::MOBILE);
2177        assert!(residency.is_disabled());
2178
2179        residency.begin_frame();
2180        let skip = residency
2181            .resolve(&image(8, 8))
2182            .expect_err("the kill switch refuses every image");
2183
2184        assert_eq!(skip, ImageSkip::AtlasDisabled);
2185        assert_eq!(residency.entry_count(), 0);
2186        assert_eq!(residency.layers(), 0, "no atlas layer is ever created");
2187        assert!(residency.uploads().is_empty());
2188        assert_eq!(residency.skipped(), 1);
2189    }
2190
2191    #[test]
2192    fn an_enabled_residency_is_what_an_unset_kill_switch_produces() {
2193        // The default test environment leaves `FRUST_ENGINE_NO_ATLAS` unset, so
2194        // this pins that `new` consults the switch rather than hardcoding
2195        // either answer; the disabled half is pinned above through the
2196        // constructor the switch selects.
2197        assert!(!ImageResidency::new(AtlasBudget::MOBILE).is_disabled());
2198    }
2199
2200    #[test]
2201    fn reading_the_plan_leaves_it_pending_and_acknowledging_it_clears_it() {
2202        let mut residency = residency();
2203        residency.begin_frame();
2204        residency.resolve(&image(8, 8)).expect("fits");
2205
2206        let (evictions, uploads) = residency.plan();
2207        assert!(evictions.is_empty());
2208        assert_eq!(uploads.len(), 1);
2209        assert!(
2210            residency.has_pending_plan(),
2211            "reading the plan is not servicing it"
2212        );
2213
2214        residency.acknowledge_plan();
2215        assert!(!residency.has_pending_plan());
2216        assert!(residency.uploads().is_empty());
2217        assert!(residency.evictions().is_empty());
2218    }
2219
2220    #[test]
2221    fn an_unacknowledged_upload_is_re_emitted_until_it_is_serviced() {
2222        // The invariant the whole pending plan exists for: a frame refused
2223        // after compiling never wrote these texels, so the entry map claiming
2224        // the image is resident has to stay answerable by a later frame's plan.
2225        let mut residency = residency();
2226        let data = image(8, 8);
2227
2228        residency.begin_frame();
2229        let first = residency.resolve(&data).expect("fits");
2230        let region = residency.uploads()[0].region;
2231
2232        for _ in 0..4 {
2233            residency.begin_frame();
2234            let again = residency.resolve(&data).expect("still resident");
2235            assert_eq!(again.region, first.region, "residency does not move");
2236            assert_eq!(
2237                residency.uploads().len(),
2238                1,
2239                "the same upload is re-offered, never duplicated"
2240            );
2241            assert_eq!(residency.uploads()[0].region, region);
2242        }
2243
2244        residency.acknowledge_plan();
2245        residency.begin_frame();
2246        residency.resolve(&data).expect("still resident");
2247        assert!(
2248            residency.uploads().is_empty(),
2249            "a serviced upload is never offered again"
2250        );
2251    }
2252
2253    #[test]
2254    fn reaping_an_unacknowledged_entry_withdraws_its_upload_with_it() {
2255        // Otherwise the plan would clear the rectangle and then write the
2256        // evicted image straight back into it, over whatever the packer handed
2257        // that space to next.
2258        let mut residency = residency();
2259        let data = image(8, 8);
2260
2261        residency.begin_frame();
2262        let resident = residency.resolve(&data).expect("fits");
2263        assert_eq!(residency.uploads().len(), 1);
2264
2265        for _ in 0..=MAX_UNSEEN_FRAMES {
2266            residency.begin_frame();
2267        }
2268
2269        assert_eq!(residency.entry_count(), 0);
2270        assert!(
2271            residency.uploads().is_empty(),
2272            "the reaped entry's unserviced upload goes with it"
2273        );
2274        assert_eq!(
2275            residency.evictions(),
2276            &[padded(resident.region, ATLAS_PADDING)]
2277        );
2278    }
2279
2280    #[test]
2281    fn every_pending_upload_lies_inside_the_atlas_its_layer_count_asks_for() {
2282        // What keeps a consumer from recording where an image lives and then
2283        // sampling a rectangle the array refused to write: the plan can only
2284        // ever name regions the budget and the reported depth already admit.
2285        let mut residency = residency();
2286        residency.begin_frame();
2287        residency.resolve(&image(64, 48)).expect("fills a layer");
2288        residency.resolve(&image(64, 48)).expect("takes the next");
2289
2290        let budget = residency.budget();
2291        let layers = residency.layers();
2292        assert_eq!(layers, 2);
2293        for upload in residency.uploads() {
2294            assert!(
2295                budget.contains(upload.region, layers),
2296                "{:?} is outside a {layers}-layer {budget:?}",
2297                upload.region
2298            );
2299        }
2300    }
2301
2302    /// A budget holding exactly two 64-square images: one per layer, with no
2303    /// room to pack a third anywhere, so "the atlas is full" is reachable in
2304    /// two allocations and needs no fragmentation reasoning.
2305    fn two_slot_residency() -> ImageResidency {
2306        ImageResidency::new(AtlasBudget {
2307            atlas_size: (64, 64),
2308            max_atlases: 2,
2309        })
2310    }
2311
2312    #[test]
2313    fn a_working_set_larger_than_the_atlas_re_uploads_rather_than_skipping() {
2314        // The scrolling-list shape: more distinct images pass the viewport
2315        // inside one age window than the atlas has rectangles for. Every draw
2316        // must still resolve — the price is a re-upload, never a hole.
2317        let mut residency = two_slot_residency();
2318        let images: Vec<ImageData> = (0..12).map(|_| image(64, 64)).collect();
2319
2320        for data in &images {
2321            residency.begin_frame();
2322            residency
2323                .resolve(data)
2324                .expect("a full atlas gives a rectangle back rather than refusing");
2325            residency.acknowledge_plan();
2326        }
2327
2328        assert_eq!(residency.skipped(), 0, "no draw was ever dropped");
2329        assert_eq!(
2330            residency.pressure_evictions(),
2331            10,
2332            "the ten images past the two slots each displaced one"
2333        );
2334        assert_eq!(residency.entry_count(), 2, "and residency stays bounded");
2335    }
2336
2337    #[test]
2338    fn an_image_resolved_this_frame_is_never_the_one_evicted() {
2339        // The ordering contract's own boundary: the plan clears before it
2340        // uploads, so a rectangle this frame already resolved cannot be handed
2341        // back inside it. A frame whose own working set outgrows the atlas is
2342        // still a skip, and nothing is scheduled for clearing.
2343        let mut residency = two_slot_residency();
2344        residency.begin_frame();
2345        residency.resolve(&image(64, 64)).expect("takes layer 0");
2346        residency.resolve(&image(64, 64)).expect("takes layer 1");
2347
2348        let skip = residency
2349            .resolve(&image(64, 64))
2350            .expect_err("nothing this frame drew is evictable");
2351
2352        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
2353        assert_eq!(residency.skipped(), 1);
2354        assert_eq!(residency.pressure_evictions(), 0);
2355        assert_eq!(residency.entry_count(), 2, "both occupants stayed put");
2356        assert!(
2357            residency.evictions().is_empty(),
2358            "no region a frame is about to sample was scheduled for clearing"
2359        );
2360    }
2361
2362    #[test]
2363    fn the_least_recently_seen_image_is_the_one_displaced() {
2364        let mut residency = two_slot_residency();
2365        let old = image(64, 64);
2366        let refreshed = image(64, 64);
2367
2368        residency.begin_frame();
2369        let refreshed_region = residency.resolve(&refreshed).expect("takes a layer").region;
2370        residency.begin_frame();
2371        let old_region = residency.resolve(&old).expect("takes the other").region;
2372        residency.acknowledge_plan();
2373
2374        // The older entry is drawn again, which makes the *other* one the least
2375        // recently seen despite having been made resident second.
2376        residency.begin_frame();
2377        residency.resolve(&refreshed).expect("still resident");
2378        let arrival = residency.resolve(&image(64, 64)).expect("displaces one");
2379
2380        assert_eq!(residency.pressure_evictions(), 1);
2381        assert_eq!(
2382            arrival.region, old_region,
2383            "the rectangle taken is the least recently seen one"
2384        );
2385        assert_eq!(
2386            residency.resolve(&refreshed).map(|again| again.region),
2387            Ok(refreshed_region),
2388            "the image drawn this frame kept its own"
2389        );
2390    }
2391
2392    #[test]
2393    fn a_pressure_eviction_reports_its_rectangle_once_and_withdraws_its_upload() {
2394        let mut residency = two_slot_residency();
2395        let displaced = image(64, 64);
2396
2397        residency.begin_frame();
2398        let displaced_region = residency.resolve(&displaced).expect("takes a layer").region;
2399        residency.resolve(&image(64, 64)).expect("takes the other");
2400        // Deliberately NOT acknowledged: the displaced image still has an
2401        // upload pending, which must go with its entry rather than be written
2402        // into a rectangle its new occupant now owns.
2403        assert_eq!(residency.uploads().len(), 2);
2404
2405        residency.begin_frame();
2406        let arrival = residency
2407            .resolve(&image(64, 64))
2408            .expect("displaces the oldest");
2409
2410        assert_eq!(
2411            residency.evictions(),
2412            &[padded(displaced_region, ATLAS_PADDING)],
2413            "exactly one rectangle, reported exactly once"
2414        );
2415        assert_eq!(
2416            arrival.region, displaced_region,
2417            "and the freed rectangle is what the new occupant took"
2418        );
2419        assert_eq!(
2420            residency.uploads().len(),
2421            2,
2422            "the displaced image's unserviced upload went with its entry — \
2423             writing it would put an evicted image back over its successor"
2424        );
2425    }
2426
2427    #[test]
2428    fn an_age_reap_and_a_pressure_eviction_are_counted_separately() {
2429        // The two bounds answer different questions: one says a screen moved
2430        // on, the other says the atlas is too small for what is on it.
2431        let mut residency = two_slot_residency();
2432        let data = image(64, 64);
2433
2434        residency.begin_frame();
2435        residency.resolve(&data).expect("fits");
2436        for _ in 0..=MAX_UNSEEN_FRAMES {
2437            residency.begin_frame();
2438        }
2439
2440        assert_eq!(residency.entry_count(), 0, "the age reap ran");
2441        assert_eq!(
2442            residency.pressure_evictions(),
2443            0,
2444            "an age reap is not a pressure eviction"
2445        );
2446        assert_eq!(residency.frame_pressure_evictions(), 0);
2447    }
2448
2449    #[test]
2450    fn the_per_frame_eviction_count_is_this_frames_alone() {
2451        let mut residency = two_slot_residency();
2452        residency.begin_frame();
2453        residency.resolve(&image(64, 64)).expect("takes a layer");
2454        residency.resolve(&image(64, 64)).expect("takes the other");
2455
2456        residency.begin_frame();
2457        residency.resolve(&image(64, 64)).expect("displaces one");
2458        assert_eq!(residency.frame_pressure_evictions(), 1);
2459        assert_eq!(residency.pressure_evictions(), 1);
2460
2461        residency.begin_frame();
2462        assert_eq!(
2463            residency.frame_pressure_evictions(),
2464            0,
2465            "a new frame starts from nothing"
2466        );
2467        assert_eq!(
2468            residency.pressure_evictions(),
2469            1,
2470            "while the lifetime total keeps counting"
2471        );
2472    }
2473
2474    /// A budget of exactly four 32-square cells in one layer, so "the atlas is
2475    /// full" needs no reasoning about how a packer splits free space and a
2476    /// request that fits *no* arrangement of the holes is one rectangle.
2477    fn four_slot_residency() -> ImageResidency {
2478        ImageResidency::new(AtlasBudget {
2479            atlas_size: (64, 64),
2480            max_atlases: 1,
2481        })
2482    }
2483
2484    #[test]
2485    fn a_request_no_candidate_set_can_place_is_refused_before_anything_is_freed() {
2486        // The storm, at the cache seam. One 32-square is this frame's and three
2487        // are last frame's; a 48-square fits none of the holes those three
2488        // would leave while the fourth is resident. Freeing them buys nothing
2489        // — the draw is skipped either way — and costs three clears plus three
2490        // re-uploads, every frame the scene repeats. So the refusal has to come
2491        // before the first rectangle is handed back.
2492        let mut residency = four_slot_residency();
2493        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
2494        residency.begin_frame();
2495        for data in &resident {
2496            residency
2497                .resolve(data)
2498                .expect("the four cells take four images");
2499        }
2500        residency.acknowledge_plan();
2501
2502        residency.begin_frame();
2503        residency.resolve(&resident[0]).expect("still resident");
2504
2505        let skip = residency
2506            .resolve(&image(48, 48))
2507            .expect_err("no arrangement of the three candidates admits a 48-square");
2508
2509        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
2510        assert_eq!(
2511            residency.pressure_evictions(),
2512            0,
2513            "a request that could not have been placed displaced nothing"
2514        );
2515        assert_eq!(residency.entry_count(), 4, "the residency is untouched");
2516        assert!(
2517            residency.evictions().is_empty(),
2518            "and nothing was scheduled for clearing on the way to failing"
2519        );
2520        assert!(residency.uploads().is_empty());
2521    }
2522
2523    #[test]
2524    fn a_request_larger_than_every_candidate_together_is_refused_on_area_alone() {
2525        // The cheaper half of the same precheck: freeing all three candidates
2526        // leaves three quarters of one layer, and the request wants all of it.
2527        // The area comes off the packer's own accounting, so it counts what the
2528        // glyph half of the allocator holds as well as this residency's own.
2529        let mut residency = four_slot_residency();
2530        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
2531        residency.begin_frame();
2532        for data in &resident {
2533            residency
2534                .resolve(data)
2535                .expect("the four cells take four images");
2536        }
2537        residency.acknowledge_plan();
2538
2539        residency.begin_frame();
2540        residency.resolve(&resident[0]).expect("still resident");
2541        residency
2542            .resolve(&image(64, 64))
2543            .expect_err("a whole layer is more than the three candidates hold");
2544
2545        assert_eq!(residency.pressure_evictions(), 0);
2546        assert_eq!(residency.entry_count(), 4);
2547        assert!(residency.evictions().is_empty());
2548    }
2549
2550    #[test]
2551    fn a_satisfiable_request_under_pressure_still_evicts_the_minimum() {
2552        // The other side of the precheck: it must not turn a working eviction
2553        // into a skip. One 32-square arriving against four resident ones is
2554        // satisfiable by freeing exactly one, and that is what it costs.
2555        let mut residency = four_slot_residency();
2556        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
2557        residency.begin_frame();
2558        for data in &resident {
2559            residency
2560                .resolve(data)
2561                .expect("the four cells take four images");
2562        }
2563        residency.acknowledge_plan();
2564
2565        residency.begin_frame();
2566        residency.resolve(&image(32, 32)).expect("displaces one");
2567
2568        assert_eq!(
2569            residency.pressure_evictions(),
2570            1,
2571            "the minimum, not the lot"
2572        );
2573        assert_eq!(residency.entry_count(), 4);
2574        assert_eq!(residency.evictions().len(), 1);
2575    }
2576
2577    #[test]
2578    fn a_blob_redrawn_at_a_new_extent_never_frees_what_this_frame_resolved() {
2579        // The extent-mismatch release under the pressure path's own rule. The
2580        // plan clears before it uploads, so giving the rectangle back on a
2581        // frame whose first draw already resolved it would zero texels that
2582        // draw is about to sample.
2583        let square = image(16, 8);
2584        let transposed = ImageData {
2585            data: square.data.clone(),
2586            format: square.format,
2587            alpha_type: square.alpha_type,
2588            width: 8,
2589            height: 16,
2590        };
2591
2592        let mut residency = residency();
2593        residency.begin_frame();
2594        let first = residency.resolve(&square).expect("takes a rectangle");
2595        let skip = residency
2596            .resolve(&transposed)
2597            .expect_err("the second extent cannot take the first one's rectangle");
2598
2599        assert!(
2600            matches!(skip, ImageSkip::SameFrameExtentConflict { .. }),
2601            "a same-frame ordering conflict, not an atlas-capacity refusal: {skip:?}"
2602        );
2603        assert!(
2604            residency.evictions().is_empty(),
2605            "no clear over a rectangle this frame already sampled"
2606        );
2607        assert_eq!(residency.entry_count(), 1);
2608        assert_eq!(residency.uploads().len(), 1);
2609
2610        // On a later frame nothing has sampled it yet, so the release is
2611        // ordinary again and the new extent takes its place.
2612        residency.acknowledge_plan();
2613        residency.begin_frame();
2614        let second = residency.resolve(&transposed).expect("the new extent fits");
2615        assert_eq!(second.natural, [8, 16]);
2616        assert_eq!(
2617            residency.evictions(),
2618            &[padded(first.region, ATLAS_PADDING)],
2619            "the rectangle the old extent held is reported for clearing"
2620        );
2621    }
2622
2623    #[test]
2624    fn a_layer_admits_only_what_every_occupant_leaves_room_for() {
2625        // The planner's own necessary condition, in isolation. A placement has
2626        // to lie entirely to one side of each occupant, so a 32-square in one
2627        // corner of a 64-square layer leaves nothing a 48-square can occupy —
2628        // while a 32-square still has three cells to choose from.
2629        let corner = PressureRect {
2630            key: 1,
2631            layer: 0,
2632            x0: 0,
2633            y0: 0,
2634            x1: 32,
2635            y1: 32,
2636            area: 1024,
2637            last_seen: 0,
2638            slot: 0,
2639            freed: false,
2640        };
2641        let rects = [corner];
2642
2643        assert!(!layer_admits(&rects, 0, 48, 48, (64, 64)));
2644        assert!(layer_admits(&rects, 0, 32, 32, (64, 64)));
2645        assert!(layer_admits(&rects, 0, 32, 64, (64, 64)));
2646        // A freed rectangle is not an obstacle, and a layer nothing occupies
2647        // admits anything the layer itself can hold.
2648        assert!(layer_admits(
2649            &[PressureRect {
2650                freed: true,
2651                ..corner
2652            }],
2653            0,
2654            64,
2655            64,
2656            (64, 64)
2657        ));
2658        assert!(!layer_admits(&[], 0, 65, 64, (64, 64)));
2659        // An occupant on another layer says nothing about this one.
2660        assert!(layer_admits(
2661            &[PressureRect { layer: 1, ..corner }],
2662            0,
2663            64,
2664            64,
2665            (64, 64)
2666        ));
2667    }
2668
2669    #[test]
2670    fn a_plan_that_would_outrun_its_budget_frees_nothing() {
2671        // The bound is on the *search*, not only on the damage: a walk that
2672        // reaches the budget without finding a fit answers `None`, and the
2673        // caller has released nothing by then.
2674        let mut rects: Vec<PressureRect> = (0..MAX_PRESSURE_EVICTION_CANDIDATES + 8)
2675            .map(|index| {
2676                let index = u32::try_from(index).expect("fits");
2677                PressureRect {
2678                    key: u64::from(index),
2679                    layer: 0,
2680                    x0: index,
2681                    y0: 0,
2682                    x1: index + 1,
2683                    y1: 1,
2684                    area: 1,
2685                    last_seen: 0,
2686                    slot: index,
2687                    freed: false,
2688                }
2689            })
2690            .collect();
2691        let order: Vec<usize> = (0..rects.len()).collect();
2692        let mut free_area = vec![0_u64];
2693        let mut layers = Vec::new();
2694
2695        assert_eq!(
2696            plan_pressure_eviction(
2697                64,
2698                64,
2699                (64, 64),
2700                &mut rects,
2701                &order,
2702                &mut free_area,
2703                &mut layers
2704            ),
2705            None,
2706            "a walk that cannot fit the request inside its budget plans nothing"
2707        );
2708        assert!(
2709            rects.iter().filter(|rect| rect.freed).count() <= MAX_PRESSURE_EVICTION_CANDIDATES + 1,
2710            "and it stopped walking rather than marking the whole residency"
2711        );
2712    }
2713
2714    #[test]
2715    fn a_satisfiable_layer_is_not_starved_by_a_hopeless_ones_candidates() {
2716        // The multi-layer shape the per-layer budget exists for. Layer 0 is
2717        // "this frame's" 64x40 anchor plus more than
2718        // `MAX_PRESSURE_EVICTION_CANDIDATES` one-pixel stale fillers packed
2719        // into the 64x24 strip left over — every one of them stale (older
2720        // than the incoming request's frame) and none of them, individually
2721        // or together, ever able to free a 32-tall rectangle, since the strip
2722        // they sit in is only 24 pixels tall. Layer 1 is four 32-square
2723        // quadrants exactly tiling the layer, three "this frame's" and one
2724        // stale. Layers 2 and 3 are each one "this frame's" 64-square image,
2725        // filling them outright so the incoming request cannot simply grow
2726        // into an unused layer.
2727        //
2728        // A single pooled budget spends its whole allowance walking layer 0's
2729        // fillers — none of which can ever satisfy the request — and never
2730        // reaches layer 1's one satisfiable candidate at all. A per-layer
2731        // budget exhausts layer 0's own allowance the same way, then gives
2732        // layer 1 a fresh one and finds the fit immediately.
2733        let mut residency = ImageResidency::new(AtlasBudget {
2734            atlas_size: (64, 64),
2735            max_atlases: 4,
2736        });
2737
2738        let anchor = image(64, 40);
2739        let fillers: Vec<ImageData> = (0..MAX_PRESSURE_EVICTION_CANDIDATES + 6)
2740            .map(|_| image(1, 1))
2741            .collect();
2742        let quadrants: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
2743        let layer2_filler = image(64, 64);
2744        let layer3_filler = image(64, 64);
2745
2746        residency.begin_frame();
2747        residency
2748            .resolve(&anchor)
2749            .expect("the anchor takes layer 0's top");
2750        for filler in &fillers {
2751            residency
2752                .resolve(filler)
2753                .expect("a one-pixel filler always finds room in the leftover strip");
2754        }
2755        for quadrant in &quadrants {
2756            residency
2757                .resolve(quadrant)
2758                .expect("a 32-square quadrant packs layer 1's empty corners");
2759        }
2760        residency
2761            .resolve(&layer2_filler)
2762            .expect("a whole-layer image grows into a fresh layer 2");
2763        residency
2764            .resolve(&layer3_filler)
2765            .expect("a whole-layer image grows into a fresh layer 3");
2766        residency.acknowledge_plan();
2767        assert_eq!(
2768            residency.layers(),
2769            4,
2770            "fixture precondition: all four layers exist"
2771        );
2772
2773        // Refresh everything except the fillers and the first quadrant, so
2774        // only they are stale candidates when the request arrives — the
2775        // fillers sort ahead of the stale quadrant because they were
2776        // resolved first and share the same `last_seen`.
2777        residency.begin_frame();
2778        residency.resolve(&anchor).expect("still resident");
2779        residency.resolve(&quadrants[1]).expect("still resident");
2780        residency.resolve(&quadrants[2]).expect("still resident");
2781        residency.resolve(&quadrants[3]).expect("still resident");
2782        residency.resolve(&layer2_filler).expect("still resident");
2783        residency.resolve(&layer3_filler).expect("still resident");
2784
2785        let arrival = residency
2786            .resolve(&image(32, 32))
2787            .expect("a per-layer budget reaches layer 1's satisfiable candidate");
2788
2789        assert_eq!(
2790            residency.pressure_evictions(),
2791            1,
2792            "exactly the one quadrant needed, not layer 0's hopeless fillers"
2793        );
2794        assert_eq!(residency.evictions().len(), 1);
2795        assert_eq!(
2796            residency.evictions()[0].layer,
2797            arrival.region.layer,
2798            "the eviction and the arrival land on the same, single layer"
2799        );
2800        assert_ne!(
2801            arrival.region.layer, 0,
2802            "the winning layer is not the one full of hopeless fillers"
2803        );
2804    }
2805
2806    /// The tier line's own text, which only a `perf-trace` build has.
2807    #[cfg(feature = "perf-trace")]
2808    #[test]
2809    fn the_resolved_tier_line_names_the_tier_and_is_written_once() {
2810        let mobile = TierCaps::fake(DownlevelProfile::WebGl2);
2811        assert_eq!(
2812            atlas_tier_line(&mobile, AtlasBudget::MOBILE),
2813            "frust-perf atlas tier=mobile budget=1024x1024x4 downlevel=WebGl2 \
2814             transient_saves_memory=false adapter=fake-webgl2"
2815        );
2816
2817        let mut tiler = TierCaps::fake(DownlevelProfile::Full);
2818        tiler.adapter_name = "Adreno (TM) 660".to_string();
2819        assert_eq!(
2820            atlas_tier_line(&tiler, AtlasBudget::DESKTOP),
2821            "frust-perf atlas tier=desktop budget=2048x2048x8 downlevel=Full \
2822             transient_saves_memory=false adapter=Adreno (TM) 660"
2823        );
2824
2825        // The signal that decides the tier is the one the line reports, so a
2826        // tile-based adapter reading `Full` still says `mobile`.
2827        tiler.transient_saves_memory = true;
2828        assert!(atlas_tier_line(&tiler, AtlasBudget::MOBILE).contains("tier=mobile"));
2829        assert!(
2830            atlas_tier_line(&tiler, AtlasBudget::MOBILE).contains("transient_saves_memory=true")
2831        );
2832
2833        let latch = TierLogOnce::new();
2834        assert!(
2835            latch.emit(&mobile, AtlasBudget::MOBILE),
2836            "the first call writes the line"
2837        );
2838        assert!(
2839            !latch.emit(&mobile, AtlasBudget::MOBILE),
2840            "and no later one repeats it"
2841        );
2842    }
2843
2844    #[test]
2845    fn an_override_redistributes_a_tiers_memory_but_never_exceeds_it() {
2846        let tier = AtlasBudget::DESKTOP;
2847
2848        // The pathological override the adapter alone would admit: a
2849        // 16384-square layer is a gibibyte on its own, so clamping the layer
2850        // count could not have brought it inside the tier's 128 MiB.
2851        let huge = AtlasBudget {
2852            atlas_size: (16_384, 16_384),
2853            max_atlases: tier.max_atlases,
2854        }
2855        .within_total_bytes(tier.total_bytes());
2856        assert!(huge.total_bytes() <= tier.total_bytes());
2857        assert_eq!(huge.atlas_size.0, huge.atlas_size.1, "aspect preserved");
2858        assert!(huge.max_atlases >= 1);
2859
2860        // A modest override is left exactly as asked for.
2861        let modest = AtlasBudget {
2862            atlas_size: (1024, 1024),
2863            max_atlases: tier.max_atlases,
2864        }
2865        .within_total_bytes(tier.total_bytes());
2866        assert_eq!(modest.atlas_size, (1024, 1024));
2867        assert_eq!(modest.max_atlases, tier.max_atlases);
2868
2869        // An extreme aspect ratio floors one axis to nothing; it is raised back
2870        // to a texel and the other axis gives the room up, so the pair still
2871        // fits and neither axis is zero.
2872        let sliver = AtlasBudget {
2873            atlas_size: (65_535, 4),
2874            max_atlases: 1,
2875        }
2876        .within_total_bytes(64);
2877        assert!(sliver.atlas_size.0 >= 1 && sliver.atlas_size.1 >= 1);
2878        assert!(sliver.total_bytes() <= 64);
2879    }
2880}