Skip to main content

ROUNDED_CORNERS

Constant ROUNDED_CORNERS 

Source
pub const ROUNDED_CORNERS: &str = r"#version 100

//_DEFINES_

#if defined(EXTERNAL)
#extension GL_OES_EGL_image_external : require
#endif

// highp where the hardware has it, which is not a nicety here.
//
// GLES 2 guarantees mediump only 10 mantissa bits. This shader multiplies a
// normalised coordinate back up into pixels -- `v_coords * tex_size` -- so one
// ULP at 3840px is about 3.75px, seven times the width of the smoothstep that
// antialiases the arc. The corner would wobble and its edge come apart, on a
// 4K screen only.
//
// Mesa evaluates mediump at fp32, so none of that is visible on the machine
// this was written on. Same shape as the logical-versus-physical radius above:
// a defect whose only symptom is on hardware the author does not have.
// `GL_FRAGMENT_PRECISION_HIGH` is the compiler's own answer to whether highp
// exists in a fragment shader, so this asks rather than assumes.
//
// smithay's `texture.frag` settles for mediump and is not a precedent: it
// samples at `v_coords` and never scales a normalised coordinate back into
// pixel space, so it has no bits to lose.
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#if defined(EXTERNAL)
uniform samplerExternalOES tex;
#else
uniform sampler2D tex;
#endif

uniform float alpha;
uniform vec4 corner_radius;
uniform vec2 tex_size;
varying vec2 v_coords;

#if defined(DEBUG_FLAGS)
uniform float tint;
#endif

