1#[derive(Debug, Clone, Copy)]
13pub struct FxaaConfig {
14 pub enabled: bool,
16 pub edge_threshold: f32,
19 pub edge_threshold_min: f32,
22}
23
24impl Default for FxaaConfig {
25 fn default() -> Self {
26 Self {
27 enabled: false, edge_threshold: 0.125,
29 edge_threshold_min: 0.0312,
30 }
31 }
32}
33
34impl FxaaConfig {
35 #[must_use] pub fn enabled() -> Self {
37 Self {
38 enabled: true,
39 ..Default::default()
40 }
41 }
42
43 #[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 #[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 #[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
71pub 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
88pub 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 const EDGE_THRESHOLD_RANGE: (f32, f32) = (0.063, 0.333);
183 const EDGE_THRESHOLD_MIN_RANGE: (f32, f32) = (0.0312, 0.0833);
185
186 fn fabs(x: f32) -> f32 {
188 if x < 0.0 { -x } else { x }
189 }
190
191 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 assert!(!contains(FXAA_FRAGMENT_SHADER, b"attribute vec2 vAttrXY;"));
599 }
600
601 #[test]
602 fn both_shaders_guard_the_es100_compatibility_defines() {
603 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 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 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 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 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 assert!(contains(FXAA_FRAGMENT_SHADER, b"min(vec2(8.0), max(vec2(-8.0)"));
680 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 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}