cryoglyph/
text_atlas.rs

1use crate::{
2    Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::ContentType,
3};
4use etagere::{Allocation, BucketedAtlasAllocator, size2};
5use lru::LruCache;
6use rustc_hash::FxHasher;
7use std::{collections::HashSet, hash::BuildHasherDefault};
8use wgpu::{
9    BindGroup, DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue,
10    RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect,
11    TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
12    TextureViewDescriptor,
13};
14
15type Hasher = BuildHasherDefault<FxHasher>;
16
17#[allow(dead_code)]
18pub(crate) struct InnerAtlas {
19    pub kind: Kind,
20    pub texture: Texture,
21    pub texture_view: TextureView,
22    pub packer: BucketedAtlasAllocator,
23    pub size: u32,
24    pub glyph_cache: LruCache<CacheKey, GlyphDetails, Hasher>,
25    pub glyphs_in_use: HashSet<CacheKey, Hasher>,
26    pub max_texture_dimension_2d: u32,
27}
28
29impl InnerAtlas {
30    const INITIAL_SIZE: u32 = 256;
31
32    fn new(device: &Device, _queue: &Queue, kind: Kind) -> Self {
33        let max_texture_dimension_2d = device.limits().max_texture_dimension_2d;
34        let size = Self::INITIAL_SIZE.min(max_texture_dimension_2d);
35
36        let packer = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
37
38        // Create a texture to use for our atlas
39        let texture = device.create_texture(&TextureDescriptor {
40            label: Some("glyphon atlas"),
41            size: Extent3d {
42                width: size,
43                height: size,
44                depth_or_array_layers: 1,
45            },
46            mip_level_count: 1,
47            sample_count: 1,
48            dimension: TextureDimension::D2,
49            format: kind.texture_format(),
50            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
51            view_formats: &[],
52        });
53
54        let texture_view = texture.create_view(&TextureViewDescriptor::default());
55
56        let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default());
57        let glyphs_in_use = HashSet::with_hasher(Hasher::default());
58
59        Self {
60            kind,
61            texture,
62            texture_view,
63            packer,
64            size,
65            glyph_cache,
66            glyphs_in_use,
67            max_texture_dimension_2d,
68        }
69    }
70
71    pub(crate) fn try_allocate(&mut self, width: usize, height: usize) -> Option<Allocation> {
72        let size = size2(width as i32, height as i32);
73
74        loop {
75            let allocation = self.packer.allocate(size);
76
77            if allocation.is_some() {
78                return allocation;
79            }
80
81            // Try to free least recently used allocation
82            let (mut key, mut value) = self.glyph_cache.peek_lru()?;
83
84            // Find a glyph with an actual size
85            while value.atlas_id.is_none() {
86                // All sized glyphs are in use, cache is full
87                if self.glyphs_in_use.contains(key) {
88                    return None;
89                }
90
91                let _ = self.glyph_cache.pop_lru();
92
93                (key, value) = self.glyph_cache.peek_lru()?;
94            }
95
96            // All sized glyphs are in use, cache is full
97            if self.glyphs_in_use.contains(key) {
98                return None;
99            }
100
101            let (_, value) = self.glyph_cache.pop_lru().unwrap();
102            self.packer.deallocate(value.atlas_id.unwrap());
103        }
104    }
105
106    pub fn num_channels(&self) -> usize {
107        self.kind.num_channels()
108    }
109
110    pub(crate) fn grow(
111        &mut self,
112        device: &wgpu::Device,
113        queue: &wgpu::Queue,
114        font_system: &mut FontSystem,
115        cache: &mut SwashCache,
116    ) -> bool {
117        if self.size >= self.max_texture_dimension_2d {
118            return false;
119        }
120
121        // Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
122        // factor of `Vec`.`
123        const GROWTH_FACTOR: u32 = 2;
124        let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
125
126        self.packer.grow(size2(new_size as i32, new_size as i32));
127
128        // Create a texture to use for our atlas
129        self.texture = device.create_texture(&TextureDescriptor {
130            label: Some("glyphon atlas"),
131            size: Extent3d {
132                width: new_size,
133                height: new_size,
134                depth_or_array_layers: 1,
135            },
136            mip_level_count: 1,
137            sample_count: 1,
138            dimension: TextureDimension::D2,
139            format: self.kind.texture_format(),
140            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
141            view_formats: &[],
142        });
143
144        // Re-upload glyphs
145        for (&cache_key, glyph) in &self.glyph_cache {
146            let (x, y) = match glyph.gpu_cache {
147                GpuCacheStatus::InAtlas { x, y, .. } => (x, y),
148                GpuCacheStatus::SkipRasterization => continue,
149            };
150
151            let image = cache.get_image_uncached(font_system, cache_key).unwrap();
152
153            let width = image.placement.width as usize;
154            let height = image.placement.height as usize;
155
156            queue.write_texture(
157                TexelCopyTextureInfo {
158                    texture: &self.texture,
159                    mip_level: 0,
160                    origin: Origin3d {
161                        x: x as u32,
162                        y: y as u32,
163                        z: 0,
164                    },
165                    aspect: TextureAspect::All,
166                },
167                &image.data,
168                TexelCopyBufferLayout {
169                    offset: 0,
170                    bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32),
171                    rows_per_image: None,
172                },
173                Extent3d {
174                    width: width as u32,
175                    height: height as u32,
176                    depth_or_array_layers: 1,
177                },
178            );
179        }
180
181        self.texture_view = self.texture.create_view(&TextureViewDescriptor::default());
182        self.size = new_size;
183
184        true
185    }
186
187    fn trim(&mut self) {
188        self.glyphs_in_use.clear();
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub(crate) enum Kind {
194    Mask,
195    Color { srgb: bool },
196}
197
198impl Kind {
199    fn num_channels(self) -> usize {
200        match self {
201            Kind::Mask => 1,
202            Kind::Color { .. } => 4,
203        }
204    }
205
206    fn texture_format(self) -> wgpu::TextureFormat {
207        match self {
208            Kind::Mask => TextureFormat::R8Unorm,
209            Kind::Color { srgb } => {
210                if srgb {
211                    TextureFormat::Rgba8UnormSrgb
212                } else {
213                    TextureFormat::Rgba8Unorm
214                }
215            }
216        }
217    }
218}
219
220/// The color mode of an [`Atlas`].
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum ColorMode {
223    /// Accurate color management.
224    ///
225    /// This mode will use a proper sRGB texture for colored glyphs. This will
226    /// produce physically accurate color blending when rendering.
227    Accurate,
228
229    /// Web color management.
230    ///
231    /// This mode reproduces the color management strategy used in the Web and
232    /// implemented by browsers.
233    ///
234    /// This entails storing glyphs colored using the sRGB color space in a
235    /// linear RGB texture. Blending will not be physically accurate, but will
236    /// produce the same results as most UI toolkits.
237    ///
238    /// This mode should be used to render to a linear RGB texture containing
239    /// sRGB colors.
240    Web,
241}
242
243/// An atlas containing a cache of rasterized glyphs that can be rendered.
244pub struct TextAtlas {
245    cache: Cache,
246    pub(crate) bind_group: BindGroup,
247    pub(crate) color_atlas: InnerAtlas,
248    pub(crate) mask_atlas: InnerAtlas,
249    pub(crate) format: TextureFormat,
250    pub(crate) color_mode: ColorMode,
251}
252
253impl TextAtlas {
254    /// Creates a new [`TextAtlas`].
255    pub fn new(device: &Device, queue: &Queue, cache: &Cache, format: TextureFormat) -> Self {
256        Self::with_color_mode(device, queue, cache, format, ColorMode::Accurate)
257    }
258
259    /// Creates a new [`TextAtlas`] with the given [`ColorMode`].
260    pub fn with_color_mode(
261        device: &Device,
262        queue: &Queue,
263        cache: &Cache,
264        format: TextureFormat,
265        color_mode: ColorMode,
266    ) -> Self {
267        let color_atlas = InnerAtlas::new(
268            device,
269            queue,
270            Kind::Color {
271                srgb: match color_mode {
272                    ColorMode::Accurate => true,
273                    ColorMode::Web => false,
274                },
275            },
276        );
277        let mask_atlas = InnerAtlas::new(device, queue, Kind::Mask);
278
279        let bind_group = cache.create_atlas_bind_group(
280            device,
281            &color_atlas.texture_view,
282            &mask_atlas.texture_view,
283        );
284
285        Self {
286            cache: cache.clone(),
287            bind_group,
288            color_atlas,
289            mask_atlas,
290            format,
291            color_mode,
292        }
293    }
294
295    pub fn trim(&mut self) {
296        self.mask_atlas.trim();
297        self.color_atlas.trim();
298    }
299
300    pub(crate) fn grow(
301        &mut self,
302        device: &wgpu::Device,
303        queue: &wgpu::Queue,
304        font_system: &mut FontSystem,
305        cache: &mut SwashCache,
306        content_type: ContentType,
307    ) -> bool {
308        let did_grow = match content_type {
309            ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache),
310            ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache),
311        };
312
313        if did_grow {
314            self.rebind(device);
315        }
316
317        did_grow
318    }
319
320    pub(crate) fn inner_for_content_mut(&mut self, content_type: ContentType) -> &mut InnerAtlas {
321        match content_type {
322            ContentType::Color => &mut self.color_atlas,
323            ContentType::Mask => &mut self.mask_atlas,
324        }
325    }
326
327    pub(crate) fn get_or_create_pipeline(
328        &self,
329        device: &Device,
330        multisample: MultisampleState,
331        depth_stencil: Option<DepthStencilState>,
332    ) -> RenderPipeline {
333        self.cache
334            .get_or_create_pipeline(device, self.format, multisample, depth_stencil)
335    }
336
337    fn rebind(&mut self, device: &wgpu::Device) {
338        self.bind_group = self.cache.create_atlas_bind_group(
339            device,
340            &self.color_atlas.texture_view,
341            &self.mask_atlas.texture_view,
342        );
343    }
344}