void main() {
    vec4 colour = texture2D(tex, v_coords);

#if defined(NO_ALPHA)
    // Forced opaque rather than sampled. The X byte of an XRGB8888 buffer is
    // undefined, so a client that leaves it at zero draws a fully invisible
    // window if it is multiplied in -- and XRGB8888 is the ordinary opaque
    // case, not an exotic one. Smithay picks this variant itself, from the
    // buffer's format.
    colour = vec4(colour.rgb, 1.0) * alpha;
#else
    // Every channel, not just alpha: a wayland surface is PREMULTIPLIED, so
    // colour and alpha have to be scaled together or a faded edge comes out
    // too bright. Smithay's own texture.frag does `color * alpha` for the
    // same reason.
    colour = colour * alpha;
#endif

#if defined(DEBUG_FLAGS)
    if (tint == 1.0)
        colour = vec4(0.0, 0.2, 0.0, 0.2) + colour * 0.8;
#endif

    // Into pixels, then into one corner's quadrant: abs() folds all four
    // corners onto one, so the distance field is written once rather than
    // four times. `v_coords` is `(tex_matrix * position).xy`, which for a
    // whole texture runs 0..1 -- see smithay's texture.vert.
    //
    // Clamped to the shorter half, and the field is written in terms of the
    // clamp. Unclamped, a radius past min(w,h)/2 pushes each corner circle's
    // centre outside the rectangle and the field calls every fragment of the
    // short edge outside: a 300x200 window at radius 150 loses its whole top
    // and bottom rows instead of degrading to a stadium. Nothing upstream
    // clamps -- Effect::rounded(Corners::all(1e9)) is accepted and is not a
    // none-effect.
    vec2 half_size = tex_size * 0.5;

    // Which corner this fragment belongs to, decided on the UNFOLDED
    // coordinate. `abs()` below makes all four look like the top-left, which
    // is what lets one expression draw four corners -- and is exactly why the
    // radius has to be chosen first. `corner_radius` is (tl, tr, bl, br).
    float picked = (v_coords.x < 0.5)
        ? ((v_coords.y < 0.5) ? corner_radius.x : corner_radius.z)
        : ((v_coords.y < 0.5) ? corner_radius.y : corner_radius.w);
    float r = min(picked, min(half_size.x, half_size.y));
    vec2 p = abs(v_coords * tex_size - half_size) - (half_size - vec2(r));

    // The exact rounded-box distance, **interior term and all**.
    //
    // `length(max(p, 0.0)) - r` on its own is only the OUTSIDE half of the
    // field. It is right on the arcs and right past every edge, and inside the
    // shape it saturates: both components of `p` are negative there, so it
    // reports a flat `-r` at every interior fragment instead of that
    // fragment's real distance to the edge. While a window had one radius that
    // was merely imprecise -- anything at `r >= 0.5` is past the smoothstep's
    // far end and comes out fully opaque either way, which is why half a pixel
    // was the bound everything downstream was written against.
    //
    // With a radius per quadrant it stops being imprecise and starts being
    // wrong, because `r` can now be 0 on one corner while the others round. A
    // zero radius makes the interior report exactly 0 -- the smoothstep's
    // MIDPOINT -- so that quadrant, a quarter of the window, is drawn at 50%
    // alpha rather than squared off. Measured on a real GPU through
    // `dev/wirecheck`: `corner_radius = (0, 16, 16, 16)` came back at alpha
    // 127 across every pixel of the top-left quadrant, and at 228 for a radius
    // of 0.3. A square-topped window is the headline case for per-corner
    // radii, not an exotic one -- `panes/flush/` is exactly that shape.
    //
    // `min(max(p.x, p.y), 0.0)` is that missing term, and adding it changes
    // nothing anywhere else: it is exactly 0.0 wherever either component of
    // `p` is positive, which is every fragment on an arc, along a straight
    // edge, or outside the shape. Checked and not merely argued -- all four
    // quadrants of a 64x64 at r=16 hash byte-identical across the change.
    //
    // It also settles where the shape's own boundary lands. A fragment centre
    // sits half a pixel inside the edge, so the outermost row now reports
    // exactly -0.5 for any `r` -- the smoothstep's near end, fully opaque --
    // which is what lets `clip::cut_corners` claim a square corner's pixels
    // and every row of an uncut side.
    float away = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r;

    // The mask goes last, after the tint. The tint ADDS a constant, so a
    // fragment masked to zero before it would be painted back in and the cut
    // corners would reappear in green whenever debug flags are on.
    //
    // The one-unit smoothstep is not a one-*pixel* antialias: `away` is in
    // texture pixels, so the softened band is one screen pixel wide only when
    // the texture is drawn at 1:1. Magnified it is a visible blur, minified it
    // aliases. The scale-correct form is smoothstep(-w, w, away) with
    // w = fwidth(away), which under #version 100 needs
    // GL_OES_standard_derivatives; until that extension is requested, this
    // buys a soft edge at native scale and a wrong-width one everywhere else.
    gl_FragColor = colour * (1.0 - smoothstep(-0.5, 0.5, away));
}
";
Expand description

Rounded corners, as a fragment program over the node’s own texture.

Smithay’s contract for a TEXTURE program, read from shaders/mod.rs rather than from the doc comment on compile_custom_texture_shader, which is wrong about the first of these:

  • the source must contain a line that is exactly //_DEFINES_ – with the trailing underscore; the doc comment omits it;
  • the source supplies its own #version 100. texture_program does not prepend one, and the built-in texture.frag carries its own. (The pixel program is the one where smithay prepends it.)
  • it is compiled six times, not once, and has to behave under each set of #defines. Three define sets – &[], &[NO_ALPHA], &[EXTERNAL] – and texture_program links each of them twice, once plain and once 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 number of variants variant_for_format picks between; six is the number that have to compile, and a shader that only builds without the debug define fails at renderer construction and takes the session with it. See the_shader_handles_every_variant_smithay_compiles_it_into, and texture.frag, which is the model this mirrors.

alpha is Smithay’s; corner_radius and tex_size are ours. A texture program gets no size uniform – only a pixel program does. The distance field is the standard rounded-box one: pick a radius for the fragment’s quadrant, fold the coordinate into that quadrant, and measure from the centre of that corner’s circle.