From 77a8d6ae73e599106c2da39e477f4d2c8db1ae83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Kozlovsk=C3=BD?= Date: Thu, 15 Oct 2026 10:19:42 +0200 Subject: [PATCH] gpu: upload cache drops textures of freed buffers on every insert Entries whose layer buffer was freed were pruned only once the cache went over its budget (1.5 GiB), so an animated layer, which makes a new buffer every frame, left a dead 4K texture per frame allocated until the cache filled up. --- crates/gpu/src/context.rs | 20 +++++++++++++------- crates/gpu/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index 51221abc..0c5763a7 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -919,6 +919,12 @@ impl GpuContext { /// Upload a (layer cache) buffer, reusing the texture while the same `Arc` is alive: static /// layers upload once, not every frame. + /// Entries in the layer upload cache. + #[cfg(test)] + pub(crate) fn upload_entries(&self) -> usize { + self.uploads.lock().map(|u| u.map.len()).unwrap_or_default() + } + pub(crate) fn upload_buf(&self, buf: &Arc) -> Option { let key = Arc::as_ptr(buf) as usize; if let Ok(mut u) = self.uploads.lock() { @@ -939,14 +945,14 @@ impl GpuContext { u.bytes -= old.bytes; } u.bytes += bytes; - // Drop textures of freed buffers, then the least recently used over budget. + // Drop textures of freed buffers at once (a layer whose buffer changes every frame + // makes a new one each time, and nothing can hit the old one again), then the least + // recently used over budget. let budget = self.upload_budget(); - if u.bytes > budget { - let dead: Vec = u.map.iter().filter(|(_, e)| e.buf.strong_count() == 0).map(|(k, _)| *k).collect(); - for k in dead { - if let Some(e) = u.map.remove(&k) { - u.bytes -= e.bytes; - } + let dead: Vec = u.map.iter().filter(|(_, e)| e.buf.strong_count() == 0).map(|(k, _)| *k).collect(); + for k in dead { + if let Some(e) = u.map.remove(&k) { + u.bytes -= e.bytes; } } if u.bytes > budget && !self.upload_full_warned.swap(true, std::sync::atomic::Ordering::Relaxed) { diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 696402d6..532e313f 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -1044,3 +1044,19 @@ fn static_effect_layers_draw_from_the_cache() { assert!(frames[1] == frames[2]); check("static effect layer vs CPU", compare_at(&s, opts(), t(2)), 0.0); } + +/// The layer upload cache drops the textures of freed buffers when it inserts, not only once +/// it is over budget (an animated layer makes a new buffer every frame). +#[test] +fn upload_cache_drops_textures_of_freed_buffers() { + let Some(g) = gpu() else { return }; + let img = effectcraft_raster::Image::new(256, 256); + let before = g.ctx.upload_entries(); + for _ in 0..6 { + let buf = std::sync::Arc::new(effectcraft_effects::Buf { img: img.clone(), offset: [0.0; 2], scale: 1.0 }); + let up = g.ctx.upload_buf(&buf).expect("upload"); + drop((up, buf)); + } + let kept = g.ctx.upload_entries(); + assert!(kept <= before + 1, "the upload cache kept {kept} textures of freed buffers (had {before})"); +}