Skip to main content

game_toolkit_gfx/
texture.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
7pub struct TextureId(pub u32);
8
9/// What a texture shows for UVs outside `0..1`. Set per texture with
10/// [`crate::Graphics::set_texture_wrap`].
11#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
12pub enum TextureWrap {
13    /// Repeat the edge texels (default). Right for sprites and atlas regions, which sample up
14    /// to their edges and would bleed in the opposite edge with wrapping.
15    #[default]
16    Clamp,
17    /// Tile the texture: a UV of `2.5` samples the same texel as `0.5`.
18    Repeat,
19    /// Tile, mirroring every other repetition so the seams match.
20    MirrorRepeat,
21}
22
23impl TextureWrap {
24    const ALL: [Self; 3] = [Self::Clamp, Self::Repeat, Self::MirrorRepeat];
25
26    fn address_mode(self) -> wgpu::AddressMode {
27        match self {
28            Self::Clamp => wgpu::AddressMode::ClampToEdge,
29            Self::Repeat => wgpu::AddressMode::Repeat,
30            Self::MirrorRepeat => wgpu::AddressMode::MirrorRepeat,
31        }
32    }
33
34    /// Position in [`TextureWrap::ALL`] and the registry's sampler array.
35    fn index(self) -> usize {
36        match self {
37            Self::Clamp => 0,
38            Self::Repeat => 1,
39            Self::MirrorRepeat => 2,
40        }
41    }
42}
43
44pub struct Texture {
45    pub size: [u32; 2],
46    pub(crate) bind_group: wgpu::BindGroup,
47    #[allow(dead_code)]
48    pub(crate) texture: wgpu::Texture,
49    pub(crate) view: wgpu::TextureView,
50    pub(crate) wrap: TextureWrap,
51    /// Debug label, reapplied when the bind group is rebuilt for a new wrap mode.
52    pub(crate) label: Option<String>,
53}
54
55pub(crate) struct TextureRegistry {
56    map: HashMap<TextureId, Texture>,
57    next: u32,
58    pub(crate) layout: wgpu::BindGroupLayout,
59    /// One sampler per [`TextureWrap`], indexed by [`TextureWrap::index`].
60    samplers: [wgpu::Sampler; 3],
61    white: TextureId,
62}
63
64impl TextureRegistry {
65    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
66        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
67            label: Some("sprite.texture_bgl"),
68            entries: &[
69                wgpu::BindGroupLayoutEntry {
70                    binding: 0,
71                    visibility: wgpu::ShaderStages::FRAGMENT,
72                    ty: wgpu::BindingType::Texture {
73                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
74                        view_dimension: wgpu::TextureViewDimension::D2,
75                        multisampled: false,
76                    },
77                    count: None,
78                },
79                wgpu::BindGroupLayoutEntry {
80                    binding: 1,
81                    visibility: wgpu::ShaderStages::FRAGMENT,
82                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
83                    count: None,
84                },
85            ],
86        });
87
88        let samplers = TextureWrap::ALL.map(|wrap| {
89            let mode = wrap.address_mode();
90            device.create_sampler(&wgpu::SamplerDescriptor {
91                label: Some("sprite.sampler"),
92                address_mode_u: mode,
93                address_mode_v: mode,
94                address_mode_w: mode,
95                mag_filter: wgpu::FilterMode::Nearest,
96                min_filter: wgpu::FilterMode::Nearest,
97                mipmap_filter: wgpu::MipmapFilterMode::Nearest,
98                ..Default::default()
99            })
100        });
101
102        let mut me = Self {
103            map: HashMap::new(),
104            next: 1,
105            layout,
106            samplers,
107            white: TextureId(0),
108        };
109        me.white = me.create_from_rgba(device, queue, 1, 1, &[255, 255, 255, 255], Some("white"));
110        me
111    }
112
113    pub fn white(&self) -> TextureId {
114        self.white
115    }
116
117    pub fn bind_group(&self, id: TextureId) -> &wgpu::BindGroup {
118        &self
119            .map
120            .get(&id)
121            .unwrap_or_else(|| self.map.get(&self.white).expect("white texture"))
122            .bind_group
123    }
124
125    pub fn get(&self, id: TextureId) -> Option<&Texture> {
126        self.map.get(&id)
127    }
128
129    /// Switch `id` to `wrap` by rebuilding its bind group with the matching sampler. Returns
130    /// `false` for an unknown id.
131    pub fn set_wrap(&mut self, device: &wgpu::Device, id: TextureId, wrap: TextureWrap) -> bool {
132        let Some(tex) = self.map.get(&id) else {
133            return false;
134        };
135        if tex.wrap != wrap {
136            let bind_group = self.create_bind_group(device, &tex.view, wrap, tex.label.as_deref());
137            let tex = self.map.get_mut(&id).expect("checked above");
138            tex.bind_group = bind_group;
139            tex.wrap = wrap;
140        }
141        true
142    }
143
144    fn create_bind_group(
145        &self,
146        device: &wgpu::Device,
147        view: &wgpu::TextureView,
148        wrap: TextureWrap,
149        label: Option<&str>,
150    ) -> wgpu::BindGroup {
151        device.create_bind_group(&wgpu::BindGroupDescriptor {
152            label,
153            layout: &self.layout,
154            entries: &[
155                wgpu::BindGroupEntry {
156                    binding: 0,
157                    resource: wgpu::BindingResource::TextureView(view),
158                },
159                wgpu::BindGroupEntry {
160                    binding: 1,
161                    resource: wgpu::BindingResource::Sampler(&self.samplers[wrap.index()]),
162                },
163            ],
164        })
165    }
166
167    #[cfg(not(target_arch = "wasm32"))]
168    pub fn load_file(
169        &mut self,
170        device: &wgpu::Device,
171        queue: &wgpu::Queue,
172        path: impl AsRef<Path>,
173    ) -> Result<TextureId> {
174        let path = path.as_ref();
175        let img = image::open(path).with_context(|| format!("loading {}", path.display()))?;
176        let rgba = img.to_rgba8();
177        let (w, h) = rgba.dimensions();
178        Ok(self.create_from_rgba(
179            device,
180            queue,
181            w,
182            h,
183            rgba.as_raw(),
184            path.file_name().and_then(|s| s.to_str()),
185        ))
186    }
187
188    /// Decode an encoded image (PNG) held in memory and upload it.
189    pub fn load_bytes(
190        &mut self,
191        device: &wgpu::Device,
192        queue: &wgpu::Queue,
193        bytes: &[u8],
194        label: Option<&str>,
195    ) -> Result<TextureId> {
196        let img = image::load_from_memory(bytes)
197            .with_context(|| format!("decoding {}", label.unwrap_or("image")))?;
198        let rgba = img.to_rgba8();
199        let (w, h) = rgba.dimensions();
200        Ok(self.create_from_rgba(device, queue, w, h, rgba.as_raw(), label))
201    }
202
203    /// Replace the contents of `id` with a freshly decoded copy of `path`. Keeps the same
204    /// `TextureId` so anything already holding it stays valid.
205    pub fn reload(
206        &mut self,
207        device: &wgpu::Device,
208        queue: &wgpu::Queue,
209        id: TextureId,
210        path: impl AsRef<Path>,
211    ) -> Result<()> {
212        let path = path.as_ref();
213        let img = image::open(path).with_context(|| format!("reloading {}", path.display()))?;
214        let rgba = img.to_rgba8();
215        let (w, h) = rgba.dimensions();
216        self.replace(
217            device,
218            queue,
219            id,
220            w,
221            h,
222            rgba.as_raw(),
223            path.file_name().and_then(|s| s.to_str()),
224        );
225        Ok(())
226    }
227
228    #[allow(clippy::too_many_arguments)]
229    pub fn replace(
230        &mut self,
231        device: &wgpu::Device,
232        queue: &wgpu::Queue,
233        id: TextureId,
234        width: u32,
235        height: u32,
236        rgba: &[u8],
237        label: Option<&str>,
238    ) {
239        let wrap = self.map.get(&id).map(|t| t.wrap).unwrap_or_default();
240        let new_id = self.create_from_rgba(device, queue, width, height, rgba, label);
241        // Keep the wrap mode across hot-reloads.
242        self.set_wrap(device, new_id, wrap);
243        if let Some(new_tex) = self.map.remove(&new_id) {
244            self.map.insert(id, new_tex);
245        }
246    }
247
248    pub fn create_from_rgba(
249        &mut self,
250        device: &wgpu::Device,
251        queue: &wgpu::Queue,
252        width: u32,
253        height: u32,
254        rgba: &[u8],
255        label: Option<&str>,
256    ) -> TextureId {
257        let size = wgpu::Extent3d {
258            width,
259            height,
260            depth_or_array_layers: 1,
261        };
262        let texture = device.create_texture(&wgpu::TextureDescriptor {
263            label,
264            size,
265            mip_level_count: 1,
266            sample_count: 1,
267            dimension: wgpu::TextureDimension::D2,
268            format: wgpu::TextureFormat::Rgba8UnormSrgb,
269            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
270            view_formats: &[],
271        });
272        queue.write_texture(
273            wgpu::TexelCopyTextureInfo {
274                texture: &texture,
275                mip_level: 0,
276                origin: wgpu::Origin3d::ZERO,
277                aspect: wgpu::TextureAspect::All,
278            },
279            rgba,
280            wgpu::TexelCopyBufferLayout {
281                offset: 0,
282                bytes_per_row: Some(4 * width),
283                rows_per_image: Some(height),
284            },
285            size,
286        );
287        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
288        let bind_group = self.create_bind_group(device, &view, TextureWrap::Clamp, label);
289
290        let id = TextureId(self.next);
291        self.next += 1;
292        self.map.insert(
293            id,
294            Texture {
295                size: [width, height],
296                bind_group,
297                texture,
298                view,
299                wrap: TextureWrap::Clamp,
300                label: label.map(str::to_owned),
301            },
302        );
303        id
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn index_matches_position_in_all() {
313        for (i, wrap) in TextureWrap::ALL.into_iter().enumerate() {
314            assert_eq!(wrap.index(), i);
315        }
316    }
317}