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 large-mobile budget's preferred shape: four 2048-square layers,
339    /// 64 MiB in total — a quarter as many layers as
340    /// [`MOBILE_LARGE_DEEP`](Self::MOBILE_LARGE_DEEP) for the same texels, so
341    /// the array grows (and copies itself) a quarter as often.
342    pub const MOBILE_LARGE_WIDE: Self = Self {
343        atlas_size: (2048, 2048),
344        max_atlases: 4,
345    };
346
347    /// The large-mobile budget's shape on an adapter whose texture edge stops
348    /// short of 2048: sixteen 1024-square layers, the same 64 MiB.
349    pub const MOBILE_LARGE_DEEP: Self = Self {
350        atlas_size: (1024, 1024),
351        max_atlases: 16,
352    };
353
354    /// The large-mobile shape `caps`' adapter can create unclamped, or `None`
355    /// when it can create neither — the tier then falls back to
356    /// [`MOBILE`](Self::MOBILE) rather than to a large budget its ceilings
357    /// would have narrowed below the plain one's.
358    #[must_use]
359    pub fn mobile_large(caps: &TierCaps) -> Option<Self> {
360        [Self::MOBILE_LARGE_WIDE, Self::MOBILE_LARGE_DEEP]
361            .into_iter()
362            .find(|shape| shape.clamped(caps) == *shape)
363    }
364
365    /// The unclamped budget `tier` stands for on `caps`' adapter.
366    #[must_use]
367    pub fn for_tier(tier: AtlasTier, caps: &TierCaps) -> Self {
368        match tier {
369            AtlasTier::Mobile => Self::MOBILE,
370            AtlasTier::MobileLarge => Self::mobile_large(caps).unwrap_or(Self::MOBILE),
371            AtlasTier::Desktop => Self::DESKTOP,
372        }
373    }
374
375    /// The budget for `caps`' adapter, clamped to what it can actually create.
376    ///
377    /// The tier choice is [`choose_tier`] over the adapter and the device's
378    /// own memory ([`device_memory_bytes`]); the clamps that follow are the
379    /// adapter's own texture-edge and array-layer ceilings plus the eight-bit
380    /// [`MAX_ATLAS_LAYERS`] the encoded-image record can address.
381    ///
382    /// `FRUST_ENGINE_ATLAS_SIZE` overrides the tier's extent, and the override
383    /// is **budgeted, not unbudgeted**: it passes through
384    /// [`within_total_bytes`](Self::within_total_bytes) against the tier's own
385    /// [`total_bytes`](Self::total_bytes) before the adapter clamps. So the knob
386    /// redistributes a tier's memory between extent and depth — one large layer
387    /// instead of eight small ones — and cannot spend more of it than the tier
388    /// itself would. Clamped to the adapter last, because an override is a
389    /// request and a request the adapter cannot honour is still narrowed to what
390    /// it can.
391    ///
392    /// The resolved budget is reported once per process through
393    /// `atlas_tier_line`, because which tier a given phone takes is otherwise
394    /// an inference from behaviour rather than an observation: the adapter
395    /// name, the driver's own capability answers and the memory reading are
396    /// all device-reported, and a device answering differently from the one
397    /// it resembles is exactly the case a benchmark capture has to be able to
398    /// name. That line is `perf-trace`-only, like every other `frust-perf`
399    /// line the engine emits — a build without the feature resolves the same
400    /// budget and writes nothing.
401    #[must_use]
402    pub fn for_caps(caps: &TierCaps) -> Self {
403        let choice = choose_tier(caps, device_memory_bytes());
404        let resolved = Self::for_choice(choice, caps);
405
406        #[cfg(feature = "perf-trace")]
407        ATLAS_TIER_LOG.emit(caps, choice, resolved);
408        resolved
409    }
410
411    /// The budget `choice` resolves to on `caps`' adapter — [`for_caps`]
412    /// without reading the device's memory or writing the tier line, so the
413    /// table's outcome is testable for any adapter and memory size.
414    ///
415    /// [`for_caps`]: Self::for_caps
416    #[must_use]
417    pub fn for_choice(choice: TierChoice, caps: &TierCaps) -> Self {
418        let tier = Self::for_tier(choice.tier, caps);
419        match config::atlas_size() {
420            None => tier.clamped(caps),
421            Some(atlas_size) => Self {
422                atlas_size,
423                max_atlases: tier.max_atlases,
424            }
425            .within_total_bytes(tier.total_bytes())
426            .clamped(caps),
427        }
428    }
429
430    /// This budget narrowed so a fully grown array costs at most `total_bytes`.
431    ///
432    /// The extent goes first, scaled uniformly until one layer fits inside the
433    /// whole allowance; the layer count then takes whatever multiple of that
434    /// layer is left, never below one. Scaling the extent rather than only the
435    /// depth is what makes the ceiling hold at all: a single 16384-square layer
436    /// is a gigabyte on its own, so a clamp that could only reduce the layer
437    /// *count* would have nothing left to reduce.
438    ///
439    /// Both axes are floored after the uniform scale, which can only take the
440    /// product below the allowance — except where an extreme aspect ratio floors
441    /// one axis to nothing and it is raised back to a texel, so each axis is
442    /// then held against what the other leaves.
443    #[must_use]
444    pub fn within_total_bytes(self, total_bytes: u64) -> Self {
445        // A single texel is the smallest atlas that exists, so an allowance
446        // below one is treated as one rather than producing a zero extent the
447        // packer could not address.
448        let allowance = total_bytes.max(ATLAS_FORMAT_BYTES);
449        let max_texels = allowance / ATLAS_FORMAT_BYTES;
450
451        let (width, height) = self.atlas_size;
452        let texels = u64::from(width).saturating_mul(u64::from(height));
453        let atlas_size = if texels <= max_texels {
454            (width, height)
455        } else {
456            // `as` on a float saturates rather than wrapping, and both factors
457            // are finite and positive, so the narrowing is a plain floor.
458            let scale = (max_texels as f64 / texels.max(1) as f64).sqrt();
459            let scaled_w = ((f64::from(width) * scale) as u32).max(1);
460            let scaled_h = ((f64::from(height) * scale) as u32).max(1);
461            let fitted_w = fit_axis(scaled_w, max_texels / u64::from(scaled_h));
462            let fitted_h = fit_axis(scaled_h, max_texels / u64::from(fitted_w));
463            (fitted_w, fitted_h)
464        };
465
466        let layer_bytes = u64::from(atlas_size.0)
467            .saturating_mul(u64::from(atlas_size.1))
468            .saturating_mul(ATLAS_FORMAT_BYTES)
469            .max(1);
470        let layers = usize::try_from(allowance / layer_bytes).unwrap_or(MAX_ATLAS_LAYERS);
471
472        Self {
473            atlas_size,
474            max_atlases: self.max_atlases.min(layers).max(1),
475        }
476    }
477
478    /// Whether `region` lies inside an atlas array of `layers` layers at this
479    /// budget's per-layer extent.
480    ///
481    /// The pure counterpart of [`crate::gpu::atlas::AtlasArray::contains`],
482    /// answerable with no device: a consumer that records where an image lives
483    /// before the array exists checks the rectangle here, so it can never come
484    /// to name a region the array will refuse to write and then sample it as
485    /// whatever the texture happened to hold.
486    #[must_use]
487    pub fn contains(self, region: AtlasRegion, layers: u32) -> bool {
488        !region.is_empty()
489            && region.layer < layers
490            && region.offset[0].saturating_add(region.size[0]) <= self.atlas_size.0
491            && region.offset[1].saturating_add(region.size[1]) <= self.atlas_size.1
492    }
493
494    /// This budget narrowed to what `caps`' adapter can create.
495    #[must_use]
496    pub fn clamped(self, caps: &TierCaps) -> Self {
497        let edge = caps.max_texture_dimension_2d.max(1);
498        let layers = usize::try_from(caps.max_texture_array_layers).unwrap_or(MAX_ATLAS_LAYERS);
499
500        Self {
501            atlas_size: (
502                self.atlas_size.0.clamp(1, edge),
503                self.atlas_size.1.clamp(1, edge),
504            ),
505            max_atlases: self.max_atlases.clamp(1, layers.min(MAX_ATLAS_LAYERS)),
506        }
507    }
508
509    /// The `vello_common` configuration this budget describes.
510    ///
511    /// `initial_atlas_count` is zero so the first layer is created by the first
512    /// allocation that needs one: an application drawing no images pays for no
513    /// atlas at all, and [`vello_common::multi_atlas::MultiAtlasManager::new`]'s
514    /// own `expect` on eager creation can never be reached.
515    #[must_use]
516    pub fn config(self) -> AtlasConfig {
517        AtlasConfig {
518            initial_atlas_count: 0,
519            max_atlases: self.max_atlases,
520            atlas_size: self.atlas_size,
521            auto_grow: true,
522            allocation_strategy: AllocationStrategy::FirstFit,
523        }
524    }
525
526    /// The bytes one fully populated layer costs at [`ATLAS_FORMAT_BYTES`].
527    #[must_use]
528    pub fn layer_bytes(self) -> u64 {
529        u64::from(self.atlas_size.0)
530            .saturating_mul(u64::from(self.atlas_size.1))
531            .saturating_mul(ATLAS_FORMAT_BYTES)
532    }
533
534    /// The bytes every layer of this budget costs once fully grown.
535    #[must_use]
536    pub fn total_bytes(self) -> u64 {
537        self.layer_bytes()
538            .saturating_mul(self.max_atlases.try_into().unwrap_or(u64::MAX))
539    }
540}
541
542/// Bytes per texel of the atlas array (`Rgba8Unorm`).
543pub const ATLAS_FORMAT_BYTES: u64 = 4;
544
545/// `evictions` less every rectangle one of `uploads` covers exactly.
546///
547/// Exact cover only — same layer, offset and extent: an upload writes every
548/// texel of its own rectangle (its pixels are exactly the region's extent), so
549/// a clear of that same rectangle ordered ahead of it is overwritten in full.
550/// A rectangle covered only partly, or only by several uploads together, keeps
551/// its clear: whatever texels no upload reaches must still read as zero to the
552/// next tenant. Order is preserved, so the clears stay in the order the
553/// residency reported them.
554#[must_use]
555pub fn uncovered_clears(evictions: &[AtlasRegion], uploads: &[ImageUpload]) -> Vec<AtlasRegion> {
556    if evictions.is_empty() || uploads.is_empty() {
557        return evictions.to_vec();
558    }
559    let written: HashSet<AtlasRegion> = uploads.iter().map(|upload| upload.region).collect();
560    evictions
561        .iter()
562        .copied()
563        .filter(|region| !written.contains(region))
564        .collect()
565}
566
567/// Whether `caps`' adapter reads as a mobile part by its capability answers
568/// alone.
569///
570/// Two signals, either of which is enough:
571///
572/// - its usable limits are clamped to the GLES-3.0/WebGL2 downlevel defaults,
573///   which is the profile every GL-backed target reports; or
574/// - it reports `transient_saves_memory`, wgpu's own answer to "is this a
575///   tile-based renderer keeping attachments on-chip" — true on the mobile
576///   TBDR parts and false on desktop immediate-mode ones.
577///
578/// This is the device-class read text hinting keys on, and the atlas tier's
579/// fallback for an adapter [`ADAPTER_FAMILIES`] does not name. It is not the
580/// atlas tier itself: some mobile drivers answer `transient_saves_memory`
581/// false (Adreno 618/620 do), which this read alone would put on the desktop
582/// budget — see [`choose_tier`].
583#[must_use]
584pub fn is_mobile_tier(caps: &TierCaps) -> bool {
585    caps.downlevel_profile != DownlevelProfile::Full || caps.transient_saves_memory
586}
587
588/// Which class of part an adapter's name places it in.
589#[derive(Debug, Clone, Copy, PartialEq, Eq)]
590pub enum AdapterFamily {
591    /// A phone/tablet GPU family: the atlas competes with the rest of a
592    /// shared-memory device's budget.
593    Mobile,
594    /// A discrete or desktop-integrated GPU family.
595    Desktop,
596}
597
598/// The adapter-name table the atlas tier is read from: a lowercase fragment
599/// and the family an adapter whose name contains it belongs to, first match
600/// winning.
601///
602/// A name table rather than capability answers alone because the capability
603/// answers disagree with the hardware: Adreno 618 and 620 report a full
604/// profile and `transient_saves_memory` false — the desktop answers — while
605/// sharing their device's memory with everything else on it. An adapter the
606/// table does not name keeps the capability read ([`is_mobile_tier`]), so a
607/// family missing here costs the old behaviour rather than a wrong one.
608///
609/// Apple is listed whole: Apple-silicon Macs already took the mobile budget
610/// through `transient_saves_memory`, so naming them changes nothing there.
611pub const ADAPTER_FAMILIES: &[(&str, AdapterFamily)] = &[
612    ("adreno", AdapterFamily::Mobile),
613    ("mali", AdapterFamily::Mobile),
614    ("immortalis", AdapterFamily::Mobile),
615    ("powervr", AdapterFamily::Mobile),
616    ("xclipse", AdapterFamily::Mobile),
617    ("maleoon", AdapterFamily::Mobile),
618    ("apple", AdapterFamily::Mobile),
619    ("tegra", AdapterFamily::Mobile),
620    ("nvidia", AdapterFamily::Desktop),
621    ("geforce", AdapterFamily::Desktop),
622    ("quadro", AdapterFamily::Desktop),
623    ("radeon", AdapterFamily::Desktop),
624    ("amd", AdapterFamily::Desktop),
625    ("intel", AdapterFamily::Desktop),
626];
627
628/// The family [`ADAPTER_FAMILIES`] places `adapter_name` in, matched
629/// case-insensitively; `None` for a name the table does not know.
630#[must_use]
631pub fn adapter_family(adapter_name: &str) -> Option<AdapterFamily> {
632    let name = adapter_name.to_ascii_lowercase();
633    ADAPTER_FAMILIES
634        .iter()
635        .find(|(fragment, _)| name.contains(fragment))
636        .map(|(_, family)| *family)
637}
638
639/// The atlas budget tiers.
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub enum AtlasTier {
642    /// [`AtlasBudget::MOBILE`].
643    Mobile,
644    /// [`AtlasBudget::mobile_large`]: a mobile family part on a device with at
645    /// least [`MOBILE_LARGE_MIN_MEMORY`] of memory.
646    MobileLarge,
647    /// [`AtlasBudget::DESKTOP`].
648    Desktop,
649}
650
651impl AtlasTier {
652    /// The tier's name as the tier line spells it.
653    #[must_use]
654    pub fn label(self) -> &'static str {
655        match self {
656            Self::Mobile => "mobile",
657            Self::MobileLarge => "mobile_large",
658            Self::Desktop => "desktop",
659        }
660    }
661}
662
663/// Which rule of [`choose_tier`] decided a tier.
664#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665pub enum TierRule {
666    /// The adapter's limits are clamped to the WebGL2 downlevel defaults.
667    Downlevel,
668    /// [`ADAPTER_FAMILIES`] named the adapter.
669    Table,
670    /// The table did not; [`is_mobile_tier`]'s capability read decided.
671    Heuristic,
672}
673
674impl TierRule {
675    /// The rule's name as the tier line spells it.
676    #[must_use]
677    pub fn label(self) -> &'static str {
678        match self {
679            Self::Downlevel => "downlevel",
680            Self::Table => "table",
681            Self::Heuristic => "heuristic",
682        }
683    }
684}
685
686/// The resolved tier, with what decided it.
687#[derive(Debug, Clone, Copy, PartialEq, Eq)]
688pub struct TierChoice {
689    /// The tier.
690    pub tier: AtlasTier,
691    /// The rule that chose it.
692    pub rule: TierRule,
693    /// The device memory the choice was made against, when it was known.
694    pub device_memory: Option<u64>,
695}
696
697/// The device memory from which a mobile family part takes the large budget:
698/// 4 GiB.
699///
700/// The large budget is 64 MiB of atlas, a sixteenth of that; below it the
701/// plain 16 MiB budget stays.
702pub const MOBILE_LARGE_MIN_MEMORY: u64 = 4 << 30;
703
704/// The atlas tier for `caps`' adapter on a device with `device_memory` bytes
705/// of memory (`None` when unknown).
706///
707/// In order:
708///
709/// 1. A downlevel (WebGL2/GLES) profile is [`AtlasTier::Mobile`], whatever the
710///    name: its limits are the browser ceiling, and the budget follows them.
711/// 2. An adapter [`ADAPTER_FAMILIES`] names takes its family's tier — a
712///    desktop family [`AtlasTier::Desktop`]; a mobile family
713///    [`AtlasTier::MobileLarge`] when the device reports at least
714///    [`MOBILE_LARGE_MIN_MEMORY`] and the adapter can create a large shape
715///    unclamped ([`AtlasBudget::mobile_large`]), else [`AtlasTier::Mobile`].
716///    Unknown memory is the small tier: the large one is earned, not assumed.
717/// 3. Any other adapter keeps [`is_mobile_tier`]'s capability read, mobile or
718///    desktop, exactly as before the table existed.
719#[must_use]
720pub fn choose_tier(caps: &TierCaps, device_memory: Option<u64>) -> TierChoice {
721    let (tier, rule) = if caps.downlevel_profile != DownlevelProfile::Full {
722        (AtlasTier::Mobile, TierRule::Downlevel)
723    } else {
724        match adapter_family(&caps.adapter_name) {
725            Some(AdapterFamily::Desktop) => (AtlasTier::Desktop, TierRule::Table),
726            Some(AdapterFamily::Mobile) => {
727                let large = device_memory.is_some_and(|bytes| bytes >= MOBILE_LARGE_MIN_MEMORY)
728                    && AtlasBudget::mobile_large(caps).is_some();
729                let tier = if large {
730                    AtlasTier::MobileLarge
731                } else {
732                    AtlasTier::Mobile
733                };
734                (tier, TierRule::Table)
735            }
736            None if is_mobile_tier(caps) => (AtlasTier::Mobile, TierRule::Heuristic),
737            None => (AtlasTier::Desktop, TierRule::Heuristic),
738        }
739    };
740    TierChoice {
741        tier,
742        rule,
743        device_memory,
744    }
745}
746
747/// The device's total memory in bytes, when this platform reports it.
748///
749/// Read once per process from `/proc/meminfo`'s `MemTotal` on Linux and
750/// Android — the kernel's own figure, readable without a permission on both —
751/// and `None` everywhere else, which keeps every other platform on the tier
752/// it took before the large one existed.
753#[must_use]
754pub fn device_memory_bytes() -> Option<u64> {
755    #[cfg(any(target_os = "linux", target_os = "android"))]
756    {
757        use std::sync::OnceLock;
758        static MEMORY: OnceLock<Option<u64>> = OnceLock::new();
759        *MEMORY.get_or_init(|| {
760            std::fs::read_to_string("/proc/meminfo")
761                .ok()
762                .and_then(|text| parse_mem_total(&text))
763        })
764    }
765    #[cfg(not(any(target_os = "linux", target_os = "android")))]
766    {
767        None
768    }
769}
770
771/// `MemTotal` from a `/proc/meminfo` text, in bytes; `None` when the line is
772/// missing or not the `<n> kB` the kernel writes.
773#[must_use]
774pub fn parse_mem_total(meminfo: &str) -> Option<u64> {
775    let line = meminfo
776        .lines()
777        .find_map(|line| line.strip_prefix("MemTotal:"))?;
778    let mut fields = line.split_whitespace();
779    let kib: u64 = fields.next()?.parse().ok()?;
780    match fields.next() {
781        Some("kB") => kib.checked_mul(1024),
782        _ => None,
783    }
784}
785
786/// The process's latch for the resolved-tier line, held by
787/// [`AtlasBudget::for_caps`].
788#[cfg(feature = "perf-trace")]
789static ATLAS_TIER_LOG: TierLogOnce = TierLogOnce::new();
790
791/// A one-shot latch for the resolved-tier line.
792///
793/// Once per *process* rather than once per call: a budget is resolved once per
794/// surface, and the question the line answers — which tier this adapter took —
795/// is about the device, so one line per run answers it completely while a line
796/// per surface would repeat it.
797///
798/// A latch value rather than a bare [`std::sync::Once`] because the rule it
799/// keeps is worth testing: [`emit`](Self::emit) answers whether *this* call was
800/// the one that wrote the line, so a test constructs a latch of its own and
801/// pins "the first call writes and no later one does" without installing a
802/// process-global logger.
803///
804/// Compiled only under `perf-trace`, with the line it writes: the release-lean
805/// gate asserts a shipping binary carries no `frust-perf` bytes at all, and a
806/// `#[cfg]` is what makes that the compiler's answer rather than the
807/// optimizer's.
808#[cfg(feature = "perf-trace")]
809#[derive(Debug, Default)]
810pub struct TierLogOnce {
811    emitted: AtomicBool,
812}
813
814#[cfg(feature = "perf-trace")]
815impl TierLogOnce {
816    /// A latch that has not emitted yet.
817    #[must_use]
818    pub const fn new() -> Self {
819        Self {
820            emitted: AtomicBool::new(false),
821        }
822    }
823
824    /// Log [`atlas_tier_line`] for `caps`, `choice` and `budget` if this latch
825    /// has not fired, answering whether this call was the one that did.
826    pub fn emit(&self, caps: &TierCaps, choice: TierChoice, budget: AtlasBudget) -> bool {
827        if self.emitted.swap(true, Ordering::Relaxed) {
828            return false;
829        }
830
831        log::info!("{}", atlas_tier_line(caps, choice, budget));
832        true
833    }
834}
835
836/// The one line a run records the resolved atlas tier as.
837///
838/// `perf-trace`-only, with [`TierLogOnce`] — see its own doc for why the gate
839/// is a `#[cfg]` rather than a branch.
840///
841/// `frust-perf`-prefixed deliberately: a benchmark capture keeps every line
842/// carrying that prefix and drops the rest, so this is what lets a run's own
843/// log say which budget the device was given rather than leaving it to be
844/// inferred from whether images went missing. `rule` names which of
845/// [`choose_tier`]'s rules decided, and `memory_mib` the device memory it was
846/// decided against (`unknown` where the platform does not report it). The
847/// adapter name is last because it is the one field that can contain spaces,
848/// which keeps every `key=value` field ahead of it parseable by splitting on
849/// whitespace.
850#[cfg(feature = "perf-trace")]
851#[must_use]
852pub fn atlas_tier_line(caps: &TierCaps, choice: TierChoice, budget: AtlasBudget) -> String {
853    let memory = choice
854        .device_memory
855        .map_or_else(|| "unknown".to_string(), |bytes| (bytes >> 20).to_string());
856
857    format!(
858        "frust-perf atlas tier={} rule={} budget={}x{}x{} downlevel={:?} \
859         transient_saves_memory={} memory_mib={memory} adapter={}",
860        choice.tier.label(),
861        choice.rule.label(),
862        budget.atlas_size.0,
863        budget.atlas_size.1,
864        budget.max_atlases,
865        caps.downlevel_profile,
866        caps.transient_saves_memory,
867        caps.adapter_name,
868    )
869}
870
871/// Where one image's texels live in the atlas array.
872///
873/// Hashable because the pending-clear list carries a set index beside it: the
874/// same rectangle can reach [`ImageResidency::evictions`] twice before a single
875/// clear has been serviced, and the dedup that keeps it reported once must not
876/// be a linear scan of a list that grows with the churn rate.
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
878pub struct AtlasRegion {
879    /// Array layer holding the region.
880    pub layer: u32,
881    /// Pixel offset of the region's top-left corner within that layer.
882    pub offset: [u32; 2],
883    /// Region extent in pixels.
884    pub size: [u32; 2],
885}
886
887impl AtlasRegion {
888    /// The bytes this region's texels occupy at [`ATLAS_FORMAT_BYTES`].
889    #[must_use]
890    pub fn byte_len(self) -> usize {
891        (self.size[0] as usize)
892            .saturating_mul(self.size[1] as usize)
893            .saturating_mul(ATLAS_FORMAT_BYTES as usize)
894    }
895
896    /// The row stride a texel copy of this region uses.
897    #[must_use]
898    pub fn bytes_per_row(self) -> u32 {
899        self.size[0].saturating_mul(ATLAS_FORMAT_BYTES as u32)
900    }
901
902    /// Whether this region has any texels at all.
903    #[must_use]
904    pub fn is_empty(self) -> bool {
905        self.size[0] == 0 || self.size[1] == 0
906    }
907}
908
909/// A resident image: the handle a paint names it by, and where its texels are.
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911pub struct ResidentImage {
912    /// The handle an [`ImageSource::OpaqueId`] carries.
913    pub id: ImageId,
914    /// The atlas rectangle holding the image's own pixels, padding excluded.
915    ///
916    /// Equal to [`natural`](Self::natural) unless the source was minified to
917    /// fit the atlas, in which case it is the smaller rectangle actually
918    /// uploaded.
919    pub region: AtlasRegion,
920    /// The source's own declared extent, before any minification.
921    ///
922    /// Carried because it is the extent a paint transform was composed against
923    /// — `compile::paint::encode_image_command` derives its natural-to-dest
924    /// mapping from the `ImageData`'s declared size, which it reads before this
925    /// residency is consulted. A consumer that ends up sampling a *minified*
926    /// rectangle has to fold [`minify_scale`](Self::minify_scale) into that
927    /// transform, and this is the half of the ratio the atlas rectangle does
928    /// not carry.
929    pub natural: [u32; 2],
930    /// Transparent padding pixels around `region` in the atlas.
931    pub padding: u32,
932    /// Whether the image's premultiplied pixels include any non-opaque texel.
933    pub may_have_transparency: bool,
934}
935
936impl ResidentImage {
937    /// This image as the paint-side source a `vello_common` encoding carries.
938    #[must_use]
939    pub fn source(&self) -> ImageSource {
940        ImageSource::opaque_id_with_transparency_hint(self.id, self.may_have_transparency)
941    }
942
943    /// The per-axis factor mapping a natural-space texel coordinate onto the
944    /// resident rectangle, or `None` when the source was stored at full size.
945    ///
946    /// `None` rather than `Some((1.0, 1.0))` so a caller can skip the rescale
947    /// entirely on the overwhelmingly common path, and so "was this image
948    /// minified?" is one question rather than a float comparison.
949    #[must_use]
950    pub fn minify_scale(&self) -> Option<(f32, f32)> {
951        if self.region.size == self.natural {
952            return None;
953        }
954        let natural_w = self.natural[0].max(1) as f32;
955        let natural_h = self.natural[1].max(1) as f32;
956        Some((
957            self.region.size[0] as f32 / natural_w,
958            self.region.size[1] as f32 / natural_h,
959        ))
960    }
961}
962
963/// One region's texels, waiting to be written into the atlas array.
964///
965/// The pixels are the premultiplied `Rgba8Unorm` the atlas stores, produced
966/// once at allocation — an image made resident on frame 1 and drawn on frames
967/// 1..1000 is converted exactly once.
968#[derive(Debug, Clone)]
969pub struct ImageUpload {
970    /// The handle the paint naming these pixels carries.
971    ///
972    /// Carried so a consumer rebuilding its own view of residency can key this
973    /// upload directly rather than pairing the frame's uploads positionally
974    /// against its encoded paints. A plan re-emitted until it is acknowledged
975    /// breaks that pairing (an upload can outlive the frame whose draw order
976    /// produced it), and an upload that names itself needs no ordering to be
977    /// read correctly.
978    pub id: ImageId,
979    /// Where the texels go.
980    pub region: AtlasRegion,
981    /// The source's own declared extent, before any minification — see
982    /// [`ResidentImage::natural`], whose value this is.
983    ///
984    /// Carried on the upload because a consumer rebuilding its own view of
985    /// residency from a compiled frame has the upload and nothing else, and
986    /// `region` alone cannot say whether it holds a minified copy.
987    pub natural: [u32; 2],
988    /// The premultiplied pixels, row-major and exactly `region`'s extent.
989    pub pixels: Arc<Pixmap>,
990    /// Whether those pixels include any non-opaque texel — the same hint
991    /// [`ResidentImage::may_have_transparency`] carries, so a consumer can
992    /// rebuild the whole residency record from this upload alone.
993    pub may_have_transparency: bool,
994}
995
996/// One cache entry: the handle, its rectangle, and when it was last drawn.
997#[derive(Debug, Clone, Copy)]
998struct Entry {
999    id: ImageId,
1000    region: AtlasRegion,
1001    /// The source extent this entry was allocated for, which is what a later
1002    /// frame's `ImageData` is matched against — `region` may be the minified
1003    /// rectangle and so says nothing about the source.
1004    natural: [u32; 2],
1005    may_have_transparency: bool,
1006    last_seen: u64,
1007}
1008
1009/// The images resident in the atlas array, keyed by blob identity.
1010///
1011/// One per compiler (and so one per surface). Its frame clock is advanced by
1012/// [`begin_frame`](Self::begin_frame), which is also where the age-based reap
1013/// runs — before any of the frame's own resolutions, so a rectangle freed this
1014/// frame is available to this frame's allocations and its clear is ordered
1015/// ahead of their uploads.
1016///
1017/// It also owns the atlas allocator the *glyph* policy packs into — see this
1018/// module's doc and [`allocator_mut`](Self::allocator_mut).
1019#[derive(Debug)]
1020pub struct ImageResidency {
1021    cache: ImageCache,
1022    budget: AtlasBudget,
1023    entries: HashMap<u64, Entry>,
1024    frame: u64,
1025    disabled: bool,
1026    uploads: Vec<ImageUpload>,
1027    evictions: Vec<AtlasRegion>,
1028    /// The pending clears' own set index: exactly the rectangles `evictions`
1029    /// holds, kept so [`release`](Self::release)'s "report a rectangle once"
1030    /// dedup is a hash lookup rather than a scan of a list whose length grows
1031    /// with the churn rate.
1032    eviction_set: HashSet<AtlasRegion>,
1033    skipped: u64,
1034    /// Rectangles taken back from a resident image because the packer had no
1035    /// room for a new one, over this residency's lifetime.
1036    pressure_evictions: u64,
1037    /// The same count for the current frame alone, zeroed by
1038    /// [`begin_frame`](Self::begin_frame) — what a per-frame report reads, since
1039    /// the lifetime total says nothing about whether the atlas is churning
1040    /// *now*.
1041    frame_pressure_evictions: u64,
1042    minified: u64,
1043    /// Blob keys whose minification has already been reported, so a source that
1044    /// is redrawn — or reaped and made resident again — logs once rather than
1045    /// once per residency.
1046    reported_minify: HashSet<u64>,
1047    /// Scratch for the reap's key list, kept so a frame that evicts allocates
1048    /// nothing.
1049    reaped: Vec<u64>,
1050    /// Scratch for the pressure planner's view of the layers — one rectangle
1051    /// per resident entry, grouped by layer — kept for the same reason: the
1052    /// frame that has to evict is the one least able to afford an allocation.
1053    pressure_rects: Vec<PressureRect>,
1054    /// Scratch for the candidate order over [`Self::pressure_rects`], least
1055    /// recently seen first. Built and sorted once per refused allocation, never
1056    /// once per eviction.
1057    pressure_order: Vec<usize>,
1058    /// Scratch for each layer's modelled free area, indexed by layer.
1059    pressure_free_area: Vec<u64>,
1060    /// Scratch for the distinct layers [`Self::pressure_order`] names, in the
1061    /// order their stalest candidate appears — the per-layer walk's own visit
1062    /// order, kept for the same no-allocation-while-evicting reason as the
1063    /// other pressure scratch.
1064    pressure_layers: Vec<u32>,
1065    /// Scratch for the keys an accepted plan releases.
1066    pressure_plan: Vec<u64>,
1067}
1068
1069impl ImageResidency {
1070    /// A residency over `budget`'s atlas geometry.
1071    ///
1072    /// `FRUST_ENGINE_NO_ATLAS` is consulted here, once, rather than per
1073    /// resolution: the switch is process-global and cached, so re-reading it on
1074    /// a hot path would buy nothing but a lock. A residency constructed with
1075    /// the switch set is exactly [`disabled`](Self::disabled).
1076    #[must_use]
1077    pub fn new(budget: AtlasBudget) -> Self {
1078        Self::with_enabled(budget, !config::atlas_disabled())
1079    }
1080
1081    /// A residency that refuses every image, allocating no atlas at all.
1082    ///
1083    /// What `FRUST_ENGINE_NO_ATLAS` selects, and what a caller wanting the same
1084    /// effect programmatically constructs. Every [`resolve`](Self::resolve)
1085    /// answers [`ImageSkip::AtlasDisabled`], so image draws are dropped with a
1086    /// logged reason rather than painted from an atlas that does not exist.
1087    #[must_use]
1088    pub fn disabled(budget: AtlasBudget) -> Self {
1089        Self::with_enabled(budget, false)
1090    }
1091
1092    fn with_enabled(budget: AtlasBudget, enabled: bool) -> Self {
1093        Self {
1094            cache: ImageCache::new_with_config(budget.config()),
1095            budget,
1096            entries: HashMap::new(),
1097            frame: 0,
1098            disabled: !enabled,
1099            uploads: Vec::new(),
1100            evictions: Vec::new(),
1101            eviction_set: HashSet::new(),
1102            skipped: 0,
1103            pressure_evictions: 0,
1104            frame_pressure_evictions: 0,
1105            minified: 0,
1106            reported_minify: HashSet::new(),
1107            reaped: Vec::new(),
1108            pressure_rects: Vec::new(),
1109            pressure_order: Vec::new(),
1110            pressure_free_area: Vec::new(),
1111            pressure_layers: Vec::new(),
1112            pressure_plan: Vec::new(),
1113        }
1114    }
1115
1116    /// A residency sized for `caps`' adapter.
1117    #[must_use]
1118    pub fn for_caps(caps: &TierCaps) -> Self {
1119        Self::new(AtlasBudget::for_caps(caps))
1120    }
1121
1122    /// The atlas geometry this residency allocates within.
1123    #[must_use]
1124    pub fn budget(&self) -> AtlasBudget {
1125        self.budget
1126    }
1127
1128    /// The single atlas allocator, for reading.
1129    ///
1130    /// Enough to resolve a handle either side allocated
1131    /// ([`ImageCache::get`]) or to count the layers created so far, without
1132    /// the mutable borrow allocation needs.
1133    #[must_use]
1134    pub fn allocator(&self) -> &ImageCache {
1135        &self.cache
1136    }
1137
1138    /// The single atlas allocator, for allocating through.
1139    ///
1140    /// This is the seam the glyph policy takes its slots from — see this
1141    /// module's doc for why one cache rather than two is a correctness
1142    /// requirement. `ImageId`s and rectangles handed out through here and
1143    /// through [`resolve`](Self::resolve) come from the same packer and the
1144    /// same id space, so a glyph and an image can never be given the same
1145    /// handle or overlapping texels.
1146    ///
1147    /// **The contract a borrower keeps:** allocate what you like, and
1148    /// deallocate *only handles you allocated yourself*. This residency's entry
1149    /// map records the rectangles it allocated and nothing re-checks them;
1150    /// freeing one of them from outside would leave an entry claiming a
1151    /// rectangle the packer has since handed to someone else. Nothing here
1152    /// enforces that, because the type the two sides must share is
1153    /// `vello_common`'s own and cannot carry an ownership tag — the glyph
1154    /// policy deallocates strictly inside `glifo`'s eviction, over the handles
1155    /// in its own entry map.
1156    #[must_use]
1157    pub fn allocator_mut(&mut self) -> &mut ImageCache {
1158        &mut self.cache
1159    }
1160
1161    /// Whether this residency refuses every image.
1162    #[must_use]
1163    pub fn is_disabled(&self) -> bool {
1164        self.disabled
1165    }
1166
1167    /// How many images are currently resident.
1168    #[must_use]
1169    pub fn entry_count(&self) -> usize {
1170        self.entries.len()
1171    }
1172
1173    /// The frame clock entries age against.
1174    #[must_use]
1175    pub fn frame(&self) -> u64 {
1176        self.frame
1177    }
1178
1179    /// How many atlas layers have been created so far.
1180    ///
1181    /// The array texture must be at least this deep before the frame's uploads
1182    /// are written.
1183    ///
1184    /// Read off the shared allocator rather than accumulated from this
1185    /// residency's own allocations, so a layer the *glyph* policy created
1186    /// counts too: the two classes pack into one array, and an array sized to
1187    /// the image half alone would refuse to hold the glyph half's pages. The
1188    /// count only ever grows — `MultiAtlasManager` appends layers and never
1189    /// drops one — so a depth reported on an earlier frame stays valid.
1190    #[must_use]
1191    pub fn layers(&self) -> u32 {
1192        u32::try_from(self.cache.atlas_count()).unwrap_or(u32::MAX)
1193    }
1194
1195    /// How many resolutions have been refused over this residency's lifetime.
1196    ///
1197    /// Counts only images that could not be uploaded at all — a format or
1198    /// extent the conversion refuses, a disabled atlas, or an atlas with no
1199    /// room left *and nothing evictable to make room with*. A source merely
1200    /// larger than one atlas layer is minified to fit, and one that displaced
1201    /// another image is counted by
1202    /// [`pressure_evictions`](Self::pressure_evictions) instead; neither
1203    /// reaches this counter.
1204    #[must_use]
1205    pub fn skipped(&self) -> u64 {
1206        self.skipped
1207    }
1208
1209    /// How many resident images have been evicted to make room for another
1210    /// over this residency's lifetime.
1211    ///
1212    /// The price of never refusing a draw the atlas could hold *something*
1213    /// for: each one is an image that will re-upload the next frame it is
1214    /// drawn on. A steady screen leaves this at zero; a scroll through more
1215    /// distinct images than the atlas has rectangles for grows it once per
1216    /// displaced image, which is the signal that the budget — not the
1217    /// mechanism — is what a frame is paying for.
1218    #[must_use]
1219    pub fn pressure_evictions(&self) -> u64 {
1220        self.pressure_evictions
1221    }
1222
1223    /// How many resident images the *current* frame has evicted to make room.
1224    ///
1225    /// Zeroed by [`begin_frame`](Self::begin_frame), so a per-frame report reads
1226    /// this rather than differencing
1227    /// [`pressure_evictions`](Self::pressure_evictions) itself.
1228    #[must_use]
1229    pub fn frame_pressure_evictions(&self) -> u64 {
1230        self.frame_pressure_evictions
1231    }
1232
1233    /// How many sources have been downsampled to fit the atlas over this
1234    /// residency's lifetime.
1235    ///
1236    /// Observational, and the counter that makes "an image too large for the
1237    /// atlas is minified, not dropped" measurable rather than asserted.
1238    #[must_use]
1239    pub fn minified(&self) -> u64 {
1240        self.minified
1241    }
1242
1243    /// The regions whose texels must be cleared before the pending uploads.
1244    ///
1245    /// Pending rather than per-frame: an entry freed on a frame nobody
1246    /// serviced is still waiting to be cleared on the next one. Both kinds of
1247    /// eviction land here — the age-based reap at the head of a frame and a
1248    /// rectangle taken back under pressure mid-frame — so the consumer's
1249    /// clear-then-upload contract covers the second without knowing it exists.
1250    #[must_use]
1251    pub fn evictions(&self) -> &[AtlasRegion] {
1252        &self.evictions
1253    }
1254
1255    /// The regions whose texels must be written after the pending evictions.
1256    ///
1257    /// Pending rather than per-frame: an image made resident on a frame that was
1258    /// refused is still waiting to be written on the next one.
1259    #[must_use]
1260    pub fn uploads(&self) -> &[ImageUpload] {
1261        &self.uploads
1262    }
1263
1264    /// A copy of the pending clear-then-write plan, evictions first.
1265    ///
1266    /// A *copy*, not a drain: the plan stays pending until
1267    /// [`acknowledge_plan`](Self::acknowledge_plan) says it was serviced against
1268    /// a live atlas, so a frame refused after compiling re-emits the same plan
1269    /// on the next frame rather than losing it (see the module doc). The upload
1270    /// pixels are behind an `Arc`, so a re-emitted plan copies handles rather
1271    /// than texels, and the steady-state plan is empty on both counts.
1272    ///
1273    /// The clears are [`evictions`](Self::evictions) less every rectangle a
1274    /// pending upload covers exactly (see [`uncovered_clears`]): that upload
1275    /// writes every texel the clear would have zeroed, after it, in the same
1276    /// plan — so the clear is pure cost, a rectangle's worth of zeroes per
1277    /// churned image. Dropped from the plan rather than from `evictions`, so
1278    /// the pending set and its acknowledgement are unchanged; a consumer
1279    /// forgetting the evicted tenant by rectangle (the renderer's registry) is
1280    /// left holding the old handle on that rectangle beside the new one, which
1281    /// nothing reads — a handle is only looked up for a draw this frame
1282    /// resolved, and a resolved handle is re-registered by its own upload — and
1283    /// which the rectangle's next reported eviction removes with the new one.
1284    #[must_use]
1285    pub fn plan(&self) -> (Vec<AtlasRegion>, Vec<ImageUpload>) {
1286        (
1287            uncovered_clears(&self.evictions, &self.uploads),
1288            self.uploads.clone(),
1289        )
1290    }
1291
1292    /// Whether any part of the pending plan is still unserviced.
1293    #[must_use]
1294    pub fn has_pending_plan(&self) -> bool {
1295        !self.evictions.is_empty() || !self.uploads.is_empty()
1296    }
1297
1298    /// Record that the pending plan reached the atlas array, clearing it.
1299    ///
1300    /// Call this only once every region it names has actually been written or
1301    /// cleared. Calling it on a frame that was refused, or whose writes the
1302    /// array declined, is exactly the defect the pending plan exists to prevent:
1303    /// the entry map would go on claiming an image is resident whose texels are
1304    /// whatever the atlas texture happened to hold.
1305    pub fn acknowledge_plan(&mut self) {
1306        self.evictions.clear();
1307        self.eviction_set.clear();
1308        self.uploads.clear();
1309    }
1310
1311    /// Advance the frame clock and reclaim everything unseen for
1312    /// [`MAX_UNSEEN_FRAMES`].
1313    ///
1314    /// Call once at the head of a frame, before any [`resolve`](Self::resolve).
1315    /// An unacknowledged plan deliberately survives this: the frame that was to
1316    /// service it may have been refused, and dropping it here is what would turn
1317    /// a refused frame into a permanently unwritten atlas region.
1318    pub fn begin_frame(&mut self) {
1319        self.frame = self.frame.saturating_add(1);
1320        self.frame_pressure_evictions = 0;
1321        self.reap();
1322    }
1323
1324    /// The atlas rectangle for `data`'s pixels, allocating and scheduling an
1325    /// upload on the first frame that asks for it.
1326    ///
1327    /// # Errors
1328    ///
1329    /// Returns the [`ImageSkip`] naming why the image was refused. Every
1330    /// refusal happens before any `vello_common` call that could assert on the
1331    /// same condition, so an image the renderer cannot hold is a skipped draw
1332    /// rather than a panicked frame (E17).
1333    pub fn resolve(&mut self, data: &ImageData) -> Result<ResidentImage, ImageSkip> {
1334        let result = self.resolve_inner(data);
1335        if result.is_err() {
1336            self.skipped = self.skipped.saturating_add(1);
1337        }
1338        result
1339    }
1340
1341    fn resolve_inner(&mut self, data: &ImageData) -> Result<ResidentImage, ImageSkip> {
1342        if self.disabled {
1343            return Err(ImageSkip::AtlasDisabled);
1344        }
1345
1346        check_supported(data)?;
1347
1348        let key = data.data.id();
1349        if let Some(entry) = self.entries.get_mut(&key)
1350            && entry.natural == [data.width, data.height]
1351        {
1352            entry.last_seen = self.frame;
1353            return Ok(ResidentImage {
1354                id: entry.id,
1355                region: entry.region,
1356                natural: entry.natural,
1357                padding: u32::from(ATLAS_PADDING),
1358                may_have_transparency: entry.may_have_transparency,
1359            });
1360        }
1361
1362        // A blob id recorded at a different extent is a caller that rebuilt the
1363        // metadata around the same buffer. Its old rectangle describes the
1364        // wrong pixels, so it is released before the new one is taken rather
1365        // than left to age out holding both — unless *this* frame has already
1366        // resolved it at the other extent. The plan clears before it uploads,
1367        // so freeing that rectangle would schedule a clear over texels a draw
1368        // this same frame already recorded, which is exactly what the pressure
1369        // path refuses to do. The second extent is skipped instead, on the same
1370        // terms as a frame whose own working set outgrows the atlas — but this
1371        // is a same-frame ordering conflict on one blob key, not an atlas
1372        // capacity shortage, so it is reported as its own reason rather than
1373        // folded into [`ImageSkip::NoAtlasSpace`].
1374        if self
1375            .entries
1376            .get(&key)
1377            .is_some_and(|entry| entry.last_seen == self.frame)
1378        {
1379            let (width, height) = fit_extent([data.width, data.height], self.budget.atlas_size);
1380            return Err(ImageSkip::SameFrameExtentConflict { width, height });
1381        }
1382        self.release(key);
1383
1384        // Checked above, so this cannot assert; the conversion is also the one
1385        // place the straight-alpha premultiply happens, and it happens once per
1386        // image rather than once per frame.
1387        let ImageSource::Pixmap(pixels) = ImageSource::from_peniko_image_data(data) else {
1388            // `from_peniko_image_data` only ever yields a pixmap; an id-backed
1389            // source would mean a `vello_common` change, which is a skip rather
1390            // than an unreachable.
1391            return Err(ImageSkip::UnsupportedFormat(data.format));
1392        };
1393
1394        let natural = [data.width, data.height];
1395        // Minified before the allocation rather than after it: the packer is
1396        // asked for the rectangle that is actually going to be uploaded, so an
1397        // over-ceiling source never takes (and then has to give back) space at
1398        // its declared extent.
1399        let (width, height) = fit_extent(natural, self.budget.atlas_size);
1400        let pixels = if [width, height] == natural {
1401            pixels
1402        } else {
1403            self.note_minify(key, natural, [width, height]);
1404            Arc::new(minify(&pixels, width, height))
1405        };
1406
1407        let no_space = ImageSkip::NoAtlasSpace {
1408            width,
1409            height,
1410            atlas_width: self.budget.atlas_size.0,
1411            atlas_height: self.budget.atlas_size.1,
1412        };
1413
1414        let id = match self.cache.allocate(width, height, ATLAS_PADDING) {
1415            Ok(id) => id,
1416            // The atlas is full. Take a rectangle back from an image no longer
1417            // on screen rather than dropping this draw — see
1418            // [`allocate_under_pressure`](Self::allocate_under_pressure), and
1419            // the module doc for why a hole is worse than a re-upload.
1420            Err(_) => self
1421                .allocate_under_pressure(width, height)
1422                .ok_or(no_space)?,
1423        };
1424
1425        let Some(resource) = self.cache.get(id) else {
1426            // A successful allocation always populates its slot; refusing here
1427            // keeps the frame path free of the `expect` the reference renderer
1428            // takes at this same point.
1429            return Err(no_space);
1430        };
1431
1432        let region = AtlasRegion {
1433            layer: resource.atlas_id.as_u32(),
1434            offset: resource.offsets(),
1435            size: resource.size(),
1436        };
1437        let may_have_transparency = pixels.may_have_transparency();
1438
1439        self.entries.insert(
1440            key,
1441            Entry {
1442                id,
1443                region,
1444                natural,
1445                may_have_transparency,
1446                last_seen: self.frame,
1447            },
1448        );
1449        self.uploads.push(ImageUpload {
1450            id,
1451            region,
1452            natural,
1453            pixels,
1454            may_have_transparency,
1455        });
1456
1457        Ok(ResidentImage {
1458            id,
1459            region,
1460            natural,
1461            padding: u32::from(ATLAS_PADDING),
1462            may_have_transparency,
1463        })
1464    }
1465
1466    /// Allocate `width` x `height` by taking rectangles back from resident
1467    /// images, least recently seen first, once the packer has refused.
1468    ///
1469    /// Answers the handle that finally fit, or `None` when no bounded set of
1470    /// candidates would have made room — which is what
1471    /// [`ImageSkip::NoAtlasSpace`] then reports, with the residency exactly as
1472    /// it was found.
1473    ///
1474    /// **Nothing is released until a plan that fits has been found.** An
1475    /// eviction that does not produce the allocation buys nothing and costs a
1476    /// clear plus a re-upload per rectangle, every frame the condition recurs
1477    /// (see the module doc). So the search runs over a *model* of the layers
1478    /// and commits only when the model says the request would then be placed:
1479    ///
1480    /// 1. **Candidates.** Only an entry last seen on an earlier frame. The
1481    ///    frame path services [`evictions`](Self::evictions) *before*
1482    ///    [`uploads`](Self::uploads), so freeing a rectangle this frame has
1483    ///    already resolved would schedule a clear over texels the same frame is
1484    ///    about to sample — painting the very hole this eviction exists to
1485    ///    prevent. A frame whose own working set outgrows the atlas therefore
1486    ///    still skips, because everything in the atlas belongs to it.
1487    /// 2. **Order.** `(last_seen, slot, key)`, least recently seen first, built
1488    ///    and sorted *once* per refused allocation rather than once per
1489    ///    eviction. The slot index breaks a tie between two entries last drawn
1490    ///    on the same frame, so which image is displaced is a property of the
1491    ///    residency rather than of the entry map's iteration order. Because the
1492    ///    order is by age, an entry drawn on the *immediately preceding* frame
1493    ///    is reached only after every staler one has already been planned —
1494    ///    which is what keeps a large image redrawn every frame from thrashing
1495    ///    a working set that is merely idle. The candidates are then walked one
1496    ///    *layer* at a time, layers visited in that same staleness order (the
1497    ///    layer whose stalest candidate sorts earliest goes first).
1498    /// 3. **Fit.** After each candidate joins the plan, the layer it sat on is
1499    ///    re-tested: the request must fit inside the freed area that layer now
1500    ///    models, and must clear every rectangle still occupying it
1501    ///    ([`layer_admits`]). The first prefix that passes ends the search.
1502    /// 4. **Budget, per layer.** Each layer's own walk stops after
1503    ///    [`MAX_PRESSURE_EVICTION_CANDIDATES`] of *that layer's* candidates, or
1504    ///    once *that layer's* plan has freed [`PRESSURE_EVICTION_AREA_BUDGET`]
1505    ///    times the requested area — a fresh allowance per layer rather than one
1506    ///    pool the whole residency shares, so candidates on a layer too
1507    ///    fragmented to ever admit the request cannot spend the allowance a
1508    ///    later layer needed to prove it could. A layer that ends on its own
1509    ///    budget contributes nothing and the walk moves to the next layer.
1510    /// 5. **Commit.** Only the planned rectangles *on the layer the fit was
1511    ///    found in* are released — a candidate the walk passed over on another
1512    ///    layer contributed nothing and is left resident.
1513    ///
1514    /// The model is optimistic about occupants it cannot see: the glyph policy
1515    /// packs through the same allocator (see the module doc) and its pages are
1516    /// not in this entry map, so [`layer_admits`] tests a subset of the real
1517    /// obstacles. That direction is the safe one — a placement clearing every
1518    /// real occupant clears the subset too, so a request eviction could satisfy
1519    /// on a *given layer* is never refused for that reason. The converse is
1520    /// possible: the packer may refuse a plan the model approved, which costs
1521    /// the plan's evictions and no allocation. It is bounded by the same
1522    /// per-layer budget as the plan itself, and it is the one residual the
1523    /// planner cannot rule out without knowing where every glyph page sits.
1524    fn allocate_under_pressure(&mut self, width: u32, height: u32) -> Option<ImageId> {
1525        // The overwhelmingly common refusal — a frame that filled the atlas
1526        // with its own draws — costs one pass over the entry map and no model
1527        // at all.
1528        if !self
1529            .entries
1530            .values()
1531            .any(|entry| entry.last_seen < self.frame)
1532        {
1533            return None;
1534        }
1535
1536        // Taken from the residency's own scratch, so the frame least able to
1537        // afford an allocation does not make one to evict with.
1538        let mut rects = std::mem::take(&mut self.pressure_rects);
1539        let mut order = std::mem::take(&mut self.pressure_order);
1540        let mut free_area = std::mem::take(&mut self.pressure_free_area);
1541        let mut layers = std::mem::take(&mut self.pressure_layers);
1542        let mut plan = std::mem::take(&mut self.pressure_plan);
1543
1544        self.model_layers(&mut rects, &mut order, &mut free_area);
1545        let fit = plan_pressure_eviction(
1546            width,
1547            height,
1548            self.budget.atlas_size,
1549            &mut rects,
1550            &order,
1551            &mut free_area,
1552            &mut layers,
1553        );
1554
1555        plan.clear();
1556        if let Some(layer) = fit {
1557            plan.extend(
1558                rects
1559                    .iter()
1560                    .filter(|rect| rect.freed && rect.layer == layer)
1561                    .map(|rect| rect.key),
1562            );
1563        }
1564
1565        let allocated = if plan.is_empty() {
1566            None
1567        } else {
1568            for key in &plan {
1569                self.release(*key);
1570                self.pressure_evictions = self.pressure_evictions.saturating_add(1);
1571                self.frame_pressure_evictions = self.frame_pressure_evictions.saturating_add(1);
1572            }
1573            self.cache.allocate(width, height, ATLAS_PADDING).ok()
1574        };
1575
1576        rects.clear();
1577        order.clear();
1578        free_area.clear();
1579        layers.clear();
1580        plan.clear();
1581        self.pressure_rects = rects;
1582        self.pressure_order = order;
1583        self.pressure_free_area = free_area;
1584        self.pressure_layers = layers;
1585        self.pressure_plan = plan;
1586        allocated
1587    }
1588
1589    /// Fill the planner's scratch with this residency's own view of the atlas.
1590    ///
1591    /// `rects` takes one entry per resident image, grouped by layer so a fit
1592    /// test reads one layer's slice; `order` takes the candidate indices, least
1593    /// recently seen first across the *whole* residency — [`plan_pressure_eviction`]
1594    /// is what regroups it per layer for its own bounded walk; `free_area` takes
1595    /// each layer's free area *as the packer reports it*, which is the one
1596    /// figure that accounts for the glyph pages this map cannot see.
1597    fn model_layers(
1598        &self,
1599        rects: &mut Vec<PressureRect>,
1600        order: &mut Vec<usize>,
1601        free_area: &mut Vec<u64>,
1602    ) {
1603        rects.clear();
1604        rects.extend(self.entries.iter().map(|(key, entry)| {
1605            let region = padded(entry.region, ATLAS_PADDING);
1606            PressureRect {
1607                key: *key,
1608                layer: region.layer,
1609                x0: region.offset[0],
1610                y0: region.offset[1],
1611                x1: region.offset[0].saturating_add(region.size[0]),
1612                y1: region.offset[1].saturating_add(region.size[1]),
1613                area: u64::from(region.size[0]).saturating_mul(u64::from(region.size[1])),
1614                last_seen: entry.last_seen,
1615                slot: entry.id.as_u32(),
1616                freed: false,
1617            }
1618        }));
1619        rects.sort_unstable_by_key(|rect| rect.layer);
1620
1621        order.clear();
1622        order.extend(
1623            rects
1624                .iter()
1625                .enumerate()
1626                .filter_map(|(index, rect)| (rect.last_seen < self.frame).then_some(index)),
1627        );
1628        order.sort_unstable_by_key(|index| {
1629            let rect = &rects[*index];
1630            (rect.last_seen, rect.slot, rect.key)
1631        });
1632
1633        free_area.clear();
1634        free_area.resize(self.cache.atlas_count(), 0);
1635        for (id, stats) in self.cache.atlas_manager().atlas_stats() {
1636            if let Some(free) = free_area.get_mut(id.as_u32() as usize) {
1637                *free = u64::from(stats.total_area.saturating_sub(stats.allocated_area));
1638            }
1639        }
1640    }
1641
1642    /// Count and report one source downsampled to fit the atlas.
1643    ///
1644    /// Once per blob id, at warning level: a minified image is a silent quality
1645    /// decision the application may not have intended (a full-resolution photo
1646    /// where a thumbnail was wanted), so it says so — but a scene redrawing it
1647    /// every frame must not repeat the message.
1648    fn note_minify(&mut self, key: u64, natural: [u32; 2], fitted: [u32; 2]) {
1649        self.minified = self.minified.saturating_add(1);
1650        if !self.reported_minify.insert(key) {
1651            return;
1652        }
1653        log::warn!(
1654            "image {}x{} is larger than the {}x{} atlas budget; uploading a {}x{} box-filtered \
1655             copy instead (reported once per image)",
1656            natural[0],
1657            natural[1],
1658            self.budget.atlas_size.0,
1659            self.budget.atlas_size.1,
1660            fitted[0],
1661            fitted[1],
1662        );
1663    }
1664
1665    /// Deallocate every entry unseen for [`MAX_UNSEEN_FRAMES`], recording each
1666    /// one's rectangle for clearing.
1667    ///
1668    /// The age half of the two bounds; the population half is
1669    /// [`allocate_under_pressure`](Self::allocate_under_pressure), which runs
1670    /// only when an allocation has already failed. Keeping them separate is
1671    /// what makes an idle screen give its texels back promptly whether or not
1672    /// anything is asking for them.
1673    fn reap(&mut self) {
1674        let mut reaped = std::mem::take(&mut self.reaped);
1675        reaped.clear();
1676        reaped.extend(self.entries.iter().filter_map(|(key, entry)| {
1677            (self.frame.saturating_sub(entry.last_seen) > MAX_UNSEEN_FRAMES).then_some(*key)
1678        }));
1679
1680        for key in reaped.drain(..) {
1681            self.release(key);
1682        }
1683        self.reaped = reaped;
1684    }
1685
1686    /// Drop `key`'s entry, freeing its atlas rectangle and recording it for
1687    /// clearing.
1688    ///
1689    /// The padded rectangle is what is cleared: an allocation reserves its
1690    /// padding too, so leaving the border behind would leave stale texels a
1691    /// later allocation could sample through.
1692    ///
1693    /// An upload still pending for that rectangle goes with the entry. It
1694    /// describes pixels no live entry claims any more, and writing them after
1695    /// the clear would put an evicted image back into a rectangle the packer has
1696    /// already handed to something else.
1697    fn release(&mut self, key: u64) {
1698        let Some(entry) = self.entries.remove(&key) else {
1699            return;
1700        };
1701        // The handle came from this cache's own `allocate`, so the atlas it
1702        // names exists and the deallocation cannot fail.
1703        self.cache.deallocate(entry.id);
1704        self.uploads.retain(|upload| upload.id != entry.id);
1705
1706        // The plan survives an unacknowledged frame, so the same rectangle can
1707        // reach here twice before a single clear has been issued for it; one
1708        // clear is what the second one would write anyway.
1709        let region = padded(entry.region, ATLAS_PADDING);
1710        if self.eviction_set.insert(region) {
1711            self.evictions.push(region);
1712        }
1713    }
1714}
1715
1716/// The most rectangles one refused allocation may plan to take back from a
1717/// single atlas layer.
1718///
1719/// A bound on the *search*, not only on the damage: a layer whose walk
1720/// reaches it without finding a fit contributes nothing to the plan, so the
1721/// cost of one hopeless layer stays bounded rather than eating into the
1722/// allowance a later, satisfiable layer needs to prove itself. Sixty-four is
1723/// far past what any satisfiable request has needed — a fit is normally found
1724/// in one, since the rectangle handed back is usually the size of the one
1725/// being asked for — while still being a small fraction of what a
1726/// mobile-tier layer can hold. Applied once per layer a refused allocation
1727/// considers, never once across every layer together — see
1728/// [`plan_pressure_eviction`].
1729const MAX_PRESSURE_EVICTION_CANDIDATES: usize = 64;
1730
1731/// How much area one refused allocation may plan to free from a single atlas
1732/// layer, as a multiple of the area it asked for.
1733///
1734/// The other half of the bound, and the one that scales with the request: a
1735/// layer that has already given back four times the texels the request wants
1736/// and still has nowhere to put them is fighting fragmentation, not scarcity,
1737/// and freeing more of that same layer will not fix it. Applied per layer,
1738/// the same way [`MAX_PRESSURE_EVICTION_CANDIDATES`] is.
1739const PRESSURE_EVICTION_AREA_BUDGET: u64 = 4;
1740
1741/// One resident rectangle as the pressure planner sees it.
1742///
1743/// A flat copy rather than a borrow of the entry map, because the plan is drawn
1744/// up while the map is still intact and the release that follows needs it
1745/// mutably. `x1`/`y1` are exclusive, and every field is the *padded* allocation
1746/// — the rectangle the packer actually holds — rather than the image's own.
1747#[derive(Debug, Clone, Copy)]
1748struct PressureRect {
1749    /// Blob key of the entry holding this rectangle.
1750    key: u64,
1751    /// Array layer the rectangle sits in.
1752    layer: u32,
1753    /// Left edge, inclusive.
1754    x0: u32,
1755    /// Top edge, inclusive.
1756    y0: u32,
1757    /// Right edge, exclusive.
1758    x1: u32,
1759    /// Bottom edge, exclusive.
1760    y1: u32,
1761    /// Texels the rectangle covers.
1762    area: u64,
1763    /// The entry's frame stamp, which is what makes it a candidate or not.
1764    last_seen: u64,
1765    /// The entry's slot index, which breaks a tie between two entries last
1766    /// drawn on the same frame.
1767    slot: u32,
1768    /// Whether the plan under construction has given this rectangle back.
1769    freed: bool,
1770}
1771
1772/// The layer a bounded prefix of `order` would make room in, or `None`.
1773///
1774/// Frees nothing: it marks `rects` and grows `free_area` in the model only, so
1775/// a request no prefix satisfies leaves the residency untouched. `order` is
1776/// least-recently-seen-first across the *whole* residency; this regroups it
1777/// one layer at a time — `layers` scratch, cleared and refilled here, in the
1778/// order each layer's stalest candidate appears in `order` — and gives each
1779/// layer its own [`MAX_PRESSURE_EVICTION_CANDIDATES`]/
1780/// [`PRESSURE_EVICTION_AREA_BUDGET`] allowance rather than pooling one across
1781/// every layer. Without that split, a layer with many candidates that could
1782/// never admit the request (too fragmented, or blocked by an occupant no
1783/// eviction touches) spends the whole budget before a later, satisfiable
1784/// layer is ever tried — the request is then refused even though a bounded
1785/// eviction on that later layer would have placed it. See
1786/// [`ImageResidency::allocate_under_pressure`] for the whole shape and for why
1787/// the answer is a layer rather than a bool — only the planned rectangles on
1788/// the fitting layer are worth releasing.
1789fn plan_pressure_eviction(
1790    width: u32,
1791    height: u32,
1792    atlas_size: (u32, u32),
1793    rects: &mut [PressureRect],
1794    order: &[usize],
1795    free_area: &mut [u64],
1796    layers: &mut Vec<u32>,
1797) -> Option<u32> {
1798    let padding = u32::from(ATLAS_PADDING).saturating_mul(2);
1799    let request_w = width.saturating_add(padding);
1800    let request_h = height.saturating_add(padding);
1801    let request_area = u64::from(request_w).saturating_mul(u64::from(request_h));
1802    let area_budget = request_area.saturating_mul(PRESSURE_EVICTION_AREA_BUDGET);
1803
1804    layers.clear();
1805    for &index in order {
1806        let Some(rect) = rects.get(index) else {
1807            continue;
1808        };
1809        if !layers.contains(&rect.layer) {
1810            layers.push(rect.layer);
1811        }
1812    }
1813
1814    for &layer in layers.iter() {
1815        let mut layer_freed_area = 0_u64;
1816        let mut step = 0_usize;
1817
1818        for &index in order {
1819            let on_this_layer = rects.get(index).is_some_and(|rect| rect.layer == layer);
1820            if !on_this_layer {
1821                continue;
1822            }
1823            if step >= MAX_PRESSURE_EVICTION_CANDIDATES || layer_freed_area > area_budget {
1824                break;
1825            }
1826            step += 1;
1827
1828            let area = {
1829                let Some(rect) = rects.get_mut(index) else {
1830                    continue;
1831                };
1832                rect.freed = true;
1833                rect.area
1834            };
1835            layer_freed_area = layer_freed_area.saturating_add(area);
1836
1837            let Some(free) = free_area.get_mut(layer as usize) else {
1838                continue;
1839            };
1840            *free = free.saturating_add(area);
1841            // Area first because it is exact — it comes off the packer's own
1842            // accounting, so it counts the glyph pages `layer_admits` cannot see —
1843            // and because it is the cheaper of the two questions.
1844            if *free >= request_area && layer_admits(rects, layer, request_w, request_h, atlas_size)
1845            {
1846                return Some(layer);
1847            }
1848        }
1849    }
1850
1851    None
1852}
1853
1854/// Whether a `width` x `height` rectangle could still be placed in `layer` once
1855/// every rectangle the plan has marked freed is out of the way.
1856///
1857/// A *necessary* condition rather than an exact packing answer. Any placement
1858/// has to avoid each occupant whole, so it must lie entirely to one side of it:
1859/// if some occupant leaves less than the requested extent on all four sides,
1860/// there is nowhere for the rectangle to go and the plan cannot succeed. A plan
1861/// this refuses is therefore certainly hopeless; one it admits may still be
1862/// refused by the packer, which is the direction that costs a bounded number of
1863/// evictions rather than a wrong picture (see
1864/// [`ImageResidency::allocate_under_pressure`]).
1865///
1866/// `rects` is grouped by layer, so this reads one layer's slice rather than the
1867/// whole residency.
1868fn layer_admits(
1869    rects: &[PressureRect],
1870    layer: u32,
1871    width: u32,
1872    height: u32,
1873    atlas_size: (u32, u32),
1874) -> bool {
1875    if width > atlas_size.0 || height > atlas_size.1 {
1876        return false;
1877    }
1878
1879    let start = rects.partition_point(|rect| rect.layer < layer);
1880    let end = rects.partition_point(|rect| rect.layer <= layer);
1881    rects[start..end]
1882        .iter()
1883        .filter(|rect| !rect.freed)
1884        .all(|rect| {
1885            width <= rect.x0
1886                || width <= atlas_size.0.saturating_sub(rect.x1)
1887                || height <= rect.y0
1888                || height <= atlas_size.1.saturating_sub(rect.y1)
1889        })
1890}
1891
1892/// The extent `natural` is stored at inside an `atlas`-sized layer: itself when
1893/// it already fits, and otherwise the largest rectangle of the same aspect ratio
1894/// that does.
1895///
1896/// The scale is taken as the smaller of the two axis ratios and applied to both,
1897/// so the two axes of the paint transform stay in step — a per-axis fit would
1898/// stretch the image. Each axis is floored (never rounding *up* past the layer)
1899/// and then held at one texel: a source that scales below a whole texel still
1900/// has to have somewhere to be, and a zero-extent allocation is not a rectangle
1901/// the packer or the shader can address.
1902#[must_use]
1903pub fn fit_extent(natural: [u32; 2], atlas: (u32, u32)) -> (u32, u32) {
1904    let (width, height) = (natural[0], natural[1]);
1905    if width <= atlas.0 && height <= atlas.1 {
1906        return (width, height);
1907    }
1908
1909    let scale = (f64::from(atlas.0) / f64::from(width.max(1)))
1910        .min(f64::from(atlas.1) / f64::from(height.max(1)));
1911    let axis = |value: u32, ceiling: u32| {
1912        let scaled = (f64::from(value) * scale).floor();
1913        // `as` on a non-finite or out-of-range float saturates in Rust, so the
1914        // clamps below are the whole check rather than a second line of defence.
1915        (scaled as u32).clamp(1, ceiling.max(1))
1916    };
1917
1918    (axis(width, atlas.0), axis(height, atlas.1))
1919}
1920
1921/// `source` box-filtered down to `width` x `height`.
1922///
1923/// Every output texel is the unweighted mean of the half-open source rectangle
1924/// it covers, computed on the premultiplied bytes the atlas stores — which is
1925/// the space the samples are composited in, so averaging there is what keeps a
1926/// partially transparent source from bleeding its colour outward.
1927///
1928/// Minification only: a request at or above the source's own extent copies the
1929/// texels straight across rather than inventing any, since a box filter has no
1930/// magnification arm and the caller never asks for one ([`fit_extent`] only ever
1931/// shrinks).
1932#[must_use]
1933pub fn minify(source: &Pixmap, width: u32, height: u32) -> Pixmap {
1934    let source_w = u32::from(source.width());
1935    let source_h = u32::from(source.height());
1936    let width = width.clamp(1, source_w.max(1));
1937    let height = height.clamp(1, source_h.max(1));
1938
1939    let texels = source.data();
1940    let mut out = Vec::with_capacity((width as usize).saturating_mul(height as usize));
1941    let mut may_have_transparency = false;
1942
1943    for y in 0..height {
1944        // Half-open source rows, derived from the output row rather than
1945        // accumulated, so rounding never leaves a gap or an overlap between
1946        // consecutive rows.
1947        let y0 = (u64::from(y) * u64::from(source_h) / u64::from(height)) as u32;
1948        let y1 = ((u64::from(y) + 1) * u64::from(source_h) / u64::from(height)) as u32;
1949        let y1 = y1.max(y0.saturating_add(1)).min(source_h);
1950
1951        for x in 0..width {
1952            let x0 = (u64::from(x) * u64::from(source_w) / u64::from(width)) as u32;
1953            let x1 = ((u64::from(x) + 1) * u64::from(source_w) / u64::from(width)) as u32;
1954            let x1 = x1.max(x0.saturating_add(1)).min(source_w);
1955
1956            let mut sum = [0_u64; 4];
1957            let mut count = 0_u64;
1958            for row in y0..y1 {
1959                let base = (row as usize).saturating_mul(source_w as usize);
1960                for column in x0..x1 {
1961                    let Some(texel) = texels.get(base.saturating_add(column as usize)) else {
1962                        continue;
1963                    };
1964                    sum[0] += u64::from(texel.r);
1965                    sum[1] += u64::from(texel.g);
1966                    sum[2] += u64::from(texel.b);
1967                    sum[3] += u64::from(texel.a);
1968                    count += 1;
1969                }
1970            }
1971
1972            // A count of zero is unreachable — both ranges are non-empty by
1973            // construction — and flooring it at one rather than branching keeps
1974            // the divide-by-zero panic off the frame path (E17) while answering
1975            // a transparent texel if it ever were reachable, since the sums
1976            // would then be zero too.
1977            let divisor = count.max(1);
1978            // Round to nearest rather than truncating: a uniform source has to
1979            // come back bit-identical, which truncating an exact integer mean
1980            // already gives, but a near-uniform one must not drift a level
1981            // darker.
1982            let mean = |channel: usize| {
1983                ((sum[channel] + divisor / 2) / divisor).min(u64::from(u8::MAX)) as u8
1984            };
1985
1986            let texel = PremulRgba8 {
1987                r: mean(0),
1988                g: mean(1),
1989                b: mean(2),
1990                a: mean(3),
1991            };
1992            may_have_transparency |= texel.a != u8::MAX;
1993            out.push(texel);
1994        }
1995    }
1996
1997    // Both extents were clamped into `1..=u16::MAX` above (a source pixmap's own
1998    // extents are `u16`), so neither conversion can lose information.
1999    Pixmap::from_parts_with_opacity(
2000        out,
2001        width.min(u32::from(u16::MAX)) as u16,
2002        height.min(u32::from(u16::MAX)) as u16,
2003        may_have_transparency,
2004    )
2005}
2006
2007/// `axis` held at or below `ceiling`, and never below the one texel an
2008/// addressable rectangle needs.
2009fn fit_axis(axis: u32, ceiling: u64) -> u32 {
2010    axis.min(u32::try_from(ceiling).unwrap_or(u32::MAX)).max(1)
2011}
2012
2013/// `region` grown by `padding` on every side, clamped at the layer origin.
2014fn padded(region: AtlasRegion, padding: u16) -> AtlasRegion {
2015    let padding = u32::from(padding);
2016    let pad_x = padding.min(region.offset[0]);
2017    let pad_y = padding.min(region.offset[1]);
2018
2019    AtlasRegion {
2020        layer: region.layer,
2021        offset: [region.offset[0] - pad_x, region.offset[1] - pad_y],
2022        size: [
2023            region.size[0].saturating_add(pad_x + padding),
2024            region.size[1].saturating_add(pad_y + padding),
2025        ],
2026    }
2027}
2028
2029/// Refuse an image `vello_common`'s own conversion would assert on.
2030///
2031/// The three conditions are exactly the three assertions downstream: the
2032/// format `unimplemented!()`, the `u16::MAX` dimension check, and
2033/// `Pixmap::from_parts_with_opacity`'s length equality. Checking them here is
2034/// what keeps the panic off the frame path.
2035fn check_supported(data: &ImageData) -> Result<(), ImageSkip> {
2036    match data.format {
2037        ImageFormat::Rgba8 | ImageFormat::Bgra8 => {}
2038        format => return Err(ImageSkip::UnsupportedFormat(format)),
2039    }
2040
2041    if data.width == 0 || data.height == 0 {
2042        return Err(ImageSkip::DegenerateExtent {
2043            width: data.width,
2044            height: data.height,
2045        });
2046    }
2047
2048    if data.width > MAX_IMAGE_DIMENSION || data.height > MAX_IMAGE_DIMENSION {
2049        return Err(ImageSkip::TooLarge {
2050            width: data.width,
2051            height: data.height,
2052            max: MAX_IMAGE_DIMENSION,
2053        });
2054    }
2055
2056    let actual = data.data.data().len();
2057    let expected = data
2058        .format
2059        .size_in_bytes(data.width, data.height)
2060        .unwrap_or(usize::MAX);
2061    if actual != expected {
2062        return Err(ImageSkip::MalformedPixels {
2063            width: data.width,
2064            height: data.height,
2065            expected,
2066            actual,
2067        });
2068    }
2069
2070    Ok(())
2071}
2072
2073#[cfg(test)]
2074mod tests {
2075    use super::*;
2076    use peniko::{Blob, ImageAlphaType};
2077
2078    fn image(width: u32, height: u32) -> ImageData {
2079        let len = (width as usize) * (height as usize) * 4;
2080        ImageData {
2081            data: Blob::new(Arc::new(vec![255_u8; len])),
2082            format: ImageFormat::Rgba8,
2083            alpha_type: ImageAlphaType::Alpha,
2084            width,
2085            height,
2086        }
2087    }
2088
2089    fn residency() -> ImageResidency {
2090        ImageResidency::new(AtlasBudget {
2091            atlas_size: (64, 64),
2092            max_atlases: 2,
2093        })
2094    }
2095
2096    #[test]
2097    fn a_mobile_budget_is_never_the_vello_default() {
2098        let default = AtlasConfig::default();
2099        for budget in [AtlasBudget::MOBILE, AtlasBudget::DESKTOP] {
2100            assert_ne!(budget.atlas_size, default.atlas_size);
2101            assert!(budget.total_bytes() < 4096 * 4096 * ATLAS_FORMAT_BYTES * 8);
2102        }
2103        assert_eq!(AtlasBudget::MOBILE.total_bytes(), 16 << 20);
2104        assert_eq!(AtlasBudget::DESKTOP.total_bytes(), 128 << 20);
2105    }
2106
2107    #[test]
2108    fn a_downlevel_adapter_takes_the_mobile_budget() {
2109        let webgl2 = TierCaps::fake(DownlevelProfile::WebGl2);
2110        assert!(is_mobile_tier(&webgl2));
2111
2112        let mut tiler = TierCaps::fake(DownlevelProfile::Full);
2113        assert!(!is_mobile_tier(&tiler));
2114        tiler.transient_saves_memory = true;
2115        assert!(is_mobile_tier(&tiler));
2116    }
2117
2118    fn named(name: &str) -> TierCaps {
2119        let mut caps = TierCaps::fake(DownlevelProfile::Full);
2120        caps.adapter_name = name.to_string();
2121        caps
2122    }
2123
2124    #[test]
2125    fn the_adapter_table_names_the_mobile_and_desktop_families() {
2126        for name in [
2127            "Adreno (TM) 618",
2128            "Adreno (TM) 660",
2129            "Mali-G78 MP14",
2130            "Immortalis-G715",
2131            "PowerVR B-Series BXM-8-256",
2132            "Samsung Xclipse 920",
2133            "Maleoon 910",
2134            "Apple A15 GPU",
2135            "NVIDIA Tegra X1",
2136        ] {
2137            assert_eq!(adapter_family(name), Some(AdapterFamily::Mobile), "{name}");
2138        }
2139        for name in [
2140            "NVIDIA T400",
2141            "NVIDIA GeForce RTX 4090",
2142            "Quadro P1000",
2143            "AMD Radeon RX 7900 XTX",
2144            "Intel(R) UHD Graphics 770",
2145        ] {
2146            assert_eq!(adapter_family(name), Some(AdapterFamily::Desktop), "{name}");
2147        }
2148        for name in ["llvmpipe (LLVM 19.1.7, 256 bits)", "fake-full", ""] {
2149            assert_eq!(adapter_family(name), None, "{name}");
2150        }
2151    }
2152
2153    #[test]
2154    fn the_tier_table_overrides_a_mobile_driver_that_answers_like_a_desktop() {
2155        const GIB: u64 = 1 << 30;
2156        // Adreno 618/620 answer full profile and no transient savings — the
2157        // desktop answers — and used to take the desktop budget on them.
2158        let adreno = named("Adreno (TM) 618");
2159        assert!(!is_mobile_tier(&adreno));
2160
2161        let small = choose_tier(&adreno, Some(3 * GIB));
2162        assert_eq!(
2163            (small.tier, small.rule),
2164            (AtlasTier::Mobile, TierRule::Table)
2165        );
2166        assert_eq!(AtlasBudget::for_choice(small, &adreno), AtlasBudget::MOBILE);
2167
2168        let unknown = choose_tier(&adreno, None);
2169        assert_eq!(
2170            unknown.tier,
2171            AtlasTier::Mobile,
2172            "unknown memory earns nothing"
2173        );
2174
2175        let large = choose_tier(&adreno, Some(6 * GIB));
2176        assert_eq!(
2177            (large.tier, large.rule),
2178            (AtlasTier::MobileLarge, TierRule::Table)
2179        );
2180        assert_eq!(
2181            AtlasBudget::for_choice(large, &adreno),
2182            AtlasBudget::MOBILE_LARGE_WIDE
2183        );
2184        assert_eq!(
2185            AtlasBudget::for_choice(choose_tier(&adreno, Some(MOBILE_LARGE_MIN_MEMORY)), &adreno),
2186            AtlasBudget::MOBILE_LARGE_WIDE,
2187            "the threshold is inclusive"
2188        );
2189        assert_eq!(AtlasBudget::MOBILE_LARGE_WIDE.total_bytes(), 64 << 20);
2190        assert_eq!(AtlasBudget::MOBILE_LARGE_DEEP.total_bytes(), 64 << 20);
2191
2192        // A desktop family stays on the desktop budget whatever it answers.
2193        let mut nvidia = named("NVIDIA T400");
2194        nvidia.transient_saves_memory = true;
2195        let desktop = choose_tier(&nvidia, Some(GIB));
2196        assert_eq!(
2197            (desktop.tier, desktop.rule),
2198            (AtlasTier::Desktop, TierRule::Table)
2199        );
2200    }
2201
2202    #[test]
2203    fn the_large_mobile_shape_is_the_one_the_adapter_can_create() {
2204        let mut caps = named("Mali-G78 MP14");
2205        assert_eq!(
2206            AtlasBudget::mobile_large(&caps),
2207            Some(AtlasBudget::MOBILE_LARGE_WIDE)
2208        );
2209
2210        // An edge short of 2048 takes the deep shape instead.
2211        caps.max_texture_dimension_2d = 1024;
2212        assert_eq!(
2213            AtlasBudget::mobile_large(&caps),
2214            Some(AtlasBudget::MOBILE_LARGE_DEEP)
2215        );
2216
2217        // Neither shape fits: the tier is the plain mobile one, never a large
2218        // budget clamped below it.
2219        caps.max_texture_array_layers = 8;
2220        assert_eq!(AtlasBudget::mobile_large(&caps), None);
2221        let choice = choose_tier(&caps, Some(16 << 30));
2222        assert_eq!(choice.tier, AtlasTier::Mobile);
2223        assert_eq!(AtlasBudget::for_choice(choice, &caps), AtlasBudget::MOBILE);
2224    }
2225
2226    #[test]
2227    fn an_unnamed_adapter_keeps_the_capability_read_and_downlevel_wins_over_the_table() {
2228        let unnamed = TierCaps::fake(DownlevelProfile::Full);
2229        let choice = choose_tier(&unnamed, Some(64 << 30));
2230        assert_eq!(
2231            (choice.tier, choice.rule),
2232            (AtlasTier::Desktop, TierRule::Heuristic)
2233        );
2234
2235        let mut tiler = unnamed.clone();
2236        tiler.transient_saves_memory = true;
2237        let choice = choose_tier(&tiler, Some(64 << 30));
2238        assert_eq!(
2239            (choice.tier, choice.rule),
2240            (AtlasTier::Mobile, TierRule::Heuristic),
2241            "the heuristic never reaches the large tier"
2242        );
2243
2244        let mut gl_nvidia = TierCaps::fake(DownlevelProfile::WebGl2);
2245        gl_nvidia.adapter_name = "ANGLE (NVIDIA, NVIDIA T400)".to_string();
2246        let choice = choose_tier(&gl_nvidia, Some(64 << 30));
2247        assert_eq!(
2248            (choice.tier, choice.rule),
2249            (AtlasTier::Mobile, TierRule::Downlevel)
2250        );
2251    }
2252
2253    #[test]
2254    fn mem_total_is_read_in_bytes_from_meminfo() {
2255        let meminfo = "MemTotal:        5768520 kB\nMemFree:          123456 kB\n";
2256        assert_eq!(parse_mem_total(meminfo), Some(5_768_520 * 1024));
2257        assert_eq!(parse_mem_total("MemFree: 1 kB\n"), None);
2258        assert_eq!(parse_mem_total("MemTotal: lots\n"), None);
2259        assert_eq!(parse_mem_total("MemTotal: 1024 MB\n"), None);
2260    }
2261
2262    #[test]
2263    fn a_budget_is_clamped_to_the_adapters_own_ceilings() {
2264        let mut caps = TierCaps::fake(DownlevelProfile::Full);
2265        caps.max_texture_dimension_2d = 1024;
2266        caps.max_texture_array_layers = 2;
2267
2268        let budget = AtlasBudget::DESKTOP.clamped(&caps);
2269        assert_eq!(budget.atlas_size, (1024, 1024));
2270        assert_eq!(budget.max_atlases, 2);
2271
2272        // The eight-bit `atlas_index` field is a ceiling of its own.
2273        caps.max_texture_array_layers = 4096;
2274        let budget = AtlasBudget {
2275            atlas_size: (256, 256),
2276            max_atlases: 4096,
2277        }
2278        .clamped(&caps);
2279        assert_eq!(budget.max_atlases, MAX_ATLAS_LAYERS);
2280    }
2281
2282    #[test]
2283    fn the_first_layer_is_created_lazily() {
2284        let residency = residency();
2285        assert_eq!(residency.layers(), 0);
2286        assert_eq!(residency.entry_count(), 0);
2287    }
2288
2289    #[test]
2290    fn one_image_over_many_frames_uploads_exactly_once() {
2291        let mut residency = residency();
2292        let data = image(8, 8);
2293        let mut uploads = 0;
2294
2295        for _ in 0..60 {
2296            residency.begin_frame();
2297            residency.resolve(&data).expect("8x8 fits a 64x64 atlas");
2298            uploads += residency.uploads().len();
2299            // Every frame here is a frame that reached the atlas.
2300            residency.acknowledge_plan();
2301        }
2302
2303        assert_eq!(uploads, 1, "residency survives every frame that draws it");
2304        assert_eq!(residency.entry_count(), 1);
2305        assert_eq!(residency.layers(), 1);
2306    }
2307
2308    #[test]
2309    fn an_unseen_image_is_reaped_and_its_padded_region_cleared() {
2310        let mut residency = residency();
2311        let data = image(8, 8);
2312
2313        residency.begin_frame();
2314        let resident = residency.resolve(&data).expect("fits");
2315        residency.acknowledge_plan();
2316
2317        for _ in 0..MAX_UNSEEN_FRAMES {
2318            residency.begin_frame();
2319            assert!(residency.evictions().is_empty(), "still inside the window");
2320        }
2321
2322        residency.begin_frame();
2323        assert_eq!(residency.entry_count(), 0);
2324        assert_eq!(
2325            residency.evictions(),
2326            &[padded(resident.region, ATLAS_PADDING)]
2327        );
2328
2329        // The freed rectangle is available again, and the reallocation
2330        // schedules a fresh upload.
2331        residency.resolve(&data).expect("fits");
2332        assert_eq!(residency.uploads().len(), 1);
2333    }
2334
2335    #[test]
2336    fn an_image_larger_than_the_atlas_is_minified_to_fit_rather_than_skipped() {
2337        let mut residency = residency();
2338        residency.begin_frame();
2339
2340        // Twice the layer's width at a 16:1 aspect ratio: the width sets the
2341        // scale, and the height follows it rather than being fitted on its own.
2342        let resident = residency.resolve(&image(128, 8)).expect("minified to fit");
2343
2344        assert_eq!(resident.region.size, [64, 4]);
2345        assert_eq!(resident.natural, [128, 8]);
2346        assert_eq!(resident.minify_scale(), Some((0.5, 0.5)));
2347        assert_eq!(residency.minified(), 1);
2348        assert_eq!(residency.skipped(), 0, "a big image is not a skipped one");
2349        assert_eq!(residency.uploads().len(), 1);
2350        let upload = &residency.uploads()[0];
2351        assert_eq!(
2352            (upload.pixels.width(), upload.pixels.height()),
2353            (64, 4),
2354            "the upload carries exactly the region's texels"
2355        );
2356    }
2357
2358    #[test]
2359    fn an_atlas_with_no_room_left_is_still_a_skip() {
2360        // Minification fits an image to a LAYER, not to the space left in one:
2361        // a budget already full refuses the next allocation as it always did.
2362        let mut residency = ImageResidency::new(AtlasBudget {
2363            atlas_size: (64, 64),
2364            max_atlases: 1,
2365        });
2366        residency.begin_frame();
2367        residency
2368            .resolve(&image(64, 64))
2369            .expect("fills the one layer");
2370
2371        let skip = residency
2372            .resolve(&image(32, 32))
2373            .expect_err("nothing is left to allocate from");
2374        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
2375        assert_eq!(residency.skipped(), 1);
2376        assert_eq!(residency.minified(), 0);
2377    }
2378
2379    #[test]
2380    fn a_minified_image_stays_resident_across_frames_at_its_declared_extent() {
2381        // Residency is keyed on the SOURCE extent, not the resident one: an
2382        // entry matched against its own minified rectangle would miss every
2383        // frame and re-upload the image on each of them.
2384        let mut residency = residency();
2385        let data = image(128, 8);
2386
2387        for _ in 0..8 {
2388            residency.begin_frame();
2389            let resident = residency.resolve(&data).expect("minified to fit");
2390            assert_eq!(resident.natural, [128, 8]);
2391            residency.acknowledge_plan();
2392        }
2393
2394        assert_eq!(residency.entry_count(), 1);
2395        assert_eq!(
2396            residency.minified(),
2397            1,
2398            "downsampled once, not once a frame"
2399        );
2400        assert!(
2401            residency.uploads().is_empty(),
2402            "no re-upload after the first"
2403        );
2404    }
2405
2406    #[test]
2407    fn a_fit_extent_preserves_the_aspect_ratio_and_never_grows() {
2408        let atlas = (64, 64);
2409        // Already inside the layer: unchanged, both axes.
2410        assert_eq!(fit_extent([64, 64], atlas), (64, 64));
2411        assert_eq!(fit_extent([8, 4], atlas), (8, 4));
2412
2413        // The `adv-huge-image` shape, at this budget's scale.
2414        assert_eq!(fit_extent([5000, 5000], (2048, 2048)), (2048, 2048));
2415
2416        // The tighter axis sets the scale for both.
2417        assert_eq!(fit_extent([128, 8], atlas), (64, 4));
2418        assert_eq!(fit_extent([8, 128], atlas), (4, 64));
2419        assert_eq!(fit_extent([1000, 10], atlas), (64, 1));
2420
2421        // A non-square layer is fitted per axis and still uniformly scaled.
2422        assert_eq!(fit_extent([400, 400], (100, 50)), (50, 50));
2423    }
2424
2425    #[test]
2426    fn an_extreme_aspect_ratio_still_fits_at_one_texel_rather_than_none() {
2427        // 10000:1 into a 64-square layer scales the short axis to 0.0064 texels.
2428        // A zero extent is not a rectangle the packer or the shader can
2429        // address, so it is held at one.
2430        let fitted = fit_extent([10_000, 1], (64, 64));
2431        assert_eq!(fitted, (64, 1));
2432
2433        let mut residency = residency();
2434        residency.begin_frame();
2435        let resident = residency
2436            .resolve(&image(10_000, 1))
2437            .expect("a sliver still becomes a rectangle");
2438        assert_eq!(resident.region.size, [64, 1]);
2439    }
2440
2441    #[test]
2442    fn a_uniform_source_minifies_to_exactly_its_own_colour() {
2443        // What `adv-huge-image` rests on: a uniform source comes back bit
2444        // identical whatever the scale, so the case's `Exact` probe is a
2445        // property of the filter rather than a tolerance.
2446        let texel = PremulRgba8 {
2447            r: 255,
2448            g: 0,
2449            b: 255,
2450            a: 255,
2451        };
2452        let source = Pixmap::from_parts_with_opacity(vec![texel; 100 * 100], 100, 100, false);
2453
2454        let small = minify(&source, 7, 3);
2455        assert_eq!((small.width(), small.height()), (7, 3));
2456        assert!(small.data().iter().all(|out| *out == texel));
2457        assert!(
2458            !small.may_have_transparency(),
2459            "an opaque source stays opaque"
2460        );
2461    }
2462
2463    #[test]
2464    fn a_box_filter_averages_the_source_texels_each_output_covers() {
2465        // A 2x1 source of black and white halves down to one texel, which must
2466        // be their mean rather than either end.
2467        let black = PremulRgba8 {
2468            r: 0,
2469            g: 0,
2470            b: 0,
2471            a: 255,
2472        };
2473        let white = PremulRgba8 {
2474            r: 255,
2475            g: 255,
2476            b: 255,
2477            a: 255,
2478        };
2479        let source = Pixmap::from_parts_with_opacity(vec![black, white], 2, 1, false);
2480
2481        let small = minify(&source, 1, 1);
2482        assert_eq!((small.width(), small.height()), (1, 1));
2483        // 255 / 2 rounded to nearest.
2484        assert_eq!(small.data()[0].r, 128);
2485        assert_eq!(small.data()[0].a, 255);
2486
2487        // Transparency is recomputed from the result, not inherited: averaging
2488        // an opaque and a fully transparent texel produces a partial one.
2489        let clear = PremulRgba8 {
2490            r: 0,
2491            g: 0,
2492            b: 0,
2493            a: 0,
2494        };
2495        let mixed = Pixmap::from_parts_with_opacity(vec![white, clear], 2, 1, true);
2496        let small = minify(&mixed, 1, 1);
2497        assert_eq!(small.data()[0].a, 128);
2498        assert!(small.may_have_transparency());
2499    }
2500
2501    #[test]
2502    fn minify_never_magnifies() {
2503        let texel = PremulRgba8 {
2504            r: 1,
2505            g: 2,
2506            b: 3,
2507            a: 255,
2508        };
2509        let source = Pixmap::from_parts_with_opacity(vec![texel; 4], 2, 2, false);
2510
2511        // A request above the source's own extent is clamped to it rather than
2512        // inventing texels the box filter has no arm for.
2513        let same = minify(&source, 8, 8);
2514        assert_eq!((same.width(), same.height()), (2, 2));
2515        assert!(same.data().iter().all(|out| *out == texel));
2516    }
2517
2518    #[test]
2519    fn a_malformed_or_degenerate_image_is_refused_before_the_conversion() {
2520        let mut short = image(4, 4);
2521        short.data = Blob::new(Arc::new(vec![0_u8; 8]));
2522        assert!(matches!(
2523            check_supported(&short),
2524            Err(ImageSkip::MalformedPixels { .. })
2525        ));
2526
2527        assert!(matches!(
2528            check_supported(&image(0, 4)),
2529            Err(ImageSkip::DegenerateExtent { .. })
2530        ));
2531
2532        let mut huge = image(1, 1);
2533        huge.width = MAX_IMAGE_DIMENSION + 1;
2534        assert!(matches!(
2535            check_supported(&huge),
2536            Err(ImageSkip::TooLarge { .. })
2537        ));
2538    }
2539
2540    #[test]
2541    fn two_images_share_a_layer_and_hold_distinct_rectangles() {
2542        let mut residency = residency();
2543        residency.begin_frame();
2544
2545        let first = residency.resolve(&image(8, 8)).expect("fits");
2546        let second = residency.resolve(&image(16, 16)).expect("fits");
2547
2548        assert_ne!(first.id, second.id);
2549        assert_eq!(first.region.layer, second.region.layer);
2550        assert_ne!(first.region.offset, second.region.offset);
2551        assert_eq!(residency.uploads().len(), 2);
2552        assert_eq!(residency.layers(), 1);
2553    }
2554
2555    #[test]
2556    fn a_second_layer_is_created_when_the_first_is_full() {
2557        let mut residency = residency();
2558        residency.begin_frame();
2559
2560        // Two 64x48 images cannot share one 64x64 layer.
2561        let first = residency.resolve(&image(64, 48)).expect("fits a layer");
2562        let second = residency.resolve(&image(64, 48)).expect("fits the next");
2563
2564        assert_eq!(first.region.layer, 0);
2565        assert_eq!(second.region.layer, 1);
2566        assert_eq!(residency.layers(), 2);
2567    }
2568
2569    #[test]
2570    fn a_slot_taken_through_the_shared_allocator_is_a_distinct_id_on_the_same_budget() {
2571        // The allocator half of the glyph/image seam, stated without the glyph
2572        // policy: whatever else packs into this cache draws from the same id
2573        // space and the same layer allowance, and its pages are visible in the
2574        // depth the array is built to.
2575        let mut residency = residency();
2576        residency.begin_frame();
2577        let resident = residency.resolve(&image(64, 48)).expect("fills a layer");
2578
2579        // Big enough that it cannot share the layer the image took, so it must
2580        // create the second one rather than report the first.
2581        let borrowed = residency
2582            .allocator_mut()
2583            .allocate(64, 48, ATLAS_PADDING)
2584            .expect("the second layer is free");
2585
2586        assert_ne!(
2587            resident.id, borrowed,
2588            "one cache never hands the same slot index to two live occupants"
2589        );
2590        assert_eq!(
2591            residency.layers(),
2592            2,
2593            "a layer created by the other class still has to be in the array"
2594        );
2595        assert_eq!(
2596            residency
2597                .allocator()
2598                .get(resident.id)
2599                .map(|resource| resource.size()),
2600            Some(resident.region.size),
2601            "and neither allocation disturbed the other's rectangle"
2602        );
2603    }
2604
2605    #[test]
2606    fn a_disabled_residency_refuses_every_image_and_touches_no_atlas() {
2607        let mut residency = ImageResidency::disabled(AtlasBudget::MOBILE);
2608        assert!(residency.is_disabled());
2609
2610        residency.begin_frame();
2611        let skip = residency
2612            .resolve(&image(8, 8))
2613            .expect_err("the kill switch refuses every image");
2614
2615        assert_eq!(skip, ImageSkip::AtlasDisabled);
2616        assert_eq!(residency.entry_count(), 0);
2617        assert_eq!(residency.layers(), 0, "no atlas layer is ever created");
2618        assert!(residency.uploads().is_empty());
2619        assert_eq!(residency.skipped(), 1);
2620    }
2621
2622    #[test]
2623    fn an_enabled_residency_is_what_an_unset_kill_switch_produces() {
2624        // The default test environment leaves `FRUST_ENGINE_NO_ATLAS` unset, so
2625        // this pins that `new` consults the switch rather than hardcoding
2626        // either answer; the disabled half is pinned above through the
2627        // constructor the switch selects.
2628        assert!(!ImageResidency::new(AtlasBudget::MOBILE).is_disabled());
2629    }
2630
2631    #[test]
2632    fn reading_the_plan_leaves_it_pending_and_acknowledging_it_clears_it() {
2633        let mut residency = residency();
2634        residency.begin_frame();
2635        residency.resolve(&image(8, 8)).expect("fits");
2636
2637        let (evictions, uploads) = residency.plan();
2638        assert!(evictions.is_empty());
2639        assert_eq!(uploads.len(), 1);
2640        assert!(
2641            residency.has_pending_plan(),
2642            "reading the plan is not servicing it"
2643        );
2644
2645        residency.acknowledge_plan();
2646        assert!(!residency.has_pending_plan());
2647        assert!(residency.uploads().is_empty());
2648        assert!(residency.evictions().is_empty());
2649    }
2650
2651    #[test]
2652    fn a_clear_is_dropped_only_for_a_rectangle_an_upload_covers_exactly() {
2653        let region = |layer, x, y, w, h| AtlasRegion {
2654            layer,
2655            offset: [x, y],
2656            size: [w, h],
2657        };
2658        let upload = |region| ImageUpload {
2659            id: ImageId::new(0),
2660            region,
2661            natural: region.size,
2662            pixels: Arc::new(Pixmap::new(
2663                u16::try_from(region.size[0]).expect("small"),
2664                u16::try_from(region.size[1]).expect("small"),
2665            )),
2666            may_have_transparency: false,
2667        };
2668
2669        let exact = region(0, 0, 0, 16, 16);
2670        let other_layer = region(1, 0, 0, 16, 16);
2671        let partial = region(0, 16, 0, 16, 16);
2672        let evictions = [exact, other_layer, partial];
2673        let uploads = [upload(exact), upload(region(0, 16, 0, 16, 8))];
2674
2675        assert_eq!(
2676            uncovered_clears(&evictions, &uploads),
2677            vec![other_layer, partial],
2678            "only the exactly covered clear goes, the rest keep their order"
2679        );
2680        assert_eq!(uncovered_clears(&evictions, &[]), evictions.to_vec());
2681        assert!(uncovered_clears(&[], &uploads).is_empty());
2682    }
2683
2684    #[test]
2685    fn a_pressure_eviction_into_the_same_rectangle_plans_no_clear() {
2686        // One slot: the second image can only take the first one's rectangle.
2687        let mut residency = ImageResidency::new(AtlasBudget {
2688            atlas_size: (16, 16),
2689            max_atlases: 1,
2690        });
2691        residency.begin_frame();
2692        let first = residency.resolve(&image(16, 16)).expect("fits");
2693        residency.acknowledge_plan();
2694
2695        residency.begin_frame();
2696        let second = residency
2697            .resolve(&image(16, 16))
2698            .expect("displaces the first");
2699        assert_eq!(second.region, first.region);
2700        assert_eq!(
2701            residency.evictions(),
2702            &[first.region],
2703            "the eviction itself is still pending"
2704        );
2705
2706        let (clears, uploads) = residency.plan();
2707        assert!(clears.is_empty(), "but the plan issues no clear for it");
2708        assert_eq!(uploads.len(), 1);
2709
2710        residency.acknowledge_plan();
2711        assert!(!residency.has_pending_plan());
2712    }
2713
2714    #[test]
2715    fn an_unacknowledged_upload_is_re_emitted_until_it_is_serviced() {
2716        // The invariant the whole pending plan exists for: a frame refused
2717        // after compiling never wrote these texels, so the entry map claiming
2718        // the image is resident has to stay answerable by a later frame's plan.
2719        let mut residency = residency();
2720        let data = image(8, 8);
2721
2722        residency.begin_frame();
2723        let first = residency.resolve(&data).expect("fits");
2724        let region = residency.uploads()[0].region;
2725
2726        for _ in 0..4 {
2727            residency.begin_frame();
2728            let again = residency.resolve(&data).expect("still resident");
2729            assert_eq!(again.region, first.region, "residency does not move");
2730            assert_eq!(
2731                residency.uploads().len(),
2732                1,
2733                "the same upload is re-offered, never duplicated"
2734            );
2735            assert_eq!(residency.uploads()[0].region, region);
2736        }
2737
2738        residency.acknowledge_plan();
2739        residency.begin_frame();
2740        residency.resolve(&data).expect("still resident");
2741        assert!(
2742            residency.uploads().is_empty(),
2743            "a serviced upload is never offered again"
2744        );
2745    }
2746
2747    #[test]
2748    fn reaping_an_unacknowledged_entry_withdraws_its_upload_with_it() {
2749        // Otherwise the plan would clear the rectangle and then write the
2750        // evicted image straight back into it, over whatever the packer handed
2751        // that space to next.
2752        let mut residency = residency();
2753        let data = image(8, 8);
2754
2755        residency.begin_frame();
2756        let resident = residency.resolve(&data).expect("fits");
2757        assert_eq!(residency.uploads().len(), 1);
2758
2759        for _ in 0..=MAX_UNSEEN_FRAMES {
2760            residency.begin_frame();
2761        }
2762
2763        assert_eq!(residency.entry_count(), 0);
2764        assert!(
2765            residency.uploads().is_empty(),
2766            "the reaped entry's unserviced upload goes with it"
2767        );
2768        assert_eq!(
2769            residency.evictions(),
2770            &[padded(resident.region, ATLAS_PADDING)]
2771        );
2772    }
2773
2774    #[test]
2775    fn every_pending_upload_lies_inside_the_atlas_its_layer_count_asks_for() {
2776        // What keeps a consumer from recording where an image lives and then
2777        // sampling a rectangle the array refused to write: the plan can only
2778        // ever name regions the budget and the reported depth already admit.
2779        let mut residency = residency();
2780        residency.begin_frame();
2781        residency.resolve(&image(64, 48)).expect("fills a layer");
2782        residency.resolve(&image(64, 48)).expect("takes the next");
2783
2784        let budget = residency.budget();
2785        let layers = residency.layers();
2786        assert_eq!(layers, 2);
2787        for upload in residency.uploads() {
2788            assert!(
2789                budget.contains(upload.region, layers),
2790                "{:?} is outside a {layers}-layer {budget:?}",
2791                upload.region
2792            );
2793        }
2794    }
2795
2796    /// A budget holding exactly two 64-square images: one per layer, with no
2797    /// room to pack a third anywhere, so "the atlas is full" is reachable in
2798    /// two allocations and needs no fragmentation reasoning.
2799    fn two_slot_residency() -> ImageResidency {
2800        ImageResidency::new(AtlasBudget {
2801            atlas_size: (64, 64),
2802            max_atlases: 2,
2803        })
2804    }
2805
2806    #[test]
2807    fn a_working_set_larger_than_the_atlas_re_uploads_rather_than_skipping() {
2808        // The scrolling-list shape: more distinct images pass the viewport
2809        // inside one age window than the atlas has rectangles for. Every draw
2810        // must still resolve — the price is a re-upload, never a hole.
2811        let mut residency = two_slot_residency();
2812        let images: Vec<ImageData> = (0..12).map(|_| image(64, 64)).collect();
2813
2814        for data in &images {
2815            residency.begin_frame();
2816            residency
2817                .resolve(data)
2818                .expect("a full atlas gives a rectangle back rather than refusing");
2819            residency.acknowledge_plan();
2820        }
2821
2822        assert_eq!(residency.skipped(), 0, "no draw was ever dropped");
2823        assert_eq!(
2824            residency.pressure_evictions(),
2825            10,
2826            "the ten images past the two slots each displaced one"
2827        );
2828        assert_eq!(residency.entry_count(), 2, "and residency stays bounded");
2829    }
2830
2831    #[test]
2832    fn an_image_resolved_this_frame_is_never_the_one_evicted() {
2833        // The ordering contract's own boundary: the plan clears before it
2834        // uploads, so a rectangle this frame already resolved cannot be handed
2835        // back inside it. A frame whose own working set outgrows the atlas is
2836        // still a skip, and nothing is scheduled for clearing.
2837        let mut residency = two_slot_residency();
2838        residency.begin_frame();
2839        residency.resolve(&image(64, 64)).expect("takes layer 0");
2840        residency.resolve(&image(64, 64)).expect("takes layer 1");
2841
2842        let skip = residency
2843            .resolve(&image(64, 64))
2844            .expect_err("nothing this frame drew is evictable");
2845
2846        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
2847        assert_eq!(residency.skipped(), 1);
2848        assert_eq!(residency.pressure_evictions(), 0);
2849        assert_eq!(residency.entry_count(), 2, "both occupants stayed put");
2850        assert!(
2851            residency.evictions().is_empty(),
2852            "no region a frame is about to sample was scheduled for clearing"
2853        );
2854    }
2855
2856    #[test]
2857    fn the_least_recently_seen_image_is_the_one_displaced() {
2858        let mut residency = two_slot_residency();
2859        let old = image(64, 64);
2860        let refreshed = image(64, 64);
2861
2862        residency.begin_frame();
2863        let refreshed_region = residency.resolve(&refreshed).expect("takes a layer").region;
2864        residency.begin_frame();
2865        let old_region = residency.resolve(&old).expect("takes the other").region;
2866        residency.acknowledge_plan();
2867
2868        // The older entry is drawn again, which makes the *other* one the least
2869        // recently seen despite having been made resident second.
2870        residency.begin_frame();
2871        residency.resolve(&refreshed).expect("still resident");
2872        let arrival = residency.resolve(&image(64, 64)).expect("displaces one");
2873
2874        assert_eq!(residency.pressure_evictions(), 1);
2875        assert_eq!(
2876            arrival.region, old_region,
2877            "the rectangle taken is the least recently seen one"
2878        );
2879        assert_eq!(
2880            residency.resolve(&refreshed).map(|again| again.region),
2881            Ok(refreshed_region),
2882            "the image drawn this frame kept its own"
2883        );
2884    }
2885
2886    #[test]
2887    fn a_pressure_eviction_reports_its_rectangle_once_and_withdraws_its_upload() {
2888        let mut residency = two_slot_residency();
2889        let displaced = image(64, 64);
2890
2891        residency.begin_frame();
2892        let displaced_region = residency.resolve(&displaced).expect("takes a layer").region;
2893        residency.resolve(&image(64, 64)).expect("takes the other");
2894        // Deliberately NOT acknowledged: the displaced image still has an
2895        // upload pending, which must go with its entry rather than be written
2896        // into a rectangle its new occupant now owns.
2897        assert_eq!(residency.uploads().len(), 2);
2898
2899        residency.begin_frame();
2900        let arrival = residency
2901            .resolve(&image(64, 64))
2902            .expect("displaces the oldest");
2903
2904        assert_eq!(
2905            residency.evictions(),
2906            &[padded(displaced_region, ATLAS_PADDING)],
2907            "exactly one rectangle, reported exactly once"
2908        );
2909        assert_eq!(
2910            arrival.region, displaced_region,
2911            "and the freed rectangle is what the new occupant took"
2912        );
2913        assert_eq!(
2914            residency.uploads().len(),
2915            2,
2916            "the displaced image's unserviced upload went with its entry — \
2917             writing it would put an evicted image back over its successor"
2918        );
2919    }
2920
2921    #[test]
2922    fn an_age_reap_and_a_pressure_eviction_are_counted_separately() {
2923        // The two bounds answer different questions: one says a screen moved
2924        // on, the other says the atlas is too small for what is on it.
2925        let mut residency = two_slot_residency();
2926        let data = image(64, 64);
2927
2928        residency.begin_frame();
2929        residency.resolve(&data).expect("fits");
2930        for _ in 0..=MAX_UNSEEN_FRAMES {
2931            residency.begin_frame();
2932        }
2933
2934        assert_eq!(residency.entry_count(), 0, "the age reap ran");
2935        assert_eq!(
2936            residency.pressure_evictions(),
2937            0,
2938            "an age reap is not a pressure eviction"
2939        );
2940        assert_eq!(residency.frame_pressure_evictions(), 0);
2941    }
2942
2943    #[test]
2944    fn the_per_frame_eviction_count_is_this_frames_alone() {
2945        let mut residency = two_slot_residency();
2946        residency.begin_frame();
2947        residency.resolve(&image(64, 64)).expect("takes a layer");
2948        residency.resolve(&image(64, 64)).expect("takes the other");
2949
2950        residency.begin_frame();
2951        residency.resolve(&image(64, 64)).expect("displaces one");
2952        assert_eq!(residency.frame_pressure_evictions(), 1);
2953        assert_eq!(residency.pressure_evictions(), 1);
2954
2955        residency.begin_frame();
2956        assert_eq!(
2957            residency.frame_pressure_evictions(),
2958            0,
2959            "a new frame starts from nothing"
2960        );
2961        assert_eq!(
2962            residency.pressure_evictions(),
2963            1,
2964            "while the lifetime total keeps counting"
2965        );
2966    }
2967
2968    /// A budget of exactly four 32-square cells in one layer, so "the atlas is
2969    /// full" needs no reasoning about how a packer splits free space and a
2970    /// request that fits *no* arrangement of the holes is one rectangle.
2971    fn four_slot_residency() -> ImageResidency {
2972        ImageResidency::new(AtlasBudget {
2973            atlas_size: (64, 64),
2974            max_atlases: 1,
2975        })
2976    }
2977
2978    #[test]
2979    fn a_request_no_candidate_set_can_place_is_refused_before_anything_is_freed() {
2980        // The storm, at the cache seam. One 32-square is this frame's and three
2981        // are last frame's; a 48-square fits none of the holes those three
2982        // would leave while the fourth is resident. Freeing them buys nothing
2983        // — the draw is skipped either way — and costs three clears plus three
2984        // re-uploads, every frame the scene repeats. So the refusal has to come
2985        // before the first rectangle is handed back.
2986        let mut residency = four_slot_residency();
2987        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
2988        residency.begin_frame();
2989        for data in &resident {
2990            residency
2991                .resolve(data)
2992                .expect("the four cells take four images");
2993        }
2994        residency.acknowledge_plan();
2995
2996        residency.begin_frame();
2997        residency.resolve(&resident[0]).expect("still resident");
2998
2999        let skip = residency
3000            .resolve(&image(48, 48))
3001            .expect_err("no arrangement of the three candidates admits a 48-square");
3002
3003        assert!(matches!(skip, ImageSkip::NoAtlasSpace { .. }));
3004        assert_eq!(
3005            residency.pressure_evictions(),
3006            0,
3007            "a request that could not have been placed displaced nothing"
3008        );
3009        assert_eq!(residency.entry_count(), 4, "the residency is untouched");
3010        assert!(
3011            residency.evictions().is_empty(),
3012            "and nothing was scheduled for clearing on the way to failing"
3013        );
3014        assert!(residency.uploads().is_empty());
3015    }
3016
3017    #[test]
3018    fn a_request_larger_than_every_candidate_together_is_refused_on_area_alone() {
3019        // The cheaper half of the same precheck: freeing all three candidates
3020        // leaves three quarters of one layer, and the request wants all of it.
3021        // The area comes off the packer's own accounting, so it counts what the
3022        // glyph half of the allocator holds as well as this residency's own.
3023        let mut residency = four_slot_residency();
3024        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
3025        residency.begin_frame();
3026        for data in &resident {
3027            residency
3028                .resolve(data)
3029                .expect("the four cells take four images");
3030        }
3031        residency.acknowledge_plan();
3032
3033        residency.begin_frame();
3034        residency.resolve(&resident[0]).expect("still resident");
3035        residency
3036            .resolve(&image(64, 64))
3037            .expect_err("a whole layer is more than the three candidates hold");
3038
3039        assert_eq!(residency.pressure_evictions(), 0);
3040        assert_eq!(residency.entry_count(), 4);
3041        assert!(residency.evictions().is_empty());
3042    }
3043
3044    #[test]
3045    fn a_satisfiable_request_under_pressure_still_evicts_the_minimum() {
3046        // The other side of the precheck: it must not turn a working eviction
3047        // into a skip. One 32-square arriving against four resident ones is
3048        // satisfiable by freeing exactly one, and that is what it costs.
3049        let mut residency = four_slot_residency();
3050        let resident: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
3051        residency.begin_frame();
3052        for data in &resident {
3053            residency
3054                .resolve(data)
3055                .expect("the four cells take four images");
3056        }
3057        residency.acknowledge_plan();
3058
3059        residency.begin_frame();
3060        residency.resolve(&image(32, 32)).expect("displaces one");
3061
3062        assert_eq!(
3063            residency.pressure_evictions(),
3064            1,
3065            "the minimum, not the lot"
3066        );
3067        assert_eq!(residency.entry_count(), 4);
3068        assert_eq!(residency.evictions().len(), 1);
3069    }
3070
3071    #[test]
3072    fn a_blob_redrawn_at_a_new_extent_never_frees_what_this_frame_resolved() {
3073        // The extent-mismatch release under the pressure path's own rule. The
3074        // plan clears before it uploads, so giving the rectangle back on a
3075        // frame whose first draw already resolved it would zero texels that
3076        // draw is about to sample.
3077        let square = image(16, 8);
3078        let transposed = ImageData {
3079            data: square.data.clone(),
3080            format: square.format,
3081            alpha_type: square.alpha_type,
3082            width: 8,
3083            height: 16,
3084        };
3085
3086        let mut residency = residency();
3087        residency.begin_frame();
3088        let first = residency.resolve(&square).expect("takes a rectangle");
3089        let skip = residency
3090            .resolve(&transposed)
3091            .expect_err("the second extent cannot take the first one's rectangle");
3092
3093        assert!(
3094            matches!(skip, ImageSkip::SameFrameExtentConflict { .. }),
3095            "a same-frame ordering conflict, not an atlas-capacity refusal: {skip:?}"
3096        );
3097        assert!(
3098            residency.evictions().is_empty(),
3099            "no clear over a rectangle this frame already sampled"
3100        );
3101        assert_eq!(residency.entry_count(), 1);
3102        assert_eq!(residency.uploads().len(), 1);
3103
3104        // On a later frame nothing has sampled it yet, so the release is
3105        // ordinary again and the new extent takes its place.
3106        residency.acknowledge_plan();
3107        residency.begin_frame();
3108        let second = residency.resolve(&transposed).expect("the new extent fits");
3109        assert_eq!(second.natural, [8, 16]);
3110        assert_eq!(
3111            residency.evictions(),
3112            &[padded(first.region, ATLAS_PADDING)],
3113            "the rectangle the old extent held is reported for clearing"
3114        );
3115    }
3116
3117    #[test]
3118    fn a_layer_admits_only_what_every_occupant_leaves_room_for() {
3119        // The planner's own necessary condition, in isolation. A placement has
3120        // to lie entirely to one side of each occupant, so a 32-square in one
3121        // corner of a 64-square layer leaves nothing a 48-square can occupy —
3122        // while a 32-square still has three cells to choose from.
3123        let corner = PressureRect {
3124            key: 1,
3125            layer: 0,
3126            x0: 0,
3127            y0: 0,
3128            x1: 32,
3129            y1: 32,
3130            area: 1024,
3131            last_seen: 0,
3132            slot: 0,
3133            freed: false,
3134        };
3135        let rects = [corner];
3136
3137        assert!(!layer_admits(&rects, 0, 48, 48, (64, 64)));
3138        assert!(layer_admits(&rects, 0, 32, 32, (64, 64)));
3139        assert!(layer_admits(&rects, 0, 32, 64, (64, 64)));
3140        // A freed rectangle is not an obstacle, and a layer nothing occupies
3141        // admits anything the layer itself can hold.
3142        assert!(layer_admits(
3143            &[PressureRect {
3144                freed: true,
3145                ..corner
3146            }],
3147            0,
3148            64,
3149            64,
3150            (64, 64)
3151        ));
3152        assert!(!layer_admits(&[], 0, 65, 64, (64, 64)));
3153        // An occupant on another layer says nothing about this one.
3154        assert!(layer_admits(
3155            &[PressureRect { layer: 1, ..corner }],
3156            0,
3157            64,
3158            64,
3159            (64, 64)
3160        ));
3161    }
3162
3163    #[test]
3164    fn a_plan_that_would_outrun_its_budget_frees_nothing() {
3165        // The bound is on the *search*, not only on the damage: a walk that
3166        // reaches the budget without finding a fit answers `None`, and the
3167        // caller has released nothing by then.
3168        let mut rects: Vec<PressureRect> = (0..MAX_PRESSURE_EVICTION_CANDIDATES + 8)
3169            .map(|index| {
3170                let index = u32::try_from(index).expect("fits");
3171                PressureRect {
3172                    key: u64::from(index),
3173                    layer: 0,
3174                    x0: index,
3175                    y0: 0,
3176                    x1: index + 1,
3177                    y1: 1,
3178                    area: 1,
3179                    last_seen: 0,
3180                    slot: index,
3181                    freed: false,
3182                }
3183            })
3184            .collect();
3185        let order: Vec<usize> = (0..rects.len()).collect();
3186        let mut free_area = vec![0_u64];
3187        let mut layers = Vec::new();
3188
3189        assert_eq!(
3190            plan_pressure_eviction(
3191                64,
3192                64,
3193                (64, 64),
3194                &mut rects,
3195                &order,
3196                &mut free_area,
3197                &mut layers
3198            ),
3199            None,
3200            "a walk that cannot fit the request inside its budget plans nothing"
3201        );
3202        assert!(
3203            rects.iter().filter(|rect| rect.freed).count() <= MAX_PRESSURE_EVICTION_CANDIDATES + 1,
3204            "and it stopped walking rather than marking the whole residency"
3205        );
3206    }
3207
3208    #[test]
3209    fn a_satisfiable_layer_is_not_starved_by_a_hopeless_ones_candidates() {
3210        // The multi-layer shape the per-layer budget exists for. Layer 0 is
3211        // "this frame's" 64x40 anchor plus more than
3212        // `MAX_PRESSURE_EVICTION_CANDIDATES` one-pixel stale fillers packed
3213        // into the 64x24 strip left over — every one of them stale (older
3214        // than the incoming request's frame) and none of them, individually
3215        // or together, ever able to free a 32-tall rectangle, since the strip
3216        // they sit in is only 24 pixels tall. Layer 1 is four 32-square
3217        // quadrants exactly tiling the layer, three "this frame's" and one
3218        // stale. Layers 2 and 3 are each one "this frame's" 64-square image,
3219        // filling them outright so the incoming request cannot simply grow
3220        // into an unused layer.
3221        //
3222        // A single pooled budget spends its whole allowance walking layer 0's
3223        // fillers — none of which can ever satisfy the request — and never
3224        // reaches layer 1's one satisfiable candidate at all. A per-layer
3225        // budget exhausts layer 0's own allowance the same way, then gives
3226        // layer 1 a fresh one and finds the fit immediately.
3227        let mut residency = ImageResidency::new(AtlasBudget {
3228            atlas_size: (64, 64),
3229            max_atlases: 4,
3230        });
3231
3232        let anchor = image(64, 40);
3233        let fillers: Vec<ImageData> = (0..MAX_PRESSURE_EVICTION_CANDIDATES + 6)
3234            .map(|_| image(1, 1))
3235            .collect();
3236        let quadrants: Vec<ImageData> = (0..4).map(|_| image(32, 32)).collect();
3237        let layer2_filler = image(64, 64);
3238        let layer3_filler = image(64, 64);
3239
3240        residency.begin_frame();
3241        residency
3242            .resolve(&anchor)
3243            .expect("the anchor takes layer 0's top");
3244        for filler in &fillers {
3245            residency
3246                .resolve(filler)
3247                .expect("a one-pixel filler always finds room in the leftover strip");
3248        }
3249        for quadrant in &quadrants {
3250            residency
3251                .resolve(quadrant)
3252                .expect("a 32-square quadrant packs layer 1's empty corners");
3253        }
3254        residency
3255            .resolve(&layer2_filler)
3256            .expect("a whole-layer image grows into a fresh layer 2");
3257        residency
3258            .resolve(&layer3_filler)
3259            .expect("a whole-layer image grows into a fresh layer 3");
3260        residency.acknowledge_plan();
3261        assert_eq!(
3262            residency.layers(),
3263            4,
3264            "fixture precondition: all four layers exist"
3265        );
3266
3267        // Refresh everything except the fillers and the first quadrant, so
3268        // only they are stale candidates when the request arrives — the
3269        // fillers sort ahead of the stale quadrant because they were
3270        // resolved first and share the same `last_seen`.
3271        residency.begin_frame();
3272        residency.resolve(&anchor).expect("still resident");
3273        residency.resolve(&quadrants[1]).expect("still resident");
3274        residency.resolve(&quadrants[2]).expect("still resident");
3275        residency.resolve(&quadrants[3]).expect("still resident");
3276        residency.resolve(&layer2_filler).expect("still resident");
3277        residency.resolve(&layer3_filler).expect("still resident");
3278
3279        let arrival = residency
3280            .resolve(&image(32, 32))
3281            .expect("a per-layer budget reaches layer 1's satisfiable candidate");
3282
3283        assert_eq!(
3284            residency.pressure_evictions(),
3285            1,
3286            "exactly the one quadrant needed, not layer 0's hopeless fillers"
3287        );
3288        assert_eq!(residency.evictions().len(), 1);
3289        assert_eq!(
3290            residency.evictions()[0].layer,
3291            arrival.region.layer,
3292            "the eviction and the arrival land on the same, single layer"
3293        );
3294        assert_ne!(
3295            arrival.region.layer, 0,
3296            "the winning layer is not the one full of hopeless fillers"
3297        );
3298    }
3299
3300    /// The tier line's own text, which only a `perf-trace` build has.
3301    #[cfg(feature = "perf-trace")]
3302    #[test]
3303    fn the_resolved_tier_line_names_the_tier_and_is_written_once() {
3304        let mobile = TierCaps::fake(DownlevelProfile::WebGl2);
3305        let choice = choose_tier(&mobile, None);
3306        assert_eq!(
3307            atlas_tier_line(&mobile, choice, AtlasBudget::MOBILE),
3308            "frust-perf atlas tier=mobile rule=downlevel budget=1024x1024x4 downlevel=WebGl2 \
3309             transient_saves_memory=false memory_mib=unknown adapter=fake-webgl2"
3310        );
3311
3312        let mut adreno = TierCaps::fake(DownlevelProfile::Full);
3313        adreno.adapter_name = "Adreno (TM) 660".to_string();
3314        let choice = choose_tier(&adreno, Some(8 << 30));
3315        assert_eq!(
3316            atlas_tier_line(&adreno, choice, AtlasBudget::MOBILE_LARGE_WIDE),
3317            "frust-perf atlas tier=mobile_large rule=table budget=2048x2048x4 downlevel=Full \
3318             transient_saves_memory=false memory_mib=8192 adapter=Adreno (TM) 660"
3319        );
3320
3321        // An adapter the table does not name says which capability answer
3322        // decided it.
3323        let mut tiler = TierCaps::fake(DownlevelProfile::Full);
3324        tiler.transient_saves_memory = true;
3325        let line = atlas_tier_line(&tiler, choose_tier(&tiler, None), AtlasBudget::MOBILE);
3326        assert!(line.contains("tier=mobile rule=heuristic"));
3327        assert!(line.contains("transient_saves_memory=true"));
3328
3329        let latch = TierLogOnce::new();
3330        let choice = choose_tier(&mobile, None);
3331        assert!(
3332            latch.emit(&mobile, choice, AtlasBudget::MOBILE),
3333            "the first call writes the line"
3334        );
3335        assert!(
3336            !latch.emit(&mobile, choice, AtlasBudget::MOBILE),
3337            "and no later one repeats it"
3338        );
3339    }
3340
3341    #[test]
3342    fn an_override_redistributes_a_tiers_memory_but_never_exceeds_it() {
3343        let tier = AtlasBudget::DESKTOP;
3344
3345        // The pathological override the adapter alone would admit: a
3346        // 16384-square layer is a gibibyte on its own, so clamping the layer
3347        // count could not have brought it inside the tier's 128 MiB.
3348        let huge = AtlasBudget {
3349            atlas_size: (16_384, 16_384),
3350            max_atlases: tier.max_atlases,
3351        }
3352        .within_total_bytes(tier.total_bytes());
3353        assert!(huge.total_bytes() <= tier.total_bytes());
3354        assert_eq!(huge.atlas_size.0, huge.atlas_size.1, "aspect preserved");
3355        assert!(huge.max_atlases >= 1);
3356
3357        // A modest override is left exactly as asked for.
3358        let modest = AtlasBudget {
3359            atlas_size: (1024, 1024),
3360            max_atlases: tier.max_atlases,
3361        }
3362        .within_total_bytes(tier.total_bytes());
3363        assert_eq!(modest.atlas_size, (1024, 1024));
3364        assert_eq!(modest.max_atlases, tier.max_atlases);
3365
3366        // An extreme aspect ratio floors one axis to nothing; it is raised back
3367        // to a texel and the other axis gives the room up, so the pair still
3368        // fits and neither axis is zero.
3369        let sliver = AtlasBudget {
3370            atlas_size: (65_535, 4),
3371            max_atlases: 1,
3372        }
3373        .within_total_bytes(64);
3374        assert!(sliver.atlas_size.0 >= 1 && sliver.atlas_size.1 >= 1);
3375        assert!(sliver.total_bytes() <= 64);
3376    }
3377}