Skip to main content

Module images

Module images 

Source
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

  1. A format or size vello_common panics on. vello_common::paint::ImageSource::from_peniko_image_data asserts on a dimension past u16::MAX and unimplemented!()s on a format outside Rgba8/Bgra8, and a pixel buffer whose length disagrees with the declared extent trips Pixmap::from_parts_with_opacity’s own assertion. All three are checked here, before the call, so an oversized or malformed image is an ImageSkip the frame path reports rather than a panic it takes (E17). These, plus an atlas with no room left in it, are now the only things ImageResidency::skipped counts: an image that is merely big is minified and drawn.
  2. Anything at all, when FRUST_ENGINE_NO_ATLAS is set. The kill switch (crate::config::atlas_disabled) makes every resolution answer ImageSkip::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§

AtlasBudget
The atlas geometry a tier of adapter is given.
AtlasRegion
Where one image’s texels live in the atlas array.
ImageResidency
The images resident in the atlas array, keyed by blob identity.
ImageUpload
One region’s texels, waiting to be written into the atlas array.
ResidentImage
A resident image: the handle a paint names it by, and where its texels are.

Enums§

ImageSkip
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 natural is stored at inside an atlas-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
source box-filtered down to width x height.