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: 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_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 atlasses = &mut self.atlasses;
 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 = atlasses
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                        atlasses.push(atlas);
109                        bounds
110                    });
111
112                // Snap sprite to pixel grid.
113                let offset = glyph_bounds.origin().to_f32()
114                    - vec2f(target_position.x().fract(), target_position.y().fract());
115
116                Some(GlyphSprite {
117                    atlas_id: atlasses.len() - 1,
118                    atlas_origin: atlas_bounds.origin(),
119                    offset,
120                    size: glyph_bounds.size(),
121                })
122            })
123            .clone()
124    }
125
126    pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
127        self.atlasses.get(atlas_id).map(|a| a.texture.as_ref())
128    }
129}
130
131struct Atlas {
132    allocator: BucketedAtlasAllocator,
133    texture: metal::Texture,
134}
135
136impl Atlas {
137    fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
138        let descriptor = TextureDescriptor::new();
139        descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
140        descriptor.set_width(size.x() as u64);
141        descriptor.set_height(size.y() as u64);
142
143        Self {
144            allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
145            texture: device.new_texture(&descriptor),
146        }
147    }
148
149    fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
150        let allocation = self
151            .allocator
152            .allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
153
154        let bounds = allocation.rectangle;
155        let region = metal::MTLRegion::new_2d(
156            bounds.min.x as u64,
157            bounds.min.y as u64,
158            size.x() as u64,
159            size.y() as u64,
160        );
161        self.texture
162            .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
163        Some(RectI::from_points(
164            vec2i(bounds.min.x, bounds.min.y),
165            vec2i(bounds.max.x, bounds.max.y),
166        ))
167    }
168}