ailloli_ui_render_wgpu/
capture.rs

1//! Swapchain readback, row padding, and PNG encoding for visual tests.
2
3use std::io::Cursor;
4
5/// Pixel format for captured frames.
6///
7/// # Examples
8///
9/// ```
10/// use ailloli_ui_render_wgpu::CapturedFrameFormat;
11/// let format = CapturedFrameFormat::Rgba8;
12/// assert_eq!(format, CapturedFrameFormat::Rgba8);
13/// ```
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CapturedFrameFormat {
16    /// Four eight-bit channels in red, green, blue, alpha byte order.
17    Rgba8,
18}
19
20/// Parameters for one capture readback.
21///
22/// The default requests PNG encoding in addition to the tight RGBA buffer.
23///
24/// # Examples
25///
26/// ```
27/// use ailloli_ui_render_wgpu::CaptureParams;
28/// let params = CaptureParams::default();
29/// assert!(params.encode_png);
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct CaptureParams {
33    /// Whether to populate [`CapturedFrame::png_data`] with an RGBA8 PNG.
34    pub encode_png: bool,
35}
36
37impl Default for CaptureParams {
38    fn default() -> Self {
39        Self { encode_png: true }
40    }
41}
42
43/// A captured frame read back from a render target.
44///
45/// `rgba` is always available. `png_data` is `None` when PNG encoding was not
46/// requested and `Some` only after a successful encode.
47///
48/// # Examples
49///
50/// ```
51/// use ailloli_ui_render_wgpu::{CapturedFrame, CapturedFrameFormat};
52/// let frame = CapturedFrame {
53///     width: 1,
54///     height: 1,
55///     format: CapturedFrameFormat::Rgba8,
56///     rgba: vec![255, 0, 0, 255],
57///     png_data: None,
58/// };
59/// assert_eq!(frame.rgba.len(), frame.width as usize * frame.height as usize * 4);
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CapturedFrame {
63    /// Frame width in physical pixels.
64    pub width: u32,
65    /// Frame height in physical pixels.
66    pub height: u32,
67    /// Byte layout used by [`Self::rgba`].
68    pub format: CapturedFrameFormat,
69    /// Tight RGBA8 buffer (`width * height * 4`).
70    pub rgba: Vec<u8>,
71    /// Optional PNG-encoded bytes for easy artifact writing.
72    pub png_data: Option<Vec<u8>>,
73}
74
75/// Returns a row byte length rounded up to wgpu's 256-byte copy alignment.
76///
77/// Zero remains zero. Callers must keep the input below the largest multiple
78/// that fits in `u32`; multiplication can otherwise overflow.
79///
80/// # Panics
81///
82/// In debug builds, panics when the rounded result exceeds `u32::MAX`.
83///
84/// # Examples
85///
86/// ```
87/// use ailloli_ui_render_wgpu::capture::bytes_per_row_padded_256;
88/// assert_eq!(bytes_per_row_padded_256(0), 0);
89/// assert_eq!(bytes_per_row_padded_256(68), 256);
90/// assert_eq!(bytes_per_row_padded_256(256), 256);
91/// ```
92pub fn bytes_per_row_padded_256(unpadded_bpr: u32) -> u32 {
93    unpadded_bpr.div_ceil(256) * 256
94}
95
96/// Strips row padding from a mapped texture buffer into a tight row layout.
97///
98/// Exactly `rows * dst_bpr` bytes are returned. Each row copies at most
99/// `dst_bpr` bytes from the corresponding `src_bpr`-spaced source row.
100///
101/// # Panics
102///
103/// Panics if a requested source row starts beyond `src`, if `src_bpr` is
104/// smaller than `dst_bpr` for a fully present row, or if output-size arithmetic
105/// overflows `usize`.
106///
107/// # Examples
108///
109/// ```
110/// use ailloli_ui_render_wgpu::capture::unpad_rows_rgba;
111/// let src = [1, 2, 0, 0, 3, 4, 0, 0];
112/// assert_eq!(unpad_rows_rgba(&src, 4, 2, 2), vec![1, 2, 3, 4]);
113/// ```
114pub fn unpad_rows_rgba(src: &[u8], src_bpr: usize, dst_bpr: usize, rows: usize) -> Vec<u8> {
115    let mut out = vec![0u8; dst_bpr * rows];
116    for row in 0..rows {
117        let src_off = row * src_bpr;
118        let dst_off = row * dst_bpr;
119        let src_end = (src_off + dst_bpr).min(src.len());
120        out[dst_off..dst_off + (src_end - src_off)].copy_from_slice(&src[src_off..src_end]);
121    }
122    out
123}
124
125/// Swaps R and B channels in-place (BGRA to RGBA).
126///
127/// A trailing slice shorter than four bytes is left unchanged.
128///
129/// # Examples
130///
131/// ```
132/// use ailloli_ui_render_wgpu::capture::bgra_to_rgba_in_place;
133/// let mut bytes = [10, 20, 30, 255, 9];
134/// bgra_to_rgba_in_place(&mut bytes);
135/// assert_eq!(bytes, [30, 20, 10, 255, 9]);
136/// ```
137pub fn bgra_to_rgba_in_place(buf: &mut [u8]) {
138    for px in buf.chunks_exact_mut(4) {
139        px.swap(0, 2);
140    }
141}
142
143/// Encodes a tight RGBA8 buffer as PNG.
144///
145/// # Errors
146///
147/// Returns an error when `rgba.len()` is not exactly `width * height * 4` or
148/// when the image encoder rejects the output.
149///
150/// # Examples
151///
152/// ```
153/// use ailloli_ui_render_wgpu::capture::encode_png_rgba;
154/// let png = encode_png_rgba(1, 1, &[255, 0, 0, 255])?;
155/// assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
156/// # Ok::<(), String>(())
157/// ```
158pub fn encode_png_rgba(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
159    let mut data = Vec::new();
160    let mut writer = Cursor::new(&mut data);
161    let img = image::RgbaImage::from_raw(width, height, rgba.to_vec())
162        .ok_or_else(|| "failed to construct RgbaImage".to_string())?;
163    image::DynamicImage::ImageRgba8(img)
164        .write_to(&mut writer, image::ImageFormat::Png)
165        .map_err(|e| format!("png encode failed: {e}"))?;
166    Ok(data)
167}
168
169#[cfg(test)]
170/// Verifies row alignment, row unpadding, and BGRA channel conversion.
171mod tests {
172    use super::*;
173
174    #[test]
175    fn padded_bpr_is_multiple_of_256_and_at_least_unpadded() {
176        let unpadded = 4 * 17;
177        let padded = bytes_per_row_padded_256(unpadded);
178        assert!(padded >= unpadded);
179        assert_eq!(padded % 256, 0);
180    }
181
182    #[test]
183    fn unpad_rows_copies_tight_prefix_per_row() {
184        let w = 3u32;
185        let h = 2usize;
186        let unpadded_bpr = (w * 4) as usize;
187        let padded_bpr = bytes_per_row_padded_256(w * 4) as usize;
188        assert!(padded_bpr >= unpadded_bpr);
189
190        // Build padded rows with known content.
191        let mut padded = vec![0u8; padded_bpr * h];
192        for row in 0..h {
193            for i in 0..unpadded_bpr {
194                padded[row * padded_bpr + i] = (row as u8) * 10 + (i as u8);
195            }
196        }
197
198        let tight = unpad_rows_rgba(&padded, padded_bpr, unpadded_bpr, h);
199        assert_eq!(tight.len(), unpadded_bpr * h);
200        for row in 0..h {
201            assert_eq!(
202                &tight[row * unpadded_bpr..(row + 1) * unpadded_bpr],
203                &padded[row * padded_bpr..row * padded_bpr + unpadded_bpr]
204            );
205        }
206    }
207
208    #[test]
209    fn bgra_to_rgba_swaps_r_and_b() {
210        let mut px = vec![1u8, 2u8, 3u8, 4u8];
211        bgra_to_rgba_in_place(&mut px);
212        assert_eq!(px, vec![3u8, 2u8, 1u8, 4u8]);
213    }
214}