From: Joshua Simmons Date: Mon, 27 Feb 2023 19:18:48 +0000 (+0100) Subject: Cache glyphs instead of re-drawing X-Git-Url: https://git.nega.tv//gitweb.cgi?a=commitdiff_plain;h=bb7677c6e695cabfccc374ccf4f3fcb4c0550582;p=josh%2Fnarcissus Cache glyphs instead of re-drawing --- diff --git a/bins/narcissus/src/main.rs b/bins/narcissus/src/main.rs index b5269b8..7848f73 100644 --- a/bins/narcissus/src/main.rs +++ b/bins/narcissus/src/main.rs @@ -1,6 +1,6 @@ use std::{path::Path, time::Instant}; -use narcissus_app::{create_app, Event, Key, WindowDesc}; +use narcissus_app::{create_app, Event, Key, PressedState, WindowDesc}; use narcissus_core::{default, obj, rand::Pcg64}; use narcissus_font::{CachedGlyph, CachedGlyphIndex, FontCollection, GlyphCache}; use narcissus_gpu::{ @@ -26,6 +26,8 @@ mod pipelines; const MAX_SHARKS: usize = 262_144; const NUM_SHARKS: usize = 50; +const GLYPH_CACHE_WIDTH: usize = 1024; +const GLYPH_CACHE_HEIGHT: usize = 512; const MAX_GLYPH_INSTANCES: usize = 262_144; const MAX_GLYPHS: usize = 8192; @@ -335,7 +337,7 @@ pub fn main() { let text_pipeline = TextPipeline::new(device.as_ref()); let fonts = Fonts::new(); - let mut glyph_cache = GlyphCache::new(&fonts, 1024, 512, 1); + let mut glyph_cache = GlyphCache::new(&fonts, GLYPH_CACHE_WIDTH, GLYPH_CACHE_HEIGHT, 1); let blåhaj_image = load_image("bins/narcissus/data/blåhaj.png"); let (blåhaj_vertices, blåhaj_indices) = load_obj("bins/narcissus/data/blåhaj.obj"); @@ -399,6 +401,36 @@ pub fn main() { std::mem::size_of::() * MAX_GLYPHS, ); + let glyph_atlas = device.create_image(&ImageDesc { + location: MemoryLocation::Device, + usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER_DST, + dimension: ImageDimension::Type2d, + format: ImageFormat::R8_UNORM, + initial_layout: ImageLayout::Optimal, + width: glyph_cache.width() as u32, + height: glyph_cache.height() as u32, + depth: 1, + layer_count: 1, + mip_levels: 1, + }); + + { + let frame = device.begin_frame(); + let mut cmd_buffer = device.create_cmd_buffer(&frame, &thread_token); + device.cmd_barrier( + &mut cmd_buffer, + None, + &[ImageBarrier::layout_optimal( + &[Access::None], + &[Access::ShaderSampledImageRead], + glyph_atlas, + ImageAspectFlags::COLOR, + )], + ); + device.submit(&frame, cmd_buffer); + device.end_frame(frame); + } + let text_sampler = device.create_sampler(&SamplerDesc { filter: SamplerFilter::Bilinear, address_mode: SamplerAddressMode::Clamp, @@ -428,6 +460,8 @@ pub fn main() { } } + let mut num_chars = 0; + let start_time = Instant::now(); 'main: loop { let frame = device.begin_frame(); @@ -438,12 +472,16 @@ pub fn main() { KeyPress { window_id: _, key, - pressed: _, + pressed, modifiers: _, } => { if key == Key::Escape { break 'main; } + if key == Key::Space && pressed == PressedState::Released { + num_chars += 1; + println!("{num_chars}"); + } } Quit => { break 'main; @@ -554,7 +592,7 @@ pub fn main() { y = y.trunc(); glyph_indices.clear(); - glyph_indices.extend(text.chars().map(|c| { + glyph_indices.extend(text.chars().take(num_chars).map(|c| { font.glyph_index(c) .unwrap_or_else(|| font.glyph_index('□').unwrap()) })); @@ -586,84 +624,73 @@ pub fn main() { let atlas_width = glyph_cache.width() as u32; let atlas_height = glyph_cache.height() as u32; - let (cached_glyphs, texture) = glyph_cache.update_atlas(); - text_uniforms.write(TextUniforms { screen_width: width, screen_height: height, atlas_width, atlas_height, }); - cached_glyph_buffer.write_slice(cached_glyphs); glyph_instance_buffer.write_slice(&glyph_instances); - // upload atlas - let glyph_atlas = { - let width = atlas_width; - let height = atlas_height; - let data = texture; - - let buffer = - create_buffer_with_data(device.as_ref(), BufferUsageFlags::TRANSFER_SRC, data); - - let image = device.create_image(&ImageDesc { - location: MemoryLocation::Device, - usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER_DST, - dimension: ImageDimension::Type2d, - format: ImageFormat::R8_UNORM, - initial_layout: ImageLayout::Optimal, - width, - height, - depth: 1, - layer_count: 1, - mip_levels: 1, - }); - - device.cmd_barrier( - &mut cmd_buffer, - None, - &[ImageBarrier::layout_optimal( - &[Access::None], - &[Access::TransferWrite], - image, - ImageAspectFlags::COLOR, - )], - ); - - device.cmd_copy_buffer_to_image( - &mut cmd_buffer, - buffer, - image, - ImageLayout::Optimal, - &[BufferImageCopy { - buffer_offset: 0, - buffer_row_length: 0, - buffer_image_height: 0, - image_subresource: default(), - image_offset: Offset3d { x: 0, y: 0, z: 0 }, - image_extent: Extent3d { - width, - height, - depth: 1, - }, - }], - ); - - device.cmd_barrier( - &mut cmd_buffer, - None, - &[ImageBarrier::layout_optimal( - &[Access::TransferWrite], - &[Access::FragmentShaderSampledImageRead], + if let Some((cached_glyphs, texture)) = glyph_cache.update_atlas() { + cached_glyph_buffer.write_slice(cached_glyphs); + + // upload atlas + { + let width = atlas_width; + let height = atlas_height; + let image = glyph_atlas; + let data = texture; + + let buffer = + create_buffer_with_data(device.as_ref(), BufferUsageFlags::TRANSFER_SRC, data); + + device.cmd_barrier( + &mut cmd_buffer, + None, + &[ImageBarrier::layout_optimal( + &[Access::ShaderSampledImageRead], + &[Access::TransferWrite], + image, + ImageAspectFlags::COLOR, + )], + ); + + device.cmd_copy_buffer_to_image( + &mut cmd_buffer, + buffer, image, - ImageAspectFlags::COLOR, - )], - ); - - device.destroy_buffer(&frame, buffer); - - image - }; + ImageLayout::Optimal, + &[BufferImageCopy { + buffer_offset: 0, + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: default(), + image_offset: Offset3d { x: 0, y: 0, z: 0 }, + image_extent: Extent3d { + width, + height, + depth: 1, + }, + }], + ); + + device.cmd_barrier( + &mut cmd_buffer, + None, + &[ImageBarrier::layout_optimal( + &[Access::TransferWrite], + &[Access::FragmentShaderSampledImageRead], + image, + ImageAspectFlags::COLOR, + )], + ); + + device.destroy_buffer(&frame, buffer); + + image + }; + } device.cmd_begin_rendering( &mut cmd_buffer, @@ -818,8 +845,6 @@ pub fn main() { device.cmd_end_rendering(&mut cmd_buffer); - device.destroy_image(&frame, glyph_atlas); - device.submit(&frame, cmd_buffer); device.end_frame(frame); diff --git a/bins/narcissus/src/shaders/text.vert.glsl b/bins/narcissus/src/shaders/text.vert.glsl index b8fba72..44519e5 100644 --- a/bins/narcissus/src/shaders/text.vert.glsl +++ b/bins/narcissus/src/shaders/text.vert.glsl @@ -48,18 +48,18 @@ void main() { vec2(cg.offset_x1, cg.offset_y1) }; - vec2 halfScreenSize = vec2(screenWidth, screenHeight) / 2.0; - vec2 glyphPosition = vec2(gi.x, gi.y); - vec2 vertexPosition = positions[gl_VertexIndex] + glyphPosition; - vec2 position = vertexPosition / halfScreenSize - 1.0; - gl_Position = vec4(position, 0.0, 1.0); - vec2 texcoords[4] = { vec2(cg.x0, cg.y0), vec2(cg.x0, cg.y1), vec2(cg.x1, cg.y0), vec2(cg.x1, cg.y1) }; + + vec2 halfScreenSize = vec2(screenWidth, screenHeight) / 2.0; + vec2 glyphPosition = vec2(gi.x, gi.y); + vec2 vertexPosition = positions[gl_VertexIndex] + glyphPosition; + vec2 position = vertexPosition / halfScreenSize - 1.0; + gl_Position = vec4(position, 0.0, 1.0); outTexcoord = texcoords[gl_VertexIndex] / vec2(atlasWidth, atlasHeight); vec4 color = unpackUnorm4x8(gi.color).bgra; diff --git a/libs/narcissus-font/src/cache.rs b/libs/narcissus-font/src/cache.rs index 65af7e9..32cb2e4 100644 --- a/libs/narcissus-font/src/cache.rs +++ b/libs/narcissus-font/src/cache.rs @@ -75,11 +75,11 @@ where padding, - glyphs: Vec::new(), - next_cached_glyph_index: 0, cached_glyph_lookup: Default::default(), + glyphs: Vec::new(), + packer: Packer::new(width - padding, height - padding), rects: Vec::new(), @@ -126,66 +126,94 @@ where }) } - pub fn update_atlas(&mut self) -> (&[CachedGlyph], &[u8]) { - // We recreate the CachedGlyphs structure completely every update, so reset the index here. - self.next_cached_glyph_index = 0; + pub fn update_atlas(&mut self) -> Option<(&[CachedGlyph], &[u8])> { + let glyphs_len = self.cached_glyphs.len(); - self.glyphs.clear(); - self.glyphs.extend(self.cached_glyph_lookup.iter().map( - |(glyph_key, &cached_glyph_index)| Glyph { - family: glyph_key.family, - glyph_index: glyph_key.glyph_index, - size_px: glyph_key.size_px, - cached_glyph_index, - }, - )); + // If we have the same number of glyphs as we have cached, then there's nothing to do. + if glyphs_len == self.cached_glyph_lookup.len() { + return None; + } - // Sort just so we avoid ping-ponging between fonts during rendering. - self.glyphs.sort_unstable(); + // Extend the glyphs list with the new glyphs. + self.glyphs + .extend(self.cached_glyph_lookup.iter().filter_map( + |(glyph_key, &cached_glyph_index)| { + if cached_glyph_index.0 < glyphs_len as u32 { + None + } else { + Some(Glyph { + family: glyph_key.family, + glyph_index: glyph_key.glyph_index, + size_px: glyph_key.size_px, + cached_glyph_index, + }) + } + }, + )); + + // The new glyphs might not be in the right order, because HashMap doesn't gaurantee + // iteration order. So we need to sort them here by their cached_glyph_index. + self.glyphs[glyphs_len..].sort_by_key( + |Glyph { + family: _, + glyph_index: _, + size_px: _, + cached_glyph_index, + }| *cached_glyph_index, + ); + + debug_assert!(self + .glyphs + .iter() + .enumerate() + .all(|(i, cached_glyph)| cached_glyph.cached_glyph_index.0 as usize == i)); let padding = self.padding as i32; - self.rects.clear(); - self.rects.extend(self.glyphs.iter().map(|glyph| { - let font = self.fonts.font(glyph.family); - let size_px = glyph.size_px.get(); - let scale = font.scale_for_size_px(size_px); - let oversample = Self::oversample_for_size(size_px); + // Add new rects for the new cached glyphs. + self.rects + .extend(self.glyphs[glyphs_len..].iter().map(|glyph| { + let font = self.fonts.font(glyph.family); + let size_px = glyph.size_px.get(); + let scale = font.scale_for_size_px(size_px); + let oversample = Self::oversample_for_size(size_px); + + let bitmap_box = font.glyph_bitmap_box( + glyph.glyph_index, + scale * oversample.as_f32(), + scale * oversample.as_f32(), + 0.0, + 0.0, + ); + + let w = bitmap_box.x1 - bitmap_box.x0 + padding + oversample.as_i32() - 1; + let h = bitmap_box.y1 - bitmap_box.y0 + padding + oversample.as_i32() - 1; + + Rect { + id: glyph.cached_glyph_index.0 as i32, + w, + h, + x: 0, + y: 0, + was_packed: 0, + } + })); + + // TODO: Emergency re-pack when this fails, dropping glyphs unused this frame. + assert!(self.packer.pack(&mut self.rects[glyphs_len..])); - let bitmap_box = font.glyph_bitmap_box( - glyph.glyph_index, - scale * oversample.as_f32(), - scale * oversample.as_f32(), - 0.0, - 0.0, - ); - - let w = bitmap_box.x1 - bitmap_box.x0 + padding + oversample.as_i32() - 1; - let h = bitmap_box.y1 - bitmap_box.y0 + padding + oversample.as_i32() - 1; - - Rect { - id: glyph.cached_glyph_index.0 as i32, - w, - h, - x: 0, - y: 0, - was_packed: 0, - } - })); - - self.packer.clear(); - assert!(self.packer.pack(&mut self.rects)); - - self.texture.fill(0); self.cached_glyphs .resize(self.glyphs.len(), CachedGlyph::default()); - let padding = self.padding as i32; - - for (glyph, rect) in self.glyphs.iter().zip(self.rects.iter_mut()) { + // Render the new glyphs. + for (glyph, rect) in self.glyphs[glyphs_len..] + .iter() + .zip(self.rects[glyphs_len..].iter_mut()) + { let font = self.fonts.font(glyph.family); // Pad on left and top. + let padding = self.padding as i32; rect.x += padding; rect.y += padding; rect.w -= padding; @@ -240,6 +268,6 @@ where cached_glyph.offset_y1 = (y0 + rect.h) as f32 / oversample.as_f32() + sub_y; } - (&self.cached_glyphs, &self.texture) + Some((&self.cached_glyphs, &self.texture)) } }