solium_effects/fragment.rs
1//! What an effect *reads*, which is what decides whether a frame needs a pass.
2//!
3//! Most effects draw over what is already there and need nothing: a wavy
4//! border, a glow, spikes. They are ordinary elements and this module has
5//! nothing to say about them. Rounded corners need the node's own pixels, but
6//! only where each of its surfaces already is, so they are drawn inline: each
7//! surface through a program of its own ([`CLIPPED_SURFACE`]), with no capture
8//! (`tests::rounding_needs_no_capture`). An effect that needs the node's
9//! pixels all at once -- a shadow derived from their silhouette -- cannot be
10//! one element in a flat list, because a flat list has nowhere to say "after
11//! the things below me, before the things above me".
12//!
13//! So `inputs` is the declaration, and the renderer reads it. This crate holds
14//! the declaration and the shader text; `crates/solium/src/pass.rs` is what
15//! compiles and runs it.
16
17/// What an effect needs before it can draw.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum Inputs {
20 /// Draws over what is there. No pass, no capture, no cost.
21 Nothing,
22 /// Drawn through a program surface by surface, in each surface's own
23 /// place: no capture. [`CLIPPED_SURFACE`] and [`CLIPPED_SOLID`] are such
24 /// programs, for rounding. A renderer can run it: `solium::pass`'s
25 /// `an_effect_drawn_inline_can_be_run`.
26 Inline,
27 /// The node's own pixels, rendered to a texture first.
28 SelfTexture,
29 /// What is already composited *beneath* the node.
30 ///
31 /// Declared here and implemented nowhere. It is what blur needs, and
32 /// naming it is most of why this enum exists -- an effect system whose
33 /// vocabulary cannot express blur has decided blur is impossible rather
34 /// than unimplemented.
35 ///
36 /// Nothing constructs it yet. Whatever plans the passes must refuse it out
37 /// loud rather than let it fall through to drawing inline: a blur that
38 /// silently renders as no blur looks like a style that failed to load, and
39 /// nobody reports that as a compositor bug.
40 Backdrop,
41}
42
43/// The four corner radii a rounded-corner program takes, in **physical**
44/// pixels, packed `(top_left, top_right, bottom_left, bottom_right)`.
45///
46/// [`Effect::radii`] is in **logical** pixels, and this is the other side of
47/// that seam. A style says `radius: 12` because that is what someone types
48/// into a `Pane.qml`, and a style should not have to know a monitor's scale;
49/// the shader measures in texture pixels throughout and knows nothing else.
50///
51/// **Whoever sets this uniform does the multiply by the output scale, on all
52/// four** -- `crates/solium/src/pass.rs`, not this crate and not the shader.
53/// The two numbers are equal on a scale-1 output, which is why getting it
54/// wrong looks perfect on the machine it was written on and shows up only on
55/// a HiDPI screen.
56pub const RADIUS_UNIFORM: &str = "corner_radius";
57
58/// The texture's size in physical pixels.
59///
60/// Ours, not Smithay's. A *pixel* program is given a `size` uniform; a
61/// *texture* program is not -- the built-in `texture.frag` declares only
62/// `tex`, `alpha` and `v_coords`. So a texture shader that needs to measure in
63/// pixels has to be told how big it is.
64pub const SIZE_UNIFORM: &str = "tex_size";
65
66/// Rounded corners, as a fragment program over the node's own texture.
67///
68/// Smithay's contract for a TEXTURE program, read from `shaders/mod.rs`
69/// rather than from the doc comment on `compile_custom_texture_shader`, which
70/// is wrong about the first of these:
71///
72/// * the source must contain a line that is exactly `//_DEFINES_` -- with the
73/// trailing underscore; the doc comment omits it;
74/// * the source supplies its own `#version 100`. `texture_program` does not
75/// prepend one, and the built-in `texture.frag` carries its own. (The
76/// *pixel* program is the one where smithay prepends it.)
77/// * it is compiled **six times**, not once, and has to behave under each set
78/// of `#define`s. Three define sets -- `&[]`, `&[NO_ALPHA]`, `&[EXTERNAL]`
79/// -- and `texture_program` links each of them *twice*, once plain and once
80/// with `DEBUG_FLAGS` chained on (`shaders/mod.rs:215-217` builds the three; the two `link_program` calls at `:142-143` are what double them). Three is the
81/// number of variants `variant_for_format` picks between; six is the number
82/// that have to compile, and a shader that only builds without the debug
83/// define fails at renderer construction and takes the session with it. See
84/// `the_shader_handles_every_variant_smithay_compiles_it_into`, and
85/// `texture.frag`, which is the model this mirrors.
86///
87/// `alpha` is Smithay's; `corner_radius` and `tex_size` are ours. A texture
88/// program gets no `size` uniform -- only a pixel program does.
89/// The distance field is the standard rounded-box one: pick a radius for the
90/// fragment's quadrant, fold the coordinate into that quadrant, and measure
91/// from the centre of that corner's circle.
92pub const ROUNDED_CORNERS: &str = r"#version 100
93
94//_DEFINES_
95
96#if defined(EXTERNAL)
97#extension GL_OES_EGL_image_external : require
98#endif
99
100// highp where the hardware has it, which is not a nicety here.
101//
102// GLES 2 guarantees mediump only 10 mantissa bits. This shader multiplies a
103// normalised coordinate back up into pixels -- `v_coords * tex_size` -- so one
104// ULP at 3840px is about 3.75px, seven times the width of the smoothstep that
105// antialiases the arc. The corner would wobble and its edge come apart, on a
106// 4K screen only.
107//
108// Mesa evaluates mediump at fp32, so none of that is visible on the machine
109// this was written on. Same shape as the logical-versus-physical radius above:
110// a defect whose only symptom is on hardware the author does not have.
111// `GL_FRAGMENT_PRECISION_HIGH` is the compiler's own answer to whether highp
112// exists in a fragment shader, so this asks rather than assumes.
113//
114// smithay's `texture.frag` settles for mediump and is not a precedent: it
115// samples at `v_coords` and never scales a normalised coordinate back into
116// pixel space, so it has no bits to lose.
117#ifdef GL_FRAGMENT_PRECISION_HIGH
118precision highp float;
119#else
120precision mediump float;
121#endif
122#if defined(EXTERNAL)
123uniform samplerExternalOES tex;
124#else
125uniform sampler2D tex;
126#endif
127
128uniform float alpha;
129uniform vec4 corner_radius;
130uniform vec2 tex_size;
131varying vec2 v_coords;
132
133#if defined(DEBUG_FLAGS)
134uniform float tint;
135#endif
136
137void main() {
138 vec4 colour = texture2D(tex, v_coords);
139
140#if defined(NO_ALPHA)
141 // Forced opaque rather than sampled. The X byte of an XRGB8888 buffer is
142 // undefined, so a client that leaves it at zero draws a fully invisible
143 // window if it is multiplied in -- and XRGB8888 is the ordinary opaque
144 // case, not an exotic one. Smithay picks this variant itself, from the
145 // buffer's format.
146 colour = vec4(colour.rgb, 1.0) * alpha;
147#else
148 // Every channel, not just alpha: a wayland surface is PREMULTIPLIED, so
149 // colour and alpha have to be scaled together or a faded edge comes out
150 // too bright. Smithay's own texture.frag does `color * alpha` for the
151 // same reason.
152 colour = colour * alpha;
153#endif
154
155#if defined(DEBUG_FLAGS)
156 if (tint == 1.0)
157 colour = vec4(0.0, 0.2, 0.0, 0.2) + colour * 0.8;
158#endif
159
160 // Into pixels, then into one corner's quadrant: abs() folds all four
161 // corners onto one, so the distance field is written once rather than
162 // four times. `v_coords` is `(tex_matrix * position).xy`, which for a
163 // whole texture runs 0..1 -- see smithay's texture.vert.
164 //
165 // Clamped to the shorter half, and the field is written in terms of the
166 // clamp. Unclamped, a radius past min(w,h)/2 pushes each corner circle's
167 // centre outside the rectangle and the field calls every fragment of the
168 // short edge outside: a 300x200 window at radius 150 loses its whole top
169 // and bottom rows instead of degrading to a stadium. Nothing upstream
170 // clamps -- Effect::rounded(Corners::all(1e9)) is accepted and is not a
171 // none-effect.
172 vec2 half_size = tex_size * 0.5;
173
174 // Which corner this fragment belongs to, decided on the UNFOLDED
175 // coordinate. `abs()` below makes all four look like the top-left, which
176 // is what lets one expression draw four corners -- and is exactly why the
177 // radius has to be chosen first. `corner_radius` is (tl, tr, bl, br).
178 float picked = (v_coords.x < 0.5)
179 ? ((v_coords.y < 0.5) ? corner_radius.x : corner_radius.z)
180 : ((v_coords.y < 0.5) ? corner_radius.y : corner_radius.w);
181 float r = min(picked, min(half_size.x, half_size.y));
182 vec2 p = abs(v_coords * tex_size - half_size) - (half_size - vec2(r));
183
184 // The exact rounded-box distance, **interior term and all**.
185 //
186 // `length(max(p, 0.0)) - r` on its own is only the OUTSIDE half of the
187 // field. It is right on the arcs and right past every edge, and inside the
188 // shape it saturates: both components of `p` are negative there, so it
189 // reports a flat `-r` at every interior fragment instead of that
190 // fragment's real distance to the edge. While a window had one radius that
191 // was merely imprecise -- anything at `r >= 0.5` is past the smoothstep's
192 // far end and comes out fully opaque either way, which is why half a pixel
193 // was the bound everything downstream was written against.
194 //
195 // With a radius per quadrant it stops being imprecise and starts being
196 // wrong, because `r` can now be 0 on one corner while the others round. A
197 // zero radius makes the interior report exactly 0 -- the smoothstep's
198 // MIDPOINT -- so that quadrant, a quarter of the window, is drawn at 50%
199 // alpha rather than squared off. Measured on a real GPU through
200 // `dev/wirecheck`: `corner_radius = (0, 16, 16, 16)` came back at alpha
201 // 127 across every pixel of the top-left quadrant, and at 228 for a radius
202 // of 0.3. A square-topped window is the headline case for per-corner
203 // radii, not an exotic one -- `panes/flush/` is exactly that shape.
204 //
205 // `min(max(p.x, p.y), 0.0)` is that missing term, and adding it changes
206 // nothing anywhere else: it is exactly 0.0 wherever either component of
207 // `p` is positive, which is every fragment on an arc, along a straight
208 // edge, or outside the shape. Checked and not merely argued -- all four
209 // quadrants of a 64x64 at r=16 hash byte-identical across the change.
210 //
211 // It also settles where the shape's own boundary lands. A fragment centre
212 // sits half a pixel inside the edge, so the outermost row now reports
213 // exactly -0.5 for any `r` -- the smoothstep's near end, fully opaque --
214 // which is what lets `clip::cut_corners` claim a square corner's pixels
215 // and every row of an uncut side.
216 float away = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r;
217
218 // The mask goes last, after the tint. The tint ADDS a constant, so a
219 // fragment masked to zero before it would be painted back in and the cut
220 // corners would reappear in green whenever debug flags are on.
221 //
222 // The one-unit smoothstep is not a one-*pixel* antialias: `away` is in
223 // texture pixels, so the softened band is one screen pixel wide only when
224 // the texture is drawn at 1:1. Magnified it is a visible blur, minified it
225 // aliases. The scale-correct form is smoothstep(-w, w, away) with
226 // w = fwidth(away), which under #version 100 needs
227 // GL_OES_standard_derivatives; until that extension is requested, this
228 // buys a soft edge at native scale and a wrong-width one everywhere else.
229 gl_FragColor = colour * (1.0 - smoothstep(-0.5, 0.5, away));
230}
231";
232
233/// The 3x3 matrix taking a surface's texture coordinate to the client's own
234/// physical pixels, column-major. Computed per draw by whoever draws the
235/// surface; wirecheck's case 11h sets it by hand.
236pub const INPUT_TO_GEO_UNIFORM: &str = "input_to_geo";
237/// The client's size in its own physical pixels.
238pub const GEO_SIZE_UNIFORM: &str = "geo_size";
239/// How many screen pixels one of the client's own is: the antialias band is
240/// one screen pixel wide at any zoom.
241pub const GEO_PX_UNIFORM: &str = "geo_px";
242/// A single-pixel buffer's colour, premultiplied.
243pub const COLOUR_UNIFORM: &str = "colour";
244
245/// Rounded corners for one client surface, drawn where it is: the surface's
246/// texture coordinate is taken into the client's rectangle through
247/// `input_to_geo`, and the distance field is `ROUNDED_CORNERS`' own, measured
248/// there. So a subsurface's corner that coincides with the client's is cut and
249/// its others are not, and the band is one screen pixel at any zoom
250/// (`geo_px`), which the old shader's own comment could not promise.
251/// `tests::the_clipped_program_is_shaped_the_way_smithay_requires`, and
252/// wirecheck's case 11h, which draws a root surface and a subsurface through it.
253pub const CLIPPED_SURFACE: &str = r"#version 100
254
255//_DEFINES_
256
257#if defined(EXTERNAL)
258#extension GL_OES_EGL_image_external : require
259#endif
260
261#ifdef GL_FRAGMENT_PRECISION_HIGH
262precision highp float;
263#else
264precision mediump float;
265#endif
266#if defined(EXTERNAL)
267uniform samplerExternalOES tex;
268#else
269uniform sampler2D tex;
270#endif
271
272uniform float alpha;
273uniform mat3 input_to_geo;
274uniform vec2 geo_size;
275uniform vec4 corner_radius;
276uniform float geo_px;
277varying vec2 v_coords;
278
279#if defined(DEBUG_FLAGS)
280uniform float tint;
281#endif
282
283void main() {
284 vec4 colour = texture2D(tex, v_coords);
285#if defined(NO_ALPHA)
286 colour = vec4(colour.rgb, 1.0) * alpha;
287#else
288 colour = colour * alpha;
289#endif
290#if defined(DEBUG_FLAGS)
291 if (tint == 1.0)
292 colour = vec4(0.0, 0.2, 0.0, 0.2) + colour * 0.8;
293#endif
294 // Into the client's own pixels, then ROUNDED_CORNERS' field, unchanged.
295 vec2 geo = (input_to_geo * vec3(v_coords, 1.0)).xy;
296 vec2 half_size = geo_size * 0.5;
297 float picked = (geo.x < half_size.x)
298 ? ((geo.y < half_size.y) ? corner_radius.x : corner_radius.z)
299 : ((geo.y < half_size.y) ? corner_radius.y : corner_radius.w);
300 float r = min(picked, min(half_size.x, half_size.y));
301 vec2 p = abs(geo - half_size) - (half_size - vec2(r));
302 float away = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r;
303 float band = 0.5 / max(geo_px, 0.0001);
304 gl_FragColor = colour * (1.0 - smoothstep(-band, band, away));
305}
306";
307
308/// The same, for a single-pixel buffer: smithay draws one as a solid colour,
309/// which no texture program reaches, so it is a pixel program over the
310/// surface's rectangle with the colour as a uniform. `v_coords` runs 0..1
311/// across that rectangle. Smithay prepends `#version 100` itself.
312/// `tests::the_solid_program_is_a_pixel_program`, and wirecheck's case 11h,
313/// which draws one.
314pub const CLIPPED_SOLID: &str = r"
315#ifdef GL_FRAGMENT_PRECISION_HIGH
316precision highp float;
317#else
318precision mediump float;
319#endif
320
321uniform float alpha;
322uniform vec2 size;
323uniform mat3 input_to_geo;
324uniform vec2 geo_size;
325uniform vec4 corner_radius;
326uniform float geo_px;
327uniform vec4 colour;
328varying vec2 v_coords;
329
330#if defined(DEBUG_FLAGS)
331uniform float tint;
332#endif
333
334void main() {
335 vec4 painted = colour * alpha;
336#if defined(DEBUG_FLAGS)
337 if (tint == 1.0)
338 painted = vec4(0.0, 0.2, 0.0, 0.2) + painted * 0.8;
339#endif
340 vec2 geo = (input_to_geo * vec3(v_coords, 1.0)).xy;
341 vec2 half_size = geo_size * 0.5;
342 float picked = (geo.x < half_size.x)
343 ? ((geo.y < half_size.y) ? corner_radius.x : corner_radius.z)
344 : ((geo.y < half_size.y) ? corner_radius.y : corner_radius.w);
345 float r = min(picked, min(half_size.x, half_size.y));
346 vec2 p = abs(geo - half_size) - (half_size - vec2(r));
347 float away = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r;
348 float band = 0.5 / max(geo_px, 0.0001);
349 gl_FragColor = painted * (1.0 - smoothstep(-band, band, away));
350}
351";
352
353/// Four corner radii, independently -- what lets a window be square-topped
354/// and round-bottomed, or any other combination, rather than one radius
355/// applied uniformly.
356///
357/// In **logical** pixels throughout, the same seam as [`Effect::radii`]: a
358/// style writes these, and does not know what scale the window will land on.
359#[derive(Clone, Copy, Debug, PartialEq)]
360pub struct Corners {
361 pub top_left: f64,
362 pub top_right: f64,
363 pub bottom_left: f64,
364 pub bottom_right: f64,
365}
366
367impl Corners {
368 /// One radius, all four corners. The common case, and the constructor
369 /// that used to be [`Effect::rounded`]'s whole job before a window was
370 /// allowed to differ corner to corner.
371 #[must_use]
372 pub const fn all(radius: f64) -> Self {
373 Self {
374 top_left: radius,
375 top_right: radius,
376 bottom_left: radius,
377 bottom_right: radius,
378 }
379 }
380
381 /// Whether every corner is `<= 0.0` or NaN.
382 ///
383 /// NaN is named rather than left to fall out of a negation, the same
384 /// spelling the single-radius [`Effect::is_none_effect`] used before this
385 /// existed: `!(r > 0.0)` says the same thing in fewer characters, but
386 /// clippy rejects a negated comparison on a partially ordered type -- and
387 /// its objection is the point: NaN is incomparable, so `r <= 0.0` on its
388 /// own would call it rounded and draw every surface through a program to
389 /// cut nothing.
390 #[must_use]
391 pub const fn is_none(&self) -> bool {
392 (self.top_left <= 0.0 || self.top_left.is_nan())
393 && (self.top_right <= 0.0 || self.top_right.is_nan())
394 && (self.bottom_left <= 0.0 || self.bottom_left.is_nan())
395 && (self.bottom_right <= 0.0 || self.bottom_right.is_nan())
396 }
397
398 /// Each side, measured by the larger of the two corners that touch it --
399 /// `(top, right, bottom, left)`. A square-topped, round-bottomed window
400 /// must not give up its top rows to a corner it does not have.
401 #[must_use]
402 pub fn max_of_side(&self) -> (f64, f64, f64, f64) {
403 let top = self.top_left.max(self.top_right);
404 let right = self.top_right.max(self.bottom_right);
405 let bottom = self.bottom_left.max(self.bottom_right);
406 let left = self.top_left.max(self.bottom_left);
407 (top, right, bottom, left)
408 }
409
410 /// The one number that stands for all four, for the `clientRadius` a layer
411 /// is still told. The *largest*, because that field's only in-tree use is
412 /// an outward hug (`PaneStyle.qml`: `clientRadius + 2`, "hug it from
413 /// outside"), and a hug has to clear the biggest cut or it clips into it.
414 #[must_use]
415 pub fn largest(&self) -> f64 {
416 self.top_left
417 .max(self.top_right)
418 .max(self.bottom_left)
419 .max(self.bottom_right)
420 }
421}
422
423/// One effect on one node.
424#[derive(Clone, Copy, Debug, PartialEq)]
425pub enum Effect {
426 /// Rounded corners, with a radius per corner, in logical pixels.
427 Rounded { radii: Corners },
428}
429
430impl Effect {
431 /// Rounded corners at `radii` **logical** pixels, one value per corner.
432 ///
433 /// Logical because this is the number a style writes, and a style does not
434 /// know what scale the window will land on. [`RADIUS_UNIFORM`] is where it
435 /// becomes physical, and says who multiplies.
436 ///
437 /// A single shared radius is the common case, and that convenience now
438 /// lives on [`Corners::all`] rather than here: `Effect::rounded(Corners::all(12.0))`
439 /// is the old one-number behaviour.
440 #[must_use]
441 pub const fn rounded(radii: Corners) -> Self {
442 Self::Rounded { radii }
443 }
444
445 /// What this effect needs before it can draw.
446 #[must_use]
447 pub const fn inputs(&self) -> Inputs {
448 match self {
449 Self::Rounded { .. } => Inputs::Inline,
450 }
451 }
452
453 /// The radii, in **logical** pixels, one per corner. [`RADIUS_UNIFORM`] is
454 /// the physical one, and the conversion between them is the caller's.
455 #[must_use]
456 pub const fn radii(&self) -> Corners {
457 match self {
458 Self::Rounded { radii } => *radii,
459 }
460 }
461
462 /// The one number [`Corners::largest`] gives this effect's radii -- see
463 /// there for why the largest. A one-line delegate rather than a second
464 /// copy of the max chain, so the reasoning has exactly one home.
465 #[must_use]
466 pub fn largest(&self) -> f64 {
467 self.radii().largest()
468 }
469
470 /// Whether this is an effect that should not be run at all.
471 ///
472 /// No corner rounded is not "rounded by nothing", it is *no effect*, and
473 /// the difference is a program on every surface of every window. Every
474 /// window on a machine with no styling declares one, so this is the arm
475 /// that keeps the ordinary case ordinary. See [`Corners::is_none`] for the
476 /// per-corner rule this now delegates to, NaN included.
477 #[must_use]
478 pub const fn is_none_effect(&self) -> bool {
479 match self {
480 Self::Rounded { radii } => radii.is_none(),
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 /// Whether the shader has this exact line, ignoring indentation.
490 ///
491 /// A whole line rather than `contains`, everywhere, because every name in
492 /// this shader is a substring of something else that is legitimately
493 /// present. `contains(RADIUS_UNIFORM)` passes for `RADIUS_UNIFORM =
494 /// "radius"`, since `corner_radius` contains it; `contains(SIZE_UNIFORM)`
495 /// passes for `"size"`, which is the *pixel* program's uniform name that
496 /// this module exists to work around; and `contains("//_DEFINES")` passes
497 /// for the wrong marker, which is a prefix of the right one. All three
498 /// compile. All three fail only on a GPU, silently.
499 fn has_line(wanted: &str) -> bool {
500 ROUNDED_CORNERS.lines().any(|line| line.trim() == wanted)
501 }
502
503 /// Four corners, and the shader has to tell them apart. A single radius is
504 /// the common case and gets a constructor; it is not the only case.
505 #[test]
506 fn corners_can_differ() {
507 let finder = Corners {
508 top_left: 0.0,
509 top_right: 0.0,
510 bottom_left: 12.0,
511 bottom_right: 12.0,
512 };
513 let effect = Effect::rounded(finder);
514 assert_eq!(effect.radii(), finder);
515 assert!(
516 !effect.is_none_effect(),
517 "two corners rounded is still an effect"
518 );
519 }
520
521 /// All four zero is *no effect*, and that is what keeps the ordinary
522 /// window off the pass path entirely.
523 #[test]
524 fn every_corner_zero_is_no_effect() {
525 assert!(Effect::rounded(Corners::all(0.0)).is_none_effect());
526 assert!(Effect::rounded(Corners::all(-3.0)).is_none_effect());
527 assert!(Effect::rounded(Corners::all(f64::NAN)).is_none_effect());
528 // But ONE corner is enough to be one.
529 let one = Corners {
530 top_left: 8.0,
531 ..Corners::all(0.0)
532 };
533 assert!(!Effect::rounded(one).is_none_effect());
534 }
535
536 /// Each side is inset by the larger of the two corners touching it. The
537 /// asymmetry is the point: a square-topped, round-bottomed window must not
538 /// give up its top rows.
539 #[test]
540 fn a_side_is_measured_by_its_larger_corner() {
541 let corners = Corners {
542 top_left: 0.0,
543 top_right: 0.0,
544 bottom_left: 12.0,
545 bottom_right: 20.0,
546 };
547 let (top, right, bottom, left) = corners.max_of_side();
548 assert!(
549 (top - 0.0).abs() < f64::EPSILON,
550 "no top corner is cut, so no top rows are lost"
551 );
552 assert!(
553 (right - 20.0).abs() < f64::EPSILON,
554 "the right side touches top-right and bottom-right"
555 );
556 assert!((bottom - 20.0).abs() < f64::EPSILON);
557 assert!((left - 12.0).abs() < f64::EPSILON);
558 }
559
560 /// The shader picks a radius per quadrant BEFORE `abs()` folds the
561 /// coordinate, which is the one line that makes four radii possible at
562 /// all. Pinned by text here and drawn for real in wirecheck.
563 #[test]
564 fn the_shader_selects_a_radius_per_quadrant() {
565 assert!(
566 has_line("uniform vec4 corner_radius;"),
567 "four radii, as tl/tr/bl/br"
568 );
569 assert!(
570 has_line("float picked = (v_coords.x < 0.5)"),
571 "the quadrant is chosen from the unfolded coordinate; after `abs()` \
572 every corner looks like the top-left and the four are indistinguishable"
573 );
574 }
575
576 /// `largest` is what a layer's own `clientRadius` still gets -- see
577 /// `Corners::largest`. It has to be the biggest of the four, not the
578 /// smallest or an average, because `PaneStyle.qml`'s `clientRadius + 2`
579 /// hug has to clear the biggest cut or it clips into it.
580 ///
581 /// On a bare `Corners` directly, because that is the shape a later task
582 /// holds with no `Effect` around it; `Effect::largest` is checked too,
583 /// since it is a caller of this and not a second implementation.
584 #[test]
585 fn largest_is_the_biggest_of_the_four() {
586 let corners = Corners {
587 top_left: 4.0,
588 top_right: 20.0,
589 bottom_left: 12.0,
590 bottom_right: 0.0,
591 };
592 assert!((corners.largest() - 20.0).abs() < f64::EPSILON);
593 assert!((Effect::rounded(corners).largest() - 20.0).abs() < f64::EPSILON);
594 }
595
596 /// **Rounding needs no capture**: it is drawn surface by surface through
597 /// [`CLIPPED_SURFACE`] and [`CLIPPED_SOLID`], where each surface is, so it
598 /// reads `Inline` and never `SelfTexture`. That distinction is what
599 /// decides whether a window is captured, so it is a value and not a
600 /// comment.
601 #[test]
602 fn rounding_needs_no_capture() {
603 let effect = Effect::rounded(Corners::all(12.0));
604 assert_eq!(effect.inputs(), Inputs::Inline);
605 assert_eq!(effect.radii(), Corners::all(12.0));
606 }
607
608 /// The shader is handed to Smithay, whose contract for a *texture* program
609 /// is not the one its own documentation states. Read from the source, not
610 /// the doc comment:
611 ///
612 /// | | texture program | pixel program |
613 /// |---|---|---|
614 /// | marker | `//_DEFINES_` | `//_DEFINES_` |
615 /// | `#version` | **the shader supplies it** | smithay prepends it |
616 /// | `size` uniform | **not provided** | provided |
617 ///
618 /// `gles/mod.rs:1964` says the marker is `//_DEFINES`, without the
619 /// trailing underscore. `shaders/mod.rs:125` is what actually runs and it
620 /// replaces `//_DEFINES_`. A shader carrying the documented spelling has
621 /// its marker left in place as a comment, compiles, and then fails to link
622 /// because the `#define`s it needed were never substituted.
623 ///
624 /// None of this fails until there is a GPU, which is why it is asserted
625 /// here and compiled for real in wirecheck.
626 #[test]
627 fn the_shader_is_shaped_the_way_smithay_actually_requires() {
628 assert!(
629 has_line("//_DEFINES_"),
630 "smithay replaces a line that is exactly `//_DEFINES_` with its #defines"
631 );
632 assert!(
633 ROUNDED_CORNERS.starts_with("#version 100"),
634 "texture_program does NOT prepend a version -- the built-in \
635 texture.frag carries its own, and so must this"
636 );
637
638 // The declaration line, built from the constant, so the name the
639 // compositor will pass to `UniformName::new` and the name the shader
640 // declares cannot drift apart. See `has_line` for why not `contains`.
641 for declaration in [
642 format!("uniform vec4 {RADIUS_UNIFORM};"),
643 format!("uniform vec2 {SIZE_UNIFORM};"),
644 ] {
645 assert!(
646 has_line(&declaration),
647 "the shader has no `{declaration}`, so GetUniformLocation \
648 returns -1 and the uniform is silently never set"
649 );
650 }
651 }
652
653 /// **Smithay compiles this shader three times, not once.**
654 ///
655 /// `shaders/mod.rs` builds a variant for each of `&[]`, `&[NO_ALPHA]` and
656 /// `&[EXTERNAL]` eagerly, and `variant_for_format` picks between them per
657 /// texture: variant 1 for any known format with `has_alpha == false`,
658 /// variant 2 whenever the format is `None`, which is how an external
659 /// texture arrives. Neither is exotic -- XRGB8888 is the ordinary opaque
660 /// window, and a hardware-decoded video surface binds
661 /// `TEXTURE_EXTERNAL_OES`.
662 ///
663 /// A shader that ignores the defines compiles perfectly and then
664 /// misbehaves on a GPU: an XRGB window drawn with its undefined X byte as
665 /// alpha is *invisible*, and an external texture read through a
666 /// `sampler2D` is *black*. `texture.frag` is the model this mirrors.
667 /// The precision guard, whose absence no test on this machine can feel.
668 ///
669 /// Mesa evaluates `mediump` at fp32, so removing this changes nothing here
670 /// and everything on a 4K screen with a conformant driver -- one mediump
671 /// ULP at 3840px is ~3.75px, against a half-pixel antialiasing band.
672 #[test]
673 fn the_shader_asks_for_highp_where_it_exists() {
674 assert!(
675 has_line("#ifdef GL_FRAGMENT_PRECISION_HIGH"),
676 "the shader scales a normalised coordinate into pixels, so it has \
677 to ask for highp rather than take mediump's ten mantissa bits"
678 );
679 assert!(has_line("precision highp float;"));
680 assert!(
681 has_line("precision mediump float;"),
682 "and still falls back: highp in a fragment shader is optional in GLES 2"
683 );
684 }
685
686 #[test]
687 fn the_shader_handles_every_variant_smithay_compiles_it_into() {
688 for required in [
689 "#if defined(NO_ALPHA)",
690 "#if defined(EXTERNAL)",
691 "#extension GL_OES_EGL_image_external : require",
692 "uniform samplerExternalOES tex;",
693 "uniform sampler2D tex;",
694 ] {
695 assert!(
696 has_line(required),
697 "no `{required}`: one of smithay's three variants would draw wrong"
698 );
699 }
700 // And the NO_ALPHA arm has to *do* something. A branch that takes the
701 // same path under a different name satisfies every assertion above and
702 // still draws an invisible window.
703 assert!(
704 has_line("colour = vec4(colour.rgb, 1.0) * alpha;"),
705 "NO_ALPHA has to replace the alpha channel, not sample it"
706 );
707 }
708
709 /// **A radius larger than the window degrades to a stadium; it does not
710 /// eat the window.**
711 ///
712 /// Nothing upstream clamps: `Effect::rounded(Corners::all(1e9))` is
713 /// accepted and is not a none-effect. Unclamped, `half_size - vec2(r)`
714 /// goes negative on the short axis, every fragment of the short edge
715 /// reports `away >= 0`, and a 300x200 window at radius 150 loses its top
716 /// and bottom rows outright.
717 #[test]
718 fn a_radius_larger_than_the_window_cannot_erode_it() {
719 assert!(
720 has_line("float r = min(picked, min(half_size.x, half_size.y));"),
721 "the picked per-corner radius is used unclamped, so a large one erodes the window"
722 );
723 // And nothing walks around the clamp. Stated as two properties rather
724 // than as a count of the uniform's name, which was the first attempt
725 // and was wrong in both directions: a comment inside the shader string
726 // that happened to mention `corner_radius` pushed the count to three
727 // and failed with "part of the distance field is unclamped", which is
728 // a wrong diagnosis of a harmless edit -- and a bypass that replaced
729 // half the field with a literal kept the count at two and passed.
730 //
731 // First: the raw uniform appears on no line but its declaration and
732 // the two branches of the per-quadrant pick that feeds the clamp --
733 // the clamp itself now reads `picked`, not `corner_radius`, which is
734 // why the assertion above pins the clamp's own line directly instead
735 // of walking to it here. Comments are free to say the uniform's name.
736 for line in ROUNDED_CORNERS.lines() {
737 let code = line.split("//").next().unwrap_or_default().trim();
738 if !code.contains(RADIUS_UNIFORM) {
739 continue;
740 }
741 assert!(
742 code == format!("uniform vec4 {RADIUS_UNIFORM};")
743 || code == "? ((v_coords.y < 0.5) ? corner_radius.x : corner_radius.z)"
744 || code == ": ((v_coords.y < 0.5) ? corner_radius.y : corner_radius.w);",
745 "`{RADIUS_UNIFORM}` is used at `{code}`, which is neither its \
746 declaration nor a branch of the per-quadrant pick -- so that \
747 part of the distance field reads the radius somewhere the \
748 clamp cannot reach"
749 );
750 }
751 // Second: both halves of the field are written in terms of the clamped
752 // copy. The check above cannot see this -- a half replaced by a
753 // literal mentions the uniform nowhere and would pass it.
754 assert!(
755 has_line("vec2 p = abs(v_coords * tex_size - half_size) - (half_size - vec2(r));"),
756 "the inset half of the distance field is not written in terms of `r`"
757 );
758 assert!(
759 has_line("float away = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r;"),
760 "this is not the exact rounded-box distance written in terms of `r`. \
761 It needs BOTH halves: the interior term `min(max(p.x, p.y), 0.0)`, \
762 which is what a fragment inside the shape is measured by, and the \
763 outset half `length(max(p, 0.0)) - r`, which is the arcs and \
764 everything past an edge"
765 );
766 // And the interior term is part of that line, pinned by the whole-line
767 // match above rather than by a second assertion -- but it is worth
768 // naming here, because dropping it is the one edit that leaves every
769 // *other* assertion in this module green. `length(max(p, 0.0)) - r`
770 // alone reports a flat `-r` inside the shape, which at `r == 0` is the
771 // smoothstep's midpoint: a square corner's whole quadrant at 50%
772 // alpha, measured at 127 on a real GPU. See the comment on the line
773 // itself.
774
775 // The two lines that decide where the corner circle sits and how wide
776 // the antialiased band is. Neither was pinned here until a review of
777 // the corner opacity work found it: a transcription of this field in
778 // `solium::pass` was the ONLY thing in the workspace that noticed
779 // `tex_size * 0.5` becoming `* 0.4`, and it lives in another crate.
780 // A shader's own crate should be the thing that catches an edit to it.
781 assert!(
782 has_line("vec2 half_size = tex_size * 0.5;"),
783 "the corner circles are placed from the half-size; at anything \
784 but 0.5 they are not in the corners"
785 );
786 assert!(
787 has_line("gl_FragColor = colour * (1.0 - smoothstep(-0.5, 0.5, away));"),
788 "the antialias band is one texel wide and centred on the edge; \
789 widening it makes the whole window translucent at small radii"
790 );
791 }
792
793 /// The clipped program is a texture program, shaped as smithay requires:
794 /// its own `#version 100`, the `//_DEFINES_` line, and every variant.
795 #[test]
796 fn the_clipped_program_is_shaped_the_way_smithay_requires() {
797 assert!(CLIPPED_SURFACE.starts_with("#version 100\n"));
798 assert!(CLIPPED_SURFACE.lines().any(|line| line == "//_DEFINES_"));
799 for define in ["EXTERNAL", "NO_ALPHA", "DEBUG_FLAGS"] {
800 assert!(
801 CLIPPED_SURFACE.contains(&format!("defined({define})")),
802 "{define} is not handled"
803 );
804 }
805 for uniform in [
806 INPUT_TO_GEO_UNIFORM,
807 GEO_SIZE_UNIFORM,
808 GEO_PX_UNIFORM,
809 RADIUS_UNIFORM,
810 ] {
811 assert!(CLIPPED_SURFACE.contains(uniform), "{uniform} is not read");
812 }
813 }
814
815 /// The solid program is a pixel program: smithay adds `#version 100`
816 /// itself and refuses a second, and it reads the colour it is given.
817 #[test]
818 fn the_solid_program_is_a_pixel_program() {
819 assert!(!CLIPPED_SOLID.contains("#version"));
820 assert!(CLIPPED_SOLID.contains("defined(DEBUG_FLAGS)"));
821 for uniform in [
822 INPUT_TO_GEO_UNIFORM,
823 GEO_SIZE_UNIFORM,
824 GEO_PX_UNIFORM,
825 RADIUS_UNIFORM,
826 COLOUR_UNIFORM,
827 ] {
828 assert!(CLIPPED_SOLID.contains(uniform), "{uniform} is not read");
829 }
830 }
831
832 /// Both keep the interior term the rounding shader's own history earned
833 /// (`ROUNDED_CORNERS`' comment on `min(max(p.x, p.y), 0.0)`) and its clamp.
834 #[test]
835 fn the_clipped_programs_keep_the_interior_term_and_the_clamp() {
836 for source in [CLIPPED_SURFACE, CLIPPED_SOLID] {
837 assert!(source.contains("min(max(p.x, p.y), 0.0)"));
838 assert!(source.contains("min(picked, min(half_size.x, half_size.y))"));
839 }
840 }
841}