frus-gpu 0.3.0

frus — GPU context and 2D rendering engine (Milestone 0)
Documentation
// Instanced rectangle rendering: rounded corners, border, gradient, shadow (SDF).

struct Viewport {
    size: vec2<f32>,
};

@group(0) @binding(0)
var<uniform> viewport: Viewport;

// The gradients' colour ramps: one row per gradient, sRGB values (milestone 644).
@group(1) @binding(0)
var ramp: texture_2d<f32>;
@group(1) @binding(1)
var ramp_sampler: sampler;

struct VertexInput {
    @location(0) unit_pos: vec2<f32>,
};

struct InstanceInput {
    @location(1) rect: vec4<f32>,     // x, y, width, height (pixels)
    @location(2) color: vec4<f32>,    // the fill, or the gradient's start
    @location(3) color2: vec4<f32>,   // the gradient's end
    @location(4) border: vec4<f32>,   // the border colour
    @location(5) params: vec4<f32>,   // code (kind + 4 tile), border_width, blur, ramp row
    @location(6) gradient: vec4<f32>, // the legacy direction, or a gradient's geometry
    @location(7) clip: vec4<f32>,     // x, y, width, height
    @location(8) radii: vec4<f32>,    // per-corner radii: tl, tr, br, bl
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) local_px: vec2<f32>,
    @location(1) uv: vec2<f32>,
    @location(2) @interpolate(flat) half_size: vec2<f32>,
    @location(3) @interpolate(flat) radii: vec4<f32>,
    @location(4) @interpolate(flat) border_width: f32,
    @location(5) @interpolate(flat) blur: f32,
    @location(6) @interpolate(flat) color: vec4<f32>,
    @location(7) @interpolate(flat) color2: vec4<f32>,
    @location(8) @interpolate(flat) border: vec4<f32>,
    @location(9) @interpolate(flat) gradient: vec4<f32>,
    @location(10) frag_px: vec2<f32>,
    @location(11) @interpolate(flat) clip: vec4<f32>,
    @location(12) @interpolate(flat) shade: vec2<f32>,
};

@vertex
fn vs_main(vert: VertexInput, inst: InstanceInput) -> VertexOutput {
    let pos_px = inst.rect.xy + vert.unit_pos * inst.rect.zw;

    let ndc = vec2<f32>(
        pos_px.x / viewport.size.x * 2.0 - 1.0,
        1.0 - pos_px.y / viewport.size.y * 2.0,
    );

    var out: VertexOutput;
    out.clip_position = vec4<f32>(ndc, 0.0, 1.0);
    out.local_px = (vert.unit_pos - vec2<f32>(0.5, 0.5)) * inst.rect.zw;
    out.uv = vert.unit_pos;
    out.half_size = inst.rect.zw * 0.5;
    out.radii = inst.radii;
    out.border_width = inst.params.y;
    out.blur = inst.params.z;
    out.color = inst.color;
    out.color2 = inst.color2;
    out.border = inst.border;
    out.gradient = inst.gradient;
    out.shade = vec2<f32>(inst.params.x, inst.params.w);
    out.frag_px = pos_px;
    out.clip = inst.clip;
    return out;
}

// Converts an sRGB colour — as authored in the scene — to linear. The target being
// sRGB, the GPU re-encodes linear→sRGB on write, so sending linear reproduces
// exactly the intended colour; encoding twice would wash it out.
fn srgb_to_linear(c: vec3<f32>) -> vec3<f32> {
    let lower = c / 12.92;
    let higher = pow((c + vec3<f32>(0.055)) / 1.055, vec3<f32>(2.4));
    return select(higher, lower, c <= vec3<f32>(0.04045));
}

// Signed distance to a centred rounded rectangle, negative inside.
fn sdf_round_box(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
    let q = abs(p) - b + vec2<f32>(r, r);
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0, 0.0))) - r;
}

// The corner radius matching `p`'s quadrant (centred coordinates, y pointing
// down). radii = (tl, tr, br, bl).
fn corner_radius(p: vec2<f32>, radii: vec4<f32>) -> f32 {
    if (p.x < 0.0) {
        return select(radii.w, radii.x, p.y < 0.0); // left: top → tl, bottom → bl
    }
    return select(radii.z, radii.y, p.y < 0.0);     // right: top → tr, bottom → br
}

