Skip to main content

pebble/wgpu/
texture_array.rs

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