Skip to main content

oximedia_gpu/
readback.rs

1//! GPU readback utilities.
2//!
3//! Provides facilities for reading GPU framebuffer / texture data back to
4//! CPU-accessible memory.  On the CPU-stub backend this is a direct copy
5//! (identity transform); on a real GPU backend the same interface wraps
6//! `wgpu::Buffer::map_read`.
7//!
8//! # Example
9//!
10//! ```rust
11//! use oximedia_gpu::readback::GpuReadback;
12//!
13//! let data = vec![0xFFu8; 4 * 4 * 4]; // 4×4 RGBA
14//! let out = GpuReadback::download(4, 4, &data);
15//! assert_eq!(out, data);
16//! ```
17
18#![allow(dead_code)]
19
20// ── GpuReadback ───────────────────────────────────────────────────────────────
21
22/// GPU → CPU readback helper.
23///
24/// The current implementation operates as a CPU stub (identity copy).
25/// Replace the body of [`GpuReadback::download`] and [`GpuReadback::download_region`] with actual
26/// WGPU buffer-mapping logic when a live device is available.
27pub struct GpuReadback;
28
29impl GpuReadback {
30    /// Download a full frame from GPU-accessible memory to a CPU `Vec<u8>`.
31    ///
32    /// On the CPU stub backend this is an identity copy of `gpu_data`.
33    ///
34    /// # Arguments
35    ///
36    /// * `width`    – Frame width in pixels.
37    /// * `height`   – Frame height in pixels.
38    /// * `gpu_data` – GPU-accessible source buffer (RGBA packed, row-major).
39    ///
40    /// # Returns
41    ///
42    /// A `Vec<u8>` containing a copy of the readback data.
43    #[must_use]
44    pub fn download(width: u32, height: u32, gpu_data: &[u8]) -> Vec<u8> {
45        let expected = (width as usize) * (height as usize) * 4;
46        // Clamp to the actual data length in case the caller passes a
47        // smaller slice (e.g. partial rows during streaming readback).
48        let len = expected.min(gpu_data.len());
49        gpu_data[..len].to_vec()
50    }
51
52    /// Download a sub-region of a frame.
53    ///
54    /// * `src_width`  – Width of the full source frame in pixels.
55    /// * `x`, `y`    – Top-left corner of the sub-region.
56    /// * `w`, `h`    – Width/height of the sub-region in pixels.
57    /// * `gpu_data`  – Full source frame data (RGBA, row-major).
58    ///
59    /// Returns an empty `Vec` if the region is fully out of bounds.
60    #[must_use]
61    pub fn download_region(
62        src_width: u32,
63        x: u32,
64        y: u32,
65        w: u32,
66        h: u32,
67        gpu_data: &[u8],
68    ) -> Vec<u8> {
69        if w == 0 || h == 0 {
70            return Vec::new();
71        }
72        let stride = (src_width as usize) * 4;
73        let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
74        for row in 0..h {
75            let src_y = (y + row) as usize;
76            let src_x = x as usize;
77            let row_start = src_y * stride + src_x * 4;
78            let row_end = row_start + (w as usize) * 4;
79            if row_end > gpu_data.len() {
80                break;
81            }
82            out.extend_from_slice(&gpu_data[row_start..row_end]);
83        }
84        out
85    }
86
87    /// Compute the expected byte length for a frame of `width × height` pixels
88    /// in RGBA format.
89    #[must_use]
90    pub fn expected_len(width: u32, height: u32) -> usize {
91        (width as usize) * (height as usize) * 4
92    }
93
94    /// Verify that `gpu_data` has exactly the expected length for a
95    /// `width × height` RGBA frame.
96    #[must_use]
97    pub fn validate_size(width: u32, height: u32, gpu_data: &[u8]) -> bool {
98        gpu_data.len() == Self::expected_len(width, height)
99    }
100
101    /// Split RGBA packed data into separate R, G, B, A channel planes.
102    ///
103    /// Returns `(r, g, b, a)` each of length `width * height`.
104    #[must_use]
105    pub fn split_channels(gpu_data: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>) {
106        let pixels = gpu_data.len() / 4;
107        let mut r = Vec::with_capacity(pixels);
108        let mut g = Vec::with_capacity(pixels);
109        let mut b = Vec::with_capacity(pixels);
110        let mut a = Vec::with_capacity(pixels);
111        for chunk in gpu_data.chunks_exact(4) {
112            r.push(chunk[0]);
113            g.push(chunk[1]);
114            b.push(chunk[2]);
115            a.push(chunk[3]);
116        }
117        (r, g, b, a)
118    }
119}
120
121// ── Tests ─────────────────────────────────────────────────────────────────────
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_download_identity() {
129        let data: Vec<u8> = (0..64).collect();
130        let result = GpuReadback::download(4, 4, &data);
131        assert_eq!(result, data);
132    }
133
134    #[test]
135    fn test_download_truncates_to_expected_len() {
136        // 2×2 RGBA = 16 bytes; supply 20 bytes → only 16 returned
137        let data = vec![0xAAu8; 20];
138        let result = GpuReadback::download(2, 2, &data);
139        assert_eq!(result.len(), 16);
140    }
141
142    #[test]
143    fn test_download_short_slice_returns_available_bytes() {
144        // Supply only 8 bytes for a 2×2 frame (expected 16)
145        let data = vec![0xBBu8; 8];
146        let result = GpuReadback::download(2, 2, &data);
147        assert_eq!(result.len(), 8);
148    }
149
150    #[test]
151    fn test_download_region_basic() {
152        // 4×4 RGBA frame, each pixel is its row index repeated 4 times.
153        let mut frame = vec![0u8; 4 * 4 * 4];
154        for row in 0..4usize {
155            for col in 0..4usize {
156                let idx = (row * 4 + col) * 4;
157                frame[idx] = row as u8;
158                frame[idx + 1] = row as u8;
159                frame[idx + 2] = row as u8;
160                frame[idx + 3] = 0xFF;
161            }
162        }
163        // Download row 1, columns 0-1 (2×1 region)
164        let region = GpuReadback::download_region(4, 0, 1, 2, 1, &frame);
165        assert_eq!(region.len(), 8); // 2 pixels × 4 channels
166        assert_eq!(region[0], 1u8); // row 1
167    }
168
169    #[test]
170    fn test_download_region_zero_dimensions() {
171        let frame = vec![0u8; 64];
172        assert!(GpuReadback::download_region(4, 0, 0, 0, 4, &frame).is_empty());
173        assert!(GpuReadback::download_region(4, 0, 0, 4, 0, &frame).is_empty());
174    }
175
176    #[test]
177    fn test_expected_len() {
178        assert_eq!(GpuReadback::expected_len(1920, 1080), 1920 * 1080 * 4);
179    }
180
181    #[test]
182    fn test_validate_size_correct() {
183        let data = vec![0u8; 4 * 4 * 4];
184        assert!(GpuReadback::validate_size(4, 4, &data));
185    }
186
187    #[test]
188    fn test_validate_size_wrong() {
189        let data = vec![0u8; 10];
190        assert!(!GpuReadback::validate_size(4, 4, &data));
191    }
192
193    #[test]
194    fn test_split_channels_correctness() {
195        // Single pixel: R=1, G=2, B=3, A=255
196        let data = vec![1u8, 2, 3, 255];
197        let (r, g, b, a) = GpuReadback::split_channels(&data);
198        assert_eq!(r, [1]);
199        assert_eq!(g, [2]);
200        assert_eq!(b, [3]);
201        assert_eq!(a, [255]);
202    }
203
204    #[test]
205    fn test_split_channels_multiple_pixels() {
206        let mut data = Vec::new();
207        for i in 0u8..4 {
208            data.extend_from_slice(&[i, i + 10, i + 20, 0xFF]);
209        }
210        let (r, g, b, a) = GpuReadback::split_channels(&data);
211        assert_eq!(r, [0, 1, 2, 3]);
212        assert_eq!(g, [10, 11, 12, 13]);
213        assert_eq!(b, [20, 21, 22, 23]);
214        assert_eq!(a, [0xFF, 0xFF, 0xFF, 0xFF]);
215    }
216}