Match shell squircle outlines with compositor masks
Use shared corner geometry for shell controls, window clipping, materials and blur masks. Preserve circular pills and controls where appropriate, track contour changes in damage, and use tagged ferese-shape and libcosmic dependencies.
This commit is contained in:
1 parent
03a265b97f
commit
d75c93db85
31 files changed
+453
-80
No files matched your search
@@ -153,28 +153,50 @@ fn coverage(distance: f64) -> f64 {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_roles_and_shader_sources_keep_circular_corners() {
|
||||
use crate::effects::SemanticRole::*;
|
||||
fn shared_geometry_matches_the_existing_window_profile() {
|
||||
let bounds = [1.25, 2.5, 61.5, 59.0];
|
||||
|
||||
for role in [Panel, PanelElevated, Popover, Menu, Notification, Hud, Modal] {
|
||||
assert_eq!(corner_shape_for_role(Some(role)), CornerShape::Circular);
|
||||
for shape in [CornerShape::Circular, CornerShape::Continuous] {
|
||||
for radius in [0.0, 0.375, 8.25, 12.375, 24.0, 29.49, 29.5] {
|
||||
let outline = ferese_shape::Outline::new(bounds, [radius; 4], shape).unwrap();
|
||||
|
||||
for y in 0..64 {
|
||||
for x in 0..64 {
|
||||
let point = [x as f64 + 0.5, y as f64 + 0.5];
|
||||
let expected = reference_distance(point, bounds, radius, shape);
|
||||
let actual = outline.signed_distance(point);
|
||||
assert!(
|
||||
(actual - expected).abs() < 0.01,
|
||||
"{shape:?} radius={radius} point={point:?}"
|
||||
);
|
||||
assert!((coverage(actual) - coverage(expected)).abs() <= 2.0 / 255.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(corner_shape_for_role(None), CornerShape::Continuous);
|
||||
|
||||
#[test]
|
||||
fn shell_masks_match_the_shared_squircle_profile() {
|
||||
for source in [BLUR_SHADER, MATERIAL_SHADER, ROUNDED_TEXTURE_SHADER] {
|
||||
assert_eq!(
|
||||
corner_shader(source),
|
||||
source.replace("//_CORNERS_", include_str!("shaders/corners.glsl"))
|
||||
corner_shader_for(source, CornerShape::Continuous)
|
||||
);
|
||||
assert!(!corner_shader(source).contains("CORNER_EXTENT"));
|
||||
assert!(corner_shader(source).contains("CORNER_EXTENT"));
|
||||
assert!(!corner_shader_for(source, CornerShape::Circular).contains("CORNER_EXTENT"));
|
||||
}
|
||||
}
|
||||
|
||||
fn renderer() -> GlesRenderer {
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
|
||||
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||
let devices = EGLDevice::enumerate().unwrap().collect::<Vec<_>>();
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = devices
|
||||
.into_iter()
|
||||
.find(|device| device.is_software() == software)
|
||||
.expect("the requested EGL device");
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }
|
||||
}
|
||||
@@ -334,12 +356,12 @@ fn apple_window_fill_and_inset_border_match_path_reference_at_fractional_scales(
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn window_programs_do_not_change_cached_shell_materials_and_blur() {
|
||||
fn circular_and_squircle_programs_keep_independent_caches() {
|
||||
let mut renderer = renderer();
|
||||
let mut resources = RenderResources::default();
|
||||
let context = renderer.context_id().erased();
|
||||
let circular = rounded_clip_program(&mut resources, &mut renderer).unwrap();
|
||||
material_program(&mut resources, &mut renderer).unwrap();
|
||||
let circular = corner_program(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
|
||||
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
|
||||
blur_program(&mut resources, &mut renderer).unwrap();
|
||||
let uniforms = [
|
||||
Uniform::new("clip_rect", [0.0f32, 0.0, 64.0, 64.0]),
|
||||
@@ -349,7 +371,7 @@ fn window_programs_do_not_change_cached_shell_materials_and_blur() {
|
||||
let before = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||
corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||
let circular = rounded_clip_program(&mut resources, &mut renderer).unwrap();
|
||||
let circular = corner_program(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
|
||||
let after = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||
assert_eq!(before, after);
|
||||
let cache = &resources.contexts[&context];
|
||||
@@ -550,3 +572,57 @@ fn measured_uikit_profile_has_flat_shoulders_but_small_internal_join_discontinui
|
||||
assert!((curvature_jump - 0.354_989_57).abs() < 1.0e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual EGL timing; optional FERESE_CORNER_REFERENCE_SHADER path"]
|
||||
fn corner_heavy_shader_timing_sample() {
|
||||
use smithay::backend::renderer::gles::{UniformName, UniformType};
|
||||
let mut renderer = renderer();
|
||||
let reference = std::env::var_os("FERESE_CORNER_REFERENCE_SHADER")
|
||||
.map(|path| std::fs::read_to_string(path).expect("reference GLSL"));
|
||||
let current = include_str!("shaders/window_corners.glsl");
|
||||
let size = (1024, 768).into();
|
||||
let rect = Rectangle::<i32, Physical>::from_size(size);
|
||||
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap();
|
||||
let body = "\nvoid main() { float d = rounded_rect_distance(mod(gl_FragCoord.xy, vec2(64.0)), vec4(0.0, 0.0, 64.0, 64.0), radius); gl_FragColor = vec4(edge_coverage(d)); }";
|
||||
for (name, source) in reference
|
||||
.as_deref()
|
||||
.map(|s| ("reference", s))
|
||||
.into_iter()
|
||||
.chain([("optimized", current)])
|
||||
{
|
||||
let shader = format!("precision highp float; uniform float radius;\n{source}{body}");
|
||||
let program = renderer
|
||||
.compile_custom_pixel_shader(shader, &[UniformName::new("radius", UniformType::_1f)])
|
||||
.unwrap();
|
||||
for radius in [8.0f32, 16.0, 24.0] {
|
||||
let mut samples = Vec::new();
|
||||
for i in 0..110 {
|
||||
let start = std::time::Instant::now();
|
||||
let mut target = renderer.bind(&mut texture).unwrap();
|
||||
let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap();
|
||||
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
|
||||
frame
|
||||
.render_pixel_shader_to(
|
||||
&program,
|
||||
Rectangle::from_size((1024.0, 768.0).into()),
|
||||
rect,
|
||||
(1024, 768).into(),
|
||||
Some(&[rect]),
|
||||
1.0,
|
||||
&[Uniform::new("radius", radius)],
|
||||
)
|
||||
.unwrap();
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
if i >= 10 {
|
||||
samples.push(start.elapsed());
|
||||
}
|
||||
}
|
||||
samples.sort_unstable();
|
||||
eprintln!(
|
||||
"corner-heavy {name} radius={radius}: median={:?}, p95={:?}",
|
||||
samples[50], samples[95]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ pub(super) fn rounded_clip_program(
|
||||
resources: &mut RenderResources,
|
||||
renderer: &mut GlesRenderer,
|
||||
) -> Option<RoundedClipPrograms> {
|
||||
corner_program(resources, renderer, CornerShape::Circular)
|
||||
corner_program(resources, renderer, CornerShape::Continuous)
|
||||
}
|
||||
|
||||
pub(super) fn corner_program(
|
||||
|
||||
@@ -428,7 +428,7 @@ pub(super) fn material_program(
|
||||
resources: &mut RenderResources,
|
||||
renderer: &mut GlesRenderer,
|
||||
) -> Option<MaterialProgram> {
|
||||
material_program_for_corners(resources, renderer, CornerShape::Circular)
|
||||
material_program_for_corners(resources, renderer, CornerShape::Continuous)
|
||||
}
|
||||
|
||||
pub(super) fn material_program_for_corners(
|
||||
|
||||
@@ -119,7 +119,7 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag");
|
||||
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
||||
|
||||
fn corner_shader(source: &str) -> String {
|
||||
corner_shader_for(source, CornerShape::Circular)
|
||||
corner_shader_for(source, CornerShape::Continuous)
|
||||
}
|
||||
|
||||
fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||
@@ -137,21 +137,9 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn window_corner_shape(window: &smithay::desktop::Window) -> CornerShape {
|
||||
// Semantic shell surfaces keep their client-matching circular outlines.
|
||||
corner_shape_for_role(
|
||||
window
|
||||
.toplevel()
|
||||
.and_then(|toplevel| crate::effects::surface_role(toplevel.wl_surface()).map(|(role, _)| role)),
|
||||
)
|
||||
}
|
||||
|
||||
fn corner_shape_for_role(role: Option<crate::effects::SemanticRole>) -> CornerShape {
|
||||
if role.is_some() {
|
||||
CornerShape::Circular
|
||||
} else {
|
||||
CornerShape::Continuous
|
||||
}
|
||||
pub(crate) fn window_corner_shape(_window: &smithay::desktop::Window) -> CornerShape {
|
||||
// Shell surfaces and application windows use the same shared squircle profile.
|
||||
CornerShape::Continuous
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -15,6 +15,9 @@ void main() {
|
||||
float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius);
|
||||
float coverage = edge_coverage(sdf);
|
||||
if (paint_mode > 0.5) {
|
||||
// The shadow pass is multiplied by (1 - coverage). Fully covered
|
||||
// pixels need neither a second contour solve nor a Gaussian sample.
|
||||
if (coverage >= 1.0) { gl_FragColor = vec4(0.0); return; }
|
||||
float distance = max(rounded_rect_distance(gl_FragCoord.xy, shadow_rect, material_radius), 0.0);
|
||||
float sigma = max(shadow_values.x * 0.5, 0.5);
|
||||
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
||||
|
||||
@@ -26,7 +26,7 @@ void main() {
|
||||
|
||||
#ifdef CONTINUOUS_WINDOW_CORNERS
|
||||
// Inset the outer distance field along its normals, rather than shrinking
|
||||
// the quartic's bounding box/radius and changing border thickness.
|
||||
// the profile's bounding box/radius and changing border thickness.
|
||||
float inner_coverage = edge_coverage(signed_distance + border_width);
|
||||
#else
|
||||
vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0));
|
||||
|
||||
@@ -44,7 +44,11 @@ float corner_nearest(vec2 query, float t, vec2 a, vec2 b, vec2 c, vec2 d) {
|
||||
vec2 error = point - query;
|
||||
float speed_squared = dot(tangent, tangent);
|
||||
float denominator = max(speed_squared + dot(error, acceleration), speed_squared * 0.25);
|
||||
t = clamp(t - clamp(dot(error, tangent) / denominator, -0.25, 0.25), 0.0, 1.0);
|
||||
float next = clamp(t - clamp(dot(error, tangent) / denominator, -0.25, 0.25), 0.0, 1.0);
|
||||
if (next == t) {
|
||||
break;
|
||||
}
|
||||
t = next;
|
||||
}
|
||||
|
||||
return t;
|
||||
@@ -71,13 +75,23 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
|
||||
return -min(inset.x, inset.y);
|
||||
}
|
||||
|
||||
vec2 query = inset / radius;
|
||||
// Reflect onto the first half of the symmetric corner. The opposite
|
||||
// shoulder cannot be nearer than its reflection, so omit segment 2.
|
||||
vec2 query = vec2(min(inset.x, inset.y), max(inset.x, inset.y)) / radius;
|
||||
float best_squared = 1.0e20;
|
||||
float side = 1.0;
|
||||
|
||||
for (int segment = 0; segment < 3; segment++) {
|
||||
for (int segment = 0; segment < 2; segment++) {
|
||||
vec2 a, b, c, d;
|
||||
corner_controls(segment, blend, a, b, c, d);
|
||||
// The cubic stays inside its control hull. Reject only when even
|
||||
// that hull's box cannot improve the closest point found so far.
|
||||
vec2 low = min(min(a, b), min(c, d));
|
||||
vec2 high = max(max(a, b), max(c, d));
|
||||
vec2 delta = max(max(low - query, query - high), 0.0);
|
||||
if (dot(delta, delta) > best_squared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int seed = 0; seed < 2; seed++) {
|
||||
float t = corner_nearest(query, float(seed), a, b, c, d);
|
||||
|
||||
Reference in new issue
Block a user