Skip to main content

pfx_live/
taa.rs

1use pfx_gpu::wgpu;
2
3pub const CYCLE: u32 = 8;
4pub const COLOUR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
5pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
6pub const MOTION_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rg16Float;
7pub const REACTIVE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
8pub const ID_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
9pub const NORMAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Snorm;
10
11const HISTORY_VALID: u32 = 1;
12const HAS_ID: u32 = 2;
13const HAS_NORMAL: u32 = 4;
14const WORKGROUP: u32 = 8;
15
16const OFFSETS: [[f32; 2]; CYCLE as usize] = offsets();
17
18const fn halton_f64(mut index: u32, base: u32) -> f64 {
19    let mut fraction = 1.0f64;
20    let mut result = 0.0f64;
21    let step = base as f64;
22    while index > 0 {
23        fraction /= step;
24        result += fraction * (index % base) as f64;
25        index /= base;
26    }
27    result
28}
29
30const fn offsets() -> [[f32; 2]; CYCLE as usize] {
31    let mut raw = [(0.0f64, 0.0f64); CYCLE as usize];
32    let mut sum_x = 0.0f64;
33    let mut sum_y = 0.0f64;
34    let mut index = 0u32;
35    while index < CYCLE {
36        let x = halton_f64(index, 2);
37        let y = halton_f64(index, 3);
38        raw[index as usize] = (x, y);
39        sum_x += x;
40        sum_y += y;
41        index += 1;
42    }
43    let mut out = [[0.0f32; 2]; CYCLE as usize];
44    index = 0;
45    while index < CYCLE {
46        out[index as usize] = [
47            (raw[index as usize].0 - sum_x / CYCLE as f64) as f32,
48            (raw[index as usize].1 - sum_y / CYCLE as f64) as f32,
49        ];
50        index += 1;
51    }
52    out
53}
54
55pub fn halton(index: u32, base: u32) -> f32 {
56    if base < 2 {
57        return 0.0;
58    }
59    halton_f64(index, base) as f32
60}
61
62pub fn jitter_pixels(frame: u32) -> [f32; 2] {
63    OFFSETS[(frame % CYCLE) as usize]
64}
65
66pub fn jitter_clip(frame: u32, width: u32, height: u32) -> [f32; 2] {
67    if width == 0 || height == 0 {
68        return [0.0, 0.0];
69    }
70    let pixels = jitter_pixels(frame);
71    [
72        pixels[0] * 2.0 / width as f32,
73        pixels[1] * 2.0 / height as f32,
74    ]
75}
76
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct Settings {
79    pub history_weight: f32,
80    pub variance_gamma: f32,
81    pub depth_threshold: f32,
82    pub normal_min_dot: f32,
83}
84
85impl Default for Settings {
86    fn default() -> Self {
87        Self {
88            history_weight: 1.0 - 1.0 / CYCLE as f32,
89            variance_gamma: 1.0,
90            depth_threshold: 0.1,
91            normal_min_dot: 0.9,
92        }
93    }
94}
95
96#[derive(Clone, Debug, PartialEq)]
97pub struct Inputs {
98    pub width: u32,
99    pub height: u32,
100    pub colour: Vec<[f32; 3]>,
101    pub depth: Vec<f32>,
102    pub motion: Vec<[f32; 2]>,
103    pub reactive: Vec<f32>,
104    pub id: Option<Vec<u32>>,
105    pub normal: Option<Vec<[f32; 3]>>,
106    pub exposure: f32,
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct History {
111    pub width: u32,
112    pub height: u32,
113    pub rgba: Vec<[f32; 4]>,
114    pub depth: Vec<f32>,
115    pub id: Vec<u32>,
116    pub normal: Vec<[f32; 3]>,
117    pub exposure: f32,
118    pub valid: bool,
119}
120
121impl History {
122    pub fn empty() -> Self {
123        Self {
124            width: 0,
125            height: 0,
126            rgba: Vec::new(),
127            depth: Vec::new(),
128            id: Vec::new(),
129            normal: Vec::new(),
130            exposure: 1.0,
131            valid: false,
132        }
133    }
134}
135
136#[derive(Clone, Debug, PartialEq)]
137pub struct Output {
138    pub colour: Vec<[f32; 3]>,
139    pub history: History,
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum Error {
144    Size,
145    Length,
146    Exposure,
147    Settings,
148}
149
150pub const RESOLVE_WGSL: &str = r#"
151struct Uniforms {
152    width: u32,
153    height: u32,
154    flags: u32,
155    exposure: f32,
156    history_exposure: f32,
157    depth_threshold: f32,
158    normal_min_dot: f32,
159    variance_gamma: f32,
160    history_weight: f32,
161    history_width: u32,
162    history_height: u32,
163}
164
165@group(0) @binding(0) var<uniform> u: Uniforms;
166@group(0) @binding(1) var colour_tex: texture_2d<f32>;
167@group(0) @binding(2) var depth_tex: texture_2d<f32>;
168@group(0) @binding(3) var motion_tex: texture_2d<f32>;
169@group(0) @binding(4) var id_tex: texture_2d<u32>;
170@group(0) @binding(5) var normal_tex: texture_2d<f32>;
171@group(0) @binding(6) var reactive_tex: texture_2d<f32>;
172@group(0) @binding(7) var history_colour: texture_2d<f32>;
173@group(0) @binding(8) var history_depth: texture_2d<f32>;
174@group(0) @binding(9) var history_id: texture_2d<u32>;
175@group(0) @binding(10) var history_normal: texture_2d<f32>;
176@group(0) @binding(11) var out_colour: texture_storage_2d<rgba16float, write>;
177@group(0) @binding(12) var out_depth: texture_storage_2d<r32float, write>;
178@group(0) @binding(13) var out_id: texture_storage_2d<r32uint, write>;
179@group(0) @binding(14) var out_normal: texture_storage_2d<rgba8snorm, write>;
180
181fn rgb_to_ycocg(c: vec3<f32>) -> vec3<f32> {
182    return vec3<f32>(
183        0.25 * c.r + 0.5 * c.g + 0.25 * c.b,
184        0.5 * c.r - 0.5 * c.b,
185        -0.25 * c.r + 0.5 * c.g - 0.25 * c.b,
186    );
187}
188
189fn ycocg_to_rgb(c: vec3<f32>) -> vec3<f32> {
190    return vec3<f32>(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z);
191}
192
193fn outside(uv: vec2<f32>) -> bool {
194    return uv.x < 0.0 || uv.y < 0.0 || uv.x >= 1.0 || uv.y >= 1.0;
195}
196
197fn clamp_pixel(p: vec2<i32>, size: vec2<i32>) -> vec2<i32> {
198    return clamp(p, vec2<i32>(0), size - vec2<i32>(1));
199}
200
201fn load3(tex: texture_2d<f32>, p: vec2<i32>, size: vec2<i32>) -> vec3<f32> {
202    return textureLoad(tex, clamp_pixel(p, size), 0).rgb;
203}
204
205fn bilinear3(tex: texture_2d<f32>, uv: vec2<f32>, size: vec2<u32>) -> vec3<f32> {
206    let s = vec2<i32>(size);
207    let pf = uv * vec2<f32>(size) - vec2<f32>(0.5);
208    let base = vec2<i32>(floor(pf));
209    let f = pf - floor(pf);
210    let c00 = load3(tex, base, s);
211    let c10 = load3(tex, base + vec2<i32>(1, 0), s);
212    let c01 = load3(tex, base + vec2<i32>(0, 1), s);
213    let c11 = load3(tex, base + vec2<i32>(1, 1), s);
214    return mix(mix(c00, c10, f.x), mix(c01, c11, f.x), f.y);
215}
216
217fn nearest(uv: vec2<f32>, size: vec2<u32>) -> vec2<i32> {
218    return clamp_pixel(vec2<i32>(floor(uv * vec2<f32>(size))), vec2<i32>(size));
219}
220
221fn clip_aabb(lo: vec3<f32>, hi: vec3<f32>, p: vec3<f32>) -> vec3<f32> {
222    let center = 0.5 * (hi + lo);
223    let extent = max(0.5 * (hi - lo), vec3<f32>(1.0e-8));
224    let delta = p - center;
225    let unit = delta / extent;
226    let m = max(abs(unit.x), max(abs(unit.y), abs(unit.z)));
227    if (m > 1.0) {
228        return center + delta / m;
229    }
230    return p;
231}
232
233@compute @workgroup_size(8, 8)
234fn resolve(@builtin(global_invocation_id) gid: vec3<u32>) {
235    let size = vec2<u32>(u.width, u.height);
236    if (gid.x >= size.x || gid.y >= size.y) {
237        return;
238    }
239    let pix = vec2<i32>(gid.xy);
240    let uv = (vec2<f32>(gid.xy) + vec2<f32>(0.5)) / vec2<f32>(size);
241    let current = textureLoad(colour_tex, pix, 0).rgb;
242    let depth = textureLoad(depth_tex, pix, 0).r;
243    let motion = textureLoad(motion_tex, pix, 0).rg;
244    let reactive = clamp(textureLoad(reactive_tex, pix, 0).r, 0.0, 1.0);
245    var pixel_id = 0u;
246    if ((u.flags & 2u) != 0u) {
247        pixel_id = textureLoad(id_tex, pix, 0).r;
248    }
249    var nrm = vec3<f32>(0.0);
250    if ((u.flags & 4u) != 0u) {
251        nrm = textureLoad(normal_tex, pix, 0).rgb;
252    }
253
254    var acc = vec3<f32>(0.0);
255    var acc2 = vec3<f32>(0.0);
256    let s = vec2<i32>(size);
257    for (var oy = -1; oy <= 1; oy = oy + 1) {
258        for (var ox = -1; ox <= 1; ox = ox + 1) {
259            let texel = load3(colour_tex, pix + vec2<i32>(ox, oy), s) * u.exposure;
260            let ycocg = rgb_to_ycocg(texel);
261            acc = acc + ycocg;
262            acc2 = acc2 + ycocg * ycocg;
263        }
264    }
265    let mean = acc / 9.0;
266    let sigma = sqrt(max(acc2 / 9.0 - mean * mean, vec3<f32>(0.0))) * u.variance_gamma;
267    let lo = mean - sigma;
268    let hi = mean + sigma;
269    let current_e = current * u.exposure;
270    var history_e = current_e;
271    var weight = 0.0;
272    if ((u.flags & 1u) != 0u) {
273        let prev = uv + motion;
274        if (!outside(prev)) {
275            let stored = max(u.history_exposure, 1.0e-8);
276            let history_size = vec2<u32>(u.history_width, u.history_height);
277            let history_hdr = bilinear3(history_colour, prev, history_size);
278            history_e = (history_hdr * stored) * (u.exposure / stored);
279            var accept = true;
280            let at = nearest(prev, history_size);
281            let previous_depth = textureLoad(history_depth, at, 0).r;
282            let scale = max(max(abs(depth), abs(previous_depth)), 1.0e-5);
283            if (abs(depth - previous_depth) / scale > u.depth_threshold) {
284                accept = false;
285            }
286            if ((u.flags & 2u) != 0u) {
287                let previous_id = textureLoad(history_id, at, 0).r;
288                if (previous_id != pixel_id) {
289                    accept = false;
290                }
291            }
292            if ((u.flags & 4u) != 0u) {
293                let previous_normal = textureLoad(history_normal, at, 0).rgb;
294                let current_len2 = dot(nrm, nrm);
295                let previous_len2 = dot(previous_normal, previous_normal);
296                if (current_len2 > 1.0e-12 && previous_len2 > 1.0e-12) {
297                    let aligned = dot(nrm, previous_normal) * inverseSqrt(current_len2 * previous_len2);
298                    if (aligned < u.normal_min_dot) {
299                        accept = false;
300                    }
301                }
302            }
303            if (accept) {
304                history_e = ycocg_to_rgb(clip_aabb(lo, hi, rgb_to_ycocg(history_e)));
305                weight = u.history_weight * (1.0 - reactive);
306            }
307        }
308    }
309    let resolved_e = history_e * weight + current_e * (1.0 - weight);
310    let resolved = resolved_e / u.exposure;
311    textureStore(out_colour, pix, vec4<f32>(resolved, u.exposure));
312    textureStore(out_depth, pix, vec4<f32>(depth, 0.0, 0.0, 0.0));
313    textureStore(out_id, pix, vec4<u32>(pixel_id, 0u, 0u, 0u));
314    textureStore(out_normal, pix, vec4<f32>(nrm, 0.0));
315}
316"#;
317
318fn finite_settings(settings: &Settings) -> Result<(f32, f32, f32, f32), Error> {
319    if !settings.history_weight.is_finite()
320        || !settings.variance_gamma.is_finite()
321        || !settings.depth_threshold.is_finite()
322        || !settings.normal_min_dot.is_finite()
323    {
324        return Err(Error::Settings);
325    }
326    Ok((
327        settings.history_weight.clamp(0.0, 1.0),
328        settings.variance_gamma.max(0.0),
329        settings.depth_threshold.max(0.0),
330        settings.normal_min_dot,
331    ))
332}
333
334fn pixel_count(width: u32, height: u32) -> Result<usize, Error> {
335    usize::try_from(u64::from(width) * u64::from(height)).map_err(|_| Error::Size)
336}
337
338fn rgb_to_ycocg(c: [f32; 3]) -> [f32; 3] {
339    [
340        0.25 * c[0] + 0.5 * c[1] + 0.25 * c[2],
341        0.5 * c[0] - 0.5 * c[2],
342        -0.25 * c[0] + 0.5 * c[1] - 0.25 * c[2],
343    ]
344}
345
346fn ycocg_to_rgb(c: [f32; 3]) -> [f32; 3] {
347    [c[0] + c[1] - c[2], c[0] + c[2], c[0] - c[1] - c[2]]
348}
349
350fn outside(uv: [f32; 2]) -> bool {
351    uv[0] < 0.0 || uv[1] < 0.0 || uv[0] >= 1.0 || uv[1] >= 1.0
352}
353
354fn clamp_index(value: i32, size: i32) -> i32 {
355    value.clamp(0, size - 1)
356}
357
358fn load3(tex: &[[f32; 3]], width: i32, height: i32, x: i32, y: i32) -> [f32; 3] {
359    let x = clamp_index(x, width);
360    let y = clamp_index(y, height);
361    tex[(y as u32 * width as u32 + x as u32) as usize]
362}
363
364fn bilinear3(tex: &[[f32; 3]], width: u32, height: u32, uv: [f32; 2]) -> [f32; 3] {
365    let w = width as i32;
366    let h = height as i32;
367    let px = uv[0] * width as f32 - 0.5;
368    let py = uv[1] * height as f32 - 0.5;
369    let x0 = px.floor();
370    let y0 = py.floor();
371    let fx = px - x0;
372    let fy = py - y0;
373    let x0 = x0 as i32;
374    let y0 = y0 as i32;
375    let c00 = load3(tex, w, h, x0, y0);
376    let c10 = load3(tex, w, h, x0 + 1, y0);
377    let c01 = load3(tex, w, h, x0, y0 + 1);
378    let c11 = load3(tex, w, h, x0 + 1, y0 + 1);
379    let mut top = [0.0; 3];
380    let mut bottom = [0.0; 3];
381    let mut out = [0.0; 3];
382    for channel in 0..3 {
383        top[channel] = c00[channel] * (1.0 - fx) + c10[channel] * fx;
384        bottom[channel] = c01[channel] * (1.0 - fx) + c11[channel] * fx;
385        out[channel] = top[channel] * (1.0 - fy) + bottom[channel] * fy;
386    }
387    out
388}
389
390fn nearest(uv: f32, size: u32) -> i32 {
391    clamp_index((uv * size as f32).floor() as i32, size as i32)
392}
393
394fn clip_aabb(lo: [f32; 3], hi: [f32; 3], p: [f32; 3]) -> [f32; 3] {
395    let mut center = [0.0; 3];
396    let mut extent = [0.0; 3];
397    let mut delta = [0.0; 3];
398    let mut unit = [0.0; 3];
399    for channel in 0..3 {
400        center[channel] = 0.5 * (hi[channel] + lo[channel]);
401        extent[channel] = (0.5 * (hi[channel] - lo[channel])).max(1.0e-8);
402        delta[channel] = p[channel] - center[channel];
403        unit[channel] = delta[channel] / extent[channel];
404    }
405    let m = unit[0].abs().max(unit[1].abs()).max(unit[2].abs());
406    if m > 1.0 {
407        [
408            center[0] + delta[0] / m,
409            center[1] + delta[1] / m,
410            center[2] + delta[2] / m,
411        ]
412    } else {
413        p
414    }
415}
416
417fn depth_rejects(current: f32, previous: f32, threshold: f32) -> bool {
418    let scale = current.abs().max(previous.abs()).max(1.0e-5);
419    (current - previous).abs() / scale > threshold
420}
421
422fn normal_rejects(current: [f32; 3], previous: [f32; 3], min_dot: f32) -> bool {
423    let current_len =
424        (current[0] * current[0] + current[1] * current[1] + current[2] * current[2]).sqrt();
425    let previous_len =
426        (previous[0] * previous[0] + previous[1] * previous[1] + previous[2] * previous[2]).sqrt();
427    if current_len <= 1.0e-6 || previous_len <= 1.0e-6 {
428        return false;
429    }
430    let aligned = (current[0] / current_len) * (previous[0] / previous_len)
431        + (current[1] / current_len) * (previous[1] / previous_len)
432        + (current[2] / current_len) * (previous[2] / previous_len);
433    aligned < min_dot
434}
435
436pub fn resolve(inputs: &Inputs, history: &History, settings: &Settings) -> Result<Output, Error> {
437    let (history_weight, variance_gamma, depth_threshold, normal_min_dot) =
438        finite_settings(settings)?;
439    if !inputs.exposure.is_finite() || inputs.exposure <= 0.0 {
440        return Err(Error::Exposure);
441    }
442    let n = pixel_count(inputs.width, inputs.height)?;
443    if inputs.width == 0 || inputs.height == 0 {
444        return Ok(Output {
445            colour: Vec::new(),
446            history: History::empty(),
447        });
448    }
449    if inputs.colour.len() != n
450        || inputs.depth.len() != n
451        || inputs.motion.len() != n
452        || inputs.reactive.len() != n
453        || inputs.id.as_ref().is_some_and(|id| id.len() != n)
454        || inputs
455            .normal
456            .as_ref()
457            .is_some_and(|normal| normal.len() != n)
458    {
459        return Err(Error::Length);
460    }
461    let stored = pixel_count(history.width, history.height)?;
462    let use_history = history.valid
463        && history.width > 0
464        && history.height > 0
465        && history.rgba.len() == stored
466        && history.depth.len() == stored
467        && history.id.len() == stored
468        && history.normal.len() == stored
469        && history.exposure.is_finite()
470        && history.exposure > 0.0;
471    let stored_exposure = if use_history {
472        history.exposure.max(1.0e-8)
473    } else {
474        1.0
475    };
476    let history_hdr: Vec<[f32; 3]> = if use_history {
477        history
478            .rgba
479            .iter()
480            .map(|pixel| [pixel[0], pixel[1], pixel[2]])
481            .collect()
482    } else {
483        Vec::new()
484    };
485    let width = inputs.width as i32;
486    let height = inputs.height as i32;
487    let mut colour = Vec::with_capacity(n);
488    let mut rgba = Vec::with_capacity(n);
489    let mut depth_out = Vec::with_capacity(n);
490    let mut id_out = Vec::with_capacity(n);
491    let mut normal_out = Vec::with_capacity(n);
492    for y in 0..inputs.height {
493        for x in 0..inputs.width {
494            let index = (y * inputs.width + x) as usize;
495            let uv = [
496                (x as f32 + 0.5) / inputs.width as f32,
497                (y as f32 + 0.5) / inputs.height as f32,
498            ];
499            let current = inputs.colour[index];
500            let depth = inputs.depth[index];
501            let motion = inputs.motion[index];
502            let reactive = inputs.reactive[index].clamp(0.0, 1.0);
503            let pixel_id = inputs.id.as_ref().map(|id| id[index]).unwrap_or(0);
504            let nrm = inputs
505                .normal
506                .as_ref()
507                .map(|normal| normal[index])
508                .unwrap_or([0.0; 3]);
509            let mut acc = [0.0; 3];
510            let mut acc2 = [0.0; 3];
511            for oy in -1..=1 {
512                for ox in -1..=1 {
513                    let texel = load3(&inputs.colour, width, height, x as i32 + ox, y as i32 + oy);
514                    let exposed = [
515                        texel[0] * inputs.exposure,
516                        texel[1] * inputs.exposure,
517                        texel[2] * inputs.exposure,
518                    ];
519                    let ycocg = rgb_to_ycocg(exposed);
520                    for channel in 0..3 {
521                        acc[channel] += ycocg[channel];
522                        acc2[channel] += ycocg[channel] * ycocg[channel];
523                    }
524                }
525            }
526            let mut mean = [0.0; 3];
527            let mut lo = [0.0; 3];
528            let mut hi = [0.0; 3];
529            for channel in 0..3 {
530                mean[channel] = acc[channel] / 9.0;
531                let variance = (acc2[channel] / 9.0 - mean[channel] * mean[channel]).max(0.0);
532                let sigma = variance.sqrt() * variance_gamma;
533                lo[channel] = mean[channel] - sigma;
534                hi[channel] = mean[channel] + sigma;
535            }
536            let current_e = [
537                current[0] * inputs.exposure,
538                current[1] * inputs.exposure,
539                current[2] * inputs.exposure,
540            ];
541            let mut history_e = current_e;
542            let mut weight = 0.0;
543            if use_history {
544                let prev = [uv[0] + motion[0], uv[1] + motion[1]];
545                if !outside(prev) {
546                    let history_hdr = bilinear3(&history_hdr, history.width, history.height, prev);
547                    history_e = [
548                        (history_hdr[0] * stored_exposure) * (inputs.exposure / stored_exposure),
549                        (history_hdr[1] * stored_exposure) * (inputs.exposure / stored_exposure),
550                        (history_hdr[2] * stored_exposure) * (inputs.exposure / stored_exposure),
551                    ];
552                    let at_x = nearest(prev[0], history.width);
553                    let at_y = nearest(prev[1], history.height);
554                    let at = (at_y as u32 * history.width + at_x as u32) as usize;
555                    let mut accept = !depth_rejects(depth, history.depth[at], depth_threshold);
556                    if accept && inputs.id.is_some() && history.id[at] != pixel_id {
557                        accept = false;
558                    }
559                    if accept
560                        && inputs.normal.is_some()
561                        && normal_rejects(nrm, history.normal[at], normal_min_dot)
562                    {
563                        accept = false;
564                    }
565                    if accept {
566                        history_e = ycocg_to_rgb(clip_aabb(lo, hi, rgb_to_ycocg(history_e)));
567                        weight = history_weight * (1.0 - reactive);
568                    }
569                }
570            }
571            let resolved_e = [
572                history_e[0] * weight + current_e[0] * (1.0 - weight),
573                history_e[1] * weight + current_e[1] * (1.0 - weight),
574                history_e[2] * weight + current_e[2] * (1.0 - weight),
575            ];
576            let resolved = [
577                resolved_e[0] / inputs.exposure,
578                resolved_e[1] / inputs.exposure,
579                resolved_e[2] / inputs.exposure,
580            ];
581            colour.push(resolved);
582            rgba.push([resolved[0], resolved[1], resolved[2], inputs.exposure]);
583            depth_out.push(depth);
584            id_out.push(pixel_id);
585            normal_out.push(nrm);
586        }
587    }
588    Ok(Output {
589        colour,
590        history: History {
591            width: inputs.width,
592            height: inputs.height,
593            rgba,
594            depth: depth_out,
595            id: id_out,
596            normal: normal_out,
597            exposure: inputs.exposure,
598            valid: true,
599        },
600    })
601}
602
603struct Slot {
604    colour: wgpu::Texture,
605    colour_view: wgpu::TextureView,
606    depth_view: wgpu::TextureView,
607    id_view: wgpu::TextureView,
608    normal_view: wgpu::TextureView,
609    textures: [wgpu::Texture; 3],
610}
611
612pub struct GpuInputs<'a> {
613    pub colour: &'a wgpu::TextureView,
614    pub depth: &'a wgpu::TextureView,
615    pub motion: &'a wgpu::TextureView,
616    pub reactive: &'a wgpu::TextureView,
617    pub id: Option<&'a wgpu::TextureView>,
618    pub normal: Option<&'a wgpu::TextureView>,
619    pub exposure: f32,
620}
621
622pub struct Pass {
623    layout: wgpu::BindGroupLayout,
624    pipeline: wgpu::ComputePipeline,
625    uniform: wgpu::Buffer,
626    dummy_id: wgpu::Texture,
627    dummy_id_view: wgpu::TextureView,
628    dummy_normal: wgpu::Texture,
629    dummy_normal_view: wgpu::TextureView,
630    slots: [Option<Slot>; 2],
631    source: usize,
632    width: u32,
633    height: u32,
634    viewport: (u32, u32),
635    history_size: (u32, u32),
636    restarts: u64,
637    exposure: f32,
638    valid: bool,
639    dummies_ready: bool,
640}
641
642fn texture(
643    device: &wgpu::Device,
644    label: &'static str,
645    width: u32,
646    height: u32,
647    format: wgpu::TextureFormat,
648    usage: wgpu::TextureUsages,
649) -> (wgpu::Texture, wgpu::TextureView) {
650    let texture = device.create_texture(&wgpu::TextureDescriptor {
651        label: Some(label),
652        size: wgpu::Extent3d {
653            width,
654            height,
655            depth_or_array_layers: 1,
656        },
657        mip_level_count: 1,
658        sample_count: 1,
659        dimension: wgpu::TextureDimension::D2,
660        format,
661        usage,
662        view_formats: &[],
663    });
664    let view = texture.create_view(&Default::default());
665    (texture, view)
666}
667
668fn float_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
669    wgpu::BindGroupLayoutEntry {
670        binding,
671        visibility: wgpu::ShaderStages::COMPUTE,
672        ty: wgpu::BindingType::Texture {
673            sample_type: wgpu::TextureSampleType::Float { filterable: false },
674            view_dimension: wgpu::TextureViewDimension::D2,
675            multisampled: false,
676        },
677        count: None,
678    }
679}
680
681fn uint_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
682    wgpu::BindGroupLayoutEntry {
683        binding,
684        visibility: wgpu::ShaderStages::COMPUTE,
685        ty: wgpu::BindingType::Texture {
686            sample_type: wgpu::TextureSampleType::Uint,
687            view_dimension: wgpu::TextureViewDimension::D2,
688            multisampled: false,
689        },
690        count: None,
691    }
692}
693
694fn storage_entry(binding: u32, format: wgpu::TextureFormat) -> wgpu::BindGroupLayoutEntry {
695    wgpu::BindGroupLayoutEntry {
696        binding,
697        visibility: wgpu::ShaderStages::COMPUTE,
698        ty: wgpu::BindingType::StorageTexture {
699            access: wgpu::StorageTextureAccess::WriteOnly,
700            format,
701            view_dimension: wgpu::TextureViewDimension::D2,
702        },
703        count: None,
704    }
705}
706
707fn history_slot(device: &wgpu::Device, width: u32, height: u32, index: usize) -> Slot {
708    let usage = wgpu::TextureUsages::TEXTURE_BINDING
709        | wgpu::TextureUsages::STORAGE_BINDING
710        | wgpu::TextureUsages::COPY_SRC;
711    let (colour, colour_view) = texture(
712        device,
713        if index == 0 {
714            "taa history colour 0"
715        } else {
716            "taa history colour 1"
717        },
718        width,
719        height,
720        COLOUR_FORMAT,
721        usage,
722    );
723    let (depth, depth_view) = texture(
724        device,
725        if index == 0 {
726            "taa history depth 0"
727        } else {
728            "taa history depth 1"
729        },
730        width,
731        height,
732        DEPTH_FORMAT,
733        usage,
734    );
735    let (id, id_view) = texture(
736        device,
737        if index == 0 {
738            "taa history id 0"
739        } else {
740            "taa history id 1"
741        },
742        width,
743        height,
744        ID_FORMAT,
745        usage,
746    );
747    let (normal, normal_view) = texture(
748        device,
749        if index == 0 {
750            "taa history normal 0"
751        } else {
752            "taa history normal 1"
753        },
754        width,
755        height,
756        NORMAL_FORMAT,
757        usage,
758    );
759    Slot {
760        colour,
761        colour_view,
762        depth_view,
763        id_view,
764        normal_view,
765        textures: [depth, id, normal],
766    }
767}
768
769impl Pass {
770    pub fn new(device: &wgpu::Device) -> Self {
771        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
772            label: Some("taa resolve"),
773            entries: &[
774                wgpu::BindGroupLayoutEntry {
775                    binding: 0,
776                    visibility: wgpu::ShaderStages::COMPUTE,
777                    ty: wgpu::BindingType::Buffer {
778                        ty: wgpu::BufferBindingType::Uniform,
779                        has_dynamic_offset: false,
780                        min_binding_size: None,
781                    },
782                    count: None,
783                },
784                float_entry(1),
785                float_entry(2),
786                float_entry(3),
787                uint_entry(4),
788                float_entry(5),
789                float_entry(6),
790                float_entry(7),
791                float_entry(8),
792                uint_entry(9),
793                float_entry(10),
794                storage_entry(11, COLOUR_FORMAT),
795                storage_entry(12, DEPTH_FORMAT),
796                storage_entry(13, ID_FORMAT),
797                storage_entry(14, NORMAL_FORMAT),
798            ],
799        });
800        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
801            label: Some("taa resolve"),
802            bind_group_layouts: &[Some(&layout)],
803            immediate_size: 0,
804        });
805        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
806            label: Some("taa resolve"),
807            source: wgpu::ShaderSource::Wgsl(RESOLVE_WGSL.into()),
808        });
809        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
810            label: Some("taa resolve"),
811            layout: Some(&pipeline_layout),
812            module: &module,
813            entry_point: Some("resolve"),
814            compilation_options: Default::default(),
815            cache: None,
816        });
817        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
818            label: Some("taa uniforms"),
819            size: 256,
820            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
821            mapped_at_creation: false,
822        });
823        let (dummy_id, dummy_id_view) = texture(
824            device,
825            "taa dummy id",
826            1,
827            1,
828            ID_FORMAT,
829            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
830        );
831        let (dummy_normal, dummy_normal_view) = texture(
832            device,
833            "taa dummy normal",
834            1,
835            1,
836            NORMAL_FORMAT,
837            wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
838        );
839        Self {
840            layout,
841            pipeline,
842            uniform,
843            dummy_id,
844            dummy_id_view,
845            dummy_normal,
846            dummy_normal_view,
847            slots: [None, None],
848            source: 0,
849            width: 0,
850            height: 0,
851            viewport: (0, 0),
852            history_size: (0, 0),
853            restarts: 0,
854            exposure: 1.0,
855            valid: false,
856            dummies_ready: false,
857        }
858    }
859
860    pub fn width(&self) -> u32 {
861        self.viewport.0
862    }
863
864    pub fn height(&self) -> u32 {
865        self.viewport.1
866    }
867
868    pub fn capacity(&self) -> (u32, u32) {
869        (self.width, self.height)
870    }
871
872    pub fn history_valid(&self) -> bool {
873        self.valid
874    }
875
876    pub fn textures(&self) -> Vec<wgpu::Texture> {
877        self.slots
878            .iter()
879            .flatten()
880            .flat_map(|slot| std::iter::once(&slot.colour).chain(&slot.textures))
881            .cloned()
882            .collect()
883    }
884
885    pub fn restarts(&self) -> u64 {
886        self.restarts
887    }
888
889    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
890        if width == self.width && height == self.height && self.slots[0].is_some() {
891            self.viewport = (width, height);
892            return;
893        }
894        self.invalidate();
895        self.source = 0;
896        self.exposure = 1.0;
897        if width == 0 || height == 0 {
898            self.slots = [None, None];
899            self.width = 0;
900            self.height = 0;
901            self.viewport = (0, 0);
902            self.history_size = (0, 0);
903            return;
904        }
905        self.slots = [
906            Some(history_slot(device, width, height, 0)),
907            Some(history_slot(device, width, height, 1)),
908        ];
909        self.width = width;
910        self.height = height;
911        self.viewport = (width, height);
912        self.history_size = (width, height);
913    }
914
915    pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
916        crate::viewport::check([width, height], [self.width, self.height])?;
917        self.viewport = (width, height);
918        Ok(())
919    }
920
921    pub fn invalidate(&mut self) {
922        if self.valid {
923            self.restarts += 1;
924        }
925        self.valid = false;
926    }
927
928    pub fn history_view(&self) -> Option<&wgpu::TextureView> {
929        if !self.valid {
930            return None;
931        }
932        self.slots[self.source]
933            .as_ref()
934            .map(|slot| &slot.colour_view)
935    }
936
937    pub fn resolve(
938        &mut self,
939        device: &wgpu::Device,
940        queue: &wgpu::Queue,
941        encoder: &mut wgpu::CommandEncoder,
942        inputs: &GpuInputs<'_>,
943        settings: &Settings,
944    ) -> Result<(), String> {
945        self.resolve_timed(device, queue, encoder, inputs, settings, None)
946    }
947
948    pub fn resolve_timed(
949        &mut self,
950        device: &wgpu::Device,
951        queue: &wgpu::Queue,
952        encoder: &mut wgpu::CommandEncoder,
953        inputs: &GpuInputs<'_>,
954        settings: &Settings,
955        mut profiler: Option<&mut pfx_gpu::GpuProfiler>,
956    ) -> Result<(), String> {
957        let (width, height) = self.viewport;
958        if width == 0 || height == 0 || self.slots[0].is_none() {
959            return Err("viewport is empty".into());
960        }
961        if !inputs.exposure.is_finite() || inputs.exposure <= 0.0 {
962            return Err("exposure must be positive".into());
963        }
964        let (history_weight, variance_gamma, depth_threshold, normal_min_dot) =
965            finite_settings(settings).map_err(|_| "settings must be finite".to_string())?;
966        if !self.dummies_ready {
967            queue.write_texture(
968                self.dummy_id.as_image_copy(),
969                &[0, 0, 0, 0],
970                wgpu::TexelCopyBufferLayout {
971                    offset: 0,
972                    bytes_per_row: Some(4),
973                    rows_per_image: Some(1),
974                },
975                self.dummy_id.size(),
976            );
977            queue.write_texture(
978                self.dummy_normal.as_image_copy(),
979                &[0u8; 4],
980                wgpu::TexelCopyBufferLayout {
981                    offset: 0,
982                    bytes_per_row: Some(4),
983                    rows_per_image: Some(1),
984                },
985                self.dummy_normal.size(),
986            );
987            self.dummies_ready = true;
988        }
989        let mut flags = 0;
990        if self.valid {
991            flags |= HISTORY_VALID;
992        }
993        if inputs.id.is_some() {
994            flags |= HAS_ID;
995        }
996        if inputs.normal.is_some() {
997            flags |= HAS_NORMAL;
998        }
999        let mut bytes = Vec::with_capacity(44);
1000        for value in [
1001            width.to_le_bytes(),
1002            height.to_le_bytes(),
1003            flags.to_le_bytes(),
1004        ] {
1005            bytes.extend_from_slice(&value);
1006        }
1007        for value in [
1008            inputs.exposure,
1009            if self.valid { self.exposure } else { 1.0 },
1010            depth_threshold,
1011            normal_min_dot,
1012            variance_gamma,
1013            history_weight,
1014        ] {
1015            bytes.extend_from_slice(&value.to_le_bytes());
1016        }
1017        for value in [self.history_size.0, self.history_size.1] {
1018            bytes.extend_from_slice(&value.to_le_bytes());
1019        }
1020        queue.write_buffer(&self.uniform, 0, &bytes);
1021        let dest = 1 - self.source;
1022        let source = self.slots[self.source].as_ref().unwrap();
1023        let target = self.slots[dest].as_ref().unwrap();
1024        let id_view = inputs.id.unwrap_or(&self.dummy_id_view);
1025        let normal_view = inputs.normal.unwrap_or(&self.dummy_normal_view);
1026        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1027            label: Some("taa resolve"),
1028            layout: &self.layout,
1029            entries: &[
1030                wgpu::BindGroupEntry {
1031                    binding: 0,
1032                    resource: self.uniform.as_entire_binding(),
1033                },
1034                view_entry(1, inputs.colour),
1035                view_entry(2, inputs.depth),
1036                view_entry(3, inputs.motion),
1037                view_entry(4, id_view),
1038                view_entry(5, normal_view),
1039                view_entry(6, inputs.reactive),
1040                view_entry(7, &source.colour_view),
1041                view_entry(8, &source.depth_view),
1042                view_entry(9, &source.id_view),
1043                view_entry(10, &source.normal_view),
1044                view_entry(11, &target.colour_view),
1045                view_entry(12, &target.depth_view),
1046                view_entry(13, &target.id_view),
1047                view_entry(14, &target.normal_view),
1048            ],
1049        });
1050        let timing = profiler
1051            .as_deref_mut()
1052            .and_then(|timer| timer.pass("TAA resolve"));
1053        {
1054            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1055                label: Some("taa resolve"),
1056                timestamp_writes: profiler
1057                    .as_deref()
1058                    .and_then(|timer| timer.compute_writes(timing)),
1059            });
1060            pass.set_pipeline(&self.pipeline);
1061            pass.set_bind_group(0, &bind, &[]);
1062            pass.dispatch_workgroups(width.div_ceil(WORKGROUP), height.div_ceil(WORKGROUP), 1);
1063        }
1064        self.source = dest;
1065        self.exposure = inputs.exposure;
1066        self.valid = true;
1067        self.history_size = (width, height);
1068        Ok(())
1069    }
1070
1071    pub fn read_colour(
1072        &self,
1073        device: &wgpu::Device,
1074        queue: &wgpu::Queue,
1075    ) -> Result<Vec<[f32; 4]>, String> {
1076        if !self.valid {
1077            return Err("history is empty".into());
1078        }
1079        let slot = self.slots[self.source].as_ref().ok_or("history is empty")?;
1080        read_rgba16(
1081            device,
1082            queue,
1083            &slot.colour,
1084            self.history_size.0,
1085            self.history_size.1,
1086        )
1087    }
1088}
1089
1090fn view_entry(binding: u32, view: &wgpu::TextureView) -> wgpu::BindGroupEntry<'_> {
1091    wgpu::BindGroupEntry {
1092        binding,
1093        resource: wgpu::BindingResource::TextureView(view),
1094    }
1095}
1096
1097fn read_rgba16(
1098    device: &wgpu::Device,
1099    queue: &wgpu::Queue,
1100    texture: &wgpu::Texture,
1101    width: u32,
1102    height: u32,
1103) -> Result<Vec<[f32; 4]>, String> {
1104    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1105    let raw = width * 8;
1106    let row = raw.div_ceil(align) * align;
1107    let size = u64::from(row) * u64::from(height);
1108    let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1109        label: Some("taa readback"),
1110        size,
1111        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1112        mapped_at_creation: false,
1113    });
1114    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1115        label: Some("taa readback"),
1116    });
1117    encoder.copy_texture_to_buffer(
1118        texture.as_image_copy(),
1119        wgpu::TexelCopyBufferInfo {
1120            buffer: &buffer,
1121            layout: wgpu::TexelCopyBufferLayout {
1122                offset: 0,
1123                bytes_per_row: Some(row),
1124                rows_per_image: Some(height),
1125            },
1126        },
1127        wgpu::Extent3d {
1128            width,
1129            height,
1130            depth_or_array_layers: 1,
1131        },
1132    );
1133    queue.submit(Some(encoder.finish()));
1134    let (send, receive) = std::sync::mpsc::channel();
1135    let slice = buffer.slice(..);
1136    slice.map_async(wgpu::MapMode::Read, move |result| {
1137        let _ = send.send(result);
1138    });
1139    device
1140        .poll(wgpu::PollType::wait_indefinitely())
1141        .map_err(|error| format!("{error:?}"))?;
1142    receive
1143        .recv()
1144        .map_err(|error| error.to_string())?
1145        .map_err(|error| error.to_string())?;
1146    let mapped = slice.get_mapped_range().expect("a mapped range");
1147    let mut pixels = Vec::with_capacity((width * height) as usize);
1148    for padded in mapped.chunks_exact(row as usize) {
1149        for texel in padded[..raw as usize].chunks_exact(8) {
1150            pixels.push([
1151                half::f16::from_le_bytes(texel[0..2].try_into().unwrap()).to_f32(),
1152                half::f16::from_le_bytes(texel[2..4].try_into().unwrap()).to_f32(),
1153                half::f16::from_le_bytes(texel[4..6].try_into().unwrap()).to_f32(),
1154                half::f16::from_le_bytes(texel[6..8].try_into().unwrap()).to_f32(),
1155            ]);
1156        }
1157    }
1158    drop(mapped);
1159    buffer.unmap();
1160    Ok(pixels)
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166
1167    fn assert_near(actual: f32, expected: f32, tol: f32) {
1168        assert!(
1169            (actual - expected).abs() <= tol,
1170            "{actual} != {expected} within {tol}"
1171        );
1172    }
1173
1174    fn pattern(reactive: f32, depth: f32, motion: [f32; 2], exposure: f32) -> Inputs {
1175        let values = [1.0, 0.0, 1.0, 0.0, 0.25, 0.0, 1.0, 0.0, 1.0];
1176        Inputs {
1177            width: 3,
1178            height: 3,
1179            colour: values.into_iter().map(|v| [v, v, v]).collect(),
1180            depth: (0..9).map(|i| if i == 4 { depth } else { 0.5 }).collect(),
1181            motion: (0..9)
1182                .map(|i| if i == 4 { motion } else { [0.0, 0.0] })
1183                .collect(),
1184            reactive: (0..9)
1185                .map(|i| if i == 4 { reactive } else { 0.0 })
1186                .collect(),
1187            id: None,
1188            normal: None,
1189            exposure,
1190        }
1191    }
1192
1193    fn filled(hdr: f32, exposure: f32, depth: f32) -> History {
1194        History {
1195            width: 3,
1196            height: 3,
1197            rgba: vec![[hdr, hdr, hdr, exposure]; 9],
1198            depth: vec![depth; 9],
1199            id: vec![0; 9],
1200            normal: vec![[0.0, 0.0, 1.0]; 9],
1201            exposure,
1202            valid: true,
1203        }
1204    }
1205
1206    #[test]
1207    fn halton_values_and_cycle() {
1208        assert_eq!(halton(0, 2).to_bits(), 0.0f32.to_bits());
1209        assert_eq!(halton(1, 2).to_bits(), 0.5f32.to_bits());
1210        assert_eq!(halton(2, 2).to_bits(), 0.25f32.to_bits());
1211        assert_eq!(halton(3, 2).to_bits(), 0.75f32.to_bits());
1212        assert_eq!(halton(4, 2).to_bits(), 0.125f32.to_bits());
1213        assert_eq!(halton(7, 2).to_bits(), 0.875f32.to_bits());
1214        assert_near(halton(1, 3), 1.0 / 3.0, 1e-6);
1215        assert_near(halton(2, 3), 2.0 / 3.0, 1e-6);
1216        assert_near(halton(3, 3), 1.0 / 9.0, 1e-6);
1217        assert_near(halton(4, 3), 4.0 / 9.0, 1e-6);
1218        assert_near(halton(5, 3), 7.0 / 9.0, 1e-6);
1219        assert_near(halton(6, 3), 2.0 / 9.0, 1e-6);
1220        assert_near(halton(7, 3), 5.0 / 9.0, 1e-6);
1221        assert_eq!(halton(1, 0).to_bits(), 0.0f32.to_bits());
1222        let expected_x = [
1223            -0.4375f32, 0.0625, -0.1875, 0.3125, -0.3125, 0.1875, -0.0625, 0.4375,
1224        ];
1225        for frame in 0..CYCLE {
1226            assert_eq!(
1227                jitter_pixels(frame)[0].to_bits(),
1228                expected_x[frame as usize].to_bits()
1229            );
1230            assert_eq!(
1231                jitter_pixels(frame)[0].to_bits(),
1232                jitter_pixels(frame + CYCLE)[0].to_bits()
1233            );
1234            assert_eq!(
1235                jitter_pixels(frame)[1].to_bits(),
1236                jitter_pixels(frame + CYCLE * 3)[1].to_bits()
1237            );
1238        }
1239        for frame in 0..CYCLE {
1240            for other in 0..frame {
1241                let a = jitter_pixels(frame);
1242                let b = jitter_pixels(other);
1243                assert!(a[0].to_bits() != b[0].to_bits() || a[1].to_bits() != b[1].to_bits());
1244            }
1245        }
1246    }
1247
1248    #[test]
1249    fn jitter_averages_to_zero() {
1250        let mut sum = [0.0f32; 2];
1251        for frame in 0..CYCLE {
1252            let jitter = jitter_pixels(frame);
1253            assert!(jitter[0].abs() <= 0.5 && jitter[1].abs() <= 0.5);
1254            sum[0] += jitter[0];
1255            sum[1] += jitter[1];
1256            let clip = jitter_clip(frame, 320, 180);
1257            assert_near(clip[0], jitter[0] * 2.0 / 320.0, 1e-7);
1258            assert_near(clip[1], jitter[1] * 2.0 / 180.0, 1e-7);
1259        }
1260        assert_near(sum[0], 0.0, 1e-6);
1261        assert_near(sum[1], 0.0, 1e-6);
1262        assert_eq!(jitter_clip(0, 0, 10)[0].to_bits(), 0.0f32.to_bits());
1263        assert_eq!(jitter_clip(0, 10, 0)[1].to_bits(), 0.0f32.to_bits());
1264    }
1265
1266    #[test]
1267    fn ycocg_roundtrip_and_clip() {
1268        let colour = [0.2, 0.5, 0.8];
1269        let back = ycocg_to_rgb(rgb_to_ycocg(colour));
1270        for channel in 0..3 {
1271            assert_near(back[channel], colour[channel], 1e-6);
1272        }
1273        let inside = clip_aabb([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.25, 0.5, 0.75]);
1274        assert_eq!(inside[0].to_bits(), 0.25f32.to_bits());
1275        let clipped = clip_aabb([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
1276        assert_near(clipped[0], 1.0, 1e-5);
1277        assert_near(clipped[1], 0.0, 1e-5);
1278    }
1279
1280    fn coverage(pixel: u32, edge: f32) -> f32 {
1281        let start = pixel as f32;
1282        let end = start + 1.0;
1283        if end <= edge {
1284            0.0
1285        } else if start >= edge {
1286            1.0
1287        } else {
1288            end - edge
1289        }
1290    }
1291
1292    fn edge_frame(frame: u32) -> Inputs {
1293        let width = 8;
1294        let height = 4;
1295        let edge = 3.35;
1296        let jitter = jitter_pixels(frame);
1297        let mut colour = Vec::with_capacity((width * height) as usize);
1298        for _y in 0..height {
1299            for x in 0..width {
1300                let sample = x as f32 + 0.5 + jitter[0];
1301                let value = if sample >= edge { 1.0 } else { 0.0 };
1302                colour.push([value, value, value]);
1303            }
1304        }
1305        Inputs {
1306            width,
1307            height,
1308            colour,
1309            depth: vec![1.0; (width * height) as usize],
1310            motion: vec![[0.0, 0.0]; (width * height) as usize],
1311            reactive: vec![0.0; (width * height) as usize],
1312            id: None,
1313            normal: None,
1314            exposure: 1.0,
1315        }
1316    }
1317
1318    #[test]
1319    fn static_jittered_edge_converges() {
1320        let edge = 3.35;
1321        let index = 8 + 3;
1322        let mut saw_black = false;
1323        let mut saw_white = false;
1324        for frame in 0..CYCLE {
1325            let value = edge_frame(frame).colour[index][0];
1326            saw_black |= value.to_bits() == 0.0f32.to_bits();
1327            saw_white |= value.to_bits() == 1.0f32.to_bits();
1328        }
1329        assert!(saw_black && saw_white);
1330        let mut history = History::empty();
1331        let mut tail = Vec::new();
1332        for frame in 0..64 {
1333            let output = resolve(&edge_frame(frame), &history, &Settings::default()).unwrap();
1334            if frame >= 56 {
1335                tail.push(output.colour[index][0]);
1336            }
1337            history = output.history;
1338        }
1339        let mean = tail.iter().sum::<f32>() / tail.len() as f32;
1340        let expected = coverage(3, edge);
1341        assert_near(mean, expected, 0.05);
1342        assert!(mean > 0.4 && mean < 0.9);
1343        let flat_black = history.rgba[8][0];
1344        let flat_white = history.rgba[8 + 7][0];
1345        assert_near(flat_black, coverage(0, edge), 0.02);
1346        assert_near(flat_white, coverage(7, edge), 0.02);
1347    }
1348
1349    #[test]
1350    fn depth_discontinuity_rejects_history() {
1351        let current = pattern(0.0, 0.8, [0.0, 0.0], 1.0);
1352        let rejected = resolve(&current, &filled(0.75, 1.0, 0.2), &Settings::default()).unwrap();
1353        assert_near(rejected.colour[4][0], 0.25, 1e-5);
1354        let kept = resolve(
1355            &pattern(0.0, 0.5, [0.0, 0.0], 1.0),
1356            &filled(0.75, 1.0, 0.5),
1357            &Settings::default(),
1358        )
1359        .unwrap();
1360        assert_near(kept.colour[4][0], 0.6875, 1e-5);
1361        let mut resized = filled(0.75, 1.0, 0.5);
1362        resized.width = 2;
1363        let dropped = resolve(
1364            &pattern(0.0, 0.5, [0.0, 0.0], 1.0),
1365            &resized,
1366            &Settings::default(),
1367        )
1368        .unwrap();
1369        assert_near(dropped.colour[4][0], 0.25, 1e-5);
1370    }
1371
1372    #[test]
1373    fn a_history_of_another_size_is_resampled_not_dropped() {
1374        let smaller = History {
1375            width: 2,
1376            height: 2,
1377            rgba: vec![[0.75, 0.75, 0.75, 1.0]; 4],
1378            depth: vec![0.5; 4],
1379            id: vec![0; 4],
1380            normal: vec![[0.0, 0.0, 1.0]; 4],
1381            exposure: 1.0,
1382            valid: true,
1383        };
1384        let kept = resolve(
1385            &pattern(0.0, 0.5, [0.0, 0.0], 1.0),
1386            &smaller,
1387            &Settings::default(),
1388        )
1389        .unwrap();
1390        assert_near(kept.colour[4][0], 0.6875, 1e-5);
1391        assert_eq!((kept.history.width, kept.history.height), (3, 3));
1392        let mut ramp = smaller.clone();
1393        ramp.width = 4;
1394        ramp.height = 1;
1395        ramp.rgba = (0..4)
1396            .map(|x| {
1397                let v = x as f32 / 3.0;
1398                [v, v, v, 1.0]
1399            })
1400            .collect();
1401        let mut current = pattern(1.0, 0.5, [0.0, 0.0], 1.0);
1402        current.reactive = vec![0.0; 9];
1403        let settings = Settings {
1404            history_weight: 1.0,
1405            variance_gamma: 1.0e6,
1406            ..Settings::default()
1407        };
1408        let resampled = resolve(&current, &ramp, &settings).unwrap();
1409        for x in 0..3 {
1410            let u = (x as f32 + 0.5) / 3.0 * 4.0 - 0.5;
1411            assert_near(resampled.colour[x][0], u.clamp(0.0, 3.0) / 3.0, 1e-5);
1412        }
1413    }
1414
1415    #[test]
1416    fn reactive_mask_limits_history() {
1417        let history = filled(0.75, 1.0, 0.5);
1418        let open = resolve(
1419            &pattern(0.0, 0.5, [0.0, 0.0], 1.0),
1420            &history,
1421            &Settings::default(),
1422        )
1423        .unwrap();
1424        let shut = resolve(
1425            &pattern(1.0, 0.5, [0.0, 0.0], 1.0),
1426            &history,
1427            &Settings::default(),
1428        )
1429        .unwrap();
1430        let over = resolve(
1431            &pattern(2.0, 0.5, [0.0, 0.0], 1.0),
1432            &history,
1433            &Settings::default(),
1434        )
1435        .unwrap();
1436        let half = resolve(
1437            &pattern(0.5, 0.5, [0.0, 0.0], 1.0),
1438            &history,
1439            &Settings::default(),
1440        )
1441        .unwrap();
1442        assert_near(open.colour[4][0], 0.6875, 1e-5);
1443        assert_near(shut.colour[4][0], 0.25, 1e-5);
1444        assert_near(over.colour[4][0], 0.25, 1e-5);
1445        assert!((half.colour[4][0] - 0.25).abs() < (open.colour[4][0] - 0.25).abs());
1446        assert!((half.colour[4][0] - 0.25).abs() > 1e-4);
1447    }
1448
1449    #[test]
1450    fn id_and_normal_rejection_and_exposure() {
1451        let mut current = pattern(0.0, 0.5, [0.0, 0.0], 1.0);
1452        current.id = Some(vec![7; 9]);
1453        let mut matched = filled(0.75, 1.0, 0.5);
1454        matched.id = vec![7; 9];
1455        let kept = resolve(&current, &matched, &Settings::default()).unwrap();
1456        assert_near(kept.colour[4][0], 0.6875, 1e-5);
1457        matched.id = vec![8; 9];
1458        let rejected = resolve(&current, &matched, &Settings::default()).unwrap();
1459        assert_near(rejected.colour[4][0], 0.25, 1e-5);
1460
1461        current.id = None;
1462        current.normal = Some(vec![[0.0, 0.0, 1.0]; 9]);
1463        let mut aligned = filled(0.75, 1.0, 0.5);
1464        aligned.normal = vec![[0.0, 0.0, 1.0]; 9];
1465        let kept = resolve(&current, &aligned, &Settings::default()).unwrap();
1466        assert_near(kept.colour[4][0], 0.6875, 1e-5);
1467        aligned.normal = vec![[0.0, 1.0, 0.0]; 9];
1468        let turned = resolve(&current, &aligned, &Settings::default()).unwrap();
1469        assert_near(turned.colour[4][0], 0.25, 1e-5);
1470        aligned.normal = vec![[0.0; 3]; 9];
1471        let missing = resolve(&current, &aligned, &Settings::default()).unwrap();
1472        assert_near(missing.colour[4][0], 0.6875, 1e-5);
1473
1474        let history = filled(0.75, 1.0, 0.5);
1475        let rescaled = resolve(
1476            &pattern(0.0, 0.5, [0.0, 0.0], 4.0),
1477            &history,
1478            &Settings::default(),
1479        )
1480        .unwrap();
1481        assert_near(rescaled.colour[4][0], 0.6875, 1e-5);
1482        assert_near(rescaled.history.rgba[4][3], 4.0, 1e-6);
1483        assert_near(rescaled.history.exposure, 4.0, 1e-6);
1484        let mut kept_hdr = history.clone();
1485        kept_hdr.exposure = 2.0;
1486        let still = resolve(
1487            &pattern(0.0, 0.5, [0.0, 0.0], 4.0),
1488            &kept_hdr,
1489            &Settings::default(),
1490        )
1491        .unwrap();
1492        assert_near(still.colour[4][0], 0.6875, 1e-5);
1493    }
1494
1495    fn wide_pattern(motion: [f32; 2]) -> Inputs {
1496        let mut colour = vec![[0.5; 3]; 12];
1497        let mut depth = vec![0.5; 12];
1498        let mut reactive = vec![0.0; 12];
1499        for y in 0..3 {
1500            for x in 0..3 {
1501                let value = match (x, y) {
1502                    (1, 1) => 0.25,
1503                    (1, _) | (_, 1) => 0.0,
1504                    _ => 1.0,
1505                };
1506                colour[y * 4 + x] = [value, value, value];
1507                depth[y * 4 + x] = 0.5;
1508                reactive[y * 4 + x] = 0.0;
1509            }
1510        }
1511        Inputs {
1512            width: 4,
1513            height: 3,
1514            colour,
1515            depth,
1516            motion: (0..12)
1517                .map(|index| if index == 5 { motion } else { [0.0, 0.0] })
1518                .collect(),
1519            reactive,
1520            id: None,
1521            normal: None,
1522            exposure: 1.0,
1523        }
1524    }
1525
1526    #[test]
1527    fn motion_reprojects_or_rejects() {
1528        let mut history = History {
1529            width: 4,
1530            height: 3,
1531            rgba: vec![[0.5, 0.5, 0.5, 1.0]; 12],
1532            depth: vec![0.5; 12],
1533            id: vec![0; 12],
1534            normal: vec![[0.0; 3]; 12],
1535            exposure: 1.0,
1536            valid: true,
1537        };
1538        history.rgba[5] = [0.5, 0.5, 0.5, 1.0];
1539        history.rgba[6] = [0.75, 0.75, 0.75, 1.0];
1540        let stayed = resolve(&wide_pattern([0.0, 0.0]), &history, &Settings::default()).unwrap();
1541        let shifted = resolve(&wide_pattern([0.25, 0.0]), &history, &Settings::default()).unwrap();
1542        let between = resolve(&wide_pattern([0.125, 0.0]), &history, &Settings::default()).unwrap();
1543        let left = resolve(&wide_pattern([2.0, 0.0]), &history, &Settings::default()).unwrap();
1544        let border = resolve(&wide_pattern([0.625, 0.0]), &history, &Settings::default()).unwrap();
1545        assert_near(stayed.colour[5][0], 0.46875, 1e-5);
1546        assert_near(shifted.colour[5][0], 0.6875, 1e-5);
1547        assert_near(between.colour[5][0], 0.578125, 1e-5);
1548        assert_near(left.colour[5][0], 0.25, 1e-5);
1549        assert_near(border.colour[5][0], 0.25, 1e-5);
1550    }
1551
1552    #[test]
1553    fn bad_inputs_are_rejected() {
1554        let mut inputs = pattern(0.0, 0.5, [0.0, 0.0], 1.0);
1555        inputs.colour.pop();
1556        assert_eq!(
1557            resolve(&inputs, &History::empty(), &Settings::default()).unwrap_err(),
1558            Error::Length
1559        );
1560        inputs = pattern(0.0, 0.5, [0.0, 0.0], 0.0);
1561        assert_eq!(
1562            resolve(&inputs, &History::empty(), &Settings::default()).unwrap_err(),
1563            Error::Exposure
1564        );
1565        let settings = Settings {
1566            history_weight: f32::NAN,
1567            ..Settings::default()
1568        };
1569        assert_eq!(
1570            resolve(
1571                &pattern(0.0, 0.5, [0.0, 0.0], 1.0),
1572                &History::empty(),
1573                &settings
1574            )
1575            .unwrap_err(),
1576            Error::Settings
1577        );
1578        assert!(RESOLVE_WGSL.contains("fn resolve"));
1579        assert!(RESOLVE_WGSL.contains("rgb_to_ycocg"));
1580    }
1581
1582    fn upload(queue: &wgpu::Queue, texture: &wgpu::Texture, width: u32, height: u32, bytes: &[u8]) {
1583        let row = bytes.len() as u32 / height;
1584        queue.write_texture(
1585            texture.as_image_copy(),
1586            bytes,
1587            wgpu::TexelCopyBufferLayout {
1588                offset: 0,
1589                bytes_per_row: Some(row),
1590                rows_per_image: Some(height),
1591            },
1592            wgpu::Extent3d {
1593                width,
1594                height,
1595                depth_or_array_layers: 1,
1596            },
1597        );
1598    }
1599
1600    fn f32_bytes(values: &[f32]) -> Vec<u8> {
1601        values
1602            .iter()
1603            .flat_map(|value| value.to_le_bytes())
1604            .collect()
1605    }
1606
1607    fn f16_bytes(values: &[f32]) -> Vec<u8> {
1608        values
1609            .iter()
1610            .flat_map(|value| half::f16::from_f32(*value).to_le_bytes())
1611            .collect()
1612    }
1613
1614    struct GpuFrame {
1615        colour: wgpu::Texture,
1616        colour_view: wgpu::TextureView,
1617        depth: wgpu::Texture,
1618        depth_view: wgpu::TextureView,
1619        motion: wgpu::Texture,
1620        motion_view: wgpu::TextureView,
1621        reactive: wgpu::Texture,
1622        reactive_view: wgpu::TextureView,
1623        id: wgpu::Texture,
1624        id_view: wgpu::TextureView,
1625        normal: wgpu::Texture,
1626        normal_view: wgpu::TextureView,
1627    }
1628
1629    impl GpuFrame {
1630        fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
1631            let sampled = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
1632            let (colour, colour_view) =
1633                texture(device, "taa colour", width, height, COLOUR_FORMAT, sampled);
1634            let (depth, depth_view) =
1635                texture(device, "taa depth", width, height, DEPTH_FORMAT, sampled);
1636            let (motion, motion_view) =
1637                texture(device, "taa motion", width, height, MOTION_FORMAT, sampled);
1638            let (reactive, reactive_view) = texture(
1639                device,
1640                "taa reactive",
1641                width,
1642                height,
1643                REACTIVE_FORMAT,
1644                sampled,
1645            );
1646            let (id, id_view) = texture(device, "taa id", width, height, ID_FORMAT, sampled);
1647            let (normal, normal_view) =
1648                texture(device, "taa normal", width, height, NORMAL_FORMAT, sampled);
1649            Self {
1650                colour,
1651                colour_view,
1652                depth,
1653                depth_view,
1654                motion,
1655                motion_view,
1656                reactive,
1657                reactive_view,
1658                id,
1659                id_view,
1660                normal,
1661                normal_view,
1662            }
1663        }
1664
1665        fn upload(&self, queue: &wgpu::Queue, inputs: &Inputs) {
1666            let width = inputs.width;
1667            let height = inputs.height;
1668            let mut colour = Vec::new();
1669            for pixel in &inputs.colour {
1670                colour.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 0.0]);
1671            }
1672            upload(queue, &self.colour, width, height, &f16_bytes(&colour));
1673            upload(queue, &self.depth, width, height, &f32_bytes(&inputs.depth));
1674            let mut motion = Vec::new();
1675            for pixel in &inputs.motion {
1676                motion.extend_from_slice(pixel);
1677            }
1678            upload(queue, &self.motion, width, height, &f16_bytes(&motion));
1679            upload(
1680                queue,
1681                &self.reactive,
1682                width,
1683                height,
1684                &inputs
1685                    .reactive
1686                    .iter()
1687                    .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
1688                    .collect::<Vec<_>>(),
1689            );
1690            let ids = inputs
1691                .id
1692                .clone()
1693                .unwrap_or_else(|| vec![0; inputs.colour.len()]);
1694            let id_bytes: Vec<u8> = ids.iter().flat_map(|id| id.to_le_bytes()).collect();
1695            upload(queue, &self.id, width, height, &id_bytes);
1696            let mut normal = Vec::new();
1697            let normals = inputs
1698                .normal
1699                .clone()
1700                .unwrap_or_else(|| vec![[0.0; 3]; inputs.colour.len()]);
1701            for pixel in normals {
1702                normal.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 0.0]);
1703            }
1704            let normal_bytes: Vec<u8> = normal
1705                .iter()
1706                .map(|value| (value.clamp(-1.0, 1.0) * 127.0).round() as i8 as u8)
1707                .collect();
1708            upload(queue, &self.normal, width, height, &normal_bytes);
1709        }
1710    }
1711
1712    fn gpu_resolve(
1713        pass: &mut Pass,
1714        gpu: &pfx_gpu::Gpu,
1715        frame: &GpuFrame,
1716        inputs: &Inputs,
1717    ) -> Vec<[f32; 4]> {
1718        frame.upload(&gpu.queue, inputs);
1719        let mut encoder = gpu.device.create_command_encoder(&Default::default());
1720        pass.resolve(
1721            &gpu.device,
1722            &gpu.queue,
1723            &mut encoder,
1724            &GpuInputs {
1725                colour: &frame.colour_view,
1726                depth: &frame.depth_view,
1727                motion: &frame.motion_view,
1728                reactive: &frame.reactive_view,
1729                id: inputs.id.as_ref().map(|_| &frame.id_view),
1730                normal: inputs.normal.as_ref().map(|_| &frame.normal_view),
1731                exposure: inputs.exposure,
1732            },
1733            &Settings::default(),
1734        )
1735        .unwrap();
1736        gpu.queue.submit(Some(encoder.finish()));
1737        pass.read_colour(&gpu.device, &gpu.queue).unwrap()
1738    }
1739
1740    fn sized_frame(step: u32, width: u32, height: u32) -> Inputs {
1741        let n = (width * height) as usize;
1742        Inputs {
1743            width,
1744            height,
1745            colour: (0..n)
1746                .map(|i| {
1747                    let x = (i as u32 % width) as f32 / width as f32;
1748                    let y = (i as u32 / width) as f32 / height as f32;
1749                    let wave = ((x * 7.0 + y * 3.0 + step as f32 * 0.7).sin() + 1.0) * 0.5;
1750                    [wave, x, y]
1751                })
1752                .collect(),
1753            depth: (0..n).map(|i| 0.4 + 0.01 * (i % 3) as f32).collect(),
1754            motion: vec![[0.03, -0.02]; n],
1755            reactive: vec![0.0; n],
1756            id: Some(vec![2; n]),
1757            normal: None,
1758            exposure: 1.0,
1759        }
1760    }
1761
1762    #[test]
1763    #[ignore = "needs a GPU; run under pgpu"]
1764    fn gpu_resamples_history_across_viewport_changes() {
1765        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1766        let mut pass = Pass::new(&gpu.device);
1767        pass.resize(&gpu.device, 8, 4);
1768        let frame = GpuFrame::new(&gpu.device, 8, 4);
1769        let mut history = History::empty();
1770        let sizes = [(8, 4), (8, 4), (5, 3), (7, 4), (8, 4), (3, 2), (6, 4)];
1771        for (step, (width, height)) in sizes.into_iter().enumerate() {
1772            pass.set_viewport(width, height).unwrap();
1773            let inputs = sized_frame(step as u32, width, height);
1774            let cpu = resolve(&inputs, &history, &Settings::default()).unwrap();
1775            let got = gpu_resolve(&mut pass, &gpu, &frame, &inputs);
1776            assert_eq!(got.len(), cpu.colour.len());
1777            for (index, pixel) in cpu.colour.iter().enumerate() {
1778                for channel in 0..3 {
1779                    assert_near(got[index][channel], pixel[channel], 1.5e-3);
1780                }
1781            }
1782            history = cpu.history;
1783        }
1784        assert!(pass.history_valid());
1785        assert_eq!(pass.restarts(), 0);
1786        assert!(pass.set_viewport(9, 4).is_err());
1787        pass.invalidate();
1788        assert_eq!(pass.restarts(), 1);
1789    }
1790
1791    #[test]
1792    #[ignore = "needs a GPU; run under pgpu"]
1793    fn gpu_resolves_a_sequence() {
1794        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1795        let mut pass = Pass::new(&gpu.device);
1796        pass.resize(&gpu.device, 8, 4);
1797        let frame = GpuFrame::new(&gpu.device, 8, 4);
1798        let mut history = History::empty();
1799        for step in 0..8 {
1800            let inputs = edge_frame(step);
1801            let cpu = resolve(&inputs, &history, &Settings::default()).unwrap();
1802            let got = gpu_resolve(&mut pass, &gpu, &frame, &inputs);
1803            for (index, pixel) in cpu.colour.iter().enumerate() {
1804                for channel in 0..3 {
1805                    assert_near(got[index][channel], pixel[channel], 1.5e-3);
1806                }
1807                assert_near(got[index][3], inputs.exposure, 1.0e-5);
1808            }
1809            history = cpu.history;
1810        }
1811        assert!(pass.history_valid());
1812        pass.resize(&gpu.device, 8, 4);
1813        assert!(pass.history_valid());
1814        pass.resize(&gpu.device, 5, 4);
1815        assert!(!pass.history_valid());
1816        let small = GpuFrame::new(&gpu.device, 5, 4);
1817        let mut inputs = pattern(1.0, 0.8, [2.0, 0.0], 4.0);
1818        inputs.width = 5;
1819        inputs.height = 4;
1820        inputs.colour = vec![[0.25; 3]; 20];
1821        inputs.depth = vec![0.8; 20];
1822        inputs.motion = vec![[0.0; 2]; 20];
1823        inputs.reactive = vec![1.0; 20];
1824        inputs.reactive[0] = 0.0;
1825        inputs.id = Some(vec![3; 20]);
1826        inputs.normal = None;
1827        let cpu = resolve(&inputs, &History::empty(), &Settings::default()).unwrap();
1828        let got = gpu_resolve(&mut pass, &gpu, &small, &inputs);
1829        for (index, pixel) in cpu.colour.iter().enumerate() {
1830            for channel in 0..3 {
1831                assert_near(got[index][channel], pixel[channel], 1.5e-3);
1832            }
1833            assert_near(got[index][3], 4.0, 1.0e-5);
1834        }
1835        let mut second = pattern(0.0, 0.5, [0.125, 0.0], 2.0);
1836        second.normal = Some(vec![[0.0, 0.0, 1.0]; 9]);
1837        let mut pass = Pass::new(&gpu.device);
1838        pass.resize(&gpu.device, 3, 3);
1839        let frame = GpuFrame::new(&gpu.device, 3, 3);
1840        let mut history = History::empty();
1841        let mut first = pattern(0.0, 0.5, [0.0, 0.0], 1.0);
1842        first.normal = Some(vec![[0.0, 0.0, 1.0]; 9]);
1843        for inputs in [first, second] {
1844            let cpu = resolve(&inputs, &history, &Settings::default()).unwrap();
1845            let got = gpu_resolve(&mut pass, &gpu, &frame, &inputs);
1846            for (index, pixel) in cpu.colour.iter().enumerate() {
1847                for channel in 0..3 {
1848                    assert_near(got[index][channel], pixel[channel], 1.5e-3);
1849                }
1850            }
1851            history = cpu.history;
1852        }
1853    }
1854
1855    #[test]
1856    #[ignore = "needs a GPU; run under pgpu"]
1857    fn gpu_resolve_4k_timing() {
1858        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1859        assert!(
1860            gpu.device
1861                .features()
1862                .contains(wgpu::Features::TIMESTAMP_QUERY)
1863        );
1864        assert!(
1865            gpu.device
1866                .features()
1867                .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS)
1868        );
1869        let mut pass = Pass::new(&gpu.device);
1870        pass.resize(&gpu.device, 3840, 2160);
1871        let frame = GpuFrame::new(&gpu.device, 3840, 2160);
1872        let mut colour = Vec::with_capacity(3840 * 2160);
1873        let mut depth = Vec::with_capacity(3840 * 2160);
1874        let mut motion = Vec::with_capacity(3840 * 2160);
1875        let mut reactive = Vec::with_capacity(3840 * 2160);
1876        let mut id = Vec::with_capacity(3840 * 2160);
1877        let mut normal = Vec::with_capacity(3840 * 2160);
1878        for y in 0..2160 {
1879            for x in 0..3840 {
1880                colour.push([
1881                    x as f32 / 3840.0,
1882                    y as f32 / 2160.0,
1883                    ((x ^ y) & 31) as f32 / 16.0,
1884                ]);
1885                depth.push(0.1 + (x % 127) as f32 / 128.0);
1886                motion.push([1.0 / 3840.0, (y % 3) as f32 / 2160.0]);
1887                reactive.push((x % 17) as f32 / 17.0);
1888                id.push(x / 32);
1889                normal.push([0.0, (y % 5) as f32 / 5.0, 1.0]);
1890            }
1891        }
1892        frame.upload(
1893            &gpu.queue,
1894            &Inputs {
1895                width: 3840,
1896                height: 2160,
1897                colour,
1898                depth,
1899                motion,
1900                reactive,
1901                id: Some(id),
1902                normal: Some(normal),
1903                exposure: 1.0,
1904            },
1905        );
1906        let inputs = GpuInputs {
1907            colour: &frame.colour_view,
1908            depth: &frame.depth_view,
1909            motion: &frame.motion_view,
1910            reactive: &frame.reactive_view,
1911            id: Some(&frame.id_view),
1912            normal: None,
1913            exposure: 1.0,
1914        };
1915        let mut encoder = gpu.device.create_command_encoder(&Default::default());
1916        pass.resolve(
1917            &gpu.device,
1918            &gpu.queue,
1919            &mut encoder,
1920            &inputs,
1921            &Settings::default(),
1922        )
1923        .unwrap();
1924        gpu.queue.submit(Some(encoder.finish()));
1925        gpu.device
1926            .poll(wgpu::PollType::wait_indefinitely())
1927            .unwrap();
1928        let queries = gpu.device.create_query_set(&wgpu::QuerySetDescriptor {
1929            label: Some("taa 4k timing"),
1930            ty: wgpu::QueryType::Timestamp,
1931            count: 2,
1932        });
1933        let resolved = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1934            label: Some("taa 4k timing resolved"),
1935            size: 16,
1936            usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
1937            mapped_at_creation: false,
1938        });
1939        let read = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1940            label: Some("taa 4k timing read"),
1941            size: 16,
1942            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1943            mapped_at_creation: false,
1944        });
1945        let mut timings = Vec::new();
1946        let mut turns = pfx_gpu::pace::Turns::default();
1947        for _ in 0..12 {
1948            let mut encoder = gpu.device.create_command_encoder(&Default::default());
1949            encoder.write_timestamp(&queries, 0);
1950            pass.resolve(
1951                &gpu.device,
1952                &gpu.queue,
1953                &mut encoder,
1954                &inputs,
1955                &Settings::default(),
1956            )
1957            .unwrap();
1958            encoder.write_timestamp(&queries, 1);
1959            encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
1960            encoder.copy_buffer_to_buffer(&resolved, 0, &read, 0, 16);
1961            gpu.queue.submit(Some(encoder.finish()));
1962            let (send, receive) = std::sync::mpsc::channel();
1963            let slice = read.slice(..);
1964            slice.map_async(wgpu::MapMode::Read, move |result| {
1965                send.send(result).unwrap();
1966            });
1967            gpu.device
1968                .poll(wgpu::PollType::wait_indefinitely())
1969                .unwrap();
1970            receive.recv().unwrap().unwrap();
1971            let data = slice.get_mapped_range().expect("a mapped range");
1972            let start = u64::from_le_bytes(data[0..8].try_into().unwrap());
1973            let end = u64::from_le_bytes(data[8..16].try_into().unwrap());
1974            timings.push((end - start) as f64 * f64::from(gpu.queue.get_timestamp_period()) / 1e6);
1975            turns.add(timings[timings.len() - 1]);
1976            drop(data);
1977            read.unmap();
1978        }
1979        timings.sort_by(f64::total_cmp);
1980        println!("4K TAA GPU ms: {timings:?}");
1981    }
1982}