// **A gradient's colour at `p`** (pixels from the rectangle's centre), milestone 644:
// `t` along a line, between two circles, or round a centre, tiled, then read from the
// gradient's row of the ramps. `extra` is the focal point, the turn and whether there is a
// focal circle.
fn gradient_fill(code: i32, local: vec2<f32>, g: vec4<f32>, extra: vec4<f32>, row: f32) -> vec4<f32> {
    let kind = code % 4;
    let tile = code / 4;
    // The gradient turned about the centre: the point turned back.
    var p = local;
    let turn = extra.z;
    if (turn != 0.0) {
        let c = cos(turn);
        let s = sin(turn);
        p = vec2<f32>(c * p.x + s * p.y, -s * p.x + c * p.y);
    }
    var t = 0.0;
    var seen = 1.0;
    if (kind == 1) {
        let d = g.zw - g.xy;
        t = dot(p - g.xy, d) / max(dot(d, d), 1e-6);
    } else if (kind == 2) {
        if (extra.w < 0.5) {
            t = length(p - g.xy) / max(g.z, 1e-6);
        } else {
            // Between the focal circle (centre extra.xy, radius g.w) and the outer one
            // (centre g.xy, radius g.z): the largest t whose circle passes through p.
            let c0 = extra.xy;
            let r0 = g.w;
            let cd = g.xy - c0;
            let dr = g.z - r0;
            let pd = p - c0;
            let a = dot(cd, cd) - dr * dr;
            let b = dot(pd, cd) + r0 * dr;
            let c = dot(pd, pd) - r0 * r0;
            if (abs(a) < 1e-4) {
                t = c / max(2.0 * b, 1e-6);
            } else {
                let disc = b * b - a * c;
                if (disc < 0.0) {
                    seen = 0.0;
                } else {
                    let root = sqrt(disc);
                    t = (b + root) / a;
                    if (r0 + t * dr < 0.0) {
                        t = (b - root) / a;
                    }
                }
            }
        }
    } else {
        let v = p - g.xy;
        var angle = atan2(v.y, v.x);
        if (angle < 0.0) {
            angle = angle + 6.28318530718;
        }
        t = (angle - g.z) / max(g.w - g.z, 1e-6);
    }
    // Past the ends: clamped, repeated, mirrored, or nothing.
    if (tile == 0) {
        t = clamp(t, 0.0, 1.0);
    } else if (tile == 1) {
        t = fract(t);
    } else if (tile == 2) {
        let m = t - 2.0 * floor(t * 0.5);
        t = select(2.0 - m, m, m <= 1.0);
    } else if (t < 0.0 || t > 1.0) {
        seen = 0.0;
    }
    let size = vec2<f32>(textureDimensions(ramp));
    let uv = vec2<f32>((t * (size.x - 1.0) + 0.5) / size.x, (row + 0.5) / size.y);
    let color = textureSampleLevel(ramp, ramp_sampler, uv, 0.0);
    return vec4<f32>(color.rgb, color.a * seen);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // Clipping.
    let inside_clip = f32(
        in.frag_px.x >= in.clip.x
        && in.frag_px.x <= in.clip.x + in.clip.z
        && in.frag_px.y >= in.clip.y
        && in.frag_px.y <= in.clip.y + in.clip.w
    );

    let r = min(corner_radius(in.local_px, in.radii), min(in.half_size.x, in.half_size.y));
    let d = sdf_round_box(in.local_px, in.half_size, r);

    // A hard edge (half a pixel either side of it), or, for a shadow, a soft one that fades
    // **inside** the quad: a shadow's rect already reaches its blur beyond the shape, so
    // the fade runs from the shape's edge less the blur, at full strength, out to the
    // quad's edge, at nothing. Ramping across the quad's edge instead left the outer half
    // of every shadow unrasterised, and cut it off at half its strength (milestone 606).
    var alpha = 1.0 - smoothstep(-0.5, 0.5, d);
    let code = i32(round(in.shade.x));
    if (in.blur > 0.0) {
        alpha = 1.0 - smoothstep(-2.0 * in.blur, 0.0, d);
        // The blur style (milestone 645), read where a flat fill leaves the ramp row
        // free. The shape's own edge runs `blur` inside the quad's.
        let style = i32(round(in.shade.y));
        let inside = 1.0 - smoothstep(-0.5, 0.5, d + in.blur);
        if (code == 0 && style == 1) {
            alpha = max(alpha, inside);
        } else if (code == 0 && style == 2) {
            alpha = alpha * (1.0 - inside);
        } else if (code == 0 && style == 3) {
            alpha = alpha * inside;
        }
    }
    alpha = alpha * inside_clip;

    // The fill: a gradient from the ramps, or the legacy two-colour fade (solid when
    // dir = 0 and color2 = color).
    var fill: vec4<f32>;
    if (code == 0) {
        let t = clamp(dot(in.uv - vec2<f32>(0.5, 0.5), in.gradient.xy) + 0.5, 0.0, 1.0);
        fill = mix(in.color, in.color2, t);
    } else {
        fill = gradient_fill(code, in.local_px, in.gradient, in.color2, in.shade.y);
    }

    // The border: a ring along the edge.
    if (in.border_width > 0.0) {
        let bt = smoothstep(-in.border_width - 0.5, -in.border_width + 0.5, d);
        fill = mix(fill, in.border, bt);
    }

    return vec4<f32>(srgb_to_linear(fill.rgb), fill.a * alpha);
}