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#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
12pub enum TextureWrap {
13 #[default]
16 Clamp,
17 Repeat,
19 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 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 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 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 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 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 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 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}