Skip to main content

frust_engine/gpu/
shader_src.rs

1//! The engine's WGSL sources, assembled at compile time.
2//!
3//! Every shader lives in `crates/frust-engine/shaders/` as its own `.wgsl`
4//! file and reaches Rust through `include_str!` — never as an inline string
5//! literal — so the downlevel lint that scans that directory
6//! (`frust_gpu::lint::lint_wgsl_dir`, driven by `frust-gpu`'s
7//! `downlevel_rules` test) sees every line the GPU ever compiles.
8//!
9//! ## The helper prelude
10//!
11//! The sources are ported from a WESL reference whose entry-point modules
12//! pull shared code in with `import package::helpers::...`. WESL's own
13//! resolver is not available here — its crate family needs a newer toolchain
14//! than this workspace's MSRV — so the import graph is resolved two ways:
15//! within [`HELPERS`] by hand-inlining, and between an entry-point module and
16//! its helpers by `concat!`, exactly the shape `frust-render`'s shader-effect
17//! `VERTEX_PRELUDE` uses.
18//!
19//! Prepending the whole helper file to every entry-point module is safe
20//! because [`HELPERS`] declares no `@group`/`@binding` global: a helper that
21//! reads a texture takes it as a parameter. An entry-point module's derived
22//! bind-group layout is therefore exactly what its own bindings say, with or
23//! without the prelude. [`FILTER_KERNELS`] is a second prelude on the same
24//! terms, prepended to [`FILTER`] alone.
25//!
26//! Every module the engine compiles is assembled here and nowhere else:
27//! [`MODULES`] lists the four fixed ones, and [`strip_class_source`] derives
28//! the strip program's paint-class specialisations from [`STRIP`] — together
29//! the whole list [`crate::gpu::pipelines::EngineShaders::register`] walks,
30//! and the one the validation tests below walk.
31//!
32//! ## The paint-class pre-pass
33//!
34//! The strip program is compiled once per [`PaintClass`] from the ONE source,
35//! by a line-oriented string pass ([`specialise`]) rather than a second copy of
36//! the shader: a block between a `//#if FEATURE` line and its `//#endif` line
37//! survives only in a class that enables `FEATURE` ([`class_features`]), and a
38//! `//#else` line inside it opens the arm that survives exactly when the `//#if`
39//! arm does not. The directives are ordinary WGSL comments, which keeps the
40//! downlevel lint (which scans `shaders/`) reading the real source.
41//!
42//! ## The two program families
43//!
44//! [`IMAGE_SAMPLER`] is not a class feature but a family switch, settled per
45//! renderer (`FRUST_ENGINE_IMAGE_SAMPLER`, [`crate::config::image_sampler_enabled`]):
46//! with it, every program that keeps `IMAGE` — [`PaintClass::Image`],
47//! [`PaintClass::ImageFiltered`] and [`PaintClass::Uber`] — reads a bilinear
48//! atlas image through one hardware-filtered `textureSampleLevel`; without it
49//! they keep the manual four-`textureLoad` path in the feature's `//#else` arm.
50//! The sampler family is the default; the switch's `0` is the kill switch
51//! that selects the manual one. A renderer compiles one family, so the switch
52//! adds no pipeline variant ([`strip_program_source`]). The manual family's
53//! [`PaintClass::Uber`]
54//! program is [`STRIP`] with only the `IMAGE_SAMPLER` arm blanked out — line
55//! for line the program the engine shipped before either axis existed.
56//!
57//! A removed line is replaced by an empty one rather than dropped, so a naga
58//! diagnostic in a specialised module names the same line number as the source
59//! file. The pass emits only lines the source already contains, and the blocks
60//! are placed so that dropping any combination of them leaves well-formed WGSL
61//! (each block is a whole `else if` arm), so a specialisation uses no construct
62//! the Uber program does not — the property the WebGL2 arm's naga GLSL-ES
63//! lowering depends on. Overrides (`override` constants) were the alternative
64//! and are deliberately not used: no pipeline here sets one, and the pre-pass
65//! needs no new pipeline-description axis to carry them.
66
67use std::borrow::Cow;
68
69use super::strips::PaintClass;
70
71/// Binding-free helper functions shared by every entry-point module:
72/// packing, quad geometry, extend modes, encoded-paint accessors, gradient
73/// and blur evaluation, and atlas/external image sampling.
74///
75/// Not a shader in its own right — it has no entry point. It is prepended to
76/// each of [`STRIP`], [`CLEAR`] and [`COPY`].
77pub const HELPERS: &str = include_str!("../../shaders/helpers.wgsl");
78
79/// The sparse-strip rasterizer: `vs_main` + `fs_main`, six render-state
80/// variants (see [`crate::gpu::pipelines`]) — and, unchanged, the
81/// [`PaintClass::Uber`] program every class specialisation is cut from.
82pub const STRIP: &str = concat!(
83    include_str!("../../shaders/helpers.wgsl"),
84    include_str!("../../shaders/strip.wgsl")
85);
86
87/// Region and fullscreen clears of an intermediate texture: `vs_main` /
88/// `vs_main_fullscreen` + `fs_main`.
89pub const CLEAR: &str = concat!(
90    include_str!("../../shaders/helpers.wgsl"),
91    include_str!("../../shaders/clear.wgsl")
92);
93
94/// Rectangular region copies between textures: `vs_main` + `fs_main`. Drawn
95/// by a backdrop blur's region copy (a region of the frame's own target into a
96/// pooled page — see `crate::renderer::RegionCopyMode`) and, built for a
97/// swapchain's format, by the host's plain present copy
98/// ([`crate::gpu::present::CopyPresentPass`]).
99pub const COPY: &str = concat!(
100    include_str!("../../shaders/helpers.wgsl"),
101    include_str!("../../shaders/copy.wgsl")
102);
103
104/// The Gaussian-blur kernels [`FILTER`]'s fragment stage dispatches into: the
105/// two rescaling steps a decimated blur is built from, and the separable
106/// convolution run between them.
107///
108/// A second prelude rather than a module of its own: like [`HELPERS`] it
109/// declares no entry point and no `@group`/`@binding` global — a kernel that
110/// reads a texture takes the texture and the sampler as parameters — so
111/// prepending it cannot disturb [`FILTER`]'s derived bind-group layout.
112pub const FILTER_KERNELS: &str = include_str!("../../shaders/filters_blur.wgsl");
113
114/// The drop-shadow passes [`FILTER`]'s fragment stage dispatches into beyond
115/// what [`FILTER_KERNELS`] already covers: the shadow's own device-space
116/// shift, and the recolour from a blurred alpha mask into the shadow's
117/// premultiplied colour.
118///
119/// A third prelude on the same terms as [`FILTER_KERNELS`]: no entry point, no
120/// `@group`/`@binding` global, so prepending it cannot disturb [`FILTER`]'s
121/// derived bind-group layout either. The layouts and constants it reads are
122/// [`crate::filters::drop_shadow`]'s; that module's own tests pin the two
123/// sides against each other.
124pub const DROP_SHADOW_KERNELS: &str = include_str!("../../shaders/filters_drop_shadow.wgsl");
125
126/// One filter pass over one destination page: `vs_main` + `fs_main`, one
127/// pipeline (see [`crate::gpu::pipelines::EnginePipeline::Filter`]).
128///
129/// The only module assembled from more than one prelude: the binding-free
130/// helpers every module gets, then [`FILTER_KERNELS`], then
131/// [`DROP_SHADOW_KERNELS`], then the entry-point module that declares the
132/// bindings and dispatches on the pass kind. The layouts and constants it
133/// reads are [`crate::filters::blur`]'s and [`crate::filters::drop_shadow`]'s;
134/// those modules' own tests pin all three sides against each other.
135pub const FILTER: &str = concat!(
136    include_str!("../../shaders/helpers.wgsl"),
137    include_str!("../../shaders/filters_blur.wgsl"),
138    include_str!("../../shaders/filters_drop_shadow.wgsl"),
139    include_str!("../../shaders/filter.wgsl")
140);
141
142/// The name [`STRIP`] is registered under in the shader library.
143pub const STRIP_NAME: &str = "frust-engine strip";
144
145/// The class feature names a `//#if` directive in `strip.wgsl` may name —
146/// [`IMAGE_SAMPLER`], the family switch, is the only other one.
147///
148/// - `IMAGE` — atlas image sampling at nearest and bilinear quality;
149/// - `IMAGE_FILTERED` — nested inside `IMAGE`: bicubic atlas sampling and the
150///   external texture;
151/// - `GRADIENT` — the linear, radial and sweep gradients;
152/// - `BLURRED_RECT` — the blurred rounded rectangle;
153/// - `LAYER` — the layer colour source a composite samples its page through;
154/// - `GLYPH` — the glyph quad: one nearest atlas load at the texel the
155///   instance's payload names. Not nested inside `IMAGE` and never reading the
156///   sampler, so it compiles identically in both program families.
157pub const STRIP_FEATURES: [&str; 6] = [
158    "IMAGE",
159    "IMAGE_FILTERED",
160    "GRADIENT",
161    "BLURRED_RECT",
162    "LAYER",
163    "GLYPH",
164];
165
166/// The family feature: bilinear atlas images through a hardware sampler (see
167/// the module doc's *The two program families*). Kept by the image programs
168/// of the sampler family only, never by a class on its own.
169pub const IMAGE_SAMPLER: &str = "IMAGE_SAMPLER";
170
171/// The `//#if` class features `class` keeps.
172///
173/// Coverage, the solid paint and the vertex stage sit outside every block, so
174/// they are in every class; [`PaintClass::Uber`] keeps every feature.
175#[must_use]
176pub const fn class_features(class: PaintClass) -> &'static [&'static str] {
177    match class {
178        PaintClass::SolidCoverage => &[],
179        PaintClass::Gradient => &["GRADIENT"],
180        PaintClass::Image => &["IMAGE"],
181        PaintClass::ImageFiltered => &["IMAGE", "IMAGE_FILTERED"],
182        PaintClass::BlurredRect => &["BLURRED_RECT"],
183        PaintClass::LayerComposite => &["LAYER"],
184        PaintClass::Glyph => &["GLYPH"],
185        PaintClass::Uber => &STRIP_FEATURES,
186    }
187}
188
189/// The name the strip program specialised for `class` is registered under in
190/// the shader library — [`STRIP_NAME`] itself for [`PaintClass::Uber`].
191#[must_use]
192pub const fn strip_class_name(class: PaintClass) -> &'static str {
193    match class {
194        PaintClass::SolidCoverage => "frust-engine strip/solid",
195        PaintClass::Gradient => "frust-engine strip/gradient",
196        PaintClass::Image => "frust-engine strip/image",
197        PaintClass::ImageFiltered => "frust-engine strip/image-filtered",
198        PaintClass::BlurredRect => "frust-engine strip/blurred-rect",
199        PaintClass::LayerComposite => "frust-engine strip/layer",
200        PaintClass::Glyph => "frust-engine strip/glyph",
201        PaintClass::Uber => STRIP_NAME,
202    }
203}
204
205/// The strip program of the manual family specialised for `class`:
206/// [`STRIP`] with every `//#if` block whose feature `class` does not keep, and
207/// the [`IMAGE_SAMPLER`] arm, blanked out.
208///
209/// [`PaintClass::Uber`] keeps every class feature, so the kill switch
210/// (`FRUST_ENGINE_UBER_PROGRAM`) compiles [`STRIP`] with only the sampler arm
211/// and the directive lines blanked — line for line the program the engine
212/// shipped before the classes existed.
213#[must_use]
214pub fn strip_class_source(class: PaintClass) -> Cow<'static, str> {
215    strip_program_source(class, false)
216}
217
218/// Whether `class`'s program reads atlas images at all, and so whether the
219/// sampler family specialises it with [`IMAGE_SAMPLER`].
220#[must_use]
221pub fn samples_images(class: PaintClass) -> bool {
222    class_features(class).contains(&"IMAGE")
223}
224
225/// The features `class`'s program keeps in the family `image_sampler` names:
226/// [`class_features`], plus [`IMAGE_SAMPLER`] for an image program of the
227/// sampler family.
228#[must_use]
229pub fn strip_program_features(class: PaintClass, image_sampler: bool) -> Vec<&'static str> {
230    let mut features = class_features(class).to_vec();
231    if image_sampler && samples_images(class) {
232        features.push(IMAGE_SAMPLER);
233    }
234    features
235}
236
237/// The strip program for `class` in the family `image_sampler` names: [`STRIP`]
238/// with every block [`strip_program_features`] does not keep blanked out.
239///
240/// The two families differ only in the programs that keep `IMAGE`; every other
241/// class compiles the same source in both.
242#[must_use]
243pub fn strip_program_source(class: PaintClass, image_sampler: bool) -> Cow<'static, str> {
244    Cow::Owned(specialise(
245        STRIP,
246        &strip_program_features(class, image_sampler),
247    ))
248}
249
250/// The shader-library name of `class`'s program in the family `image_sampler`
251/// names — [`strip_class_name`] for every program the families share, with a
252/// suffix on the sampler family's image programs so a library never holds two
253/// different sources under one name.
254#[must_use]
255pub const fn strip_program_name(class: PaintClass, image_sampler: bool) -> &'static str {
256    if !image_sampler {
257        return strip_class_name(class);
258    }
259    match class {
260        PaintClass::Image => "frust-engine strip/image+sampler",
261        PaintClass::ImageFiltered => "frust-engine strip/image-filtered+sampler",
262        PaintClass::Uber => "frust-engine strip+sampler",
263        _ => strip_class_name(class),
264    }
265}
266
267/// The directive opening a feature block, followed by one feature name.
268const DIRECTIVE_IF: &str = "//#if ";
269
270/// The directive closing the innermost open feature block.
271const DIRECTIVE_ENDIF: &str = "//#endif";
272
273/// The directive switching the innermost open feature block to its other arm.
274const DIRECTIVE_ELSE: &str = "//#else";
275
276/// Runs the feature pre-pass over `source`, keeping the blocks whose feature
277/// is in `enabled`.
278///
279/// Line-oriented and total: a directive is a line whose trimmed text starts
280/// with `//#if ` (one feature name after it) or is exactly `//#else` or
281/// `//#endif`; blocks nest, a `//#else` inverts its block for the lines after
282/// it, and a line is kept only while every enclosing block is live. Each
283/// directive line, and each line of a dead arm, becomes an empty line, so the
284/// output has exactly the source's line count. An `//#else` or `//#endif` with
285/// no open block is ignored and an unclosed block runs to the end of the
286/// source; neither occurs in the shipped shaders, which the tests below pin.
287#[must_use]
288pub fn specialise(source: &str, enabled: &[&str]) -> String {
289    let mut out = String::with_capacity(source.len());
290    // Whether each open block is live, innermost last; a line is kept only
291    // when every entry is.
292    let mut open: Vec<bool> = Vec::new();
293    for line in source.split_inclusive('\n') {
294        let trimmed = line.trim();
295        let newline = if line.ends_with('\n') { "\n" } else { "" };
296        if let Some(feature) = trimmed.strip_prefix(DIRECTIVE_IF) {
297            let feature = feature.trim();
298            open.push(enabled.contains(&feature));
299            out.push_str(newline);
300        } else if trimmed == DIRECTIVE_ELSE {
301            if let Some(live) = open.last_mut() {
302                *live = !*live;
303            }
304            out.push_str(newline);
305        } else if trimmed == DIRECTIVE_ENDIF {
306            open.pop();
307            out.push_str(newline);
308        } else if open.iter().all(|live| *live) {
309            out.push_str(line);
310        } else {
311            out.push_str(newline);
312        }
313    }
314    out
315}
316
317/// The name [`CLEAR`] is registered under in the shader library.
318pub const CLEAR_NAME: &str = "frust-engine clear";
319
320/// The name [`COPY`] is registered under in the shader library.
321pub const COPY_NAME: &str = "frust-engine copy";
322
323/// The name [`FILTER`] is registered under in the shader library.
324pub const FILTER_NAME: &str = "frust-engine filter";
325
326/// Every module source the engine assembles at compile time, as
327/// `(name, source)` pairs — the list the validation tests walk.
328///
329/// [`STRIP`] is listed as written, both [`IMAGE_SAMPLER`] arms present, which
330/// is valid WGSL in its own right; what a renderer compiles from it are the
331/// specialisations [`strip_program_source`] derives at start-up, the Uber
332/// program among them.
333pub const MODULES: [(&str, &str); 4] = [
334    (STRIP_NAME, STRIP),
335    (CLEAR_NAME, CLEAR),
336    (COPY_NAME, COPY),
337    (FILTER_NAME, FILTER),
338];
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::gpu::{GpuConfig, GpuStrip};
344    use wgpu::naga;
345
346    /// Parses and validates `src` the way a `wgpu::Device` would, returning
347    /// the validated module.
348    ///
349    /// `wgpu` re-exports the same `naga` its own WGSL front end uses, so this
350    /// is the device's validation path minus the device: no GPU, no adapter,
351    /// and a failure surfaces here as a test failure instead of as a
352    /// pipeline-creation error at run time. The device-backed counterpart
353    /// (`gpu::pipelines`' real-pipeline test) additionally proves the
354    /// *pipelines* are accepted, but it needs hardware and is `#[ignore]`d.
355    fn validate(name: &str, src: &str) -> naga::Module {
356        let module = naga::front::wgsl::parse_str(src)
357            .unwrap_or_else(|err| panic!("{name} failed to parse: {err:?}"));
358        naga::valid::Validator::new(
359            naga::valid::ValidationFlags::all(),
360            naga::valid::Capabilities::empty(),
361        )
362        .validate(&module)
363        .unwrap_or_else(|err| panic!("{name} failed validation: {err:?}"));
364        module
365    }
366
367    /// The byte size naga computed for the struct named `name`.
368    fn struct_span(module: &naga::Module, name: &str) -> u32 {
369        module
370            .types
371            .iter()
372            .find_map(|(_, ty)| match (&ty.name, &ty.inner) {
373                (Some(ty_name), naga::TypeInner::Struct { span, .. }) if ty_name == name => {
374                    Some(*span)
375                }
376                _ => None,
377            })
378            .unwrap_or_else(|| panic!("the module declares no struct named {name}"))
379    }
380
381    #[test]
382    fn every_module_parses_and_validates() {
383        for (name, src) in MODULES {
384            validate(name, src);
385        }
386        for image_sampler in [false, true] {
387            for class in PaintClass::ALL {
388                validate(
389                    strip_program_name(class, image_sampler),
390                    &strip_program_source(class, image_sampler),
391                );
392            }
393        }
394    }
395
396    /// Whether `line` is a pre-pass directive.
397    fn is_directive(line: &str) -> bool {
398        let trimmed = line.trim();
399        trimmed.starts_with(DIRECTIVE_IF) || trimmed == DIRECTIVE_ELSE || trimmed == DIRECTIVE_ENDIF
400    }
401
402    /// The kill switch's whole promise: the manual family's Uber class
403    /// compiles the shipped source, not a reconstruction of it — every line of
404    /// [`STRIP`] kept in place, save the directives and the sampler family's
405    /// own arm, which are blanked rather than dropped.
406    #[test]
407    fn the_manual_uber_program_is_the_strip_source_without_the_sampler_arm() {
408        let uber = strip_class_source(PaintClass::Uber);
409        let sampled = strip_program_source(PaintClass::Uber, true);
410        assert_eq!(strip_class_name(PaintClass::Uber), STRIP_NAME);
411        assert_eq!(uber.lines().count(), STRIP.lines().count());
412        let mut blanked = 0;
413        for ((source, kept), sampled) in STRIP.lines().zip(uber.lines()).zip(sampled.lines()) {
414            if kept == source {
415                continue;
416            }
417            assert!(kept.is_empty(), "a kept line was rewritten: `{source}`");
418            if is_directive(source) || source.trim().is_empty() {
419                continue;
420            }
421            assert_eq!(
422                sampled, source,
423                "the manual Uber program blanked `{source}`, which is not the sampler arm"
424            );
425            blanked += 1;
426        }
427        assert!(blanked > 0, "the sampler arm is in the source");
428        assert!(!uber.contains("atlas_sampler"));
429        assert!(!uber.contains("textureSampleLevel("));
430        assert!(sampled.contains("var atlas_sampler: sampler;"));
431    }
432
433    /// The families differ exactly where the switch says: in the programs
434    /// that read atlas images, and nowhere else.
435    #[test]
436    fn the_families_differ_only_in_the_image_programs() {
437        for class in PaintClass::ALL {
438            let default = strip_program_source(class, false);
439            let sampled = strip_program_source(class, true);
440            assert_eq!(
441                default == sampled,
442                !samples_images(class),
443                "{}",
444                class.label()
445            );
446            assert_eq!(
447                strip_program_name(class, false) == strip_program_name(class, true),
448                !samples_images(class),
449                "{}",
450                class.label()
451            );
452            assert_eq!(strip_program_name(class, false), strip_class_name(class));
453            assert_eq!(default, strip_class_source(class));
454        }
455        assert!(samples_images(PaintClass::Image));
456        assert!(samples_images(PaintClass::ImageFiltered));
457        assert!(samples_images(PaintClass::Uber));
458        // The sampler arm replaces the manual one: neither family keeps both.
459        for class in [
460            PaintClass::Image,
461            PaintClass::ImageFiltered,
462            PaintClass::Uber,
463        ] {
464            let default = strip_program_source(class, false);
465            let sampled = strip_program_source(class, true);
466            let fs =
467                |src: &str| src[src.find("fn fs_main(").expect("a fragment stage")..].to_string();
468            assert!(fs(&default).contains("sample_color = bilinear_sample("));
469            assert!(!fs(&default).contains("sampled_bilinear("));
470            assert!(fs(&sampled).contains("sample_color = sampled_bilinear("));
471            assert!(!fs(&sampled).contains("bilinear_sample("));
472        }
473    }
474
475    #[test]
476    fn every_directive_names_a_known_feature_and_every_block_closes() {
477        let mut depth = 0_usize;
478        let mut seen = Vec::new();
479        for (number, line) in STRIP.lines().enumerate() {
480            let trimmed = line.trim();
481            if let Some(feature) = trimmed.strip_prefix(DIRECTIVE_IF) {
482                assert!(
483                    STRIP_FEATURES.contains(&feature.trim()) || feature.trim() == IMAGE_SAMPLER,
484                    "line {number} names an unknown feature `{feature}`"
485                );
486                seen.push(feature.trim().to_string());
487                depth += 1;
488            } else if trimmed == DIRECTIVE_ELSE {
489                assert!(
490                    depth > 0,
491                    "line {number} switches a block that was never opened"
492                );
493            } else if trimmed == DIRECTIVE_ENDIF {
494                assert!(
495                    depth > 0,
496                    "line {number} closes a block that was never opened"
497                );
498                depth -= 1;
499            } else {
500                assert!(
501                    !trimmed.starts_with("//#"),
502                    "line {number} looks like a directive but is not one: `{trimmed}`"
503                );
504            }
505        }
506        assert_eq!(
507            depth, 0,
508            "a feature block is left open at the end of the source"
509        );
510        for feature in STRIP_FEATURES.into_iter().chain([IMAGE_SAMPLER]) {
511            assert!(
512                seen.iter().any(|f| f == feature),
513                "feature {feature} gates nothing — a class enabling it would equal one that does not"
514            );
515        }
516    }
517
518    #[test]
519    fn the_pre_pass_keeps_line_numbers_and_only_drops_disabled_blocks() {
520        let src = "a\n//#if X\nb\n//#if Y\nc\n//#endif\n//#endif\nd";
521        assert_eq!(specialise(src, &["X", "Y"]), "a\n\nb\n\nc\n\n\nd");
522        assert_eq!(specialise(src, &["X"]), "a\n\nb\n\n\n\n\nd");
523        assert_eq!(specialise(src, &["Y"]), "a\n\n\n\n\n\n\nd");
524        assert_eq!(specialise(src, &[]), "a\n\n\n\n\n\n\nd");
525        // An `//#else` arm is live exactly when its `//#if` arm is not, and
526        // only while every enclosing block is.
527        let src = "a\n//#if X\nb\n//#if Y\nc\n//#else\ne\n//#endif\n//#endif\nd";
528        assert_eq!(specialise(src, &["X", "Y"]), "a\n\nb\n\nc\n\n\n\n\nd");
529        assert_eq!(specialise(src, &["X"]), "a\n\nb\n\n\n\ne\n\n\nd");
530        assert_eq!(specialise(src, &["Y"]), "a\n\n\n\n\n\n\n\n\nd");
531        for image_sampler in [false, true] {
532            for class in PaintClass::ALL {
533                assert_eq!(
534                    strip_program_source(class, image_sampler).lines().count(),
535                    STRIP.lines().count(),
536                    "{} must keep the source's line numbering",
537                    class.label()
538                );
539            }
540        }
541    }
542
543    /// Every class keeps exactly its own colour-source branches: the fragment
544    /// entry point of each specialisation names only the paint types the
545    /// class owns, and the solid paint and both coverage paths in all of them.
546    #[test]
547    fn each_class_keeps_only_its_own_branches() {
548        let branch = |src: &str, needle: &str| src.contains(needle);
549        let image = "paint_type == PAINT_TYPE_IMAGE {";
550        let bicubic = "sample_color = bicubic_sample(";
551        let external = "sample_color = sample_external_image(";
552        let bilinear = "sample_color = bilinear_sample(";
553        let linear = "paint_type == PAINT_TYPE_LINEAR_GRADIENT {";
554        let radial = "paint_type == PAINT_TYPE_RADIAL_GRADIENT {";
555        let sweep = "paint_type == PAINT_TYPE_SWEEP_GRADIENT {";
556        let blurred = "paint_type == PAINT_TYPE_BLURRED_ROUNDED_RECT {";
557        let layer = "} else if color_source == COLOR_SOURCE_LAYER {\n        let layer_opacity";
558        for class in PaintClass::ALL {
559            let src = strip_class_source(class);
560            let fs = &src[src.find("fn fs_main(").expect("a fragment entry point")..];
561            let keeps = |feature: &str| class_features(class).contains(&feature);
562            assert!(
563                branch(fs, "paint_type == PAINT_TYPE_SOLID {"),
564                "{}",
565                class.label()
566            );
567            assert!(
568                branch(fs, "if is_rect && rect_frac != 0u {"),
569                "{}",
570                class.label()
571            );
572            assert_eq!(branch(fs, image), keeps("IMAGE"), "{}", class.label());
573            assert_eq!(branch(fs, bilinear), keeps("IMAGE"), "{}", class.label());
574            for filtered in [bicubic, external] {
575                assert_eq!(
576                    branch(fs, filtered),
577                    keeps("IMAGE_FILTERED"),
578                    "{}",
579                    class.label()
580                );
581            }
582            for gradient in [linear, radial, sweep] {
583                assert_eq!(branch(fs, gradient), keeps("GRADIENT"), "{}", class.label());
584            }
585            assert_eq!(
586                branch(fs, blurred),
587                keeps("BLURRED_RECT"),
588                "{}",
589                class.label()
590            );
591            assert_eq!(branch(&src, layer), keeps("LAYER"), "{}", class.label());
592        }
593    }
594
595    /// candB: the strip program addresses its alpha and LUT textures by the
596    /// `Config` shifts alone — no per-fragment `textureDimensions` query and no
597    /// integer `%`/`/` against a queried width (see `gpu::config`'s guard).
598    /// The one query in the source is the sampler family's normalisation of an
599    /// atlas sample position, which no manual-family program keeps.
600    #[test]
601    fn the_strip_program_addresses_its_resource_textures_by_config_shifts() {
602        assert_eq!(STRIP.matches("textureDimensions(").count(), 1);
603        assert!(STRIP.contains("vec2<f32>(textureDimensions(tex))"));
604        for class in PaintClass::ALL {
605            assert!(!strip_class_source(class).contains("textureDimensions("));
606            let sampled = strip_program_source(class, true);
607            assert_eq!(
608                sampled.contains("textureDimensions("),
609                samples_images(class),
610                "{}",
611                class.label()
612            );
613        }
614        assert!(STRIP.contains("(1u << config.alphas_tex_width_bits) - 1u"));
615        assert_eq!(
616            STRIP
617                .matches(
618                    "get_gradient_texture_width(gradient_texel0),\n                \
619                     config.encoded_paints_tex_width_bits\n"
620                )
621                .count(),
622            3,
623            "each gradient kind passes the LUT width shift"
624        );
625        assert!(HELPERS.contains("flat_coord & ((1u << lut_width_bits) - 1u)"));
626        assert!(HELPERS.contains("flat_coord >> lut_width_bits"));
627    }
628
629    /// The glyph program keeps the glyph quad's one atlas load and nothing of
630    /// the image paths — and, holding no `IMAGE` feature, is the same program in
631    /// both families.
632    #[test]
633    fn the_glyph_class_keeps_only_the_glyph_branch() {
634        let glyph_arm = "paint_type == PAINT_TYPE_GLYPH {";
635        for image_sampler in [false, true] {
636            let src = strip_program_source(PaintClass::Glyph, image_sampler);
637            let fs = &src[src.find("fn fs_main(").expect("a fragment entry point")..];
638            assert!(fs.contains(glyph_arm));
639            assert!(!fs.contains("paint_type == PAINT_TYPE_IMAGE {"));
640            assert!(!fs.contains("sampled_bilinear("));
641            assert!(!fs.contains("bilinear_sample("));
642            assert!(!src.contains("var atlas_sampler: sampler;"));
643            assert!(fs.contains("paint_type == PAINT_TYPE_SOLID {"));
644        }
645        assert_eq!(
646            strip_program_source(PaintClass::Glyph, false),
647            strip_program_source(PaintClass::Glyph, true)
648        );
649        for class in PaintClass::ALL {
650            let src = strip_class_source(class);
651            let fs = &src[src.find("fn fs_main(").expect("a fragment entry point")..];
652            assert_eq!(
653                fs.contains(glyph_arm),
654                class_features(class).contains(&"GLYPH"),
655                "{}",
656                class.label()
657            );
658        }
659        assert!(strip_class_source(PaintClass::Uber).contains(glyph_arm));
660    }
661
662    #[test]
663    fn the_helper_prelude_validates_on_its_own() {
664        // It has no entry point, so nothing else would catch a syntax error
665        // in a helper that no current entry point happens to call.
666        validate("helpers", HELPERS);
667    }
668
669    #[test]
670    fn every_module_is_the_helper_prelude_followed_by_its_own_source() {
671        for (name, src) in MODULES {
672            assert!(
673                src.starts_with(HELPERS),
674                "{name} must be assembled as the helper prelude followed by its own source"
675            );
676            assert!(
677                src.len() > HELPERS.len(),
678                "{name} must contribute source of its own"
679            );
680        }
681    }
682
683    #[test]
684    fn the_helper_prelude_declares_no_bindings() {
685        // The whole reason every entry-point module can be prefixed with it
686        // without disturbing its derived bind-group layout. Comment lines are
687        // skipped: this file's own header describes the rule. The blur
688        // kernels are held to the same rule for the same reason — they are a
689        // prelude too, just one only the filter module gets.
690        for (name, prelude) in [
691            ("helpers", HELPERS),
692            ("filter kernels", FILTER_KERNELS),
693            ("drop shadow kernels", DROP_SHADOW_KERNELS),
694        ] {
695            let declaration = prelude
696                .lines()
697                .find(|line| !line.trim_start().starts_with("//") && line.contains("@group"));
698            assert!(
699                declaration.is_none(),
700                "a prelude that declared a binding would change the bind-group layout of every \
701                 module it is prepended to ({name}): {declaration:?}"
702            );
703        }
704    }
705
706    #[test]
707    fn the_filter_module_is_every_prelude_followed_by_its_own_source() {
708        // `every_module_is_the_helper_prelude_followed_by_its_own_source`
709        // above only pins the first prelude; the filter program is the one
710        // module assembled from three, and each has to sit between the helpers
711        // it calls and the entry point that calls it.
712        assert!(FILTER.starts_with(HELPERS));
713        assert!(FILTER[HELPERS.len()..].starts_with(FILTER_KERNELS));
714        assert!(FILTER[HELPERS.len() + FILTER_KERNELS.len()..].starts_with(DROP_SHADOW_KERNELS));
715        assert!(FILTER.len() > HELPERS.len() + FILTER_KERNELS.len() + DROP_SHADOW_KERNELS.len());
716    }
717
718    #[test]
719    fn the_filter_instance_matches_its_rust_layout() {
720        let module = validate(FILTER_NAME, FILTER);
721        assert_eq!(
722            struct_span(&module, "FilterInstanceData") as usize,
723            size_of::<crate::filters::blur::FilterInstanceData>(),
724            "the shader's `FilterInstanceData` must stay byte-identical to the Rust one"
725        );
726    }
727
728    #[test]
729    fn the_strip_config_uniform_matches_its_rust_layout() {
730        let module = validate(STRIP_NAME, STRIP);
731        assert_eq!(
732            u64::from(struct_span(&module, "Config")),
733            GpuConfig::SIZE,
734            "the shader's `Config` must stay byte-identical to `GpuConfig`"
735        );
736    }
737
738    #[test]
739    fn the_strip_instance_matches_its_rust_layout() {
740        let module = validate(STRIP_NAME, STRIP);
741        assert_eq!(
742            struct_span(&module, "StripInstance") as usize,
743            size_of::<GpuStrip>(),
744            "the shader's `StripInstance` must stay byte-identical to `GpuStrip`"
745        );
746    }
747}