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
9pub struct Texture {
10    pub size: [u32; 2],
11    pub(crate) bind_group: wgpu::BindGroup,
12    #[allow(dead_code)]
13    pub(crate) texture: wgpu::Texture,
14    #[allow(dead_code)]
15    pub(crate) view: wgpu::TextureView,
16}
17
18pub(crate) struct TextureRegistry {
19    map: HashMap<TextureId, Texture>,
20    next: u32,
21    pub(crate) layout: wgpu::BindGroupLayout,
22    sampler: wgpu::Sampler,
23    white: TextureId,
24}
25
26impl TextureRegistry {
27    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
28        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
29            label: Some("sprite.texture_bgl"),
30            entries: &[
31                wgpu::BindGroupLayoutEntry {
32                    binding: 0,
33                    visibility: wgpu::ShaderStages::FRAGMENT,
34                    ty: wgpu::BindingType::Texture {
35                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
36                        view_dimension: wgpu::TextureViewDimension::D2,
37                        multisampled: false,
38                    },
39                    count: None,
40                },
41                wgpu::BindGroupLayoutEntry {
42                    binding: 1,
43                    visibility: wgpu::ShaderStages::FRAGMENT,
44                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
45                    count: None,
46                },
47            ],
48        });
49
50        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
51            label: Some("sprite.sampler"),
52            address_mode_u: wgpu::AddressMode::ClampToEdge,
53            address_mode_v: wgpu::AddressMode::ClampToEdge,
54            address_mode_w: wgpu::AddressMode::ClampToEdge,
55            mag_filter: wgpu::FilterMode::Nearest,
56            min_filter: wgpu::FilterMode::Nearest,
57            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
58            ..Default::default()
59        });
60
61        let mut me = Self {
62            map: HashMap::new(),
63            next: 1,
64            layout,
65            sampler,
66            white: TextureId(0),
67        };
68        me.white = me.create_from_rgba(device, queue, 1, 1, &[255, 255, 255, 255], Some("white"));
69        me
70    }
71
72    pub fn white(&self) -> TextureId {
73        self.white
74    }
75
76    pub fn bind_group(&self, id: TextureId) -> &wgpu::BindGroup {
77        &self
78            .map
79            .get(&id)
80            .unwrap_or_else(|| self.map.get(&self.white).expect("white texture"))
81            .bind_group
82    }
83
84    /// Reserved accessor: look up a texture by id (e.g. to inspect its size).
85    /// Kept as part of the registry's intended API though not yet wired up.
86    #[allow(dead_code)]
87    pub fn get(&self, id: TextureId) -> Option<&Texture> {
88        self.map.get(&id)
89    }
90
91    #[cfg(not(target_arch = "wasm32"))]
92    pub fn load_file(
93        &mut self,
94        device: &wgpu::Device,
95        queue: &wgpu::Queue,
96        path: impl AsRef<Path>,
97    ) -> Result<TextureId> {
98        let path = path.as_ref();
99        let img = image::open(path).with_context(|| format!("loading {}", path.display()))?;
100        let rgba = img.to_rgba8();
101        let (w, h) = rgba.dimensions();
102        Ok(self.create_from_rgba(
103            device,
104            queue,
105            w,
106            h,
107            rgba.as_raw(),
108            path.file_name().and_then(|s| s.to_str()),
109        ))
110    }
111
112    /// Decode an encoded image (PNG) held in memory and upload it.
113    pub fn load_bytes(
114        &mut self,
115        device: &wgpu::Device,
116        queue: &wgpu::Queue,
117        bytes: &[u8],
118        label: Option<&str>,
119    ) -> Result<TextureId> {
120        let img = image::load_from_memory(bytes)
121            .with_context(|| format!("decoding {}", label.unwrap_or("image")))?;
122        let rgba = img.to_rgba8();
123        let (w, h) = rgba.dimensions();
124        Ok(self.create_from_rgba(device, queue, w, h, rgba.as_raw(), label))
125    }
126
127    /// Replace the contents of `id` with a freshly decoded copy of `path`. Keeps the same
128    /// `TextureId` so anything already holding it stays valid.
129    pub fn reload(
130        &mut self,
131        device: &wgpu::Device,
132        queue: &wgpu::Queue,
133        id: TextureId,
134        path: impl AsRef<Path>,
135    ) -> Result<()> {
136        let path = path.as_ref();
137        let img = image::open(path).with_context(|| format!("reloading {}", path.display()))?;
138        let rgba = img.to_rgba8();
139        let (w, h) = rgba.dimensions();
140        self.replace(
141            device,
142            queue,
143            id,
144            w,
145            h,
146            rgba.as_raw(),
147            path.file_name().and_then(|s| s.to_str()),
148        );
149        Ok(())
150    }
151
152    #[allow(clippy::too_many_arguments)]
153    pub fn replace(
154        &mut self,
155        device: &wgpu::Device,
156        queue: &wgpu::Queue,
157        id: TextureId,
158        width: u32,
159        height: u32,
160        rgba: &[u8],
161        label: Option<&str>,
162    ) {
163        let new_id = self.create_from_rgba(device, queue, width, height, rgba, label);
164        if let Some(new_tex) = self.map.remove(&new_id) {
165            self.map.insert(id, new_tex);
166        }
167    }
168
169    pub fn create_from_rgba(
170        &mut self,
171        device: &wgpu::Device,
172        queue: &wgpu::Queue,
173        width: u32,
174        height: u32,
175        rgba: &[u8],
176        label: Option<&str>,
177    ) -> TextureId {
178        let size = wgpu::Extent3d {
179            width,
180            height,
181            depth_or_array_layers: 1,
182        };
183        let texture = device.create_texture(&wgpu::TextureDescriptor {
184            label,
185            size,
186            mip_level_count: 1,
187            sample_count: 1,
188            dimension: wgpu::TextureDimension::D2,
189            format: wgpu::TextureFormat::Rgba8UnormSrgb,
190            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
191            view_formats: &[],
192        });
193        queue.write_texture(
194            wgpu::TexelCopyTextureInfo {
195                texture: &texture,
196                mip_level: 0,
197                origin: wgpu::Origin3d::ZERO,
198                aspect: wgpu::TextureAspect::All,
199            },
200            rgba,
201            wgpu::TexelCopyBufferLayout {
202                offset: 0,
203                bytes_per_row: Some(4 * width),
204                rows_per_image: Some(height),
205            },
206            size,
207        );
208        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
209        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
210            label,
211            layout: &self.layout,
212            entries: &[
213                wgpu::BindGroupEntry {
214                    binding: 0,
215                    resource: wgpu::BindingResource::TextureView(&view),
216                },
217                wgpu::BindGroupEntry {
218                    binding: 1,
219                    resource: wgpu::BindingResource::Sampler(&self.sampler),
220                },
221            ],
222        });
223
224        let id = TextureId(self.next);
225        self.next += 1;
226        self.map.insert(
227            id,
228            Texture {
229                size: [width, height],
230                bind_group,
231                texture,
232                view,
233            },
234        );
235        id
236    }
237}