Skip to main content

gpui_engine/
atlas.rs

1//! Sprite atlas contract and tile identifiers shared by the engine and its
2//! platform backends.
3
4use crate::{RenderGlyphParams, RenderImageParams, RenderSvgParams};
5use anyhow::Result;
6use collections::FxHashMap;
7use gpui_types::{Bounds, DevicePixels, Point, Size};
8use std::{borrow::Cow, collections::hash_map::Entry, ops};
9
10#[derive(PartialEq, Eq, Hash, Clone)]
11#[expect(missing_docs)]
12pub enum AtlasKey {
13    Glyph(RenderGlyphParams),
14    Svg(RenderSvgParams),
15    Image(RenderImageParams),
16}
17
18impl AtlasKey {
19    /// Returns the texture kind for this atlas key.
20    pub fn texture_kind(&self) -> AtlasTextureKind {
21        match self {
22            AtlasKey::Glyph(params) => {
23                if params.is_emoji {
24                    AtlasTextureKind::Polychrome
25                } else if params.subpixel_rendering {
26                    AtlasTextureKind::Subpixel
27                } else {
28                    AtlasTextureKind::Monochrome
29                }
30            }
31            AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
32            AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
33        }
34    }
35}
36
37impl From<RenderGlyphParams> for AtlasKey {
38    fn from(params: RenderGlyphParams) -> Self {
39        Self::Glyph(params)
40    }
41}
42
43impl From<RenderSvgParams> for AtlasKey {
44    fn from(params: RenderSvgParams) -> Self {
45        Self::Svg(params)
46    }
47}
48
49impl From<RenderImageParams> for AtlasKey {
50    fn from(params: RenderImageParams) -> Self {
51        Self::Image(params)
52    }
53}
54
55#[expect(missing_docs)]
56pub trait PlatformAtlas {
57    /// The builder runs with the atlas locked and must not re-enter the same atlas.
58    fn get_or_insert_with<'a>(
59        &self,
60        key: AtlasKey,
61        build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
62    ) -> Result<Option<AtlasTile>>;
63    fn remove(&self, key: &AtlasKey);
64
65    #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
66    fn contains(&self, _key: &AtlasKey) -> bool {
67        false
68    }
69}
70
71#[doc(hidden)]
72pub trait AtlasBackend {
73    fn insert(
74        &mut self,
75        kind: AtlasTextureKind,
76        size: Size<DevicePixels>,
77        bytes: &[u8],
78    ) -> Result<AtlasTile>;
79
80    fn remove(&mut self, tile: AtlasTile);
81}
82
83#[doc(hidden)]
84pub struct AtlasState<Backend> {
85    tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
86    pub backend: Backend,
87}
88
89impl<Backend> AtlasState<Backend> {
90    pub fn new(backend: Backend) -> Self {
91        Self {
92            tiles_by_key: FxHashMap::default(),
93            backend,
94        }
95    }
96
97    pub fn contains(&self, key: &AtlasKey) -> bool {
98        self.tiles_by_key.contains_key(key)
99    }
100
101    pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
102        self.tiles_by_key.clear();
103        reset_backend(&mut self.backend);
104    }
105}
106
107impl<Backend: Default> Default for AtlasState<Backend> {
108    fn default() -> Self {
109        Self::new(Backend::default())
110    }
111}
112
113impl<Backend: AtlasBackend> AtlasState<Backend> {
114    pub fn get_or_insert_with<'a>(
115        &mut self,
116        key: AtlasKey,
117        build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
118    ) -> Result<Option<AtlasTile>> {
119        match self.tiles_by_key.entry(key) {
120            Entry::Occupied(entry) => Ok(Some(*entry.get())),
121            Entry::Vacant(entry) => {
122                profiling::scope!("new tile");
123                let Some((size, bytes)) = build()? else {
124                    return Ok(None);
125                };
126                let tile = self
127                    .backend
128                    .insert(entry.key().texture_kind(), size, &bytes)?;
129                entry.insert(tile);
130                Ok(Some(tile))
131            }
132        }
133    }
134
135    pub fn remove(&mut self, key: &AtlasKey) {
136        if let Some(tile) = self.tiles_by_key.remove(key) {
137            self.backend.remove(tile);
138        }
139    }
140}
141
142/// A sprite atlas for windows without a GPU. It hands out uniquely identified
143/// tiles without uploading any pixels, so glyph, SVG, and image painting can
144/// run to completion in tests and headless platforms.
145#[derive(Default)]
146pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
147
148#[doc(hidden)]
149#[derive(Default)]
150pub struct HeadlessAtlasBackend {
151    next_id: u32,
152}
153
154impl AtlasBackend for HeadlessAtlasBackend {
155    fn insert(
156        &mut self,
157        kind: AtlasTextureKind,
158        size: Size<DevicePixels>,
159        _bytes: &[u8],
160    ) -> Result<AtlasTile> {
161        self.next_id += 1;
162        let texture_id = self.next_id;
163        self.next_id += 1;
164        let tile_id = self.next_id;
165        Ok(AtlasTile {
166            texture_id: AtlasTextureId {
167                index: texture_id,
168                kind,
169            },
170            tile_id: TileId(tile_id),
171            padding: 0,
172            bounds: Bounds {
173                origin: Point::default(),
174                size,
175            },
176        })
177    }
178
179    fn remove(&mut self, _tile: AtlasTile) {}
180}
181
182impl PlatformAtlas for HeadlessAtlas {
183    fn get_or_insert_with<'a>(
184        &self,
185        key: AtlasKey,
186        build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
187    ) -> Result<Option<AtlasTile>> {
188        self.0.lock().get_or_insert_with(key, build)
189    }
190
191    fn remove(&self, key: &AtlasKey) {
192        self.0.lock().remove(key);
193    }
194
195    #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
196    fn contains(&self, key: &AtlasKey) -> bool {
197        self.0.lock().contains(key)
198    }
199}
200
201#[doc(hidden)]
202pub struct AtlasTextureList<T> {
203    pub textures: Vec<Option<T>>,
204    pub free_list: Vec<usize>,
205}
206
207impl<T> Default for AtlasTextureList<T> {
208    fn default() -> Self {
209        Self {
210            textures: Vec::default(),
211            free_list: Vec::default(),
212        }
213    }
214}
215
216impl<T> ops::Index<usize> for AtlasTextureList<T> {
217    type Output = Option<T>;
218
219    fn index(&self, index: usize) -> &Self::Output {
220        &self.textures[index]
221    }
222}
223
224impl<T> AtlasTextureList<T> {
225    #[allow(unused)]
226    pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
227        self.free_list.clear();
228        self.textures.drain(..)
229    }
230
231    #[allow(dead_code)]
232    pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
233        self.textures.iter_mut().flatten()
234    }
235}
236
237/// A tile within a sprite atlas texture.
238#[derive(Copy, Clone, Debug, PartialEq, Eq)]
239#[repr(C)]
240pub struct AtlasTile {
241    /// The texture this tile belongs to.
242    pub texture_id: AtlasTextureId,
243    /// The unique ID of this tile within its texture.
244    pub tile_id: TileId,
245    /// Padding around the tile content in pixels.
246    pub padding: u32,
247    /// The bounds of this tile within the texture.
248    pub bounds: Bounds<DevicePixels>,
249}
250
251/// Identifies a texture within the sprite atlas.
252#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
253#[repr(C)]
254pub struct AtlasTextureId {
255    // We use u32 instead of usize for Metal Shader Language compatibility
256    /// The index of this texture in the atlas.
257    pub index: u32,
258    /// The kind of content stored in this texture.
259    pub kind: AtlasTextureKind,
260}
261
262/// The kind of content stored in an atlas texture.
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264#[repr(C)]
265#[cfg_attr(
266    all(
267        any(target_os = "linux", target_os = "freebsd"),
268        not(any(feature = "x11", feature = "wayland"))
269    ),
270    allow(dead_code)
271)]
272pub enum AtlasTextureKind {
273    /// Single-channel coverage.
274    Monochrome = 0,
275    /// Full-color pixels.
276    Polychrome = 1,
277    /// Subpixel-antialiased coverage.
278    Subpixel = 2,
279}
280
281/// The unique ID of a tile within its texture.
282#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
283#[repr(C)]
284pub struct TileId(pub u32);
285
286impl From<etagere::AllocId> for TileId {
287    fn from(id: etagere::AllocId) -> Self {
288        Self(id.serialize())
289    }
290}
291
292impl From<TileId> for etagere::AllocId {
293    fn from(id: TileId) -> Self {
294        Self::deserialize(id.0)
295    }
296}
297
298#[cfg(test)]
299mod atlas_tests {
300    use super::*;
301
302    use anyhow::Context as _;
303
304    const TILE_SIZE: Size<DevicePixels> = Size {
305        width: DevicePixels(1),
306        height: DevicePixels(1),
307    };
308
309    #[derive(Default)]
310    struct RecordingAtlasBackend {
311        insert_calls: u32,
312        fail_next_insert: bool,
313        removed_tiles: Vec<AtlasTile>,
314    }
315
316    impl AtlasBackend for RecordingAtlasBackend {
317        fn insert(
318            &mut self,
319            kind: AtlasTextureKind,
320            size: Size<DevicePixels>,
321            _bytes: &[u8],
322        ) -> Result<AtlasTile> {
323            self.insert_calls += 1;
324            if std::mem::take(&mut self.fail_next_insert) {
325                anyhow::bail!("backend failed");
326            }
327            Ok(AtlasTile {
328                texture_id: AtlasTextureId { index: 0, kind },
329                tile_id: TileId(self.insert_calls),
330                padding: 0,
331                bounds: Bounds {
332                    origin: Point::default(),
333                    size,
334                },
335            })
336        }
337
338        fn remove(&mut self, tile: AtlasTile) {
339            self.removed_tiles.push(tile);
340        }
341    }
342
343    fn image_key(image_id: usize) -> AtlasKey {
344        AtlasKey::Image(RenderImageParams {
345            image_id: crate::ImageId(image_id),
346            frame_index: 0,
347        })
348    }
349
350    fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
351        Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
352    }
353
354    #[test]
355    fn only_successful_inserts_are_cached() -> Result<()> {
356        let mut state = AtlasState::new(RecordingAtlasBackend::default());
357        let key = image_key(1);
358
359        assert_eq!(
360            state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
361            None
362        );
363        state
364            .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
365            .expect_err("builder error should propagate");
366        assert!(!state.contains(&key));
367        assert_eq!(state.backend.insert_calls, 0);
368
369        state.backend.fail_next_insert = true;
370        state
371            .get_or_insert_with(key.clone(), &mut build_tile)
372            .expect_err("backend error should propagate");
373        assert!(!state.contains(&key));
374        assert_eq!(state.backend.insert_calls, 1);
375
376        let tile = state
377            .get_or_insert_with(key.clone(), &mut build_tile)?
378            .context("builder should produce a tile")?;
379        assert_eq!(tile.texture_id.kind, key.texture_kind());
380        assert_eq!(
381            state.get_or_insert_with(key.clone(), &mut || {
382                anyhow::bail!("cache hit must not call the builder")
383            })?,
384            Some(tile)
385        );
386        assert!(state.contains(&key));
387        assert_eq!(state.backend.insert_calls, 2);
388        Ok(())
389    }
390
391    #[test]
392    fn remove_and_clear_invalidate_keys() -> Result<()> {
393        let mut state = AtlasState::new(RecordingAtlasBackend::default());
394        let key = image_key(1);
395        let other_key = image_key(2);
396        let tile = state
397            .get_or_insert_with(key.clone(), &mut build_tile)?
398            .context("builder should produce a tile")?;
399        state
400            .get_or_insert_with(other_key.clone(), &mut build_tile)?
401            .context("builder should produce another tile")?;
402
403        state.remove(&key);
404        state.remove(&key);
405        assert!(!state.contains(&key));
406        assert!(state.contains(&other_key));
407        assert_eq!(state.backend.removed_tiles, vec![tile]);
408
409        let mut reset_calls = 0;
410        state.clear(|_| reset_calls += 1);
411        assert_eq!(reset_calls, 1);
412        assert!(!state.contains(&other_key));
413        assert_eq!(state.backend.removed_tiles, vec![tile]);
414        Ok(())
415    }
416}