4.6 KiB
APP-3877: Tech Spec — Prep 1: TextureCache Cascade Eviction
Context
This is the first preparatory changeset for the image cache debounce strategy described in the broader APP-3877 plan. It establishes the per-size eviction API on ImageCache and verifies that GPU memory is freed correctly when an ImageCache entry is dropped.
Relevant files
crates/warpui_core/src/image_cache.rs:799—ImageCache, storesRwLock<HashMap<u64, HashMap<RenderedImageCacheKey, Rc<Image>>>>. Whole-asset eviction exists (evict_image); per-size eviction does not yet exist.crates/warpui_core/src/rendering/texture_cache.rs:26—TextureCache<T>, stores aVec<TextureInfo<T>>where each entry holdsWeak<StaticImage>and alast_accessed_framecounter.crates/warpui_core/src/elements/image.rs:302—Image::paint()callsImageCache::image(), receivesRc<Image>, immediately unwraps the innerArc<StaticImage>, and pushes it to theScene. TheRc<Image>is never stored beyondpaint().crates/warpui_core/src/scene.rs:109—Scene::Image { asset: Arc<StaticImage> }— the scene stores a strong reference during the current frame only.crates/warpui/src/rendering/wgpu/renderer/image.rs:53—Pipeline::texture_cache: TextureCache<TextureInfo>, populated fromscene.images[*].asseteach frame.
How the cascade currently works
TextureCache::end_frame() already evicts entries in two cases:
asset.strong_count() == 0— the backingArc<StaticImage>has no remaining strong holders; the asset has been dropped entirely.frame_index - last_accessed_frame >= MAX_UNUSED_FRAMES— the texture has not been rendered in 10+ frames, regardless of whether the asset is still alive.
For CacheOption::BySize resized images, ImageCache allocates a fresh Arc<StaticImage> per (asset, size) entry that is shared with no other subsystem. After Image::paint() returns and the scene for that frame is discarded, the sole persistent strong holder is ImageCache. Dropping that entry — via either evict_image or the new evict_size — reduces the strong count to zero, which causes end_frame() to evict the corresponding GPU texture on the next frame.
No changes to TextureCache are required. The Weak<StaticImage> mechanism already provides the correct cascade behavior.
Proposed changes
1. Add evict_size to ImageCache
Add a private method that removes a single (cache_key, RenderedImageCacheKey) entry, cleaning up the outer map if the inner map becomes empty. The main changeset will call this from inside image() during its lazy eviction pass.
fn evict_size(&self, cache_key: u64, rendered_key: RenderedImageCacheKey) {
let mut cache = self.images.write();
if let Some(inner_map) = cache.get_mut(&cache_key) {
inner_map.remove(&rendered_key);
if inner_map.is_empty() {
cache.remove(&cache_key);
}
}
}
2. Add a test-only StaticImage constructor
StaticImage currently has no public constructor. TextureCache tests need Arc<StaticImage> to exercise get_or_insert_by_asset. Add a pub(crate) mod test_utils under #[cfg(test)] in image_cache.rs with a make_static_image(width, height) helper.
3. Add tests
image_cache_tests.rs
test_evict_image_drops_arc_for_resized_bysize: load aBySizeimage at a size different from the source (forcing a resize and a freshArc), capture aWeak<StaticImage>, drop the localRc<Image>clone, assertstrong_count == 1(onlyImageCacheholds it), callevict_image, assertstrong_count == 0.test_evict_size_drops_arc_for_single_entry: same setup with two different sizes; callevict_sizefor one entry and assert that only the targeted size'sArcis released while the other remains alive.
texture_cache_tests.rs (new file, wired in via #[path])
test_end_frame_evicts_when_asset_dropped: insert an asset, drop theArc, callend_frame, verify the entry count drops to zero.test_end_frame_evicts_after_max_unused_frames: insert an asset, keep theArcalive, advanceframe_indexpastMAX_UNUSED_FRAMESby callingend_framerepeatedly without re-accessing the texture, verify the entry is evicted.test_end_frame_retains_recently_used_entry: insert, re-access each frame, advance pastMAX_UNUSED_FRAMES, verify the texture is retained because it was used.
Testing and validation
All new behavior is verified by unit tests above. No rendering hardware is required — TextureCache<T> is generic and tests use T = (). Run with:
cargo nextest run -p warpui_core