git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit0235e2f94deedb56c937dd0ac38783442d5e9bf8
parenta594a77919
authorLucas Galante <[email protected]>
date2026-08-28 12:41
feat(bevel): focus_sharpness as a config knob

The glint's falloff exponent was a #define, so trying a value meant a
rebuild — and it is a look setting, which is exactly the kind nobody
tunes if tuning costs a compile. Plumb it through the way focus_color
already goes: shader uniform, bevel_shader location, fx_render_bevel
option, scene-node field on the set_focus setter, LayoutConfig, and the
`bevel` node in config.kdl.

    bevel ... focus_sharpness=(f64)3.0

Default 3.0, so absent config is exactly what shipped in a594a77.
Clamped to [1, 64]: below 1 the glint would spread wider than the rim's
own slope, which is what `thickness` is for.

Only the focus branch reads the uniform, so the desktop-grid bevels in
output.rs — which never call set_focus — are unaffected, and their
change-detection key does not need it.

Verified against the hardcoded builds from the sweep: default, 1.0 and
12.0 each reproduce their pinned-constant capture with zero differing
pixels.

Co-Authored-By: Claude Opus 5 <[email protected]>

 scenefx/include/render/fx_renderer/shaders.h  |  1 +
 scenefx/include/scenefx/render/pass.h         |  2 ++
 scenefx/include/scenefx/types/wlr_scene.h     |  4 +++-
 scenefx/render/fx_renderer/fx_pass.c          |  1 +
 scenefx/render/fx_renderer/shaders.c          |  1 +
 scenefx/render/fx_renderer/shaders/bevel.frag |  9 +++++----
 scenefx/types/scene/wlr_scene.c               |  6 ++++--
 src/server/config.rs                          | 22 ++++++++++++++++++++++
 src/server/window.rs                          |  1 +
 9 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index ac77b75..10f28e8 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -177,6 +177,7 @@ struct bevel_shader {
 	GLint shoulder;
 	GLint focus;
 	GLint focus_color;
+	GLint focus_sharpness;
 };
 
 bool link_bevel_program(struct bevel_shader *shader);
