Skip to main content

pebble/wgpu/
texture_array.rs

1use crate::{
2    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3    ecs::system::Res,
4    wgpu::{
5        backend::WGPUBackend,
6        gpu_context::GpuContext,
7        mipmap::MipmapGenerator,
8        texture_format::TextureFormat,
9        textures::{bytes_per_pixel, check_texture_array_layers, check_texture_dimensions, decode_file, write_texture_level0},
10    },
11};
12
13/// Source data for [`GPUTextureArray`]. Fields are private — the only way to
14/// construct one is [`TextureArrayBuilder`]: `TextureArrayBuilder::from_files(...).build()`.
15pub struct TextureArray {
16    /// One file path per layer. Every layer must decode to the same
17    /// `width`/`height`.
18    files: Option<Vec<&'static str>>,
19    /// Width in pixels. Ignored when loading from `files`.
20    width: u32,
21    /// Height in pixels. Ignored when loading from `files`.
22    height: u32,
23    /// GPU pixel format to upload as. Defaults to `Rgba8UnormSrgb`.
24    format: TextureFormat,
25    /// Raw pixel bytes per layer, used when `files` is `None`.
26    data: Option<Vec<Vec<u8>>>,
27    /// Whether to generate a full mip chain (via [`MipmapGenerator`]).
28    generate_mips: bool,
29    /// Number of layers to allocate. Only used when both `files` and `data`
30    /// are `None` (i.e. [`empty`](TextureArrayBuilder::empty)); otherwise
31    /// layer count is derived from the length of `files`/`data`.
32    layer_count: u32,
33}
34
35/// Builds a [`TextureArray`] — load one layer per file, supply raw bytes per
36/// layer directly, or allocate an empty array with no initial data. Start
37/// from [`from_files`](Self::from_files)/[`from_data`](Self::from_data)/[`empty`](Self::empty),
38/// chain the setters below, then finish with
39/// [`build`](Self::build)/[`build_asset`](Self::build_asset).
40pub struct TextureArrayBuilder {
41    files: Option<Vec<&'static str>>,
42    width: u32,
43    height: u32,
44    format: TextureFormat,
45    data: Option<Vec<Vec<u8>>>,
46    generate_mips: bool,
47    layer_count: u32,
48}
49
50impl TextureArrayBuilder {
51    /// Load one layer per file. Width/height are inferred from the first
52    /// file and every subsequent layer must match.
53    pub fn from_files(files: Vec<&'static str>) -> Self {
54        Self {
55            files: Some(files),
56            width: 0,
57            height: 0,
58            format: TextureFormat::Rgba8UnormSrgb,
59            data: None,
60            generate_mips: false,
61            layer_count: 0,
62        }
63    }
64
65    /// Supply raw pixel bytes per layer directly, matching `width`/`height`/`format`.
66    pub fn from_data(width: u32, height: u32, format: TextureFormat, layers: Vec<Vec<u8>>) -> Self {
67        Self {
68            files: None,
69            width,
70            height,
71            format,
72            data: Some(layers),
73            generate_mips: false,
74            layer_count: 0,
75        }
76    }
77
78    /// Allocate an empty texture array on the GPU with no initial pixel data.
79    /// Content is undefined until written via [`GPUTextureArray::write_layer`].
80    pub fn empty(width: u32, height: u32, format: TextureFormat, layer_count: u32) -> Self {
81        Self {
82            files: None,
83            width,
84            height,
85            format,
86            data: None,
87            generate_mips: false,
88            layer_count,
89        }
90    }
91
92    pub fn with_format(mut self, format: TextureFormat) -> Self {
93        self.format = format;
94        self
95    }
96
97    pub fn with_mips(mut self) -> Self {
98        self.generate_mips = true;
99        self
100    }
101
102    /// Logs a WARN if [`from_data`](Self::from_data) was given a zero
103    /// width/height — same rationale as the equivalent check on
104    /// [`TextureBuilder`](super::textures::TextureBuilder).
105    fn validate(&self) {
106        if self.data.is_some() && (self.width == 0 || self.height == 0) {
107            tracing::warn!(
108                "TextureArrayBuilder::from_data(): width/height is 0 ({}x{}) — did you swap the \
109                 argument order, or forget to pass the real dimensions?",
110                self.width,
111                self.height,
112            );
113        }
114    }
115
116    /// Consume the builder and return the finished [`TextureArray`] value.
117    pub fn build(self) -> TextureArray {
118        self.validate();
119        TextureArray {
120            files: self.files,
121            width: self.width,
122            height: self.height,
123            format: self.format,
124            data: self.data,
125            generate_mips: self.generate_mips,
126            layer_count: self.layer_count,
127        }
128    }
129
130    /// Consume the builder, insert into `assets` under `name`, and return
131    /// the resulting [`Handle<TextureArray>`].
132    pub fn build_asset(self, name: &str, assets: &mut Assets<TextureArray>) -> Handle<TextureArray> {
133        let texture_array = self.build();
134        assets.insert(name, texture_array)
135    }
136}
137
138/// A 2D texture array uploaded to the GPU, ready to bind (e.g. via
139/// [`BindingInstanceEntry::TextureArray`](super::instance::BindingInstanceEntry::TextureArray)).
140/// Opaque — bind it via
141/// [`BindGroupBuilder::texture_array`](super::buffers::BindGroupBuilder::texture_array).
142pub struct GPUTextureArray {
143    texture: wgpu::Texture,
144    view: wgpu::TextureView,
145    layer_count: u32,
146    width: u32,
147    height: u32,
148    format: TextureFormat,
149    ctx: GpuContext,
150}
151
152impl GPUTextureArray {
153    /// Overwrites one layer's level-0 pixel data. See
154    /// [`GPUTexture::write`](super::textures::GPUTexture::write) for the
155    /// same caveat about mip levels not being regenerated.
156    pub fn write_layer(&self, layer: u32, pixels: &[u8]) {
157        write_texture_level0(self.ctx.queue(), &self.texture, layer, self.format.into(), self.width, self.height, pixels);
158    }
159
160    pub fn layer_count(&self) -> u32 {
161        self.layer_count
162    }
163
164    pub fn width(&self) -> u32 {
165        self.width
166    }
167
168    pub fn height(&self) -> u32 {
169        self.height
170    }
171
172    pub(crate) fn view(&self) -> &wgpu::TextureView {
173        &self.view
174    }
175}
176
177impl AssetSource for TextureArray {
178    type Processed = GPUTextureArray;
179}
180
181impl Asset<WGPUBackend> for TextureArray {
182    type Deps<'a> = Res<'a, MipmapGenerator>;
183
184    fn upload<'a>(
185        &self,
186        backend: &WGPUBackend,
187        mipmap_generator: &Res<'a, MipmapGenerator>,
188    ) -> Option<GPUTextureArray> {
189        let (width, height, layer_count, layers): (u32, u32, u32, Option<Vec<Vec<u8>>>) =
190            if let Some(files) = &self.files {
191                let mut width = self.width;
192                let mut height = self.height;
193                let mut layers = Vec::with_capacity(files.len());
194                for (i, path) in files.iter().enumerate() {
195                    let (w, h, data) = decode_file(path, self.format.into())?;
196                    if i == 0 {
197                        width = w;
198                        height = h;
199                    } else if w != width || h != height {
200                        tracing::error!(
201                            "TextureArraySpec: layer {i} ('{path}') is {w}x{h}, expected {width}x{height}"
202                        );
203                        return None;
204                    }
205                    layers.push(data);
206                }
207                let count = layers.len() as u32;
208                (width, height, count, Some(layers))
209            } else if let Some(data) = &self.data {
210                let count = data.len() as u32;
211                (self.width, self.height, count, Some(data.clone()))
212            } else {
213                // empty array — no initial data, content is undefined until written
214                (self.width, self.height, self.layer_count, None)
215            };
216
217        if layer_count == 0 {
218            tracing::error!("TextureArraySpec resolved to zero layers");
219            return None;
220        }
221
222        check_texture_dimensions(&backend.device, "GPUTextureArray", width, height);
223        check_texture_array_layers(&backend.device, "GPUTextureArray", layer_count);
224
225        let mip_count = super::mipmap::mip_count(width.max(height), self.generate_mips);
226
227        let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
228            label: None,
229            size: wgpu::Extent3d {
230                width,
231                height,
232                depth_or_array_layers: layer_count,
233            },
234            mip_level_count: mip_count,
235            sample_count: 1,
236            dimension: wgpu::TextureDimension::D2,
237            format: self.format.into(),
238            usage: super::mipmap::texture_usage(mip_count),
239            view_formats: &[],
240        });
241
242        if let Some(layers) = &layers {
243            for (layer, data) in layers.iter().enumerate() {
244                backend.queue.write_texture(
245                    wgpu::TexelCopyTextureInfo {
246                        texture: &texture,
247                        mip_level: 0,
248                        origin: wgpu::Origin3d {
249                            x: 0,
250                            y: 0,
251                            z: layer as u32,
252                        },
253                        aspect: wgpu::TextureAspect::All,
254                    },
255                    data,
256                    wgpu::TexelCopyBufferLayout {
257                        offset: 0,
258                        bytes_per_row: Some(bytes_per_pixel(self.format.into()) * width),
259                        rows_per_image: Some(height),
260                    },
261                    wgpu::Extent3d {
262                        width,
263                        height,
264                        depth_or_array_layers: 1,
265                    },
266                );
267            }
268
269            if mip_count > 1 {
270                mipmap_generator.generate_mips(
271                    &backend.device,
272                    &backend.queue,
273                    &texture,
274                    self.format.into(),
275                    mip_count,
276                    layer_count,
277                );
278            }
279        }
280
281        let view = texture.create_view(&wgpu::TextureViewDescriptor {
282            dimension: Some(wgpu::TextureViewDimension::D2Array),
283            ..Default::default()
284        });
285        Some(GPUTextureArray {
286            texture,
287            view,
288            layer_count,
289            width,
290            height,
291            format: self.format,
292            ctx: GpuContext::from_backend(backend),
293        })
294    }
295}
296
297crate::wgpu::plugin_macros::mipmap_asset_plugin! {
298    /// Registers the [`GPUTextureArray`] asset pipeline (`Assets<TextureArray>`),
299    /// plus the [`MipmapGenerator`] it depends on for `generate_mips`. Included
300    /// by [`WGPUPlugin`](super::backend::WGPUPlugin); add directly only if
301    /// you're assembling the `wgpu` module's plugins by hand.
302    TextureArrayPlugin, TextureArray
303}