Skip to main content

pebble/wgpu/
texture_array.rs

1use crate::{
2    assets::upload::Asset,
3    ecs::system::Res,
4    wgpu::{
5        backend::WGPUBackend,
6        mipmap::MipmapGenerator,
7        textures::{bytes_per_pixel, decode_file},
8    },
9};
10
11/// Source data for [`GPUTextureArray`]. Prefer the
12/// [`from_files`](Self::from_files)/[`from_data`](Self::from_data)
13/// constructors over setting fields by hand.
14pub struct TextureArrayDescriptor {
15    /// One file path per layer. Every layer must decode to the same
16    /// `width`/`height`.
17    pub files: Option<Vec<&'static str>>,
18    /// Width in pixels. Ignored when loading from `files`.
19    pub width: u32,
20    /// Height in pixels. Ignored when loading from `files`.
21    pub height: u32,
22    /// GPU pixel format to upload as. Defaults to `Rgba8UnormSrgb`.
23    pub format: wgpu::TextureFormat,
24    /// Raw pixel bytes per layer, used when `files` is `None`.
25    pub data: Option<Vec<Vec<u8>>>,
26    /// Whether to generate a full mip chain (via [`MipmapGenerator`]).
27    pub generate_mips: bool,
28}
29
30impl TextureArrayDescriptor {
31    /// Load one layer per file. Width/height are inferred from the first
32    /// file and every subsequent layer must match.
33    pub fn from_files(files: Vec<&'static str>) -> Self {
34        Self {
35            files: Some(files),
36            width: 0,
37            height: 0,
38            format: wgpu::TextureFormat::Rgba8UnormSrgb,
39            data: None,
40            generate_mips: false,
41        }
42    }
43
44    /// Supply raw pixel bytes per layer directly, matching `width`/`height`/`format`.
45    pub fn from_data(width: u32, height: u32, format: wgpu::TextureFormat, layers: Vec<Vec<u8>>) -> Self {
46        Self {
47            files: None,
48            width,
49            height,
50            format,
51            data: Some(layers),
52            generate_mips: false,
53        }
54    }
55
56    pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
57        self.format = format;
58        self
59    }
60
61    pub fn with_mips(mut self) -> Self {
62        self.generate_mips = true;
63        self
64    }
65}
66
67/// A 2D texture array uploaded to the GPU, ready to bind (e.g. via
68/// [`BindingInstanceEntry::TextureArray`](super::instance::BindingInstanceEntry::TextureArray)).
69pub struct GPUTextureArray {
70    pub texture: wgpu::Texture,
71    pub view: wgpu::TextureView,
72    pub layer_count: u32,
73}
74
75impl Asset<WGPUBackend> for GPUTextureArray {
76    type Source = TextureArrayDescriptor;
77    type Deps<'a> = Res<'a, MipmapGenerator>;
78
79    fn upload<'a>(
80        source: &TextureArrayDescriptor,
81        backend: &WGPUBackend,
82        mipmap_generator: &Res<'a, MipmapGenerator>,
83    ) -> Option<Self> {
84        let (width, height, layers): (u32, u32, Vec<Vec<u8>>) = if let Some(files) = &source.files {
85            let mut width = source.width;
86            let mut height = source.height;
87            let mut layers = Vec::with_capacity(files.len());
88            for (i, path) in files.iter().enumerate() {
89                let (w, h, data) = decode_file(path, source.format)?;
90                if i == 0 {
91                    width = w;
92                    height = h;
93                } else if w != width || h != height {
94                    tracing::error!(
95                        "TextureArraySpec: layer {i} ('{path}') is {w}x{h}, expected {width}x{height}"
96                    );
97                    return None;
98                }
99                layers.push(data);
100            }
101            (width, height, layers)
102        } else if let Some(data) = &source.data {
103            (source.width, source.height, data.clone())
104        } else {
105            tracing::error!("TextureArraySpec has neither `files` nor `data` set");
106            return None;
107        };
108
109        if layers.is_empty() {
110            tracing::error!("TextureArraySpec resolved to zero layers");
111            return None;
112        }
113        let layer_count = layers.len() as u32;
114
115        let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
116
117        let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
118            label: None,
119            size: wgpu::Extent3d {
120                width,
121                height,
122                depth_or_array_layers: layer_count,
123            },
124            mip_level_count: mip_count,
125            sample_count: 1,
126            dimension: wgpu::TextureDimension::D2,
127            format: source.format,
128            usage: super::mipmap::texture_usage(mip_count),
129            view_formats: &[],
130        });
131
132        for (layer, data) in layers.iter().enumerate() {
133            backend.queue.write_texture(
134                wgpu::TexelCopyTextureInfo {
135                    texture: &texture,
136                    mip_level: 0,
137                    origin: wgpu::Origin3d {
138                        x: 0,
139                        y: 0,
140                        z: layer as u32,
141                    },
142                    aspect: wgpu::TextureAspect::All,
143                },
144                data,
145                wgpu::TexelCopyBufferLayout {
146                    offset: 0,
147                    bytes_per_row: Some(bytes_per_pixel(source.format) * width),
148                    rows_per_image: Some(height),
149                },
150                wgpu::Extent3d {
151                    width,
152                    height,
153                    depth_or_array_layers: 1,
154                },
155            );
156        }
157
158        if mip_count > 1 {
159            mipmap_generator.generate_mips(
160                &backend.device,
161                &backend.queue,
162                &texture,
163                source.format,
164                mip_count,
165                layer_count,
166            );
167        }
168
169        let view = texture.create_view(&wgpu::TextureViewDescriptor {
170            dimension: Some(wgpu::TextureViewDimension::D2Array),
171            ..Default::default()
172        });
173        Some(Self {
174            texture,
175            view,
176            layer_count,
177        })
178    }
179}
180
181crate::wgpu::plugin_macros::mipmap_asset_plugin! {
182    /// Registers the [`GPUTextureArray`] asset pipeline
183    /// (`Assets<TextureArrayDescriptor>` → `ProcessedAssets<GPUTextureArray>`),
184    /// plus the [`MipmapGenerator`] it depends on for `generate_mips`. Included
185    /// by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if
186    /// you're assembling the `wgpu` module's plugins by hand.
187    TextureArrayPlugin, GPUTextureArray
188}