sprite_cache.rs

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