use fontdue::Font; use lru::LruCache; use wgpu; /* A note on how Casey Muratori's refterm works in terms of font rendering, because it's not bad and handles things that we're not going to handle because I just want to get some dang text on the dang screen right now. - Step 1: You break the text into runs of characters that need to be rendered together. - Step 2: You figure out how many uniform cells this run occupies. - Step 3: For each cell, you figure out if you already have the necessary part of the run in the texture atlas. (Right? Each part of the run has a distinct ID based on the actual run and the cell within the run.) If you don't have this [run,cell] in the atlas, then: - Step 3a: Render the run as a bitmap, if you haven't already. - Step 3b: Get coordinates from the cache for this [run,cell] pair, evicting something if necessary. - Step 3c: Copy the part of the bitmap for this [run,cell] into the texture atlas at the coordinates. Put the coordinates (either newly generated or pulled from the cache) into the cell. - Step 4: Load all the cells onto the GPU and do a shader that renders the cells. Specifically what I'm doing right now is going character-by-caracter, and not doing runs and not handling things that are wider than a cell. I'm also not doing the efficient big grid render because I want to render characters at pixel offsets and kern between characters and whatnot, which is different from what they're doing. Mine is almost certainly less efficient. */ #[derive(Eq, PartialEq, Hash)] enum CellCacheKey { Garbage(u32), // Exists so that I can prime the LRU, not used generally. GlyphIndex(u16), // Actual factual cache entries. } pub struct FontCache { font: Font, size: f32, texture: wgpu::Texture, cell_width: u16, cell_height: u16, pub view: wgpu::TextureView, pub sampler: wgpu::Sampler, cells: Vec, cell_cache: LruCache, } enum SlotState { Empty, Rendered(u16, fontdue::Metrics), } struct GlyphCell { // The coordinates in the atlas x: u16, y: u16, state: SlotState, } pub struct Glyph { pub x: f32, pub y: f32, pub adjust_x: f32, pub adjust_y: f32, pub w: f32, pub h: f32, pub advance_width: f32, } impl FontCache { pub fn new(device: &wgpu::Device, bytes: &[u8], size: f32) -> Self { let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default()) .expect("Could not parse font"); // Set up the texture that we'll use to cache the glyphs we're rendering. let atlas_width = 2048; let atlas_height = 2048; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Font Glyph Atlas"), size: wgpu::Extent3d { width: atlas_width.into(), height: atlas_height.into(), depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, // 4 for multisample? dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::R8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); // Measure the font to figure out the size of a cell in the cache. // NOTE: This metric nonsense is bad, probably. let mut char_height = 0; if let Some(line_metrics) = font.horizontal_line_metrics(size) { char_height = (line_metrics.new_line_size + 0.5) as usize; } let metrics = font.metrics('M', size); let mut char_width = metrics.width; char_height = metrics.height.max(char_height); let metrics = font.metrics('g', size); char_width = metrics.width.max(char_width); char_height = metrics.height.max(char_height); eprintln!("For this font, width={char_width} height={char_height}"); // Allocate the individual cells in the texture atlas; this records // the state of what's in the cell and whatnot. let mut cells = vec![]; for y in (0..atlas_height).step_by(char_height) { for x in (0..atlas_width).step_by(char_width) { cells.push(GlyphCell { x, y, state: SlotState::Empty, }); } } // Allocate the LRU cache for the cells. Fill it with garbage so that // we can always "allocate" by pulling from the LRU. let mut cell_cache = LruCache::new(std::num::NonZeroUsize::new(cells.len()).unwrap()); for i in 0..cells.len() { cell_cache.put( CellCacheKey::Garbage(i.try_into().expect("Too many cells!")), i, ); } FontCache { font, size, texture, cell_width: char_width.try_into().unwrap(), cell_height: char_height.try_into().unwrap(), view, sampler, cells, cell_cache, } } pub fn get_char(&mut self, queue: &wgpu::Queue, c: char) -> Glyph { let index = self.font.lookup_glyph_index(c); let key = CellCacheKey::GlyphIndex(index); let cell = match self.cell_cache.get(&key) { Some(cell_index) => &mut self.cells[*cell_index], None => { let (_, cell_index) = self .cell_cache .pop_lru() .expect("did not put all available things in the LRU cache"); self.cell_cache.put(key, cell_index); let cell = &mut self.cells[cell_index]; cell.state = SlotState::Empty; // This isn't what it used to be. cell } }; // I mean, technically if we got an LRU hit here it's rendered, but // convincing the compiler of that is a pain. let metrics = match cell.state { SlotState::Rendered(_, metrics) => metrics, SlotState::Empty => { let (metrics, bitmap) = self.font.rasterize_indexed(index, self.size); // eprintln!("Rasterizing '{c}' (index {index}): {metrics:?}"); // For a good time, call // { // eprintln!(); // let mut i = 0; // for _ in (0..metrics.height) { // for _ in (0..metrics.width) { // let bv = bitmap[i]; // let rc = if bv == 0 { // ' ' // } else if bv < 25 { // '.' // } else { // 'X' // }; // eprint!("{rc}"); // i += 1; // } // eprintln!(); // } // eprintln!(); // } let mut texture = self.texture.as_image_copy(); texture.origin.x = cell.x.into(); texture.origin.y = cell.y.into(); // eprintln!(" Rendering to {}, {}", texture.origin.x, texture.origin.y); queue.write_texture( texture, &bitmap, wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(metrics.width as u32), rows_per_image: None, }, wgpu::Extent3d { width: metrics.width as u32, height: metrics.height as u32, depth_or_array_layers: 1, }, ); cell.state = SlotState::Rendered(index, metrics.clone()); metrics } }; Glyph { x: cell.x as f32, y: cell.y as f32, adjust_x: metrics.xmin as f32, adjust_y: (self.cell_height as f32) + floor(-metrics.bounds.height - metrics.bounds.ymin), // PositiveYDown, w: self.cell_width as f32, h: self.cell_height as f32, advance_width: metrics.advance_width, } } } fn floor(x: f32) -> f32 { let mut ui = x.to_bits(); let e = (((ui >> 23) as i32) & 0xff) - 0x7f; if e >= 23 { return x; } if e >= 0 { let m: u32 = 0x007fffff >> e; if (ui & m) == 0 { return x; } if ui >> 31 != 0 { ui += m; } ui &= !m; } else { if ui >> 31 == 0 { ui = 0; } else if ui << 1 != 0 { return -1.0; } } f32::from_bits(ui) } // pub fn inconsolata() { // let font = include_bytes!("./Inconsolata-Regular.ttf") as &[u8]; // let font = fontdue::Font::from_bytes(font, fontdue::FontSettings::default()).unwrap(); // let text = "Hello World!"; // // Break to characters. // let size = 16.0; // let mut prev = None; // let mut left = 0.0; // for c in text.chars() { // // TODO: Cache this. // let (metrics, bitmap) = font.rasterize(c, size); // left += match prev { // Some(pc) => match font.horizontal_kern(pc, c, size) { // Some(k) => k, // None => 0.0, // }, // None => 0.0, // }; // left += metrics.advance_width; // } // }