Skip to main content

pebble/wgpu/
textures.rs

1pub struct TextureSpec {
2    pub width: u32,
3    pub height: u32,
4    pub format: wgpu::TextureFormat,
5    /// Base (mip 0) pixel data, tightly packed, 4 bytes per texel (matching
6    /// `format`). Required even when `format` isn't literally `Rgba8*` —
7    /// mip generation always treats it as 4 bytes/texel.
8    pub data: Vec<u8>,
9    pub generate_mips: bool,
10}
11
12impl TextureSpec {
13    /// Number of mip levels this texture will have: the full chain down to
14    /// 1x1 if `generate_mips`, otherwise just the base level.
15    pub fn mip_level_count(&self) -> u32 {
16        if self.generate_mips {
17            32 - self.width.max(self.height).max(1).leading_zeros()
18        } else {
19            1
20        }
21    }
22
23    /// Pure conversion — matches what [`upload`](Self::upload) actually
24    /// creates, including `mip_level_count`. You can call
25    /// `device.create_texture(&spec.wgpu_descriptor())` yourself if you'd
26    /// rather write the mip levels some other way; [`upload`](Self::upload)
27    /// is the batteries-included path.
28    pub fn wgpu_descriptor(&self) -> wgpu::TextureDescriptor<'static> {
29        wgpu::TextureDescriptor {
30            label: None,
31            size: wgpu::Extent3d {
32                width: self.width,
33                height: self.height,
34                depth_or_array_layers: 1,
35            },
36            mip_level_count: self.mip_level_count(),
37            sample_count: 1,
38            dimension: wgpu::TextureDimension::D2,
39            format: self.format,
40            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
41            view_formats: &[],
42        }
43    }
44
45    /// Box-filter downsample to half size (minimum 1x1), 4 bytes/texel.
46    fn downsample(data: &[u8], width: u32, height: u32) -> (Vec<u8>, u32, u32) {
47        let next_width = (width / 2).max(1);
48        let next_height = (height / 2).max(1);
49        let mut out = vec![0u8; (next_width * next_height * 4) as usize];
50
51        for y in 0..next_height {
52            for x in 0..next_width {
53                let x0 = (x * 2).min(width - 1);
54                let x1 = (x * 2 + 1).min(width - 1);
55                let y0 = (y * 2).min(height - 1);
56                let y1 = (y * 2 + 1).min(height - 1);
57
58                for c in 0..4 {
59                    let sample = |sx: u32, sy: u32| -> u32 {
60                        data[((sy * width + sx) * 4 + c) as usize] as u32
61                    };
62                    let avg = (sample(x0, y0) + sample(x1, y0) + sample(x0, y1) + sample(x1, y1))
63                        / 4;
64                    out[((y * next_width + x) * 4 + c) as usize] = avg as u8;
65                }
66            }
67        }
68
69        (out, next_width, next_height)
70    }
71
72    /// Create the texture, write every mip level (generating each one from
73    /// the last via a box filter if `generate_mips` is set), and return it
74    /// along with a default view over the whole chain.
75    pub fn upload(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> (wgpu::Texture, wgpu::TextureView) {
76        let texture = device.create_texture(&self.wgpu_descriptor());
77        let mip_count = self.mip_level_count();
78
79        let mut level_data = self.data.clone();
80        let mut level_width = self.width;
81        let mut level_height = self.height;
82
83        for level in 0..mip_count {
84            queue.write_texture(
85                wgpu::TexelCopyTextureInfo {
86                    texture: &texture,
87                    mip_level: level,
88                    origin: wgpu::Origin3d::default(),
89                    aspect: wgpu::TextureAspect::All,
90                },
91                &level_data,
92                wgpu::TexelCopyBufferLayout {
93                    offset: 0,
94                    bytes_per_row: Some(4 * level_width),
95                    rows_per_image: Some(level_height),
96                },
97                wgpu::Extent3d {
98                    width: level_width,
99                    height: level_height,
100                    depth_or_array_layers: 1,
101                },
102            );
103
104            if level + 1 < mip_count {
105                let (next_data, next_width, next_height) =
106                    Self::downsample(&level_data, level_width, level_height);
107                level_data = next_data;
108                level_width = next_width;
109                level_height = next_height;
110            }
111        }
112
113        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
114        (texture, view)
115    }
116}
117
118impl TextureSpec {
119    pub fn new(width: u32, height: u32, data: Vec<u8>, generate_mips: bool) -> Self {
120        Self {
121            width,
122            height,
123            format: wgpu::TextureFormat::Rgba8Unorm,
124            data,
125            generate_mips,
126        }
127    }
128
129    pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
130        self.format = format;
131        self
132    }
133}