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}