Skip to main content

pebble/graphics/pipeline/
texture_array.rs

1use crate::{
2    assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3    ecs::resources::Read,
4    graphics::{
5        pipeline::{
6            mipmap::{MipLevels, MipmapGenerator},
7            texture_view::TextureView,
8            textures::{bytes_per_pixel, check_texture_array_layers, check_texture_dimensions, decode_file},
9        },
10        render::{Backend, gpu_context::GpuContext},
11        types::TextureFormat,
12    },
13};
14
15/// A 2D array texture asset — same construction pattern as [`Texture`](super::textures::Texture),
16/// but each layer's pixels come from a separate file/buffer. All layers must
17/// share the same dimensions.
18pub struct TextureArray {
19    files: Option<Vec<&'static str>>,
20    width: u32,
21    height: u32,
22    format: TextureFormat,
23    data: Option<Vec<Vec<u8>>>,
24    layer_count: u32,
25    mip_levels: MipLevels,
26}
27
28impl TextureArray {
29    pub fn from_files(files: Vec<&'static str>) -> Self {
30        Self { files: Some(files), width: 0, height: 0, format: TextureFormat::Rgba8UnormSrgb, data: None, layer_count: 0, mip_levels: MipLevels::None }
31    }
32
33    pub fn from_data(width: u32, height: u32, format: TextureFormat, layers: Vec<Vec<u8>>) -> Self {
34        Self { files: None, width, height, format, data: Some(layers), layer_count: 0, mip_levels: MipLevels::None }
35    }
36
37    /// No source data — a render target, or something you'll [`write_layer`](GPUTextureArray::write_layer) yourself.
38    pub fn empty(width: u32, height: u32, format: TextureFormat, layer_count: u32) -> Self {
39        Self { files: None, width, height, format, data: None, layer_count, mip_levels: MipLevels::None }
40    }
41
42    pub fn with_format(mut self, format: TextureFormat) -> Self {
43        self.format = format;
44        self
45    }
46
47    pub fn with_mips(mut self) -> Self {
48        self.mip_levels = MipLevels::Full;
49        self
50    }
51
52    pub fn with_mip_count(mut self, count: u32) -> Self {
53        self.mip_levels = MipLevels::Fixed(count);
54        self
55    }
56
57    fn validate(&self) {
58        if self.data.is_some() && (self.width == 0 || self.height == 0) {
59            tracing::warn!(
60                "TextureArray::from_data(): width/height is 0 ({}x{}) — did you swap the \
61                 argument order, or forget to pass the real dimensions?",
62                self.width,
63                self.height,
64            );
65        }
66    }
67
68    pub fn build_asset(self, name: &str, assets: &mut Assets<TextureArray>) -> Handle<TextureArray> {
69        self.validate();
70        assets.insert(name, self)
71    }
72
73    /// CPU-side layers — only ever `Some` for a `from_data()` array. See
74    /// [`Texture::data`](super::textures::Texture::data).
75    pub fn data(&self) -> Option<&[Vec<u8>]> {
76        self.data.as_deref()
77    }
78
79    /// Frees the CPU-side copy. See
80    /// [`Texture::release_cpu_data`](super::textures::Texture::release_cpu_data).
81    pub fn release_cpu_data(&mut self) {
82        self.data = None;
83    }
84}
85
86/// The GPU-resident array texture an uploaded [`TextureArray`] produces.
87pub struct GPUTextureArray {
88    texture: wgpu::Texture,
89    view: wgpu::TextureView,
90    layer_count: u32,
91    width: u32,
92    height: u32,
93    format: TextureFormat,
94    ctx: GpuContext,
95}
96
97impl GPUTextureArray {
98    /// Overwrites one mip level of one layer with new pixel data.
99    pub fn write_layer(&self, layer: u32, mip_level: u32, pixels: &[u8]) {
100        crate::graphics::pipeline::textures::write_texture_mip(
101            self.ctx.queue(),
102            &self.texture,
103            layer,
104            mip_level,
105            self.format.into(),
106            self.width,
107            self.height,
108            pixels,
109        );
110    }
111
112    pub fn layer_count(&self) -> u32 {
113        self.layer_count
114    }
115
116    pub fn width(&self) -> u32 {
117        self.width
118    }
119
120    pub fn height(&self) -> u32 {
121        self.height
122    }
123
124    /// A view into a single layer and mip level.
125    pub fn get_view(&self, layer: u32, mip_level: u32) -> TextureView {
126        assert!(layer < self.layer_count, "GPUTextureArray::get_view: layer {layer} out of range (0..{})", self.layer_count);
127        let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
128            dimension: Some(wgpu::TextureViewDimension::D2),
129            base_mip_level: mip_level,
130            mip_level_count: Some(1),
131            base_array_layer: layer,
132            array_layer_count: Some(1),
133            ..Default::default()
134        });
135        TextureView::from_raw(view, self.texture.clone())
136    }
137
138    pub(crate) fn view(&self) -> &wgpu::TextureView {
139        &self.view
140    }
141}
142
143impl AssetSource for TextureArray {
144    type Processed = GPUTextureArray;
145}
146
147impl Asset<Backend> for TextureArray {
148    type Deps<'a> = Read<'a, MipmapGenerator>;
149
150    fn upload<'a>(&self, backend: &Backend, mipmap_generator: &Read<'a, MipmapGenerator>) -> Option<GPUTextureArray> {
151        let (width, height, layer_count, layers): (u32, u32, u32, Option<Vec<Vec<u8>>>) =
152            if let Some(files) = &self.files {
153                let mut width = self.width;
154                let mut height = self.height;
155                let mut layers = Vec::with_capacity(files.len());
156                for (i, path) in files.iter().enumerate() {
157                    let (w, h, data) = decode_file(path, self.format.into())?;
158                    if i == 0 {
159                        width = w;
160                        height = h;
161                    } else if w != width || h != height {
162                        tracing::error!(
163                            "TextureArraySpec: layer {i} ('{path}') is {w}x{h}, expected {width}x{height}"
164                        );
165                        return None;
166                    }
167                    layers.push(data);
168                }
169                let count = layers.len() as u32;
170                (width, height, count, Some(layers))
171            } else if let Some(data) = &self.data {
172                let count = data.len() as u32;
173                (self.width, self.height, count, Some(data.clone()))
174            } else {
175                (self.width, self.height, self.layer_count, None)
176            };
177
178        if layer_count == 0 {
179            tracing::error!("TextureArraySpec resolved to zero layers");
180            return None;
181        }
182
183        check_texture_dimensions(&backend.device, "GPUTextureArray", width, height);
184        check_texture_array_layers(&backend.device, "GPUTextureArray", layer_count);
185
186        let mip_count = crate::graphics::pipeline::mipmap::mip_count(width.max(height), self.mip_levels);
187        let usage = crate::graphics::pipeline::mipmap::texture_usage_for(mip_count, layers.is_some());
188
189        let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
190            label: None,
191            size: wgpu::Extent3d { width, height, depth_or_array_layers: layer_count },
192            mip_level_count: mip_count,
193            sample_count: 1,
194            dimension: wgpu::TextureDimension::D2,
195            format: self.format.into(),
196            usage,
197            view_formats: &[],
198        });
199
200        if let Some(layers) = &layers {
201            for (layer, data) in layers.iter().enumerate() {
202                backend.queue.write_texture(
203                    wgpu::TexelCopyTextureInfo {
204                        texture: &texture,
205                        mip_level: 0,
206                        origin: wgpu::Origin3d { x: 0, y: 0, z: layer as u32 },
207                        aspect: wgpu::TextureAspect::All,
208                    },
209                    data,
210                    wgpu::TexelCopyBufferLayout {
211                        offset: 0,
212                        bytes_per_row: Some(bytes_per_pixel(self.format.into()) * width),
213                        rows_per_image: Some(height),
214                    },
215                    wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
216                );
217            }
218
219            if mip_count > 1 {
220                mipmap_generator.generate_mips(backend, &texture, self.format.into(), mip_count, layer_count);
221            }
222        }
223
224        let view = texture.create_view(&wgpu::TextureViewDescriptor {
225            dimension: Some(wgpu::TextureViewDimension::D2Array),
226            ..Default::default()
227        });
228        Some(GPUTextureArray { texture, view, layer_count, width, height, format: self.format, ctx: GpuContext::from_backend(backend) })
229    }
230}