Skip to main content

vector_core/simd/
image.rs

1//! SIMD-accelerated image operations.
2//!
3//! # Platform Support
4//!
5//! - **ARM64**: NEON (vsetq_lane_u32 gather + vst1q_u32 store)
6//! - **x86_64**: SSE2 (_mm_set_epi32 gather + _mm_storeu_si128 store)
7//! - **Other**: Scalar u32-at-a-time pixel copy
8//!
9//! # Algorithms
10//!
11//! **Nearest-neighbor downsample**: Processes 4 output pixels per SIMD iteration.
12//! Pre-computes x-coordinate mapping table (shared across rows) to avoid
13//! per-pixel integer division. Used for thumbhash generation (100x100 max).
14
15#[cfg(target_arch = "aarch64")]
16use std::arch::aarch64::*;
17
18#[cfg(target_arch = "x86_64")]
19use std::arch::x86_64::*;
20
21// ============================================================================
22// Nearest-Neighbor RGBA Downsample
23// ============================================================================
24
25/// Nearest-neighbor RGBA downsample with SIMD acceleration.
26///
27/// Validates input dimensions. Panics on overflow or undersized buffer.
28pub fn nearest_neighbor_downsample_rgba(
29    pixels: &[u8],
30    src_width: u32,
31    src_height: u32,
32    dst_width: u32,
33    dst_height: u32,
34) -> Vec<u8> {
35    let src_size = (src_width as usize)
36        .checked_mul(src_height as usize)
37        .and_then(|n| n.checked_mul(4))
38        .expect("source dimensions overflow");
39    assert!(pixels.len() >= src_size, "pixels buffer too small for source dimensions");
40
41    let dst_size = (dst_width as usize)
42        .checked_mul(dst_height as usize)
43        .and_then(|n| n.checked_mul(4))
44        .expect("destination dimensions overflow");
45
46    let mut result: Vec<u8> = vec![0u8; dst_size];
47    let src_stride = src_width as usize * 4;
48
49    // Pre-compute x mapping table (shared across all rows)
50    let x_map: Vec<usize> = (0..dst_width)
51        .map(|tx| (tx as u64 * src_width as u64 / dst_width as u64) as usize * 4)
52        .collect();
53
54    #[cfg(target_arch = "aarch64")]
55    downsample_neon(pixels, &mut result, src_stride, src_height, dst_width, dst_height, &x_map);
56
57    #[cfg(target_arch = "x86_64")]
58    downsample_sse2(pixels, &mut result, src_stride, src_height, dst_width, dst_height, &x_map);
59
60    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
61    downsample_scalar(pixels, &mut result, src_stride, src_height, dst_width, dst_height, &x_map);
62
63    result
64}
65
66// ============================================================================
67// NEON (ARM64)
68// ============================================================================
69
70/// Gather 4 source pixels into a NEON register and store.
71#[cfg(target_arch = "aarch64")]
72fn downsample_neon(
73    src: &[u8], dst: &mut [u8], src_stride: usize,
74    src_height: u32, dst_width: u32, dst_height: u32, x_map: &[usize],
75) {
76    unsafe {
77        let src_ptr = src.as_ptr();
78        let dst_ptr = dst.as_mut_ptr() as *mut u32;
79        let mut dst_idx = 0usize;
80        let chunks = dst_width as usize / 4;
81        let remainder = dst_width as usize % 4;
82
83        for ty in 0..dst_height {
84            let sy = (ty as u64 * src_height as u64 / dst_height as u64) as usize;
85            let row = src_ptr.add(sy * src_stride);
86
87            for cx in 0..chunks {
88                let base = cx * 4;
89                let p0 = std::ptr::read_unaligned(row.add(x_map[base]) as *const u32);
90                let p1 = std::ptr::read_unaligned(row.add(x_map[base + 1]) as *const u32);
91                let p2 = std::ptr::read_unaligned(row.add(x_map[base + 2]) as *const u32);
92                let p3 = std::ptr::read_unaligned(row.add(x_map[base + 3]) as *const u32);
93
94                let mut v = vdupq_n_u32(p0);
95                v = vsetq_lane_u32(p1, v, 1);
96                v = vsetq_lane_u32(p2, v, 2);
97                v = vsetq_lane_u32(p3, v, 3);
98                vst1q_u32(dst_ptr.add(dst_idx) as *mut u32, v);
99                dst_idx += 4;
100            }
101
102            for rx in 0..remainder {
103                std::ptr::write_unaligned(
104                    dst_ptr.add(dst_idx),
105                    std::ptr::read_unaligned(row.add(x_map[chunks * 4 + rx]) as *const u32),
106                );
107                dst_idx += 1;
108            }
109        }
110    }
111}
112
113// ============================================================================
114// SSE2 (x86_64)
115// ============================================================================
116
117/// Gather 4 source pixels into an SSE2 register and store.
118#[cfg(target_arch = "x86_64")]
119fn downsample_sse2(
120    src: &[u8], dst: &mut [u8], src_stride: usize,
121    src_height: u32, dst_width: u32, dst_height: u32, x_map: &[usize],
122) {
123    unsafe {
124        let src_ptr = src.as_ptr();
125        let dst_ptr = dst.as_mut_ptr();
126        let mut dst_idx = 0usize;
127        let chunks = dst_width as usize / 4;
128        let remainder = dst_width as usize % 4;
129
130        for ty in 0..dst_height {
131            let sy = (ty as u64 * src_height as u64 / dst_height as u64) as usize;
132            let row = src_ptr.add(sy * src_stride);
133
134            for cx in 0..chunks {
135                let base = cx * 4;
136                let p0 = std::ptr::read_unaligned(row.add(x_map[base]) as *const i32);
137                let p1 = std::ptr::read_unaligned(row.add(x_map[base + 1]) as *const i32);
138                let p2 = std::ptr::read_unaligned(row.add(x_map[base + 2]) as *const i32);
139                let p3 = std::ptr::read_unaligned(row.add(x_map[base + 3]) as *const i32);
140
141                let v = _mm_set_epi32(p3, p2, p1, p0);
142                _mm_storeu_si128(dst_ptr.add(dst_idx * 4) as *mut __m128i, v);
143                dst_idx += 4;
144            }
145
146            for rx in 0..remainder {
147                let sx = x_map[chunks * 4 + rx];
148                std::ptr::write_unaligned(
149                    dst_ptr.add(dst_idx * 4) as *mut u32,
150                    std::ptr::read_unaligned(row.add(sx) as *const u32),
151                );
152                dst_idx += 1;
153            }
154        }
155    }
156}
157
158// ============================================================================
159// Scalar Fallback
160// ============================================================================
161
162/// u32-at-a-time pixel copy — for platforms without SIMD (WASM, etc.).
163#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
164fn downsample_scalar(
165    src: &[u8], dst: &mut [u8], src_stride: usize,
166    src_height: u32, dst_width: u32, dst_height: u32, x_map: &[usize],
167) {
168    unsafe {
169        let src_ptr = src.as_ptr();
170        let dst_ptr = dst.as_mut_ptr() as *mut u32;
171        let mut dst_idx = 0usize;
172
173        for ty in 0..dst_height {
174            let sy = (ty as u64 * src_height as u64 / dst_height as u64) as usize;
175            let row = src_ptr.add(sy * src_stride);
176
177            for tx in 0..dst_width as usize {
178                std::ptr::write_unaligned(
179                    dst_ptr.add(dst_idx),
180                    std::ptr::read_unaligned(row.add(x_map[tx]) as *const u32),
181                );
182                dst_idx += 1;
183            }
184        }
185    }
186}
187
188// ============================================================================
189// Tests
190// ============================================================================
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn downsample_basic_red() {
198        // 4x4 red → 2x2 — every pixel stays red
199        let pixels: Vec<u8> = vec![255, 0, 0, 255].repeat(16);
200        let result = nearest_neighbor_downsample_rgba(&pixels, 4, 4, 2, 2);
201        assert_eq!(result.len(), 2 * 2 * 4);
202        for chunk in result.chunks(4) {
203            assert_eq!(chunk, &[255, 0, 0, 255]);
204        }
205    }
206
207    #[test]
208    fn downsample_identity() {
209        // Same size → same output
210        let pixels: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
211        let result = nearest_neighbor_downsample_rgba(&pixels, 2, 2, 2, 2);
212        assert_eq!(result, pixels);
213    }
214
215    #[test]
216    fn downsample_checkerboard() {
217        // 4x4 checkerboard → 2x2
218        let red = [255u8, 0, 0, 255];
219        let blue = [0u8, 0, 255, 255];
220        let mut pixels = Vec::with_capacity(4 * 4 * 4);
221        for y in 0..4 {
222            for x in 0..4 {
223                if (x + y) % 2 == 0 { pixels.extend_from_slice(&red); }
224                else { pixels.extend_from_slice(&blue); }
225            }
226        }
227        let result = nearest_neighbor_downsample_rgba(&pixels, 4, 4, 2, 2);
228        assert_eq!(result.len(), 2 * 2 * 4);
229        // (0,0) → src(0,0) = red
230        assert_eq!(&result[0..4], &red);
231        // (1,0) → src(2,0) = red
232        assert_eq!(&result[4..8], &red);
233    }
234
235    #[test]
236    fn downsample_large_to_small() {
237        // 100x100 → 10x10 — verify dimensions and no panic
238        let pixels: Vec<u8> = vec![128, 64, 32, 255].repeat(100 * 100);
239        let result = nearest_neighbor_downsample_rgba(&pixels, 100, 100, 10, 10);
240        assert_eq!(result.len(), 10 * 10 * 4);
241    }
242
243    #[test]
244    fn downsample_non_power_of_two() {
245        // 7x5 → 3x2 — odd dimensions, tests remainder handling
246        let pixels: Vec<u8> = (0..7 * 5 * 4).map(|i| (i % 256) as u8).collect();
247        let result = nearest_neighbor_downsample_rgba(&pixels, 7, 5, 3, 2);
248        assert_eq!(result.len(), 3 * 2 * 4);
249    }
250
251    #[test]
252    #[should_panic(expected = "pixels buffer too small")]
253    fn downsample_undersized_buffer() {
254        let pixels = vec![0u8; 10]; // too small for 4x4
255        nearest_neighbor_downsample_rgba(&pixels, 4, 4, 2, 2);
256    }
257}