Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix: the bevel rim traced a circle where the corner is a superellipse
corner_shape > 2 makes every corner in the DE a squircle, and
widen_corner_radius compensates by handing the shaders a span-WIDENED
radius (at corner_shape 4.5 the configured 12 goes out as 35) so the
superellipse's diagonal curvature lands on the corner the clients draw.
Every other corner cut reads that radius through corner_alpha.frag's
get_dist, which knows about the exponent.
bevel.frag did not. link_bevel_program linked the shader on its own —
unlike link_box_shadow_program, which concatenates corner_alpha_frag_src —
and the shader carried a private circular rounded_rect_sdf. So it took the
widened 35 literally and drew an arc of radius 35 where the real corner
reads as ~17: measured on a scale-2 output, a 68px bevel arc against a
34px window corner. The rim peeled away from the edge as it came round,
15 logical px adrift at the diagonal, only rejoining well down the
straight run.
The rim now uses the shared corner_dist, so its boundary is the same
curve the surface clip, blur and shadow use. The lighting gradient is
central-differenced through that same SDF, which is what the effect
wanted in the first place — the highlight sweeps around the actual
squircle instead of a circle that merely touches it. Because the rect is
symmetric with one radius on all four corners, corner_dist's internal y
flip cancels and shifting the position uniform is a plain translation of
the sample point, so the differences stay in the space light_dir is
authored in. The discard moved above the gradient so the four extra SDF
evaluations only run on the thin band that shades.
Measured in a nested instance at corner_shape 4.5: rim-to-edge inset mean
0.4px, max 3px (antialiasing), down from ~15 logical px; top-left still
lit +18, bottom-right still shaded, so the light direction is unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
scenefx/include/render/fx_renderer/shaders.h | 3 ++
scenefx/render/fx_renderer/fx_pass.c | 3 ++
scenefx/render/fx_renderer/shaders.c | 9 +++++-
scenefx/render/fx_renderer/shaders/bevel.frag | 40 +++++++++++++++++----------
4 files changed, 39 insertions(+), 16 deletions(-)
diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index 1353ab4..3534042 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -167,6 +167,9 @@ struct bevel_shader {
GLint position;
GLint size;
GLint corner_radius;
+ // The shared corner-shape exponent (see corner_alpha.frag) — the rim has
+ // to trace the same superellipse every other corner cut does.
+ GLint corner_shape;
GLint thickness;
GLint light_dir;
GLint light_intensity;
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 740db98..f1a8738 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1146,6 +1146,9 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
glUniform2f(renderer->shaders.bevel.size, box.width, box.height);
glUniform2f(renderer->shaders.bevel.position, box.x, box.y);
glUniform1f(renderer->shaders.bevel.corner_radius, options->corner_radius);
+ // The radius the compositor sends is span-widened for this exponent, so
+ // the rim has to read it as the same superellipse the other cuts do.
+ glUniform1f(renderer->shaders.bevel.corner_shape, fx_corner_shape());
glUniform1f(renderer->shaders.bevel.thickness, options->thickness);
glUniform2f(renderer->shaders.bevel.light_dir,
options->light_dir[0], options->light_dir[1]);
diff --git a/scenefx/render/fx_renderer/shaders.c b/scenefx/render/fx_renderer/shaders.c
index 05cbcd9..54f1c4d 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -335,8 +335,14 @@ bool link_box_shadow_program(struct box_shadow_shader *shader) {
}
bool link_bevel_program(struct bevel_shader *shader) {
+ // Same pairing as link_box_shadow_program: the rim's SDF is the shared
+ // corner routine, so the shape follows corner_shape like every other cut.
+ GLchar bevel_src[8192];
+ snprintf(bevel_src, sizeof(bevel_src), "%s\n%s", bevel_frag_src,
+ corner_alpha_frag_src);
+
GLuint prog;
- shader->program = prog = link_program(bevel_frag_src);
+ shader->program = prog = link_program(bevel_src);
if (!shader->program) {
return false;
}
@@ -346,6 +352,7 @@ bool link_bevel_program(struct bevel_shader *shader) {
shader->position = glGetUniformLocation(prog, "position");
shader->size = glGetUniformLocation(prog, "size");
shader->corner_radius = glGetUniformLocation(prog, "corner_radius");
+ shader->corner_shape = glGetUniformLocation(prog, "corner_shape");
shader->thickness = glGetUniformLocation(prog, "thickness");
shader->light_dir = glGetUniformLocation(prog, "light_dir");
shader->light_intensity = glGetUniformLocation(prog, "light_intensity");
diff --git a/scenefx/render/fx_renderer/shaders/bevel.frag b/scenefx/render/fx_renderer/shaders/bevel.frag
index 9a1890b..6ded18b 100644
--- a/scenefx/render/fx_renderer/shaders/bevel.frag
+++ b/scenefx/render/fx_renderer/shaders/bevel.frag
@@ -30,20 +30,30 @@ uniform float shade_intensity;
// 0 = a hard flat chamfer, 1 = fully rounded shoulder.
uniform float shoulder;
-// Signed distance to a rounded rect; negative inside.
-float rounded_rect_sdf(vec2 p, vec2 half_size, float radius) {
- vec2 q = abs(p) - half_size + radius;
- return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
+// Defined in corner_alpha.frag, which is concatenated after this source (the
+// same way box_shadow.frag gets it). Using the shared routine rather than a
+// private circular SDF is the whole point: at corner_shape > 2 the DE's
+// corners are superellipses and the compositor hands us the span-WIDENED
+// radius that shape needs. Read as a circle, that radius drew an arc twice
+// the size of the real corner and the rim peeled ~15px off the edge at the
+// diagonal.
+float corner_dist(vec2 size, vec2 position,
+ float radius_tl, float radius_tr, float radius_bl, float radius_br);
+
+// The rounded rect is symmetric in both axes here (one radius on all four
+// corners), so corner_dist's internal y flip cancels and offsetting the
+// `position` uniform by d is just a translation of the sample point by -d.
+float bevel_dist(vec2 offset) {
+ return corner_dist(size, position + offset,
+ corner_radius, corner_radius, corner_radius, corner_radius);
}
void main() {
- vec2 half_size = size * 0.5;
- vec2 center = position + half_size;
- vec2 p = gl_FragCoord.xy - center;
+ float dist = bevel_dist(vec2(0.0));
- float dist = rounded_rect_sdf(p, half_size, corner_radius);
-
- // Outside the rect, or deeper in than the rim: nothing to draw.
+ // Outside the rect, or deeper in than the rim: nothing to draw. Discard
+ // before the gradient so the four extra SDF evaluations only run on the
+ // thin band that actually shades.
float rim = max(thickness, 1.0);
if (dist > 0.0 || dist < -rim) {
discard;
@@ -54,12 +64,12 @@ void main() {
// The surface normal of the chamfer. Its in-plane part points OUT of the
// rect (the gradient of the SDF), and its steepness falls off across the
- // rim — steep at the edge, flat where it meets the face.
+ // rim — steep at the edge, flat where it meets the face. Sampling at
+ // -offset means the differences below are already in the same space the
+ // light direction is authored in.
vec2 grad = normalize(vec2(
- rounded_rect_sdf(p + vec2(1.0, 0.0), half_size, corner_radius) -
- rounded_rect_sdf(p - vec2(1.0, 0.0), half_size, corner_radius),
- rounded_rect_sdf(p + vec2(0.0, 1.0), half_size, corner_radius) -
- rounded_rect_sdf(p - vec2(0.0, 1.0), half_size, corner_radius)
+ bevel_dist(vec2(-1.0, 0.0)) - bevel_dist(vec2(1.0, 0.0)),
+ bevel_dist(vec2(0.0, -1.0)) - bevel_dist(vec2(0.0, 1.0))
) + vec2(1e-6));
// Slope profile across the rim. Mixing linear and smoothstep gives the