use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FftDirection {
Forward,
Inverse,
}
#[derive(Debug, Clone)]
pub struct WgslFftPlan {
n: u32,
log2n: u32,
direction: FftDirection,
bit_reversal: Vec<u32>,
twiddles: Vec<f32>,
}
impl WgslFftPlan {
pub fn new(n: u32, direction: FftDirection) -> Result<Self, String> {
if n == 0 {
return Err("FFT length must be non-zero".to_string());
}
if !n.is_power_of_two() {
return Err(format!("FFT length {n} is not a power of two"));
}
let log2n = n.trailing_zeros();
let bit_reversal = (0..n).map(|i| reverse_bits(i, log2n)).collect();
let sign = match direction {
FftDirection::Forward => -1.0_f64,
FftDirection::Inverse => 1.0_f64,
};
let half = (n / 2).max(1) as usize;
let mut twiddles = Vec::with_capacity(half * 2);
for k in 0..half {
let angle = sign * 2.0 * PI * (k as f64) / (n as f64);
twiddles.push(angle.cos() as f32);
twiddles.push(angle.sin() as f32);
}
Ok(Self {
n,
log2n,
direction,
bit_reversal,
twiddles,
})
}
#[must_use]
pub fn len(&self) -> u32 {
self.n
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.n == 0
}
#[must_use]
pub fn num_stages(&self) -> u32 {
self.log2n
}
#[must_use]
pub fn direction(&self) -> FftDirection {
self.direction
}
#[must_use]
pub fn bit_reversal(&self) -> &[u32] {
&self.bit_reversal
}
#[must_use]
pub fn twiddles(&self) -> &[f32] {
&self.twiddles
}
#[must_use]
pub fn inverse_scale(&self) -> f32 {
match self.direction {
FftDirection::Inverse => 1.0 / self.n as f32,
FftDirection::Forward => 1.0,
}
}
#[must_use]
pub fn stage_half_size(&self, stage: u32) -> u32 {
if stage >= self.log2n {
0
} else {
1u32 << stage
}
}
}
#[inline]
#[must_use]
pub fn reverse_bits(value: u32, bits: u32) -> u32 {
if bits == 0 {
return 0;
}
let mut v = value;
let mut r = 0u32;
for _ in 0..bits {
r = (r << 1) | (v & 1);
v >>= 1;
}
r
}
#[must_use]
pub fn fft_bitreverse_wgsl() -> String {
r#"
struct FftReorderParams {
n: u32,
}
@group(0) @binding(0) var<storage, read> src: array<f32>;
@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
@group(0) @binding(2) var<storage, read> perm: array<u32>;
@group(0) @binding(3) var<uniform> params: FftReorderParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= params.n) { return; }
let j = perm[i];
// Move complex element j -> i.
dst[2u * i] = src[2u * j];
dst[2u * i + 1u] = src[2u * j + 1u];
}
"#
.to_string()
}
#[must_use]
pub fn fft_stage_wgsl() -> String {
r#"
struct FftStageParams {
n: u32,
half_size: u32, // m / 2 for this stage
twiddle_step: u32, // n / (2 * half_size)
_pad: u32,
}
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@group(0) @binding(1) var<storage, read> twiddles: array<f32>;
@group(0) @binding(2) var<uniform> params: FftStageParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let pair = gid.x;
let total_pairs = params.n / 2u;
if (pair >= total_pairs) { return; }
let m = params.half_size * 2u;
// Index of this butterfly within its group, and the group base.
let k = pair % params.half_size;
let group = pair / params.half_size;
let base = group * m;
let i0 = base + k; // top of the butterfly
let i1 = i0 + params.half_size; // bottom
// Twiddle W = (wr, wi) for this k.
let tw = k * params.twiddle_step;
let wr = twiddles[2u * tw];
let wi = twiddles[2u * tw + 1u];
let ar = data[2u * i0];
let ai = data[2u * i0 + 1u];
let br = data[2u * i1];
let bi = data[2u * i1 + 1u];
// t = W * b (complex multiply).
let tr = wr * br - wi * bi;
let ti = wr * bi + wi * br;
// Butterfly: a' = a + t, b' = a - t.
data[2u * i0] = ar + tr;
data[2u * i0 + 1u] = ai + ti;
data[2u * i1] = ar - tr;
data[2u * i1 + 1u] = ai - ti;
}
"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reverse_bits_known_values() {
assert_eq!(reverse_bits(0b001, 3), 0b100);
assert_eq!(reverse_bits(0b110, 3), 0b011);
assert_eq!(reverse_bits(0b000, 3), 0b000);
assert_eq!(reverse_bits(0b111, 3), 0b111);
assert_eq!(reverse_bits(5, 0), 0);
}
#[test]
fn plan_rejects_non_power_of_two() {
assert!(WgslFftPlan::new(0, FftDirection::Forward).is_err());
assert!(WgslFftPlan::new(3, FftDirection::Forward).is_err());
assert!(WgslFftPlan::new(6, FftDirection::Forward).is_err());
assert!(WgslFftPlan::new(7, FftDirection::Forward).is_err());
}
#[test]
fn plan_accepts_powers_of_two() {
for &n in &[1u32, 2, 4, 8, 16, 1024] {
let plan = WgslFftPlan::new(n, FftDirection::Forward).expect("power of two");
assert_eq!(plan.len(), n);
assert_eq!(plan.num_stages(), n.trailing_zeros());
assert!(!plan.is_empty());
}
}
#[test]
fn plan_bit_reversal_is_a_permutation() {
let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
let perm = plan.bit_reversal();
assert_eq!(perm.len(), 8);
assert_eq!(perm, &[0, 4, 2, 6, 1, 5, 3, 7]);
let mut seen = perm.to_vec();
seen.sort_unstable();
assert_eq!(seen, (0u32..8).collect::<Vec<_>>());
}
#[test]
fn plan_twiddles_length_and_dc() {
let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
assert_eq!(plan.twiddles().len(), 8);
assert!((plan.twiddles()[0] - 1.0).abs() < 1e-6);
assert!(plan.twiddles()[1].abs() < 1e-6);
}
#[test]
fn plan_forward_twiddle_sign_is_negative() {
let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
let re = plan.twiddles()[2];
let im = plan.twiddles()[3];
assert!((re - 0.707_106_77).abs() < 1e-5);
assert!((im + 0.707_106_77).abs() < 1e-5, "im was {im}");
}
#[test]
fn plan_inverse_twiddle_sign_is_positive() {
let plan = WgslFftPlan::new(8, FftDirection::Inverse).expect("plan");
let im = plan.twiddles()[3];
assert!(
im > 0.0,
"inverse imaginary twiddle should be positive, got {im}"
);
assert!((plan.inverse_scale() - (1.0 / 8.0)).abs() < 1e-7);
}
#[test]
fn plan_forward_inverse_scale() {
let fwd = WgslFftPlan::new(16, FftDirection::Forward).expect("plan");
assert!((fwd.inverse_scale() - 1.0).abs() < 1e-7);
let inv = WgslFftPlan::new(16, FftDirection::Inverse).expect("plan");
assert!((inv.inverse_scale() - 1.0 / 16.0).abs() < 1e-7);
}
#[test]
fn plan_stage_half_sizes() {
let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
assert_eq!(plan.stage_half_size(0), 1);
assert_eq!(plan.stage_half_size(1), 2);
assert_eq!(plan.stage_half_size(2), 4);
assert_eq!(plan.stage_half_size(3), 0);
}
#[test]
fn plan_n1_has_zero_stages() {
let plan = WgslFftPlan::new(1, FftDirection::Forward).expect("plan");
assert_eq!(plan.num_stages(), 0);
assert_eq!(plan.twiddles().len(), 2);
}
#[test]
fn wgsl_bitreverse_moves_by_permutation() {
let src = fft_bitreverse_wgsl();
assert!(src.contains("@compute @workgroup_size(256)"));
assert!(src.contains("let j = perm[i];"));
assert!(src.contains("dst[2u * i] = src[2u * j];"));
assert!(src.contains("dst[2u * i + 1u] = src[2u * j + 1u];"));
assert!(src.contains("perm: array<u32>"));
}
#[test]
fn wgsl_fft_stage_complex_butterfly() {
let src = fft_stage_wgsl();
assert!(src.contains("@compute @workgroup_size(256)"));
assert!(src.contains("let tr = wr * br - wi * bi;"));
assert!(src.contains("let ti = wr * bi + wi * br;"));
assert!(src.contains("data[2u * i0] = ar + tr;"));
assert!(src.contains("data[2u * i1] = ar - tr;"));
}
#[test]
fn wgsl_fft_stage_uses_precomputed_twiddles() {
let src = fft_stage_wgsl();
assert!(src.contains("twiddles: array<f32>"));
assert!(src.contains("let wr = twiddles[2u * tw];"));
assert!(src.contains("let wi = twiddles[2u * tw + 1u];"));
assert!(!src.contains("sin("));
assert!(!src.contains("cos("));
assert!(src.contains("half_size: u32"));
assert!(src.contains("twiddle_step: u32"));
}
#[test]
fn wgsl_fft_stage_has_bounds_guard() {
let src = fft_stage_wgsl();
assert!(src.contains("let total_pairs = params.n / 2u;"));
assert!(src.contains("if (pair >= total_pairs) { return; }"));
}
}