Skip to main content

proof_engine/deferred/
antialiasing.rs

1//! Anti-aliasing implementations for the deferred rendering pipeline.
2//!
3//! Provides:
4//! - FXAA (Fast Approximate Anti-Aliasing): luminance-based edge detection,
5//!   subpixel shift, configurable quality presets (LOW/MEDIUM/HIGH/ULTRA)
6//! - TAA (Temporal Anti-Aliasing): jitter sequences, history buffer,
7//!   velocity-based reprojection, neighborhood clamping, exponential blend
8//! - MSAA configuration (2x/4x/8x sample patterns)
9//! - CAS (Contrast Adaptive Sharpening) pass
10
11use std::fmt;
12
13use super::{Viewport, clampf, lerpf, saturate};
14
15// ---------------------------------------------------------------------------
16// Anti-aliasing mode selection
17// ---------------------------------------------------------------------------
18
19/// Which anti-aliasing technique is active.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AntiAliasingMode {
22    /// No anti-aliasing.
23    None,
24    /// Fast Approximate Anti-Aliasing (post-process).
25    Fxaa,
26    /// Temporal Anti-Aliasing (requires motion vectors).
27    Taa,
28    /// Multisample Anti-Aliasing (hardware).
29    Msaa,
30    /// FXAA + TAA combined.
31    FxaaPlusTaa,
32}
33
34impl AntiAliasingMode {
35    pub fn name(&self) -> &'static str {
36        match self {
37            Self::None => "None",
38            Self::Fxaa => "FXAA",
39            Self::Taa => "TAA",
40            Self::Msaa => "MSAA",
41            Self::FxaaPlusTaa => "FXAA+TAA",
42        }
43    }
44
45    /// Cycle to the next AA mode.
46    pub fn next(&self) -> Self {
47        match self {
48            Self::None => Self::Fxaa,
49            Self::Fxaa => Self::Taa,
50            Self::Taa => Self::Msaa,
51            Self::Msaa => Self::FxaaPlusTaa,
52            Self::FxaaPlusTaa => Self::None,
53        }
54    }
55}
56
57impl Default for AntiAliasingMode {
58    fn default() -> Self {
59        Self::Fxaa
60    }
61}
62
63impl fmt::Display for AntiAliasingMode {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.name())
66    }
67}
68
69// ---------------------------------------------------------------------------
70// FXAA
71// ---------------------------------------------------------------------------
72
73/// Quality preset for FXAA.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum FxaaQuality {
76    /// Fastest, least quality. 3 search steps.
77    Low,
78    /// Balanced. 5 search steps.
79    Medium,
80    /// High quality. 8 search steps.
81    High,
82    /// Maximum quality. 12 search steps.
83    Ultra,
84}
85
86impl FxaaQuality {
87    /// Number of edge search steps.
88    pub fn search_steps(&self) -> u32 {
89        match self {
90            Self::Low => 3,
91            Self::Medium => 5,
92            Self::High => 8,
93            Self::Ultra => 12,
94        }
95    }
96
97    /// Edge detection threshold (lower = more edges detected).
98    pub fn edge_threshold(&self) -> f32 {
99        match self {
100            Self::Low => 0.250,
101            Self::Medium => 0.166,
102            Self::High => 0.125,
103            Self::Ultra => 0.063,
104        }
105    }
106
107    /// Minimum edge threshold (very dark areas).
108    pub fn edge_threshold_min(&self) -> f32 {
109        match self {
110            Self::Low => 0.0833,
111            Self::Medium => 0.0625,
112            Self::High => 0.0312,
113            Self::Ultra => 0.0156,
114        }
115    }
116
117    /// Subpixel quality (0 = off, 1 = full).
118    pub fn subpixel_quality(&self) -> f32 {
119        match self {
120            Self::Low => 0.50,
121            Self::Medium => 0.75,
122            Self::High => 0.875,
123            Self::Ultra => 1.0,
124        }
125    }
126
127    /// Step sizes for the edge search at each quality level.
128    pub fn search_step_sizes(&self) -> Vec<f32> {
129        match self {
130            Self::Low => vec![1.0, 1.5, 2.0],
131            Self::Medium => vec![1.0, 1.0, 1.0, 1.5, 2.0],
132            Self::High => vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 4.0],
133            Self::Ultra => vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 4.0, 8.0],
134        }
135    }
136}
137
138impl Default for FxaaQuality {
139    fn default() -> Self {
140        Self::High
141    }
142}
143
144impl fmt::Display for FxaaQuality {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        let name = match self {
147            Self::Low => "Low",
148            Self::Medium => "Medium",
149            Self::High => "High",
150            Self::Ultra => "Ultra",
151        };
152        write!(f, "{}", name)
153    }
154}
155
156/// FXAA post-processing pass.
157#[derive(Debug)]
158pub struct FxaaPass {
159    /// Whether FXAA is enabled.
160    pub enabled: bool,
161    /// Quality preset.
162    pub quality: FxaaQuality,
163    /// Shader program handle.
164    pub shader_handle: u64,
165    /// Time taken for this pass (microseconds).
166    pub time_us: u64,
167    /// Custom edge threshold override (0 = use preset).
168    pub custom_edge_threshold: f32,
169    /// Custom subpixel quality override (negative = use preset).
170    pub custom_subpixel_quality: f32,
171    /// Whether to show edges only (debug mode).
172    pub show_edges: bool,
173}
174
175impl FxaaPass {
176    pub fn new() -> Self {
177        Self {
178            enabled: true,
179            quality: FxaaQuality::High,
180            shader_handle: 0,
181            time_us: 0,
182            custom_edge_threshold: 0.0,
183            custom_subpixel_quality: -1.0,
184            show_edges: false,
185        }
186    }
187
188    pub fn with_quality(mut self, quality: FxaaQuality) -> Self {
189        self.quality = quality;
190        self
191    }
192
193    /// Get the effective edge threshold.
194    pub fn edge_threshold(&self) -> f32 {
195        if self.custom_edge_threshold > 0.0 {
196            self.custom_edge_threshold
197        } else {
198            self.quality.edge_threshold()
199        }
200    }
201
202    /// Get the effective subpixel quality.
203    pub fn subpixel_quality(&self) -> f32 {
204        if self.custom_subpixel_quality >= 0.0 {
205            self.custom_subpixel_quality
206        } else {
207            self.quality.subpixel_quality()
208        }
209    }
210
211    /// Compute the luminance of an RGB color (sRGB-weighted).
212    pub fn luminance(r: f32, g: f32, b: f32) -> f32 {
213        0.299 * r + 0.587 * g + 0.114 * b
214    }
215
216    /// Perform FXAA on a single pixel (CPU reference implementation).
217    /// Takes a pixel sampler closure that returns (r, g, b) given (x, y) offsets.
218    pub fn process_pixel<F>(
219        &self,
220        center_x: u32,
221        center_y: u32,
222        width: u32,
223        height: u32,
224        sample: F,
225    ) -> [f32; 3]
226    where
227        F: Fn(i32, i32) -> [f32; 3],
228    {
229        let c = sample(center_x as i32, center_y as i32);
230        let lum_c = Self::luminance(c[0], c[1], c[2]);
231
232        // Sample neighbors
233        let n = sample(center_x as i32, center_y as i32 - 1);
234        let s = sample(center_x as i32, center_y as i32 + 1);
235        let e = sample(center_x as i32 + 1, center_y as i32);
236        let w = sample(center_x as i32 - 1, center_y as i32);
237
238        let lum_n = Self::luminance(n[0], n[1], n[2]);
239        let lum_s = Self::luminance(s[0], s[1], s[2]);
240        let lum_e = Self::luminance(e[0], e[1], e[2]);
241        let lum_w = Self::luminance(w[0], w[1], w[2]);
242
243        let lum_min = lum_c.min(lum_n).min(lum_s).min(lum_e).min(lum_w);
244        let lum_max = lum_c.max(lum_n).max(lum_s).max(lum_e).max(lum_w);
245        let lum_range = lum_max - lum_min;
246
247        // Edge detection
248        let threshold = self.edge_threshold();
249        let threshold_min = self.quality.edge_threshold_min();
250        if lum_range < threshold.max(threshold_min) {
251            return c; // no edge
252        }
253
254        // Diagonal neighbors
255        let ne = sample(center_x as i32 + 1, center_y as i32 - 1);
256        let nw = sample(center_x as i32 - 1, center_y as i32 - 1);
257        let se = sample(center_x as i32 + 1, center_y as i32 + 1);
258        let sw = sample(center_x as i32 - 1, center_y as i32 + 1);
259
260        let lum_ne = Self::luminance(ne[0], ne[1], ne[2]);
261        let lum_nw = Self::luminance(nw[0], nw[1], nw[2]);
262        let lum_se = Self::luminance(se[0], se[1], se[2]);
263        let lum_sw = Self::luminance(sw[0], sw[1], sw[2]);
264
265        // Subpixel aliasing test
266        let lum_avg = (lum_n + lum_s + lum_e + lum_w) * 0.25;
267        let subpixel_offset = saturate(
268            ((lum_avg - lum_c).abs() / lum_range.max(1e-6)) * self.subpixel_quality(),
269        );
270
271        // Determine edge direction (horizontal vs vertical)
272        let edge_h = (lum_nw + lum_ne - 2.0 * lum_n).abs()
273            + 2.0 * (lum_w + lum_e - 2.0 * lum_c).abs()
274            + (lum_sw + lum_se - 2.0 * lum_s).abs();
275        let edge_v = (lum_nw + lum_sw - 2.0 * lum_w).abs()
276            + 2.0 * (lum_n + lum_s - 2.0 * lum_c).abs()
277            + (lum_ne + lum_se - 2.0 * lum_e).abs();
278        let is_horizontal = edge_h >= edge_v;
279
280        // Step direction perpendicular to edge
281        let step_length = if is_horizontal {
282            1.0 / height as f32
283        } else {
284            1.0 / width as f32
285        };
286
287        let (lum_positive, lum_negative) = if is_horizontal {
288            (lum_s, lum_n)
289        } else {
290            (lum_e, lum_w)
291        };
292
293        let gradient_positive = (lum_positive - lum_c).abs();
294        let gradient_negative = (lum_negative - lum_c).abs();
295
296        let _step = if gradient_positive >= gradient_negative {
297            step_length
298        } else {
299            -step_length
300        };
301
302        // Final blend
303        let blend_factor = subpixel_offset * subpixel_offset;
304
305        // Blend with neighbor in the edge direction
306        let neighbor = if is_horizontal {
307            if gradient_positive >= gradient_negative { s } else { n }
308        } else {
309            if gradient_positive >= gradient_negative { e } else { w }
310        };
311
312        [
313            lerpf(c[0], neighbor[0], blend_factor),
314            lerpf(c[1], neighbor[1], blend_factor),
315            lerpf(c[2], neighbor[2], blend_factor),
316        ]
317    }
318
319    /// Execute the FXAA pass (GPU simulation).
320    pub fn execute(&mut self, _viewport: &Viewport) {
321        let start = std::time::Instant::now();
322
323        if !self.enabled {
324            self.time_us = 0;
325            return;
326        }
327
328        // In a real engine:
329        // 1. Bind post-process FBO
330        // 2. Bind scene color texture
331        // 3. Set FXAA uniforms (texel size, thresholds)
332        // 4. Draw fullscreen quad with FXAA shader
333        // 5. Output to screen or next post-process stage
334
335        self.time_us = start.elapsed().as_micros() as u64;
336    }
337
338    /// Generate the FXAA fragment shader.
339    pub fn fragment_shader(&self) -> String {
340        let quality = &self.quality;
341        let steps = quality.search_steps();
342        let step_sizes = quality.search_step_sizes();
343
344        let mut shader = String::from(r#"#version 330 core
345in vec2 v_texcoord;
346out vec4 frag_color;
347
348uniform sampler2D u_scene;
349uniform vec2 u_texel_size;
350uniform float u_edge_threshold;
351uniform float u_edge_threshold_min;
352uniform float u_subpixel_quality;
353
354float luma(vec3 c) {
355    return dot(c, vec3(0.299, 0.587, 0.114));
356}
357
358void main() {
359    vec3 rgbM = texture(u_scene, v_texcoord).rgb;
360    float lumaM = luma(rgbM);
361
362    float lumaN = luma(texture(u_scene, v_texcoord + vec2(0.0, -u_texel_size.y)).rgb);
363    float lumaS = luma(texture(u_scene, v_texcoord + vec2(0.0,  u_texel_size.y)).rgb);
364    float lumaE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x, 0.0)).rgb);
365    float lumaW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x, 0.0)).rgb);
366
367    float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW)));
368    float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW)));
369    float lumaRange = lumaMax - lumaMin;
370
371    if (lumaRange < max(u_edge_threshold, u_edge_threshold_min)) {
372        frag_color = vec4(rgbM, 1.0);
373        return;
374    }
375
376    float lumaNE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x, -u_texel_size.y)).rgb);
377    float lumaNW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x, -u_texel_size.y)).rgb);
378    float lumaSE = luma(texture(u_scene, v_texcoord + vec2( u_texel_size.x,  u_texel_size.y)).rgb);
379    float lumaSW = luma(texture(u_scene, v_texcoord + vec2(-u_texel_size.x,  u_texel_size.y)).rgb);
380
381    float edgeH = abs(lumaNW + lumaNE - 2.0*lumaN)
382                + 2.0*abs(lumaW + lumaE - 2.0*lumaM)
383                + abs(lumaSW + lumaSE - 2.0*lumaS);
384    float edgeV = abs(lumaNW + lumaSW - 2.0*lumaW)
385                + 2.0*abs(lumaN + lumaS - 2.0*lumaM)
386                + abs(lumaNE + lumaSE - 2.0*lumaE);
387    bool isHorizontal = edgeH >= edgeV;
388
389    float stepLength = isHorizontal ? u_texel_size.y : u_texel_size.x;
390    float lumaP = isHorizontal ? lumaS : lumaE;
391    float lumaN2 = isHorizontal ? lumaN : lumaW;
392    float gradP = abs(lumaP - lumaM);
393    float gradN = abs(lumaN2 - lumaM);
394    float step = (gradP >= gradN) ? stepLength : -stepLength;
395
396    vec2 edgeDir = isHorizontal ? vec2(u_texel_size.x, 0.0) : vec2(0.0, u_texel_size.y);
397    vec2 pos = v_texcoord;
398    if (isHorizontal) pos.y += step * 0.5;
399    else pos.x += step * 0.5;
400
401    float lumaEnd = (gradP >= gradN) ? lumaP : lumaN2;
402    float lumaLocalAvg = 0.5 * (lumaEnd + lumaM);
403    bool sign = (lumaLocalAvg - lumaM) >= 0.0;
404
405    // Edge search
406"#);
407
408        // Generate edge search loop
409        shader.push_str(&format!(
410            "    vec2 posP = pos + edgeDir;\n    vec2 posN = pos - edgeDir;\n"
411        ));
412        shader.push_str(
413            "    float lumaEndP = luma(texture(u_scene, posP).rgb) - lumaLocalAvg;\n"
414        );
415        shader.push_str(
416            "    float lumaEndN = luma(texture(u_scene, posN).rgb) - lumaLocalAvg;\n"
417        );
418        shader.push_str("    bool doneP = abs(lumaEndP) >= lumaRange * 0.25;\n");
419        shader.push_str("    bool doneN = abs(lumaEndN) >= lumaRange * 0.25;\n\n");
420
421        for i in 1..steps {
422            let step_size = if (i as usize) < step_sizes.len() {
423                step_sizes[i as usize]
424            } else {
425                1.0
426            };
427            shader.push_str(&format!(
428                "    if (!doneP) posP += edgeDir * {:.1};\n",
429                step_size
430            ));
431            shader.push_str(&format!(
432                "    if (!doneN) posN -= edgeDir * {:.1};\n",
433                step_size
434            ));
435            shader.push_str(
436                "    if (!doneP) lumaEndP = luma(texture(u_scene, posP).rgb) - lumaLocalAvg;\n"
437            );
438            shader.push_str(
439                "    if (!doneN) lumaEndN = luma(texture(u_scene, posN).rgb) - lumaLocalAvg;\n"
440            );
441            shader.push_str("    if (!doneP) doneP = abs(lumaEndP) >= lumaRange * 0.25;\n");
442            shader.push_str("    if (!doneN) doneN = abs(lumaEndN) >= lumaRange * 0.25;\n\n");
443        }
444
445        shader.push_str(r#"
446    float distP = isHorizontal ? (posP.x - v_texcoord.x) : (posP.y - v_texcoord.y);
447    float distN = isHorizontal ? (v_texcoord.x - posN.x) : (v_texcoord.y - posN.y);
448    float dist = min(distP, distN);
449    float spanLength = distP + distN;
450    float pixelOffset = -dist / spanLength + 0.5;
451
452    float lumaAvg = (1.0/12.0) * (2.0*(lumaN+lumaS+lumaE+lumaW) + lumaNE+lumaNW+lumaSE+lumaSW);
453    float subPixelDelta = clamp(abs(lumaAvg - lumaM) / lumaRange, 0.0, 1.0);
454    float subPixelOffset = (-2.0*subPixelDelta + 3.0)*subPixelDelta*subPixelDelta * u_subpixel_quality;
455    float finalOffset = max(pixelOffset, subPixelOffset);
456
457    vec2 finalUv = v_texcoord;
458    if (isHorizontal) finalUv.y += finalOffset * step;
459    else finalUv.x += finalOffset * step;
460
461    frag_color = vec4(texture(u_scene, finalUv).rgb, 1.0);
462}
463"#);
464
465        shader
466    }
467}
468
469impl Default for FxaaPass {
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475// ---------------------------------------------------------------------------
476// TAA (Temporal Anti-Aliasing)
477// ---------------------------------------------------------------------------
478
479/// Jitter sequence type for TAA.
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum JitterSequence {
482    /// Halton(2,3) quasi-random sequence.
483    Halton23,
484    /// 8-sample rotated grid pattern.
485    RotatedGrid8,
486    /// 16-sample Halton sequence.
487    Halton16,
488    /// Blue noise derived jitter.
489    BlueNoise,
490}
491
492impl JitterSequence {
493    /// Get the jitter offset for a given frame index in the sequence.
494    /// Returns (x, y) offsets in [-0.5, 0.5] pixel space.
495    pub fn sample(&self, frame_index: u32) -> [f32; 2] {
496        match self {
497            Self::Halton23 => {
498                let x = halton(frame_index + 1, 2) - 0.5;
499                let y = halton(frame_index + 1, 3) - 0.5;
500                [x, y]
501            }
502            Self::RotatedGrid8 => {
503                let samples: [[f32; 2]; 8] = [
504                    [-0.375, -0.375],
505                    [ 0.125, -0.375],
506                    [-0.125, -0.125],
507                    [ 0.375, -0.125],
508                    [-0.375,  0.125],
509                    [ 0.125,  0.125],
510                    [-0.125,  0.375],
511                    [ 0.375,  0.375],
512                ];
513                let idx = (frame_index as usize) % 8;
514                samples[idx]
515            }
516            Self::Halton16 => {
517                let x = halton((frame_index % 16) + 1, 2) - 0.5;
518                let y = halton((frame_index % 16) + 1, 3) - 0.5;
519                [x, y]
520            }
521            Self::BlueNoise => {
522                // Simple blue-noise approximation using interleaved Halton
523                let x = halton(frame_index * 7 + 1, 2) - 0.5;
524                let y = halton(frame_index * 11 + 1, 3) - 0.5;
525                [x, y]
526            }
527        }
528    }
529
530    /// Sequence length before repeating.
531    pub fn length(&self) -> u32 {
532        match self {
533            Self::Halton23 => 256,
534            Self::RotatedGrid8 => 8,
535            Self::Halton16 => 16,
536            Self::BlueNoise => 256,
537        }
538    }
539}
540
541/// Compute the Halton sequence value for a given index and base.
542fn halton(mut index: u32, base: u32) -> f32 {
543    let mut result = 0.0f32;
544    let mut f = 1.0f32 / base as f32;
545    while index > 0 {
546        result += f * (index % base) as f32;
547        index /= base;
548        f /= base as f32;
549    }
550    result
551}
552
553/// Configuration for TAA.
554#[derive(Debug, Clone)]
555pub struct TaaConfig {
556    /// Jitter sequence to use.
557    pub jitter_sequence: JitterSequence,
558    /// Blend factor for history (0 = full current, 1 = full history).
559    /// Typical values: 0.9 - 0.95.
560    pub history_blend: f32,
561    /// Whether to use velocity-based reprojection.
562    pub velocity_reprojection: bool,
563    /// Whether to use neighborhood clamping (prevents ghosting).
564    pub neighborhood_clamping: bool,
565    /// Clamping AABB expansion factor (higher = less ghosting removal).
566    pub clamp_gamma: f32,
567    /// Whether to use variance clipping instead of simple clamping.
568    pub variance_clipping: bool,
569    /// Variance clip gamma (typically 1.0-1.5).
570    pub variance_clip_gamma: f32,
571    /// Whether to apply motion-vector-based blur rejection.
572    pub motion_rejection: bool,
573    /// Velocity weight (how much to reduce history blend for fast-moving pixels).
574    pub motion_rejection_strength: f32,
575    /// Sharpening amount applied after TAA (0 = off).
576    pub sharpen_amount: f32,
577    /// Whether to use catmull-rom filtering for history sampling.
578    pub catmull_rom_history: bool,
579    /// Whether to apply a luminance weight to the blend factor.
580    pub luminance_weighting: bool,
581    /// Whether flicker reduction is enabled.
582    pub flicker_reduction: bool,
583    /// Flicker reduction strength.
584    pub flicker_strength: f32,
585}
586
587impl TaaConfig {
588    pub fn new() -> Self {
589        Self {
590            jitter_sequence: JitterSequence::Halton23,
591            history_blend: 0.9,
592            velocity_reprojection: true,
593            neighborhood_clamping: true,
594            clamp_gamma: 1.0,
595            variance_clipping: false,
596            variance_clip_gamma: 1.0,
597            motion_rejection: true,
598            motion_rejection_strength: 0.5,
599            sharpen_amount: 0.0,
600            catmull_rom_history: true,
601            luminance_weighting: true,
602            flicker_reduction: false,
603            flicker_strength: 0.5,
604        }
605    }
606
607    /// Preset for high-quality TAA.
608    pub fn high_quality() -> Self {
609        Self {
610            jitter_sequence: JitterSequence::Halton23,
611            history_blend: 0.95,
612            velocity_reprojection: true,
613            neighborhood_clamping: true,
614            clamp_gamma: 1.0,
615            variance_clipping: true,
616            variance_clip_gamma: 1.25,
617            motion_rejection: true,
618            motion_rejection_strength: 0.7,
619            sharpen_amount: 0.2,
620            catmull_rom_history: true,
621            luminance_weighting: true,
622            flicker_reduction: true,
623            flicker_strength: 0.5,
624        }
625    }
626
627    /// Preset for performance-oriented TAA.
628    pub fn fast() -> Self {
629        Self {
630            jitter_sequence: JitterSequence::RotatedGrid8,
631            history_blend: 0.85,
632            velocity_reprojection: true,
633            neighborhood_clamping: true,
634            clamp_gamma: 1.5,
635            variance_clipping: false,
636            variance_clip_gamma: 1.0,
637            motion_rejection: false,
638            motion_rejection_strength: 0.0,
639            sharpen_amount: 0.0,
640            catmull_rom_history: false,
641            luminance_weighting: false,
642            flicker_reduction: false,
643            flicker_strength: 0.0,
644        }
645    }
646}
647
648impl Default for TaaConfig {
649    fn default() -> Self {
650        Self::new()
651    }
652}
653
654/// TAA pass state.
655#[derive(Debug)]
656pub struct TaaPass {
657    /// Whether TAA is enabled.
658    pub enabled: bool,
659    /// Configuration.
660    pub config: TaaConfig,
661    /// Shader program handle.
662    pub shader_handle: u64,
663    /// History color buffer handle.
664    pub history_handle: u64,
665    /// Previous frame's history buffer handle (ping-pong).
666    pub prev_history_handle: u64,
667    /// Velocity buffer handle.
668    pub velocity_handle: u64,
669    /// Current jitter offset.
670    pub current_jitter: [f32; 2],
671    /// Current frame index in the jitter sequence.
672    pub frame_index: u32,
673    /// Previous frame's view-projection matrix (for reprojection).
674    pub prev_view_proj: super::Mat4,
675    /// Current frame's view-projection matrix.
676    pub current_view_proj: super::Mat4,
677    /// History buffer dimensions.
678    pub history_width: u32,
679    pub history_height: u32,
680    /// Whether the history buffer is valid (false on first frame or resize).
681    pub history_valid: bool,
682    /// Time taken (microseconds).
683    pub time_us: u64,
684    /// Whether this is a ping or pong frame.
685    pub ping_pong: bool,
686}
687
688impl TaaPass {
689    pub fn new() -> Self {
690        Self {
691            enabled: true,
692            config: TaaConfig::new(),
693            shader_handle: 0,
694            history_handle: 0,
695            prev_history_handle: 0,
696            velocity_handle: 0,
697            current_jitter: [0.0, 0.0],
698            frame_index: 0,
699            prev_view_proj: super::Mat4::IDENTITY,
700            current_view_proj: super::Mat4::IDENTITY,
701            history_width: 0,
702            history_height: 0,
703            history_valid: false,
704            time_us: 0,
705            ping_pong: false,
706        }
707    }
708
709    pub fn with_config(mut self, config: TaaConfig) -> Self {
710        self.config = config;
711        self
712    }
713
714    /// Advance to the next frame: update jitter, store previous matrices.
715    pub fn begin_frame(&mut self, view_proj: &super::Mat4) {
716        self.prev_view_proj = self.current_view_proj;
717        self.current_view_proj = *view_proj;
718
719        self.current_jitter = self.config.jitter_sequence.sample(self.frame_index);
720        self.frame_index = (self.frame_index + 1) % self.config.jitter_sequence.length();
721        self.ping_pong = !self.ping_pong;
722    }
723
724    /// Get the jitter offset in NDC space for a given viewport size.
725    pub fn jitter_ndc(&self, viewport: &Viewport) -> [f32; 2] {
726        [
727            self.current_jitter[0] * 2.0 / viewport.width as f32,
728            self.current_jitter[1] * 2.0 / viewport.height as f32,
729        ]
730    }
731
732    /// Apply jitter to a projection matrix.
733    pub fn jittered_projection(&self, proj: &super::Mat4, viewport: &Viewport) -> super::Mat4 {
734        let jitter = self.jitter_ndc(viewport);
735        let mut jittered = *proj;
736        jittered.cols[2][0] += jitter[0];
737        jittered.cols[2][1] += jitter[1];
738        jittered
739    }
740
741    /// Resize the history buffers.
742    pub fn resize(&mut self, width: u32, height: u32) {
743        if self.history_width != width || self.history_height != height {
744            self.history_width = width;
745            self.history_height = height;
746            self.history_valid = false;
747            // In a real engine: reallocate history textures
748        }
749    }
750
751    /// Invalidate history (e.g., on camera cut).
752    pub fn invalidate_history(&mut self) {
753        self.history_valid = false;
754    }
755
756    /// Neighborhood clamp: constrain history color to the min/max of the 3x3 neighborhood
757    /// of the current frame. This prevents ghosting.
758    pub fn neighborhood_clamp(
759        current_color: [f32; 3],
760        history_color: [f32; 3],
761        neighborhood_min: [f32; 3],
762        neighborhood_max: [f32; 3],
763        gamma: f32,
764    ) -> [f32; 3] {
765        // Expand the AABB by gamma
766        let center = [
767            (neighborhood_min[0] + neighborhood_max[0]) * 0.5,
768            (neighborhood_min[1] + neighborhood_max[1]) * 0.5,
769            (neighborhood_min[2] + neighborhood_max[2]) * 0.5,
770        ];
771        let extent = [
772            (neighborhood_max[0] - neighborhood_min[0]) * 0.5 * gamma,
773            (neighborhood_max[1] - neighborhood_min[1]) * 0.5 * gamma,
774            (neighborhood_max[2] - neighborhood_min[2]) * 0.5 * gamma,
775        ];
776        let clamped_min = [
777            center[0] - extent[0],
778            center[1] - extent[1],
779            center[2] - extent[2],
780        ];
781        let clamped_max = [
782            center[0] + extent[0],
783            center[1] + extent[1],
784            center[2] + extent[2],
785        ];
786
787        let _ = current_color;
788
789        [
790            clampf(history_color[0], clamped_min[0], clamped_max[0]),
791            clampf(history_color[1], clamped_min[1], clamped_max[1]),
792            clampf(history_color[2], clamped_min[2], clamped_max[2]),
793        ]
794    }
795
796    /// Variance clipping: use mean and variance of the neighborhood for tighter clamping.
797    pub fn variance_clip(
798        history_color: [f32; 3],
799        neighborhood_mean: [f32; 3],
800        neighborhood_variance: [f32; 3],
801        gamma: f32,
802    ) -> [f32; 3] {
803        let sigma = [
804            neighborhood_variance[0].sqrt() * gamma,
805            neighborhood_variance[1].sqrt() * gamma,
806            neighborhood_variance[2].sqrt() * gamma,
807        ];
808        [
809            clampf(
810                history_color[0],
811                neighborhood_mean[0] - sigma[0],
812                neighborhood_mean[0] + sigma[0],
813            ),
814            clampf(
815                history_color[1],
816                neighborhood_mean[1] - sigma[1],
817                neighborhood_mean[1] + sigma[1],
818            ),
819            clampf(
820                history_color[2],
821                neighborhood_mean[2] - sigma[2],
822                neighborhood_mean[2] + sigma[2],
823            ),
824        ]
825    }
826
827    /// Compute the blend factor, adjusting for motion.
828    pub fn compute_blend_factor(
829        &self,
830        velocity_length: f32,
831    ) -> f32 {
832        let mut blend = self.config.history_blend;
833
834        // Reduce history contribution for fast-moving pixels
835        if self.config.motion_rejection && velocity_length > 0.001 {
836            let motion_factor = saturate(velocity_length * self.config.motion_rejection_strength * 100.0);
837            blend *= 1.0 - motion_factor;
838        }
839
840        // If history is invalid, use only current frame
841        if !self.history_valid {
842            return 0.0;
843        }
844
845        clampf(blend, 0.0, 0.98)
846    }
847
848    /// Execute the TAA pass.
849    pub fn execute(&mut self, _viewport: &Viewport) {
850        let start = std::time::Instant::now();
851
852        if !self.enabled {
853            self.time_us = 0;
854            return;
855        }
856
857        // In a real engine:
858        // 1. Bind TAA resolve FBO
859        // 2. Bind current color, history, velocity textures
860        // 3. Set uniforms (jitter, prev VP matrix, blend factor, etc.)
861        // 4. Draw fullscreen quad
862        // 5. Swap ping-pong buffers
863
864        self.history_valid = true;
865        self.time_us = start.elapsed().as_micros() as u64;
866    }
867
868    /// Generate the TAA resolve fragment shader.
869    pub fn fragment_shader(&self) -> String {
870        let mut s = String::from(r#"#version 330 core
871in vec2 v_texcoord;
872out vec4 frag_color;
873
874uniform sampler2D u_current;
875uniform sampler2D u_history;
876uniform sampler2D u_velocity;
877uniform sampler2D u_depth;
878uniform vec2 u_texel_size;
879uniform float u_blend_factor;
880uniform mat4 u_prev_vp;
881uniform mat4 u_inv_vp;
882uniform vec2 u_jitter;
883uniform bool u_use_variance_clip;
884
885vec3 rgb_to_ycocg(vec3 rgb) {
886    return vec3(
887        0.25*rgb.r + 0.5*rgb.g + 0.25*rgb.b,
888        0.5*rgb.r - 0.5*rgb.b,
889        -0.25*rgb.r + 0.5*rgb.g - 0.25*rgb.b
890    );
891}
892
893vec3 ycocg_to_rgb(vec3 ycocg) {
894    return vec3(
895        ycocg.x + ycocg.y - ycocg.z,
896        ycocg.x + ycocg.z,
897        ycocg.x - ycocg.y - ycocg.z
898    );
899}
900
901void main() {
902    // Remove jitter from current frame UV
903    vec2 uv = v_texcoord - u_jitter * 0.5;
904
905    vec3 current = texture(u_current, uv).rgb;
906
907    // Reproject using velocity
908    vec2 velocity = texture(u_velocity, v_texcoord).rg;
909    vec2 history_uv = v_texcoord - velocity;
910
911    // Check if history UV is valid
912    if (history_uv.x < 0.0 || history_uv.x > 1.0 || history_uv.y < 0.0 || history_uv.y > 1.0) {
913        frag_color = vec4(current, 1.0);
914        return;
915    }
916
917    vec3 history = texture(u_history, history_uv).rgb;
918
919    // Neighborhood clamping in YCoCg space
920    vec3 s0 = rgb_to_ycocg(current);
921    vec3 s1 = rgb_to_ycocg(texture(u_current, uv + vec2(-u_texel_size.x, 0)).rgb);
922    vec3 s2 = rgb_to_ycocg(texture(u_current, uv + vec2( u_texel_size.x, 0)).rgb);
923    vec3 s3 = rgb_to_ycocg(texture(u_current, uv + vec2(0, -u_texel_size.y)).rgb);
924    vec3 s4 = rgb_to_ycocg(texture(u_current, uv + vec2(0,  u_texel_size.y)).rgb);
925
926"#);
927
928        if self.config.variance_clipping {
929            s.push_str(&format!(r#"
930    vec3 mean = (s0+s1+s2+s3+s4) / 5.0;
931    vec3 sq_mean = (s0*s0+s1*s1+s2*s2+s3*s3+s4*s4) / 5.0;
932    vec3 variance = sq_mean - mean*mean;
933    vec3 sigma = sqrt(max(variance, vec3(0))) * {:.2};
934    vec3 hist_ycocg = rgb_to_ycocg(history);
935    hist_ycocg = clamp(hist_ycocg, mean - sigma, mean + sigma);
936    history = ycocg_to_rgb(hist_ycocg);
937"#, self.config.variance_clip_gamma));
938        } else {
939            s.push_str(r#"
940    vec3 nmin = min(s0, min(min(s1, s2), min(s3, s4)));
941    vec3 nmax = max(s0, max(max(s1, s2), max(s3, s4)));
942    vec3 hist_ycocg = rgb_to_ycocg(history);
943    hist_ycocg = clamp(hist_ycocg, nmin, nmax);
944    history = ycocg_to_rgb(hist_ycocg);
945"#);
946        }
947
948        s.push_str(r#"
949    // Exponential blend
950    float blend = u_blend_factor;
951"#);
952
953        if self.config.motion_rejection {
954            s.push_str(&format!(r#"
955    float vel_len = length(velocity);
956    blend *= 1.0 - clamp(vel_len * {:.1}, 0.0, 0.9);
957"#, self.config.motion_rejection_strength * 100.0));
958        }
959
960        if self.config.luminance_weighting {
961            s.push_str(r#"
962    float lum_current = dot(current, vec3(0.2126, 0.7152, 0.0722));
963    float lum_history = dot(history, vec3(0.2126, 0.7152, 0.0722));
964    float lum_diff = abs(lum_current - lum_history) / max(lum_current, max(lum_history, 0.001));
965    blend *= 1.0 - lum_diff * 0.5;
966"#);
967        }
968
969        s.push_str(r#"
970    vec3 result = mix(current, history, clamp(blend, 0.0, 0.98));
971    frag_color = vec4(result, 1.0);
972}
973"#);
974
975        s
976    }
977}
978
979impl Default for TaaPass {
980    fn default() -> Self {
981        Self::new()
982    }
983}
984
985// ---------------------------------------------------------------------------
986// MSAA
987// ---------------------------------------------------------------------------
988
989/// Number of MSAA samples.
990#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
991pub enum MsaaSampleCount {
992    /// 2x MSAA.
993    X2,
994    /// 4x MSAA.
995    X4,
996    /// 8x MSAA.
997    X8,
998}
999
1000impl MsaaSampleCount {
1001    /// Get the numeric sample count.
1002    pub fn count(&self) -> u32 {
1003        match self {
1004            Self::X2 => 2,
1005            Self::X4 => 4,
1006            Self::X8 => 8,
1007        }
1008    }
1009
1010    /// Get the standard sample positions for this sample count.
1011    /// Returns positions in pixel space [-0.5, 0.5].
1012    pub fn sample_positions(&self) -> Vec<[f32; 2]> {
1013        match self {
1014            Self::X2 => vec![
1015                [-0.25, -0.25],
1016                [ 0.25,  0.25],
1017            ],
1018            Self::X4 => vec![
1019                [-0.375, -0.125],
1020                [ 0.125, -0.375],
1021                [-0.125,  0.375],
1022                [ 0.375,  0.125],
1023            ],
1024            Self::X8 => vec![
1025                [-0.375, -0.375],
1026                [ 0.125, -0.375],
1027                [-0.375, -0.125],
1028                [ 0.375, -0.125],
1029                [-0.125,  0.125],
1030                [ 0.375,  0.125],
1031                [-0.125,  0.375],
1032                [ 0.125,  0.375],
1033            ],
1034        }
1035    }
1036
1037    /// Memory multiplier compared to non-MSAA.
1038    pub fn memory_multiplier(&self) -> f32 {
1039        self.count() as f32
1040    }
1041}
1042
1043impl fmt::Display for MsaaSampleCount {
1044    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1045        write!(f, "{}x MSAA", self.count())
1046    }
1047}
1048
1049/// MSAA configuration.
1050#[derive(Debug, Clone)]
1051pub struct MsaaConfig {
1052    /// Whether MSAA is enabled.
1053    pub enabled: bool,
1054    /// Number of samples.
1055    pub sample_count: MsaaSampleCount,
1056    /// Whether to use alpha-to-coverage.
1057    pub alpha_to_coverage: bool,
1058    /// Whether to enable sample shading (full per-sample fragment shading).
1059    pub sample_shading: bool,
1060    /// Minimum sample shading rate (0..1). 1.0 = shade every sample.
1061    pub min_sample_shading: f32,
1062    /// Whether centroid interpolation is used.
1063    pub centroid_interpolation: bool,
1064    /// Whether a resolve pass is needed (true for deferred rendering).
1065    pub needs_resolve: bool,
1066    /// Resolve filter (box, tent, etc.).
1067    pub resolve_filter: MsaaResolveFilter,
1068}
1069
1070/// Filter used when resolving MSAA to non-MSAA.
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub enum MsaaResolveFilter {
1073    /// Simple box filter (average all samples).
1074    Box,
1075    /// Tent/triangle filter.
1076    Tent,
1077    /// Catmull-Rom filter (sharper).
1078    CatmullRom,
1079    /// Blackman-Harris filter (wider, softer).
1080    BlackmanHarris,
1081}
1082
1083impl MsaaConfig {
1084    pub fn new(sample_count: MsaaSampleCount) -> Self {
1085        Self {
1086            enabled: true,
1087            sample_count,
1088            alpha_to_coverage: false,
1089            sample_shading: false,
1090            min_sample_shading: 1.0,
1091            centroid_interpolation: true,
1092            needs_resolve: true,
1093            resolve_filter: MsaaResolveFilter::Box,
1094        }
1095    }
1096
1097    /// Estimated memory overhead multiplier.
1098    pub fn memory_multiplier(&self) -> f32 {
1099        if self.enabled {
1100            self.sample_count.memory_multiplier()
1101        } else {
1102            1.0
1103        }
1104    }
1105
1106    /// Performance cost multiplier (approximate).
1107    pub fn performance_cost(&self) -> f32 {
1108        if !self.enabled {
1109            return 1.0;
1110        }
1111        let base = self.sample_count.count() as f32;
1112        if self.sample_shading {
1113            base // full per-sample shading
1114        } else {
1115            1.0 + (base - 1.0) * 0.3 // partial overhead for rasterization
1116        }
1117    }
1118}
1119
1120impl Default for MsaaConfig {
1121    fn default() -> Self {
1122        Self::new(MsaaSampleCount::X4)
1123    }
1124}
1125
1126// ---------------------------------------------------------------------------
1127// CAS (Contrast Adaptive Sharpening)
1128// ---------------------------------------------------------------------------
1129
1130/// Configuration for AMD's Contrast Adaptive Sharpening (CAS).
1131#[derive(Debug, Clone)]
1132pub struct CasConfig {
1133    /// Sharpening amount (0..1). 0 = no sharpening, 1 = maximum.
1134    pub sharpness: f32,
1135    /// Whether CAS is applied before or after tone mapping.
1136    pub apply_before_tonemapping: bool,
1137    /// Whether to limit sharpening in low-contrast areas.
1138    pub limit_low_contrast: bool,
1139    /// Denoise factor (reduces sharpening of noise).
1140    pub denoise: f32,
1141}
1142
1143impl CasConfig {
1144    pub fn new(sharpness: f32) -> Self {
1145        Self {
1146            sharpness: clampf(sharpness, 0.0, 1.0),
1147            apply_before_tonemapping: false,
1148            limit_low_contrast: true,
1149            denoise: 0.1,
1150        }
1151    }
1152}
1153
1154impl Default for CasConfig {
1155    fn default() -> Self {
1156        Self::new(0.5)
1157    }
1158}
1159
1160/// Sharpening pass using Contrast Adaptive Sharpening.
1161#[derive(Debug)]
1162pub struct SharpeningPass {
1163    /// Whether the pass is enabled.
1164    pub enabled: bool,
1165    /// Configuration.
1166    pub config: CasConfig,
1167    /// Shader program handle.
1168    pub shader_handle: u64,
1169    /// Time taken (microseconds).
1170    pub time_us: u64,
1171}
1172
1173impl SharpeningPass {
1174    pub fn new() -> Self {
1175        Self {
1176            enabled: false,
1177            config: CasConfig::default(),
1178            shader_handle: 0,
1179            time_us: 0,
1180        }
1181    }
1182
1183    pub fn with_sharpness(mut self, sharpness: f32) -> Self {
1184        self.config.sharpness = clampf(sharpness, 0.0, 1.0);
1185        self
1186    }
1187
1188    /// Apply CAS to a single pixel (CPU reference implementation).
1189    pub fn sharpen_pixel<F>(
1190        &self,
1191        x: u32,
1192        y: u32,
1193        sample: F,
1194    ) -> [f32; 3]
1195    where
1196        F: Fn(i32, i32) -> [f32; 3],
1197    {
1198        let c = sample(x as i32, y as i32);
1199        if !self.enabled || self.config.sharpness < 0.001 {
1200            return c;
1201        }
1202
1203        // Sample cross neighborhood
1204        let nb_n = sample(x as i32, y as i32 - 1);
1205        let nb_s = sample(x as i32, y as i32 + 1);
1206        let nb_e = sample(x as i32 + 1, y as i32);
1207        let nb_w = sample(x as i32 - 1, y as i32);
1208
1209        // Find min/max per channel
1210        let mut c_min = [f32::MAX; 3];
1211        let mut c_max = [f32::MIN; 3];
1212        for pixel in &[c, nb_n, nb_s, nb_e, nb_w] {
1213            for i in 0..3 {
1214                c_min[i] = c_min[i].min(pixel[i]);
1215                c_max[i] = c_max[i].max(pixel[i]);
1216            }
1217        }
1218
1219        // CAS weight: based on the reciprocal of the maximum delta
1220        let sharp = self.config.sharpness;
1221        let mut result = [0.0f32; 3];
1222        for i in 0..3 {
1223            let range = c_max[i] - c_min[i];
1224            let wt = if range < 1e-6 {
1225                0.0
1226            } else {
1227                let rcpmax = 1.0 / c_max[i].max(1e-6);
1228                let peak = -1.0 / (range * rcpmax * 4.0 + (1.0 - sharp));
1229                saturate(peak)
1230            };
1231
1232            // Weighted sharpened value
1233            let sum = nb_n[i] + nb_s[i] + nb_e[i] + nb_w[i];
1234            let sharpened = (c[i] + sum * wt) / (1.0 + 4.0 * wt);
1235            result[i] = clampf(sharpened, c_min[i], c_max[i]);
1236        }
1237
1238        result
1239    }
1240
1241    /// Execute the sharpening pass.
1242    pub fn execute(&mut self, _viewport: &Viewport) {
1243        let start = std::time::Instant::now();
1244
1245        if !self.enabled {
1246            self.time_us = 0;
1247            return;
1248        }
1249
1250        // In a real engine:
1251        // 1. Bind post-process FBO
1252        // 2. Bind scene color texture
1253        // 3. Set CAS uniforms
1254        // 4. Draw fullscreen quad
1255
1256        self.time_us = start.elapsed().as_micros() as u64;
1257    }
1258
1259    /// Generate the CAS fragment shader.
1260    pub fn fragment_shader(&self) -> String {
1261        format!(r#"#version 330 core
1262in vec2 v_texcoord;
1263out vec4 frag_color;
1264
1265uniform sampler2D u_scene;
1266uniform vec2 u_texel_size;
1267uniform float u_sharpness;
1268
1269void main() {{
1270    vec3 c = texture(u_scene, v_texcoord).rgb;
1271    vec3 n = texture(u_scene, v_texcoord + vec2(0.0, -u_texel_size.y)).rgb;
1272    vec3 s = texture(u_scene, v_texcoord + vec2(0.0,  u_texel_size.y)).rgb;
1273    vec3 e = texture(u_scene, v_texcoord + vec2( u_texel_size.x, 0.0)).rgb;
1274    vec3 w = texture(u_scene, v_texcoord + vec2(-u_texel_size.x, 0.0)).rgb;
1275
1276    vec3 cMin = min(c, min(min(n, s), min(e, w)));
1277    vec3 cMax = max(c, max(max(n, s), max(e, w)));
1278
1279    // Adaptive sharpening weight
1280    vec3 range = cMax - cMin;
1281    vec3 rcpMax = 1.0 / max(cMax, vec3(0.0001));
1282    vec3 peak = -1.0 / (range * rcpMax * 4.0 + (1.0 - {sharpness:.4}));
1283    vec3 wt = clamp(peak, vec3(0.0), vec3(1.0));
1284
1285    vec3 result = (c + (n + s + e + w) * wt) / (1.0 + 4.0 * wt);
1286    result = clamp(result, cMin, cMax);
1287
1288    frag_color = vec4(result, 1.0);
1289}}
1290"#, sharpness = self.config.sharpness)
1291    }
1292}
1293
1294impl Default for SharpeningPass {
1295    fn default() -> Self {
1296        Self::new()
1297    }
1298}
1299
1300// ---------------------------------------------------------------------------
1301// Tests
1302// ---------------------------------------------------------------------------
1303
1304#[cfg(test)]
1305mod tests {
1306    use super::*;
1307
1308    #[test]
1309    fn test_aa_mode_cycling() {
1310        let mut mode = AntiAliasingMode::None;
1311        mode = mode.next();
1312        assert_eq!(mode, AntiAliasingMode::Fxaa);
1313        mode = mode.next();
1314        assert_eq!(mode, AntiAliasingMode::Taa);
1315        mode = mode.next();
1316        assert_eq!(mode, AntiAliasingMode::Msaa);
1317        mode = mode.next();
1318        assert_eq!(mode, AntiAliasingMode::FxaaPlusTaa);
1319        mode = mode.next();
1320        assert_eq!(mode, AntiAliasingMode::None);
1321    }
1322
1323    #[test]
1324    fn test_fxaa_luminance() {
1325        assert!((FxaaPass::luminance(1.0, 1.0, 1.0) - 1.0).abs() < 0.01);
1326        assert!((FxaaPass::luminance(0.0, 0.0, 0.0) - 0.0).abs() < 0.01);
1327    }
1328
1329    #[test]
1330    fn test_fxaa_quality_presets() {
1331        assert!(FxaaQuality::Low.search_steps() < FxaaQuality::Ultra.search_steps());
1332        assert!(FxaaQuality::Low.edge_threshold() > FxaaQuality::Ultra.edge_threshold());
1333    }
1334
1335    #[test]
1336    fn test_fxaa_no_edge() {
1337        let fxaa = FxaaPass::new();
1338        // Uniform color = no edge
1339        let result = fxaa.process_pixel(5, 5, 10, 10, |_x, _y| [0.5, 0.5, 0.5]);
1340        assert!((result[0] - 0.5).abs() < 0.01);
1341    }
1342
1343    #[test]
1344    fn test_halton_sequence() {
1345        let h0 = halton(1, 2);
1346        assert!((h0 - 0.5).abs() < 0.01);
1347        let h1 = halton(2, 2);
1348        assert!((h1 - 0.25).abs() < 0.01);
1349    }
1350
1351    #[test]
1352    fn test_jitter_sequences() {
1353        for seq in &[JitterSequence::Halton23, JitterSequence::RotatedGrid8,
1354                     JitterSequence::Halton16, JitterSequence::BlueNoise] {
1355            for i in 0..seq.length() {
1356                let [x, y] = seq.sample(i);
1357                assert!(x >= -0.5 && x <= 0.5, "Jitter x out of range: {}", x);
1358                assert!(y >= -0.5 && y <= 0.5, "Jitter y out of range: {}", y);
1359            }
1360        }
1361    }
1362
1363    #[test]
1364    fn test_taa_jitter_ndc() {
1365        let mut taa = TaaPass::new();
1366        let vp = Viewport::new(1920, 1080);
1367        taa.begin_frame(&super::super::Mat4::IDENTITY);
1368        let ndc = taa.jitter_ndc(&vp);
1369        assert!(ndc[0].abs() < 0.01); // small in NDC space
1370        assert!(ndc[1].abs() < 0.01);
1371    }
1372
1373    #[test]
1374    fn test_taa_jittered_projection() {
1375        let taa = TaaPass::new();
1376        let proj = super::super::Mat4::IDENTITY;
1377        let vp = Viewport::new(1920, 1080);
1378        let jittered = taa.jittered_projection(&proj, &vp);
1379        // Jittered projection should be different from original (unless jitter is zero)
1380        let _ = jittered;
1381    }
1382
1383    #[test]
1384    fn test_neighborhood_clamp() {
1385        let result = TaaPass::neighborhood_clamp(
1386            [0.5, 0.5, 0.5],
1387            [2.0, 0.0, 0.5],
1388            [0.3, 0.3, 0.3],
1389            [0.7, 0.7, 0.7],
1390            1.0,
1391        );
1392        assert!(result[0] <= 0.7);
1393        assert!(result[1] >= 0.3);
1394    }
1395
1396    #[test]
1397    fn test_variance_clip() {
1398        let result = TaaPass::variance_clip(
1399            [5.0, -1.0, 0.5],
1400            [0.5, 0.5, 0.5],
1401            [0.01, 0.01, 0.01],
1402            1.0,
1403        );
1404        assert!((result[0] - 0.6).abs() < 0.01);
1405    }
1406
1407    #[test]
1408    fn test_taa_blend_factor() {
1409        let taa = TaaPass::new();
1410        // With zero velocity
1411        let blend = taa.compute_blend_factor(0.0);
1412        assert_eq!(blend, 0.0); // history not valid yet
1413
1414        let mut taa2 = TaaPass::new();
1415        taa2.history_valid = true;
1416        let blend = taa2.compute_blend_factor(0.0);
1417        assert!((blend - 0.9).abs() < 0.01);
1418
1419        // With high velocity
1420        let blend_fast = taa2.compute_blend_factor(0.1);
1421        assert!(blend_fast < blend);
1422    }
1423
1424    #[test]
1425    fn test_msaa_sample_count() {
1426        assert_eq!(MsaaSampleCount::X2.count(), 2);
1427        assert_eq!(MsaaSampleCount::X4.count(), 4);
1428        assert_eq!(MsaaSampleCount::X8.count(), 8);
1429    }
1430
1431    #[test]
1432    fn test_msaa_sample_positions() {
1433        let positions = MsaaSampleCount::X4.sample_positions();
1434        assert_eq!(positions.len(), 4);
1435        for pos in &positions {
1436            assert!(pos[0] >= -0.5 && pos[0] <= 0.5);
1437            assert!(pos[1] >= -0.5 && pos[1] <= 0.5);
1438        }
1439    }
1440
1441    #[test]
1442    fn test_msaa_memory() {
1443        let config = MsaaConfig::new(MsaaSampleCount::X4);
1444        assert_eq!(config.memory_multiplier(), 4.0);
1445    }
1446
1447    #[test]
1448    fn test_cas_no_sharpen() {
1449        let pass = SharpeningPass::new();
1450        // When disabled, should return center pixel unchanged
1451        let result = pass.sharpen_pixel(5, 5, |_x, _y| [0.5, 0.3, 0.7]);
1452        assert!((result[0] - 0.5).abs() < 0.01);
1453    }
1454
1455    #[test]
1456    fn test_cas_sharpen_uniform() {
1457        let mut pass = SharpeningPass::new();
1458        pass.enabled = true;
1459        pass.config.sharpness = 0.5;
1460        // Uniform image should not change
1461        let result = pass.sharpen_pixel(5, 5, |_x, _y| [0.5, 0.5, 0.5]);
1462        assert!((result[0] - 0.5).abs() < 0.01);
1463    }
1464
1465    #[test]
1466    fn test_cas_config() {
1467        let config = CasConfig::new(0.8);
1468        assert!((config.sharpness - 0.8).abs() < 0.01);
1469
1470        let clamped = CasConfig::new(2.0);
1471        assert!((clamped.sharpness - 1.0).abs() < 0.01);
1472    }
1473
1474    #[test]
1475    fn test_fxaa_shader_generation() {
1476        let fxaa = FxaaPass::new().with_quality(FxaaQuality::Low);
1477        let shader = fxaa.fragment_shader();
1478        assert!(shader.contains("#version 330 core"));
1479        assert!(shader.contains("luma"));
1480    }
1481
1482    #[test]
1483    fn test_taa_shader_generation() {
1484        let taa = TaaPass::new();
1485        let shader = taa.fragment_shader();
1486        assert!(shader.contains("#version 330 core"));
1487        assert!(shader.contains("u_history"));
1488    }
1489
1490    #[test]
1491    fn test_cas_shader_generation() {
1492        let pass = SharpeningPass::new().with_sharpness(0.75);
1493        let shader = pass.fragment_shader();
1494        assert!(shader.contains("#version 330 core"));
1495        assert!(shader.contains("u_sharpness"));
1496    }
1497
1498    #[test]
1499    fn test_taa_invalidate_history() {
1500        let mut taa = TaaPass::new();
1501        taa.history_valid = true;
1502        taa.invalidate_history();
1503        assert!(!taa.history_valid);
1504    }
1505
1506    #[test]
1507    fn test_taa_config_presets() {
1508        let hq = TaaConfig::high_quality();
1509        let fast = TaaConfig::fast();
1510        assert!(hq.history_blend > fast.history_blend);
1511        assert!(hq.variance_clipping);
1512        assert!(!fast.variance_clipping);
1513    }
1514}