linux: shadow rendering

Dzmitry Malyshau created

Change summary

Cargo.toml                                       |   7 
crates/gpui/Cargo.toml                           |   4 
crates/gpui/build.rs                             |   4 
crates/gpui/src/platform/linux/blade_renderer.rs |  70 +++++-
crates/gpui/src/platform/linux/platform.rs       |   5 
crates/gpui/src/platform/linux/shaders.wgsl      | 200 ++++++++++++++---
crates/gpui/src/platform/linux/window.rs         |   6 
crates/gpui/src/scene.rs                         |   1 
crates/gpui/src/window/element_cx.rs             |   1 
9 files changed, 236 insertions(+), 62 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -182,7 +182,7 @@ tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1d897
 wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" }
 
 # TODO - Remove when corresponding Blade versions are published
-# Currently in https://github.com/kvark/blade/tree/zed
+[patch."https://github.com/kvark/blade"]
 blade-graphics = { path = "/x/Code/blade/blade-graphics" }
 blade-macros = { path = "/x/Code/blade/blade-macros" }
 
@@ -190,6 +190,11 @@ blade-macros = { path = "/x/Code/blade/blade-macros" }
 split-debuginfo = "unpacked"
 debug = "limited"
 
+# TODO - Remove this
+[profile.dev.package.blade-graphics]
+split-debuginfo = "off"
+debug = "full"
+
 [profile.dev.package.taffy]
 opt-level = 3
 

crates/gpui/Cargo.toml 🔗

@@ -26,8 +26,8 @@ anyhow.workspace = true
 async-task = "4.7"
 backtrace = { version = "0.3", optional = true }
 bitflags = "2.4.0"
-blade-graphics = "0.3"
-blade-macros = "0.2"
+blade-graphics = { git = "https://github.com/kvark/blade", branch = "zed" }
+blade-macros = { git = "https://github.com/kvark/blade", branch = "zed" }
 bytemuck = "1"
 collections = { path = "../collections" }
 ctor.workspace = true

crates/gpui/build.rs 🔗

