Skip to main content

oximedia_gpu/
video_process.rs

1//! GPU-accelerated video frame processing.
2//!
3//! This module provides CPU-fallback simulations of GPU compute operations
4//! for video frame processing. In production, these would be replaced by
5//! actual WGPU compute shader kernels.
6
7use crate::{GpuError, Result};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// Configuration for GPU-based frame operations.
11#[derive(Debug, Clone)]
12pub struct FrameProcessConfig {
13    /// Frame width in pixels.
14    pub width: u32,
15    /// Frame height in pixels.
16    pub height: u32,
17    /// Number of channels: 1=Y (grayscale), 3=RGB, 4=RGBA.
18    pub channels: u8,
19}
20
21/// Result of a GPU frame processing operation.
22#[derive(Debug, Clone)]
23pub struct FrameProcessResult {
24    /// Processed pixel data.
25    pub data: Vec<u8>,
26    /// Frame width.
27    pub width: u32,
28    /// Frame height.
29    pub height: u32,
30    /// Processing time in microseconds.
31    pub processing_time_us: u64,
32}
33
34/// GPU-accelerated video frame processor.
35///
36/// Provides CPU-fallback implementations of common video frame operations
37/// that would execute on the GPU in production environments.
38pub struct VideoFrameProcessor {
39    config: FrameProcessConfig,
40}
41
42impl VideoFrameProcessor {
43    /// Create a new `VideoFrameProcessor` with the given configuration.
44    #[must_use]
45    pub fn new(config: FrameProcessConfig) -> Self {
46        Self { config }
47    }
48
49    /// Get the current timestamp in microseconds (for timing).
50    fn timestamp_us() -> u64 {
51        SystemTime::now()
52            .duration_since(UNIX_EPOCH)
53            .unwrap_or_default()
54            .subsec_micros()
55            .into()
56    }
57
58    /// Validate that a frame buffer has the expected size.
59    fn validate_frame(&self, frame: &[u8]) -> Result<()> {
60        let expected = self.config.width as usize
61            * self.config.height as usize
62            * self.config.channels as usize;
63        if frame.len() != expected {
64            return Err(GpuError::InvalidBufferSize {
65                expected,
66                actual: frame.len(),
67            });
68        }
69        Ok(())
70    }
71
72    /// Simulate GPU-accelerated frame histogram computation.
73    ///
74    /// For each channel, counts pixel value occurrences (0-255).
75    /// Returns a `Vec` of `256 * channels` counts (interleaved per channel).
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the frame buffer size does not match the configured dimensions.
80    pub fn compute_histogram(&self, frame: &[u8]) -> Result<Vec<u32>> {
81        self.validate_frame(frame)?;
82
83        let channels = self.config.channels as usize;
84        let mut histogram = vec![0u32; 256 * channels];
85
86        for (i, &pixel) in frame.iter().enumerate() {
87            let ch = i % channels;
88            histogram[ch * 256 + pixel as usize] += 1;
89        }
90
91        Ok(histogram)
92    }
93
94    /// Simulate GPU-accelerated frame brightness adjustment.
95    ///
96    /// Adds `delta` to each pixel value, clamping the result to `[0, 255]`.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the frame buffer size does not match the configured dimensions.
101    pub fn adjust_brightness(&self, frame: &[u8], delta: i16) -> Result<Vec<u8>> {
102        self.validate_frame(frame)?;
103
104        let result = frame
105            .iter()
106            .map(|&p| (i16::from(p) + delta).clamp(0, 255) as u8)
107            .collect();
108
109        Ok(result)
110    }
111
112    /// Simulate GPU-accelerated contrast adjustment.
113    ///
114    /// For each pixel: `clamp((pixel - 128) * factor + 128, 0, 255)`.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the frame buffer size does not match the configured dimensions.
119    pub fn adjust_contrast(&self, frame: &[u8], factor: f32) -> Result<Vec<u8>> {
120        self.validate_frame(frame)?;
121
122        let result = frame
123            .iter()
124            .map(|&p| {
125                let adjusted = (f32::from(p) - 128.0) * factor + 128.0;
126                adjusted.clamp(0.0, 255.0) as u8
127            })
128            .collect();
129
130        Ok(result)
131    }
132
133    /// Simulate GPU-accelerated saturation adjustment for RGB frames (3 channels).
134    ///
135    /// Converts each RGB pixel to HSL, multiplies the S component by `factor`,
136    /// then converts back to RGB. For non-RGB frames this is a no-op copy.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the frame buffer size does not match the configured dimensions.
141    pub fn adjust_saturation(&self, frame: &[u8], factor: f32) -> Result<Vec<u8>> {
142        self.validate_frame(frame)?;
143
144        if self.config.channels != 3 {
145            // For non-RGB frames, return as-is (saturation is RGB concept)
146            return Ok(frame.to_vec());
147        }
148
149        let mut result = Vec::with_capacity(frame.len());
150        for chunk in frame.chunks(3) {
151            let (r, g, b) = (
152                f32::from(chunk[0]) / 255.0,
153                f32::from(chunk[1]) / 255.0,
154                f32::from(chunk[2]) / 255.0,
155            );
156
157            let (h, s, l) = rgb_to_hsl(r, g, b);
158            let new_s = (s * factor).clamp(0.0, 1.0);
159            let (nr, ng, nb) = hsl_to_rgb(h, new_s, l);
160
161            result.push((nr * 255.0).clamp(0.0, 255.0) as u8);
162            result.push((ng * 255.0).clamp(0.0, 255.0) as u8);
163            result.push((nb * 255.0).clamp(0.0, 255.0) as u8);
164        }
165
166        Ok(result)
167    }
168
169    /// Compute frame difference (absolute difference per pixel).
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if either frame buffer size does not match the configured dimensions.
174    pub fn frame_difference(&self, frame_a: &[u8], frame_b: &[u8]) -> Result<Vec<u8>> {
175        self.validate_frame(frame_a)?;
176        self.validate_frame(frame_b)?;
177
178        let result = frame_a
179            .iter()
180            .zip(frame_b.iter())
181            .map(|(&a, &b)| a.abs_diff(b))
182            .collect();
183
184        Ok(result)
185    }
186
187    /// Compute mean absolute error between two frames.
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if either frame buffer size does not match the configured dimensions.
192    pub fn mean_absolute_error(&self, frame_a: &[u8], frame_b: &[u8]) -> Result<f64> {
193        self.validate_frame(frame_a)?;
194        self.validate_frame(frame_b)?;
195
196        if frame_a.is_empty() {
197            return Ok(0.0);
198        }
199
200        let sum: u64 = frame_a
201            .iter()
202            .zip(frame_b.iter())
203            .map(|(&a, &b)| u64::from(a.abs_diff(b)))
204            .sum();
205
206        Ok(sum as f64 / frame_a.len() as f64)
207    }
208
209    /// Get the configuration.
210    #[must_use]
211    pub fn config(&self) -> &FrameProcessConfig {
212        &self.config
213    }
214
215    /// Process a frame and return a `FrameProcessResult` with timing information.
216    ///
217    /// This is a convenience wrapper that applies brightness adjustment and
218    /// records the simulated GPU processing time.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the frame buffer size does not match the configured dimensions.
223    pub fn process_frame(&self, frame: &[u8], brightness_delta: i16) -> Result<FrameProcessResult> {
224        let start = Self::timestamp_us();
225        let data = self.adjust_brightness(frame, brightness_delta)?;
226        let end = Self::timestamp_us();
227
228        Ok(FrameProcessResult {
229            data,
230            width: self.config.width,
231            height: self.config.height,
232            processing_time_us: end.saturating_sub(start),
233        })
234    }
235}
236
237// ---------------------------------------------------------------------------
238// HSL / RGB conversion helpers
239// ---------------------------------------------------------------------------
240
241/// Convert RGB (0.0–1.0 each) to HSL.
242fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
243    let max = r.max(g).max(b);
244    let min = r.min(g).min(b);
245    let delta = max - min;
246    let l = (max + min) / 2.0;
247
248    if delta < f32::EPSILON {
249        return (0.0, 0.0, l);
250    }
251
252    let s = if l < 0.5 {
253        delta / (max + min)
254    } else {
255        delta / (2.0 - max - min)
256    };
257
258    let h = if (max - r).abs() < f32::EPSILON {
259        ((g - b) / delta).rem_euclid(6.0) / 6.0
260    } else if (max - g).abs() < f32::EPSILON {
261        ((b - r) / delta + 2.0) / 6.0
262    } else {
263        ((r - g) / delta + 4.0) / 6.0
264    };
265
266    (h, s, l)
267}
268
269/// Helper for HSL-to-RGB conversion.
270fn hsl_hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
271    if t < 0.0 {
272        t += 1.0;
273    }
274    if t > 1.0 {
275        t -= 1.0;
276    }
277    if t < 1.0 / 6.0 {
278        return p + (q - p) * 6.0 * t;
279    }
280    if t < 1.0 / 2.0 {
281        return q;
282    }
283    if t < 2.0 / 3.0 {
284        return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
285    }
286    p
287}
288
289/// Convert HSL to RGB (0.0–1.0 each).
290fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
291    if s < f32::EPSILON {
292        return (l, l, l);
293    }
294
295    let q = if l < 0.5 {
296        l * (1.0 + s)
297    } else {
298        l + s - l * s
299    };
300    let p = 2.0 * l - q;
301
302    let r = hsl_hue_to_rgb(p, q, h + 1.0 / 3.0);
303    let g = hsl_hue_to_rgb(p, q, h);
304    let b = hsl_hue_to_rgb(p, q, h - 1.0 / 3.0);
305
306    (r, g, b)
307}
308
309// ---------------------------------------------------------------------------
310// Tests
311// ---------------------------------------------------------------------------
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    fn make_processor(w: u32, h: u32, ch: u8) -> VideoFrameProcessor {
318        VideoFrameProcessor::new(FrameProcessConfig {
319            width: w,
320            height: h,
321            channels: ch,
322        })
323    }
324
325    #[test]
326    fn test_histogram_uniform_frame() {
327        // 4x4 single-channel frame, all pixels = 128
328        let proc = make_processor(4, 4, 1);
329        let frame = vec![128u8; 16];
330        let hist = proc.compute_histogram(&frame).unwrap();
331
332        assert_eq!(hist.len(), 256);
333        assert_eq!(hist[128], 16, "All 16 pixels should be at bin 128");
334        for i in 0..256 {
335            if i != 128 {
336                assert_eq!(hist[i], 0);
337            }
338        }
339    }
340
341    #[test]
342    fn test_histogram_rgb_frame() {
343        // 2x2 RGB frame: all red=255, green=0, blue=128
344        let proc = make_processor(2, 2, 3);
345        let frame: Vec<u8> = (0..4).flat_map(|_| vec![255u8, 0u8, 128u8]).collect();
346        let hist = proc.compute_histogram(&frame).unwrap();
347
348        assert_eq!(hist.len(), 768); // 3 * 256
349                                     // Channel 0 (red): all 4 pixels at 255
350        assert_eq!(hist[0 * 256 + 255], 4);
351        // Channel 1 (green): all 4 pixels at 0
352        assert_eq!(hist[1 * 256 + 0], 4);
353        // Channel 2 (blue): all 4 pixels at 128
354        assert_eq!(hist[2 * 256 + 128], 4);
355    }
356
357    #[test]
358    fn test_adjust_brightness_clamp_up() {
359        let proc = make_processor(2, 2, 1);
360        let frame = vec![200u8, 100u8, 50u8, 10u8];
361        let result = proc.adjust_brightness(&frame, 100).unwrap();
362        assert_eq!(result, vec![255, 200, 150, 110]);
363    }
364
365    #[test]
366    fn test_adjust_brightness_clamp_down() {
367        let proc = make_processor(2, 2, 1);
368        let frame = vec![200u8, 100u8, 50u8, 10u8];
369        let result = proc.adjust_brightness(&frame, -100).unwrap();
370        assert_eq!(result, vec![100, 0, 0, 0]);
371    }
372
373    #[test]
374    fn test_adjust_contrast() {
375        let proc = make_processor(1, 1, 1);
376        // pixel=128, factor=1.0 → should stay at 128
377        let frame = vec![128u8];
378        let result = proc.adjust_contrast(&frame, 1.0).unwrap();
379        assert_eq!(result[0], 128);
380    }
381
382    #[test]
383    fn test_adjust_contrast_increase() {
384        let proc = make_processor(1, 1, 1);
385        // pixel=200, factor=2.0 → (200-128)*2+128 = 272 → clamped to 255
386        let frame = vec![200u8];
387        let result = proc.adjust_contrast(&frame, 2.0).unwrap();
388        assert_eq!(result[0], 255);
389    }
390
391    #[test]
392    fn test_adjust_saturation_no_change_at_one() {
393        let proc = make_processor(1, 1, 3);
394        let frame = vec![255u8, 0u8, 0u8]; // pure red
395        let result = proc.adjust_saturation(&frame, 1.0).unwrap();
396        // With factor=1.0, saturation should be unchanged, red should stay red
397        assert_eq!(result[0], 255);
398        assert_eq!(result[1], 0);
399        assert_eq!(result[2], 0);
400    }
401
402    #[test]
403    fn test_adjust_saturation_zero_desaturates() {
404        let proc = make_processor(1, 1, 3);
405        let frame = vec![255u8, 0u8, 0u8]; // pure red
406        let result = proc.adjust_saturation(&frame, 0.0).unwrap();
407        // With factor=0.0, becomes grayscale: all channels equal
408        assert_eq!(result[0], result[1]);
409        assert_eq!(result[1], result[2]);
410    }
411
412    #[test]
413    fn test_frame_difference() {
414        let proc = make_processor(2, 2, 1);
415        let a = vec![100u8, 200u8, 50u8, 0u8];
416        let b = vec![80u8, 210u8, 50u8, 255u8];
417        let diff = proc.frame_difference(&a, &b).unwrap();
418        assert_eq!(diff, vec![20, 10, 0, 255]);
419    }
420
421    #[test]
422    fn test_mean_absolute_error() {
423        let proc = make_processor(2, 2, 1);
424        let a = vec![100u8, 100u8, 100u8, 100u8];
425        let b = vec![110u8, 90u8, 100u8, 120u8];
426        // diffs: 10, 10, 0, 20 → mean = 10.0
427        let mae = proc.mean_absolute_error(&a, &b).unwrap();
428        assert!((mae - 10.0).abs() < 1e-9);
429    }
430
431    #[test]
432    fn test_invalid_frame_size() {
433        let proc = make_processor(4, 4, 1);
434        let frame = vec![0u8; 10]; // wrong size
435        assert!(proc.compute_histogram(&frame).is_err());
436        assert!(proc.adjust_brightness(&frame, 0).is_err());
437        assert!(proc.adjust_contrast(&frame, 1.0).is_err());
438    }
439
440    #[test]
441    fn test_config_accessor() {
442        let config = FrameProcessConfig {
443            width: 1920,
444            height: 1080,
445            channels: 4,
446        };
447        let proc = VideoFrameProcessor::new(config.clone());
448        assert_eq!(proc.config().width, 1920);
449        assert_eq!(proc.config().height, 1080);
450        assert_eq!(proc.config().channels, 4);
451    }
452}