sprite_cache.rs

  1use crate::{
  2    fonts::{FontId, GlyphId},
  3    geometry::{
  4        rect::RectI,
  5        vector::{vec2i, Vector2I},
  6    },
  7    platform,
  8};
  9use etagere::BucketedAtlasAllocator;
 10use metal::{MTLPixelFormat, TextureDescriptor};
 11use ordered_float::OrderedFloat;
 12use std::{collections::HashMap, sync::Arc};
 13
 14#[derive(Hash, Eq, PartialEq)]
 15struct GlyphDescriptor {
 16    font_id: FontId,
 17    font_size: OrderedFloat<f32>,
 18    glyph_id: GlyphId,
 19}
 20
 21#[derive(Clone)]
 22pub struct GlyphSprite {
 23    pub atlas_id: usize,
 24    pub atlas_origin: Vector2I,
 25    pub offset: Vector2I,
 26    pub size: Vector2I,
 27}
 28
 29pub struct SpriteCache {
 30    device: metal::Device,
 31    atlas_size: Vector2I,
 32    fonts: Arc<dyn platform::FontSystem>,
 33    atlasses: Vec<Atlas>,
 34    glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
 35}
 36
 37impl SpriteCache {
 38    pub fn new(
 39        device: metal::Device,
 40        size: Vector2I,
 41        fonts: Arc<dyn platform::FontSystem>,
 42    ) -> Self {
 43        let atlasses = vec![Atlas::new(&device, size)];
 44        Self {
 45            device,
 46            atlas_size: size,
 47            fonts,
 48            atlasses,
 49            glyphs: Default::default(),
 50        }
 51    }
 52
 53    pub fn atlas_size(&self) -> Vector2I {
 54        self.atlas_size
 55    }
 56
 57    pub fn render_glyph(
 58        &mut self,
 59        font_id: FontId,
 60        font_size: f32,
 61        glyph_id: GlyphId,
 62        scale_factor: f32,
 63    ) -> Option<GlyphSprite> {
 64        let fonts = &self.fonts;
 65        let atlasses = &mut self.atlasses;
 66        let atlas_size = self.atlas_size;
 67        let device = &self.device;
 68        self.glyphs
 69            .entry(GlyphDescriptor {
 70                font_id,
 71                font_size: OrderedFloat(font_size),
 72                glyph_id,
 73            })
 74            .or_insert_with(|| {
 75                let (glyph_bounds, mask) =
 76                    fonts.rasterize_glyph(font_id, font_size, glyph_id, scale_factor)?;
 77                assert!(glyph_bounds.width() < atlas_size.x());
 78                assert!(glyph_bounds.height() < atlas_size.y());
 79
 80                let atlas_bounds = atlasses
 81                    .last_mut()
 82                    .unwrap()
 83                    .try_insert(glyph_bounds.size(), &mask)
 84                    .unwrap_or_else(|| {
 85                        let mut atlas = Atlas::new(device, atlas_size);
 86                        let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
 87                        atlasses.push(atlas);
 88                        bounds
 89                    });
 90
 91                Some(GlyphSprite {
 92                    atlas_id: atlasses.len() - 1,
 93                    atlas_origin: atlas_bounds.origin(),
 94                    offset: glyph_bounds.origin(),
 95                    size: glyph_bounds.size(),
 96                })
 97            })
 98            .clone()
 99    }
100
101    pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
102        self.atlasses.get(atlas_id).map(|a| a.texture.as_ref())
103    }
104}
105
106struct Atlas {
107    allocator: BucketedAtlasAllocator,
108    texture: metal::Texture,
109}
110
111impl Atlas {
112    fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
113        let descriptor = TextureDescriptor::new();
114        descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
115        descriptor.set_width(size.x() as u64);
116        descriptor.set_height(size.y() as u64);
117
118        Self {
119            allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
120            texture: device.new_texture(&descriptor),
121        }
122    }
123
124    fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
125        let allocation = self
126            .allocator
127            .allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
128
129        let bounds = allocation.rectangle;
130        let region = metal::MTLRegion::new_2d(
131            bounds.min.x as u64,
132            bounds.min.y as u64,
133            size.x() as u64,
134            size.y() as u64,
135        );
136        self.texture
137            .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
138        Some(RectI::from_points(
139            vec2i(bounds.min.x, bounds.min.y),
140            vec2i(bounds.max.x, bounds.max.y),
141        ))
142    }
143}