From 40129147c64fbf6f7f021ddbeed7f10a3aa5f91b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Aug 2025 20:40:33 -0700 Subject: [PATCH] Respect paths' content masks when copying them from MSAA texture to drawable (#35688) Fixes a regression introduced in https://github.com/zed-industries/zed/pull/34992 ### Background Paths are rendered first to an intermediate MSAA texture, and then copied to the final drawable. Because paths can have transparency, it's important that pixels are not copied repeatedly if paths have overlapping bounding boxes. When N paths have the same draw order, we infer that they must have disjoint bounding boxes, so that we can copy them each individually (as opposed to copying a single rect that contains them all). Previously, the bounding box that we were using to copy paths was not accounting for the path's content mask (but it is accounted for in the bounds tree that determines their draw order). This cause bugs like this, where certain path pixels spuriously had their opacity doubled: https://github.com/user-attachments/assets/d792e60c-790b-49ad-b435-6695daba430f This PR fixes that bug. * [x] mac * [x] linux * [x] windows Release Notes: - Fixed a bug where a selection's opacity was computed incorrectly when it overlapped with another editor's selections in a certain way. --- .../gpui/src/platform/blade/blade_renderer.rs | 8 ++++---- crates/gpui/src/platform/mac/metal_renderer.rs | 6 +++--- .../src/platform/windows/directx_renderer.rs | 8 ++++---- crates/gpui/src/scene.rs | 17 ++++++++++++++++- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 2e18d2be222dc561d4494ac68db6b5b8d00abed2..46d3c16c72a9c10c0e686aff425fcc236c253ce7 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -606,7 +606,7 @@ impl BladeRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.bounds.intersect(&path.content_mask.bounds), + bounds: path.clipped_bounds(), })); } let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; @@ -735,13 +735,13 @@ impl BladeRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect() } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } vec![PathSprite { bounds }] }; diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index fb5cb852d656e8d07354e44b103422afe261f12e..629654014d5a15632c5992d9347cab3ee1fd28d9 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -791,13 +791,13 @@ impl MetalRenderer { sprites = paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect(); } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } sprites = vec![PathSprite { bounds }]; } diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 72cc12a5b4aa6eb2d89d8a936a8df1b2dd599cd1..ac285b79acee82571456943d2f660947687b56c3 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -435,7 +435,7 @@ impl DirectXRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.bounds.intersect(&path.content_mask.bounds), + bounds: path.clipped_bounds(), })); } @@ -487,13 +487,13 @@ impl DirectXRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect::>() } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } vec![PathSprite { bounds }] }; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ec8d720cdfaf84a521d585b581c80a2dbe2ff6f7..c527dfe750beb2d19ed6750e48f71208ec2720bf 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -8,7 +8,12 @@ use crate::{ AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, }; -use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; +use std::{ + fmt::Debug, + iter::Peekable, + ops::{Add, Range, Sub}, + slice, +}; #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; @@ -793,6 +798,16 @@ impl Path { } } +impl Path +where + T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, +{ + #[allow(unused)] + pub(crate) fn clipped_bounds(&self) -> Bounds { + self.bounds.intersect(&self.content_mask.bounds) + } +} + impl From> for Primitive { fn from(path: Path) -> Self { Primitive::Path(path)