WIP: Start on rendering glyphs

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                              |  26 ++++++
gpui/Cargo.toml                         |   1 
gpui/build.rs                           |   2 
gpui/src/platform/mac/mod.rs            |   1 
gpui/src/platform/mac/renderer.rs       |  78 ++++++++++++++++++
gpui/src/platform/mac/shaders/shaders.h |  13 +++
gpui/src/platform/mac/sprite_cache.rs   |  19 ++++
gpui/src/scene.rs                       |  29 ++++++
gpui/src/text_layout.rs                 | 115 +++++++++++---------------
9 files changed, 215 insertions(+), 69 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -549,6 +549,25 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "etagere"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520d7de540904fd09b11c03a47d50a7ce4ff37d1aa763f454fa60d9088ef8356"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51e5bac4ec41ece6346fd867815a57a221abdf48f4eb931b033789b5b4b6fc70"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "event-listener"
 version = "2.5.1"
@@ -745,6 +764,7 @@ dependencies = [
  "core-graphics",
  "core-text",
  "ctor",
+ "etagere",
  "font-kit",
  "foreign-types",
  "log",
@@ -1483,6 +1503,12 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
 [[package]]
 name = "syn"
 version = "1.0.60"

gpui/Cargo.toml 🔗

@@ -7,6 +7,7 @@ version = "0.1.0"
 [dependencies]
 async-task = {git = "https://github.com/zedit-io/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"}
 ctor = "0.1"
+etagere = "0.2"
 num_cpus = "1.13"
 ordered-float = "2.1.1"
 parking_lot = "0.11.1"

gpui/build.rs 🔗

@@ -89,6 +89,8 @@ fn generate_shader_bindings() {
         .whitelist_type("GPUIQuad")
         .whitelist_type("GPUIShadowInputIndex")
         .whitelist_type("GPUIShadow")
+        .whitelist_type("GPUISpriteInputIndex")
+        .whitelist_type("GPUISprite")
         .parse_callbacks(Box::new(bindgen::CargoCallbacks))
         .generate()
         .expect("unable to generate bindings");

gpui/src/platform/mac/renderer.rs 🔗

@@ -1,4 +1,4 @@
-use std::{ffi::c_void, mem};
+use std::{collections::HashMap, ffi::c_void, mem};
 
 use self::shaders::ToUchar4;
 
@@ -15,8 +15,10 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
 pub struct Renderer {
     quad_pipeline_state: metal::RenderPipelineState,
     shadow_pipeline_state: metal::RenderPipelineState,
+    sprite_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
+    sprite_cache: SpriteCache,
 }
 
 impl Renderer {
@@ -60,6 +62,14 @@ impl Renderer {
                 "shadow_fragment",
                 pixel_format,
             )?,
+            sprite_pipeline_state: build_pipeline_state(
+                device,
+                &library,
+                "sprite",
+                "sprite_vertex",
+                "sprite_fragment",
+                pixel_format,
+            )?,
             unit_vertices,
             instances,
         })
@@ -79,6 +89,7 @@ impl Renderer {
         for layer in scene.layers() {
             self.render_shadows(scene, layer, &mut offset, ctx);
             self.render_quads(scene, layer, &mut offset, ctx);
+            self.render_sprites(scene, layer, &mut offset, ctx);
         }
     }
 
@@ -234,6 +245,71 @@ impl Renderer {
             layer.quads().len() as u64,
         );
     }
