Skip to main content

azul_core/
gl_fxaa.rs

1//! FXAA (Fast Approximate Anti-Aliasing) shader implementation.
2//!
3//! Post-processing AA that detects edges via luminance and selectively blurs them.
4//! Faster than supersampling and works without hardware MSAA support.
5//!
6//! Shader compilation: `GlContextPtrInner.fxaa_shader` (see `core/src/gl.rs`).
7//! FXAA pass: `apply_fxaa` / `apply_fxaa_with_config` (see `layout/src/xml/svg.rs`).
8//!
9//! Presets: `FxaaConfig::enabled()`, `::high_quality()`, `::balanced()`, `::performance()`
10
11/// FXAA shader configuration
12#[derive(Debug, Clone, Copy)]
13pub struct FxaaConfig {
14    /// Enable/disable FXAA
15    pub enabled: bool,
16    /// Edge detection threshold (0.063 - 0.333, default: 0.125)
17    /// Lower = more edges detected = more AA but potential blur
18    pub edge_threshold: f32,
19    /// Minimum edge threshold (0.0312 - 0.0833, default: 0.0312)
20    /// Prevents AA on very low contrast edges
21    pub edge_threshold_min: f32,
22}
23
24impl Default for FxaaConfig {
25    fn default() -> Self {
26        Self {
27            enabled: false, // Disabled by default for performance
28            edge_threshold: 0.125,
29            edge_threshold_min: 0.0312,
30        }
31    }
32}
33
34impl FxaaConfig {
35    /// Create config with FXAA enabled and default quality settings
36    #[must_use] pub fn enabled() -> Self {
37        Self {
38            enabled: true,
39            ..Default::default()
40        }
41    }
42
43    /// High quality preset - more aggressive edge detection
44    #[must_use] pub const fn high_quality() -> Self {
45        Self {
46            enabled: true,
47            edge_threshold: 0.063,
48            edge_threshold_min: 0.0312,
49        }
50    }
51
52    /// Balanced preset - default settings
53    #[must_use] pub const fn balanced() -> Self {
54        Self {
55            enabled: true,
56            edge_threshold: 0.125,
57            edge_threshold_min: 0.0312,
58        }
59    }
60
61    /// Performance preset - less aggressive, faster
62    #[must_use] pub const fn performance() -> Self {
63        Self {
64            enabled: true,
65            edge_threshold: 0.25,
66            edge_threshold_min: 0.0625,
67        }
68    }
69}
70
71/// FXAA vertex shader - simple fullscreen quad pass-through
72pub static FXAA_VERTEX_SHADER: &[u8] = b"#version 150
73
74#if __VERSION__ != 100
75    #define varying out
76    #define attribute in
77#endif
78
79attribute vec2 vAttrXY;
80varying vec2 vTexCoord;
81
82void main() {
83    vTexCoord = vAttrXY * 0.5 + 0.5; // Convert from [-1,1] to [0,1]
84    gl_Position = vec4(vAttrXY, 0.0, 1.0);
85}
86";
87
88/// FXAA fragment shader - implements edge-based anti-aliasing
89pub static FXAA_FRAGMENT_SHADER: &[u8] = b"#version 150
90
91precision highp float;
92
93#if __VERSION__ == 100
94    #define oFragColor gl_FragColor
95    #define texture texture2D
96#else
97    out vec4 oFragColor;
98#endif
99
100#if __VERSION__ != 100
101    #define varying in
102#endif
103
104uniform sampler2D uTexture;
105uniform vec2 uTexelSize; // 1.0 / texture dimensions
106uniform float uEdgeThreshold;
107uniform float uEdgeThresholdMin;
108
109varying vec2 vTexCoord;
110
111// Luminance conversion (Rec. 709)
112float luminance(vec3 color) {
113    return dot(color, vec3(0.2126, 0.7152, 0.0722));
114}
115
116void main() {
117    // Sample center and 4-neighborhood
118    vec3 colorCenter = texture(uTexture, vTexCoord).rgb;
119    vec3 colorN = texture(uTexture, vTexCoord + vec2(0.0, -uTexelSize.y)).rgb;
120    vec3 colorS = texture(uTexture, vTexCoord + vec2(0.0, uTexelSize.y)).rgb;
121    vec3 colorE = texture(uTexture, vTexCoord + vec2(uTexelSize.x, 0.0)).rgb;
122    vec3 colorW = texture(uTexture, vTexCoord + vec2(-uTexelSize.x, 0.0)).rgb;
123    
124    // Calculate luminance
125    float lumCenter = luminance(colorCenter);
126    float lumN = luminance(colorN);
127    float lumS = luminance(colorS);
128    float lumE = luminance(colorE);
129    float lumW = luminance(colorW);
130    
131    // Find min/max luminance in neighborhood
132    float lumMin = min(lumCenter, min(min(lumN, lumS), min(lumE, lumW)));
133    float lumMax = max(lumCenter, max(max(lumN, lumS), max(lumE, lumW)));
134    float lumRange = lumMax - lumMin;
135    
136    // Early exit if no edge detected
137    if (lumRange < max(uEdgeThresholdMin, lumMax * uEdgeThreshold)) {
138        oFragColor = vec4(colorCenter, 1.0);
139        return;
140    }
141    
142    // Calculate edge direction
143    float lumNS = lumN + lumS;
144    float lumEW = lumE + lumW;
145    
146    vec2 dir;
147    dir.x = lumNS - lumEW;
148    dir.y = lumN - lumS;
149    
150    // Normalize edge direction
151    float dirReduce = max((lumN + lumS + lumE + lumW) * 0.25 * 0.25, 0.0078125);
152    float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
153    dir = min(vec2(8.0), max(vec2(-8.0), dir * rcpDirMin)) * uTexelSize;
154    
155    // Sample along edge direction
156    vec3 color1 = 0.5 * (
157        texture(uTexture, vTexCoord + dir * (1.0/3.0 - 0.5)).rgb +
158        texture(uTexture, vTexCoord + dir * (2.0/3.0 - 0.5)).rgb
159    );
160    
161    vec3 color2 = color1 * 0.5 + 0.25 * (
162        texture(uTexture, vTexCoord + dir * -0.5).rgb +
163        texture(uTexture, vTexCoord + dir * 0.5).rgb
164    );
165    
166    float lum2 = luminance(color2);
167    
168    // Choose appropriate sample based on luminance range
169    if (lum2 < lumMin || lum2 > lumMax) {
170        oFragColor = vec4(color1, 1.0);
171    } else {
172        oFragColor = vec4(color2, 1.0);
173    }
174}
175";
176
177#[cfg(test)]
178mod autotest_generated {
179    use super::*;
180
181    /// Documented bounds from the `FxaaConfig::edge_threshold` doc comment.
182    const EDGE_THRESHOLD_RANGE: (f32, f32) = (0.063, 0.333);
183    /// Documented bounds from the `FxaaConfig::edge_threshold_min` doc comment.
184    const EDGE_THRESHOLD_MIN_RANGE: (f32, f32) = (0.0312, 0.0833);
185
186    /// `f32::abs` lives in `std`, and this crate is `no_std`-capable.
187    fn fabs(x: f32) -> f32 {
188        if x < 0.0 { -x } else { x }
189    }
190
191    /// `FxaaConfig` derives neither `PartialEq` nor `Eq`, so compare field-wise.
192    fn same(a: FxaaConfig, b: FxaaConfig) -> bool {
193        a.enabled == b.enabled
194            && a.edge_threshold.to_bits() == b.edge_threshold.to_bits()
195            && a.edge_threshold_min.to_bits() == b.edge_threshold_min.to_bits()
196    }
197
198    fn all_presets() -> [(&'static str, FxaaConfig); 5] {
199        [
200            ("default", FxaaConfig::default()),
201            ("enabled", FxaaConfig::enabled()),
202            ("high_quality", FxaaConfig::high_quality()),
203            ("balanced", FxaaConfig::balanced()),
204            ("performance", FxaaConfig::performance()),
205        ]
206    }
207
208    fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
209        if needle.is_empty() || haystack.len() < needle.len() {
210            return None;
211        }
212        (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
213    }
214
215    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
216        find(haystack, needle).is_some()
217    }
218
219    // ---------------------------------------------------------------------
220    // Preset values: exact literals, documented ranges, ordering
221    // ---------------------------------------------------------------------
222
223    #[test]
224    fn default_is_disabled_with_documented_defaults() {
225        let d = FxaaConfig::default();
226        assert!(!d.enabled, "FXAA must default to off for performance");
227        assert_eq!(d.edge_threshold, 0.125_f32);
228        assert_eq!(d.edge_threshold_min, 0.0312_f32);
229    }
230
231    #[test]
232    fn enabled_is_default_with_only_the_flag_flipped() {
233        // `enabled()` is spelled `..Default::default()`; this pins that it never
234        // silently picks up a different quality preset if Default ever changes.
235        let e = FxaaConfig::enabled();
236        let d = FxaaConfig::default();
237        assert!(e.enabled);
238        assert_eq!(e.edge_threshold.to_bits(), d.edge_threshold.to_bits());
239        assert_eq!(
240            e.edge_threshold_min.to_bits(),
241            d.edge_threshold_min.to_bits()
242        );
243    }
244
245    #[test]
246    fn balanced_matches_enabled_default() {
247        // "Balanced preset - default settings" — the doc claims these are the same.
248        assert!(
249            same(FxaaConfig::balanced(), FxaaConfig::enabled()),
250            "balanced() drifted away from the documented default settings"
251        );
252    }
253
254    #[test]
255    fn every_preset_except_default_is_enabled() {
256        for (name, cfg) in all_presets() {
257            if name == "default" {
258                assert!(!cfg.enabled, "{name} should be disabled");
259            } else {
260                assert!(cfg.enabled, "{name}() must enable FXAA");
261            }
262        }
263    }
264
265    #[test]
266    fn preset_thresholds_are_within_documented_ranges() {
267        let (lo, hi) = EDGE_THRESHOLD_RANGE;
268        let (min_lo, min_hi) = EDGE_THRESHOLD_MIN_RANGE;
269        for (name, cfg) in all_presets() {
270            assert!(
271                cfg.edge_threshold >= lo && cfg.edge_threshold <= hi,
272                "{name}: edge_threshold {} outside documented [{lo}, {hi}]",
273                cfg.edge_threshold
274            );
275            assert!(
276                cfg.edge_threshold_min >= min_lo && cfg.edge_threshold_min <= min_hi,
277                "{name}: edge_threshold_min {} outside documented [{min_lo}, {min_hi}]",
278                cfg.edge_threshold_min
279            );
280        }
281    }
282
283    #[test]
284    fn preset_thresholds_are_finite_and_positive() {
285        for (name, cfg) in all_presets() {
286            assert!(!cfg.edge_threshold.is_nan(), "{name}: edge_threshold is NaN");
287            assert!(
288                cfg.edge_threshold.is_finite(),
289                "{name}: edge_threshold is not finite"
290            );
291            assert!(
292                !cfg.edge_threshold_min.is_nan(),
293                "{name}: edge_threshold_min is NaN"
294            );
295            assert!(
296                cfg.edge_threshold_min.is_finite(),
297                "{name}: edge_threshold_min is not finite"
298            );
299            // A zero/negative threshold makes the shader's `lumRange < max(...)`
300            // early-exit unreachable -> AA runs on every single fragment.
301            assert!(
302                cfg.edge_threshold > 0.0,
303                "{name}: edge_threshold must be > 0"
304            );
305            assert!(
306                cfg.edge_threshold_min > 0.0,
307                "{name}: edge_threshold_min must be > 0"
308            );
309        }
310    }
311
312    #[test]
313    fn min_threshold_never_exceeds_edge_threshold() {
314        // The shader computes `max(uEdgeThresholdMin, lumMax * uEdgeThreshold)`;
315        // with lumMax <= 1.0 the relative term can only ever win if
316        // edge_threshold >= edge_threshold_min. If min > threshold, the relative
317        // threshold is dead code for every possible luminance.
318        for (name, cfg) in all_presets() {
319            assert!(
320                cfg.edge_threshold_min <= cfg.edge_threshold,
321                "{name}: edge_threshold_min ({}) > edge_threshold ({}) makes the \
322                 relative threshold unreachable",
323                cfg.edge_threshold_min,
324                cfg.edge_threshold
325            );
326        }
327    }
328
329    #[test]
330    fn presets_are_ordered_by_aggressiveness() {
331        // high_quality = most edges detected (lowest threshold),
332        // performance = fewest (highest threshold), balanced in between.
333        let hq = FxaaConfig::high_quality();
334        let bal = FxaaConfig::balanced();
335        let perf = FxaaConfig::performance();
336        assert!(
337            hq.edge_threshold < bal.edge_threshold,
338            "high_quality must detect more edges than balanced"
339        );
340        assert!(
341            bal.edge_threshold < perf.edge_threshold,
342            "balanced must detect more edges than performance"
343        );
344        assert!(
345            hq.edge_threshold_min <= bal.edge_threshold_min,
346            "high_quality min-threshold must not exceed balanced's"
347        );
348        assert!(
349            bal.edge_threshold_min <= perf.edge_threshold_min,
350            "balanced min-threshold must not exceed performance's"
351        );
352    }
353
354    #[test]
355    fn const_presets_are_usable_in_const_context() {
356        // The three `const fn` presets must stay const-evaluable: shader setup
357        // sites may store them in statics.
358        const HQ: FxaaConfig = FxaaConfig::high_quality();
359        const BAL: FxaaConfig = FxaaConfig::balanced();
360        const PERF: FxaaConfig = FxaaConfig::performance();
361        assert!(same(HQ, FxaaConfig::high_quality()));
362        assert!(same(BAL, FxaaConfig::balanced()));
363        assert!(same(PERF, FxaaConfig::performance()));
364    }
365
366    #[test]
367    fn presets_are_deterministic_across_calls() {
368        for _ in 0..64 {
369            assert!(same(FxaaConfig::enabled(), FxaaConfig::enabled()));
370            assert!(same(FxaaConfig::high_quality(), FxaaConfig::high_quality()));
371            assert!(same(FxaaConfig::balanced(), FxaaConfig::balanced()));
372            assert!(same(FxaaConfig::performance(), FxaaConfig::performance()));
373            assert!(same(FxaaConfig::default(), FxaaConfig::default()));
374        }
375    }
376
377    #[test]
378    fn config_is_copy_not_aliased() {
379        let original = FxaaConfig::high_quality();
380        let mut copy = original;
381        copy.enabled = false;
382        copy.edge_threshold = f32::NAN;
383        copy.edge_threshold_min = f32::INFINITY;
384        assert!(!copy.enabled);
385        assert!(copy.edge_threshold.is_nan());
386        // `original` must be untouched (Copy, no interior mutability / no heap).
387        assert!(original.enabled);
388        assert_eq!(original.edge_threshold, 0.063_f32);
389        assert_eq!(original.edge_threshold_min, 0.0312_f32);
390    }
391
392    #[test]
393    fn threshold_bits_round_trip_through_f32_repr() {
394        // These values are uploaded verbatim as GL float uniforms; a bit-level
395        // round-trip guards against any lossy re-encoding in between.
396        for (name, cfg) in all_presets() {
397            let t = f32::from_bits(cfg.edge_threshold.to_bits());
398            let m = f32::from_bits(cfg.edge_threshold_min.to_bits());
399            assert_eq!(t.to_bits(), cfg.edge_threshold.to_bits(), "{name}");
400            assert_eq!(m.to_bits(), cfg.edge_threshold_min.to_bits(), "{name}");
401        }
402    }
403
404    // ---------------------------------------------------------------------
405    // The shader's threshold math, replayed in Rust against every preset
406    // ---------------------------------------------------------------------
407
408    /// Mirrors the shader's early-exit predicate:
409    /// `lumRange < max(uEdgeThresholdMin, lumMax * uEdgeThreshold)`.
410    fn shader_skips_aa(cfg: FxaaConfig, lum_min: f32, lum_max: f32) -> bool {
411        let lum_range = lum_max - lum_min;
412        let threshold = if cfg.edge_threshold_min > lum_max * cfg.edge_threshold {
413            cfg.edge_threshold_min
414        } else {
415            lum_max * cfg.edge_threshold
416        };
417        lum_range < threshold
418    }
419
420    #[test]
421    fn flat_regions_never_trigger_aa() {
422        // Uniform luminance (lumRange == 0) must always take the early-exit path,
423        // for every preset and across the whole legal luminance domain.
424        for (name, cfg) in all_presets() {
425            for step in 0..=32u32 {
426                let lum = f32::from(step as u16) / 32.0;
427                assert!(
428                    shader_skips_aa(cfg, lum, lum),
429                    "{name}: flat region at lum={lum} would be blurred"
430                );
431            }
432        }
433    }
434
435    #[test]
436    fn maximum_contrast_edge_always_triggers_aa() {
437        // A pure black/white edge (lumRange == 1.0) must never be skipped:
438        // that requires edge_threshold < 1.0 AND edge_threshold_min < 1.0.
439        for (name, cfg) in all_presets() {
440            assert!(
441                !shader_skips_aa(cfg, 0.0, 1.0),
442                "{name}: a full-contrast edge would be skipped by the shader"
443            );
444        }
445    }
446
447    #[test]
448    fn dark_low_contrast_edges_are_gated_by_the_min_threshold() {
449        // Near-black gradients: `lumMax * edge_threshold` collapses toward 0, so
450        // only edge_threshold_min prevents AA-ing sensor noise. A tiny ramp well
451        // below the min threshold must still be skipped.
452        for (name, cfg) in all_presets() {
453            let lum_min = 0.0;
454            let lum_max = cfg.edge_threshold_min * 0.5;
455            assert!(
456                shader_skips_aa(cfg, lum_min, lum_max),
457                "{name}: sub-min-threshold dark gradient (range {lum_max}) would be AA'd"
458            );
459        }
460    }
461
462    #[test]
463    fn threshold_predicate_is_nan_safe() {
464        // GL_RGBA16F / GL_RGBA32F render targets can legitimately carry NaN.
465        // The predicate must not panic and must fall through to "no AA" (all
466        // float comparisons against NaN are false, so lumRange < t is false).
467        for (_, cfg) in all_presets() {
468            let skipped = shader_skips_aa(cfg, f32::NAN, f32::NAN);
469            assert!(!skipped, "NaN luminance must not take the flat-region path");
470            let _ = shader_skips_aa(cfg, f32::NEG_INFINITY, f32::INFINITY);
471            let _ = shader_skips_aa(cfg, f32::MIN, f32::MAX);
472            let _ = shader_skips_aa(cfg, f32::MAX, f32::MIN);
473        }
474    }
475
476    #[test]
477    fn extreme_luminance_inputs_do_not_produce_nan_thresholds() {
478        // lumMax * edge_threshold with a huge lumMax must stay finite-or-inf,
479        // never NaN (NaN would silently disable the early exit).
480        for (name, cfg) in all_presets() {
481            for &lum_max in &[0.0_f32, 1.0, 1e30, f32::MAX, f32::MIN_POSITIVE] {
482                let t = lum_max * cfg.edge_threshold;
483                assert!(!t.is_nan(), "{name}: threshold went NaN at lumMax={lum_max}");
484            }
485        }
486    }
487
488    // ---------------------------------------------------------------------
489    // Shader source bytes: what the GL driver actually receives
490    // ---------------------------------------------------------------------
491
492    #[test]
493    fn shaders_are_non_empty_ascii_utf8() {
494        for (name, src) in [
495            ("vertex", FXAA_VERTEX_SHADER),
496            ("fragment", FXAA_FRAGMENT_SHADER),
497        ] {
498            assert!(!src.is_empty(), "{name} shader is empty");
499            let text = core::str::from_utf8(src)
500                .unwrap_or_else(|e| panic!("{name} shader is not valid UTF-8: {e}"));
501            // GLSL 1.50 source must be ASCII outside of comments; a stray
502            // non-ASCII byte (e.g. a smart quote from an editor) is a hard
503            // compile error on some drivers.
504            assert!(
505                text.is_ascii(),
506                "{name} shader contains non-ASCII bytes (unicode smuggled into GLSL)"
507            );
508        }
509    }
510
511    #[test]
512    fn shaders_contain_no_interior_nul_byte() {
513        // These are handed to glShaderSource; an embedded NUL truncates the
514        // source at the driver and yields a baffling "missing main()" error.
515        for (name, src) in [
516            ("vertex", FXAA_VERTEX_SHADER),
517            ("fragment", FXAA_FRAGMENT_SHADER),
518        ] {
519            assert!(
520                !src.contains(&0u8),
521                "{name} shader contains an interior NUL byte"
522            );
523        }
524    }
525
526    #[test]
527    fn version_directive_is_the_first_token() {
528        // GLSL requires #version to precede everything but comments/whitespace.
529        for (name, src) in [
530            ("vertex", FXAA_VERTEX_SHADER),
531            ("fragment", FXAA_FRAGMENT_SHADER),
532        ] {
533            let text = core::str::from_utf8(src).expect("utf8");
534            assert!(
535                text.starts_with("#version 150"),
536                "{name} shader must open with `#version 150`, got: {:?}",
537                &text[..text.len().min(24)]
538            );
539        }
540    }
541
542    #[test]
543    fn shaders_have_balanced_braces_and_parens() {
544        for (name, src) in [
545            ("vertex", FXAA_VERTEX_SHADER),
546            ("fragment", FXAA_FRAGMENT_SHADER),
547        ] {
548            let mut braces: i32 = 0;
549            let mut parens: i32 = 0;
550            for &b in src {
551                match b {
552                    b'{' => braces += 1,
553                    b'}' => braces -= 1,
554                    b'(' => parens += 1,
555                    b')' => parens -= 1,
556                    _ => {}
557                }
558                assert!(braces >= 0, "{name} shader closes a brace it never opened");
559                assert!(parens >= 0, "{name} shader closes a paren it never opened");
560            }
561            assert_eq!(braces, 0, "{name} shader has unbalanced braces");
562            assert_eq!(parens, 0, "{name} shader has unbalanced parens");
563        }
564    }
565
566    #[test]
567    fn shaders_define_main() {
568        assert!(contains(FXAA_VERTEX_SHADER, b"void main()"));
569        assert!(contains(FXAA_FRAGMENT_SHADER, b"void main()"));
570    }
571
572    #[test]
573    fn fragment_shader_declares_every_uniform_the_host_feeds() {
574        // These names are the contract with the GL pass in layout/src/xml/svg.rs;
575        // renaming one of them here silently turns the uniform lookup into -1.
576        for uniform in [
577            &b"uniform sampler2D uTexture;"[..],
578            &b"uniform vec2 uTexelSize;"[..],
579            &b"uniform float uEdgeThreshold;"[..],
580            &b"uniform float uEdgeThresholdMin;"[..],
581        ] {
582            assert!(
583                contains(FXAA_FRAGMENT_SHADER, uniform),
584                "fragment shader is missing uniform declaration: {}",
585                core::str::from_utf8(uniform).unwrap()
586            );
587        }
588    }
589
590    #[test]
591    fn vertex_and_fragment_varyings_match() {
592        // vTexCoord is written by the vertex stage and read by the fragment
593        // stage; a name mismatch is a link error at runtime only.
594        assert!(contains(FXAA_VERTEX_SHADER, b"varying vec2 vTexCoord;"));
595        assert!(contains(FXAA_FRAGMENT_SHADER, b"varying vec2 vTexCoord;"));
596        assert!(contains(FXAA_VERTEX_SHADER, b"attribute vec2 vAttrXY;"));
597        // The fragment stage must not redeclare the vertex attribute.
598        assert!(!contains(FXAA_FRAGMENT_SHADER, b"attribute vec2 vAttrXY;"));
599    }
600
601    #[test]
602    fn both_shaders_guard_the_es100_compatibility_defines() {
603        // `#define varying out` (VS) / `#define varying in` (FS) must stay behind
604        // a `__VERSION__ != 100` guard, or ES2 builds break.
605        assert!(contains(FXAA_VERTEX_SHADER, b"#if __VERSION__ != 100"));
606        assert!(contains(FXAA_VERTEX_SHADER, b"#define varying out"));
607        assert!(contains(FXAA_FRAGMENT_SHADER, b"#if __VERSION__ != 100"));
608        assert!(contains(FXAA_FRAGMENT_SHADER, b"#define varying in"));
609        assert!(contains(FXAA_FRAGMENT_SHADER, b"#if __VERSION__ == 100"));
610        assert!(contains(FXAA_FRAGMENT_SHADER, b"#define oFragColor gl_FragColor"));
611    }
612
613    #[test]
614    fn luminance_weights_are_rec709_and_sum_to_one() {
615        // Parse the literal `dot(color, vec3(...))` weights straight out of the
616        // shader text. Weights that don't sum to 1.0 would darken/brighten the
617        // edge-detection luminance and desync it from the thresholds above.
618        let text = core::str::from_utf8(FXAA_FRAGMENT_SHADER).expect("utf8");
619        let start = find(FXAA_FRAGMENT_SHADER, b"dot(color, vec3(")
620            .expect("fragment shader must compute luminance via dot(color, vec3(..))")
621            + b"dot(color, vec3(".len();
622        let rest = &text[start..];
623        let end = rest.find(')').expect("unterminated vec3(");
624        let mut weights = [0.0_f32; 3];
625        let mut count = 0usize;
626        for part in rest[..end].split(',') {
627            let v: f32 = part
628                .trim()
629                .parse()
630                .unwrap_or_else(|_| panic!("non-numeric luminance weight: {part:?}"));
631            assert!(
632                count < 3,
633                "luminance vec3 has more than 3 components: {:?}",
634                &rest[..end]
635            );
636            weights[count] = v;
637            count += 1;
638        }
639        assert_eq!(count, 3, "luminance vec3 must have exactly 3 components");
640
641        // Rec. 709 coefficients.
642        assert_eq!(weights[0], 0.2126_f32, "R weight is not Rec.709");
643        assert_eq!(weights[1], 0.7152_f32, "G weight is not Rec.709");
644        assert_eq!(weights[2], 0.0722_f32, "B weight is not Rec.709");
645
646        let sum = weights[0] + weights[1] + weights[2];
647        assert!(
648            fabs(sum - 1.0) < 1e-6,
649            "luminance weights sum to {sum}, not 1.0 — white would not map to lum 1.0"
650        );
651
652        // Consequence the thresholds rely on: luminance(white) == 1.0, so
653        // lumMax is bounded by 1.0 for any LDR color, which is what makes
654        // `lumMax * edge_threshold <= edge_threshold` hold.
655        for (name, cfg) in all_presets() {
656            assert!(
657                sum * cfg.edge_threshold <= cfg.edge_threshold + 1e-6,
658                "{name}: relative threshold can exceed edge_threshold for white"
659            );
660        }
661    }
662
663    #[test]
664    fn fragment_shader_writes_the_output_on_every_path() {
665        // Both the early-exit branch and the two edge branches must assign
666        // oFragColor; an unwritten output is undefined-value garbage.
667        let text = core::str::from_utf8(FXAA_FRAGMENT_SHADER).expect("utf8");
668        let writes = text.matches("oFragColor =").count();
669        assert!(
670            writes >= 3,
671            "expected >= 3 oFragColor writes (early-exit + 2 branches), found {writes}"
672        );
673    }
674
675    #[test]
676    fn shader_direction_clamp_is_symmetric() {
677        // `min(vec2(8.0), max(vec2(-8.0), dir * rcpDirMin))` — the FXAA span
678        // clamp must be symmetric, otherwise edges blur asymmetrically.
679        assert!(contains(FXAA_FRAGMENT_SHADER, b"min(vec2(8.0), max(vec2(-8.0)"));
680        // dirReduce must be clamped away from zero, or rcpDirMin divides by 0.
681        assert!(contains(FXAA_FRAGMENT_SHADER, b"0.0078125"));
682        assert!(contains(FXAA_FRAGMENT_SHADER, b"max("));
683    }
684
685    #[test]
686    fn shader_dir_reduce_never_divides_by_zero() {
687        // Replay `1.0 / (min(|dir.x|, |dir.y|) + dirReduce)` for the worst case:
688        // an entirely black neighborhood, where every luminance is 0.
689        let (lum_n, lum_s, lum_e, lum_w) = (0.0_f32, 0.0, 0.0, 0.0);
690        let dir_x = (lum_n + lum_s) - (lum_e + lum_w);
691        let dir_y = lum_n - lum_s;
692        let dir_reduce = {
693            let raw = (lum_n + lum_s + lum_e + lum_w) * 0.25 * 0.25;
694            if raw > 0.007_812_5 { raw } else { 0.007_812_5 }
695        };
696        let min_abs = if fabs(dir_x) < fabs(dir_y) {
697            fabs(dir_x)
698        } else {
699            fabs(dir_y)
700        };
701        let rcp = 1.0_f32 / (min_abs + dir_reduce);
702        assert!(
703            rcp.is_finite(),
704            "rcpDirMin must stay finite even for an all-black neighborhood"
705        );
706        assert_eq!(rcp, 128.0_f32, "1.0 / 0.0078125 == 128.0");
707    }
708}