sprite_cache.rs

  1use crate::{
  2    fonts::{FontId, GlyphId},
  3    geometry::{
  4        rect::RectI,
  5        vector::{vec2f, 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, u8),
 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    fonts: Arc<dyn platform::FontSystem>,
 34    atlases: 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 atlases = vec![Atlas::new(&device, size)];
 45        Self {
 46            device,
 47            atlas_size: size,
 48            fonts,
 49            atlases,
 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_position: Vector2F,
 64        scale_factor: f32,
 65    ) -> Option<GlyphSprite> {
 66        const SUBPIXEL_VARIANTS: u8 = 4;
 67
 68        let target_position = target_position * scale_factor;
 69        let fonts = &self.fonts;
 70        let atlases = &mut self.atlases;
 71        let atlas_size = self.atlas_size;
 72        let device = &self.device;
 73        let subpixel_variant = (
 74            (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
 75                % SUBPIXEL_VARIANTS,
 76            (target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
 77                % SUBPIXEL_VARIANTS,
 78        );
 79        self.glyphs
 80            .entry(GlyphDescriptor {
 81                font_id,
 82                font_size: OrderedFloat(font_size),
 83                glyph_id,
 84                subpixel_variant,
 85            })
 86            .or_insert_with(|| {
 87                let subpixel_shift = vec2f(
 88                    subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
 89                    subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
 90                );
 91                let (glyph_bounds, mask) = fonts.rasterize_glyph(
 92                    font_id,
 93                    font_size,
 94                    glyph_id,
 95                    subpixel_shift,
 96                    scale_factor,
 97                )?;
 98                assert!(glyph_bounds.width() < atlas_size.x());
 99                assert!(glyph_bounds.height() < atlas_size.y());
100
101                let atlas_bounds = atlases
102                    .last_mut()
103                    .unwrap()
104                    .try_insert(glyph_bounds.size(), &mask)
105                    .unwrap_or_else(|| {
106                        let mut atlas = Atlas::new(device, atlas_size);
107                        let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
108                        atlases.push(atlas);
109                        bounds
110                    });
111
112                Some(GlyphSprite {
113                    atlas_id: atlases.len() - 1,
114                    atlas_origin: atlas_bounds.origin(),
115                    offset: glyph_bounds.origin(),
116                    size: glyph_bounds.size(),
117                })
118            })
119            .clone()
120    }
121
122    pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
123        self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
124    }
125}
126
127struct Atlas {
128    allocator: BucketedAtlasAllocator,
129    texture: metal::Texture,
130}
131
132impl Atlas {
133    fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
134        let descriptor = TextureDescriptor::new();
135        descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
136        descriptor.set_width(size.x() as u64);
137        descriptor.set_height(size.y() as u64);
138
139        Self {
140            allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
141            texture: device.new_texture(&descriptor),
142        }
143    }
144
145    fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
146        let allocation = self
147            .allocator
148            .allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
149
150        let bounds = allocation.rectangle;
151        let region = metal::MTLRegion::new_2d(
152            bounds.min.x as u64,
153            bounds.min.y as u64,
154            size.x() as u64,
155            size.y() as u64,
156        );
157        self.texture
158            .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
159        Some(RectI::from_points(
160            vec2i(bounds.min.x, bounds.min.y),
161            vec2i(bounds.max.x, bounds.max.y),
162        ))
163    }
164}