Skip to main content

oxihuman_viewer/
gpu_readback.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! GPU readback helpers: staging buffer management and pixel-format conversion.
6
7/// Format of the readback data.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[allow(dead_code)]
10pub enum ReadbackFormat {
11    Rgba8Unorm,
12    Rgba16Float,
13    R32Float,
14    Depth32Float,
15}
16
17/// A pending readback request.
18#[derive(Debug, Clone)]
19#[allow(dead_code)]
20pub struct ReadbackRequest {
21    pub format: ReadbackFormat,
22    pub width: u32,
23    pub height: u32,
24    /// Byte offset into the staging buffer.
25    pub offset: u64,
26}
27
28/// A completed readback with raw byte payload.
29#[derive(Debug, Clone)]
30#[allow(dead_code)]
31pub struct ReadbackResult {
32    pub format: ReadbackFormat,
33    pub width: u32,
34    pub height: u32,
35    pub data: Vec<u8>,
36}
37
38/// Return the bytes-per-pixel for a `ReadbackFormat`.
39#[allow(dead_code)]
40pub fn bytes_per_pixel(fmt: ReadbackFormat) -> usize {
41    match fmt {
42        ReadbackFormat::Rgba8Unorm => 4,
43        ReadbackFormat::Rgba16Float => 8,
44        ReadbackFormat::R32Float => 4,
45        ReadbackFormat::Depth32Float => 4,
46    }
47}
48
49/// Compute the required staging buffer size in bytes.
50#[allow(dead_code)]
51pub fn staging_buffer_size(req: &ReadbackRequest) -> u64 {
52    let bpp = bytes_per_pixel(req.format) as u64;
53    bpp * req.width as u64 * req.height as u64
54}
55
56/// Create a blank (zeroed) `ReadbackResult`.
57#[allow(dead_code)]
58pub fn blank_result(fmt: ReadbackFormat, width: u32, height: u32) -> ReadbackResult {
59    let size = bytes_per_pixel(fmt) * width as usize * height as usize;
60    ReadbackResult {
61        format: fmt,
62        width,
63        height,
64        data: vec![0u8; size],
65    }
66}
67
68/// Convert an Rgba8Unorm pixel at index `i` to linear [f32; 4].
69#[allow(dead_code)]
70pub fn rgba8_to_f32(data: &[u8], pixel_index: usize) -> [f32; 4] {
71    let base = pixel_index * 4;
72    [
73        data[base] as f32 / 255.0,
74        data[base + 1] as f32 / 255.0,
75        data[base + 2] as f32 / 255.0,
76        data[base + 3] as f32 / 255.0,
77    ]
78}
79
80/// Read an R32Float value at `pixel_index`.
81#[allow(dead_code)]
82pub fn r32_to_f32(data: &[u8], pixel_index: usize) -> f32 {
83    let base = pixel_index * 4;
84    let bytes: [u8; 4] = data[base..base + 4].try_into().unwrap_or([0; 4]);
85    f32::from_le_bytes(bytes)
86}
87
88/// Validate that a `ReadbackResult` data length matches width × height × bpp.
89#[allow(dead_code)]
90pub fn validate_result(res: &ReadbackResult) -> bool {
91    let expected = bytes_per_pixel(res.format) * res.width as usize * res.height as usize;
92    res.data.len() == expected
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn bytes_per_pixel_rgba8() {
101        assert_eq!(bytes_per_pixel(ReadbackFormat::Rgba8Unorm), 4);
102    }
103
104    #[test]
105    fn bytes_per_pixel_rgba16() {
106        assert_eq!(bytes_per_pixel(ReadbackFormat::Rgba16Float), 8);
107    }
108
109    #[test]
110    fn staging_buffer_size_correct() {
111        let req = ReadbackRequest {
112            format: ReadbackFormat::Rgba8Unorm,
113            width: 4,
114            height: 4,
115            offset: 0,
116        };
117        assert_eq!(staging_buffer_size(&req), 64);
118    }
119
120    #[test]
121    fn blank_result_correct_size() {
122        let r = blank_result(ReadbackFormat::Rgba8Unorm, 2, 2);
123        assert!(validate_result(&r));
124    }
125
126    #[test]
127    fn rgba8_to_f32_white() {
128        let data = vec![255u8; 4];
129        let px = rgba8_to_f32(&data, 0);
130        assert!((px[0] - 1.0).abs() < 1e-3);
131    }
132
133    #[test]
134    fn rgba8_to_f32_black() {
135        let data = vec![0u8; 8];
136        let px = rgba8_to_f32(&data, 0);
137        assert_eq!(px[0], 0.0);
138    }
139
140    #[test]
141    fn r32_to_f32_roundtrip() {
142        let val = std::f32::consts::PI;
143        let bytes = val.to_le_bytes();
144        let data: Vec<u8> = bytes.to_vec();
145        assert!((r32_to_f32(&data, 0) - val).abs() < 1e-6);
146    }
147
148    #[test]
149    fn validate_result_correct() {
150        let r = blank_result(ReadbackFormat::R32Float, 3, 3);
151        assert!(validate_result(&r));
152    }
153
154    #[test]
155    fn validate_result_wrong_size() {
156        let mut r = blank_result(ReadbackFormat::Rgba8Unorm, 2, 2);
157        r.data.pop();
158        assert!(!validate_result(&r));
159    }
160}