+
+    fn render_sprites(
+        &mut self,
+        scene: &Scene,
+        layer: &Layer,
+        offset: &mut usize,
+        ctx: &RenderContext,
+    ) {
+        if layer.glyphs().is_empty() {
+            return;
+        }
+
+        align_offset(offset);
+        let next_offset = *offset + layer.glyphs().len() * mem::size_of::<shaders::GPUISprite>();
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        let mut sprites = HashMap::new();
+        for glyph in layer.glyphs() {
+            let (atlas, bounds) =
+                self.sprite_cache
+                    .rasterize_glyph(glyph.font_id, glyph.font_size, glyph.glyph_id);
+            sprites
+                .entry(atlas)
+                .or_insert_with(Vec::new)
+                .push(shaders::GPUISprite {
+                    origin: glyph.origin.to_float2(),
+                    size: bounds.size().to_float2(),
+                    atlas_origin: bounds.origin().to_float2(),
+                    color: glyph.color.to_uchar4(),
+                });
+        }
+
+        ctx.command_encoder
+            .set_render_pipeline_state(&self.sprite_pipeline_state);
+        ctx.command_encoder.set_vertex_buffer(
+            shaders::GPUISpriteInputIndex_GPUISpriteInputIndexVertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        ctx.command_encoder.set_vertex_buffer(
+            shaders::GPUISpriteInputIndex_GPUISpriteInputIndexSprites as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        ctx.command_encoder.set_vertex_bytes(
+            shaders::GPUISpriteInputIndex_GPUISpriteInputIndexUniforms as u64,
+            mem::size_of::<shaders::GPUIUniforms>() as u64,
+            [shaders::GPUIUniforms {
+                viewport_size: ctx.drawable_size.to_float2(),
+            }]
+            .as_ptr() as *const c_void,
+        );
+
+        let buffer_contents = unsafe {
+            (self.instances.contents() as *mut u8).offset(*offset as isize)
+                as *mut shaders::GPUISprite
+        };
+
+        for glyph in layer.glyphs() {
+            let sprite = self.sprite_cache.rasterize_glyph();
+        }
+    }
 }
 
 fn align_offset(offset: &mut usize) {

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -35,3 +35,16 @@ typedef struct {
     float sigma;
     vector_uchar4 color;
 } GPUIShadow;
+
+typedef enum {
+    GPUISpriteInputIndexVertices = 0,
+    GPUISpriteInputIndexSprites = 1,
+    GPUISpriteInputIndexUniforms = 2,
+} GPUISpriteInputIndex;
+
+typedef struct {
+    vector_float2 origin;
+    vector_float2 size;
+    vector_float2 atlas_origin;
+    vector_uchar4 color;
+} GPUISprite;

gpui/src/platform/mac/sprite_cache.rs 🔗

@@ -0,0 +1,19 @@
+use crate::geometry::vector::Vector2I;
+use etagere::BucketedAtlasAllocator;
+
+struct SpriteCache {
+    atlasses: Vec<etagere::BucketedAtlasAllocator>,
+}
+
+impl SpriteCache {
+    fn new(size: Vector2I) -> Self {
+        let size = etagere::Size::new(size.x(), size.y());
+        Self {
+            atlasses: vec![BucketedAtlasAllocator::new(size)],
+        }
+    }
+
+    fn render_glyph(&mut self) {
+        self.atlasses.last().unwrap()
+    }
+}

gpui/src/scene.rs 🔗

@@ -1,6 +1,9 @@
-use core::f32;
+use crate::{
+    color::ColorU,
+    fonts::{FontId, GlyphId},
+    geometry::{rect::RectF, vector::Vector2F},
+};
 
-use crate::{color::ColorU, geometry::rect::RectF};
 pub struct Scene {
     scale_factor: f32,
     layers: Vec<Layer>,
@@ -12,6 +15,7 @@ pub struct Layer {
     clip_bounds: Option<RectF>,
     quads: Vec<Quad>,
     shadows: Vec<Shadow>,
+    glyphs: Vec<Glyph>,
 }
 
 #[derive(Default, Debug)]
@@ -30,6 +34,15 @@ pub struct Shadow {
     pub color: ColorU,
 }
 
+#[derive(Debug)]
+pub struct Glyph {
+    pub font_id: FontId,
+    pub font_size: f32,
+    pub glyph_id: GlyphId,
+    pub origin: Vector2F,
+    pub color: ColorU,
+}
+
 #[derive(Clone, Copy, Default, Debug)]
 pub struct Border {
     pub width: f32,
@@ -76,6 +89,10 @@ impl Scene {
         self.active_layer().push_shadow(shadow)
     }
 
+    pub fn push_glyph(&mut self, glyph: Glyph) {
+        self.active_layer().push_glyph(glyph)
+    }
+
     fn active_layer(&mut self) -> &mut Layer {
         &mut self.layers[*self.active_layer_stack.last().unwrap()]
     }
@@ -97,6 +114,14 @@ impl Layer {
     pub fn shadows(&self) -> &[Shadow] {
         self.shadows.as_slice()
     }
+
+    fn push_glyph(&mut self, glyph: Glyph) {
+        self.glyphs.push(glyph);
+    }
+
+    pub fn glyphs(&self) -> &[Glyph] {
+        self.glyphs.as_slice()
+    }
 }
 
 impl Border {

gpui/src/text_layout.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     color::ColorU,
     fonts::{FontCache, FontId, GlyphId},
     geometry::rect::RectF,
-    PaintContext,
+    scene, PaintContext,
 };
 use core_foundation::{
     attributed_string::CFMutableAttributedString,
@@ -186,71 +186,54 @@ impl Line {
         }
     }
 
-    pub fn paint(
-        &self,
-        _bounds: RectF,
-        _colors: &[(Range<usize>, ColorU)],
-        _ctx: &mut PaintContext,
-    ) {
-        // canvas.set_font_size(self.font_size);
-        // let mut colors = colors.iter().peekable();
-
-        // for run in &self.runs {
-        //     let bounding_box = font_cache.bounding_box(run.font_id, self.font_size);
-        //     let ascent = font_cache.scale_metric(
-        //         font_cache.metric(run.font_id, |m| m.ascent),
-        //         run.font_id,
-        //         self.font_size,
-        //     );
-        //     let descent = font_cache.scale_metric(
-        //         font_cache.metric(run.font_id, |m| m.descent),
-        //         run.font_id,
-        //         self.font_size,
-        //     );
-
-        //     let max_glyph_width = bounding_box.x();
-        //     let font = font_cache.font(run.font_id);
-        //     let font_name = font_cache.font_name(run.font_id);
-        //     let is_emoji = font_cache.is_emoji(run.font_id);
-        //     for glyph in &run.glyphs {
-        //         let glyph_origin = origin + glyph.position - vec2f(0.0, descent);
-
-        //         if glyph_origin.x() + max_glyph_width < viewport_rect.origin().x() {
-        //             continue;
-        //         }
-
-        //         if glyph_origin.x() > viewport_rect.upper_right().x() {
-        //             break;
-        //         }
-
-        //         while let Some((range, color)) = colors.peek() {
-        //             if glyph.index >= range.end {
-        //                 colors.next();
-        //             } else {
-        //                 if glyph.index == range.start {
-        //                     canvas.set_fill_style(FillStyle::Color(*color));
-        //                 }
-        //                 break;
-        //             }
-        //         }
-
-        //         if is_emoji {
-        //             match font_cache.render_emoji(glyph.id, self.font_size) {
-        //                 Ok(image) => {
-        //                     canvas.draw_image(image, RectF::new(glyph_origin, bounding_box));
-        //                 }
-        //                 Err(error) => log::error!("rasterizing emoji: {}", error),
-        //             }
-        //         } else {
-        //             canvas.fill_glyph(
-        //                 &font,
-        //                 &font_name,
-        //                 glyph.id,
-        //                 glyph_origin + vec2f(0.0, ascent),
-        //             );
-        //         }
-        //     }
-        // }
+    pub fn paint(&self, bounds: RectF, colors: &[(Range<usize>, ColorU)], ctx: &mut PaintContext) {
+        let mut colors = colors.iter().peekable();
+        let mut color = ColorU::black();
+
+        for run in &self.runs {
+            let bounding_box = ctx.font_cache.bounding_box(run.font_id, self.font_size);
+            let ascent = ctx.font_cache.scale_metric(
+                ctx.font_cache.metric(run.font_id, |m| m.ascent),
+                run.font_id,
+                self.font_size,
+            );
+            let descent = ctx.font_cache.scale_metric(
+                ctx.font_cache.metric(run.font_id, |m| m.descent),
+                run.font_id,
+                self.font_size,
+            );
+
+            let max_glyph_width = bounding_box.x();
+            let font = ctx.font_cache.font(run.font_id);
+            let font_name = ctx.font_cache.font_name(run.font_id);
+            let is_emoji = ctx.font_cache.is_emoji(run.font_id);
+            for glyph in &run.glyphs {
+                let glyph_origin = bounds.origin() + glyph.position;
+                if glyph_origin.x() + max_glyph_width < bounds.origin().x() {
+                    continue;
+                }
+                if glyph_origin.x() > bounds.upper_right().x() {
+                    break;
+                }
+
+                while let Some((range, next_color)) = colors.peek() {
+                    if glyph.index >= range.end {
+                        colors.next();
+                    } else {
+                        color = *next_color;
+                        break;
+                    }
+                }
+
+                ctx.scene.push_glyph(scene::Glyph {
+                    font_id: run.font_id,
+                    font_size: self.font_size,
+                    glyph_id: glyph.id,
+                    origin: glyph_origin,
+                    color,
+                });
+            }
+        }
     }
 }