1use std::f64::consts::PI;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum FftDirection {
30 Forward,
32 Inverse,
34}
35
36#[derive(Debug, Clone)]
41pub struct WgslFftPlan {
42 n: u32,
43 log2n: u32,
44 direction: FftDirection,
45 bit_reversal: Vec<u32>,
46 twiddles: Vec<f32>,
48}
49
50impl WgslFftPlan {
51 pub fn new(n: u32, direction: FftDirection) -> Result<Self, String> {
57 if n == 0 {
58 return Err("FFT length must be non-zero".to_string());
59 }
60 if !n.is_power_of_two() {
61 return Err(format!("FFT length {n} is not a power of two"));
62 }
63 let log2n = n.trailing_zeros();
64 let bit_reversal = (0..n).map(|i| reverse_bits(i, log2n)).collect();
65
66 let sign = match direction {
68 FftDirection::Forward => -1.0_f64,
69 FftDirection::Inverse => 1.0_f64,
70 };
71 let half = (n / 2).max(1) as usize;
72 let mut twiddles = Vec::with_capacity(half * 2);
73 for k in 0..half {
74 let angle = sign * 2.0 * PI * (k as f64) / (n as f64);
75 twiddles.push(angle.cos() as f32);
76 twiddles.push(angle.sin() as f32);
77 }
78
79 Ok(Self {
80 n,
81 log2n,
82 direction,
83 bit_reversal,
84 twiddles,
85 })
86 }
87
88 #[must_use]
90 pub fn len(&self) -> u32 {
91 self.n
92 }
93
94 #[must_use]
96 pub fn is_empty(&self) -> bool {
97 self.n == 0
98 }
99
100 #[must_use]
102 pub fn num_stages(&self) -> u32 {
103 self.log2n
104 }
105
106 #[must_use]
108 pub fn direction(&self) -> FftDirection {
109 self.direction
110 }
111
112 #[must_use]
115 pub fn bit_reversal(&self) -> &[u32] {
116 &self.bit_reversal
117 }
118
119 #[must_use]
121 pub fn twiddles(&self) -> &[f32] {
122 &self.twiddles
123 }
124
125 #[must_use]
128 pub fn inverse_scale(&self) -> f32 {
129 match self.direction {
130 FftDirection::Inverse => 1.0 / self.n as f32,
131 FftDirection::Forward => 1.0,
132 }
133 }
134
135 #[must_use]
140 pub fn stage_half_size(&self, stage: u32) -> u32 {
141 if stage >= self.log2n {
142 0
143 } else {
144 1u32 << stage
145 }
146 }
147}
148
149#[inline]
151#[must_use]
152pub fn reverse_bits(value: u32, bits: u32) -> u32 {
153 if bits == 0 {
154 return 0;
155 }
156 let mut v = value;
157 let mut r = 0u32;
158 for _ in 0..bits {
159 r = (r << 1) | (v & 1);
160 v >>= 1;
161 }
162 r
163}
164
165#[must_use]
171pub fn fft_bitreverse_wgsl() -> String {
172 r#"
173struct FftReorderParams {
174 n: u32,
175}
176
177@group(0) @binding(0) var<storage, read> src: array<f32>;
178@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
179@group(0) @binding(2) var<storage, read> perm: array<u32>;
180@group(0) @binding(3) var<uniform> params: FftReorderParams;
181
182@compute @workgroup_size(256)
183fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
184 let i = gid.x;
185 if (i >= params.n) { return; }
186 let j = perm[i];
187 // Move complex element j -> i.
188 dst[2u * i] = src[2u * j];
189 dst[2u * i + 1u] = src[2u * j + 1u];
190}
191"#
192 .to_string()
193}
194
195#[must_use]
205pub fn fft_stage_wgsl() -> String {
206 r#"
207struct FftStageParams {
208 n: u32,
209 half_size: u32, // m / 2 for this stage
210 twiddle_step: u32, // n / (2 * half_size)
211 _pad: u32,
212}
213
214@group(0) @binding(0) var<storage, read_write> data: array<f32>;
215@group(0) @binding(1) var<storage, read> twiddles: array<f32>;
216@group(0) @binding(2) var<uniform> params: FftStageParams;
217
218@compute @workgroup_size(256)
219fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
220 let pair = gid.x;
221 let total_pairs = params.n / 2u;
222 if (pair >= total_pairs) { return; }
223
224 let m = params.half_size * 2u;
225 // Index of this butterfly within its group, and the group base.
226 let k = pair % params.half_size;
227 let group = pair / params.half_size;
228 let base = group * m;
229 let i0 = base + k; // top of the butterfly
230 let i1 = i0 + params.half_size; // bottom
231
232 // Twiddle W = (wr, wi) for this k.
233 let tw = k * params.twiddle_step;
234 let wr = twiddles[2u * tw];
235 let wi = twiddles[2u * tw + 1u];
236
237 let ar = data[2u * i0];
238 let ai = data[2u * i0 + 1u];
239 let br = data[2u * i1];
240 let bi = data[2u * i1 + 1u];
241
242 // t = W * b (complex multiply).
243 let tr = wr * br - wi * bi;
244 let ti = wr * bi + wi * br;
245
246 // Butterfly: a' = a + t, b' = a - t.
247 data[2u * i0] = ar + tr;
248 data[2u * i0 + 1u] = ai + ti;
249 data[2u * i1] = ar - tr;
250 data[2u * i1 + 1u] = ai - ti;
251}
252"#
253 .to_string()
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn reverse_bits_known_values() {
262 assert_eq!(reverse_bits(0b001, 3), 0b100);
264 assert_eq!(reverse_bits(0b110, 3), 0b011);
265 assert_eq!(reverse_bits(0b000, 3), 0b000);
266 assert_eq!(reverse_bits(0b111, 3), 0b111);
267 assert_eq!(reverse_bits(5, 0), 0);
269 }
270
271 #[test]
272 fn plan_rejects_non_power_of_two() {
273 assert!(WgslFftPlan::new(0, FftDirection::Forward).is_err());
274 assert!(WgslFftPlan::new(3, FftDirection::Forward).is_err());
275 assert!(WgslFftPlan::new(6, FftDirection::Forward).is_err());
276 assert!(WgslFftPlan::new(7, FftDirection::Forward).is_err());
277 }
278
279 #[test]
280 fn plan_accepts_powers_of_two() {
281 for &n in &[1u32, 2, 4, 8, 16, 1024] {
282 let plan = WgslFftPlan::new(n, FftDirection::Forward).expect("power of two");
283 assert_eq!(plan.len(), n);
284 assert_eq!(plan.num_stages(), n.trailing_zeros());
285 assert!(!plan.is_empty());
286 }
287 }
288
289 #[test]
290 fn plan_bit_reversal_is_a_permutation() {
291 let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
292 let perm = plan.bit_reversal();
293 assert_eq!(perm.len(), 8);
294 assert_eq!(perm, &[0, 4, 2, 6, 1, 5, 3, 7]);
296 let mut seen = perm.to_vec();
298 seen.sort_unstable();
299 assert_eq!(seen, (0u32..8).collect::<Vec<_>>());
300 }
301
302 #[test]
303 fn plan_twiddles_length_and_dc() {
304 let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
305 assert_eq!(plan.twiddles().len(), 8);
307 assert!((plan.twiddles()[0] - 1.0).abs() < 1e-6);
309 assert!(plan.twiddles()[1].abs() < 1e-6);
310 }
311
312 #[test]
313 fn plan_forward_twiddle_sign_is_negative() {
314 let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
316 let re = plan.twiddles()[2];
317 let im = plan.twiddles()[3];
318 assert!((re - 0.707_106_77).abs() < 1e-5);
319 assert!((im + 0.707_106_77).abs() < 1e-5, "im was {im}");
320 }
321
322 #[test]
323 fn plan_inverse_twiddle_sign_is_positive() {
324 let plan = WgslFftPlan::new(8, FftDirection::Inverse).expect("plan");
326 let im = plan.twiddles()[3];
327 assert!(
328 im > 0.0,
329 "inverse imaginary twiddle should be positive, got {im}"
330 );
331 assert!((plan.inverse_scale() - (1.0 / 8.0)).abs() < 1e-7);
332 }
333
334 #[test]
335 fn plan_forward_inverse_scale() {
336 let fwd = WgslFftPlan::new(16, FftDirection::Forward).expect("plan");
337 assert!((fwd.inverse_scale() - 1.0).abs() < 1e-7);
338 let inv = WgslFftPlan::new(16, FftDirection::Inverse).expect("plan");
339 assert!((inv.inverse_scale() - 1.0 / 16.0).abs() < 1e-7);
340 }
341
342 #[test]
343 fn plan_stage_half_sizes() {
344 let plan = WgslFftPlan::new(8, FftDirection::Forward).expect("plan");
345 assert_eq!(plan.stage_half_size(0), 1);
347 assert_eq!(plan.stage_half_size(1), 2);
348 assert_eq!(plan.stage_half_size(2), 4);
349 assert_eq!(plan.stage_half_size(3), 0);
351 }
352
353 #[test]
354 fn plan_n1_has_zero_stages() {
355 let plan = WgslFftPlan::new(1, FftDirection::Forward).expect("plan");
356 assert_eq!(plan.num_stages(), 0);
357 assert_eq!(plan.twiddles().len(), 2);
359 }
360
361 #[test]
364 fn wgsl_bitreverse_moves_by_permutation() {
365 let src = fft_bitreverse_wgsl();
366 assert!(src.contains("@compute @workgroup_size(256)"));
367 assert!(src.contains("let j = perm[i];"));
368 assert!(src.contains("dst[2u * i] = src[2u * j];"));
369 assert!(src.contains("dst[2u * i + 1u] = src[2u * j + 1u];"));
370 assert!(src.contains("perm: array<u32>"));
372 }
373
374 #[test]
375 fn wgsl_fft_stage_complex_butterfly() {
376 let src = fft_stage_wgsl();
377 assert!(src.contains("@compute @workgroup_size(256)"));
378 assert!(src.contains("let tr = wr * br - wi * bi;"));
380 assert!(src.contains("let ti = wr * bi + wi * br;"));
381 assert!(src.contains("data[2u * i0] = ar + tr;"));
383 assert!(src.contains("data[2u * i1] = ar - tr;"));
384 }
385
386 #[test]
387 fn wgsl_fft_stage_uses_precomputed_twiddles() {
388 let src = fft_stage_wgsl();
389 assert!(src.contains("twiddles: array<f32>"));
391 assert!(src.contains("let wr = twiddles[2u * tw];"));
392 assert!(src.contains("let wi = twiddles[2u * tw + 1u];"));
393 assert!(!src.contains("sin("));
394 assert!(!src.contains("cos("));
395 assert!(src.contains("half_size: u32"));
397 assert!(src.contains("twiddle_step: u32"));
398 }
399
400 #[test]
401 fn wgsl_fft_stage_has_bounds_guard() {
402 let src = fft_stage_wgsl();
403 assert!(src.contains("let total_pairs = params.n / 2u;"));
404 assert!(src.contains("if (pair >= total_pairs) { return; }"));
405 }
406}