Skip to main content

rustfft/
array_utils.rs

1use crate::common::RadixFactor;
2use crate::Complex;
3use crate::FftNum;
4use std::ops::{Deref, DerefMut};
5
6/// Given an array of size width * height, representing a flattened 2D array,
7/// transpose the rows and columns of that 2D array into the output
8/// benchmarking shows that loop tiling isn't effective for small arrays (in the range of 50x50 or smaller)
9pub unsafe fn transpose_small<T: Copy>(width: usize, height: usize, input: &[T], output: &mut [T]) {
10    for x in 0..width {
11        for y in 0..height {
12            let input_index = x + y * width;
13            let output_index = y + x * height;
14
15            *output.get_unchecked_mut(output_index) = *input.get_unchecked(input_index);
16        }
17    }
18}
19
20#[allow(unused)]
21pub unsafe fn workaround_transmute<T, U>(slice: &[T]) -> &[U] {
22    let ptr = slice.as_ptr() as *const U;
23    let len = slice.len();
24    std::slice::from_raw_parts(ptr, len)
25}
26#[allow(unused)]
27pub unsafe fn workaround_transmute_mut<T, U>(slice: &mut [T]) -> &mut [U] {
28    let ptr = slice.as_mut_ptr() as *mut U;
29    let len = slice.len();
30    std::slice::from_raw_parts_mut(ptr, len)
31}
32
33pub(crate) trait LoadStore<T: FftNum>: DerefMut {
34    unsafe fn load(&self, idx: usize) -> Complex<T>;
35    unsafe fn store(&mut self, val: Complex<T>, idx: usize);
36}
37
38impl<T: FftNum> LoadStore<T> for &mut [Complex<T>] {
39    #[inline(always)]
40    unsafe fn load(&self, idx: usize) -> Complex<T> {
41        debug_assert!(idx < self.len());
42        *self.get_unchecked(idx)
43    }
44    #[inline(always)]
45    unsafe fn store(&mut self, val: Complex<T>, idx: usize) {
46        debug_assert!(idx < self.len());
47        *self.get_unchecked_mut(idx) = val;
48    }
49}
50impl<T: FftNum, const N: usize> LoadStore<T> for &mut [Complex<T>; N] {
51    #[inline(always)]
52    unsafe fn load(&self, idx: usize) -> Complex<T> {
53        debug_assert!(idx < self.len());
54        *self.get_unchecked(idx)
55    }
56    #[inline(always)]
57    unsafe fn store(&mut self, val: Complex<T>, idx: usize) {
58        debug_assert!(idx < self.len());
59        *self.get_unchecked_mut(idx) = val;
60    }
61}
62
63pub(crate) struct DoubleBuf<'a, T> {
64    pub input: &'a [Complex<T>],
65    pub output: &'a mut [Complex<T>],
66}
67impl<'a, T> Deref for DoubleBuf<'a, T> {
68    type Target = [Complex<T>];
69    fn deref(&self) -> &Self::Target {
70        self.input
71    }
72}
73impl<'a, T> DerefMut for DoubleBuf<'a, T> {
74    fn deref_mut(&mut self) -> &mut Self::Target {
75        self.output
76    }
77}
78impl<'a, T: FftNum> LoadStore<T> for DoubleBuf<'a, T> {
79    #[inline(always)]
80    unsafe fn load(&self, idx: usize) -> Complex<T> {
81        debug_assert!(idx < self.input.len());
82        *self.input.get_unchecked(idx)
83    }
84    #[inline(always)]
85    unsafe fn store(&mut self, val: Complex<T>, idx: usize) {
86        debug_assert!(idx < self.output.len());
87        *self.output.get_unchecked_mut(idx) = val;
88    }
89}
90
91pub(crate) trait Load<T: FftNum>: Deref {
92    unsafe fn load(&self, idx: usize) -> Complex<T>;
93}
94
95impl<T: FftNum> Load<T> for &[Complex<T>] {
96    #[inline(always)]
97    unsafe fn load(&self, idx: usize) -> Complex<T> {
98        debug_assert!(idx < self.len());
99        *self.get_unchecked(idx)
100    }
101}
102impl<T: FftNum, const N: usize> Load<T> for &[Complex<T>; N] {
103    #[inline(always)]
104    unsafe fn load(&self, idx: usize) -> Complex<T> {
105        debug_assert!(idx < self.len());
106        *self.get_unchecked(idx)
107    }
108}
109
110#[cfg(test)]
111mod unit_tests {
112    use super::*;
113    use crate::test_utils::random_signal;
114    use num_complex::Complex;
115    use num_traits::Zero;
116
117    #[test]
118    fn test_transpose() {
119        let sizes: Vec<usize> = (1..16).collect();
120
121        for &width in &sizes {
122            for &height in &sizes {
123                let len = width * height;
124
125                let input: Vec<Complex<f32>> = random_signal(len);
126                let mut output = vec![Zero::zero(); len];
127
128                unsafe { transpose_small(width, height, &input, &mut output) };
129
130                for x in 0..width {
131                    for y in 0..height {
132                        assert_eq!(
133                            input[x + y * width],
134                            output[y + x * height],
135                            "x = {}, y = {}",
136                            x,
137                            y
138                        );
139                    }
140                }
141            }
142        }
143    }
144}
145
146// A utility that validates the following conditions, then calls chunk_fn() on each chunk of buffer. Passes the entire scratch buffer with each call.
147// - buffer1.len() % chunk_size == 0
148// - scratch.len() >= required_scratch
149// Returns Ok(()) if the validation passed, Err(()) if there was a problem
150// Since this is duplicated into every FFT algorithm we provide, this is tuned to reduce code size as much as possible, with a secondary focus being on ease of implementation
151pub fn validate_and_iter<T>(
152    mut buffer: &mut [T],
153    scratch: &mut [T],
154    chunk_size: usize,
155    required_scratch: usize,
156    mut chunk_fn: impl FnMut(&mut [T], &mut [T]),
157) -> Result<(), ()> {
158    if scratch.len() < required_scratch {
159        return Err(());
160    }
161    let scratch = &mut scratch[..required_scratch];
162
163    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
164    while buffer.len() >= chunk_size {
165        let (head, tail) = buffer.split_at_mut(chunk_size);
166        buffer = tail;
167
168        chunk_fn(head, scratch);
169    }
170
171    // We have a remainder if there's still data in the buffers -- in which case we want to indicate to the caller that there was an unwanted remainder
172    if buffer.len() == 0 {
173        Ok(())
174    } else {
175        Err(())
176    }
177}
178
179// A utility that validates that buffer1.len() % chunk_size == 0, then calls chunk_fn() on each chunk of buffer.
180// This version does 2x partial unrolling of the buffer, because most SIMD butterfly algorithms operate that way.
181// Returns Ok(()) if the validation passed, Err(()) if there was a problem
182pub fn validate_and_iter_unroll2x<T>(
183    mut buffer: &mut [T],
184    chunk_size: usize,
185    mut chunk2x_fn: impl FnMut(&mut [T]),
186    mut chunk_fn: impl FnMut(&mut [T]),
187) -> Result<(), ()> {
188    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
189    while buffer.len() >= chunk_size * 2 {
190        let (head, tail) = buffer.split_at_mut(chunk_size * 2);
191        buffer = tail;
192
193        chunk2x_fn(head);
194    }
195
196    if buffer.len() == chunk_size {
197        chunk_fn(buffer);
198        Ok(())
199    } else if buffer.len() == 0 {
200        Ok(())
201    } else {
202        Err(())
203    }
204}
205
206// A utility that validates the following conditions, then calls chunk_fn() on each chunk of buffer1 and buffer 2 zipped together. Passes the entire scratch buffer with each call.
207// - buffer1.len() == buffer2.len()
208// - buffer1.len() % chunk_size == 0
209// - scratch.len() >= required_scratch
210// Returns Ok(()) if the validation passed, Err(()) if there was a problem
211// Since this is duplicated into every FFT algorithm we provide, this is tuned to reduce code size as much as possible, with a secondary focus being on ease of implementation
212pub fn validate_and_zip<T>(
213    mut buffer1: &[T],
214    mut buffer2: &mut [T],
215    scratch: &mut [T],
216    chunk_size: usize,
217    required_scratch: usize,
218    mut chunk_fn: impl FnMut(&[T], &mut [T], &mut [T]),
219) -> Result<(), ()> {
220    if scratch.len() < required_scratch {
221        return Err(());
222    }
223    let scratch = &mut scratch[..required_scratch];
224
225    if buffer1.len() != buffer2.len() {
226        return Err(());
227    }
228
229    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
230    while buffer1.len() >= chunk_size {
231        let (head1, tail1) = buffer1.split_at(chunk_size);
232        buffer1 = tail1;
233
234        let (head2, tail2) = buffer2.split_at_mut(chunk_size);
235        buffer2 = tail2;
236
237        chunk_fn(head1, head2, scratch);
238    }
239
240    // We have a remainder if the 2 chunks were uneven to start with, or if there's still data in the buffers -- in which case we want to indicate to the caller that there was an unwanted remainder
241    if buffer1.len() == 0 {
242        Ok(())
243    } else {
244        Err(())
245    }
246}
247
248// A utility that validates the following conditions, then calls chunk_fn() on each chunk of buffer1 and buffer 2 zipped together. Passes the entire scratch buffer with each call.
249// - buffer1.len() == buffer2.len()
250// - buffer1.len() % chunk_size == 0
251// Returns Ok(()) if the validation passed, Err(()) if there was a problem
252// This version does 2x partial unrolling of the buffer, because most SIMD butterfly algorithms operate that way.
253// Since this is duplicated into every FFT algorithm we provide, this is tuned to reduce code size as much as possible, with a secondary focus being on ease of implementation
254pub fn validate_and_zip_unroll2x<T>(
255    mut buffer1: &[T],
256    mut buffer2: &mut [T],
257    chunk_size: usize,
258    mut chunk2x_fn: impl FnMut(&[T], &mut [T]),
259    mut chunk_fn: impl FnMut(&[T], &mut [T]),
260) -> Result<(), ()> {
261    if buffer1.len() != buffer2.len() {
262        return Err(());
263    }
264
265    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
266    while buffer1.len() >= chunk_size * 2 {
267        let (head1, tail1) = buffer1.split_at(chunk_size * 2);
268        buffer1 = tail1;
269
270        let (head2, tail2) = buffer2.split_at_mut(chunk_size * 2);
271        buffer2 = tail2;
272
273        chunk2x_fn(head1, head2);
274    }
275
276    // We have a remainder if the 2 chunks were uneven to start with, or if there's still data in the buffers -- in which case we want to indicate to the caller that there was an unwanted remainder
277    if buffer1.len() == chunk_size {
278        chunk_fn(buffer1, buffer2);
279        Ok(())
280    } else if buffer1.len() == 0 {
281        Ok(())
282    } else {
283        Err(())
284    }
285}
286
287// A utility that validates the following conditions, then calls chunk_fn() on each chunk of buffer1 and buffer 2 zipped together. Passes the entire scratch buffer with each call.
288// - buffer1.len() == buffer2.len()
289// - buffer1.len() % chunk_size == 0
290// - scratch.len() >= required_scratch
291// Returns Ok(()) if the validation passed, Err(()) if there was a problem
292// Since this is duplicated into every FFT algorithm we provide, this is tuned to reduce code size as much as possible, with a secondary focus being on ease of implementation
293pub fn validate_and_zip_mut<T>(
294    mut buffer1: &mut [T],
295    mut buffer2: &mut [T],
296    scratch: &mut [T],
297    chunk_size: usize,
298    required_scratch: usize,
299    mut chunk_fn: impl FnMut(&mut [T], &mut [T], &mut [T]),
300) -> Result<(), ()> {
301    if scratch.len() < required_scratch {
302        return Err(());
303    }
304    let scratch = &mut scratch[..required_scratch];
305
306    if buffer1.len() != buffer2.len() {
307        return Err(());
308    }
309
310    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
311    while buffer1.len() >= chunk_size {
312        let (head1, tail1) = buffer1.split_at_mut(chunk_size);
313        buffer1 = tail1;
314
315        let (head2, tail2) = buffer2.split_at_mut(chunk_size);
316        buffer2 = tail2;
317
318        chunk_fn(head1, head2, scratch);
319    }
320
321    // We have a remainder if the 2 chunks were uneven to start with, or if there's still data in the buffers -- in which case we want to indicate to the caller that there was an unwanted remainder
322    if buffer1.len() == 0 {
323        Ok(())
324    } else {
325        Err(())
326    }
327}
328
329// A utility that validates the following conditions, then calls chunk_fn() on each chunk of buffer1 and buffer 2 zipped together. Passes the entire scratch buffer with each call.
330// - buffer1.len() == buffer2.len()
331// - buffer1.len() % chunk_size == 0
332// Returns Ok(()) if the validation passed, Err(()) if there was a problem
333// This version does 2x partial unrolling of the buffer, because most SIMD butterfly algorithms operate that way.
334// Since this is duplicated into every FFT algorithm we provide, this is tuned to reduce code size as much as possible, with a secondary focus being on ease of implementation
335pub fn validate_and_zip_mut_unroll2x<T>(
336    mut buffer1: &mut [T],
337    mut buffer2: &mut [T],
338    chunk_size: usize,
339    mut chunk2x_fn: impl FnMut(&mut [T], &mut [T]),
340    mut chunk_fn: impl FnMut(&mut [T], &mut [T]),
341) -> Result<(), ()> {
342    if buffer1.len() != buffer2.len() {
343        return Err(());
344    }
345
346    // Now that we know the two slices are the same length, loop over each one, splicing off chunk_size at a time, and calling chunk_fn on each
347    while buffer1.len() >= chunk_size * 2 {
348        let (head1, tail1) = buffer1.split_at_mut(chunk_size * 2);
349        buffer1 = tail1;
350
351        let (head2, tail2) = buffer2.split_at_mut(chunk_size * 2);
352        buffer2 = tail2;
353
354        chunk2x_fn(head1, head2);
355    }
356
357    // We have a remainder if the 2 chunks were uneven to start with, or if there's still data in the buffers -- in which case we want to indicate to the caller that there was an unwanted remainder
358    if buffer1.len() == chunk_size {
359        chunk_fn(buffer1, buffer2);
360        Ok(())
361    } else if buffer1.len() == 0 {
362        Ok(())
363    } else {
364        Err(())
365    }
366}
367
368// Utility to help reorder data as a part of computing RadixD FFTs. Conceputally, it works like a transpose, but with the column indexes bit-reversed.
369// Use a lookup table to avoid repeating the slow bit reverse operations.
370// Unrolling the outer loop by a factor D helps speed things up.
371// const parameter D (for Divisor) determines the divisor to use for the "bit reverse", and how much to unroll. `input.len() / height` must be a power of D.
372pub fn bitreversed_transpose<T: Copy, const D: usize>(
373    height: usize,
374    input: &[T],
375    output: &mut [T],
376) {
377    let width = input.len() / height;
378
379    // Let's make sure the arguments are ok
380    assert!(D > 1 && input.len() % height == 0 && input.len() == output.len());
381
382    let strided_width = width / D;
383    let rev_digits = if D.is_power_of_two() {
384        let width_bits = width.trailing_zeros();
385        let d_bits = D.trailing_zeros();
386
387        // verify that width is a power of d
388        assert!(width_bits % d_bits == 0);
389        width_bits / d_bits
390    } else {
391        compute_logarithm::<D>(width).unwrap()
392    };
393
394    for x in 0..strided_width {
395        let mut i = 0;
396        let x_fwd = [(); D].map(|_| {
397            let value = D * x + i;
398            i += 1;
399            value
400        }); // If we had access to rustc 1.63, we could use std::array::from_fn instead
401        let x_rev = x_fwd.map(|x| reverse_bits::<D>(x, rev_digits));
402
403        // Assert that the the bit reversed indices will not exceed the length of the output.
404        // The highest index the loop reaches is: (x_rev[n] + 1)*height - 1
405        // The last element of the data is at index: width*height - 1
406        // Thus it is sufficient to assert that x_rev[n]<width.
407        for r in x_rev {
408            assert!(r < width);
409        }
410        for y in 0..height {
411            for (fwd, rev) in x_fwd.iter().zip(x_rev.iter()) {
412                let input_index = *fwd + y * width;
413                let output_index = y + *rev * height;
414
415                unsafe {
416                    let temp = *input.get_unchecked(input_index);
417                    *output.get_unchecked_mut(output_index) = temp;
418                }
419            }
420        }
421    }
422}
423
424// Repeatedly divide `value` by divisor `D`, `iters` times, and apply the remainders to a new value
425// When D is a power of 2, this is exactly equal (implementation and assembly)-wise to a bit reversal
426// When D is not a power of 2, think of this function as a logical equivalent to a bit reversal
427pub fn reverse_bits<const D: usize>(value: usize, rev_digits: u32) -> usize {
428    assert!(D > 1);
429
430    let mut result: usize = 0;
431    let mut value = value;
432    for _ in 0..rev_digits {
433        result = (result * D) + (value % D);
434        value = value / D;
435    }
436    result
437}
438
439// computes `n` such that `D ^ n == value`. Returns `None` if `value` is not a perfect power of `D`, otherwise returns `Some(n)`
440pub fn compute_logarithm<const D: usize>(value: usize) -> Option<u32> {
441    if value == 0 || D < 2 {
442        return None;
443    }
444
445    let mut current_exponent = 0;
446    let mut current_value = value;
447
448    while current_value % D == 0 {
449        current_exponent += 1;
450        current_value /= D;
451    }
452
453    if current_value == 1 {
454        Some(current_exponent)
455    } else {
456        None
457    }
458}
459
460pub(crate) struct TransposeFactor {
461    pub factor: RadixFactor,
462    pub count: u8,
463}
464
465// Utility to help reorder data as a part of computing RadixD FFTs. Conceputally, it works like a transpose, but with the column indexes bit-reversed.
466// Use a lookup table to avoid repeating the slow bit reverse operations.
467// Unrolling the outer loop by a factor D helps speed things up.
468// const parameter D (for Divisor) determines how much to unroll. `input.len() / height` must divisible by D.
469pub(crate) fn factor_transpose<T: Copy, const D: usize>(
470    height: usize,
471    input: &[T],
472    output: &mut [T],
473    factors: &[TransposeFactor],
474) {
475    let width = input.len() / height;
476
477    // Let's make sure the arguments are ok
478    assert!(width % D == 0 && D > 1 && input.len() % width == 0 && input.len() == output.len());
479
480    let strided_width = width / D;
481    for x in 0..strided_width {
482        let mut i = 0;
483        let x_fwd = [(); D].map(|_| {
484            let value = D * x + i;
485            i += 1;
486            value
487        }); // If we had access to rustc 1.63, we could use std::array::from_fn instead
488        let x_rev = x_fwd.map(|x| reverse_remainders(x, factors));
489
490        // Assert that the the bit reversed indices will not exceed the length of the output.
491        // The highest index the loop reaches is: (x_rev[n] + 1)*height - 1
492        // The last element of the data is at index: width*height - 1
493        // Thus it is sufficient to assert that x_rev[n]<width.
494        for r in x_rev {
495            assert!(r < width);
496        }
497        for y in 0..height {
498            for (fwd, rev) in x_fwd.iter().zip(x_rev.iter()) {
499                let input_index = *fwd + y * width;
500                let output_index = y + *rev * height;
501
502                unsafe {
503                    let temp = *input.get_unchecked(input_index);
504                    *output.get_unchecked_mut(output_index) = temp;
505                }
506            }
507        }
508    }
509}
510
511// Divide `value` by the provided array of factors, and push the remainders into a new number
512// When all of the provided factors are 2, this is exactly equal to a bit reversal
513// When some of the factors are not 2, think of this as a "generalization" of a bit reversal, to something like a "Remainder reversal".
514pub(crate) fn reverse_remainders(value: usize, factors: &[TransposeFactor]) -> usize {
515    let mut result: usize = 0;
516    let mut value = value;
517    for f in factors.iter() {
518        match f.factor {
519            RadixFactor::Factor2 => {
520                for _ in 0..f.count {
521                    result = (result * 2) + (value % 2);
522                    value = value / 2;
523                }
524            }
525            RadixFactor::Factor3 => {
526                for _ in 0..f.count {
527                    result = (result * 3) + (value % 3);
528                    value = value / 3;
529                }
530            }
531            RadixFactor::Factor4 => {
532                for _ in 0..f.count {
533                    result = (result * 4) + (value % 4);
534                    value = value / 4;
535                }
536            }
537            RadixFactor::Factor5 => {
538                for _ in 0..f.count {
539                    result = (result * 5) + (value % 5);
540                    value = value / 5;
541                }
542            }
543            RadixFactor::Factor6 => {
544                for _ in 0..f.count {
545                    result = (result * 6) + (value % 6);
546                    value = value / 6;
547                }
548            }
549            RadixFactor::Factor7 => {
550                for _ in 0..f.count {
551                    result = (result * 7) + (value % 7);
552                    value = value / 7;
553                }
554            }
555        }
556    }
557    result
558}