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