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