Retired: free images a view stopped drawing

GPUI keeps every painted RenderImage in the window's atlas until
drop_image; views that repaint tiles grew without bound.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 16:27:45 -04:00
1 parent fd0efc7939
commit cd50824049
3 files changed
+73 -1

No files matched your search

+2
View File
@@ -9,6 +9,7 @@ mod image;
mod layer;
mod measure;
mod pressable;
mod retired;
mod severity;
mod tooltip;
@@ -31,5 +32,6 @@ pub(crate) use image::{checked_ratio, framed};
pub use layer::{Raised, raise};
pub use measure::{IntersectionObserver, Measure};
pub use pressable::Pressable;
pub use retired::Retired;
pub use severity::Severity;
pub use tooltip::{HelpTooltip, Tooltip, TooltipTrigger};
+30
View File
@@ -0,0 +1,30 @@
//! Images a view stopped drawing, freed from the window's atlas.
use std::sync::Arc;
use gpui::{RenderImage, Window};
/// Images to free; GPUI keeps each painted image until told.
#[derive(Default)]
pub struct Retired(Vec<Arc<RenderImage>>);
impl Retired {
/// An image no longer drawn; freed at the next paint.
pub fn push(&mut self, image: Arc<RenderImage>) {
self.0.push(image);
}
/// Frees every retired image; call while painting.
pub fn release(&mut self, window: &mut Window) {
for image in self.0.drain(..) {
if let Err(error) = window.drop_image(image) {
log::error!("images: a retired image stayed: {error}");
}
}
}
/// Whether nothing waits to be freed.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
+41 -1
View File
@@ -217,8 +217,48 @@ fn f6_leaves_a_field_that_keeps_tab(cx: &mut TestAppContext) {
let after = view.read_with(cx, |kept, _| kept.after.clone());
cx.simulate_keystrokes("f6");
assert_eq!(view.read_with(cx, |kept, _| kept.indents), 1, "Tab stayed in the field");
cx.update(|window, cx| assert!(after.is_focused(window), "F6 moved on"));
cx.update(|window, _| assert!(after.is_focused(window), "F6 moved on"));
cx.simulate_keystrokes("shift-f6");
let field = view.read_with(cx, |kept, _| kept.field.clone());
cx.update(|window, _| assert!(field.is_focused(window), "Shift-F6 came back"));
}
/// A view that paints one image until told to retire it.
struct Painted {
image: Option<std::sync::Arc<gpui::RenderImage>>,
retired: super::Retired,
}
impl Render for Painted {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
gpui::canvas(
|_, _, _| {},
move |bounds, _, window, cx| {
view.update(cx, |painted, _| {
painted.retired.release(window);
if let Some(image) = painted.image.clone() {
window.paint_image(bounds, bounds, Default::default(), image, 0, false).expect("paints");
}
})
},
)
.size(px(10.0))
}
}
#[gpui::test]
fn a_retired_image_is_freed_at_the_next_paint(cx: &mut TestAppContext) {
cx.update(Theme::init);
let pixels = image::RgbaImage::from_pixel(2, 2, image::Rgba([1, 2, 3, 255]));
let image = std::sync::Arc::new(gpui::RenderImage::new([image::Frame::new(pixels)]));
let (view, cx) = cx.add_window_view(|_, _| Painted { image: Some(image), retired: super::Retired::default() });
cx.run_until_parked();
view.update(cx, |painted, cx| {
let old = painted.image.take().expect("painted");
painted.retired.push(old);
cx.notify();
});
cx.run_until_parked();
assert!(view.read_with(cx, |painted, _| painted.retired.is_empty()), "the old image was freed");
}