Expand description
Image residency: which decoded images own atlas space, and for how long.
A widget hands the display list the same decoded image every frame — the
peniko::ImageData in a Command::Image is a
reference-counted handle cloned per frame, never a re-decode — so the pixels
behind it are stable across frames and belong on the GPU once rather than
per draw. ImageResidency is what makes that “once” true: it keys an
image on its blob’s process-unique id (peniko::Blob::id) and hands back
the same ImageId and the same atlas rectangle for every later frame that
draws it.
Allocation itself is pure — a rectangle packer over
vello_common::multi_atlas::MultiAtlasManager, no device in the loop — so
everything here is host-testable. The two GPU-side halves it describes
(writing a newly allocated region’s texels, clearing an evicted one’s) are
carried out by crate::gpu::atlas against the plan this module produces:
ImageResidency::evictions first, ImageResidency::uploads second. That
order is a contract, not a preference — a rectangle freed this frame can be
re-allocated in the same frame, so clearing after uploading would erase the
image that just moved in.
§Residency is committed only once the plan is serviced
The cache decides residency long before anything reaches a queue: a
compiled frame can still be refused afterwards (a layer shape the scheduler
will not serve, a page past the pool’s ceiling, coverage or paints outgrowing
their textures), and a frame refused after compiling is never drawn and never
uploaded. So the plan is pending rather than taken: ImageResidency::plan
hands out a copy, begin_frame leaves it
alone, and only acknowledge_plan —
called by a consumer that has actually written the regions into a live atlas
— clears it.
That is what makes the entry map and the atlas’s real contents agree. Without it, a refused frame leaves an image recorded as resident whose texels were never written: every later frame resolves the entry, schedules no upload (residency is a hit), and either drops the draw for the life of the process or samples a rectangle nothing ever wrote. Re-emitting the same plan until it is acknowledged costs a vector copy per frame in the steady state — where the plan is empty — and is what makes “an image drawn on a thousand frames uploads once” true of the atlas rather than only of the cache.
§An image larger than the atlas is minified, not dropped
A source whose own extent will not fit one atlas layer is downsampled to fit
(fit_extent, minify) instead of being refused. A photograph decoded
at its capture resolution is routinely several times the atlas budget while
its destination rectangle is a thumbnail, so refusing it drops a draw the
display list plainly describes; and every texel past the destination’s own
scale is one the sampler would have thrown away anyway.
The filter is a plain box average over the premultiplied texels, hand-written
rather than pulled in: the workspace’s version pins are law, an image decoder
is not a dependency this crate carries, and a box average is exactly right
for the minification direction (every output texel is the mean of the source
texels it covers). The aspect ratio is preserved, so the paint transform’s
two axes stay in step; what changes is the resident rectangle’s extent, which
ResidentImage::natural records alongside it so the consumer can rescale
the encoded paint’s own transform into the smaller rectangle.
§Two things are refused rather than attempted
- A format or size
vello_commonpanics on.vello_common::paint::ImageSource::from_peniko_image_dataasserts on a dimension pastu16::MAXandunimplemented!()s on a format outsideRgba8/Bgra8, and a pixel buffer whose length disagrees with the declared extent tripsPixmap::from_parts_with_opacity’s own assertion. All three are checked here, before the call, so an oversized or malformed image is anImageSkipthe frame path reports rather than a panic it takes (E17). These, plus an atlas with no room left in it, are now the only thingsImageResidency::skippedcounts: an image that is merely big is minified and drawn. - Anything at all, when
FRUST_ENGINE_NO_ATLASis set. The kill switch (crate::config::atlas_disabled) makes every resolution answerImageSkip::AtlasDisabled, so no atlas is allocated, nothing is uploaded, and image draws fall back to painting nothing — the switch’s whole point being to take the atlas out of a frame under diagnosis.
§Residency is bounded by age, and by pressure
An entry unseen for MAX_UNSEEN_FRAMES consecutive frames is deallocated
and its rectangle reported for clearing — the same age-based reap
frust-render’s shader-effect cache uses, and for the same reason: a screen
that stops drawing an image should give its texels back promptly, while an
image drawn every other frame must never be mistaken for gone.
Age alone is not a bound on population, though: it says nothing about how
many distinct images a screen draws inside one age window. A list scrolling
faster than MAX_UNSEEN_FRAMES streams more of them past the viewport
than the atlas has rectangles for, and every one past the last free
rectangle used to be an ImageSkip::NoAtlasSpace — a draw that painted
nothing, on a screen whose solid fills all still landed, which reads as a
hole rather than as a cache miss. So an allocation the packer refuses now
evicts instead of giving up: ImageResidency::resolve works out which
least-recently-seen rectangles would make room, frees exactly those, and
each one whose rectangle was taken re-uploads on the next frame that draws
it. A working set larger than the atlas therefore degrades to re-uploads —
bandwidth — rather than to missing content, and
ImageResidency::pressure_evictions counts how often that trade was made.
§An eviction is planned on paper before anything is freed
An eviction that does not produce the allocation is pure loss. The draw is skipped anyway, so the frame pays a clear per freed rectangle and the re-upload of every displaced image and the hole it was trying to avoid. And the condition recurs: a request the atlas can never satisfy would strip and rebuild the residency on every frame that asks for it, which on a mobile budget is megabytes of texture traffic each way, per frame, buying nothing.
So the pressure path commits nothing speculatively. allocate_under_pressure
walks the candidates in least-recently-seen order over a model of the
layers — the rectangles this residency knows are occupied, and each layer’s
free area as the packer itself reports it — and stops at the first prefix
that would admit the request. Only then is anything released, and only the
candidates on the layer the fit was found in. A request no bounded set of
candidates can satisfy therefore stays the cheap ImageSkip::NoAtlasSpace
it was before eviction existed, with the residency untouched.
The model is deliberately optimistic: it can see this residency’s own rectangles but not the glyph pages sharing the same allocator (see below), so it never refuses a request eviction could have satisfied on a given layer — a placement that clears the real occupants clears the model’s subset of them too. The reverse can happen: the packer may still refuse a plan the model approved, which costs the planned evictions and no allocation. That case is bounded by the same plan budget rather than open-ended, and it is the one residual the planner cannot rule out without knowing where every glyph page sits.
The bounded search itself is scoped per layer: each atlas layer gets its own candidate-count and area allowance rather than the whole residency sharing one pool, so a request only one layer could ever satisfy is not starved by candidates on layers that were never going to host it — each layer is either cleared within its own allowance or ruled out on its own terms, never on account of what another layer’s candidates already cost.
The one entry never freed on either route is one this frame has already
resolved. The plan clears before it uploads, so handing back a rectangle the
frame is about to sample would erase exactly the image the eviction was
meant to make room for. A working set that outgrows the atlas inside a
single frame is therefore still a skip, and honestly so: nothing is
evictable. The same guard covers the extent-mismatch release in
ImageResidency::resolve, where a blob redrawn at a new extent gives its
old rectangle back: on a frame that already sampled that rectangle the
second draw is skipped instead, as
ImageSkip::SameFrameExtentConflict rather than as a capacity refusal —
the atlas may hold nothing at all beside the first extent’s own rectangle.
§One allocator for images and glyphs
The ImageCache this module holds is the process’s only atlas allocator:
the glyph policy (crate::text::atlas_policy) owns no cache of its own and
allocates its slots through ImageResidency::allocator_mut.
That is a correctness requirement, not tidiness. An ImageId is a slot
index into one cache — ImageCache::allocate hands out the next free index
of its own slots vector — and the strip shader has exactly one atlas
texture array to sample from. Two caches over the same page geometry would
therefore mint the same small integers for unrelated occupants and pack them
into overlapping rectangles of the same layers, so a glyph and an image would
address each other’s texels by construction. Sharing one cache makes both
collisions unrepresentable: one id space, one packer, one set of live
rectangles.
It is also what upstream does — vello_hybrid’s Resources holds a single
image_cache and hands it to its glyph atlas at every call — and it is what
keeps the budget honest: a glyph page and an image page come out of the same
AtlasBudget::max_atlases allowance, and ImageResidency::layers counts
whichever of the two created a layer.
The one rule a borrower must keep is ownership of its own handles: each side
deallocates only the ids it allocated. Nothing enforces that beyond the two
call sites, and it is the reason ImageResidency::allocator_mut is
documented as a contract rather than a plain accessor.
Structs§
- Atlas
Budget - The atlas geometry a tier of adapter is given.
- Atlas
Region - Where one image’s texels live in the atlas array.
- Image
Residency - The images resident in the atlas array, keyed by blob identity.
- Image
Upload - One region’s texels, waiting to be written into the atlas array.
- Resident
Image - A resident image: the handle a paint names it by, and where its texels are.
Enums§
- Image
Skip - Why one image could not be made resident.
Constants§
- ATLAS_
FORMAT_ BYTES - Bytes per texel of the atlas array (
Rgba8Unorm). - ATLAS_
PADDING - Transparent padding pixels placed around each image in the atlas.
- MAX_
ATLAS_ LAYERS - The most atlas layers the shader’s encoded-image record can name.
- MAX_
IMAGE_ DIMENSION - The largest image edge that can be made resident at all.
- MAX_
UNSEEN_ FRAMES - Consecutive frames an image may go undrawn before its atlas rectangle is reclaimed.
Functions§
- fit_
extent - The extent
naturalis stored at inside anatlas-sized layer: itself when it already fits, and otherwise the largest rectangle of the same aspect ratio that does. - is_
mobile_ tier - Whether
caps’ adapter takes the mobile atlas budget. - minify
sourcebox-filtered down towidthxheight.