syrillian_render 0.7.1

Renderer of the Syrillian Game Engine
Documentation
use dashmap::DashSet;
use fdsm::bezier::scanline::FillRule;
use fdsm::generate::generate_msdf;
use fdsm::render::correct_sign_msdf;
use fdsm::shape::Shape;
use fdsm::transform::Transform;
use fdsm_ttf_parser::load_shape_from_face;
use image::RgbImage;
use parking_lot::RwLock;
use std::sync::Arc;
use syrillian_asset::{Font, HMaterialInstance, HTexture2D};
use ttf_parser::Face;
use wgpu::{Device, Queue};

use crate::cache::glyph::GlyphBitmap;
use crate::cache::msdf_atlas::{FontLineMetrics, GlyphAtlasEntry, MsdfAtlas};
use crate::cache::{AssetCache, CacheType};
#[cfg(not(target_arch = "wasm32"))]
use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
use fdsm::bezier::prepared::PreparedColoredShape;
use nalgebra::{Affine2, Matrix3};

pub mod glyph;
pub mod msdf_atlas;

pub struct FontAtlas {
    atlas: Arc<RwLock<MsdfAtlas>>,
    requested: DashSet<char>,

    #[cfg(not(target_arch = "wasm32"))]
    gen_tx: Sender<char>,
    #[cfg(not(target_arch = "wasm32"))]
    ready_rx: Receiver<GlyphBitmap>,

    #[cfg(target_arch = "wasm32")]
    pending: RwLock<std::collections::VecDeque<char>>,

    #[cfg(target_arch = "wasm32")]
    wasm_face_bytes: Arc<Vec<u8>>,
    #[cfg(target_arch = "wasm32")]
    wasm_units_per_em: f32,
    #[cfg(target_arch = "wasm32")]
    wasm_shrinkage: f64,
    #[cfg(target_arch = "wasm32")]
    wasm_range: f64,
}

impl CacheType for Font {
    type Hot = Arc<FontAtlas>;

    fn upload(self, _device: &Device, _queue: &Queue, cache: &AssetCache) -> Self::Hot {
        let msdf = MsdfAtlas::new(
            self.font_bytes.clone(),
            self.atlas_em_px,
            16.0,
            4.0,
            cache.store(),
        );
        let atlas = Arc::new(RwLock::new(msdf));

        #[cfg(not(target_arch = "wasm32"))]
        let (gen_tx, ready_rx) = spawn_native_worker(&atlas);

        #[cfg(target_arch = "wasm32")]
        let (pending, wasm_face_bytes, wasm_units_per_em, wasm_shrinkage, wasm_range) =
            prepare_wasm_state(&atlas);

        Arc::new(FontAtlas {
            atlas,
            requested: DashSet::new(),

            #[cfg(not(target_arch = "wasm32"))]
            gen_tx,
            #[cfg(not(target_arch = "wasm32"))]
            ready_rx,

            #[cfg(target_arch = "wasm32")]
            pending,
            #[cfg(target_arch = "wasm32")]
            wasm_face_bytes,
            #[cfg(target_arch = "wasm32")]
            wasm_units_per_em,
            #[cfg(target_arch = "wasm32")]
            wasm_shrinkage,
            #[cfg(target_arch = "wasm32")]
            wasm_range,
        })
    }
}

impl FontAtlas {
    pub fn atlas(&self) -> HMaterialInstance {
        self.atlas.read().material()
    }
    pub fn texture(&self) -> HTexture2D {
        self.atlas.read().texture()
    }
    pub fn metrics(&self) -> FontLineMetrics {
        self.atlas.read().metrics()
    }

    pub fn face_data(&self) -> (Arc<Vec<u8>>, f32) {
        let atlas = self.atlas.read();
        let (bytes, units_per_em, ..) = atlas.font_params();
        (bytes, units_per_em)
    }

    pub fn entry(&self, ch: char) -> Option<GlyphAtlasEntry> {
        self.atlas.read().entry(ch)
    }

    pub fn request_glyphs(&self, chars: impl IntoIterator<Item = char>) {
        for ch in chars {
            self.enqueue_glyph_if_missing(ch);
        }
    }

    pub fn pump(&self, cache: &AssetCache, queue: &Queue, max_glyphs: usize) -> bool {
        if self.requested.is_empty() {
            return false;
        }

        let mut processed = 0;
        let mut updated = false;

        #[cfg(not(target_arch = "wasm32"))]
        while processed < max_glyphs {
            match self.ready_rx.try_recv() {
                Ok(bmp) => {
                    updated |= self.integrate_ready_bitmap(cache, queue, bmp);
                    processed += 1;
                }
                Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => break,
            }
        }

        #[cfg(target_arch = "wasm32")]
        while processed < max_glyphs {
            let Some(ch) = self.pending.write().unwrap().pop_front() else {
                break;
            };

            if let Some(bmp) = rasterize_msdf_glyph(
                &self.wasm_face_bytes,
                ch,
                self.wasm_shrinkage,
                self.wasm_range,
                self.wasm_units_per_em,
            ) {
                updated |= self.integrate_ready_bitmap(cache, queue, bmp);
            } else {
                self.requested.remove(&ch);
            }
            processed += 1;
        }

        updated
    }

    fn enqueue_glyph_if_missing(&self, ch: char) {
        if self.atlas.read().contains(ch) {
            return;
        }

        if !self.requested.insert(ch) {
            return;
        }

        #[cfg(not(target_arch = "wasm32"))]
        let _ = self.gen_tx.send(ch);

        #[cfg(target_arch = "wasm32")]
        self.pending.write().unwrap().push_back(ch);
    }

