diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs index e14429b03..eb152667c 100644 --- a/crates/horizon-client/src/gfx.rs +++ b/crates/horizon-client/src/gfx.rs @@ -569,6 +569,11 @@ pub struct Renderer { shadow_terrain: wgpu::RenderPipeline, shadow_entity: wgpu::RenderPipeline, hd_shadow_visible: Vec, + /// The far cascade as last drawn (matrix, camera, light) and the frames since. + hd_far: Option<(Mat4, glam::DVec3, Vec3, u32)>, + /// HD's TAA: frames jittered so far, and the last one's unjittered view-projection and camera. + taa_frame: u32, + taa_prev: Option<(Mat4, glam::DVec3)>, hd: Option, mod_params: (Vec, f32), /// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`). @@ -2815,6 +2820,9 @@ impl Renderer { shadow_terrain, shadow_entity, hd_shadow_visible: Vec::new(), + hd_far: None, + taa_frame: 0, + taa_prev: None, hd: None, mod_params: (Vec::new(), 0.0), pass_timing: pack_timing::PassTiming::logging(), @@ -3954,7 +3962,29 @@ impl Renderer { frame.block_flicker, frame.boss_darken, ); - let view_proj = self.view_projection(frame); + let hd_wanted = !frame.title + && frame.hd.is_some() + && !self.pack_post.as_ref().is_some_and(|p| p.1.is_some()) + && !self.native.as_ref().is_some_and(|n| n.post.is_some()); + // AirDef fork: HD's temporal anti-aliasing shifts the whole projection by a sub-pixel + // offset each frame; `unjittered` is what the next frame reprojects with. + let unjittered = self.view_projection(frame); + let taa = hd_wanted && hd::taa_enabled(); + let view_proj = if taa { + let size = self + .world_post + .size() + .unwrap_or([self.config.width, self.config.height]); + let j = hd::jitter(self.taa_frame, size); + self.taa_frame = self.taa_frame.wrapping_add(1); + Mat4::from_translation(Vec3::new(j[0], j[1], 0.0)) * unjittered + } else { + unjittered + }; + let taa_prev = std::mem::replace( + &mut self.taa_prev, + taa.then_some((unjittered, frame.camera)), + ); if frame.title { self.panorama.prepare( &self.queue, @@ -4158,13 +4188,12 @@ impl Renderer { } self.drawn_sections = visible.len() + visible_lods.len(); // AirDef fork: the HD sun shadows draw every section in the farther cascade's box. - let hd_wanted = !frame.title - && frame.hd.is_some() - && !self.pack_post.as_ref().is_some_and(|p| p.1.is_some()) - && !self.native.as_ref().is_some_and(|n| n.post.is_some()); - let hd_scene = hd::HdScene { + let mut hd_scene = hd::HdScene { + cascades: [Mat4::IDENTITY; 2], view_proj, - camera, + taa: taa && taa_prev.is_some(), + prev_view_proj: taa_prev.map_or(unjittered, |p| p.0), + camera_delta: taa_prev.map_or(Vec3::ZERO, |p| (frame.camera - p.1).as_vec3()), // The world's sun: vanilla turns the sky -90° about Y, so it crosses from east to west. sun: Vec3::new(-sun_angle.sin(), sun_angle.cos(), 0.0), dark: 1.0 - frame.sky.sky_light_factor, @@ -4174,10 +4203,34 @@ impl Renderer { sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater, }; let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene); - let hd_cascades = hd::cascades(camera, shadow_dir); + let mut hd_cascades = hd::cascades(camera, shadow_dir); + // The far cascade is redrawn every second frame (it is snapped to texels and the sun is + // slow); between, the lighting pass gets the matrix its map was drawn with. + let hd_far_fresh = if hd_wanted && shadow_strength > 0.0 { + let fresh = match self.hd_far { + Some((_, at, dir, age)) if hd_shadow_fast() => { + age >= 1 + || (camera - at).length_squared() > 48.0 * 48.0 + || dir.dot(shadow_dir) < 0.99995 + || self.hd.is_none() + } + _ => true, + }; + if fresh { + self.hd_far = Some((hd_cascades[1], camera, shadow_dir, 0)); + } else if let Some((held, _, _, age)) = &mut self.hd_far { + hd_cascades[1] = *held; + *age += 1; + } + fresh + } else { + self.hd_far = None; + true + }; + hd_scene.cascades = hd_cascades; let mut hd_shadow_visible = std::mem::take(&mut self.hd_shadow_visible); hd_shadow_visible.clear(); - if hd_wanted && shadow_strength > 0.0 { + if hd_wanted && shadow_strength > 0.0 && hd_far_fresh { let radius = ((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[1] / 16.0) as i32 + 2); let light = Frustum { @@ -4191,6 +4244,11 @@ impl Renderer { |_, _| false, &mut hd_shadow_visible, ); + if hd_shadow_fast() { + drop_buried(&mut hd_shadow_visible, |slot| { + self.sections.get(slot).solid > 0 + }); + } } // Mods' cameras: their sections round them, all of them (no occlusion), after the shadow's. let globals_size = std::mem::size_of::() as u64; @@ -5022,7 +5080,12 @@ impl Renderer { } self.queue .write_buffer(&hd.main_globals, 0, bytemuck::bytes_of(&globals)); + // Facing the light only: the nearest surface to it is always one that does. + let toward = (hd_shadow_fast() && self.sides_on).then(|| shadow_dir * 1.0e5); for (c, cascade) in hd_cascades.iter().enumerate() { + if c == 1 && !hd_far_fresh { + continue; + } let light = Globals { view_proj: cascade.to_cols_array_2d(), ..globals @@ -5059,7 +5122,13 @@ impl Renderer { page = Some(s.page); } let instance = hd_shadow_first + k as u32; - pass.draw_indexed(0..s.solid * 6, s.start as i32, instance..instance + 1); + facing_runs(&s.sides, toward, v.origin, |first, quads| { + pass.draw_indexed( + 0..quads * 6, + s.start as i32 + first as i32 * 4, + instance..instance + 1, + ); + }); } if !self.shadow_casters.is_empty() { pass.set_pipeline(&self.shadow_entity); @@ -5652,9 +5721,13 @@ impl Renderer { self.mod_params.1, ); post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx); - hd.finish(&self.device, &mut encoder, hd::HdSource::Fx, &view_target); + let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth); + hd.finish(&self.device, &mut encoder, source, &view_target); + } + _ => { + let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Lit, depth); + hd.finish(&self.device, &mut encoder, source, &view_target); } - _ => hd.finish(&self.device, &mut encoder, hd::HdSource::Lit, &view_target), } } else if let Some((color, depth)) = &scaled && mod_post @@ -6055,6 +6128,35 @@ fn facing_runs(sides: &Sides, eye: Option, origin: [f32; 3], mut draw: imp None => {} } } +/// `HORIZON_HD_SHADOW_FAST=0` draws the HD shadow cascades the slow way (every section, both +/// faces, the far one every frame), for comparison. +fn hd_shadow_fast() -> bool { + static FAST: std::sync::OnceLock = std::sync::OnceLock::new(); + *FAST.get_or_init(|| !std::env::var("HORIZON_HD_SHADOW_FAST").is_ok_and(|v| v == "0")) +} +/// Drops the sections that sit well under the ground of their own and all eight neighbouring +/// columns: shut in by rock, what they hold neither lights nor shades anything the sun reaches. +fn drop_buried(list: &mut Vec, has_solid: impl Fn(u32) -> bool) { + let mut top: std::collections::HashMap<(i32, i32), i32> = std::collections::HashMap::new(); + for v in list.iter() { + if has_solid(v.slot) { + let e = top.entry((v.key.0, v.key.2)).or_insert(i32::MIN); + *e = (*e).max(v.key.1); + } + } + list.retain(|v| { + let mut lowest = i32::MAX; + for dx in -1..=1 { + for dz in -1..=1 { + match top.get(&(v.key.0 + dx, v.key.2 + dz)) { + Some(&t) => lowest = lowest.min(t), + None => return true, + } + } + } + v.key.1 > lowest - 3 + }); +} fn frustum(m: Mat4) -> [Vec4; 6] { let r = |i| m.row(i); let planes = [ diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs index d3944e97b..16610cb02 100644 --- a/crates/horizon-client/src/gfx/hd.rs +++ b/crates/horizon-client/src/gfx/hd.rs @@ -85,14 +85,26 @@ struct HdFrame { ambient: [f32; 4], /// x: shadows on; y, z: the cascades' half sizes; w: the light tiles per row. shadow: [f32; 4], + /// The previous frame's camera-relative view-projection without jitter (for TAA). + prev_view_proj: [[f32; 4]; 4], + /// xyz: how far the camera moved since that frame; w: 1 when the history is usable. + taa: [f32; 4], + /// x: the history's weight; y: the sharpening (0 without TAA). + taa_params: [f32; 4], } /// What the world around looks like this frame (from the renderer's globals). #[derive(Clone, Copy, Debug)] pub struct HdScene { + /// The shadow cascades' matrices (`cascades`; the far one may be a frame or two old). + pub cascades: [Mat4; 2], + /// The (jittered) projection the world was drawn with. pub view_proj: Mat4, - /// The camera in the world, for steady shadows. - pub camera: glam::DVec3, + /// Last frame's view-projection without jitter, and the camera's move since (TAA). + pub prev_view_proj: Mat4, + pub camera_delta: glam::Vec3, + /// This frame is anti-aliased over time (the projection is jittered). + pub taa: bool, /// Towards the sun, and the sky's darkening (0 day, 1 night). pub sun: glam::Vec3, pub dark: f32, @@ -119,6 +131,9 @@ struct HdFrame { fog: vec4, ambient: vec4, shadow: vec4, + prev_view_proj: mat4x4, + taa: vec4, + taa_params: vec4, } struct FullOut { @builtin(position) position: vec4, @@ -507,7 +522,21 @@ const TONEMAP: &str = r#" @fragment fn fs_tonemap(in: FullOut) -> @location(0) vec4 { - var c = textureSampleLevel(hdr, linear, in.uv, 0.0).rgb * frame.params.y; + var c = textureSampleLevel(hdr, linear, in.uv, 0.0).rgb; + // A slight sharpen after TAA (its history resampling softens): the centre against its four + // neighbours, kept within their range so it can't ring. + let k = frame.taa_params.y; + if k > 0.0 { + let t = 1.0 / vec2(textureDimensions(hdr)); + let n0 = textureSampleLevel(hdr, linear, in.uv + vec2(t.x, 0.0), 0.0).rgb; + let n1 = textureSampleLevel(hdr, linear, in.uv - vec2(t.x, 0.0), 0.0).rgb; + let n2 = textureSampleLevel(hdr, linear, in.uv + vec2(0.0, t.y), 0.0).rgb; + let n3 = textureSampleLevel(hdr, linear, in.uv - vec2(0.0, t.y), 0.0).rgb; + let lo = min(min(n0, n1), min(min(n2, n3), c)); + let hi = max(max(n0, n1), max(max(n2, n3), c)); + c = clamp(c * (1.0 + 4.0 * k) - (n0 + n1 + n2 + n3) * k, lo, hi); + } + c = c * frame.params.y; c += textureSampleLevel(bloom, linear, in.uv, 0.0).rgb * frame.params.z; // Up to 0.8 as it is (vanilla's look); above it, rolled off towards white by the // brightest channel, so hues hold instead of clipping. @@ -528,6 +557,130 @@ fn fs_tonemap(in: FullOut) -> @location(0) vec4 { } "#; +const TAA: &str = r#" +struct TaaOut { + @location(0) color: vec4, + @location(1) history: vec4, +} +@group(0) @binding(0) var frame: HdFrame; +@group(0) @binding(1) var source: texture_2d; +@group(0) @binding(2) var depth_tex: texture_depth_2d; +@group(0) @binding(3) var history: texture_2d; +@group(0) @binding(4) var linear: sampler; + +fn to_ycocg(c: vec3) -> vec3 { + return vec3(c.r * 0.25 + c.g * 0.5 + c.b * 0.25, c.r * 0.5 - c.b * 0.5, c.g * 0.5 - c.r * 0.25 - c.b * 0.25); +} +fn from_ycocg(c: vec3) -> vec3 { + let t = c.x - c.z; + return vec3(t + c.y, c.x + c.z, t - c.y); +} +// Compress the range so a very bright pixel does not dominate the blend or the clamp. +fn squash(c: vec3) -> vec3 { + return c / (1.0 + max(c.r, max(c.g, c.b))); +} +fn unsquash(c: vec3) -> vec3 { + return c / max(1.0 - max(c.r, max(c.g, c.b)), 0.001); +} + +// The history at a uv, filtered with a Catmull-Rom spline (five bilinear taps) so that +// resampling it every frame doesn't blur. +fn history_color(uv: vec2) -> vec3 { + let size = vec2(textureDimensions(history)); + let pos = uv * size; + let c1 = floor(pos - 0.5) + 0.5; + let f = pos - c1; + let w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); + let w1 = 1.0 + f * f * (-2.5 + 1.5 * f); + let w2 = f * (0.5 + f * (2.0 - 1.5 * f)); + let w3 = f * f * (-0.5 + 0.5 * f); + let w12 = w1 + w2; + let t0 = (c1 - 1.0) / size; + let t3 = (c1 + 2.0) / size; + let t12 = (c1 + w2 / w12) / size; + var sum = textureSampleLevel(history, linear, vec2(t12.x, t0.y), 0.0).rgb * (w12.x * w0.y); + var total = w12.x * w0.y; + sum += textureSampleLevel(history, linear, vec2(t0.x, t12.y), 0.0).rgb * (w0.x * w12.y); + total += w0.x * w12.y; + sum += textureSampleLevel(history, linear, t12, 0.0).rgb * (w12.x * w12.y); + total += w12.x * w12.y; + sum += textureSampleLevel(history, linear, vec2(t3.x, t12.y), 0.0).rgb * (w3.x * w12.y); + total += w3.x * w12.y; + sum += textureSampleLevel(history, linear, vec2(t12.x, t3.y), 0.0).rgb * (w12.x * w3.y); + total += w12.x * w3.y; + return max(sum / total, vec3(0.0)); +} + +// Pulls `p` towards the box's centre until it is inside. +fn clip_to_box(lo: vec3, hi: vec3, p: vec3) -> vec3 { + let centre = (lo + hi) * 0.5; + let ext = max((hi - lo) * 0.5, vec3(0.00001)); + let off = (p - centre) / ext; + let m = max(abs(off.x), max(abs(off.y), abs(off.z))); + return select(p, centre + (p - centre) / m, m > 1.0); +} + +// Temporal anti-aliasing: the history, reprojected by depth and the camera's move, clamped to +// the colours round this pixel (so what moved or was uncovered can't ghost), blended in. +@fragment +fn fs_taa(in: FullOut) -> TaaOut { + let size = vec2(textureDimensions(source)); + let px = vec2(in.position.xy); + let d = textureLoad(depth_tex, px, 0); + var sum = vec3(0.0); + var sum2 = vec3(0.0); + var lo = vec3(1.0e9); + var hi = vec3(-1.0e9); + var centre = vec3(0.0); + for (var y = -1; y <= 1; y++) { + for (var x = -1; x <= 1; x++) { + let q = clamp(px + vec2(x, y), vec2(0), size - 1); + let s = to_ycocg(squash(textureLoad(source, q, 0).rgb)); + sum += s; + sum2 += s * s; + lo = min(lo, s); + hi = max(hi, s); + if x == 0 && y == 0 { centre = s; } + } + } + let sky = d <= 0.0; + let ndc = vec4(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(d, 0.000001), 1.0); + let h = frame.inverse_view_proj * ndc; + let p = h.xyz / h.w; + let dist = select(length(p), 0.0, sky); + var resolved = centre; + if frame.taa.w > 0.5 { + let prev = frame.prev_view_proj * vec4(select(p + frame.taa.xyz, p, sky), 1.0); + if prev.w > 0.0 { + let pn = prev.xy / prev.w; + let uv = vec2(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5); + if all(uv > vec2(0.0)) && all(uv < vec2(1.0)) { + let hp = clamp(vec2(uv * vec2(size)), vec2(0), size - 1); + let seen = textureLoad(history, hp, 0).a; + let expected = select(length(p + frame.taa.xyz), 0.0, sky); + // The same surface as last frame (not uncovered), by its distance. + if abs(seen - expected) <= expected * 0.03 + 0.25 { + let mean = sum / 9.0; + let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3(0.0))); + let box_lo = min(max(mean - sigma * 1.25, lo), centre); + let box_hi = max(min(mean + sigma * 1.25, hi), centre); + let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv)))); + // Fast motion trusts the history less. + let moved = length((uv - in.uv) * vec2(size)); + let keep = mix(frame.taa_params.x, frame.taa_params.x * 0.75, clamp(moved / 4.0, 0.0, 1.0)); + resolved = mix(centre, past, keep); + } + } + } + } + let color = unsquash(from_ycocg(resolved)); + var out: TaaOut; + out.color = vec4(color, 1.0); + out.history = vec4(color, dist); + return out; +} +"#; + struct Target { view: wgpu::TextureView, } @@ -570,6 +723,35 @@ type LightKey = ( pub enum HdSource { Lit, Fx, + /// The anti-aliased result of `taa`. + Taa, +} + +/// TAA is on unless `HORIZON_HD_TAA=0`. +pub fn taa_enabled() -> bool { + static ON: std::sync::OnceLock = std::sync::OnceLock::new(); + *ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0")) +} + +/// The Halton sequence's `index`-th number in `base`, in (0, 1). +fn halton(mut index: u32, base: u32) -> f32 { + let (mut f, mut r) = (1.0, 0.0); + while index > 0 { + f /= base as f32; + r += f * (index % base) as f32; + index /= base; + } + r +} + +/// Frame `frame`'s sub-pixel offset of the projection in clip space (a fraction of the +/// screen's size in x and y), from the Halton(2, 3) sequence over 16 frames. +pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] { + let i = frame % 16 + 1; + [ + (halton(i, 2) - 0.5) * 2.0 / size[0].max(1) as f32, + (halton(i, 3) - 0.5) * 2.0 / size[1].max(1) as f32, + ] } pub struct Hd { @@ -589,6 +771,15 @@ pub struct Hd { /// The lit world, and the post pass's output. pub lit: wgpu::TextureView, pub fx: wgpu::TextureView, + /// TAA: the resolved frame, and the two histories (one read, one written, alternating). + taa: wgpu::TextureView, + history: [wgpu::TextureView; 2], + history_ready: bool, + history_flip: usize, + taa_active: bool, + taa_layout: wgpu::BindGroupLayout, + taa_pipeline: wgpu::RenderPipeline, + taa_groups: Option<((wgpu::TextureView, wgpu::TextureView), [wgpu::BindGroup; 2])>, bloom: Vec, /// Bind groups, rebuilt only when their views change: `resize` drops all of them, the /// lighting and the source-reading ones are keyed by the views they read. @@ -902,6 +1093,44 @@ impl Hd { UNFILTERED, ]), }); + let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("HD TAA"), + entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]), + }); + let taa_module = module(device, "HD TAA", TAA); + let taa_pipeline = { + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("HD TAA"), + bind_group_layouts: &[Some(&taa_layout)], + ..Default::default() + }); + let target = Some(wgpu::ColorTargetState { + format: HDR, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + }); + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("HD TAA"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &taa_module, + entry_point: Some("vs_full"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &taa_module, + entry_point: Some("fs_taa"), + compilation_options: Default::default(), + targets: &[target.clone(), target], + }), + primitive: Default::default(), + depth_stencil: None, + multisample: Default::default(), + multiview_mask: None, + cache: None, + }) + }; let ao_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("HD ambient occlusion"), entries: &entries(&[UNIFORM, DEPTH, UNFILTERED]), @@ -1080,6 +1309,17 @@ impl Hd { .view, lit: target(device, "HD lit", [1, 1], HDR).view, fx: target(device, "HD post", [1, 1], HDR).view, + taa: target(device, "HD TAA", [1, 1], HDR).view, + history: [ + target(device, "HD TAA history", [1, 1], HDR).view, + target(device, "HD TAA history", [1, 1], HDR).view, + ], + history_ready: false, + history_flip: 0, + taa_active: false, + taa_layout, + taa_pipeline, + taa_groups: None, bloom: Vec::new(), light_group: None, prefilter_group: None, @@ -1131,6 +1371,13 @@ impl Hd { self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view; self.lit = target(device, "HD lit", size, HDR).view; self.fx = target(device, "HD post", size, HDR).view; + self.taa = target(device, "HD TAA", size, HDR).view; + self.history = [ + target(device, "HD TAA history", size, HDR).view, + target(device, "HD TAA history", size, HDR).view, + ]; + self.history_ready = false; + self.taa_groups = None; let half = [size[0].div_ceil(2), size[1].div_ceil(2)]; self.ao_raw = target(device, "HD AO", half, AO_FORMAT).view; self.ao_blurred = target(device, "HD AO blurred", half, AO_FORMAT).view; @@ -1206,9 +1453,22 @@ impl Hd { } // The sun by day, the moon (opposite) by night, faded out round the horizon. let (dir, strength, color) = light_of(scene); - let cascades = cascades(scene.camera, dir); + let cascades = scene.cascades; let night = scene.dark.clamp(0.0, 1.0); + self.taa_active = scene.taa && taa_enabled(); + // A long jump of the camera (a teleport) starts the history over. + if !self.taa_active || scene.camera_delta.length() > 64.0 { + self.history_ready = false; + } let frame = HdFrame { + prev_view_proj: scene.prev_view_proj.to_cols_array_2d(), + taa: [ + scene.camera_delta.x, + scene.camera_delta.y, + scene.camera_delta.z, + if self.history_ready { 1.0 } else { 0.0 }, + ], + taa_params: [0.9, if self.taa_active { 0.12 } else { 0.0 }, 0.0, 0.0], inverse_view_proj: scene.view_proj.inverse().to_cols_array_2d(), resolution: [ self.size[0] as f32, @@ -1489,6 +1749,7 @@ impl Hd { let source = match source { HdSource::Lit => self.lit.clone(), HdSource::Fx => self.fx.clone(), + HdSource::Taa => self.taa.clone(), }; if self .prefilter_group @@ -1572,6 +1833,70 @@ impl Hd { ); } + /// Temporal anti-aliasing of `source` (with the world's `depth`): the source it left, to hand + /// to `finish` — the same one when TAA is off this frame. + pub fn taa( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + source: HdSource, + depth: &wgpu::TextureView, + ) -> HdSource { + if !self.taa_active { + return source; + } + let input = match source { + HdSource::Lit => self.lit.clone(), + HdSource::Fx => self.fx.clone(), + HdSource::Taa => return source, + }; + let key = (input, depth.clone()); + if self.taa_groups.as_ref().is_none_or(|(k, _)| *k != key) { + let groups = [0, 1].map(|read| { + self.make_group( + device, + "HD TAA", + &self.taa_layout, + &[ + wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()), + wgpu::BindingResource::TextureView(&key.0), + wgpu::BindingResource::TextureView(depth), + wgpu::BindingResource::TextureView(&self.history[read]), + wgpu::BindingResource::Sampler(&self.sampler), + ], + ) + }); + self.taa_groups = Some((key, groups)); + } + let Some((_, groups)) = &self.taa_groups else { + return source; + }; + let (read, write) = (self.history_flip, 1 - self.history_flip); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("HD TAA"), + timestamp_writes: stamp(&mut self.timing, "hd taa"), + color_attachments: &[&self.taa, &self.history[write]].map(|view| { + Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + }) + }), + ..Default::default() + }); + pass.set_pipeline(&self.taa_pipeline); + pass.set_bind_group(0, &groups[read], &[]); + pass.draw(0..3, 0..1); + drop(pass); + self.history_flip = write; + self.history_ready = true; + HdSource::Taa + } + /// The ambient occlusion from the G-buffer's depth and normals: the half-resolution pass, then /// its blur. Run after the G-buffer pass; nothing when the strength is 0. pub fn ao(&mut self, encoder: &mut wgpu::CommandEncoder) { @@ -1698,6 +2023,7 @@ mod tests { ("light", LIGHT), ("bloom", BLOOM), ("tonemap", TONEMAP), + ("taa", TAA), ("ao", AO), ("ao blur", AO_BLUR), ] { @@ -1714,6 +2040,19 @@ mod tests { } } + #[test] + fn jitter_stays_within_a_pixel() { + let size = [1920u32, 1080]; + let mut seen = std::collections::HashSet::new(); + for f in 0..32 { + let j = jitter(f, size); + assert!(j[0].abs() <= 1.0 / size[0] as f32 + 1e-9); + assert!(j[1].abs() <= 1.0 / size[1] as f32 + 1e-9); + seen.insert((j[0].to_bits(), j[1].to_bits())); + } + assert_eq!(seen.len(), 16); + } + #[test] fn tile_lists_keep_lights_that_reach_a_tile() { let size = [320, 160];