sprite_cache.rs

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