]> git.nega.tv - josh/narcissus/commitdiff
Cache glyphs instead of re-drawing
authorJoshua Simmons <josh@nega.tv>
Mon, 27 Feb 2023 19:18:48 +0000 (20:18 +0100)
committerJoshua Simmons <josh@nega.tv>
Mon, 27 Feb 2023 19:18:48 +0000 (20:18 +0100)
bins/narcissus/src/main.rs
bins/narcissus/src/shaders/text.vert.glsl
libs/narcissus-font/src/cache.rs

index b5269b8360e3ae3d21a166a4ad6ecc126f552cae..7848f73908ee43cdc32748da58e654b37fe9a4b4 100644 (file)
@@ -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::<CachedGlyph>() * 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);
index b8fba72136fa797d43683684a02cfcfa9c59d5ed..44519e577077819a2225324016492926b573dc9e 100644 (file)
@@ -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;
index 65af7e913a11912f43b47bd7fcf3229e18da8451..32cb2e422352126917777100f0fd7b193af6f69f 100644 (file)
@@ -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))
     }
 }