Skip to main content

kui_core/
fragment.rs

1//! A fragment: WGSL an app registers, validated here so a frame never sees
2//! a source that cannot compile.
3//!
4//! The app writes one function:
5//!
6//! ```wgsl
7//! fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
8//!     let t = in.local.y / max(in.size.y, 1.0);
9//!     return mix(params[0], params[1], t);
10//! }
11//! ```
12//!
13//! and this module puts [`PRELUDE`] in front of it and [`EPILOGUE`] behind
14//! it. The prelude declares what the function reads; the epilogue is the
15//! entry point that calls it and then does what every other quad gets for
16//! free — the node's rounded box, the inherited clip (rounded when an
17//! ancestor rounds it), the group opacity, and the premultiply the blend
18//! expects. An app never writes a pipeline, a bind group, a clip or a
19//! blend, and cannot get any of them wrong.
20//!
21//! [`module_source`] is the one place that assembles the three, so the
22//! text the core validates at registration is character for character the
23//! text a backend compiles. `kui-wgpu` calls it rather than building its
24//! own.
25//!
26//! Everything this module declares is spelled `kui_` or `KUI_` so an app's
27//! own names cannot collide with it. The two names that are the contract —
28//! `FragmentIn` and `fragment` — are not prefixed, because they are what
29//! the app writes.
30
31/// Where a node's [`Draw`] lives in the frame's [`FragmentList`].
32#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
33pub struct FragmentDrawId(pub u32);
34
35/// What a `fragment` node names: the function, and the image it reads
36/// through `kui_sample` if it declared one. Every fragment door takes `impl
37/// Into<FragmentRef>`, so a bare
38/// [`FragmentId`](crate::resources::FragmentId) is the no-image form and
39/// `id.with_image(img)` the other; nothing else about the node changes.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct FragmentRef {
42    pub id: crate::resources::FragmentId,
43    pub image: Option<crate::resources::ImageId>,
44}
45
46impl From<crate::resources::FragmentId> for FragmentRef {
47    fn from(id: crate::resources::FragmentId) -> Self {
48        FragmentRef { id, image: None }
49    }
50}
51
52impl crate::resources::FragmentId {
53    /// This function reading `image`: what `kui_sample(uv)` returns texels
54    /// of, and `FragmentIn::image` the texel rect of.
55    pub fn with_image(self, image: crate::resources::ImageId) -> FragmentRef {
56        FragmentRef {
57            id: self,
58            image: Some(image),
59        }
60    }
61}
62
63/// One `fragment` node's draw as the builder records it: what the node
64/// declared, before emission resolves the image to an atlas slot or a
65/// texture entry (which is per window, so it cannot happen here) and
66/// writes the wire form, [`crate::display::FragmentDraw`].
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct Draw {
69    pub id: crate::resources::FragmentId,
70    pub image: Option<crate::resources::ImageId>,
71    /// The view's `params`, zero-padded to sixteen.
72    pub params: [f32; 16],
73}
74
75/// The frame's fragment draws, one per `fragment` node, indexed by the
76/// [`FragmentDrawId`] the node's `NodeContent` carries.
77///
78/// It is a side list for the same reason `DisplayList::fragments` is: a
79/// handle and sixteen floats is 72 bytes, and `NodeContent` is one entry
80/// per node on a tree that pushes 10,000 of them. Four bytes on the
81/// node, the rest here.
82///
83/// The previous frame's draws are kept while an `exit` needs them, by the
84/// same gated buffer swap `LineStore` and the text list use: a ghost is
85/// built out of the *previous* tree, whose nodes index the list that frame
86/// filled. A frame with no departure pays one `clear`.
87#[derive(Default)]
88pub struct FragmentList {
89    draws: crate::retain::Kept<Draw>,
90}
91
92impl FragmentList {
93    /// Starts a frame. `keep_prev` retains the list just finished so a
94    /// departing fragment can copy its draw out of it.
95    pub(crate) fn begin_frame(&mut self, keep_prev: bool) {
96        self.draws.begin(keep_prev);
97    }
98
99    /// Records a draw and returns where it went.
100    pub(crate) fn push(&mut self, draw: Draw) -> FragmentDrawId {
101        let id = FragmentDrawId(self.draws.len() as u32);
102        self.draws.push(draw);
103        id
104    }
105
106    pub(crate) fn get(&self, id: FragmentDrawId) -> Draw {
107        self.draws[id.0 as usize]
108    }
109
110    /// A draw from the frame before this one — what a ghost copies. Out of
111    /// range when the previous list was not kept, which is a departure the
112    /// swap did not expect; it draws nothing rather than something else's
113    /// picture.
114    pub(crate) fn prev_get(&self, id: FragmentDrawId) -> Option<Draw> {
115        self.draws.prev().get(id.0 as usize).copied()
116    }
117}
118
119/// An app's `params` as the shader takes them: the first sixteen numbers,
120/// zero-padded. The second value is how many were dropped, which the
121/// builder turns into a `fragment-params-truncated` warning.
122pub fn params_of(params: &[f32]) -> ([f32; 16], usize) {
123    let mut out = [0.0f32; 16];
124    let n = params.len().min(16);
125    out[..n].copy_from_slice(&params[..n]);
126    (out, params.len().saturating_sub(16))
127}
128
129/// What kui declares before the app's source: the inputs its `fragment`
130/// function reads, the globals behind them, and the same rounded-box
131/// distance the renderer draws every node with, so a fragment that wants
132/// to draw a shape has the tool the core uses.
133///
134/// The `KuiGlobals` it declares must describe the same bytes as
135/// `kui_wgpu`'s `Globals` struct; that crate's `globals_layout_matches` is
136/// the test that says so.
137pub const PRELUDE: &str = r#"
138// The frame's own numbers, shared with the quad pipeline (group 0).
139struct KuiGlobals {
140    viewport: vec2<f32>,
141    atlas_size: vec2<f32>,
142    time: f32,
143    scale: f32,
144    _pad: vec2<f32>,
145};
146@group(0) @binding(0) var<uniform> kui_globals: KuiGlobals;
147// The atlas — or, for a fragment whose `image` has a texture of its own,
148// that texture bound in the atlas's place with `atlas_size` set to its
149// size, exactly as a texture-backed `image` node is drawn (ADR 0025).
150@group(0) @binding(1) var kui_atlas: texture_2d<f32>;
151@group(0) @binding(2) var kui_sampler: sampler;
152@group(0) @binding(3) var kui_sampler_nearest: sampler;
153
154// The texel rect of the node's `image` in `kui_atlas`, `[x, y, w, h]`;
155// zero with no image. Module-private so `kui_sample` needs no argument
156// for it; the epilogue sets it before calling `fragment`.
157var<private> kui_image_rect: vec4<f32>;
158
159// What an app's `fragment` function is given.
160struct FragmentIn {
161    // The pixel being painted, in the node's own space: physical px from
162    // the node's top-left corner, y down.
163    local: vec2<f32>,
164    // The node's size in physical px.
165    size: vec2<f32>,
166    // The frame clock in seconds, the same one transitions read. Only
167    // moves between frames, so a fragment that uses it wants `animate`.
168    time: f32,
169    // Physical px per logical px.
170    scale: f32,
171    // The quad's colour, straight alpha: white on a `fragment` node, the
172    // `bg` fill on a `polygon` (ADR 0025, decision 6). A fragment that
173    // wants a colour the view chose reads it here rather than spending
174    // four params on one.
175    color: vec4<f32>,
176    // The node's `image` as a texel rect, `[x, y, w, h]`, in whatever
177    // `kui_sample` reads from — the atlas or the image's own texture; the
178    // app never needs to know which. `zw` is the image's size in texels,
179    // which is what a data texture's row and column count are. Zero with
180    // no image.
181    image: vec4<f32>,
182};
183
184// The node's `image` at `uv`, `(0,0)` its top-left and `(1,1)` its
185// bottom-right, bilinear between texels and clamped half a texel in from
186// the rect's edge so the neighbour past it — a glyph, in the atlas —
187// never bleeds in. Transparent black with no image. Straight alpha, as
188// the image was registered.
189fn kui_sample(uv: vec2<f32>) -> vec4<f32> {
190    let r = kui_image_rect;
191    let lo = r.xy + vec2<f32>(0.5, 0.5);
192    let hi = r.xy + r.zw - vec2<f32>(0.5, 0.5);
193    let t = clamp(r.xy + clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)) * r.zw, lo, max(lo, hi));
194    let c = textureSample(kui_atlas, kui_sampler, t / kui_globals.atlas_size);
195    return select(vec4<f32>(0.0), c, r.z > 0.0 && r.w > 0.0);
196}
197
198// `kui_sample` reading the nearest texel instead of blending four — a
199// heatmap cell, a pixel-art sprite, anything whose texels are values.
200fn kui_sample_nearest(uv: vec2<f32>) -> vec4<f32> {
201    let r = kui_image_rect;
202    let lo = r.xy + vec2<f32>(0.5, 0.5);
203    let hi = r.xy + r.zw - vec2<f32>(0.5, 0.5);
204    let t = clamp(r.xy + clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)) * r.zw, lo, max(lo, hi));
205    let c = textureSample(kui_atlas, kui_sampler_nearest, t / kui_globals.atlas_size);
206    return select(vec4<f32>(0.0), c, r.z > 0.0 && r.w > 0.0);
207}
208
209// Half-width of every SDF edge ramp, in physical px. The renderer's `AA`.
210const KUI_AA: f32 = 0.75;
211
212// Signed distance to a box with one radius per corner. `p` is centered
213// (y down), `radii` is tl, tr, br, bl; each is clamped to the half extents
214// so oversized radii degrade to a pill, never a fold.
215fn kui_sd_rounded_box(p: vec2<f32>, half: vec2<f32>, radii: vec4<f32>) -> f32 {
216    let right = p.x > 0.0;
217    let bottom = p.y > 0.0;
218    let top_r = select(radii.x, radii.y, right);
219    let bottom_r = select(radii.w, radii.z, right);
220    let r = select(top_r, bottom_r, bottom);
221    let rr = min(r, min(half.x, half.y));
222    let q = abs(p) - half + vec2<f32>(rr, rr);
223    return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - rr;
224}
225"#;
226
227/// What kui declares after it: the parameters uniform and the entry point.
228///
229/// The locations are `VsOut`'s in `kui-wgpu/src/shader.wgsl`, because the
230/// vertex stage of a fragment pipeline is that file's `vs_main`. The
231/// coverage math is `shade`'s, for the solid case: a square node covers a
232/// pixel by the area of it inside the quad, a rounded one by its SDF
233/// ramped over `KUI_AA`, and a rounded clip the same way.
234pub const EPILOGUE: &str = r#"
235// The sixteen params, then the image's texel rect — one slot per draw,
236// laid out as `kui_wgpu`'s `FragmentParams`.
237struct KuiFragmentParams { p: array<vec4<f32>, 4>, image: vec4<f32> };
238@group(1) @binding(0) var<uniform> kui_fragment_params: KuiFragmentParams;
239
240@fragment
241fn kui_fs_fragment(
242    @builtin(position) frag_pos: vec4<f32>,
243    @location(0) local: vec2<f32>,
244    @location(1) size: vec2<f32>,
245    @location(2) color: vec4<f32>,
246    @location(6) clip: vec4<f32>,
247    @location(7) radii: vec4<f32>,
248    @location(8) clip_radii: vec4<f32>,
249) -> @location(0) vec4<f32> {
250    var kui_in: FragmentIn;
251    kui_in.local = local;
252    kui_in.size = size;
253    kui_in.time = kui_globals.time;
254    kui_in.scale = kui_globals.scale;
255    kui_in.color = vec4<f32>(color.rgb, 1.0);
256    kui_in.image = kui_fragment_params.image;
257    kui_image_rect = kui_fragment_params.image;
258    let kui_c = fragment(kui_in, kui_fragment_params.p);
259
260    // The node's own box, exactly as a solid gets it: a square one by the
261    // area of the pixel inside it, so one on whole pixels is solid to its
262    // edge and a stack of them meets without a seam; a rounded one by its
263    // SDF.
264    let kui_half = size * 0.5;
265    let kui_d = kui_sd_rounded_box(local - kui_half, kui_half, radii);
266    let kui_lo = max(local - vec2<f32>(0.5, 0.5), vec2<f32>(0.0, 0.0));
267    let kui_hi = min(local + vec2<f32>(0.5, 0.5), size);
268    let kui_area = clamp(kui_hi - kui_lo, vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0));
269    let kui_cov = select(
270        1.0 - smoothstep(-KUI_AA, KUI_AA, kui_d),
271        kui_area.x * kui_area.y,
272        all(radii <= vec4<f32>(0.0)),
273    );
274
275    // The inherited clip, in framebuffer space, rounded when rounded.
276    let kui_p = frag_pos.xy;
277    var kui_inside: f32;
278    if all(clip_radii <= vec4<f32>(0.0)) {
279        kui_inside = f32(
280            kui_p.x >= clip.x && kui_p.y >= clip.y
281            && kui_p.x <= clip.x + clip.z && kui_p.y <= clip.y + clip.w
282        );
283    } else {
284        let kui_ch = clip.zw * 0.5;
285        let kui_cd = kui_sd_rounded_box(kui_p - (clip.xy + kui_ch), kui_ch, clip_radii);
286        kui_inside = 1.0 - smoothstep(-KUI_AA, KUI_AA, kui_cd);
287    }
288
289    // `color.a` is the group opacity the subtree inherited, times the fill
290    // alpha on a polygon; `rgb` reached the function as `in.color`, and a
291    // fragment that ignores it returns its own colour as it always did.
292    let kui_a = clamp(kui_c.a, 0.0, 1.0) * kui_cov * kui_inside * color.a;
293    return vec4<f32>(clamp(kui_c.rgb, vec3<f32>(0.0), vec3<f32>(1.0)) * kui_a, kui_a);
294}
295"#;
296
297/// The stock fragment a rounded span background is painted with once the
298/// frame has joined it with the ones it meets: one piece of the shape per line, its quad as tall as
299/// the line and as wide as the piece and whatever of its neighbours'
300/// reach its corners can fill. The params are physical px from the
301/// quad's left: `params[0]` this piece `[a, b]` and the line above's,
302/// `params[1]` the line below's, the radius and which neighbours there
303/// are (1 above, 2 below); the fill is `in.color`, its alpha the quad's.
304///
305/// A corner is convex where this piece reaches past its neighbour on that
306/// side, a fillet past its end where the neighbour reaches past it
307/// (concave), square where the two end together, and round where there is
308/// no neighbour or it does not overlap. At a join the radius is half the
309/// step at most, and both pieces work it out from the same two ends, so
310/// the convex half above and the fillet below meet. The ends and the arcs
311/// are sampled sixteen times a pixel, and only near them; the pieces meet
312/// on whole pixels because a span's background is on them already.
313/// Registered by the core, as [`POLYGON`] is.
314pub const JOIN: &str = r#"
315fn join_radii(cx: f32, e: f32, has: bool, sx: f32, r: f32, lone: f32) -> vec2<f32> {
316    // A corner's (convex, concave) radii: `e` the neighbour's end on this
317    // side, `sx` outward.
318    if !has {
319        return vec2<f32>(lone, 0.0);
320    }
321    let d = (e - cx) * sx;
322    return vec2<f32>(min(r, max(-d, 0.0) * 0.5), min(r, max(d, 0.0) * 0.5));
323}
324
325fn join_cut(p: vec2<f32>, cx: f32, cy: f32, sx: f32, sy: f32, rc: f32) -> bool {
326    // Inside the piece's box, but outside a convex corner's arc.
327    let near = (cx - p.x) * sx < rc && (cy - p.y) * sy < rc;
328    let c = vec2<f32>(cx - sx * rc, cy - sy * rc);
329    return rc > 0.0 && near && distance(p, c) > rc;
330}
331
332fn join_fillet(p: vec2<f32>, cx: f32, cy: f32, sx: f32, sy: f32, rf: f32) -> bool {
333    // Past the piece's end, inside a concave corner's fillet.
334    let dx = (p.x - cx) * sx;
335    let dy = (cy - p.y) * sy;
336    let c = vec2<f32>(cx + sx * rf, cy - sy * rf);
337    return rf > 0.0 && dx >= 0.0 && dx < rf && dy >= 0.0 && dy < rf && distance(p, c) >= rf;
338}
339
340fn join_inside(p: vec2<f32>, h: f32, a: f32, b: f32, pv: vec2<f32>, hp: bool, nx: vec2<f32>, hn: bool, r: f32) -> bool {
341    let lone = min(r, (b - a) * 0.5);
342    let tl = join_radii(a, pv.x, hp, -1.0, r, lone);
343    let tr = join_radii(b, pv.y, hp, 1.0, r, lone);
344    let bl = join_radii(a, nx.x, hn, -1.0, r, lone);
345    let br = join_radii(b, nx.y, hn, 1.0, r, lone);
346    let in_box = p.x >= a && p.x < b && p.y >= 0.0 && p.y < h;
347    let cut = join_cut(p, a, 0.0, -1.0, -1.0, tl.x) || join_cut(p, b, 0.0, 1.0, -1.0, tr.x)
348        || join_cut(p, a, h, -1.0, 1.0, bl.x) || join_cut(p, b, h, 1.0, 1.0, br.x);
349    let fill = join_fillet(p, a, 0.0, -1.0, -1.0, tl.y) || join_fillet(p, b, 0.0, 1.0, -1.0, tr.y)
350        || join_fillet(p, a, h, -1.0, 1.0, bl.y) || join_fillet(p, b, h, 1.0, 1.0, br.y);
351    return (in_box && !cut) || fill;
352}
353
354fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
355    let a = params[0].x;
356    let b = params[0].y;
357    let pv = params[0].zw;
358    let nx = params[1].xy;
359    let h = in.size.y;
360    let r = min(params[1].z, h * 0.5);
361    let flags = u32(params[1].w + 0.5);
362    // A neighbour shapes the corners only where it overlaps this piece.
363    let hp = (flags & 1u) != 0u && pv.x < b && pv.y > a;
364    let hn = (flags & 2u) != 0u && nx.x < b && nx.y > a;
365    let x = in.local.x;
366    // Past the fillets nothing; away from both ends every pixel.
367    if x < a - r - 1.0 || x > b + r + 1.0 {
368        return vec4<f32>(0.0);
369    }
370    if x > a + r + 1.0 && x < b - r - 1.0 {
371        return vec4<f32>(in.color.rgb, 1.0);
372    }
373    var n = 0.0;
374    for (var i = 0; i < 4; i++) {
375        for (var j = 0; j < 4; j++) {
376            let o = vec2<f32>((f32(i) + 0.5) * 0.25 - 0.5, (f32(j) + 0.5) * 0.25 - 0.5);
377            if join_inside(in.local + o, h, a, b, pv, hp, nx, hn, r) {
378                n += 1.0;
379            }
380        }
381    }
382    return vec4<f32>(in.color.rgb, n / 16.0);
383}
384"#;
385
386/// The entry point [`EPILOGUE`] declares — what a backend names when it
387/// builds the pipeline.
388pub const ENTRY_POINT: &str = "kui_fs_fragment";
389
390/// How many vertices a `polygon` takes: one `vec2` per pair of the
391/// sixteen params.
392pub const POLYGON_MAX_POINTS: usize = 8;
393
394/// The stock fragment a `polygon` node paints with: up to eight vertices, one
395/// per `vec2` of the sixteen params, each
396/// normalised to the node's box — a polygon's box is its own bounding box
397/// inflated by a pixel, so the vertices span it — the last vertex
398/// repeated to pad, filled in `in.color`. The distance is the polygon
399/// SDF whose sign flips at every edge crossing — even-odd, the same rule
400/// the hit test uses — so a concave outline fills correctly and a
401/// self-intersecting one leaves its overlaps unfilled; a padding edge of
402/// zero length is skipped before it can divide by itself. The edge ramps over one
403/// physical pixel, like a box's. Registered by the core itself, once per
404/// session, through the same idempotent `add_fragment` an app's source
405/// takes, so `kui_fragment_source` hands a C host the function kui
406/// validated.
407pub const POLYGON: &str = r#"
408fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
409    var v: array<vec2<f32>, 8>;
410    for (var i = 0; i < 4; i++) {
411        v[i * 2] = params[i].xy * in.size;
412        v[i * 2 + 1] = params[i].zw * in.size;
413    }
414    let p = in.local;
415    var d = dot(p - v[0], p - v[0]);
416    var s = 1.0;
417    var j = 7;
418    for (var i = 0; i < 8; i++) {
419        let e = v[j] - v[i];
420        let w = p - v[i];
421        let ee = dot(e, e);
422        if ee > 0.0 {
423            let b = w - e * clamp(dot(w, e) / ee, 0.0, 1.0);
424            d = min(d, dot(b, b));
425            let c = vec3<bool>((p.y >= v[i].y), (p.y < v[j].y), (e.x * w.y > e.y * w.x));
426            if all(c) || all(!c) {
427                s = -s;
428            }
429        }
430        j = i;
431    }
432    let dist = s * sqrt(d);
433    let cov = clamp(0.5 - dist, 0.0, 1.0);
434    return vec4<f32>(in.color.rgb, cov);
435}
436"#;
437
438/// The whole module for an app's source: prelude, the app, epilogue. What
439/// the core validates and what a backend compiles, from one function so
440/// they cannot differ.
441pub fn module_source(app: &str) -> String {
442    format!("{PRELUDE}\n{app}\n{EPILOGUE}")
443}
444
445/// How far an error's line number has to move to land in the app's own
446/// numbering: the prelude's own lines, plus the blank one
447/// [`module_source`] puts between it and the app.
448fn prelude_lines() -> usize {
449    PRELUDE.lines().count() + 1
450}
451
452/// Parses and validates an app's fragment source between the prelude and
453/// the epilogue. `Err` carries naga's message with line numbers moved into
454/// the app's own numbering, so a caller can print it as-is.
455///
456/// This is 55-73 us for a typical source, which is why `add_fragment` is
457/// idempotent: a view that registers every frame should pay a comparison,
458/// not this.
459pub fn validate(app: &str) -> Result<(), String> {
460    use naga::valid::{Capabilities, ValidationFlags, Validator};
461    let full = module_source(app);
462    let module = naga::front::wgsl::parse_str(&full)
463        .map_err(|e| renumber(&e.emit_to_string(&full), prelude_lines()))?;
464    Validator::new(ValidationFlags::all(), Capabilities::empty())
465        .validate(&module)
466        .map(|_| ())
467        .map_err(|e| renumber(&e.emit_to_string(&full), prelude_lines()))
468}
469
470/// Rewrites `wgsl:N:` line references by subtracting the prelude's length,
471/// so an app sees its own line numbers. A reference inside the prelude or
472/// the epilogue clamps to 1 rather than going negative — an app cannot fix
473/// a line it did not write, and the message text still names what failed.
474fn renumber(msg: &str, offset: usize) -> String {
475    let mut out = String::with_capacity(msg.len());
476    let mut rest = msg;
477    while let Some(i) = rest.find("wgsl:") {
478        out.push_str(&rest[..i + 5]);
479        rest = &rest[i + 5..];
480        let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
481        if digits.is_empty() {
482            continue;
483        }
484        let n = digits.parse::<usize>().unwrap_or(offset + 1);
485        out.push_str(&n.saturating_sub(offset).max(1).to_string());
486        rest = &rest[digits.len()..];
487    }
488    out.push_str(rest);
489    out
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    const GRADIENT: &str = "\
497fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
498    let t = in.local.y / max(in.size.y, 1.0);
499    return mix(params[0], params[1], t);
500}";
501
502    #[test]
503    fn a_gradient_validates() {
504        validate(GRADIENT).unwrap();
505    }
506
507    /// The stock polygon is validated like any app source — at build,
508    /// here, rather than at the first `polygon` node of a session.
509    #[test]
510    fn the_stock_polygon_validates() {
511        validate(POLYGON).unwrap();
512    }
513
514    #[test]
515    fn the_stock_join_validates() {
516        validate(JOIN).unwrap();
517    }
518
519    /// `in.color` is what the prelude added for it.
520    #[test]
521    fn the_quad_colour_is_reachable_from_the_app() {
522        let src = "\
523fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
524    return vec4<f32>(in.color.rgb * params[0].x, 1.0);
525}";
526        validate(src).unwrap();
527    }
528
529    /// `kui_sample`, `kui_sample_nearest` and `in.image` are the image
530    /// input; a source using them validates without an image
531    /// bound, since binding is a per-frame fact.
532    #[test]
533    fn the_image_input_is_reachable_from_the_app() {
534        let src = "\
535fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
536    let uv = in.local / max(in.size, vec2<f32>(1.0));
537    let cells = in.image.zw;
538    return mix(kui_sample(uv), kui_sample_nearest(uv), step(1.0, cells.x)) * params[0];
539}";
540        validate(src).unwrap();
541    }
542
543    #[test]
544    fn the_prelude_is_reachable_from_the_app() {
545        // An app may use `kui_sd_rounded_box`, `KUI_AA`, `time` and `scale`.
546        let src = "\
547fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
548    let d = kui_sd_rounded_box(in.local - in.size * 0.5, in.size * 0.5, vec4<f32>(8.0));
549    let a = 1.0 - smoothstep(-KUI_AA, KUI_AA, d);
550    return vec4<f32>(params[0].rgb, a * (0.5 + 0.5 * sin(in.time)) * in.scale / in.scale);
551}";
552        validate(src).unwrap();
553    }
554
555    #[test]
556    fn a_syntax_error_reports_the_apps_own_line() {
557        // The missing semicolon is on the app's line 2.
558        let src = "\
559fn fragment(in: FragmentIn, params: array<vec4<f32>, 4>) -> vec4<f32> {
560    return vec4<f32>(1.0, 0.0, 0.0, 1.0)
561}";
562        let err = validate(src).unwrap_err();
563        assert!(
564            err.contains("wgsl:2:") || err.contains("wgsl:3:"),
565            "should point into the app's source, got:\n{err}"
566        );
567        assert!(
568            !err.contains(&format!("wgsl:{}:", prelude_lines() + 2)),
569            "line number was not moved out of the prelude:\n{err}"
570        );
571    }
572
573    #[test]
574    fn a_missing_fragment_function_is_refused() {
575        let err = validate("fn other() -> f32 { return 1.0; }").unwrap_err();
576        assert!(err.contains("fragment"), "{err}");
577    }
578
579    #[test]
580    fn the_wrong_signature_is_refused() {
581        let err = validate("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }").unwrap_err();
582        assert!(
583            !err.is_empty(),
584            "a no-argument `fragment` must not validate"
585        );
586    }
587
588    #[test]
589    fn an_empty_source_is_refused() {
590        assert!(validate("").is_err());
591    }
592
593    #[test]
594    fn the_module_is_the_three_parts_in_order() {
595        let m = module_source(GRADIENT);
596        let (p, a, e) = (
597            m.find("struct FragmentIn").unwrap(),
598            m.find("fn fragment(").unwrap(),
599            m.find(ENTRY_POINT).unwrap(),
600        );
601        assert!(p < a && a < e, "prelude, app, epilogue");
602        assert!(m.contains(ENTRY_POINT));
603    }
604}