atagen

monolith

public
Star0
Watch0

Commit 96942b9aee39

tarball

atagen <boss@atagen.co>2026-10-19 10:59 UTC

1 file changed+23-53
cursor: keep only the chosen theme image per shape and size

Cursor held every size and frame of the 17 preloaded shapes, a few MB resident, and cached a failed upload for the life of the process. A shape is now read from the theme when first drawn at a scale and only the image it picked stays. A missing theme file is remembered, an upload failure is retried.
Modified file: crates/monolith/src/cursor.rs+23 additions−53 deletions
@@ -16,7 +16,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1616 use smithay::utils::user_data::UserDataMap;
1717 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform};
1818 use smithay::wayland::compositor::with_states;
19-use std::collections::HashMap;
19+use std::collections::{HashMap, HashSet};
2020 use std::sync::Mutex;
2121
2222 /// A theme shape sized for one output scale.
@@ -30,10 +30,10 @@ struct Prepared {
3030 pub struct Cursor {
3131 theme: xcursor::CursorTheme,
3232 size: u32,
33- /// Every image of the shapes read so far, including ones the theme lacks.
34- files: HashMap<CursorIcon, Option<Vec<xcursor::parser::Image>>>,
3533 /// Shapes sized for an output scale, in steps of 1/120.
36- sized: HashMap<(CursorIcon, u32), Option<Prepared>>,
34+ sized: HashMap<(CursorIcon, u32), Prepared>,
35+ /// Shapes the theme has no file for, which a retry would not change.
36+ absent: HashSet<CursorIcon>,
3737 }
3838
3939 impl Cursor {
@@ -43,55 +43,29 @@ impl Cursor {
4343 .ok()
4444 .and_then(|s| s.parse().ok())
4545 .unwrap_or(24u32);
46- let mut cursor = Self {
46+ let cursor = Self {
4747 theme: xcursor::CursorTheme::load(&theme),
4848 size,
49- files: HashMap::new(),
5049 sized: HashMap::new(),
50+ absent: HashSet::new(),
5151 };
52- if cursor.file(CursorIcon::Default).is_none() {
52+ if cursor.read(CursorIcon::Default).is_none() {
5353 tracing::warn!(theme, "no xcursor theme found, drawing a plain cursor");
5454 }
55- // Reading a shape from the theme takes milliseconds, which in a frame
56- // makes it miss, so the ones clients ask for most load up front.
57- for icon in [
58- CursorIcon::Text,
59- CursorIcon::Pointer,
60- CursorIcon::Grab,
61- CursorIcon::Grabbing,
62- CursorIcon::NotAllowed,
63- CursorIcon::Wait,
64- CursorIcon::Progress,
65- CursorIcon::EwResize,
66- CursorIcon::NsResize,
67- CursorIcon::NResize,
68- CursorIcon::SResize,
69- CursorIcon::EResize,
70- CursorIcon::WResize,
71- CursorIcon::NwResize,
72- CursorIcon::NeResize,
73- CursorIcon::SwResize,
74- CursorIcon::SeResize,
75- ] {
76- cursor.file(icon);
77- }
7855 cursor
7956 }
8057
8158 /// The theme's images for `icon`, falling back to the default arrow.
82- fn file(&mut self, icon: CursorIcon) -> Option<&[xcursor::parser::Image]> {
83- if !self.files.contains_key(&icon) {
84- let names = std::iter::once(icon.name()).chain(icon.alt_names().iter().copied());
85- let images = names
86- .chain(["default", "left_ptr"])
87- .find_map(|n| load_file(&self.theme, n));
88- self.files.insert(icon, images);
89- }
90- self.files.get(&icon)?.as_deref()
59+ fn read(&self, icon: CursorIcon) -> Option<Vec<xcursor::parser::Image>> {
60+ let names = std::iter::once(icon.name()).chain(icon.alt_names().iter().copied());
61+ names
62+ .chain(["default", "left_ptr"])
63+ .find_map(|n| load_file(&self.theme, n))
9164 }
9265
9366 /// The theme's image nearest the cursor size times `scale`, stretched to
94- /// exactly that size so it covers whole pixels.
67+ /// exactly that size so it covers whole pixels. Only that image is kept,
68+ /// and an upload that fails is retried on the next call.
9569 fn sized(
9670 &mut self,
9771 renderer: &mut VulkanRenderer,
@@ -99,21 +73,17 @@ impl Cursor {
9973 scale: f64,
10074 ) -> Option<&Prepared> {
10175 let key = (icon, (scale * 120.0).round() as u32);
102- if !self.sized.contains_key(&key) {
76+ if !self.sized.contains_key(&key) && !self.absent.contains(&icon) {
77+ let Some(images) = self.read(icon) else {
78+ self.absent.insert(icon);
79+ return None;
80+ };
10381 let wanted = ((f64::from(self.size) * scale).round() as u32).max(1);
104- let prepared = self
105- .file(icon)
106- .and_then(|images| images.iter().min_by_key(|i| i.size.abs_diff(wanted)))
107- .and_then(|image| {
108- prepare(
109- renderer,
110- image,
111- f64::from(wanted) / f64::from(image.size.max(1)),
112- )
113- });
114- self.sized.insert(key, prepared);
82+ let image = images.iter().min_by_key(|i| i.size.abs_diff(wanted))?;
83+ let ratio = f64::from(wanted) / f64::from(image.size.max(1));
84+ self.sized.insert(key, prepare(renderer, image, ratio)?);
11585 }
116- self.sized.get(&key)?.as_ref()
86+ self.sized.get(&key)
11787 }
11888
11989 /// Elements for the pointer at `pos`, in output-local coordinates.