Skip to main content

oximedia_gpu/
mipmap_gen.rs

1#![allow(dead_code)]
2//! Mipmap generation utilities for GPU textures.
3//!
4//! This module computes mip-chain metadata and performs CPU-side
5//! mipmap generation using box-filter downsampling. It can be used
6//! as a reference implementation or for CPU fallback paths when
7//! GPU mipmap generation is unavailable.
8
9use std::fmt;
10
11/// Describes a single mip level within a mip chain.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct MipLevel {
14    /// Mip level index (0 = full resolution).
15    pub level: u32,
16    /// Width of this mip level in pixels.
17    pub width: u32,
18    /// Height of this mip level in pixels.
19    pub height: u32,
20    /// Byte offset into the mip chain buffer.
21    pub offset: usize,
22    /// Size in bytes of this mip level.
23    pub size: usize,
24}
25
26impl MipLevel {
27    /// Total number of pixels at this level.
28    pub fn pixel_count(&self) -> u64 {
29        u64::from(self.width) * u64::from(self.height)
30    }
31}
32
33impl fmt::Display for MipLevel {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(
36            f,
37            "Mip[{}]: {}x{} (offset={}, size={})",
38            self.level, self.width, self.height, self.offset, self.size
39        )
40    }
41}
42
43/// Describes the full mip chain for a texture.
44#[derive(Debug, Clone)]
45pub struct MipChain {
46    /// Base width of the texture (level 0).
47    pub base_width: u32,
48    /// Base height of the texture (level 0).
49    pub base_height: u32,
50    /// Number of channels per pixel.
51    pub channels: u32,
52    /// Individual mip level descriptors.
53    pub levels: Vec<MipLevel>,
54}
55
56impl MipChain {
57    /// Compute the full mip chain for a texture with given dimensions.
58    ///
59    /// Generates levels down to 1x1 unless `max_levels` limits the count.
60    pub fn compute(
61        base_width: u32,
62        base_height: u32,
63        channels: u32,
64        max_levels: Option<u32>,
65    ) -> Self {
66        let full_count = compute_mip_count(base_width, base_height);
67        let level_count = max_levels.map_or(full_count, |m| m.min(full_count));
68
69        let mut levels = Vec::with_capacity(level_count as usize);
70        let mut w = base_width;
71        let mut h = base_height;
72        let mut offset = 0usize;
73
74        for i in 0..level_count {
75            let size = (w as usize) * (h as usize) * (channels as usize);
76            levels.push(MipLevel {
77                level: i,
78                width: w,
79                height: h,
80                offset,
81                size,
82            });
83            offset += size;
84            w = (w / 2).max(1);
85            h = (h / 2).max(1);
86        }
87
88        Self {
89            base_width,
90            base_height,
91            channels,
92            levels,
93        }
94    }
95
96    /// Total number of mip levels in the chain.
97    pub fn level_count(&self) -> u32 {
98        self.levels.len() as u32
99    }
100
101    /// Total size in bytes for the entire mip chain.
102    pub fn total_size(&self) -> usize {
103        self.levels.iter().map(|l| l.size).sum()
104    }
105
106    /// Get a specific mip level by index.
107    pub fn level(&self, index: u32) -> Option<&MipLevel> {
108        self.levels.get(index as usize)
109    }
110
111    /// Check whether the chain includes only a single level (no mips).
112    pub fn is_single_level(&self) -> bool {
113        self.levels.len() <= 1
114    }
115}
116
117/// Compute the maximum number of mip levels for given dimensions.
118///
119/// This is `floor(log2(max(width, height))) + 1`.
120#[allow(clippy::cast_precision_loss)]
121pub fn compute_mip_count(width: u32, height: u32) -> u32 {
122    if width == 0 || height == 0 {
123        return 0;
124    }
125    let max_dim = width.max(height);
126    (max_dim as f64).log2().floor() as u32 + 1
127}
128
129/// Compute the dimension of a specific mip level.
130pub fn mip_dimension(base: u32, level: u32) -> u32 {
131    (base >> level).max(1)
132}
133
134/// Downsample a single-channel u8 image by 2x using a box filter (CPU path).
135///
136/// The source dimensions must be at least 2x2.
137pub fn downsample_box_u8(src: &[u8], src_width: u32, src_height: u32, channels: u32) -> Vec<u8> {
138    let dst_width = (src_width / 2).max(1);
139    let dst_height = (src_height / 2).max(1);
140    let ch = channels as usize;
141    let mut dst = vec![0u8; dst_width as usize * dst_height as usize * ch];
142
143    let sw = src_width as usize;
144
145    for dy in 0..dst_height as usize {
146        for dx in 0..dst_width as usize {
147            let sx = dx * 2;
148            let sy = dy * 2;
149
150            // Clamp secondary samples
151            let sx1 = (sx + 1).min(src_width as usize - 1);
152            let sy1 = (sy + 1).min(src_height as usize - 1);
153
154            for c in 0..ch {
155                let s00 = u16::from(src[(sy * sw + sx) * ch + c]);
156                let s10 = u16::from(src[(sy * sw + sx1) * ch + c]);
157                let s01 = u16::from(src[(sy1 * sw + sx) * ch + c]);
158                let s11 = u16::from(src[(sy1 * sw + sx1) * ch + c]);
159                let avg = ((s00 + s10 + s01 + s11 + 2) / 4) as u8;
160                dst[(dy * dst_width as usize + dx) * ch + c] = avg;
161            }
162        }
163    }
164
165    dst
166}
167
168/// Generate the full mip chain data from a base level image.
169///
170/// Returns a flat buffer containing all mip levels concatenated,
171/// along with the mip chain descriptor.
172pub fn generate_mip_chain_u8(
173    base_data: &[u8],
174    width: u32,
175    height: u32,
176    channels: u32,
177) -> (Vec<u8>, MipChain) {
178    let chain = MipChain::compute(width, height, channels, None);
179    let mut output = Vec::with_capacity(chain.total_size());
180
181    // Level 0 is the original image
182    output.extend_from_slice(base_data);
183
184    let mut prev_data = base_data.to_vec();
185    let mut prev_w = width;
186    let mut prev_h = height;
187
188    for level in chain.levels.iter().skip(1) {
189        let down = downsample_box_u8(&prev_data, prev_w, prev_h, channels);
190        output.extend_from_slice(&down);
191        prev_w = level.width;
192        prev_h = level.height;
193        prev_data = down;
194    }
195
196    (output, chain)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_compute_mip_count_square() {
205        assert_eq!(compute_mip_count(256, 256), 9); // log2(256)+1 = 9
206    }
207
208    #[test]
209    fn test_compute_mip_count_nonsquare() {
210        assert_eq!(compute_mip_count(1920, 1080), 11); // log2(1920)+1 = 11
211    }
212
213    #[test]
214    fn test_compute_mip_count_one() {
215        assert_eq!(compute_mip_count(1, 1), 1);
216    }
217
218    #[test]
219    fn test_compute_mip_count_zero() {
220        assert_eq!(compute_mip_count(0, 100), 0);
221        assert_eq!(compute_mip_count(100, 0), 0);
222    }
223
224    #[test]
225    fn test_mip_dimension() {
226        assert_eq!(mip_dimension(256, 0), 256);
227        assert_eq!(mip_dimension(256, 1), 128);
228        assert_eq!(mip_dimension(256, 8), 1);
229        assert_eq!(mip_dimension(256, 20), 1); // Clamped to 1
230    }
231
232    #[test]
233    fn test_mip_chain_compute() {
234        let chain = MipChain::compute(64, 64, 4, None);
235        assert_eq!(chain.level_count(), 7); // log2(64)+1 = 7
236        assert_eq!(chain.levels[0].width, 64);
237        assert_eq!(chain.levels[0].height, 64);
238        assert_eq!(chain.levels[1].width, 32);
239        assert_eq!(chain.levels[6].width, 1);
240    }
241
242    #[test]
243    fn test_mip_chain_max_levels() {
244        let chain = MipChain::compute(256, 256, 4, Some(3));
245        assert_eq!(chain.level_count(), 3);
246        assert_eq!(chain.levels[2].width, 64);
247    }
248
249    #[test]
250    fn test_mip_chain_total_size() {
251        let chain = MipChain::compute(4, 4, 1, None);
252        // 4x4=16 + 2x2=4 + 1x1=1 = 21
253        assert_eq!(chain.total_size(), 21);
254    }
255
256    #[test]
257    fn test_mip_chain_offsets() {
258        let chain = MipChain::compute(8, 8, 1, None);
259        assert_eq!(chain.levels[0].offset, 0);
260        assert_eq!(chain.levels[1].offset, 64); // 8*8*1
261        assert_eq!(chain.levels[2].offset, 80); // 64 + 4*4*1
262    }
263
264    #[test]
265    fn test_mip_level_display() {
266        let level = MipLevel {
267            level: 2,
268            width: 64,
269            height: 32,
270            offset: 1000,
271            size: 2048,
272        };
273        assert_eq!(format!("{level}"), "Mip[2]: 64x32 (offset=1000, size=2048)");
274    }
275
276    #[test]
277    fn test_downsample_box_u8_simple() {
278        // 4x4 image, 1 channel, all 200
279        let src = vec![200u8; 16];
280        let dst = downsample_box_u8(&src, 4, 4, 1);
281        assert_eq!(dst.len(), 4); // 2x2
282        for &v in &dst {
283            assert_eq!(v, 200);
284        }
285    }
286
287    #[test]
288    fn test_downsample_box_u8_gradient() {
289        // 2x2 image with values [0, 100, 200, 56]
290        let src = vec![0u8, 100, 200, 56];
291        let dst = downsample_box_u8(&src, 2, 2, 1);
292        assert_eq!(dst.len(), 1);
293        // Average: (0+100+200+56+2)/4 = 89 (integer)
294        assert_eq!(dst[0], 89);
295    }
296
297    #[test]
298    fn test_generate_mip_chain_u8() {
299        let base = vec![128u8; 4 * 4]; // 4x4, 1 channel
300        let (data, chain) = generate_mip_chain_u8(&base, 4, 4, 1);
301        assert_eq!(chain.level_count(), 3);
302        assert_eq!(data.len(), chain.total_size());
303        // Level 0 should be the original
304        assert_eq!(&data[0..16], &base[..]);
305    }
306
307    #[test]
308    fn test_mip_chain_is_single_level() {
309        let chain = MipChain::compute(1, 1, 4, None);
310        assert!(chain.is_single_level());
311        let chain2 = MipChain::compute(4, 4, 4, None);
312        assert!(!chain2.is_single_level());
313    }
314
315    #[test]
316    fn test_mip_level_pixel_count() {
317        let level = MipLevel {
318            level: 0,
319            width: 100,
320            height: 50,
321            offset: 0,
322            size: 0,
323        };
324        assert_eq!(level.pixel_count(), 5000);
325    }
326}