    fn integrate_ready_bitmap(
        &self,
        cache: &AssetCache,
        queue: &Queue,
        bitmap: GlyphBitmap,
    ) -> bool {
        let ch = bitmap.ch;
        let integrated = self
            .atlas
            .write()
            .integrate_ready_glyph(cache, queue, bitmap)
            .is_some();

        self.requested.remove(&ch);
        integrated
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn spawn_native_worker(atlas: &Arc<RwLock<MsdfAtlas>>) -> (Sender<char>, Receiver<GlyphBitmap>) {
    let (tx_req, rx_req) = unbounded();
    let (tx_ready, rx_ready) = unbounded();
    let (face_bytes, units_per_em, shrinkage, range) = atlas.read().font_params();

    std::thread::spawn(move || {
        while let Ok(ch) = rx_req.recv() {
            if let Some(bmp) = rasterize_msdf_glyph(&face_bytes, ch, shrinkage, range, units_per_em)
                && tx_ready.send(bmp).is_err()
            {
                break;
            }
        }
    });

    (tx_req, rx_ready)
}

#[cfg(target_arch = "wasm32")]
fn prepare_wasm_state(
    atlas: &Arc<RwLock<MsdfAtlas>>,
) -> (
    RwLock<std::collections::VecDeque<char>>,
    Arc<Vec<u8>>,
    f32,
    f64,
    f64,
) {
    let (fb, upm, s, r) = atlas.read().unwrap().font_params();
    (RwLock::default(), fb, upm, s, r)
}

fn rasterize_msdf_glyph(
    face_bytes: &Arc<Vec<u8>>,
    ch: char,
    shrinkage: f64,
    range: f64,
    metrics_units_per_em: f32,
) -> Option<GlyphBitmap> {
    let face = Face::parse(face_bytes, 0).ok()?;
    let gid = face.glyph_index(ch)?;

    let bbox = glyph_bounds(&face, gid);
    let plane = plane_bounds(metrics_units_per_em, bbox, shrinkage, range);
    let (width_px, height_px) = glyph_dimensions(bbox, shrinkage, range);
    let transform = glyph_transform(bbox, shrinkage, range);

    let mut shape: Shape<_> = load_shape_from_face(&face, gid)?;
    shape.transform(&transform);
    let prepared = Shape::edge_coloring_simple(shape, 0.03, 0xD15EA5u64).prepare();

    let msdf = build_msdf_image(&prepared, width_px, height_px, range);
    let pixels_rgba = expand_to_rgba(&msdf);

    let adv_units = face.glyph_hor_advance(gid).unwrap_or(0) as f32;
    let advance_em = adv_units / metrics_units_per_em;

    Some(GlyphBitmap {
        ch,
        width_px,
        height_px,
        plane_min: plane.min,
        plane_max: plane.max,
        advance_em,
        msdf_range_px: range as f32,
        pixels_rgba,
    })
}

fn glyph_bounds(face: &Face, gid: ttf_parser::GlyphId) -> ttf_parser::Rect {
    face.glyph_bounding_box(gid).unwrap_or(ttf_parser::Rect {
        x_min: 0,
        y_min: 0,
        x_max: 1,
        y_max: 1,
    })
}

struct PlaneBounds {
    min: [f32; 2],
    max: [f32; 2],
}

fn plane_bounds(
    units_per_em: f32,
    bbox: ttf_parser::Rect,
    shrinkage: f64,
    range: f64,
) -> PlaneBounds {
    let upm = units_per_em as f64;
    let left_em = bbox.x_min as f64 / upm;
    let right_em = bbox.x_max as f64 / upm;
    let bottom_em = bbox.y_min as f64 / upm;
    let top_em = bbox.y_max as f64 / upm;
    let pad_em = (range * shrinkage) / upm;

    PlaneBounds {
        min: [(left_em - pad_em) as f32, (bottom_em - pad_em) as f32],
        max: [(right_em + pad_em) as f32, (top_em + pad_em) as f32],
    }
}

fn glyph_dimensions(bbox: ttf_parser::Rect, shrinkage: f64, range: f64) -> (u32, u32) {
    let width_px = (((bbox.x_max - bbox.x_min) as f64) / shrinkage + 2.0 * range)
        .ceil()
        .max(1.0) as u32;
    let height_px = (((bbox.y_max - bbox.y_min) as f64) / shrinkage + 2.0 * range)
        .ceil()
        .max(1.0) as u32;
    (width_px, height_px)
}

fn glyph_transform(bbox: ttf_parser::Rect, shrinkage: f64, range: f64) -> Affine2<f64> {
    let s = 1.0 / shrinkage;
    let tx = range - (bbox.x_min as f64) * s;
    let ty = range + (bbox.y_max as f64) * s;

    Affine2::from_matrix_unchecked(Matrix3::new(s, 0.0, tx, 0.0, -s, ty, 0.0, 0.0, 1.0))
}

fn build_msdf_image(
    prepared: &PreparedColoredShape,
    width_px: u32,
    height_px: u32,
    range: f64,
) -> RgbImage {
    let mut msdf = RgbImage::new(width_px, height_px);
    generate_msdf(prepared, range, &mut msdf);
    correct_sign_msdf(&mut msdf, prepared, FillRule::Nonzero);
    msdf
}

fn expand_to_rgba(msdf: &RgbImage) -> Vec<u8> {
    let mut pixels_rgba = vec![0u8; (msdf.width() as usize) * (msdf.height() as usize) * 4];
    let src = msdf.as_raw();

    for (dst, chunk) in pixels_rgba.chunks_exact_mut(4).zip(src.chunks_exact(3)) {
        dst[..3].copy_from_slice(chunk);
        dst[3] = 255;
    }

    pixels_rgba
}