use wgpu::TextureFormat;
pub const TAA_WGSL: &str = include_str!("taa.wgsl");
pub const TAA_PHASES: u32 = 8;
pub const TAA_FEEDBACK_CAP: f32 = 0.97;
#[must_use]
pub fn halton(index: u32, base: u32) -> f32 {
if base < 2 {
return 0.0;
}
let mut f = 1.0_f64;
let mut r = 0.0_f64;
let mut i = index;
let b = f64::from(base);
while i > 0 {
f /= b;
r += f * f64::from(i % base);
i /= base;
}
r as f32
}
#[must_use]
pub fn jitter_px(frame: u64) -> [f32; 2] {
let i = (frame % u64::from(TAA_PHASES)) as u32 + 1;
[halton(i, 2) - 0.5, halton(i, 3) - 0.5]
}
#[must_use]
pub fn history_weight(frame: u64, cap: f32) -> f32 {
if frame == 0 {
return 0.0;
}
let ramp = frame as f32 / (frame as f32 + 1.0);
ramp.min(cap)
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
struct TaaUniform {
res: [f32; 4],
params: [f32; 4],
}
pub struct TaaPass {
pipeline: wgpu::RenderPipeline,
bgl: wgpu::BindGroupLayout,
uniform: wgpu::Buffer,
sampler: wgpu::Sampler,
zero_velocity: wgpu::TextureView,
history: Option<[wgpu::Texture; 2]>,
history_views: Option<[wgpu::TextureView; 2]>,
cur: usize,
format: TextureFormat,
size: (u32, u32),
frame: u64,
feedback_cap: f32,
}
impl TaaPass {
#[must_use]
pub fn new(device: &wgpu::Device, format: TextureFormat) -> Self {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("l0_taa"),
source: wgpu::ShaderSource::Wgsl(TAA_WGSL.into()),
});
let tex = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("l0_taa_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
tex(1),
tex(2),
tex(3),
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pll = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("l0_taa_pll"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("l0_taa_resolve"),
layout: Some(&pll),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("taa_vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("taa_resolve_fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
let zero = device.create_texture(&wgpu::TextureDescriptor {
label: Some("l0_taa_zero_velocity"),
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rg16Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
Self {
pipeline,
bgl,
uniform: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_taa_uniform"),
size: std::mem::size_of::<TaaUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
sampler: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("l0_taa_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
}),
zero_velocity: zero.create_view(&Default::default()),
history: None,
history_views: None,
cur: 0,
format,
size: (0, 0),
frame: 0,
feedback_cap: TAA_FEEDBACK_CAP,
}
}
#[must_use]
pub fn with_feedback_cap(mut self, cap: f32) -> Self {
self.feedback_cap = cap.clamp(0.0, 1.0);
self
}
pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
let w = w.max(1);
let h = h.max(1);
if self.size == (w, h) && self.history.is_some() {
return;
}
let mk = |label: &str| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
})
};
let a = mk("l0_taa_history_a");
let b = mk("l0_taa_history_b");
self.history_views = Some([a.create_view(&Default::default()), b.create_view(&Default::default())]);
self.history = Some([a, b]);
self.size = (w, h);
self.reset();
}
pub fn reset(&mut self) {
self.frame = 0;
self.cur = 0;
}
#[must_use]
pub fn jitter_px(&self) -> [f32; 2] {
jitter_px(self.frame)
}
#[must_use]
pub fn frame(&self) -> u64 {
self.frame
}
#[must_use]
pub fn weight(&self) -> f32 {
history_weight(self.frame, self.feedback_cap)
}
pub fn record(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
current: &wgpu::TextureView,
velocity: Option<&wgpu::TextureView>,
) -> bool {
let Some(views) = &self.history_views else { return false };
let (w, h) = self.size;
let dst = 1 - self.cur;
queue.write_buffer(
&self.uniform,
0,
bytemuck::bytes_of(&TaaUniform {
res: [w as f32, h as f32, 1.0 / w as f32, 1.0 / h as f32],
params: {
let j = self.jitter_px();
[self.weight(), j[0], j[1], 0.0]
},
}),
);
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("l0_taa_bind"),
layout: &self.bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: self.uniform.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(current) },
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&views[self.cur]),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(velocity.unwrap_or(&self.zero_velocity)),
},
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.sampler) },
],
});
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("l0_taa_resolve_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &views[dst],
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.pipeline);
rp.set_bind_group(0, &bind, &[]);
rp.draw(0..3, 0..1);
}
self.cur = dst;
self.frame += 1;
true
}
#[must_use]
pub fn resolved(&self) -> Option<&wgpu::Texture> {
self.history.as_ref().map(|h| &h[self.cur])
}
#[must_use]
pub fn resolved_view(&self) -> Option<&wgpu::TextureView> {
self.history_views.as_ref().map(|v| &v[self.cur])
}
#[must_use]
pub fn size(&self) -> (u32, u32) {
self.size
}
#[must_use]
pub fn ready(&self) -> bool {
self.history.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn halton_is_the_radical_inverse() {
for (i, want) in [(1u32, 0.5), (2, 0.25), (3, 0.75), (4, 0.125), (5, 0.625), (6, 0.375), (7, 0.875)] {
assert!((halton(i, 2) - want).abs() < 1e-6, "halton({i}, 2) = {} want {want}", halton(i, 2));
}
for (i, want) in [(1u32, 1.0 / 3.0), (2, 2.0 / 3.0), (3, 1.0 / 9.0), (4, 4.0 / 9.0), (5, 7.0 / 9.0)] {
assert!((halton(i, 3) - want).abs() < 1e-6, "halton({i}, 3) = {} want {want}", halton(i, 3));
}
assert_eq!(halton(0, 2), 0.0, "index 0 is the origin — which is why jitter_px offsets by one");
assert_eq!(halton(5, 1), 0.0, "base < 2 is not a sequence");
assert_eq!(halton(5, 0), 0.0);
}
#[test]
fn every_jitter_phase_is_a_distinct_nonzero_subpixel_offset() {
let phases: Vec<[f32; 2]> = (0..TAA_PHASES as u64).map(jitter_px).collect();
for (n, j) in phases.iter().enumerate() {
assert!(
j[0] >= -0.5 && j[0] < 0.5 && j[1] >= -0.5 && j[1] < 0.5,
"phase {n} offset {j:?} is inside the pixel"
);
assert!(
j[0] != 0.0 || j[1] != 0.0,
"phase {n} is the ORIGIN — a phase that does not move the sample cannot antialias"
);
}
for i in 0..phases.len() {
for j in (i + 1)..phases.len() {
assert_ne!(phases[i], phases[j], "phases {i} and {j} are the same sample position");
}
}
let xs: Vec<f32> = phases.iter().map(|j| j[0]).collect();
let ys: Vec<f32> = phases.iter().map(|j| j[1]).collect();
let span = |v: &[f32]| v.iter().cloned().fold(f32::MIN, f32::max) - v.iter().cloned().fold(f32::MAX, f32::min);
assert!(span(&xs) >= 0.5, "the x offsets span {} of a pixel", span(&xs));
assert!(span(&ys) >= 0.5, "the y offsets span {} of a pixel", span(&ys));
}
#[test]
fn the_jitter_cycles_with_the_declared_period() {
for f in 0..40u64 {
assert_eq!(jitter_px(f), jitter_px(f + u64::from(TAA_PHASES)), "frame {f}");
}
assert_ne!(jitter_px(0), jitter_px(1), "…but consecutive frames differ");
}
#[test]
fn the_history_weight_ramps_then_saturates() {
assert_eq!(history_weight(0, 0.97), 0.0, "frame 0 has no history to weight");
assert!((history_weight(1, 0.97) - 0.5).abs() < 1e-6, "frame 1 averages two samples");
assert!((history_weight(2, 0.97) - 2.0 / 3.0).abs() < 1e-6);
assert!((history_weight(3, 0.97) - 0.75).abs() < 1e-6);
for f in 0..500u64 {
assert!(history_weight(f, 0.97) <= 0.97 + 1e-6, "frame {f} respects the cap");
}
assert!((history_weight(10_000, 0.97) - 0.97).abs() < 1e-6, "saturated");
for f in 0..10u64 {
assert_eq!(history_weight(f, 0.0), 0.0);
}
}
#[test]
fn the_uniform_is_two_vec4s() {
assert_eq!(std::mem::size_of::<TaaUniform>(), 32);
assert!(TAA_WGSL.contains("res: vec4<f32>"), "and the shader agrees");
assert!(TAA_WGSL.contains("params: vec4<f32>"));
assert!(
!TAA_WGSL.contains("vec3<"),
"no vec3 in a uniform — it aligns to 16, not 12, and silently resizes the struct"
);
}
#[test]
fn the_resolve_rejects_history_it_cannot_trust() {
assert!(TAA_WGSL.contains("clamp(hist, lo, hi)"), "history is box-clamped");
assert!(
TAA_WGSL.contains("huv.x >= 0.0 && huv.x <= 1.0"),
"a pixel reprojecting off screen has no history"
);
assert!(
!TAA_WGSL.contains("params.y > 0.5"),
"the redundant history-valid gate must stay gone — frame 0's weight is already 0"
);
}
}