diff --git a/scenefx/include/scenefx/render/pass.h b/scenefx/include/scenefx/render/pass.h
index eb928c9..8914105 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -101,6 +101,8 @@ struct fx_render_bevel_options {
 	 * in focus_color (the DE's focused-plate glint). */
 	float focus;
 	float focus_color[3];
+	/* Falloff exponent of the focus glint across the rim; 1 = linear. */
+	float focus_sharpness;
 	/* Tint of the highlight; alpha scales the whole effect. */
 	struct wlr_render_color color;
 };
diff --git a/scenefx/include/scenefx/types/wlr_scene.h b/scenefx/include/scenefx/types/wlr_scene.h
index c9161e7..9239a8d 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -212,6 +212,8 @@ struct wlr_scene_bevel {
 	 */
 	float focus;
 	float focus_color[3];
+	/** Falloff exponent of the focus glint across the rim; 1 = linear. */
+	float focus_sharpness;
 };
 
 struct wlr_scene_blur {
@@ -669,7 +671,7 @@ void wlr_scene_droplet_set_silhouette(struct wlr_scene_droplet *droplet,
 void wlr_scene_droplet_set_lens(struct wlr_scene_droplet *droplet,
 		float band_px, float refr, float ghost);
 void wlr_scene_bevel_set_color(struct wlr_scene_bevel *bevel, const float color[static 4]);
-void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus,
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus, float sharpness,
 	const float color[static 3]);
 
 /**
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 8f943ff..0662dbf 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1162,6 +1162,7 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
 	glUniform1f(renderer->shaders.bevel.focus, options->focus);
 	glUniform3f(renderer->shaders.bevel.focus_color,
 			options->focus_color[0], options->focus_color[1], options->focus_color[2]);
+	glUniform1f(renderer->shaders.bevel.focus_sharpness, options->focus_sharpness);
 
 	render(&box, &clip_region, renderer->shaders.bevel.pos_attrib);
 	pixman_region32_fini(&clip_region);
diff --git a/scenefx/render/fx_renderer/shaders.c b/scenefx/render/fx_renderer/shaders.c
index cd2e6ef..6af59fc 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -369,6 +369,7 @@ bool link_bevel_program(struct bevel_shader *shader) {
 	shader->shade_intensity = glGetUniformLocation(prog, "shade_intensity");
 	shader->focus = glGetUniformLocation(prog, "focus");
 	shader->focus_color = glGetUniformLocation(prog, "focus_color");
+	shader->focus_sharpness = glGetUniformLocation(prog, "focus_sharpness");
 	shader->shoulder = glGetUniformLocation(prog, "shoulder");
 
 	return true;
diff --git a/scenefx/render/fx_renderer/shaders/bevel.frag b/scenefx/render/fx_renderer/shaders/bevel.frag
index 8d15091..95c57e1 100644
--- a/scenefx/render/fx_renderer/shaders/bevel.frag
+++ b/scenefx/render/fx_renderer/shaders/bevel.frag
@@ -42,9 +42,10 @@ uniform vec3 focus_color;
 // inside t=0 and is feathered by edge_aa, so it is already below 1.0 and the
 // power pushes it down (measured peak, green channel: 242 at 1.0, 222 at 3.0,
 // 198 at 6.0, 168 at 12.0). Past ~12 the curve stops narrowing and only dims:
-// 12 and 24 differ by 2/255. 3.0 keeps a defined edge without washing the
-// accent out to white.
-#define FOCUS_SHARPNESS 3.0
+// 12 and 24 differ by 2/255. The DE default is 3.0 — a defined edge without
+// washing the accent out to white — set by `bevel focus_sharpness=` in
+// config.kdl. Only the focus branch reads it.
+uniform float focus_sharpness;
 
 // 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
@@ -114,7 +115,7 @@ void main() {
         // power keeps the peak at the silhouette and falls away fast, which
         // is what makes cce-ui's plate line read as a glint rather than a
         // glow (shader2d.wgsl raises its profile to `shininess`).
-        float h = light_intensity * pow(slope, FOCUS_SHARPNESS);
+        float h = light_intensity * pow(slope, max(focus_sharpness, 0.01));
         // ADDITIVE: alpha 0 with premultiplied colour gives dst + rgb under
         // the pass's GL_ONE/GL_ONE_MINUS_SRC_ALPHA blend, so the glint ADDS
         // light to the window instead of cross-fading toward the accent.
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 520ad96..7832189 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -1263,13 +1263,14 @@ void wlr_scene_bevel_set_color(struct wlr_scene_bevel *bevel, const float color[
 	scene_node_update(&bevel->node, NULL);
 }
 
-void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus,
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus, float sharpness,
 		const float color[static 3]) {
-	if (bevel->focus == focus &&
+	if (bevel->focus == focus && bevel->focus_sharpness == sharpness &&
 			memcmp(bevel->focus_color, color, sizeof(bevel->focus_color)) == 0) {
 		return;
 	}
 	bevel->focus = focus;
+	bevel->focus_sharpness = sharpness;
 	memcpy(bevel->focus_color, color, sizeof(bevel->focus_color));
 	scene_node_update(&bevel->node, NULL);
 }
@@ -2543,6 +2544,7 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 				scene_bevel->focus_color[1],
 				scene_bevel->focus_color[2],
 			},
+			.focus_sharpness = scene_bevel->focus_sharpness,
 			.color = {
 				.r = scene_bevel->color[0],
 				.g = scene_bevel->color[1],
diff --git a/src/server/config.rs b/src/server/config.rs
index ae075d3..f5c76c7 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -92,6 +92,12 @@ pub struct Layout {
     /// Focused-window rim accent: the bevel highlight wraps all four sides
     /// in this color for the focused window (the DE focus glint).
     pub bevel_focus_color: [f32; 3],
+    /// How sharply the focused rim's glint falls off across the bevel:
+    /// the exponent on the rim slope. 1 is a linear ramp over the whole
+    /// thickness (reads as a wash); higher concentrates the light at the
+    /// silhouette, at some cost in peak brightness, since the outermost
+    /// rendered sample is already below 1.0. Past ~12 it only dims.
+    pub bevel_focus_sharpness: f32,
     /// Magnetic grid snap for interactive move/resize.
     pub desktop_snap: bool,
     /// Speed ramp + duration (ms) for the overview enter/exit transition.
@@ -214,6 +220,7 @@ impl Default for Layout {
             bevel_shoulder: 0.55,
             bevel_color: [1.0, 1.0, 1.0, 1.0],
             bevel_focus_color: [0.35, 0.78, 0.78],
+            bevel_focus_sharpness: 3.0,
             shadow_enabled: true,
             shadow_sigma: 22.0,
             shadow_color: [0.0, 0.0, 0.0, 0.55],
@@ -501,6 +508,8 @@ pub struct SurfaceConfig {
     pub bevel_color: String,
     #[serde(default = "default_bevel_focus_color")]
     pub bevel_focus_color: String,
+    #[serde(default = "default_bevel_focus_sharpness")]
+    pub bevel_focus_sharpness: f64,
 }
 
 fn default_shadow_enabled() -> bool { true }
@@ -517,6 +526,7 @@ fn default_bevel_shade_intensity() -> f64 { 0.5 }
 fn default_bevel_shoulder() -> f64 { 0.55 }
 fn default_bevel_color() -> String { "#ffffffff".to_string() }
 fn default_bevel_focus_color() -> String { "#59c7c7".to_string() }
+fn default_bevel_focus_sharpness() -> f64 { 3.0 }
 
 /// Map a compass point to a unit vector pointing TOWARD the light, in screen
 /// space (y down). Anything unrecognized keeps the DE's top-left default.
@@ -577,6 +587,7 @@ impl Default for SurfaceConfig {
             bevel_shoulder: default_bevel_shoulder(),
             bevel_color: default_bevel_color(),
             bevel_focus_color: default_bevel_focus_color(),
+            bevel_focus_sharpness: default_bevel_focus_sharpness(),
         }
      }
 }
@@ -2045,6 +2056,13 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
                                             surface.bevel_color = val.to_string();
                                         }
                                     }
+                                    "focus_sharpness" => {
+                                        if let Some(val) = entry.value().as_f64() {
+                                            surface.bevel_focus_sharpness = val;
+                                        } else if let Some(val) = entry.value().as_i64() {
+                                            surface.bevel_focus_sharpness = val as f64;
+                                        }
+                                    }
                                     "focus_color" => {
                                         if let Some(val) = entry.value().as_string() {
                                             surface.bevel_focus_color = val.to_string();
@@ -2359,6 +2377,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
     state.layout.bevel_color = parse_hex_color_rgba(&config.surface.bevel_color);
     let fc = parse_hex_color_rgba(&config.surface.bevel_focus_color);
     state.layout.bevel_focus_color = [fc[0], fc[1], fc[2]];
+    // Clamped low at 1: below that the glint would spread WIDER than the
+    // rim's own slope, which is what `thickness` is for.
+    state.layout.bevel_focus_sharpness =
+        config.surface.bevel_focus_sharpness.clamp(1.0, 64.0) as f32;
 
     for (key, val) in &config.env {
         let expanded = expand_env_vars(val);
diff --git a/src/server/window.rs b/src/server/window.rs
index 2f48b95..6e39c21 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3255,6 +3255,7 @@ impl Window {
         ffi::wlr_scene_bevel_set_focus(
             self.bevel,
             if focused { 1.0 } else { 0.0 },
+            layout.bevel_focus_sharpness,
             layout.bevel_focus_color.as_ptr(),
         );
         ffi::river_scene_node_set_position_if_changed(node, 0, 0);