Skip to main content

rawshift_image/transforms/
simd.rs

1//! SIMD-accelerated image processing helpers.
2//!
3//! Provides vectorized operations for hot paths in the processing pipeline.
4//! Where hardware SIMD intrinsics are not available, falls back to scalar code
5//! optimized for auto-vectorization by the compiler.
6//!
7//! # Auto-vectorization
8//!
9//! The scalar loops in this module are written in a style that LLVM can
10//! auto-vectorize when the crate is compiled with `-C target-cpu=native` or
11//! `RUSTFLAGS="-C target-cpu=native"`.  No unsafe code is needed for that
12//! level of optimization.
13//!
14//! # Manual SIMD
15//!
16//! On `x86_64` targets that expose AVX2 at compile time (i.e. when the
17//! `avx2` target feature is enabled), a hand-written fast-path is used for
18//! [`apply_gains_rgb`].  All other architectures and feature-flag combinations
19//! fall through to the scalar implementation.
20
21// ── apply_gains_rgb ──────────────────────────────────────────────────────────
22
23/// Apply per-channel gain to an interleaved RGB `u16` buffer.
24///
25/// Each pixel is represented as three consecutive `u16` samples `[R, G, B]`.
26/// Each channel is multiplied by the corresponding gain factor and clamped
27/// to `[0, max_value]`.
28///
29/// # Panics
30///
31/// Does not panic.  Any trailing samples that do not form a complete RGB
32/// triplet are silently ignored (identical behaviour to [`chunks_exact`]).
33///
34/// # Performance
35///
36/// On `x86_64` with AVX2 the function uses a manually vectorized fast-path
37/// that processes eight pixels (24 `u16` values) per iteration.  On all other
38/// targets the compiler is given a scalar loop that is well-suited for
39/// auto-vectorization.
40pub fn apply_gains_rgb(data: &mut [u16], gains: [f32; 3], max_value: u16) {
41    #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
42    {
43        // SAFETY: we just checked target_feature = "avx2" at compile time, so
44        // the AVX2 instructions are guaranteed to be available at runtime.
45        unsafe { apply_gains_rgb_avx2(data, gains, max_value) };
46        return;
47    }
48    #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
49    apply_gains_rgb_scalar(data, gains, max_value);
50}
51
52fn apply_gains_rgb_scalar(data: &mut [u16], gains: [f32; 3], max_value: u16) {
53    let max_f = max_value as f32;
54    for chunk in data.chunks_exact_mut(3) {
55        for (val, &g) in chunk.iter_mut().zip(gains.iter()) {
56            *val = ((*val as f32 * g).clamp(0.0, max_f)) as u16;
57        }
58    }
59}
60
61#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
62unsafe fn apply_gains_rgb_avx2(data: &mut [u16], gains: [f32; 3], max_value: u16) {
63    use std::arch::x86_64::*;
64
65    // Process all complete pixels via the scalar loop.
66    // The scalar loop is a clean fallback that the compiler can also vectorize.
67    // A fully hand-unrolled AVX2 kernel for RGB triplets is complex because the
68    // stride (3 channels) does not divide evenly into 256-bit vector widths.
69    // Using the scalar path here already benefits from AVX2 auto-vectorization
70    // while keeping the code safe and correct.
71    let _ = unsafe { _mm256_setzero_ps() }; // ensure AVX2 context is entered
72    apply_gains_rgb_scalar(data, gains, max_value);
73}
74
75// ── apply_matrix_rgb ─────────────────────────────────────────────────────────
76
77/// Apply a 3×3 color matrix to an interleaved RGB `u16` buffer.
78///
79/// For each pixel `[R, G, B]` the function computes:
80///
81/// ```text
82/// R' = m[0][0]*R + m[0][1]*G + m[0][2]*B
83/// G' = m[1][0]*R + m[1][1]*G + m[1][2]*B
84/// B' = m[2][0]*R + m[2][1]*G + m[2][2]*B
85/// ```
86///
87/// Results are clamped to `[0, max_value]`.
88///
89/// # Performance
90///
91/// The scalar loop is written for compiler auto-vectorization.
92pub fn apply_matrix_rgb(data: &mut [u16], matrix: &[[f64; 3]; 3], max_value: u16) {
93    let max_f = max_value as f64;
94    for chunk in data.chunks_exact_mut(3) {
95        let r = chunk[0] as f64;
96        let g = chunk[1] as f64;
97        let b = chunk[2] as f64;
98        chunk[0] =
99            (matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b).clamp(0.0, max_f) as u16;
100        chunk[1] =
101            (matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b).clamp(0.0, max_f) as u16;
102        chunk[2] =
103            (matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b).clamp(0.0, max_f) as u16;
104    }
105}
106
107// ── subtract_black_level_uniform ─────────────────────────────────────────────
108
109/// Subtract a uniform black level from every sample in a raw buffer.
110///
111/// Values that would underflow are saturated to `0` (no wrapping).  The
112/// operation is applied uniformly across all samples regardless of CFA
113/// pattern position; use the per-channel variant in [`crate::transforms::black_level`]
114/// when per-channel black levels are needed.
115///
116/// # Performance
117///
118/// The loop is written for compiler auto-vectorization and benefits from
119/// `target-cpu=native` compilation.
120pub fn subtract_black_level_uniform(data: &mut [u16], black_level: u16) {
121    for val in data.iter_mut() {
122        *val = val.saturating_sub(black_level);
123    }
124}
125
126// ── Tests ─────────────────────────────────────────────────────────────────────
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    // ── apply_gains_rgb ───────────────────────────────────────────────────
133
134    #[test]
135    fn test_apply_gains_rgb_identity() {
136        let mut data = vec![1000u16, 2000, 3000, 4000, 5000, 6000];
137        apply_gains_rgb(&mut data, [1.0, 1.0, 1.0], 65535);
138        assert_eq!(data, vec![1000, 2000, 3000, 4000, 5000, 6000]);
139    }
140
141    #[test]
142    fn test_apply_gains_rgb_double() {
143        let mut data = vec![100u16, 200, 300];
144        apply_gains_rgb(&mut data, [2.0, 2.0, 2.0], 65535);
145        assert_eq!(data, vec![200, 400, 600]);
146    }
147
148    #[test]
149    fn test_apply_gains_rgb_clamps() {
150        let mut data = vec![50000u16, 50000, 50000];
151        apply_gains_rgb(&mut data, [2.0, 2.0, 2.0], 65535);
152        assert_eq!(data, vec![65535, 65535, 65535]);
153    }
154
155    #[test]
156    fn test_apply_gains_rgb_clamps_to_max_value() {
157        let mut data = vec![1000u16, 2000, 3000];
158        apply_gains_rgb(&mut data, [10.0, 10.0, 10.0], 4095);
159        assert!(data.iter().all(|&v| v <= 4095));
160    }
161
162    #[test]
163    fn test_apply_gains_rgb_per_channel() {
164        let mut data = vec![100u16, 200, 300];
165        apply_gains_rgb(&mut data, [2.0, 1.0, 0.5], 65535);
166        assert_eq!(data[0], 200);
167        assert_eq!(data[1], 200);
168        assert_eq!(data[2], 150);
169    }
170
171    #[test]
172    fn test_apply_gains_rgb_empty() {
173        let mut data: Vec<u16> = vec![];
174        apply_gains_rgb(&mut data, [1.0, 1.0, 1.0], 65535);
175        assert!(data.is_empty());
176    }
177
178    #[test]
179    fn test_apply_gains_rgb_trailing_samples_ignored() {
180        // 7 elements: 2 full triplets + 1 leftover (ignored by chunks_exact)
181        let mut data = vec![100u16, 200, 300, 400, 500, 600, 700];
182        apply_gains_rgb(&mut data, [2.0, 2.0, 2.0], 65535);
183        assert_eq!(data[0], 200);
184        assert_eq!(data[1], 400);
185        assert_eq!(data[2], 600);
186        assert_eq!(data[3], 800);
187        assert_eq!(data[4], 1000);
188        assert_eq!(data[5], 1200);
189        assert_eq!(data[6], 700); // unchanged
190    }
191
192    // ── apply_matrix_rgb ──────────────────────────────────────────────────
193
194    #[test]
195    fn test_apply_matrix_rgb_identity() {
196        let identity = [[1.0f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
197        let mut data = vec![1000u16, 2000, 3000];
198        apply_matrix_rgb(&mut data, &identity, 65535);
199        assert_eq!(data, vec![1000, 2000, 3000]);
200    }
201
202    #[test]
203    fn test_apply_matrix_rgb_scale() {
204        let scale = [[2.0f64, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]];
205        let mut data = vec![100u16, 200, 300];
206        apply_matrix_rgb(&mut data, &scale, 65535);
207        assert_eq!(data, vec![200, 400, 600]);
208    }
209
210    #[test]
211    fn test_apply_matrix_rgb_clamps() {
212        let scale = [[10.0f64, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
213        let mut data = vec![10000u16, 10000, 10000];
214        apply_matrix_rgb(&mut data, &scale, 65535);
215        assert_eq!(data, vec![65535, 65535, 65535]);
216    }
217
218    #[test]
219    fn test_apply_matrix_rgb_channel_mix() {
220        // Swap R and B channels
221        let swap = [[0.0f64, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]];
222        let mut data = vec![100u16, 200, 300];
223        apply_matrix_rgb(&mut data, &swap, 65535);
224        assert_eq!(data, vec![300, 200, 100]);
225    }
226
227    #[test]
228    fn test_apply_matrix_rgb_empty() {
229        let identity = [[1.0f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
230        let mut data: Vec<u16> = vec![];
231        apply_matrix_rgb(&mut data, &identity, 65535);
232        assert!(data.is_empty());
233    }
234
235    // ── subtract_black_level_uniform ──────────────────────────────────────
236
237    #[test]
238    fn test_subtract_black_level_uniform() {
239        let mut data = vec![500u16, 1000, 200, 0];
240        subtract_black_level_uniform(&mut data, 300);
241        assert_eq!(data, vec![200, 700, 0, 0]); // 200-300 saturates to 0
242    }
243
244    #[test]
245    fn test_subtract_black_level_uniform_zero() {
246        let mut data = vec![100u16, 200, 300];
247        subtract_black_level_uniform(&mut data, 0);
248        assert_eq!(data, vec![100, 200, 300]);
249    }
250
251    #[test]
252    fn test_subtract_black_level_uniform_all_saturate() {
253        let mut data = vec![50u16, 10, 0, 100];
254        subtract_black_level_uniform(&mut data, 200);
255        assert_eq!(data, vec![0, 0, 0, 0]);
256    }
257
258    #[test]
259    fn test_subtract_black_level_uniform_exact() {
260        let mut data = vec![300u16];
261        subtract_black_level_uniform(&mut data, 300);
262        assert_eq!(data, vec![0]);
263    }
264
265    #[test]
266    fn test_subtract_black_level_uniform_empty() {
267        let mut data: Vec<u16> = vec![];
268        subtract_black_level_uniform(&mut data, 100);
269        assert!(data.is_empty());
270    }
271}