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.
This commit is contained in:
Max Brunsfeld 2025-08-05 20:40:33 -07:00 committed by GitHub
parent a884e861e9
commit 40129147c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 27 additions and 12 deletions

View file

@ -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 }]
};

View file

@ -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 }];
}

View file

@ -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::<Vec<_>>()
} 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 }]
};

View file

@ -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<ScaledPixels>;
@ -793,6 +798,16 @@ impl Path<Pixels> {
}
}
impl<T> Path<T>
where
T: Clone + Debug + Default + PartialEq + PartialOrd + Add<T, Output = T> + Sub<Output = T>,
{
#[allow(unused)]
pub(crate) fn clipped_bounds(&self) -> Bounds<T> {
self.bounds.intersect(&self.content_mask.bounds)
}
}
impl From<Path<ScaledPixels>> for Primitive {
fn from(path: Path<ScaledPixels>) -> Self {
Primitive::Path(path)