1use crate::context::GpuContext;
23use crate::error::{GpuError, GpuResult};
24use crate::shaders::{
25 ComputePipelineBuilder, WgslShader, create_compute_bind_group_layout, storage_buffer_layout,
26};
27use std::f32::consts::PI;
28use std::sync::Arc;
29use tracing::debug;
30use wgpu::{BindGroupDescriptor, BindGroupEntry, BufferDescriptor, BufferUsages};
31
32pub fn bit_reverse(x: u32, log2_n: u32) -> u32 {
48 let mut result = 0u32;
49 let mut v = x;
50 for _ in 0..log2_n {
51 result = (result << 1) | (v & 1);
52 v >>= 1;
53 }
54 result
55}
56
57pub fn twiddle_factor(k: u32, n: u32, inverse: bool) -> (f32, f32) {
63 let sign = if inverse { 1.0_f32 } else { -1.0_f32 };
64 let angle = sign * 2.0 * PI * (k as f32) / (n as f32);
65 (angle.cos(), angle.sin())
66}
67
68pub fn make_fft_shader_source(size: u32, inverse: bool) -> String {
83 let log2_n = size.trailing_zeros();
84 let half_size = size / 2;
85 let sign_str = if inverse { "1.0" } else { "-1.0" };
86 let is_inverse_str = if inverse { "true" } else { "false" };
87
88 let bit_rev_fn = build_bit_rev_fn(log2_n);
90
91 let butterfly_stages = build_butterfly_stages(log2_n, half_size, sign_str);
93
94 let normalise_block = if inverse {
96 format!(
97 r#"
98 // --- Inverse normalisation: divide by N ---
99 shared_real[thread_id] /= f32({size}u);
100 shared_imag[thread_id] /= f32({size}u);
101 shared_real[thread_id + {half_size}u] /= f32({size}u);
102 shared_imag[thread_id + {half_size}u] /= f32({size}u);
103 workgroupBarrier();
104"#,
105 size = size,
106 half_size = half_size,
107 )
108 } else {
109 String::new()
110 };
111
112 format!(
114 r#"// FFT size = {size}, log2 = {log2_n}, inverse = {is_inverse_str}
115@group(0) @binding(0) var<storage, read_write> real_buf: array<f32>;
116@group(0) @binding(1) var<storage, read_write> imag_buf: array<f32>;
117
118var<workgroup> shared_real: array<f32, {size}>;
119var<workgroup> shared_imag: array<f32, {size}>;
120
121{bit_rev_fn}
122
123@compute @workgroup_size({half_size}, 1, 1)
124fn main(
125 @builtin(local_invocation_id) lid: vec3<u32>,
126 @builtin(workgroup_id) wid: vec3<u32>,
127) {{
128 let thread_id: u32 = lid.x;
129 let batch_offset: u32 = wid.x * {size}u;
130
131 // --- 1. Bit-reversal permutation ---
132 let rev0: u32 = bit_rev(thread_id);
133 let rev1: u32 = bit_rev(thread_id + {half_size}u);
134 shared_real[rev0] = real_buf[batch_offset + thread_id];
135 shared_imag[rev0] = imag_buf[batch_offset + thread_id];
136 shared_real[rev1] = real_buf[batch_offset + thread_id + {half_size}u];
137 shared_imag[rev1] = imag_buf[batch_offset + thread_id + {half_size}u];
138 workgroupBarrier();
139
140 // --- 2. FFT butterfly stages (unrolled {log2_n} stages) ---
141{butterfly_stages}
142{normalise_block}
143 // --- 3. Write back ---
144 real_buf[batch_offset + thread_id] = shared_real[thread_id];
145 imag_buf[batch_offset + thread_id] = shared_imag[thread_id];
146 real_buf[batch_offset + thread_id + {half_size}u] = shared_real[thread_id + {half_size}u];
147 imag_buf[batch_offset + thread_id + {half_size}u] = shared_imag[thread_id + {half_size}u];
148}}
149"#,
150 size = size,
151 log2_n = log2_n,
152 half_size = half_size,
153 is_inverse_str = is_inverse_str,
154 bit_rev_fn = bit_rev_fn,
155 butterfly_stages = butterfly_stages,
156 normalise_block = normalise_block,
157 )
158}
159
160fn build_bit_rev_fn(log2_n: u32) -> String {
162 let mut body = String::new();
163 body.push_str("fn bit_rev(x: u32) -> u32 {\n");
164 body.push_str(" var v: u32 = x;\n");
165 body.push_str(" var r: u32 = 0u;\n");
166 for _ in 0..log2_n {
167 body.push_str(" r = (r << 1u) | (v & 1u);\n");
168 body.push_str(" v = v >> 1u;\n");
169 }
170 body.push_str(" return r;\n");
171 body.push_str("}\n");
172 body
173}
174
175fn build_butterfly_stages(log2_n: u32, _half_size: u32, sign_str: &str) -> String {
177 let mut code = String::new();
178 for s in 0..log2_n {
179 let span = 1u32 << (s + 1);
180 let half_span = span >> 1;
181 code.push_str(&format!(
182 r#"
183 // Stage {s}: span = {span}, half_span = {half_span}
184 {{
185 let span_{s}: u32 = {span}u;
186 let half_span_{s}: u32 = {half_span}u;
187 let pos_{s}: u32 = thread_id % half_span_{s};
188 let grp_{s}: u32 = thread_id / half_span_{s};
189 let i_{s}: u32 = grp_{s} * span_{s} + pos_{s};
190 let j_{s}: u32 = i_{s} + half_span_{s};
191 let angle_{s}: f32 = {sign_str} * 2.0 * 3.14159265358979323846 * f32(pos_{s}) / f32(span_{s});
192 let wr_{s}: f32 = cos(angle_{s});
193 let wi_{s}: f32 = sin(angle_{s});
194 let tr_{s}: f32 = wr_{s} * shared_real[j_{s}] - wi_{s} * shared_imag[j_{s}];
195 let ti_{s}: f32 = wr_{s} * shared_imag[j_{s}] + wi_{s} * shared_real[j_{s}];
196 shared_real[j_{s}] = shared_real[i_{s}] - tr_{s};
197 shared_imag[j_{s}] = shared_imag[i_{s}] - ti_{s};
198 shared_real[i_{s}] = shared_real[i_{s}] + tr_{s};
199 shared_imag[i_{s}] = shared_imag[i_{s}] + ti_{s};
200 workgroupBarrier();
201 }}
202"#,
203 s = s,
204 span = span,
205 half_span = half_span,
206 sign_str = sign_str,
207 ));
208 }
209 code
210}
211
212pub struct Fft1d {
222 pipeline: Arc<wgpu::ComputePipeline>,
223 bind_group_layout: wgpu::BindGroupLayout,
224 size: u32,
226 inverse: bool,
228}
229
230impl Fft1d {
231 pub fn new(ctx: &GpuContext, size: u32, inverse: bool) -> GpuResult<Self> {
243 if !size.is_power_of_two() {
245 return Err(GpuError::invalid_kernel_params(
246 "FFT size must be power of two",
247 ));
248 }
249 if size < 4 {
250 return Err(GpuError::invalid_kernel_params("FFT size must be >= 4"));
251 }
252 if size > 2048 {
253 return Err(GpuError::invalid_kernel_params("FFT size must be <= 2048"));
254 }
255
256 let shader_src = make_fft_shader_source(size, inverse);
258 let mut shader = WgslShader::new(shader_src, "main");
259 let shader_module = shader.compile(ctx.device())?;
260
261 let bind_group_layout = create_compute_bind_group_layout(
263 ctx.device(),
264 &[
265 storage_buffer_layout(0, false), storage_buffer_layout(1, false), ],
268 Some("Fft1d Bind Group Layout"),
269 )?;
270
271 let pipeline = ComputePipelineBuilder::new(ctx.device(), shader_module, "main")
272 .bind_group_layout(&bind_group_layout)
273 .label("Fft1d Pipeline")
274 .build()?;
275
276 debug!("Fft1d created: size={}, inverse={}", size, inverse);
277
278 Ok(Self {
279 pipeline: Arc::new(pipeline),
280 bind_group_layout,
281 size,
282 inverse,
283 })
284 }
285
286 pub fn size(&self) -> u32 {
290 self.size
291 }
292
293 pub fn is_inverse(&self) -> bool {
295 self.inverse
296 }
297
298 pub fn execute(
315 &self,
316 ctx: &GpuContext,
317 real_in: &[f32],
318 imag_in: &[f32],
319 ) -> GpuResult<(Vec<f32>, Vec<f32>)> {
320 let n = self.size as usize;
321 if real_in.len() != n {
322 return Err(GpuError::invalid_kernel_params(format!(
323 "real_in length {} != FFT size {}",
324 real_in.len(),
325 n
326 )));
327 }
328 if imag_in.len() != n {
329 return Err(GpuError::invalid_kernel_params(format!(
330 "imag_in length {} != FFT size {}",
331 imag_in.len(),
332 n
333 )));
334 }
335
336 let results = self.dispatch_batch_internal(ctx, &[(real_in.to_vec(), imag_in.to_vec())])?;
337 let (r, i) = results.into_iter().next().ok_or_else(|| {
338 GpuError::internal("dispatch returned empty batch for single transform")
339 })?;
340 Ok((r, i))
341 }
342
343 pub fn execute_batch(
353 &self,
354 ctx: &GpuContext,
355 batch: &[(Vec<f32>, Vec<f32>)],
356 ) -> GpuResult<Vec<(Vec<f32>, Vec<f32>)>> {
357 let n = self.size as usize;
358 for (idx, (r, im)) in batch.iter().enumerate() {
359 if r.len() != n {
360 return Err(GpuError::invalid_kernel_params(format!(
361 "batch[{}].real length {} != FFT size {}",
362 idx,
363 r.len(),
364 n
365 )));
366 }
367 if im.len() != n {
368 return Err(GpuError::invalid_kernel_params(format!(
369 "batch[{}].imag length {} != FFT size {}",
370 idx,
371 im.len(),
372 n
373 )));
374 }
375 }
376
377 self.dispatch_batch_internal(ctx, batch)
378 }
379
380 fn dispatch_batch_internal(
383 &self,
384 ctx: &GpuContext,
385 batch: &[(Vec<f32>, Vec<f32>)],
386 ) -> GpuResult<Vec<(Vec<f32>, Vec<f32>)>> {
387 let batch_count = batch.len() as u32;
388 let n = self.size as usize;
389 let total = batch_count as usize * n;
390
391 let mut flat_real: Vec<f32> = Vec::with_capacity(total);
393 let mut flat_imag: Vec<f32> = Vec::with_capacity(total);
394 for (r, im) in batch {
395 flat_real.extend_from_slice(r);
396 flat_imag.extend_from_slice(im);
397 }
398
399 let buf_size = (total * std::mem::size_of::<f32>()) as u64;
401 let aligned_buf_size = (buf_size + 255) & !255;
403
404 let real_buf = ctx.device().create_buffer(&BufferDescriptor {
405 label: Some("Fft1d real_buf"),
406 size: aligned_buf_size,
407 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
408 mapped_at_creation: false,
409 });
410 let imag_buf = ctx.device().create_buffer(&BufferDescriptor {
411 label: Some("Fft1d imag_buf"),
412 size: aligned_buf_size,
413 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST,
414 mapped_at_creation: false,
415 });
416
417 ctx.queue()
419 .write_buffer(&real_buf, 0, bytemuck::cast_slice(&flat_real));
420 ctx.queue()
421 .write_buffer(&imag_buf, 0, bytemuck::cast_slice(&flat_imag));
422
423 let bind_group = ctx.device().create_bind_group(&BindGroupDescriptor {
425 label: Some("Fft1d BindGroup"),
426 layout: &self.bind_group_layout,
427 entries: &[
428 BindGroupEntry {
429 binding: 0,
430 resource: real_buf.as_entire_binding(),
431 },
432 BindGroupEntry {
433 binding: 1,
434 resource: imag_buf.as_entire_binding(),
435 },
436 ],
437 });
438
439 let mut encoder = ctx
441 .device()
442 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
443 label: Some("Fft1d encoder"),
444 });
445
446 {
447 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
448 label: Some("Fft1d compute pass"),
449 timestamp_writes: None,
450 });
451 cpass.set_pipeline(&self.pipeline);
452 cpass.set_bind_group(0, &bind_group, &[]);
453 cpass.dispatch_workgroups(batch_count, 1, 1);
454 }
455
456 let staging_real = ctx.device().create_buffer(&BufferDescriptor {
458 label: Some("Fft1d staging_real"),
459 size: aligned_buf_size,
460 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
461 mapped_at_creation: false,
462 });
463 let staging_imag = ctx.device().create_buffer(&BufferDescriptor {
464 label: Some("Fft1d staging_imag"),
465 size: aligned_buf_size,
466 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
467 mapped_at_creation: false,
468 });
469
470 encoder.copy_buffer_to_buffer(&real_buf, 0, &staging_real, 0, aligned_buf_size);
471 encoder.copy_buffer_to_buffer(&imag_buf, 0, &staging_imag, 0, aligned_buf_size);
472
473 ctx.queue().submit(Some(encoder.finish()));
474
475 let real_slice = staging_real.slice(..);
477 let (real_tx, real_rx) = std::sync::mpsc::sync_channel(1);
478 real_slice.map_async(wgpu::MapMode::Read, move |result| {
479 let _ = real_tx.send(result);
480 });
481
482 let imag_slice = staging_imag.slice(..);
483 let (imag_tx, imag_rx) = std::sync::mpsc::sync_channel(1);
484 imag_slice.map_async(wgpu::MapMode::Read, move |result| {
485 let _ = imag_tx.send(result);
486 });
487
488 ctx.device()
489 .poll(wgpu::PollType::wait_indefinitely())
490 .map_err(|e| GpuError::execution_failed(format!("device poll failed: {e}")))?;
491
492 let real_out = Self::finish_staging_read(&staging_real, real_slice, real_rx, total)?;
493 let imag_out = Self::finish_staging_read(&staging_imag, imag_slice, imag_rx, total)?;
494
495 debug!(
496 "Fft1d dispatched: batch={}, size={}, inverse={}",
497 batch_count, self.size, self.inverse
498 );
499
500 let mut results = Vec::with_capacity(batch_count as usize);
502 for i in 0..batch_count as usize {
503 let r = real_out[i * n..(i + 1) * n].to_vec();
504 let im = imag_out[i * n..(i + 1) * n].to_vec();
505 results.push((r, im));
506 }
507 Ok(results)
508 }
509
510 fn finish_staging_read(
514 staging: &wgpu::Buffer,
515 slice: wgpu::BufferSlice<'_>,
516 rx: std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
517 count: usize,
518 ) -> GpuResult<Vec<f32>> {
519 rx.recv()
520 .map_err(|_| GpuError::buffer_mapping("staging channel closed"))?
521 .map_err(|e| GpuError::buffer_mapping(e.to_string()))?;
522
523 let mapped = slice.get_mapped_range();
524 let floats: &[f32] = bytemuck::cast_slice(&mapped[..count * 4]);
525 let out = floats.to_vec();
526 drop(mapped);
527 staging.unmap();
528 Ok(out)
529 }
530}
531
532pub fn validate_size(size: u32) -> GpuResult<()> {
548 if !size.is_power_of_two() {
549 return Err(GpuError::invalid_kernel_params(
550 "FFT size must be power of two",
551 ));
552 }
553 if size < 4 {
554 return Err(GpuError::invalid_kernel_params("FFT size must be >= 4"));
555 }
556 if size > 2048 {
557 return Err(GpuError::invalid_kernel_params("FFT size must be <= 2048"));
558 }
559 Ok(())
560}
561
562#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn test_bit_reverse_known_values() {
572 assert_eq!(bit_reverse(0, 3), 0);
574 assert_eq!(bit_reverse(1, 3), 4);
575 assert_eq!(bit_reverse(2, 3), 2);
576 assert_eq!(bit_reverse(3, 3), 6);
577 assert_eq!(bit_reverse(4, 3), 1);
578 assert_eq!(bit_reverse(5, 3), 5);
579 assert_eq!(bit_reverse(6, 3), 3);
580 assert_eq!(bit_reverse(7, 3), 7);
581 }
582
583 #[test]
584 fn test_twiddle_factor_known() {
585 let (r, i) = twiddle_factor(0, 4, false);
586 assert!((r - 1.0).abs() < 1e-6, "cos(0) = 1, got {}", r);
587 assert!(i.abs() < 1e-6, "sin(0) = 0, got {}", i);
588
589 let (r, i) = twiddle_factor(1, 4, false);
590 assert!(r.abs() < 1e-6, "cos(-π/2) ≈ 0, got {}", r);
591 assert!((i + 1.0).abs() < 1e-6, "sin(-π/2) = -1, got {}", i);
592 }
593
594 #[test]
595 fn test_make_shader_contains_log2n_3() {
596 let src = make_fft_shader_source(8, false);
597 assert!(
599 src.contains("3u") || src.contains("3"),
600 "shader must reference log2(8) = 3"
601 );
602 }
603
604 #[test]
605 fn test_make_shader_inverse_contains_scale() {
606 let src = make_fft_shader_source(8, true);
607 assert!(
609 src.contains("8u") || src.contains("8.0") || src.contains("0.125"),
610 "inverse shader must contain 1/N scale factor for N=8"
611 );
612 }
613
614 #[test]
615 fn test_new_rejects_non_power_of_two() {
616 let result = validate_size(6);
619 assert!(
620 result.is_err(),
621 "size=6 is not a power of two, must be rejected"
622 );
623 }
624
625 #[test]
626 fn test_new_rejects_size_below_min() {
627 let result = validate_size(2);
628 assert!(result.is_err(), "size=2 < 4, must be rejected");
629 }
630
631 #[test]
632 fn test_new_rejects_size_above_max() {
633 let result = validate_size(4096);
634 assert!(result.is_err(), "size=4096 > 2048, must be rejected");
635 }
636}