// Every shape Denise draws, as one pipeline.
//
// A vertex carries the pixel it sits on, the clip it must not leave, a
// premultiplied colour, two vec4s of shape parameters and a kind. The fragment
// shader turns the kind into a signed distance, the distance into coverage, and
// the coverage into how much of the colour reaches the target. Rectangles are
// kind 0 and skip all of that: their edges are the triangle's own. A polygon
// has more edges than a vertex can carry, so it carries a range of a storage
// buffer that holds them instead.
struct Globals {
// Target size in pixels, for the NDC transform.
size: vec2<f32>,
// 1 if the target is an sRGB format, so what we hand it must be linear.
srgb: u32,
pad: u32,
};
@group(0) @binding(0) var<uniform> globals: Globals;
@group(1) @binding(0) var tex: texture_2d<f32>;
@group(1) @binding(1) var samp: sampler;
// Every edge of every polygon in the frame, as x0, y0, x1, y1. A polygon's
// vertices say where its own run starts and how long it is.
@group(2) @binding(0) var<storage, read> edges: array<vec4<f32>>;
struct VsIn {
@location(0) pos: vec2<f32>,
@location(1) clip: vec4<f32>,
@location(2) color: vec4<f32>,
@location(3) a: vec4<f32>,
@location(4) b: vec4<f32>,
@location(5) kind: u32,
// First edge and edge count, for a polygon; zero for everything else.
@location(6) poly: vec2<u32>,
};
struct VsOut {
@builtin(position) position: vec4<f32>,
@location(0) clip: vec4<f32>,
@location(1) color: vec4<f32>,
@location(2) a: vec4<f32>,
@location(3) b: vec4<f32>,
@location(4) @interpolate(flat) kind: u32,
@location(5) @interpolate(flat) poly: vec2<u32>,
};
@vertex
fn vs(in: VsIn) -> VsOut {
var out: VsOut;
let ndc = vec2<f32>(
in.pos.x / globals.size.x * 2.0 - 1.0,
1.0 - in.pos.y / globals.size.y * 2.0,
);
out.position = vec4<f32>(ndc, 0.0, 1.0);
out.clip = in.clip;
out.color = in.color;
out.a = in.a;
out.b = in.b;
out.kind = in.kind;
out.poly = in.poly;
return out;
}
// Anti-aliasing is one pixel wide: a fragment centre on the edge is half
// covered, one inside is fully covered, one outside not at all.
fn coverage(d: f32) -> f32 {
return clamp(0.5 - d, 0.0, 1.0);
}
// Signed distance to a box of half-extents `half` with corner radius `r`,
// centred on the origin.
fn sd_round_box(p: vec2<f32>, half: vec2<f32>, r: f32) -> f32 {
let q = abs(p) - half + vec2<f32>(r, r);
return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
}
fn sd_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
let pa = p - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
return length(pa - ba * h);
}
// The angle of `v` in turns: 0 at twelve o'clock, clockwise positive, y down.
// The same convention as `denise::angle::TURN`, scaled to 0..1.
fn turns(v: vec2<f32>) -> f32 {
let t = atan2(v.x, -v.y) / 6.283185307;
return fract(t + 1.0);
}
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {
let p = in.position.xy;
if p.x < in.clip.x || p.y < in.clip.y || p.x >= in.clip.z || p.y >= in.clip.w {
discard;
}
// Sampled unconditionally: `textureSample` needs uniform control flow, and
// a switch on a per-primitive kind does not count as uniform to naga.
let texel = textureSample(tex, samp, in.a.xy);
var color = in.color;
var cov = 1.0;
let a = in.a;
let b = in.b;
let poly = in.poly;
switch in.kind {
// 0: solid triangle — a rectangle, or a quad another kind rides on.
case 0u: {}
// 1: rounded rectangle, filled.
case 1u: {
cov = coverage(sd_round_box(p - a.xy, a.zw, b.x));
}
// 2: rounded rectangle, stroked inside its bounds by b.y.
case 2u: {
let outer = sd_round_box(p - a.xy, a.zw, b.x);
let inner = sd_round_box(p - a.xy, a.zw - vec2<f32>(b.y, b.y), max(b.x - b.y, 0.0));
cov = coverage(outer) - coverage(inner);
}
// 3: circle, filled.
case 3u: {
cov = coverage(length(p - a.xy) - a.z);
}
// 4: circle, stroked inside its radius by a.w.
case 4u: {
let d = length(p - a.xy);
cov = coverage(d - a.z) - coverage(d - (a.z - a.w));
}
// 5: arc — the ring of kind 4 cut to b.x..b.x+b.y turns.
case 5u: {
let d = length(p - a.xy);
cov = coverage(d - a.z) - coverage(d - (a.z - a.w));
let t = fract(turns(p - a.xy) - b.x + 1.0);
if t >= b.y {
cov = 0.0;
}
}
// 6: a line from a.xy to a.zw, half-width b.x.
case 6u: {
cov = coverage(sd_segment(p, a.xy, a.zw) - b.x);
}
// 7: a premultiplied texture, sampled at a.xy.
case 7u: {
color = texel;
}
// 8: a coverage mask in the texture's red channel, in the vertex colour.
case 8u: {
cov = texel.r;
}
// 9: a texture masked to a rounded box: box centre b.xy, half b.zw, radius a.z.
case 9u: {
color = texel;
cov = coverage(sd_round_box(p - b.xy, b.zw, a.z));
}
// 10: a polygon, over the run of edges poly.x..poly.x + poly.y.
//
// The distance to the nearest edge is how far the fragment is from the
// outline; the even-odd crossing count is which side of it the
// fragment is on. Together they are the polygon's signed distance, and
// the same one-pixel ramp every other shape gets applies to it. Odd
// means inside, the same rule the software rasteriser fills by, so a
// shape that crosses itself is filled the same way by both.
case 10u: {
var near = 1e30;
var inside = false;
for (var i = 0u; i < poly.y; i = i + 1u) {
let e = edges[poly.x + i];
near = min(near, sd_segment(p, e.xy, e.zw));
// Does a ray to the right of the fragment cross this edge? The
// ends are half open in y, so a vertex on the ray counts once.
if (e.y > p.y) != (e.w > p.y) {
let t = (p.y - e.y) / (e.w - e.y);
if p.x < e.x + t * (e.z - e.x) {
inside = !inside;
}
}
}
cov = coverage(select(near, -near, inside));
}
default: {}
}
var out = color * cov;
if globals.srgb == 1u {
out = vec4<f32>(pow(out.rgb, vec3<f32>(2.2, 2.2, 2.2)), out.a);
}
return out;
}