Align corner clipping, antialiasing, and fractional material regions

This commit is contained in:
Samuel Onoja committed 2026-10-01 16:27:31 +01:00
1 parent f110ec80e1
commit 9666e4ff6c
16 files changed
+237 -68

No files matched your search

+25 -12
View File
@@ -56,7 +56,7 @@ struct SurfaceEffectsState {
attached: AtomicBool,
generation: AtomicU64,
role: Mutex<Option<SemanticRole>>,
regions: Mutex<Option<Vec<[i32; 5]>>>,
regions: Mutex<Option<Vec<[f64; 5]>>>,
opacity: Mutex<u32>,
presentation_supported: AtomicBool,
dismissing: AtomicBool,
@@ -183,7 +183,7 @@ impl Dispatch<FereseSurfaceMaterialV1, SurfaceEffectsUserData> for Ferese {
}
}
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[f64; 5]>> {
with_states(surface, |states| {
states
.data_map
@@ -195,7 +195,7 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
})
}
fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 {
return None;
}
@@ -204,9 +204,12 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
.0
.iter()
.map(|tuple| {
let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
(values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768))
.then_some(values)
let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i])));
(values[2] > 0.0
&& values[3] > 0.0
&& values[4] >= 0.0
&& values.iter().all(|v| v.is_finite() && v.abs() <= 32768.0))
.then_some(values)
})
.collect()
}
@@ -439,15 +442,25 @@ mod tests {
use super::*;
#[test]
fn rounded_regions_are_bounded_and_validated() {
let region = [12_i32, 8, 120, 48, 11];
let bytes: Vec<_> = region.into_iter().flat_map(i32::to_ne_bytes).collect();
assert_eq!(decode_regions(&bytes), Some(vec![region]));
fn fractional_regions_validate_all_wire_values() {
let region = [0.25f32, -0.75, 100.5, 40.25, 13.5];
let encode = |r: [f32; 5]| r.into_iter().flat_map(f32::to_ne_bytes).collect::<Vec<_>>();
let bytes = encode(region);
assert_eq!(decode_regions(&bytes), Some(vec![region.map(f64::from)]));
assert_eq!(decode_regions(&[]), Some(vec![]));
assert!(decode_regions(&bytes[..19]).is_none());
assert!(decode_regions(&bytes.repeat(33)).is_none());
for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] {
assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::<Vec<_>>()).is_none());
for index in 0..5 {
for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 32769.0] {
let mut bad = region;
bad[index] = invalid;
assert!(decode_regions(&encode(bad)).is_none());
}
}
for (index, value) in [(2, 0.0), (3, -1.0), (4, -0.25)] {
let mut bad = region;
bad[index] = value;
assert!(decode_regions(&encode(bad)).is_none());
}
}
+15 -4
View File
@@ -32,11 +32,22 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
let texture = renderer.compile_custom_texture_shader(&corner_shader(ROUNDED_TEXTURE_SHADER), &texture_uniforms);
let border = renderer.compile_custom_pixel_shader(&corner_shader(ROUNDED_BORDER_SHADER), &border_uniforms);
let shadow = renderer.compile_custom_pixel_shader(&corner_shader(WINDOW_SHADOW_SHADER), &shadow_uniforms);
let compiled = texture.and_then(|texture| border.and_then(|border| shadow.map(|shadow| (texture, border, shadow))));
let solid = renderer.compile_custom_pixel_shader(
&corner_shader(ROUNDED_SOLID_SHADER),
&[
UniformName::new("clip_rect", UniformType::_4f),
UniformName::new("radius", UniformType::_1f),
UniformName::new("color", UniformType::_4f),
],
);
let compiled = texture.and_then(|texture| {
border.and_then(|border| shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid))))
});
match compiled {
Ok((texture, border, shadow)) => {
Ok((texture, border, shadow, solid)) => {
let programs = RoundedClipPrograms {
texture,
solid,
border,
shadow,
};
@@ -343,7 +354,7 @@ pub(super) fn rounded_window_elements(
alpha: f32,
clip_changed: bool,
output: &Output,
program: GlesTexProgram,
programs: RoundedClipPrograms,
behavior: ConstrainScaleBehavior,
) -> Vec<AnimatedWindowRenderElement> {
let Some(toplevel) = window.toplevel() else {
@@ -390,7 +401,7 @@ pub(super) fn rounded_window_elements(
.map(|inner| {
RoundedSurfaceRenderElement {
inner,
program: program.clone(),
programs: programs.clone(),
clip_rect: clip,
radius,
clip_changed,
+44 -15
View File
@@ -20,23 +20,12 @@ pub(super) fn append_material_surface(
};
let regions = crate::effects::surface_regions(surface);
let targets: Vec<_> = match &regions {
None => vec![(geometry, radius as f32)],
None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius))],
Some(regions) => regions
.iter()
.filter_map(|r| {
Rectangle::new(
(content_origin.x + r[0], content_origin.y + r[1]).into(),
(r[2], r[3]).into(),
)
.intersection(geometry)
.map(|rect| (rect, r[4] as f32))
})
.filter_map(|r| material_region_geometry(*r, geometry, content_origin, scale))
.collect(),
};
let targets: Vec<_> = targets
.into_iter()
.map(|(rect, radius)| (rect, RoundedRect::from_logical(rect, scale, f64::from(radius))))
.collect();
let materials: Vec<_> = targets
.iter()
.enumerate()
@@ -65,7 +54,7 @@ pub(super) fn append_material_surface(
let mode = output.current_mode()?;
let program = rounded_clip_program(state, renderer)?;
Some((
program.texture,
program,
framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
corners.radius,
))
@@ -84,7 +73,7 @@ pub(super) fn append_material_surface(
let element = if let Some((program, clip_rect, radius)) = &clip {
WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement {
inner: element,
program: program.clone(),
programs: program.clone(),
clip_rect: *clip_rect,
radius: *radius,
clip_changed: false,
@@ -102,6 +91,27 @@ pub(super) fn append_material_surface(
}
}
/// Preserve the surface-local fractional edges until the shared physical snap.
fn material_region_geometry(
region: [f64; 5],
geometry: Rectangle<i32, Logical>,
origin: Point<i32, Logical>,
scale: f64,
) -> Option<(Rectangle<i32, Logical>, RoundedRect)> {
let rect = Rectangle::<f64, Logical>::new(
(f64::from(origin.x) + region[0], f64::from(origin.y) + region[1]).into(),
(region[2], region[3]).into(),
)
.intersection(geometry.to_f64())?;
let corners = RoundedRect::new(
ferese_layout::Rect::new(rect.loc.x, rect.loc.y, rect.size.w, rect.size.h),
(0, 0).into(),
scale,
region[4],
);
Some((rect.to_i32_up(), corners))
}
pub(super) struct MaterialSurface {
pub geometry: Rectangle<i32, Logical>,
pub corners: RoundedRect,
@@ -424,3 +434,22 @@ pub(super) fn blur_damage(
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
}
}
#[cfg(test)]
mod region_geometry_tests {
use super::*;
#[test]
fn fractional_regions_snap_once_in_output_pixels() {
let bounds = Rectangle::from_size((200, 100).into());
let (_, corners) = material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5).unwrap();
assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into()));
assert_eq!(corners.radius, 21.0);
// The old logical rounding would put this at physical (0, 0).
let (_, clipped) =
material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5).unwrap();
assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into()));
assert_eq!(clipped.radius, 30.0);
assert!(material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5).is_none());
}
}
+78 -3
View File
@@ -96,6 +96,8 @@ render_elements! {
const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag");
const ROUNDED_SOLID_SHADER: &str = include_str!("shaders/rounded_solid_shader.frag");
const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag");
const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag");
@@ -280,6 +282,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
#[derive(Clone, Debug)]
pub(crate) struct RoundedClipPrograms {
solid: GlesPixelProgram,
texture: GlesTexProgram,
border: GlesPixelProgram,
shadow: GlesPixelProgram,
@@ -390,7 +393,7 @@ pub(crate) struct WindowShadowBuffers {
#[derive(Debug)]
struct RoundedSurfaceRenderElement {
inner: WaylandSurfaceRenderElement<GlesRenderer>,
program: GlesTexProgram,
programs: RoundedClipPrograms,
clip_rect: [f32; 4],
radius: f32,
clip_changed: bool,
@@ -459,13 +462,25 @@ impl RenderElement<GlesRenderer> for RoundedSurfaceRenderElement {
opaque_regions,
self.transform(),
self.alpha(),
Some(&self.program),
Some(&self.programs.texture),
&[
Uniform::new("clip_rect", self.clip_rect),
Uniform::new("radius", self.radius),
],
),
WaylandSurfaceTexture::SolidColor(_) => self.inner.draw(frame, src, dst, damage, opaque_regions),
WaylandSurfaceTexture::SolidColor(color) => frame.render_pixel_shader_to(
&self.programs.solid,
src,
dst,
(dst.size.w, dst.size.h).into(),
Some(damage),
self.alpha(),
&[
Uniform::new("clip_rect", self.clip_rect),
Uniform::new("radius", self.radius),
Uniform::new("color", color.components()),
],
),
}
}
@@ -514,6 +529,65 @@ mod tests {
scaled_visual_rect, shadow_bounds,
};
#[test]
#[ignore = "requires an EGL rendering device"]
fn solid_corner_mask_preserves_premultiplied_color() {
use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform, UniformName, UniformType};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
let program = renderer
.compile_custom_pixel_shader(
&super::corner_shader(super::ROUNDED_SOLID_SHADER),
&[
UniformName::new("clip_rect", UniformType::_4f),
UniformName::new("radius", UniformType::_1f),
UniformName::new("color", UniformType::_4f),
],
)
.unwrap();
let size = (32, 32).into();
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let rect = Rectangle::<i32, Physical>::from_size((32, 32).into());
for (radius, corner_alpha, inset) in [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] {
let mut target = renderer.bind(&mut texture).unwrap();
{
let mut frame = renderer
.render(&mut target, (32, 32).into(), Transform::Normal)
.unwrap();
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
frame
.render_pixel_shader_to(
&program,
Rectangle::from_size(size.to_f64()),
rect,
size,
Some(&[rect]),
0.5,
&[
Uniform::new("clip_rect", [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset]),
Uniform::new("radius", radius),
Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]),
],
)
.unwrap();
let _ = frame.finish().unwrap();
}
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
let pixels = renderer.map_texture(&mapping).unwrap();
let center = (16 * 32 + 16) * 4;
assert!(pixels[center].abs_diff(64) <= 1);
assert!(pixels[center + 3].abs_diff(64) <= 1);
assert!(pixels[3].abs_diff(corner_alpha) <= 1);
}
}
#[test]
#[ignore = "requires an EGL rendering device"]
fn shared_corner_shaders_compile() {
@@ -529,6 +603,7 @@ mod tests {
.unwrap();
}
for source in [
super::ROUNDED_SOLID_SHADER,
super::ROUNDED_BORDER_SHADER,
super::WINDOW_SHADOW_SHADER,
super::MATERIAL_SHADER,
+1 -1
View File
@@ -228,7 +228,7 @@ pub(super) fn overview_strip_elements(
alpha,
true,
output,
programs.texture.clone(),
programs.clone(),
ConstrainScaleBehavior::Stretch,
));
}
+1 -1
View File
@@ -265,7 +265,7 @@ pub(crate) fn output_elements(
.get(&id)
.is_some_and(|geometry| geometry.presentation_changed),
output,
programs.texture.clone(),
programs.clone(),
behavior,
));
let source = window.geometry().size;
+2 -1
View File
@@ -6,5 +6,6 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
}
float edge_coverage(float distance) {
return 1.0 - smoothstep(-0.5, 0.5, distance);
// Match Iced/WGPU quad edges: a one-physical-pixel linear ramp.
return clamp(0.5 - distance, 0.0, 1.0);
}
@@ -0,0 +1,13 @@
precision highp float;
uniform float alpha;
uniform vec4 clip_rect;
uniform float radius;
uniform vec4 color;
//_CORNERS_
void main() {
float distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
// Single-pixel buffer colors are already premultiplied.
gl_FragColor = color * alpha * edge_coverage(distance);
}