@@ -7,7 +7,7 @@ use cbindgen::Config;
 
 fn main() {
     //generate_dispatch_bindings();
-    let _header_path = generate_shader_bindings();
+    //let header_path = generate_shader_bindings();
     //#[cfg(feature = "runtime_shaders")]
     //emit_stitched_shaders(&header_path);
     //#[cfg(not(feature = "runtime_shaders"))]
@@ -38,7 +38,7 @@ fn _generate_dispatch_bindings() {
         .expect("couldn't write dispatch bindings");
 }
 
-fn generate_shader_bindings() -> PathBuf {
+fn _generate_shader_bindings() -> PathBuf {
     let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
     let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
     let mut config = Config::default();

crates/gpui/src/platform/linux/blade_renderer.rs 🔗

@@ -1,5 +1,8 @@
+// Doing `if let` gives you nice scoping with passes/encoders
+#![allow(irrefutable_let_patterns)]
+
 use super::{BladeBelt, BladeBeltDescriptor};
-use crate::{PrimitiveBatch, Quad, Scene};
+use crate::{PrimitiveBatch, Quad, Scene, Shadow};
 use bytemuck::{Pod, Zeroable};
 
 use blade_graphics as gpu;
@@ -18,11 +21,18 @@ struct GlobalParams {
 #[derive(blade_macros::ShaderData)]
 struct ShaderQuadsData {
     globals: GlobalParams,
-    quads: gpu::BufferPiece,
+    b_quads: gpu::BufferPiece,
+}
+
+#[derive(blade_macros::ShaderData)]
+struct ShaderShadowsData {
+    globals: GlobalParams,
+    b_shadows: gpu::BufferPiece,
 }
 
 struct BladePipelines {
     quads: gpu::RenderPipeline,
+    shadows: gpu::RenderPipeline,
 }
 
 impl BladePipelines {
@@ -31,18 +41,36 @@ impl BladePipelines {
             source: include_str!("shaders.wgsl"),
         });
         shader.check_struct_size::<Quad>();
-        let layout = <ShaderQuadsData as gpu::ShaderData>::layout();
+        shader.check_struct_size::<Shadow>();
+        let quads_layout = <ShaderQuadsData as gpu::ShaderData>::layout();
+        let shadows_layout = <ShaderShadowsData as gpu::ShaderData>::layout();
         Self {
             quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
                 name: "quads",
-                data_layouts: &[&layout],
-                vertex: shader.at("vs_quads"),
+                data_layouts: &[&quads_layout],
+                vertex: shader.at("vs_quad"),
+                primitive: gpu::PrimitiveState {
+                    topology: gpu::PrimitiveTopology::TriangleStrip,
+                    ..Default::default()
+                },
+                depth_stencil: None,
+                fragment: shader.at("fs_quad"),
+                color_targets: &[gpu::ColorTargetState {
+                    format: surface_format,
+                    blend: Some(gpu::BlendState::ALPHA_BLENDING),
+                    write_mask: gpu::ColorWrites::default(),
+                }],
+            }),
+            shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
+                name: "shadows",
+                data_layouts: &[&shadows_layout],
+                vertex: shader.at("vs_shadow"),
                 primitive: gpu::PrimitiveState {
                     topology: gpu::PrimitiveTopology::TriangleStrip,
                     ..Default::default()
                 },
                 depth_stencil: None,
-                fragment: shader.at("fs_quads"),
+                fragment: shader.at("fs_shadow"),
                 color_targets: &[gpu::ColorTargetState {
                     format: surface_format,
                     blend: Some(gpu::BlendState::ALPHA_BLENDING),
@@ -117,6 +145,14 @@ impl BladeRenderer {
         self.command_encoder.start();
         self.command_encoder.init_texture(frame.texture());
 
+        let globals = GlobalParams {
+            viewport_size: [
+                self.viewport_size.width as f32,
+                self.viewport_size.height as f32,
+            ],
+            pad: [0; 2],
+        };
+
         if let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
             colors: &[gpu::RenderTarget {
                 view: frame.texture_view(),
@@ -133,18 +169,24 @@ impl BladeRenderer {
                         encoder.bind(
                             0,
                             &ShaderQuadsData {
-                                globals: GlobalParams {
-                                    viewport_size: [
-                                        self.viewport_size.width as f32,
-                                        self.viewport_size.height as f32,
-                                    ],
-                                    pad: [0; 2],
-                                },
-                                quads: instances,
+                                globals,
+                                b_quads: instances,
                             },
                         );
                         encoder.draw(0, 4, 0, quads.len() as u32);
                     }
+                    PrimitiveBatch::Shadows(shadows) => {
+                        let instances = self.instance_belt.alloc_data(shadows, &self.gpu);
+                        let mut encoder = pass.with(&self.pipelines.shadows);
+                        encoder.bind(
+                            0,
+                            &ShaderShadowsData {
+                                globals,
+                                b_shadows: instances,
+                            },
+                        );
+                        encoder.draw(0, 4, 0, shadows.len() as u32);
+                    }
                     _ => continue,
                 }
             }

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -112,10 +112,10 @@ impl Platform for LinuxPlatform {
                 xcb::Event::X(x::Event::Expose(ev)) => {
                     repaint_x_window = Some(ev.window());
                 }
-                xcb::Event::X(x::Event::ResizeRequest(ev)) => {
+                xcb::Event::X(x::Event::ConfigureNotify(ev)) => {
                     let this = self.0.lock();
                     LinuxWindowState::resize(&this.windows[&ev.window()], ev.width(), ev.height());
-                    repaint_x_window = Some(ev.window());
+                    this.xcb_connection.flush();
                 }
                 _ => {}
             }
@@ -175,7 +175,6 @@ impl Platform for LinuxPlatform {
 
         let window_ptr = LinuxWindowState::new_ptr(
             options,
-            handle,
             &this.xcb_connection,
             this.x_root_index,
             x_window,

crates/gpui/src/platform/linux/shaders.wgsl 🔗

@@ -1,3 +1,17 @@
+struct Globals {
+    viewport_size: vec2<f32>,
+    pad: vec2<u32>,
+}
+
+var<uniform> globals: Globals;
+
+const M_PI_F: f32 = 3.1415926;
+
+struct ViewId {
+	lo: u32,
+	hi: u32,
+}
+
 struct Bounds {
     origin: vec2<f32>,
     size: vec2<f32>,
@@ -21,35 +35,6 @@ struct Hsla {
     a: f32,
 }
 
-struct Quad {
-    view_id: vec2<u32>,
-    layer_id: u32,
-    order: u32,
-    bounds: Bounds,
-    content_mask: Bounds,
-    background: Hsla,
-    border_color: Hsla,
-    corner_radii: Corners,
-    border_widths: Edges,
-}
-
-struct Globals {
-    viewport_size: vec2<f32>,
-    pad: vec2<u32>,
-}
-
-var<uniform> globals: Globals;
-var<storage, read> quads: array<Quad>;
-
-struct QuadsVarying {
-    @builtin(position) position: vec4<f32>,
-    @location(0) @interpolate(flat) background_color: vec4<f32>,
-    @location(1) @interpolate(flat) border_color: vec4<f32>,
-    @location(2) @interpolate(flat) quad_id: u32,
-    //TODO: use `clip_distance` once Naga supports it
-    @location(3) clip_distances: vec4<f32>,
-}
-
 fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
     let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
     let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
@@ -99,17 +84,62 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
 }
 
 fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
-  let alpha = above.a + below.a * (1.0 - above.a);
-  let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
-  return vec4<f32>(color, alpha);
+    let alpha = above.a + below.a * (1.0 - above.a);
+    let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
+    return vec4<f32>(color, alpha);
+}
+
+// A standard gaussian function, used for weighting samples
+fn gaussian(x: f32, sigma: f32) -> f32{
+    return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
+}
+
+// This approximates the error function, needed for the gaussian integral
+fn erf(v: vec2<f32>) -> vec2<f32> {
+    let s = sign(v);
+    let a = abs(v);
+    let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
+    let r2 = r1 * r1;
+    return s - s / (r2 * r2);
+}
+
+fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
+  let delta = min(half_size.y - corner - abs(y), 0.0);
+  let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
+  let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
+  return integral.y - integral.x;
+}
+
+// --- quads --- //
+
+struct Quad {
+    view_id: ViewId,
+    layer_id: u32,
+    order: u32,
+    bounds: Bounds,
+    content_mask: Bounds,
+    background: Hsla,
+    border_color: Hsla,
+    corner_radii: Corners,
+    border_widths: Edges,
+}
+var<storage, read> b_quads: array<Quad>;
+
+struct QuadVarying {
+    @builtin(position) position: vec4<f32>,
+    @location(0) @interpolate(flat) background_color: vec4<f32>,
+    @location(1) @interpolate(flat) border_color: vec4<f32>,
+    @location(2) @interpolate(flat) quad_id: u32,
+    //TODO: use `clip_distance` once Naga supports it
+    @location(3) clip_distances: vec4<f32>,
 }
 
 @vertex
-fn vs_quads(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadsVarying {
+fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
     let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
-    let quad = quads[instance_id];
+    let quad = b_quads[instance_id];
 
-    var out = QuadsVarying();
+    var out = QuadVarying();
     out.position = to_device_position(unit_vertex, quad.bounds);
     out.background_color = hsla_to_rgba(quad.background);
     out.border_color = hsla_to_rgba(quad.border_color);
@@ -119,7 +149,7 @@ fn vs_quads(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) inst
 }
 
 @fragment
-fn fs_quads(input: QuadsVarying) -> @location(0) vec4<f32> {
+fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
     // Alpha clip first, since we don't have `clip_distance`.
     let min_distance = min(
         min(input.clip_distances.x, input.clip_distances.y),
@@ -129,7 +159,7 @@ fn fs_quads(input: QuadsVarying) -> @location(0) vec4<f32> {
         return vec4<f32>(0.0);
     }
 
-    let quad = quads[input.quad_id];
+    let quad = b_quads[input.quad_id];
     let half_size = quad.bounds.size / 2.0;
     let center = quad.bounds.origin + half_size;
     let center_to_point = input.position.xy - center;
@@ -180,4 +210,98 @@ fn fs_quads(input: QuadsVarying) -> @location(0) vec4<f32> {
     }
 
     return color * vec4<f32>(1.0, 1.0, 1.0, saturate(0.5 - distance));
-}
+}
+
+// --- shadows --- //
+
+struct Shadow {
+    view_id: ViewId,
+    layer_id: u32,
+    order: u32,
+    bounds: Bounds,
+    corner_radii: Corners,
+    content_mask: Bounds,
+    color: Hsla,
+    blur_radius: f32,
+    pad: u32,
+}
+var<storage, read> b_shadows: array<Shadow>;
+
+struct ShadowVarying {
+    @builtin(position) position: vec4<f32>,
+    @location(0) @interpolate(flat) color: vec4<f32>,
+    @location(1) @interpolate(flat) shadow_id: u32,
+    //TODO: use `clip_distance` once Naga supports it
+    @location(3) clip_distances: vec4<f32>,
+}
+
+@vertex
+fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
+    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
+    let shadow = b_shadows[instance_id];
+
+    let margin = 3.0 * shadow.blur_radius;
+    // Set the bounds of the shadow and adjust its size based on the shadow's
+    // spread radius to achieve the spreading effect
+    var bounds = shadow.bounds;
+    bounds.origin -= vec2<f32>(margin);
+    bounds.size += 2.0 * vec2<f32>(margin);
+
+    var out = ShadowVarying();
+    out.position = to_device_position(unit_vertex, shadow.bounds);
+    out.color = hsla_to_rgba(shadow.color);
+    out.shadow_id = instance_id;
+    out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
+    return out;
+}
+
+@fragment
+fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
+    // Alpha clip first, since we don't have `clip_distance`.
+    let min_distance = min(
+        min(input.clip_distances.x, input.clip_distances.y),
+        min(input.clip_distances.z, input.clip_distances.w)
+    );
+    if min_distance <= 0.0 {
+        return vec4<f32>(0.0);
+    }
+
+    let shadow = b_shadows[input.shadow_id];
+    let half_size = shadow.bounds.size / 2.0;
+    let center = shadow.bounds.origin + half_size;
+    let center_to_point = input.position.xy - center;
+
+    var corner_radius = 0.0;
+    if (center_to_point.x < 0.0) {
+        if (center_to_point.y < 0.0) {
+            corner_radius = shadow.corner_radii.top_left;
+        } else {
+            corner_radius = shadow.corner_radii.bottom_left;
+        }
+    } else {
+        if (center_to_point.y < 0.) {
+            corner_radius = shadow.corner_radii.top_right;
+        } else {
+            corner_radius = shadow.corner_radii.bottom_right;
+        }
+    }
+
+    // The signal is only non-zero in a limited range, so don't waste samples
+    let low = center_to_point.y - half_size.y;
+    let high = center_to_point.y + half_size.y;
+    let start = clamp(-3.0 * shadow.blur_radius, low, high);
+    let end = clamp(3.0 * shadow.blur_radius, low, high);
+
+    // Accumulate samples (we can get away with surprisingly few samples)
+    let step = (end - start) / 4.0;
+    var y = start + step * 0.5;
+    var alpha = 0.0;
+    for (var i = 0; i < 4; i += 1) {
+        let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
+            shadow.blur_radius, corner_radius, half_size);
+        alpha +=  blur * gaussian(y, shadow.blur_radius) * step;
+        y += step;
+    }
+
+    return input.color * vec4<f32>(1.0, 1.0, 1.0, alpha);
+}

crates/gpui/src/platform/linux/window.rs 🔗

@@ -38,11 +38,13 @@ struct RawWindow {
     connection: *mut c_void,
     screen_id: i32,
     window_id: u32,
+    visual_id: u32,
 }
 unsafe impl raw_window_handle::HasRawWindowHandle for RawWindow {
     fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle {
         let mut wh = raw_window_handle::XcbWindowHandle::empty();
         wh.window = self.window_id;
+        wh.visual_id = self.visual_id;
         wh.into()
     }
 }
@@ -58,7 +60,6 @@ unsafe impl raw_window_handle::HasRawDisplayHandle for RawWindow {
 impl LinuxWindowState {
     pub fn new_ptr(
         options: WindowOptions,
-        handle: AnyWindowHandle,
         xcb_connection: &xcb::Connection,
         x_main_screen_index: i32,
         x_window: x::Window,
@@ -76,7 +77,7 @@ impl LinuxWindowState {
         let xcb_values = [
             x::Cw::BackPixel(screen.white_pixel()),
             x::Cw::EventMask(
-                x::EventMask::EXPOSURE | x::EventMask::RESIZE_REDIRECT | x::EventMask::KEY_PRESS,
+                x::EventMask::EXPOSURE | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::KEY_PRESS,
             ),
         ];
 
@@ -136,6 +137,7 @@ impl LinuxWindowState {
             ) as *mut _,
             screen_id: x_screen_index,
             window_id: x_window.resource_id(),
+            visual_id: screen.root_visual(),
         };
         let gpu = Arc::new(
             unsafe {

crates/gpui/src/scene.rs 🔗

@@ -577,6 +577,7 @@ pub(crate) struct Shadow {
     pub content_mask: ContentMask<ScaledPixels>,
     pub color: Hsla,
     pub blur_radius: ScaledPixels,
+    pub pad: u32, // align to 8 bytes
 }
 
 impl Ord for Shadow {

crates/gpui/src/window/element_cx.rs 🔗

@@ -677,6 +677,7 @@ impl<'a> ElementContext<'a> {
                     corner_radii: corner_radii.scale(scale_factor),
                     color: shadow.color,
                     blur_radius: shadow.blur_radius.scale(scale_factor),
+                    pad: 0,
                 },
             );
         }