WIP: Render path winding numbers to stencil buffer

Max Brunsfeld and Nathan Sobo created

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

Change summary

gpui/src/geometry.rs                        |  60 +++++-----
gpui/src/platform/mac/renderer.rs           | 109 +++++++++++++++++++
gpui/src/platform/mac/shaders/shaders.h     |  10 +
gpui/src/platform/mac/shaders/shaders.metal |  28 +++++
gpui/src/scene.rs                           |  28 +++++
zed/src/editor/buffer_element.rs            | 126 ++++++++++++++--------
6 files changed, 277 insertions(+), 84 deletions(-)

Detailed changes

gpui/src/geometry.rs 🔗

@@ -1,31 +1,27 @@
 pub use pathfinder_geometry::*;
 
+use super::scene::{Path, PathVertex};
 use vector::{vec2f, Vector2F};
 
-pub(crate) struct Vertex {
-    xy_position: Vector2F,
-    st_position: Vector2F,
-}
-
-pub struct Path {
-    vertices: Vec<Vertex>,
+pub struct PathBuilder {
+    vertices: Vec<PathVertex>,
     start: Vector2F,
     current: Vector2F,
-    countours_len: usize,
+    contour_count: usize,
 }
 
-enum Kind {
+enum PathVertexKind {
     Solid,
     Quadratic,
 }
 
-impl Path {
-    fn new() -> Self {
+impl PathBuilder {
+    pub fn new() -> Self {
         Self {
             vertices: Vec::new(),
             start: vec2f(0., 0.),
             current: vec2f(0., 0.),
-            countours_len: 0,
+            contour_count: 0,
         }
     }
 
@@ -33,58 +29,60 @@ impl Path {
         self.vertices.clear();
         self.start = point;
         self.current = point;
-        self.countours_len = 0;
+        self.contour_count = 0;
     }
 
     pub fn line_to(&mut self, point: Vector2F) {
-        self.countours_len += 1;
-        if self.countours_len > 1 {
-            self.push_triangle(self.start, self.current, point, Kind::Solid);
+        self.contour_count += 1;
+        if self.contour_count > 1 {
+            self.push_triangle(self.start, self.current, point, PathVertexKind::Solid);
         }
 
         self.current = point;
     }
 
     pub fn curve_to(&mut self, point: Vector2F, ctrl: Vector2F) {
-        self.countours_len += 1;
-        if self.countours_len > 1 {
-            self.push_triangle(self.start, self.current, point, Kind::Solid);
+        self.contour_count += 1;
+        if self.contour_count > 1 {
+            self.push_triangle(self.start, self.current, point, PathVertexKind::Solid);
         }
 
-        self.push_triangle(self.current, ctrl, point, Kind::Quadratic);
+        self.push_triangle(self.current, ctrl, point, PathVertexKind::Quadratic);
         self.current = point;
     }
 
-    pub(crate) fn close(self) -> Vec<Vertex> {
-        self.vertices
+    pub fn build(self) -> Path {
+        Path {
+            vertices: self.vertices,
+        }
     }
 
-    fn push_triangle(&mut self, a: Vector2F, b: Vector2F, c: Vector2F, kind: Kind) {
+    fn push_triangle(&mut self, a: Vector2F, b: Vector2F, c: Vector2F, kind: PathVertexKind) {
         match kind {
-            Kind::Solid => {
-                self.vertices.push(Vertex {
+            PathVertexKind::Solid => {
+                self.vertices.push(PathVertex {
                     xy_position: a,
                     st_position: vec2f(0., 1.),
                 });
-                self.vertices.push(Vertex {
+                self.vertices.push(PathVertex {
                     xy_position: b,
                     st_position: vec2f(0., 1.),
                 });
-                self.vertices.push(Vertex {
+                self.vertices.push(PathVertex {
                     xy_position: c,
                     st_position: vec2f(0., 1.),
                 });
             }
-            Kind::Quadratic => {
-                self.vertices.push(Vertex {
+            PathVertexKind::Quadratic => {
+                self.vertices.push(PathVertex {
                     xy_position: a,
                     st_position: vec2f(0., 0.),
                 });
-                self.vertices.push(Vertex {
+                self.vertices.push(PathVertex {
                     xy_position: b,
                     st_position: vec2f(0.5, 0.),
                 });
-                self.vertices.push(Vertex {
+                self.vertices.push(PathVertex {
                     xy_position: c,
                     st_position: vec2f(1., 1.),
                 });

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

@@ -22,6 +22,7 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
 struct RenderContext<'a> {
     drawable_size: Vector2F,
     command_encoder: &'a metal::RenderCommandEncoderRef,
+    command_buffer: &'a metal::CommandBufferRef,
 }
 
 pub struct Renderer {
@@ -29,9 +30,10 @@ pub struct Renderer {
     quad_pipeline_state: metal::RenderPipelineState,
     shadow_pipeline_state: metal::RenderPipelineState,
     sprite_pipeline_state: metal::RenderPipelineState,
+    path_winding_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
-    paths_texture: metal::Texture,
+    path_winding_texture: metal::Texture,
 }
 
 impl Renderer {
@@ -64,10 +66,12 @@ impl Renderer {
 
         let paths_texture_size = vec2f(2048., 2048.);
         let descriptor = metal::TextureDescriptor::new();
-        descriptor.set_pixel_format(metal::MTLPixelFormat::A8Unorm);
+        descriptor.set_pixel_format(metal::MTLPixelFormat::Stencil8);
         descriptor.set_width(paths_texture_size.x() as u64);
         descriptor.set_height(paths_texture_size.y() as u64);
-        let paths_texture = device.new_texture(&descriptor);
+        descriptor.set_usage(metal::MTLTextureUsage::RenderTarget);
+        descriptor.set_storage_mode(metal::MTLStorageMode::Private);
+        let path_winding_texture = device.new_texture(&descriptor);
 
         let atlas_size: Vector2I = vec2i(1024, 768);
         Ok(Self {
@@ -96,9 +100,17 @@ impl Renderer {
                 "sprite_fragment",
                 pixel_format,
             )?,
+            path_winding_pipeline_state: build_stencil_pipeline_state(
+                &device,
+                &library,
+                "path_winding",
+                "path_winding_vertex",
+                "path_winding_fragment",
+                path_winding_texture.pixel_format(),
+            )?,
             unit_vertices,
             instances,
-            paths_texture,
+            path_winding_texture,
         })
     }
 
@@ -133,12 +145,15 @@ impl Renderer {
         let ctx = RenderContext {
             drawable_size,
             command_encoder,
+            command_buffer,
         };
+
         let mut offset = 0;
         for layer in scene.layers() {
             self.clip(scene, layer, &ctx);
             self.render_shadows(scene, layer, &mut offset, &ctx);
             self.render_quads(scene, layer, &mut offset, &ctx);
+            self.render_paths(scene, layer, &mut offset, &ctx);
             self.render_sprites(scene, layer, &mut offset, &ctx);
         }
 
@@ -318,6 +333,66 @@ impl Renderer {
         offset: &mut usize,
         ctx: &RenderContext,
     ) {
+        for (color, paths) in layer.paths_by_color() {
+            let winding_render_pass_descriptor = metal::RenderPassDescriptor::new();
+            let stencil_attachment = winding_render_pass_descriptor.stencil_attachment().unwrap();
+            stencil_attachment.set_texture(Some(&self.path_winding_texture));
+            stencil_attachment.set_load_action(metal::MTLLoadAction::Clear);
+            stencil_attachment.set_store_action(metal::MTLStoreAction::Store);
+            let winding_command_encoder = ctx
+                .command_buffer
+                .new_render_command_encoder(winding_render_pass_descriptor);
+
+            align_offset(offset);
+            let vertex_count = paths.iter().map(|p| p.vertices.len()).sum::<usize>();
+            let next_offset = *offset + vertex_count * mem::size_of::<shaders::GPUIPathVertex>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            winding_command_encoder.set_render_pipeline_state(&self.path_winding_pipeline_state);
+            winding_command_encoder.set_vertex_buffer(
+                shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexVertices
+                    as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            winding_command_encoder.set_vertex_bytes(
+                shaders::GPUIPathWindingVertexInputIndex_GPUIPathWindingVertexInputIndexViewportSize
+                    as u64,
+                mem::size_of::<shaders::vector_float2>() as u64,
+                [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::GPUIPathVertex
+            };
+
+            for (ix, vertex) in paths.iter().flat_map(|p| &p.vertices).enumerate() {
+                let shader_vertex = shaders::GPUIPathVertex {
+                    xy_position: vertex.xy_position.to_float2(),
+                    st_position: vertex.st_position.to_float2(),
+                };
+                unsafe {
+                    *(buffer_contents.offset(ix as isize)) = shader_vertex;
+                }
+            }
+
+            self.instances.did_modify_range(NSRange {
+                location: *offset as u64,
+                length: (next_offset - *offset) as u64,
+            });
+            *offset = next_offset;
+
+            winding_command_encoder.draw_primitives(
+                metal::MTLPrimitiveType::Triangle,
+                0,
+                vertex_count as u64,
+            );
+            winding_command_encoder.end_encoding();
+        }
     }
 
     fn render_sprites(
@@ -455,6 +530,32 @@ fn build_pipeline_state(
         .map_err(|message| anyhow!("could not create render pipeline state: {}", message))
 }
 
+fn build_stencil_pipeline_state(
+    device: &metal::DeviceRef,
+    library: &metal::LibraryRef,
+    label: &str,
+    vertex_fn_name: &str,
+    fragment_fn_name: &str,
+    pixel_format: metal::MTLPixelFormat,
+) -> Result<metal::RenderPipelineState> {
+    let vertex_fn = library
+        .get_function(vertex_fn_name, None)
+        .map_err(|message| anyhow!("error locating vertex function: {}", message))?;
+    let fragment_fn = library
+        .get_function(fragment_fn_name, None)
+        .map_err(|message| anyhow!("error locating fragment function: {}", message))?;
+
+    let descriptor = metal::RenderPipelineDescriptor::new();
+    descriptor.set_label(label);
+    descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
+    descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
+    descriptor.set_stencil_attachment_pixel_format(pixel_format);
+
+    device
+        .new_render_pipeline_state(&descriptor)
+        .map_err(|message| anyhow!("could not create render pipeline state: {}", message))
+}
+
 mod shaders {
     #![allow(non_upper_case_globals)]
     #![allow(non_camel_case_types)]

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

@@ -53,3 +53,13 @@ typedef struct {
     vector_float2 atlas_origin;
     vector_uchar4 color;
 } GPUISprite;
+
+typedef enum {
+    GPUIPathWindingVertexInputIndexVertices = 0,
+    GPUIPathWindingVertexInputIndexViewportSize = 1,
+} GPUIPathWindingVertexInputIndex;
+
+typedef struct {
+    vector_float2 xy_position;
+    vector_float2 st_position;
+} GPUIPathVertex;

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

@@ -201,3 +201,31 @@ fragment float4 sprite_fragment(
     color.a *= mask.a;
     return color;
 }
+
+struct PathWindingFragmentInput {
+    float4 position [[position]];
+    float2 st_position;
+};
+
+vertex PathWindingFragmentInput path_winding_vertex(
+    uint vertex_id [[vertex_id]],
+    constant GPUIPathVertex *vertices [[buffer(GPUIPathWindingVertexInputIndexVertices)]],
+    constant float2 *viewport_size [[buffer(GPUIPathWindingVertexInputIndexViewportSize)]]
+) {
+    GPUIPathVertex v = vertices[vertex_id];
+    float4 device_position = to_device_position(v.xy_position, *viewport_size);
+    return PathWindingFragmentInput {
+        device_position,
+        v.st_position,
+    };
+}
+
+fragment float4 path_winding_fragment(
+    PathWindingFragmentInput input [[stage_in]]
+) {
+    if (input.st_position.x * input.st_position.x - input.st_position.y > 0.0) {
+        return float4(0.0);
+    } else {
+        return float4(float3(0.0), 1.0 / 255.0);
+    }
+}

gpui/src/scene.rs 🔗

@@ -16,6 +16,7 @@ pub struct Layer {
     quads: Vec<Quad>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
+    paths: Vec<(ColorU, Vec<Path>)>,
 }
 
 #[derive(Default, Debug)]
@@ -53,6 +54,17 @@ pub struct Border {
     pub left: bool,
 }
 
+#[derive(Debug)]
+pub struct Path {
+    pub vertices: Vec<PathVertex>,
+}
+
+#[derive(Debug)]
+pub struct PathVertex {
+    pub xy_position: Vector2F,
+    pub st_position: Vector2F,
+}
+
 impl Scene {
     pub fn new(scale_factor: f32) -> Self {
         Scene {
@@ -93,6 +105,10 @@ impl Scene {
         self.active_layer().push_glyph(glyph)
     }
 
+    pub fn push_path(&mut self, color: ColorU, path: Path) {
+        self.active_layer().push_path(color, path);
+    }
+
     fn active_layer(&mut self) -> &mut Layer {
         &mut self.layers[*self.active_layer_stack.last().unwrap()]
     }
@@ -105,6 +121,7 @@ impl Layer {
             quads: Vec::new(),
             shadows: Vec::new(),
             glyphs: Vec::new(),
+            paths: Vec::new(),
         }
     }
 
@@ -135,6 +152,17 @@ impl Layer {
     pub fn glyphs(&self) -> &[Glyph] {
         self.glyphs.as_slice()
     }
+
+    fn push_path(&mut self, color: ColorU, path: Path) {
+        match self.paths.binary_search_by_key(&color, |(c, path)| *c) {
+            Err(i) => self.paths.insert(i, (color, vec![path])),
+            Ok(i) => self.paths[i].1.push(path),
+        }
+    }
+
+    pub fn paths_by_color(&self) -> &[(ColorU, Vec<Path>)] {
+        self.paths.as_slice()
+    }
 }
 
 impl Border {

zed/src/editor/buffer_element.rs 🔗

@@ -4,6 +4,7 @@ use gpui::{
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
+        PathBuilder,
     },
     text_layout::{self, TextLayoutCache},
     AfterLayoutContext, AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext,
@@ -211,51 +212,51 @@ impl BufferElement {
         });
 
         // Draw selections
-        // let corner_radius = 2.5;
+        let corner_radius = 2.5;
         let mut cursors = SmallVec::<[Cursor; 32]>::new();
 
         for selection in view.selections_in_range(
             DisplayPoint::new(start_row, 0)..DisplayPoint::new(end_row, 0),
             ctx.app,
         ) {
-            // if selection.start != selection.end {
-            //     let range_start = cmp::min(selection.start, selection.end);
-            //     let range_end = cmp::max(selection.start, selection.end);
-            //     let row_range = if range_end.column() == 0 {
-            //         cmp::max(range_start.row(), start_row)..cmp::min(range_end.row(), end_row)
-            //     } else {
-            //         cmp::max(range_start.row(), start_row)..cmp::min(range_end.row() + 1, end_row)
-            //     };
-
-            //     let selection = Selection {
-            //         line_height,
-            //         start_y: row_range.start as f32 * line_height - scroll_top,
-            //         lines: row_range
-            //             .into_iter()
-            //             .map(|row| {
-            //                 let line_layout = &layout.line_layouts[(row - start_row) as usize];
-            //                 SelectionLine {
-            //                     start_x: if row == range_start.row() {
-            //                         line_layout.x_for_index(range_start.column() as usize)
-            //                             - scroll_left
-            //                             - descent
-            //                     } else {
-            //                         -scroll_left
-            //                     },
-            //                     end_x: if row == range_end.row() {
-            //                         line_layout.x_for_index(range_end.column() as usize)
-            //                             - scroll_left
-            //                             - descent
-            //                     } else {
-            //                         line_layout.width + corner_radius * 2.0 - scroll_left - descent
-            //                     },
-            //                 }
-            //             })
-            //             .collect(),
-            //     };
-
-            //     selection.paint(scene);
-            // }
+            if selection.start != selection.end {
+                let range_start = cmp::min(selection.start, selection.end);
+                let range_end = cmp::max(selection.start, selection.end);
+                let row_range = if range_end.column() == 0 {
+                    cmp::max(range_start.row(), start_row)..cmp::min(range_end.row(), end_row)
+                } else {
+                    cmp::max(range_start.row(), start_row)..cmp::min(range_end.row() + 1, end_row)
+                };
+
+                let selection = Selection {
+                    line_height,
+                    start_y: row_range.start as f32 * line_height - scroll_top,
+                    lines: row_range
+                        .into_iter()
+                        .map(|row| {
+                            let line_layout = &layout.line_layouts[(row - start_row) as usize];
+                            SelectionLine {
+                                start_x: if row == range_start.row() {
+                                    line_layout.x_for_index(range_start.column() as usize)
+                                        - scroll_left
+                                        - descent
+                                } else {
+                                    -scroll_left
+                                },
+                                end_x: if row == range_end.row() {
+                                    line_layout.x_for_index(range_end.column() as usize)
+                                        - scroll_left
+                                        - descent
+                                } else {
+                                    line_layout.width + corner_radius * 2.0 - scroll_left - descent
+                                },
+                            }
+                        })
+                        .collect(),
+                };
+
+                selection.paint(ctx.scene);
+            }
 
             if view.cursors_visible() {
                 let cursor_position = selection.end;
@@ -597,20 +598,47 @@ impl Selection {
     }
 
     fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], scene: &mut Scene) {
-        // use Direction::*;
+        if lines.is_empty() {
+            return;
+        }
 
-        // if lines.is_empty() {
-        //     return;
-        // }
+        let mut path = PathBuilder::new();
+        let corner_radius = 0.08 * self.line_height;
 
-        // let mut path = Path2D::new();
-        // let corner_radius = 0.08 * self.line_height;
+        let first_line = lines.first().unwrap();
+        path.reset(vec2f(first_line.end_x - corner_radius, start_y));
+        path.curve_to(
+            vec2f(first_line.end_x, start_y + corner_radius),
+            vec2f(first_line.end_x, start_y),
+        );
+        path.line_to(vec2f(
+            first_line.end_x,
+            start_y + self.line_height - corner_radius,
+        ));
+        path.curve_to(
+            vec2f(first_line.end_x - corner_radius, start_y + self.line_height),
+            vec2f(first_line.end_x, start_y + self.line_height),
+        );
+        path.line_to(vec2f(
+            first_line.start_x + corner_radius,
+            start_y + self.line_height,
+        ));
+        path.curve_to(
+            vec2f(
+                first_line.start_x,
+                start_y + self.line_height - corner_radius,
+            ),
+            vec2f(first_line.start_x, start_y + self.line_height),
+        );
+        path.line_to(vec2f(first_line.start_x, start_y + corner_radius));
+        path.curve_to(
+            vec2f(first_line.start_x + corner_radius, start_y),
+            vec2f(first_line.start_x, start_y),
+        );
+        path.line_to(vec2f(first_line.end_x - corner_radius, start_y));
 
-        // let first_line = lines.first().unwrap();
-        // let last_line = lines.last().unwrap();
+        scene.push_path(ColorU::from_u32(0xff0000ff), path.build());
 
-        // let corner = vec2f(first_line.end_x, start_y);
-        // path.move_to(corner - vec2f(corner_radius, 0.0));
         // rounded_corner(&mut path, corner, corner_radius, Right, Down);
 
         // let mut iter = lines.iter().enumerate().peekable();