Skip to main content

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}