frus-gpu 0.3.0

frus — GPU context and 2D rendering engine (Milestone 0)
Documentation
// Rendering of tessellated vector paths: indexed triangles, each vertex carrying
// its colour (sRGB), its gradient and its clip rectangle. The geometry, fill or
// stroke, is produced on the CPU by lyon; this shader projects, fades and clips.
//
// The gradient is described, not baked: every vertex of a path carries the same
// description, and the fragment resolves it from the pixel's own position. Baking it
// per vertex would make the fade a function of the tessellation — fine for a straight
// fade, which is affine, and wrong for a radial one.

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

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

struct VertexInput {
    @location(0) pos: vec2<f32>,       // pixels: logical space scaled by the DPI
    @location(1) color: vec4<f32>,     // sRGB, the fill or the gradient's start
    @location(2) color2: vec4<f32>,    // the gradient's end; == color when flat
    @location(3) geometry: vec4<f32>,  // (from, to), or (centre, radii) if radial
    @location(4) kind: vec2<f32>,      // (0 flat / 1 straight / 2 radial, inner)
    @location(5) clip: vec4<f32>,      // x, y, width, height
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) @interpolate(flat) color: vec4<f32>,
    @location(1) @interpolate(flat) color2: vec4<f32>,
    @location(2) @interpolate(flat) geometry: vec4<f32>,
    @location(3) @interpolate(flat) kind: vec2<f32>,
    @location(4) @interpolate(flat) clip: vec4<f32>,
    @location(5) frag_px: vec2<f32>,
};

@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
    let ndc = vec2<f32>(
        v.pos.x / viewport.size.x * 2.0 - 1.0,
        1.0 - v.pos.y / viewport.size.y * 2.0,
    );
    var out: VertexOutput;
    out.clip_position = vec4<f32>(ndc, 0.0, 1.0);
    out.color = v.color;
    out.color2 = v.color2;
    out.geometry = v.geometry;
    out.kind = v.kind;
    out.clip = v.clip;
    out.frag_px = v.pos;
    return out;
}

// sRGB → linear: an sRGB target re-encodes on write, so we send linear to
// reproduce the authored colour exactly, as quad.wgsl does.
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));
}

// How far through the fade this pixel is, before clamping.
fn gradient_t(kind: vec2<f32>, geometry: vec4<f32>, p: vec2<f32>) -> f32 {
    if (kind.x < 0.5) {
        return 0.0;
    }
    if (kind.x < 1.5) {
        let axis = geometry.zw - geometry.xy;
        let len2 = dot(axis, axis);
        if (len2 <= 0.0) {
            return 0.0;
        }
        return dot(p - geometry.xy, axis) / len2;
    }
    // Radial: the distance to the centre measured in radii, so the far end of the
    // fade is the ellipse itself whichever way the boundary curves.
    let radii = max(geometry.zw, vec2<f32>(1e-6, 1e-6));
    let d = length((p - geometry.xy) / radii);
    let inner = min(kind.y, 0.999);
    return (d - inner) / (1.0 - inner);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    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 t = gradient_t(in.kind, in.geometry, in.frag_px);
    let color = mix(in.color, in.color2, clamp(t, 0.0, 1.0));
    return vec4<f32>(srgb_to_linear(color.rgb), color.a * inside_clip);
}