use super::texture::TextureWrapper; use ab_glyph::*; use etagere::{AllocId, AtlasAllocator}; use glam::Vec4; use lru::LruCache; use std::sync::LazyLock; use ttf_parser::{ Face, Tag, gpos::{PairAdjustment, PositioningSubtable}, }; use wgpu::{Device, Queue, TextureFormat, TextureUsages}; static FONT_DATA: &[u8] = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/assets/KazukiReiwa-Regular.ttf" )); static FONT: LazyLock> = LazyLock::new(|| FontRef::try_from_slice(FONT_DATA).expect("fatal: failed to load font")); static KERN: LazyLock>> = LazyLock::new(|| { let face = Face::parse(FONT_DATA, 0).expect("fatal: failed to load font"); let gpos = face.tables().gpos.expect("fatal: failed to load GPOS"); gpos.features .into_iter() .find(|v| v.tag == Tag::from_bytes(b"kern")) .expect("fatal: failed to load kern") .lookup_indices .into_iter() .filter_map(|v| gpos.lookups.get(v)) .flat_map(|v| v.subtables.into_iter::()) .filter_map(|v| match v { PositioningSubtable::Pair(v) => Some(v), _ => None, }) .collect() }); pub fn kern(scale: f32, left: GlyphId, right: GlyphId) -> f32 { let left = ttf_parser::GlyphId(left.0); let right = ttf_parser::GlyphId(right.0); KERN.iter() .find_map(|v| match v { PairAdjustment::Format1 { coverage, sets } => sets.get(coverage.get(left)?)?.get(right), PairAdjustment::Format2 { coverage, classes, matrix, } => { coverage.get(left)?; matrix.get((classes.0.get(left), classes.1.get(right))) } }) .map_or(0.0, |(v, _)| v.x_advance as f32 * factor(scale)) } pub fn ascent(scale: f32) -> f32 { FONT.ascent_unscaled() * factor(scale) } pub fn line_height(scale: f32) -> f32 { (FONT.height_unscaled() + FONT.line_gap_unscaled()) * factor(scale) } pub fn h_advance(scale: f32, id: GlyphId) -> f32 { FONT.h_advance_unscaled(id) * factor(scale) } fn factor(scale: f32) -> f32 { scale / FONT.units_per_em().unwrap() } const WIDTH: u32 = 512; const HEIGHT: u32 = WIDTH; // NOTE: macOSではswizzleをゼロコストで使えないのでRGBAにする。 const FORMAT: TextureFormat = if cfg!(target_os = "macos") { TextureFormat::Rgba8Unorm } else { TextureFormat::R8Unorm }; /// ラスタライズ可能な最大文字サイズ (論理解像度) const MAX_CHAR_SIZE: f32 = 64.0; /// 1文字テクスチャの1辺におけるマージン /// /// NOTE: 境界で隣の文字のピクセルを含まないように、 /// テクスチャ上で四辺に2pxのマージンを取る。 /// 文字情報(UV)はこのマージンを除いた領域を指定する。 const MARGIN: usize = 2; /// 1文字テクスチャの1方向におけるマージン const MARGIN_AXIS: usize = MARGIN * 2; #[derive(Default)] pub struct CharInfo { pub glyph_id: GlyphId, pub alloc_id: Option, pub width: f32, pub height: f32, pub x_offset: f32, pub y_offset: f32, pub uv: Vec4, } pub struct CharAtlas { /// ウィンドウの倍率 /// /// 物理解像度と論理解像度の差を考慮するため、 /// この倍率に従ってテクスチャアトラス全体をスケールする。 /// /// NOTE: 実行中に変化しうるが、致命的ではないので初期化時の倍率に従うことにする。 scale_factor: f32, /// テクスチャの物理幅 (uv計算用) width: f32, /// テクスチャの物理高 (uv計算用) height: f32, /// 文字テクスチャアトラス /// /// etagereが賢く文字テクスチャを配置してくれる。 atlas: AtlasAllocator, /// 文字テクスチャアトラスに登録されている文字とそのID /// /// NOTE: 文字テクスチャアトラスが満ちている場合に // 最も古く使われた文字を消すために、LRUキャッシュを用いている。 registereds: LruCache<(char, u32), CharInfo>, /// アロケーション回避目的のラスタライズ作業用バッファ scratch: Vec, pub texture: TextureWrapper, } impl CharAtlas { pub fn new(scale_factor: f32, device: &Device) -> Self { let width = WIDTH as f32 * scale_factor; let height = HEIGHT as f32 * scale_factor; let max_side = (MAX_CHAR_SIZE * scale_factor).ceil() as usize + MARGIN_AXIS; let channel_count = FORMAT.block_copy_size(None).unwrap() as usize; Self { scale_factor, width, height, atlas: AtlasAllocator::new(etagere::size2(width as i32, height as i32)), registereds: LruCache::unbounded(), scratch: vec![0x00; channel_count * max_side * max_side], texture: TextureWrapper::new( device, width as u32, height as u32, FORMAT, TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, ), } } /// 文字情報を取得するメソッド /// /// * queue - queue /// * c - 対象文字 /// * scale - 論理解像度上の文字サイズ /// /// 対象文字がテクスチャアトラス上に存在しない場合はラスタライズ・登録を行う。 /// また、同時にLRUキャッシュの更新を行う。 /// /// 対象文字がフォントにない場合(豆腐)やグリフが存在しない場合(空白等)、 /// ラスタライズ・登録を行わず、`glyph_id`のみ有効で・無害な結果を返す。 /// そのため、`CharInfo.alloc_id`が`None`である場合、 /// その`glyph_id`で水平方向のadvanceを取得すること。 /// /// WARN: アトラス上の全ての文字が同時に使用される場合、 /// 一部文字はテクスチャが更新されるため、意図しない結果を得る。 pub fn acquire(&mut self, queue: &Queue, c: char, scale: f32) -> &CharInfo { let key = (c, scale.to_bits()); if !self.registereds.contains(&key) { self.register_character(queue, c, scale); } self.registereds.get(&key).unwrap() } fn register_character(&mut self, queue: &Queue, c: char, scale: f32) { let id = FONT.glyph_id(c); // ラスタライズ let Some(rasterized) = RasterizedChar::new(id, scale * self.scale_factor, &mut self.scratch) else { let info = CharInfo { glyph_id: id, ..Default::default() }; self.registereds.push((c, scale.to_bits()), info); return; }; // アロケート let size = etagere::size2(rasterized.width as i32, rasterized.height as i32); let allocated = loop { if let Some(alloc) = self.atlas.allocate(size) { break alloc; } else { let least_used = self .registereds .pop_lru() .expect("internal error: no character exists but character atlas is full"); if let Some(alloc_id) = least_used.1.alloc_id { self.atlas.deallocate(alloc_id); } } }; // アップロード self.texture.write_partial( queue, &self.scratch, // NOTE: 非負が保証されているため、`u32`にキャストして良い。 allocated.rectangle.min.x as u32, // NOTE: 非負が保証されているため、`u32`にキャストして良い。 allocated.rectangle.min.y as u32, rasterized.width, rasterized.height, ); // 登録 let width = rasterized.width as f32 - MARGIN_AXIS as f32; let height = rasterized.height as f32 - MARGIN_AXIS as f32; let info = CharInfo { glyph_id: id, alloc_id: Some(allocated.id), width: width / self.scale_factor, height: height / self.scale_factor, x_offset: rasterized.x_offset / self.scale_factor, y_offset: rasterized.y_offset / self.scale_factor, uv: Vec4::new( (allocated.rectangle.min.x as f32 + MARGIN as f32) / self.width, (allocated.rectangle.min.y as f32 + MARGIN as f32) / self.height, width / self.width, height / self.height, ), }; self.registereds.push((c, scale.to_bits()), info); } } struct RasterizedChar { width: u32, height: u32, x_offset: f32, y_offset: f32, } impl RasterizedChar { /// 文字をラスタライズするコンストラクタ /// /// * id - 対象グリフ /// * scale - 物理解像度上の文字サイズ /// * data - アロケーション回避用の作業用バッファ /// /// ラスタライズ不可能なグリフに対しては`None`を返す。 fn new(id: GlyphId, scale: f32, data: &mut [u8]) -> Option { let scale = PxScale::from(factor(scale) * FONT.height_unscaled()); let outlined = FONT.outline_glyph(id.with_scale(scale))?; let bounds = outlined.px_bounds(); let width = bounds.width() as usize + MARGIN_AXIS; let height = bounds.height() as usize + MARGIN_AXIS; let channel_count = FORMAT.block_copy_size(None).unwrap() as usize; // 0埋め // // NOTE: 描画ではマージンが初期化されないため。 // マージンだけ0埋めするとコードが煩雑になるため。 data[..channel_count * width * height].fill(0x00); outlined.draw(|x, y, c| { let i = channel_count * (width * (y as usize + MARGIN) + x as usize + MARGIN); data[i] = (c * 255.0) as u8; #[cfg(target_os = "macos")] data[i + 1..i + 4].fill(255); }); Some(Self { width: width as u32, height: height as u32, x_offset: bounds.min.x, y_offset: bounds.min.y, }) } }