From cbedb7f2f538018d156fee13548f35164e0231e2 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:50:50 +0000 Subject: [PATCH 1/3] Re-apply backdrop blur on the vendored gpui snapshot The Background API patch from the old fork, retargeted because upstream added Checkerboard at tag 3 (backdrop blur takes tag 4), plus the wgpu renderer side: a captured colour target bound on the quads pipeline and the blur computed inside fs_quad. The capture pass that feeds the blur is disabled. Sampling the colour target makes the preceding pass's draws disappear, and that is not yet explained. See the PR for WI-1349. --- vendor/zed/PATCHES.md | 1 + vendor/zed/crates/gpui/src/color.rs | 34 ++ vendor/zed/crates/gpui/src/style.rs | 5 +- vendor/zed/crates/gpui/src/styled.rs | 16 +- vendor/zed/crates/gpui_wgpu/src/shaders.wgsl | 78 +++- .../zed/crates/gpui_wgpu/src/wgpu_renderer.rs | 335 +++++++++++++++++- 6 files changed, 446 insertions(+), 23 deletions(-) diff --git a/vendor/zed/PATCHES.md b/vendor/zed/PATCHES.md index 14f3909..ab6eada 100644 --- a/vendor/zed/PATCHES.md +++ b/vendor/zed/PATCHES.md @@ -5,3 +5,4 @@ Each local patch to this tree is a commit on the integrating branch, never on `z | patch | commit | files | |---|---|---| | `assets/fonts` | this commit | `vendor/zed/assets/fonts/` | +| `backdrop-blur` | this commit | `vendor/zed/crates/gpui/src/color.rs`, `vendor/zed/crates/gpui/src/style.rs`, `vendor/zed/crates/gpui/src/styled.rs`, `vendor/zed/crates/gpui_wgpu/src/shaders.wgsl`, `vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs` | diff --git a/vendor/zed/crates/gpui/src/color.rs b/vendor/zed/crates/gpui/src/color.rs index 3bd893f..f6291f6 100644 --- a/vendor/zed/crates/gpui/src/color.rs +++ b/vendor/zed/crates/gpui/src/color.rs @@ -747,6 +747,11 @@ pub(crate) enum BackgroundTag { LinearGradient = 1, PatternSlash = 2, Checkerboard = 3, + /// A blurred copy of the pixels drawn behind the element, tinted with + /// `solid`. `gradient_angle_or_pattern_height` carries the blur radius in + /// device pixels. Tag 4, because upstream added `Checkerboard` at 3 after + /// this patch was first written. + BackdropBlur = 4, } /// A color space for color interpolation. @@ -805,6 +810,11 @@ impl std::fmt::Debug for Background { "Checkerboard({:?}, {})", self.solid, self.gradient_angle_or_pattern_height ), + BackgroundTag::BackdropBlur => write!( + f, + "BackdropBlur(radius: {}, tint: {:?})", + self.gradient_angle_or_pattern_height, self.solid + ), } } } @@ -847,6 +857,22 @@ pub fn checkerboard(color: impl Into, size: f32) -> Background { } } +/// Creates a backdrop-blur background: the pixels already drawn behind the +/// element are blurred by `radius` device pixels and tinted with `tint`. +/// +/// `tint`'s alpha is the tint strength — `hsla(0., 0., 0., 0.)` leaves the +/// blur untinted, an opaque tint hides it. A renderer that cannot capture its +/// own colour target (the Metal path today) draws `tint` as a flat colour +/// instead, which is why callers pick a tint that reads on its own. +pub fn backdrop_blur(radius: f32, tint: impl Into) -> Background { + Background { + tag: BackgroundTag::BackdropBlur, + solid: tint.into(), + gradient_angle_or_pattern_height: radius, + ..Default::default() + } +} + /// Creates a solid background color. pub fn solid_background(color: impl Into) -> Background { Background { @@ -943,8 +969,16 @@ impl Background { BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), BackgroundTag::PatternSlash => self.solid.is_transparent(), BackgroundTag::Checkerboard => self.solid.is_transparent(), + // A blur always shows whatever is behind it, even with no tint. + BackgroundTag::BackdropBlur => false, } } + + /// Whether this background needs the renderer to capture the pixels drawn + /// so far, so the quad shader can blur them. + pub fn needs_backdrop(&self) -> bool { + matches!(self.tag, BackgroundTag::BackdropBlur) + } } impl From for Background { diff --git a/vendor/zed/crates/gpui/src/style.rs b/vendor/zed/crates/gpui/src/style.rs index 9150fdd..8655d5c 100644 --- a/vendor/zed/crates/gpui/src/style.rs +++ b/vendor/zed/crates/gpui/src/style.rs @@ -716,7 +716,10 @@ impl Style { Some(color) => match color.tag { BackgroundTag::Solid | BackgroundTag::PatternSlash - | BackgroundTag::Checkerboard => color.solid, + | BackgroundTag::Checkerboard + // The blur's border fades against its tint, which is what + // the quad shader composites the blur under. + | BackgroundTag::BackdropBlur => color.solid, BackgroundTag::LinearGradient => color .colors diff --git a/vendor/zed/crates/gpui/src/styled.rs b/vendor/zed/crates/gpui/src/styled.rs index a24982a..863d4b3 100644 --- a/vendor/zed/crates/gpui/src/styled.rs +++ b/vendor/zed/crates/gpui/src/styled.rs @@ -2,7 +2,7 @@ use crate::{ self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle, DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle, FontWeight, GridPlacement, GridTemplate, GridTemplateMinSize, Hsla, JustifyContent, Length, - SharedString, StrikethroughStyle, StyleRefinement, TextAlign, TextOverflow, + Pixels, SharedString, StrikethroughStyle, StyleRefinement, TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, }; pub use gpui_macros::{ @@ -496,6 +496,20 @@ pub trait Styled: Sized { self } + /// Blurs the pixels drawn behind the element by `radius` device pixels and + /// tints the result with `tint`. `tint`'s alpha is the tint strength. + /// + /// Equivalent to `bg(backdrop_blur(radius, tint))`. + fn backdrop_blur(mut self, radius: impl Into, tint: impl Into) -> Self + where + Self: Sized, + { + self.style().background = Some( + crate::backdrop_blur(f32::from(radius.into()), tint).into(), + ); + self + } + /// Sets the border style of the element. fn border_dashed(mut self) -> Self { self.style().border_style = Some(BorderStyle::Dashed); diff --git a/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl b/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl index 718cb75..a016739 100644 --- a/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl +++ b/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl @@ -96,6 +96,13 @@ struct GammaParams { @group(2) @binding(0) var t_sprite: texture_2d; @group(2) @binding(1) var s_sprite: sampler; +// A copy of the colour target as it stood before the current quad batch, +// bound on the quads pipeline (group 3) and unused everywhere else. The quad +// fragment shader blurs it directly when a quad's background tag is +// BackdropBlur, so there is no separate blur pass. +@group(3) @binding(0) var t_backdrop: texture_2d; +@group(3) @binding(1) var s_backdrop: sampler; + const M_PI_F: f32 = 3.1415926; const GRAYSCALE_FACTORS: vec3 = vec3(0.2126, 0.7152, 0.0722); @@ -135,6 +142,7 @@ struct Background { // 1u is LinearGradient // 2u is PatternSlash // 3u is Checkerboard + // 4u is BackdropBlur tag: u32, // 0u is sRGB linear color // 1u is Oklab color @@ -538,6 +546,20 @@ struct QuadVarying { @location(5) @interpolate(flat) background_color1: vec4, } +@vertex +fn vs_backdrop(@builtin(vertex_index) index: u32) -> @builtin(position) vec4 { + let x = f32(index & 1u) * 2.0 - 1.0; + let y = f32((index >> 1u) & 1u) * 2.0 - 1.0; + return vec4(x, y, 0.0, 1.0); +} + +@fragment +fn fs_backdrop(@builtin(position) position: vec4) -> @location(0) vec4 { + let dims = textureDimensions(t_sprite); + let texel = vec2(position.xy); + return textureLoad(t_sprite, texel, 0); +} + @vertex fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); @@ -570,8 +592,60 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let quad = load_quad(input.quad_id); - let background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); + var background_color: vec4; + if (quad.background.tag == 4u) { + // BackdropBlur. `t_backdrop` holds the colour target as it stood + // before this quad batch, and the blur radius rides in + // `gradient_angle_or_pattern_height` (device pixels). A 13-tap + // cross-pattern kernel approximates a gaussian of sigma = radius / 2, + // which is the look the old blade renderer produced with its own blur + // pass. `solid` is the tint: its alpha is the tint strength, and it + // composites over the blur with the standard "over" operator. + let radius = max(quad.background.gradient_angle_or_pattern_height, 0.5); + let uv = input.position.xy / globals.viewport_size; + let step = 1.0 / globals.viewport_size; + let off1 = step * (radius * 0.25); + let off2 = step * (radius * 0.5); + let off3 = step * (radius * 0.75); + let off4 = step * radius; + + let w0 = 0.227027; + let w1 = 0.194594; + let w2 = 0.121622; + let w3 = 0.054054; + let w4 = 0.016216; + + var blurred = textureSample(t_backdrop, s_backdrop, uv) * w0; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(off1.x, 0.0)) * w1; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(off1.x, 0.0)) * w1; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(off2.x, 0.0)) * w2; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(off2.x, 0.0)) * w2; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(off3.x, 0.0)) * w3; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(off3.x, 0.0)) * w3; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(off4.x, 0.0)) * w4; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(off4.x, 0.0)) * w4; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(0.0, off1.y)) * w1; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(0.0, off1.y)) * w1; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(0.0, off2.y)) * w2; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(0.0, off2.y)) * w2; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(0.0, off3.y)) * w3; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(0.0, off3.y)) * w3; + blurred += textureSample(t_backdrop, s_backdrop, uv + vec2(0.0, off4.y)) * w4; + blurred += textureSample(t_backdrop, s_backdrop, uv - vec2(0.0, off4.y)) * w4; + + // w0 plus both axes of four offset pairs. + let total_weight = w0 + 4.0 * (w1 + w2 + w3 + w4); + blurred = blurred / total_weight; + + let tint = input.background_solid; + background_color = vec4( + tint.rgb * tint.a + blurred.rgb * (1.0 - tint.a), + tint.a + blurred.a * (1.0 - tint.a), + ); + } else { + background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + input.background_solid, input.background_color0, input.background_color1); + } let unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && diff --git a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs index 8387528..307f6bb 100644 --- a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -134,6 +134,10 @@ struct WgpuPipelines { poly_sprites: wgpu::RenderPipeline, #[allow(dead_code)] surfaces: wgpu::RenderPipeline, + /// Copies the colour target into the backdrop texture by sampling it. + /// Unused while the capture pass in `capture_backdrop` is disabled. + #[allow(dead_code)] + backdrop_copy: wgpu::RenderPipeline, } /// One frame allocation of instance data, ready to bind. @@ -161,6 +165,9 @@ struct WgpuBindGroupLayouts { instances: wgpu::BindGroupLayout, texture: wgpu::BindGroupLayout, surfaces: wgpu::BindGroupLayout, + /// Bound as group 3 on the quads pipeline: the captured colour target that + /// a backdrop-blur quad samples. + backdrop: wgpu::BindGroupLayout, } /// Shared GPU context reference, used to coordinate device recovery across multiple windows. @@ -194,6 +201,12 @@ struct WgpuResources { path_intermediate_view: Option, path_msaa_texture: Option, path_msaa_view: Option, + /// A copy of the colour target taken before a quad batch that contains a + /// backdrop blur, so the quad shader can sample what has been drawn so + /// far. Rebuilt when the target size changes. + backdrop_texture: Option, + backdrop_view: Option, + backdrop_bind_group: Option, } struct CachedTextureBindGroup { @@ -207,6 +220,9 @@ impl WgpuResources { self.path_intermediate_view = None; self.path_msaa_texture = None; self.path_msaa_view = None; + self.backdrop_texture = None; + self.backdrop_view = None; + self.backdrop_bind_group = None; } } @@ -608,11 +624,37 @@ impl WgpuRendererCore { ], }); + // Same shape as `texture`, but a distinct layout so the quads pipeline + // can bind the captured colour target at group 3 without colliding + // with the sprite texture at group 2. + let backdrop = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("backdrop_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + WgpuBindGroupLayouts { globals, instances, texture, surfaces, + backdrop, } } @@ -724,12 +766,18 @@ impl WgpuRendererCore { globals_layout: &wgpu::BindGroupLayout, data_layout: &wgpu::BindGroupLayout, texture_layout: Option<&wgpu::BindGroupLayout>, + extra_layout: Option<&wgpu::BindGroupLayout>, topology: wgpu::PrimitiveTopology, color_targets: &[Option], sample_count: u32, module: &wgpu::ShaderModule| { let mut bind_group_layouts = vec![Some(globals_layout), Some(data_layout)]; - bind_group_layouts.extend(texture_layout.map(Some)); + if texture_layout.is_some() || extra_layout.is_some() { + bind_group_layouts.push(texture_layout); + } + if extra_layout.is_some() { + bind_group_layouts.push(extra_layout); + } let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(&format!("{name}_layout")), bind_group_layouts: &bind_group_layouts, @@ -778,6 +826,8 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, None, + // Group 3: the captured colour target a backdrop-blur quad samples. + Some(&layouts.backdrop), wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], 1, @@ -791,6 +841,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, None, + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], 1, @@ -804,6 +855,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, None, + None, wgpu::PrimitiveTopology::TriangleList, &[Some(wgpu::ColorTargetState { format: surface_format, @@ -834,6 +886,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, Some(&layouts.texture), + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(wgpu::ColorTargetState { format: surface_format, @@ -851,6 +904,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, None, + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], 1, @@ -864,6 +918,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, Some(&layouts.texture), + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], 1, @@ -891,6 +946,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, Some(&layouts.texture), + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(wgpu::ColorTargetState { format: surface_format, @@ -911,6 +967,7 @@ impl WgpuRendererCore { &layouts.globals, &layouts.instances, Some(&layouts.texture), + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], 1, @@ -924,12 +981,48 @@ impl WgpuRendererCore { &layouts.globals, &layouts.surfaces, None, + None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target)], 1, &shader_module, ); + let backdrop_copy_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("backdrop_copy_layout"), + bind_group_layouts: &[Some(&layouts.globals), None, Some(&layouts.texture)], + immediate_size: 0, + }); + let backdrop_copy = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("backdrop_copy"), + layout: Some(&backdrop_copy_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("vs_backdrop"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("fs_backdrop"), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + WgpuPipelines { quads, shadows, @@ -940,6 +1033,7 @@ impl WgpuRendererCore { subpixel_sprites, poly_sprites, surfaces, + backdrop_copy, } } @@ -1199,6 +1293,7 @@ impl WgpuRenderer { if let Err(error) = core.render_frame( scene, &frame_view, + None, size, premultiplied_alpha, wgpu::Color::TRANSPARENT, @@ -1337,6 +1432,36 @@ impl WgpuRendererCore { }); let max_texture_size = device.limits().max_texture_dimension_2d; + let placeholder = device.create_texture(&wgpu::TextureDescriptor { + label: Some("backdrop_placeholder"), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: target_format, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + let placeholder_view = placeholder.create_view(&wgpu::TextureViewDescriptor::default()); + let backdrop_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("backdrop_bind_group"), + layout: &bind_group_layouts.backdrop, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&placeholder_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&atlas_sampler), + }, + ], + }); + Self { resources: WgpuResources { device, @@ -1353,6 +1478,9 @@ impl WgpuRendererCore { path_intermediate_view: None, path_msaa_texture: None, path_msaa_view: None, + backdrop_texture: Some(placeholder), + backdrop_view: Some(placeholder_view), + backdrop_bind_group: Some(backdrop_bind_group), }, atlas, path_globals_offset, @@ -1418,6 +1546,7 @@ impl WgpuRendererCore { &mut self, scene: &Scene, target_view: &wgpu::TextureView, + sample_view: Option<&wgpu::TextureView>, size: Size, premultiplied_alpha: bool, clear_color: wgpu::Color, @@ -1468,7 +1597,7 @@ impl WgpuRendererCore { bytemuck::bytes_of(&gamma_params), ); - self.record_frame(scene, target_view, clear_color) + self.record_frame(scene, target_view, sample_view, clear_color) .inspect_err(|_| { // Queue writes are staged before encoding; flush them even if the frame fails. self.resources.queue.submit(std::iter::empty()); @@ -1479,6 +1608,7 @@ impl WgpuRendererCore { &mut self, scene: &Scene, frame_view: &wgpu::TextureView, + sample_view: Option<&wgpu::TextureView>, clear_color: wgpu::Color, ) -> Result { let mut instance_offset = 0; @@ -1505,6 +1635,7 @@ impl WgpuRendererCore { label: Some("main_encoder"), }); + let mut submission = None; { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("main_pass"), @@ -1523,12 +1654,44 @@ impl WgpuRendererCore { for batch in scene.batches() { match batch { - PrimitiveBatch::Quads(range) => self.draw_instances( - &instance_bindings.quads, - &self.resources().pipelines.quads, - instance_range(range), - &mut pass, - ), + PrimitiveBatch::Quads(range) => { + if scene.quads[range.clone()] + .iter() + .any(|quad| quad.background.needs_backdrop()) + { + // The quad shader blurs the colour target as it + // stood before this batch. The preceding pass has + // to be submitted before the copy, or the copy + // reads a target the GPU has not written yet. + drop(pass); + submission = Some(self.resources().queue.submit([encoder.finish()])); + encoder = self.resources().device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("backdrop_encoder"), + }, + ); + self.capture_backdrop(&mut encoder, frame_view, sample_view); + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass_continued"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + } + self.draw_quads( + &instance_bindings.quads, + instance_range(range), + &mut pass, + ); + } PrimitiveBatch::Shadows(range) => self.draw_instances( &instance_bindings.shadows, &self.resources().pipelines.shadows, @@ -1616,11 +1779,11 @@ impl WgpuRendererCore { } } - let submission = self + let index = self .resources() .queue .submit(std::iter::once(encoder.finish())); - Ok(submission) + Ok(submission.unwrap_or(index)) } fn write_instances( @@ -1732,6 +1895,99 @@ impl WgpuRendererCore { } } + /// Sample the colour target into the backdrop texture, so the quad shader + /// can blur what has already been drawn. A render pass rather than a copy: + /// a copy reads the texture before the preceding pass's store is visible, + /// and sampling it sees the stored contents. The texture matches the + /// target's size and is rebuilt when that changes. + fn capture_backdrop( + &mut self, + encoder: &mut wgpu::CommandEncoder, + frame_view: &wgpu::TextureView, + sample_view: Option<&wgpu::TextureView>, + ) { + let texture = frame_view.texture(); + let width = texture.width(); + let height = texture.height(); + + let current = self.resources().backdrop_texture.as_ref().is_some_and( + |backdrop| backdrop.width() == width && backdrop.height() == height, + ); + if !current { + let device = &self.resources().device; + let backdrop = device.create_texture(&wgpu::TextureDescriptor { + label: Some("backdrop_capture"), + size: wgpu::Extent3d { + width: width.max(1), + height: height.max(1), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: texture.format(), + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let view = backdrop.create_view(&wgpu::TextureViewDescriptor::default()); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("backdrop_bind_group"), + layout: &self.resources().bind_group_layouts.backdrop, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.resources().atlas_sampler), + }, + ], + }); + let resources = self.resources_mut(); + resources.backdrop_texture = Some(backdrop); + resources.backdrop_view = Some(view); + resources.backdrop_bind_group = Some(bind_group); + } + + // KNOWN ISSUE (WI-1349): the capture pass below is disabled. Sampling + // the colour target into the backdrop texture makes the preceding + // pass's draws disappear from the target — the red quad drawn before + // the blur reads back black once this pass runs, and the blur quad + // then samples black. What was ruled out: creating the backdrop + // texture is harmless, and splitting the submission without this pass + // is harmless too. The suspect is the source bind group (the colour + // target bound as a sampled texture) or the capture pass itself. + // `textureLoad` of the target also reads black, while a constant + // fragment colour written by this pass does reach the blur quad, so + // the backdrop texture, its bind group and the quad shader's sampling + // of it all work. Re-enable by deleting this return. + let _ = (encoder, sample_view); + return; + } + fn draw_quads( + &self, + instances: &InstanceBinding, + range: Range, + pass: &mut wgpu::RenderPass<'_>, + ) { + if range.is_empty() { + return; + } + pass.set_pipeline(&self.resources().pipelines.quads); + pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); + pass.set_bind_group(1, &instances.bind_group, &[]); + // The pipeline always binds the capture. Batches without a blur quad + // never sample it, but the binding has to exist, so a 1x1 placeholder + // stands in until the first capture. + pass.set_bind_group(3, self.resources().backdrop_bind_group.as_ref().unwrap(), &[]); + pass.draw( + 0..4, + instances.first_instance + range.start..instances.first_instance + range.end, + ); + } + fn draw_instances( &self, instances: &InstanceBinding, @@ -2287,6 +2543,9 @@ impl WgpuRenderer { struct HeadlessRenderTarget { texture: wgpu::Texture, view: wgpu::TextureView, + /// A second view of the same texture, created to be bound as a sampled + /// texture. The view a render pass writes through cannot be sampled. + sample_view: wgpu::TextureView, } #[cfg(all( @@ -2373,11 +2632,18 @@ impl WgpuHeadlessRenderer { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: self.core.target_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - self.render_target = Some(HeadlessRenderTarget { texture, view }); + let sample_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + self.render_target = Some(HeadlessRenderTarget { + texture, + view, + sample_view, + }); Ok(()) } @@ -2399,14 +2665,15 @@ impl WgpuHeadlessRenderer { fn render(&mut self, scene: &Scene, size: Size) -> anyhow::Result<()> { self.check_gpu_errors()?; self.ensure_render_target(size)?; - let view = self - .render_target - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))? - .view - .clone(); + let (view, sample_view) = { + let target = self + .render_target + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))?; + (target.view.clone(), target.sample_view.clone()) + }; self.core - .render_frame(scene, &view, size, false, wgpu::Color::BLACK)?; + .render_frame(scene, &view, Some(&sample_view), size, false, wgpu::Color::BLACK)?; Ok(()) } @@ -2692,6 +2959,36 @@ mod tests { Ok(()) } + /// A backdrop-blur quad must read what was drawn before it, not the clear + /// colour. A red quad fills the left half; a blur quad covers the seam, so + /// its left edge has to come out red and its right edge black. + /// + /// Ignored: the capture pass that feeds the blur is disabled. See the + /// "Known issue" note on `capture_backdrop` and the PR for WI-1349. + #[ignore = "backdrop capture pass disabled; see WI-1349"] + #[cfg(target_os = "linux")] + #[test] + fn headless_renderer_blurs_the_pixels_drawn_behind_a_quad() -> anyhow::Result<()> { + let mut renderer = WgpuHeadlessRenderer::new()?; + let mut scene = Scene::default(); + scene.insert_primitive(solid_quad(0.0, 0.0, 32.0, 32.0, gpui::red())); + + let mut blur = solid_quad(16.0, 0.0, 32.0, 32.0, gpui::transparent_black()); + blur.background = gpui::backdrop_blur(8.0, gpui::transparent_black()); + scene.insert_primitive(blur); + scene.finish(); + + let image = renderer.render_scene_to_image(&scene, device_size(64, 32))?; + assert_pixel(&image, 8, 16, RED); + // Inside the blur, over the red quad: still red, since red blurs to red. + assert_pixel(&image, 24, 16, RED); + // Inside the blur, over untouched black: still black. + assert_pixel(&image, 40, 16, BLACK); + // Past the blur quad the red never reached. + assert_pixel(&image, 56, 16, BLACK); + Ok(()) + } + #[cfg(target_os = "linux")] #[test] fn headless_renderer_captures_each_requested_size() -> anyhow::Result<()> { From b49d509ed3816fadb564e850af09968ea752bb29 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:59:37 +0000 Subject: [PATCH 2/3] Fix the wgpu backdrop capture: split quad batches at blur quads The capture ran before the whole quad batch, so a blur quad sharing a batch with the quads behind it sampled the clear colour and painted it over them. Draw the batch in runs that start at a blur quad, capture by texture copy between runs, and drop the sampling pipeline. The surface asks for COPY_SRC where supported. The tint now reaches the shader. Co-Authored-By: Claude Opus 5.5 --- vendor/zed/crates/gpui_wgpu/src/shaders.wgsl | 17 +- .../zed/crates/gpui_wgpu/src/wgpu_renderer.rs | 266 +++++++++--------- 2 files changed, 132 insertions(+), 151 deletions(-) diff --git a/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl b/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl index a016739..85cb73f 100644 --- a/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl +++ b/vendor/zed/crates/gpui_wgpu/src/shaders.wgsl @@ -413,7 +413,8 @@ fn prepare_gradient_color(tag: u32, color_space: u32, solid: Hsla, colors: array) -> GradientColor { var result = GradientColor(); - if (tag == 0u || tag == 2u || tag == 3u) { + // 4u (BackdropBlur) carries its tint in `solid`. + if (tag == 0u || tag == 2u || tag == 3u || tag == 4u) { result.solid = hsla_to_rgba(solid); } else if (tag == 1u) { // The hsla_to_rgba is returns a linear sRGB color @@ -546,20 +547,6 @@ struct QuadVarying { @location(5) @interpolate(flat) background_color1: vec4, } -@vertex -fn vs_backdrop(@builtin(vertex_index) index: u32) -> @builtin(position) vec4 { - let x = f32(index & 1u) * 2.0 - 1.0; - let y = f32((index >> 1u) & 1u) * 2.0 - 1.0; - return vec4(x, y, 0.0, 1.0); -} - -@fragment -fn fs_backdrop(@builtin(position) position: vec4) -> @location(0) vec4 { - let dims = textureDimensions(t_sprite); - let texel = vec2(position.xy); - return textureLoad(t_sprite, texel, 0); -} - @vertex fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); diff --git a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs index 307f6bb..a052201 100644 --- a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -134,10 +134,6 @@ struct WgpuPipelines { poly_sprites: wgpu::RenderPipeline, #[allow(dead_code)] surfaces: wgpu::RenderPipeline, - /// Copies the colour target into the backdrop texture by sampling it. - /// Unused while the capture pass in `capture_backdrop` is disabled. - #[allow(dead_code)] - backdrop_copy: wgpu::RenderPipeline, } /// One frame allocation of instance data, ready to bind. @@ -460,8 +456,13 @@ impl WgpuRenderer { ); } + // COPY_SRC lets a backdrop-blur quad capture what was drawn before it. + // Where the surface cannot be copied from, the blur samples the last + // capture (or nothing) instead. + let surface_usage = wgpu::TextureUsages::RENDER_ATTACHMENT + | (surface_caps.usages & wgpu::TextureUsages::COPY_SRC); let surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + usage: surface_usage, format: surface_format, width: clamped_width.max(1), height: clamped_height.max(1), @@ -988,41 +989,6 @@ impl WgpuRendererCore { &shader_module, ); - let backdrop_copy_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("backdrop_copy_layout"), - bind_group_layouts: &[Some(&layouts.globals), None, Some(&layouts.texture)], - immediate_size: 0, - }); - let backdrop_copy = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("backdrop_copy"), - layout: Some(&backdrop_copy_layout), - vertex: wgpu::VertexState { - module: &shader_module, - entry_point: Some("vs_backdrop"), - buffers: &[], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader_module, - entry_point: Some("fs_backdrop"), - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleStrip, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview_mask: None, - cache: None, - }); - WgpuPipelines { quads, shadows, @@ -1033,7 +999,6 @@ impl WgpuRendererCore { subpixel_sprites, poly_sprites, surfaces, - backdrop_copy, } } @@ -1293,7 +1258,6 @@ impl WgpuRenderer { if let Err(error) = core.render_frame( scene, &frame_view, - None, size, premultiplied_alpha, wgpu::Color::TRANSPARENT, @@ -1546,7 +1510,6 @@ impl WgpuRendererCore { &mut self, scene: &Scene, target_view: &wgpu::TextureView, - sample_view: Option<&wgpu::TextureView>, size: Size, premultiplied_alpha: bool, clear_color: wgpu::Color, @@ -1597,7 +1560,7 @@ impl WgpuRendererCore { bytemuck::bytes_of(&gamma_params), ); - self.record_frame(scene, target_view, sample_view, clear_color) + self.record_frame(scene, target_view, clear_color) .inspect_err(|_| { // Queue writes are staged before encoding; flush them even if the frame fails. self.resources.queue.submit(std::iter::empty()); @@ -1608,7 +1571,6 @@ impl WgpuRendererCore { &mut self, scene: &Scene, frame_view: &wgpu::TextureView, - sample_view: Option<&wgpu::TextureView>, clear_color: wgpu::Color, ) -> Result { let mut instance_offset = 0; @@ -1635,7 +1597,6 @@ impl WgpuRendererCore { label: Some("main_encoder"), }); - let mut submission = None; { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("main_pass"), @@ -1655,42 +1616,47 @@ impl WgpuRendererCore { for batch in scene.batches() { match batch { PrimitiveBatch::Quads(range) => { - if scene.quads[range.clone()] - .iter() - .any(|quad| quad.background.needs_backdrop()) - { - // The quad shader blurs the colour target as it - // stood before this batch. The preceding pass has - // to be submitted before the copy, or the copy - // reads a target the GPU has not written yet. - drop(pass); - submission = Some(self.resources().queue.submit([encoder.finish()])); - encoder = self.resources().device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("backdrop_encoder"), - }, + // A blur quad samples the colour target as it stood + // just before it, so the batch is drawn in runs that + // each start at a blur quad, with a capture between + // them. Adjacent blur quads share one capture. + let quads = &scene.quads[range.clone()]; + let mut start = 0; + while start < quads.len() { + let blur = quads[start].background.needs_backdrop(); + let end = (start + 1..quads.len()) + .find(|&ix| { + quads[ix].background.needs_backdrop() + && !quads[ix - 1].background.needs_backdrop() + }) + .unwrap_or(quads.len()); + if blur { + drop(pass); + self.capture_backdrop(&mut encoder, frame_view); + pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_pass_continued"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + } + let first = range.start + start; + let last = range.start + end; + self.draw_quads( + &instance_bindings.quads, + instance_range(first..last), + &mut pass, ); - self.capture_backdrop(&mut encoder, frame_view, sample_view); - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("main_pass_continued"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: frame_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); + start = end; } - self.draw_quads( - &instance_bindings.quads, - instance_range(range), - &mut pass, - ); } PrimitiveBatch::Shadows(range) => self.draw_instances( &instance_bindings.shadows, @@ -1779,11 +1745,11 @@ impl WgpuRendererCore { } } - let index = self + let submission = self .resources() .queue .submit(std::iter::once(encoder.finish())); - Ok(submission.unwrap_or(index)) + Ok(submission) } fn write_instances( @@ -1895,39 +1861,45 @@ impl WgpuRendererCore { } } - /// Sample the colour target into the backdrop texture, so the quad shader - /// can blur what has already been drawn. A render pass rather than a copy: - /// a copy reads the texture before the preceding pass's store is visible, - /// and sampling it sees the stored contents. The texture matches the - /// target's size and is rebuilt when that changes. + /// Copy the colour target into the backdrop texture, so the quad shader + /// can blur what has already been drawn. The texture matches the target's + /// size and is rebuilt when that changes. A target that cannot be copied + /// from (a surface without COPY_SRC) leaves the previous capture bound. fn capture_backdrop( &mut self, encoder: &mut wgpu::CommandEncoder, frame_view: &wgpu::TextureView, - sample_view: Option<&wgpu::TextureView>, ) { let texture = frame_view.texture(); + if !texture.usage().contains(wgpu::TextureUsages::COPY_SRC) { + return; + } let width = texture.width(); let height = texture.height(); - let current = self.resources().backdrop_texture.as_ref().is_some_and( - |backdrop| backdrop.width() == width && backdrop.height() == height, - ); + let current = self + .resources() + .backdrop_texture + .as_ref() + .is_some_and(|backdrop| { + backdrop.width() == width + && backdrop.height() == height + && backdrop.format() == texture.format() + }); if !current { let device = &self.resources().device; let backdrop = device.create_texture(&wgpu::TextureDescriptor { label: Some("backdrop_capture"), size: wgpu::Extent3d { - width: width.max(1), - height: height.max(1), + width, + height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: texture.format(), - usage: wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::RENDER_ATTACHMENT, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let view = backdrop.create_view(&wgpu::TextureViewDescriptor::default()); @@ -1951,21 +1923,18 @@ impl WgpuRendererCore { resources.backdrop_bind_group = Some(bind_group); } - // KNOWN ISSUE (WI-1349): the capture pass below is disabled. Sampling - // the colour target into the backdrop texture makes the preceding - // pass's draws disappear from the target — the red quad drawn before - // the blur reads back black once this pass runs, and the blur quad - // then samples black. What was ruled out: creating the backdrop - // texture is harmless, and splitting the submission without this pass - // is harmless too. The suspect is the source bind group (the colour - // target bound as a sampled texture) or the capture pass itself. - // `textureLoad` of the target also reads black, while a constant - // fragment colour written by this pass does reach the blur quad, so - // the backdrop texture, its bind group and the quad shader's sampling - // of it all work. Re-enable by deleting this return. - let _ = (encoder, sample_view); - return; + let backdrop = self.resources().backdrop_texture.as_ref().unwrap(); + encoder.copy_texture_to_texture( + texture.as_image_copy(), + backdrop.as_image_copy(), + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); } + fn draw_quads( &self, instances: &InstanceBinding, @@ -1981,7 +1950,11 @@ impl WgpuRendererCore { // The pipeline always binds the capture. Batches without a blur quad // never sample it, but the binding has to exist, so a 1x1 placeholder // stands in until the first capture. - pass.set_bind_group(3, self.resources().backdrop_bind_group.as_ref().unwrap(), &[]); + pass.set_bind_group( + 3, + self.resources().backdrop_bind_group.as_ref().unwrap(), + &[], + ); pass.draw( 0..4, instances.first_instance + range.start..instances.first_instance + range.end, @@ -2543,9 +2516,6 @@ impl WgpuRenderer { struct HeadlessRenderTarget { texture: wgpu::Texture, view: wgpu::TextureView, - /// A second view of the same texture, created to be bound as a sampled - /// texture. The view a render pass writes through cannot be sampled. - sample_view: wgpu::TextureView, } #[cfg(all( @@ -2632,18 +2602,11 @@ impl WgpuHeadlessRenderer { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: self.core.target_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::TEXTURE_BINDING, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let sample_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - self.render_target = Some(HeadlessRenderTarget { - texture, - view, - sample_view, - }); + self.render_target = Some(HeadlessRenderTarget { texture, view }); Ok(()) } @@ -2665,15 +2628,14 @@ impl WgpuHeadlessRenderer { fn render(&mut self, scene: &Scene, size: Size) -> anyhow::Result<()> { self.check_gpu_errors()?; self.ensure_render_target(size)?; - let (view, sample_view) = { - let target = self - .render_target - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))?; - (target.view.clone(), target.sample_view.clone()) - }; + let view = self + .render_target + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))? + .view + .clone(); self.core - .render_frame(scene, &view, Some(&sample_view), size, false, wgpu::Color::BLACK)?; + .render_frame(scene, &view, size, false, wgpu::Color::BLACK)?; Ok(()) } @@ -2960,12 +2922,10 @@ mod tests { } /// A backdrop-blur quad must read what was drawn before it, not the clear - /// colour. A red quad fills the left half; a blur quad covers the seam, so - /// its left edge has to come out red and its right edge black. - /// - /// Ignored: the capture pass that feeds the blur is disabled. See the - /// "Known issue" note on `capture_backdrop` and the PR for WI-1349. - #[ignore = "backdrop capture pass disabled; see WI-1349"] + /// colour. A red quad fills the left half; a blur quad covers the seam. + /// Both share one quad batch, so the renderer has to split the batch and + /// capture between them. Away from the seam the backdrop shows through + /// unchanged; on the seam it is blurred into a mix of red and black. #[cfg(target_os = "linux")] #[test] fn headless_renderer_blurs_the_pixels_drawn_behind_a_quad() -> anyhow::Result<()> { @@ -2986,6 +2946,40 @@ mod tests { assert_pixel(&image, 40, 16, BLACK); // Past the blur quad the red never reached. assert_pixel(&image, 56, 16, BLACK); + // On the seam the red is spread across it: neither solid nor gone. + for x in [31, 32] { + let [r, g, b, _] = image.get_pixel(x, 16).0; + assert!( + (20..=235).contains(&r) && g <= 3 && b <= 3, + "pixel ({x}, 16) was {:?}, expected a red/black mix", + image.get_pixel(x, 16).0 + ); + } + Ok(()) + } + + /// The tint composites over the blurred backdrop. + #[cfg(target_os = "linux")] + #[test] + fn headless_renderer_tints_a_backdrop_blur() -> anyhow::Result<()> { + let mut renderer = WgpuHeadlessRenderer::new()?; + let mut scene = Scene::default(); + scene.insert_primitive(solid_quad(0.0, 0.0, 64.0, 32.0, gpui::red())); + + let mut blur = solid_quad(0.0, 0.0, 64.0, 32.0, gpui::transparent_black()); + let mut tint = gpui::blue(); + tint.a = 0.5; + blur.background = gpui::backdrop_blur(4.0, tint); + scene.insert_primitive(blur); + scene.finish(); + + let image = renderer.render_scene_to_image(&scene, device_size(64, 32))?; + let [r, g, b, _] = image.get_pixel(32, 16).0; + assert!( + (100..=160).contains(&r) && g <= 3 && (100..=160).contains(&b), + "pixel (32, 16) was {:?}, expected half red, half blue", + image.get_pixel(32, 16).0 + ); Ok(()) } From 15aabcb35aa67c9b2db96d735c654b1af78ad12b Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:06:40 +0000 Subject: [PATCH 3/3] Backdrop blur on the DirectX renderer Mirror the wgpu path: split quad batches at blur quads, CopyResource the render target into a backdrop texture bound at t2, and blur it in quad_fragment with the same 13-tap kernel and tint. Record the files in PATCHES.md. Co-Authored-By: Claude Opus 5.5 --- vendor/zed/PATCHES.md | 2 +- .../gpui_windows/src/directx_renderer.rs | 83 ++++++++++++++++--- .../zed/crates/gpui_windows/src/shaders.hlsl | 37 ++++++++- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/vendor/zed/PATCHES.md b/vendor/zed/PATCHES.md index ab6eada..40bda38 100644 --- a/vendor/zed/PATCHES.md +++ b/vendor/zed/PATCHES.md @@ -5,4 +5,4 @@ Each local patch to this tree is a commit on the integrating branch, never on `z | patch | commit | files | |---|---|---| | `assets/fonts` | this commit | `vendor/zed/assets/fonts/` | -| `backdrop-blur` | this commit | `vendor/zed/crates/gpui/src/color.rs`, `vendor/zed/crates/gpui/src/style.rs`, `vendor/zed/crates/gpui/src/styled.rs`, `vendor/zed/crates/gpui_wgpu/src/shaders.wgsl`, `vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs` | +| `backdrop-blur` | this commit | `vendor/zed/crates/gpui/src/color.rs`, `vendor/zed/crates/gpui/src/style.rs`, `vendor/zed/crates/gpui/src/styled.rs`, `vendor/zed/crates/gpui_wgpu/src/shaders.wgsl`, `vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs`, `vendor/zed/crates/gpui_windows/src/directx_renderer.rs`, `vendor/zed/crates/gpui_windows/src/shaders.hlsl` | diff --git a/vendor/zed/crates/gpui_windows/src/directx_renderer.rs b/vendor/zed/crates/gpui_windows/src/directx_renderer.rs index f8b86bc..e64b532 100644 --- a/vendor/zed/crates/gpui_windows/src/directx_renderer.rs +++ b/vendor/zed/crates/gpui_windows/src/directx_renderer.rs @@ -79,6 +79,11 @@ struct DirectXResources { path_intermediate_msaa_texture: ID3D11Texture2D, path_intermediate_msaa_view: Option, + // A copy of the render target taken before a run of backdrop-blur quads, + // bound at t2 so the quad shader can blur what was drawn before them. + backdrop_texture: ID3D11Texture2D, + backdrop_srv: Option, + // Cached viewport viewport: D3D11_VIEWPORT, } @@ -368,7 +373,7 @@ impl DirectXRenderer { .map(|annotation| Annotation::new(annotation, HSTRING::from(batch.label()))); match batch { PrimitiveBatch::Shadows(range) => self.draw_shadows(range.start, range.len()), - PrimitiveBatch::Quads(range) => self.draw_quads(range.start, range.len()), + PrimitiveBatch::Quads(range) => self.draw_quads(&scene.quads[range.clone()], range.start), PrimitiveBatch::Paths(range) => { let paths = &scene.paths[range]; self.draw_paths_to_intermediate(paths)?; @@ -596,20 +601,62 @@ impl DirectXRenderer { ) } - fn draw_quads(&mut self, start: usize, len: usize) -> Result<()> { - if len == 0 { - return Ok(()); + /// Draw a quad batch. A backdrop-blur quad samples the render target as + /// it stood just before it, so the batch is drawn in runs that each start + /// at a blur quad, with a copy of the target taken between them. Adjacent + /// blur quads share one copy. + fn draw_quads(&mut self, quads: &[Quad], first: usize) -> Result<()> { + let mut start = 0; + while start < quads.len() { + let end = (start + 1..quads.len()) + .find(|&ix| { + quads[ix].background.needs_backdrop() + && !quads[ix - 1].background.needs_backdrop() + }) + .unwrap_or(quads.len()); + if quads[start].background.needs_backdrop() { + self.capture_backdrop()?; + } + let devices = self.devices.as_ref().context("devices missing")?; + self.pipelines.quad_pipeline.draw_range( + &devices.device_context, + self.globals + .batch_params_buffer + .as_ref() + .context("batch params buffer missing")?, + (first + start) as u32, + (end - start) as u32, + )?; + start = end; } + Ok(()) + } + + /// Copy the render target into the backdrop texture and bind it at t2, + /// with the shared linear sampler at s0, for the quad shader to blur. + fn capture_backdrop(&mut self) -> Result<()> { let devices = self.devices.as_ref().context("devices missing")?; - self.pipelines.quad_pipeline.draw_range( - &devices.device_context, - self.globals - .batch_params_buffer - .as_ref() - .context("batch params buffer missing")?, - start as u32, - len as u32, - ) + let resources = self.resources.as_ref().context("resources missing")?; + let render_target = resources + .render_target + .as_ref() + .context("missing render target")?; + unsafe { + // Unbind the capture so the copy does not write a bound resource. + devices + .device_context + .PSSetShaderResources(2, Some(&[None])); + devices + .device_context + .CopyResource(&resources.backdrop_texture, render_target); + devices + .device_context + .PSSetShaderResources(2, Some(slice::from_ref(&resources.backdrop_srv))); + devices + .device_context + .PSSetSamplers(0, Some(slice::from_ref(&self.globals.sampler))); + } + Ok(()) } fn draw_paths_to_intermediate(&mut self, paths: &[Path]) -> Result<()> { @@ -899,6 +946,8 @@ impl DirectXResources { viewport, ) = create_resources(devices, &swap_chain, width, height)?; set_rasterizer_state(&devices.device, &devices.device_context)?; + let (backdrop_texture, backdrop_srv) = + create_path_intermediate_texture(&devices.device, width, height)?; Ok(Self { swap_chain, @@ -908,6 +957,8 @@ impl DirectXResources { path_intermediate_msaa_texture, path_intermediate_msaa_view, path_intermediate_srv, + backdrop_texture, + backdrop_srv, viewport, }) } @@ -934,6 +985,12 @@ impl DirectXResources { self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; self.path_intermediate_msaa_view = path_intermediate_msaa_view; self.path_intermediate_srv = path_intermediate_srv; + // Release the old capture before allocating its replacement. + self.backdrop_srv = None; + let (backdrop_texture, backdrop_srv) = + create_path_intermediate_texture(&devices.device, width, height)?; + self.backdrop_texture = backdrop_texture; + self.backdrop_srv = backdrop_srv; self.viewport = viewport; Ok(()) } diff --git a/vendor/zed/crates/gpui_windows/src/shaders.hlsl b/vendor/zed/crates/gpui_windows/src/shaders.hlsl index ba89c68..8795290 100644 --- a/vendor/zed/crates/gpui_windows/src/shaders.hlsl +++ b/vendor/zed/crates/gpui_windows/src/shaders.hlsl @@ -16,6 +16,9 @@ cbuffer BatchParams: register(b1) { Texture2D t_sprite: register(t0); SamplerState s_sprite: register(s0); +// A copy of the render target taken before the current run of backdrop-blur +// quads. Bound only while such quads draw; the quad shader blurs it. +Texture2D t_backdrop: register(t2); struct SubpixelSpriteFragmentOutput { float4 foreground : SV_Target0; @@ -316,7 +319,8 @@ float quad_sdf(float2 pt, Bounds bounds, Corners corner_radii) { GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, LinearColorStop colors[2]) { GradientColor output; - if (tag == 0 || tag == 2 || tag == 3) { + // 4 (BackdropBlur) carries its tint in `solid`. + if (tag == 0 || tag == 2 || tag == 3 || tag == 4) { output.solid = hsla_to_rgba(solid); } else if (tag == 1) { output.color0 = hsla_to_rgba(colors[0].color); @@ -558,8 +562,35 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_I float4 quad_fragment(QuadFragmentInput input): SV_Target { Quad quad = quads[input.quad_id]; - float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); + float4 background_color; + if (quad.background.tag == 4) { + // BackdropBlur, mirroring `fs_quad` in gpui_wgpu's shaders.wgsl. The + // radius rides in `gradient_angle_or_pattern_height` (device pixels); + // a 13-tap cross kernel approximates a gaussian of sigma = radius / 2, + // and `solid` is the tint, composited over the blur. + float radius = max(quad.background.gradient_angle_or_pattern_height, 0.5); + float2 uv = input.position.xy / global_viewport_size; + float2 texel = 1.0 / global_viewport_size; + const float weights[5] = { 0.227027, 0.194594, 0.121622, 0.054054, 0.016216 }; + float4 blurred = t_backdrop.Sample(s_sprite, uv) * weights[0]; + for (int i = 1; i < 5; i++) { + float2 offset = texel * (radius * 0.25 * i); + blurred += t_backdrop.Sample(s_sprite, uv + float2(offset.x, 0.0)) * weights[i]; + blurred += t_backdrop.Sample(s_sprite, uv - float2(offset.x, 0.0)) * weights[i]; + blurred += t_backdrop.Sample(s_sprite, uv + float2(0.0, offset.y)) * weights[i]; + blurred += t_backdrop.Sample(s_sprite, uv - float2(0.0, offset.y)) * weights[i]; + } + blurred /= weights[0] + 4.0 * (weights[1] + weights[2] + weights[3] + weights[4]); + + float4 tint = input.background_solid; + background_color = float4( + tint.rgb * tint.a + blurred.rgb * (1.0 - tint.a), + tint.a + blurred.a * (1.0 - tint.a) + ); + } else { + background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + input.background_solid, input.background_color0, input.background_color1); + } bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.top_right == 0.0 &&