diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index cd2c7749..279c316a 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -845,6 +845,26 @@ impl Compositor { self.prefix.forget(doc); } + /// [`Self::forget_doc`] for every document `keep` rejects: what the compositor holds for + /// documents that were closed, however their canvas entry went (#4529). + pub fn retain_docs(&mut self, keep: impl Fn(DocId) -> bool) { + let mut gone: Vec = + self.residents.values().map(|r| r.doc).chain(self.fx.values().map(|e| e.doc)).chain(self.prefix.doc()).filter(|d| !keep(*d)).collect(); + gone.sort_by_key(|d| d.0); + gone.dedup(); + for d in gone { + self.forget_doc(d); + } + } + + /// Documents the compositor holds pages, effect maps or a lower-stack composite for. + pub fn held_docs(&self) -> Vec { + let mut docs: Vec = self.residents.values().map(|r| r.doc).chain(self.fx.values().map(|e| e.doc)).chain(self.prefix.doc()).collect(); + docs.sort_by_key(|d| d.0); + docs.dedup(); + docs + } + /// GPU memory held by cached effect maps (bytes). pub fn fx_cache_bytes(&self) -> usize { self.fx.values().map(FxEntry::bytes).sum() diff --git a/crates/gpu/src/prefix.rs b/crates/gpu/src/prefix.rs index fcd5c0a0..0312a25f 100644 --- a/crates/gpu/src/prefix.rs +++ b/crates/gpu/src/prefix.rs @@ -85,6 +85,11 @@ impl Prefix { } } + /// The document the cached composite belongs to, if any. + pub(crate) fn doc(&self) -> Option { + self.context.as_ref().map(|d| d.id) + } + pub(crate) fn prepare(&mut self, doc: &Document, plan: &Plan<'_>) -> Option { if self.budget == 0 { return None; diff --git a/crates/gpu/tests/parity.rs b/crates/gpu/tests/parity.rs index 944c7566..bdeb9fe0 100644 --- a/crates/gpu/tests/parity.rs +++ b/crates/gpu/tests/parity.rs @@ -187,6 +187,27 @@ fn check(g: &mut Gpu, doc: &Document, what: &str) { diff_rect(g, doc, doc.bounds(), what).unwrap_or_else(|e| panic!("{e}")); } +/// Closing a document frees what the compositor holds for it, however its canvas entry went +/// (#4529): `retain_docs` drops the resident pages, effect maps and lower-stack composite of +/// documents that aren't open, and keeps the open ones. +#[test] +fn retain_docs_frees_closed_documents() { + let Some(mut g) = gpu() else { return }; + let a = base_doc(64, 48); + let mut b = base_doc(32, 32); + b.id = photocraft_doc::DocId(a.id.0 + 1); + for d in [&a, &b] { + render_to_vec(&mut g.comp, &g.device, &g.queue, d, d.bounds()).expect("render"); + } + assert_eq!(g.comp.held_docs(), vec![a.id, b.id]); + g.comp.retain_docs(|d| d == b.id); + assert_eq!(g.comp.held_docs(), vec![b.id], "only the open document is left"); + g.comp.retain_docs(|_| false); + assert!(g.comp.held_docs().is_empty()); + assert_eq!(g.comp.composite_cache_bytes(), 0); + check(&mut g, &a, "renders again after being forgotten"); +} + #[test] fn first_frame_renders() { // Regression: the first submission after device creation is dropped on some drivers diff --git a/crates/ui-egui/src/gpu_canvas.rs b/crates/ui-egui/src/gpu_canvas.rs index e73c5998..bcf0e695 100644 --- a/crates/ui-egui/src/gpu_canvas.rs +++ b/crates/ui-egui/src/gpu_canvas.rs @@ -638,14 +638,14 @@ impl GpuCanvas { let mut renderer = self.rs.renderer.write(); let Some(res) = renderer.callback_resources.get_mut::() else { return }; if res.docs.keys().any(|k| !live.contains(k)) { - let gone: Vec = res.docs.keys().filter(|k| !live.contains(k)).copied().collect(); - if let Some(c) = res.compositor.as_mut() { - for k in &gone { - c.forget_doc(photocraft_doc::DocId(*k)); - } - } res.docs.retain(|k, _| live.contains(k)); } + // Everything the compositor holds for documents that aren't open, also when their + // texture entry was dropped earlier (over budget, a failed render), which used to leave + // their pages and effect maps resident after closing (#4529). + if let Some(c) = res.compositor.as_mut() { + c.retain_docs(|d| live.contains(&d.0)); + } // LUTs and identity-transform signatures can outlive document textures. Prune both, // or a preserved-ID reopen can skip rebuilding a missing LUT. res.luts.retain(|k, _| live.contains(&k.0));