1use 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 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 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#[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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
239#[repr(C)]
240pub struct AtlasTile {
241 pub texture_id: AtlasTextureId,
243 pub tile_id: TileId,
245 pub padding: u32,
247 pub bounds: Bounds<DevicePixels>,
249}
250
251#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
253#[repr(C)]
254pub struct AtlasTextureId {
255 pub index: u32,
258 pub kind: AtlasTextureKind,
260}
261
262#[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 Monochrome = 0,
275 Polychrome = 1,
277 Subpixel = 2,
279}
280
281#[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}