Skip to main content

pebble/graphics/pipeline/
textures.rs

1use crate::{
2    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3    ecs::resources::Read,
4    graphics::{
5        pipeline::{mipmap::{MipLevels, MipmapGenerator}, texture_view::TextureView},
6        render::{Backend, gpu_context::GpuContext},
7        types::TextureFormat,
8    },
9};
10
11pub struct Texture {
12    file: Option<&'static str>,
13    width: u32,
14    height: u32,
15    format: TextureFormat,
16    data: Option<Vec<u8>>,
17    mip_levels: MipLevels,
18}
19
20impl Texture {
21    pub fn from_file(path: &'static str) -> Self {
22        Self { file: Some(path), width: 0, height: 0, format: TextureFormat::Rgba8UnormSrgb, data: None, mip_levels: MipLevels::None }
23    }
24
25    pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
26        Self { file: None, width, height, format, data: Some(data), mip_levels: MipLevels::None }
27    }
28
29    pub fn empty(width: u32, height: u32, format: TextureFormat) -> Self {
30        Self { file: None, width, height, format, data: None, mip_levels: MipLevels::None }
31    }
32
33    pub fn with_format(mut self, format: TextureFormat) -> Self {
34        self.format = format;
35        self
36    }
37
38    pub fn with_mips(mut self) -> Self {
39        self.mip_levels = MipLevels::Full;
40        self
41    }
42
43    pub fn with_mip_count(mut self, count: u32) -> Self {
44        self.mip_levels = MipLevels::Fixed(count);
45        self
46    }
47
48    fn validate(&self) {
49        if self.data.is_some() && (self.width == 0 || self.height == 0) {
50            tracing::warn!(
51                "Texture::from_data(): width/height is 0 ({}x{}) — did you swap the \
52                 argument order, or forget to pass the real dimensions?",
53                self.width,
54                self.height,
55            );
56        }
57    }
58
59    pub fn build_asset(self, name: &str, assets: &mut Assets<Texture>) -> Handle<Texture> {
60        self.validate();
61        assets.insert(name, self)
62    }
63
64    // CPU-side pixels, when there are any to give back — e.g. for CPU-side
65    // heightmap sampling from a texture built via from_data(). A
66    // from_file() texture always returns None here: it re-decodes from
67    // disk on each upload rather than caching a copy, so there's nothing
68    // to hand back without changing that behavior (and re-introducing the
69    // permanent-memory-duplication problem from_file was written to avoid).
70    pub fn data(&self) -> Option<&[u8]> {
71        self.data.as_deref()
72    }
73
74    // Frees the CPU-side copy for a from_data() texture — call once you've
75    // read whatever you needed via data(). A released texture upload()s as
76    // an empty texture on any future re-upload (e.g. after GPU backend
77    // loss) rather than its original contents — same as building it with
78    // empty() from the start, not an error, but not what it used to be
79    // either. No-op for a from_file()/empty() texture, which never held
80    // this data to begin with.
81    pub fn release_cpu_data(&mut self) {
82        self.data = None;
83    }
84}
85
86pub struct GPUTexture {
87    texture: wgpu::Texture,
88    view: wgpu::TextureView,
89    width: u32,
90    height: u32,
91    format: TextureFormat,
92    ctx: GpuContext,
93}
94
95impl GPUTexture {
96    pub fn write(&self, mip_level: u32, pixels: &[u8]) {
97        write_texture_mip(self.ctx.queue(), &self.texture, 0, mip_level, self.format.into(), self.width, self.height, pixels);
98    }
99
100    pub fn width(&self) -> u32 {
101        self.width
102    }
103
104    pub fn height(&self) -> u32 {
105        self.height
106    }
107
108    pub fn get_view(&self, mip_level: u32) -> TextureView {
109        let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
110            dimension: Some(wgpu::TextureViewDimension::D2),
111            base_mip_level: mip_level,
112            mip_level_count: Some(1),
113            ..Default::default()
114        });
115        TextureView::from_raw(view, self.texture.clone())
116    }
117
118    pub(crate) fn view(&self) -> &wgpu::TextureView {
119        &self.view
120    }
121}
122
123pub(crate) fn write_texture_mip(
124    queue: &wgpu::Queue,
125    texture: &wgpu::Texture,
126    origin_z: u32,
127    mip_level: u32,
128    format: wgpu::TextureFormat,
129    width: u32,
130    height: u32,
131    pixels: &[u8],
132) {
133    let width = (width >> mip_level).max(1);
134    let height = (height >> mip_level).max(1);
135    queue.write_texture(
136        wgpu::TexelCopyTextureInfo {
137            texture,
138            mip_level,
139            origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
140            aspect: wgpu::TextureAspect::All,
141        },
142        pixels,
143        wgpu::TexelCopyBufferLayout {
144            offset: 0,
145            bytes_per_row: Some(bytes_per_pixel(format) * width),
146            rows_per_image: Some(height),
147        },
148        wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
149    );
150}
151
152pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
153    use wgpu::TextureFormat as F;
154    match format {
155        F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
156        F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
157        | F::Rg8Uint | F::Rg8Sint => 2,
158        F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
159        | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
160        | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
161        | F::Rgb9e5Ufloat => 4,
162        F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
163        | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
164        F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
165        other => panic!(
166            "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
167             multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
168             this helper can compute"
169        ),
170    }
171}
172
173pub(crate) fn check_texture_dimensions(device: &wgpu::Device, what: &str, width: u32, height: u32) {
174    let max = device.limits().max_texture_dimension_2d;
175    if width > max || height > max {
176        panic!("{what}: {width}x{height} exceeds this device's max_texture_dimension_2d ({max})");
177    }
178}
179
180pub(crate) fn check_texture_array_layers(device: &wgpu::Device, what: &str, layer_count: u32) {
181    let max = device.limits().max_texture_array_layers;
182    if layer_count > max {
183        panic!("{what}: {layer_count} layers exceeds this device's max_texture_array_layers ({max})");
184    }
185}
186
187fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
188    rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
189}
190
191fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
192    rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
193}
194
195fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
196    rgba32f
197        .chunks_exact(4)
198        .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
199        .collect()
200}
201
202fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
203    rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
204}
205
206fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
207    rgba32f
208        .iter()
209        .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
210        .collect()
211}
212
213pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
214    use wgpu::TextureFormat as F;
215
216    let img = match image::open(path) {
217        Ok(img) => img,
218        Err(e) => {
219            tracing::error!("failed to load texture '{path}': {e}");
220            return None;
221        }
222    };
223
224    Some(match format {
225        F::Rgba8Unorm | F::Rgba8UnormSrgb => {
226            let img = img.to_rgba8();
227            let (w, h) = img.dimensions();
228            (w, h, img.into_raw())
229        }
230        F::Bgra8Unorm | F::Bgra8UnormSrgb => {
231            let img = img.to_rgba8();
232            let (w, h) = img.dimensions();
233            (w, h, bgra_swap(&img.into_raw()))
234        }
235        F::R8Unorm => {
236            let img = img.to_rgba8();
237            let (w, h) = img.dimensions();
238            (w, h, take_channels_u8(&img.into_raw(), 1))
239        }
240        F::Rg8Unorm => {
241            let img = img.to_rgba8();
242            let (w, h) = img.dimensions();
243            (w, h, take_channels_u8(&img.into_raw(), 2))
244        }
245        F::Rgba16Unorm => {
246            let img = img.to_rgba32f();
247            let (w, h) = img.dimensions();
248            (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
249        }
250        F::Rgba32Float => {
251            let img = img.to_rgba32f();
252            let (w, h) = img.dimensions();
253            let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
254            (w, h, bytes)
255        }
256        F::Rg32Float => {
257            let img = img.to_rgba32f();
258            let (w, h) = img.dimensions();
259            (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
260        }
261        F::R32Float => {
262            let img = img.to_rgba32f();
263            let (w, h) = img.dimensions();
264            (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
265        }
266        F::Rgba16Float => {
267            let img = img.to_rgba32f();
268            let (w, h) = img.dimensions();
269            (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
270        }
271        F::Rg16Float => {
272            let img = img.to_rgba32f();
273            let (w, h) = img.dimensions();
274            (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
275        }
276        F::R16Float => {
277            let img = img.to_rgba32f();
278            let (w, h) = img.dimensions();
279            (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
280        }
281        other => panic!(
282            "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
283             regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
284             formats aren't decodable from an ordinary image file this way"
285        ),
286    })
287}
288
289impl AssetSource for Texture {
290    type Processed = GPUTexture;
291}
292
293impl Asset<Backend> for Texture {
294    type Deps<'a> = Read<'a, MipmapGenerator>;
295
296    fn upload<'a>(&self, backend: &Backend, mipmap_generator: &Read<'a, MipmapGenerator>) -> Option<GPUTexture> {
297        let (width, height, data) = if let Some(path) = self.file {
298            let (w, h, d) = decode_file(path, self.format.into())?;
299            (w, h, Some(d))
300        } else if let Some(data) = &self.data {
301            (self.width, self.height, Some(data.clone()))
302        } else {
303            (self.width, self.height, None)
304        };
305
306        check_texture_dimensions(&backend.device, "GPUTexture", width, height);
307
308        let mip_count = crate::graphics::pipeline::mipmap::mip_count(width.max(height), self.mip_levels);
309        let usage = crate::graphics::pipeline::mipmap::texture_usage_for(mip_count, data.is_some());
310
311        let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
312            label: None,
313            size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
314            mip_level_count: mip_count,
315            sample_count: 1,
316            dimension: wgpu::TextureDimension::D2,
317            format: self.format.into(),
318            usage,
319            view_formats: &[],
320        });
321
322        if let Some(data) = &data {
323            write_texture_mip(&backend.queue, &texture, 0, 0, self.format.into(), width, height, data);
324
325            if mip_count > 1 {
326                mipmap_generator.generate_mips(backend, &texture, self.format.into(), mip_count, 1);
327            }
328        }
329
330        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
331        Some(GPUTexture { texture, view, width, height, format: self.format, ctx: GpuContext::from_backend(backend) })
332    }
333}