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 pub(crate) texture: wgpu::Texture,
48 pub(crate) view: wgpu::TextureView,
49 pub(crate) wrap: TextureWrap,
50 pub(crate) label: Option<String>,
52}
53
54pub(crate) struct TextureRegistry {
55 map: HashMap<TextureId, Texture>,
56 next: u32,
57 pub(crate) layout: wgpu::BindGroupLayout,
58 samplers: [wgpu::Sampler; 3],
60 white: TextureId,
61}
62
63impl TextureRegistry {
64 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
65 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
66 label: Some("sprite.texture_bgl"),
67 entries: &[
68 wgpu::BindGroupLayoutEntry {
69 binding: 0,
70 visibility: wgpu::ShaderStages::FRAGMENT,
71 ty: wgpu::BindingType::Texture {
72 sample_type: wgpu::TextureSampleType::Float { filterable: true },
73 view_dimension: wgpu::TextureViewDimension::D2,
74 multisampled: false,
75 },
76 count: None,
77 },
78 wgpu::BindGroupLayoutEntry {
79 binding: 1,
80 visibility: wgpu::ShaderStages::FRAGMENT,
81 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
82 count: None,
83 },
84 ],
85 });
86
87 let samplers = TextureWrap::ALL.map(|wrap| {
88 let mode = wrap.address_mode();
89 device.create_sampler(&wgpu::SamplerDescriptor {
90 label: Some("sprite.sampler"),
91 address_mode_u: mode,
92 address_mode_v: mode,
93 address_mode_w: mode,
94 mag_filter: wgpu::FilterMode::Nearest,
95 min_filter: wgpu::FilterMode::Nearest,
96 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
97 ..Default::default()
98 })
99 });
100
101 let mut me = Self {
102 map: HashMap::new(),
103 next: 1,
104 layout,
105 samplers,
106 white: TextureId(0),
107 };
108 me.white = me.create_from_rgba(device, queue, 1, 1, &[255, 255, 255, 255], Some("white"));
109 me
110 }
111
112 pub fn white(&self) -> TextureId {
113 self.white
114 }
115
116 pub fn bind_group(&self, id: TextureId) -> &wgpu::BindGroup {
117 &self
118 .map
119 .get(&id)
120 .unwrap_or_else(|| self.map.get(&self.white).expect("white texture"))
121 .bind_group
122 }
123
124 pub fn get(&self, id: TextureId) -> Option<&Texture> {
125 self.map.get(&id)
126 }
127
128 pub fn set_wrap(&mut self, device: &wgpu::Device, id: TextureId, wrap: TextureWrap) -> bool {
131 let Some(tex) = self.map.get(&id) else {
132 return false;
133 };
134 if tex.wrap != wrap {
135 let bind_group = self.create_bind_group(device, &tex.view, wrap, tex.label.as_deref());
136 let tex = self.map.get_mut(&id).expect("checked above");
137 tex.bind_group = bind_group;
138 tex.wrap = wrap;
139 }
140 true
141 }
142
143 fn create_bind_group(
144 &self,
145 device: &wgpu::Device,
146 view: &wgpu::TextureView,
147 wrap: TextureWrap,
148 label: Option<&str>,
149 ) -> wgpu::BindGroup {
150 device.create_bind_group(&wgpu::BindGroupDescriptor {
151 label,
152 layout: &self.layout,
153 entries: &[
154 wgpu::BindGroupEntry {
155 binding: 0,
156 resource: wgpu::BindingResource::TextureView(view),
157 },
158 wgpu::BindGroupEntry {
159 binding: 1,
160 resource: wgpu::BindingResource::Sampler(&self.samplers[wrap.index()]),
161 },
162 ],
163 })
164 }
165
166 #[cfg(not(target_arch = "wasm32"))]
167 pub fn load_file(
168 &mut self,
169 device: &wgpu::Device,
170 queue: &wgpu::Queue,
171 path: impl AsRef<Path>,
172 ) -> Result<TextureId> {
173 let path = path.as_ref();
174 let img = image::open(path).with_context(|| format!("loading {}", path.display()))?;
175 let rgba = img.to_rgba8();
176 let (w, h) = rgba.dimensions();
177 Ok(self.create_from_rgba(
178 device,
179 queue,
180 w,
181 h,
182 rgba.as_raw(),
183 path.file_name().and_then(|s| s.to_str()),
184 ))
185 }
186
187 pub fn load_bytes(
189 &mut self,
190 device: &wgpu::Device,
191 queue: &wgpu::Queue,
192 bytes: &[u8],
193 label: Option<&str>,
194 ) -> Result<TextureId> {
195 let img = image::load_from_memory(bytes)
196 .with_context(|| format!("decoding {}", label.unwrap_or("image")))?;
197 let rgba = img.to_rgba8();
198 let (w, h) = rgba.dimensions();
199 Ok(self.create_from_rgba(device, queue, w, h, rgba.as_raw(), label))
200 }
201
202 pub fn reload(
205 &mut self,
206 device: &wgpu::Device,
207 queue: &wgpu::Queue,
208 id: TextureId,
209 path: impl AsRef<Path>,
210 ) -> Result<()> {
211 let path = path.as_ref();
212 let img = image::open(path).with_context(|| format!("reloading {}", path.display()))?;
213 let rgba = img.to_rgba8();
214 let (w, h) = rgba.dimensions();
215 self.replace(
216 device,
217 queue,
218 id,
219 w,
220 h,
221 rgba.as_raw(),
222 path.file_name().and_then(|s| s.to_str()),
223 );
224 Ok(())
225 }
226
227 #[allow(clippy::too_many_arguments)]
228 pub fn replace(
229 &mut self,
230 device: &wgpu::Device,
231 queue: &wgpu::Queue,
232 id: TextureId,
233 width: u32,
234 height: u32,
235 rgba: &[u8],
236 label: Option<&str>,
237 ) {
238 let wrap = self.map.get(&id).map(|t| t.wrap).unwrap_or_default();
239 let new_id = self.create_from_rgba(device, queue, width, height, rgba, label);
240 self.set_wrap(device, new_id, wrap);
242 if let Some(new_tex) = self.map.remove(&new_id) {
243 self.map.insert(id, new_tex);
244 }
245 }
246
247 pub fn write_rgba(&self, queue: &wgpu::Queue, id: TextureId, rgba: &[u8]) -> bool {
251 let Some(tex) = self.map.get(&id) else {
252 return false;
253 };
254 let [width, height] = tex.size;
255 if rgba.len() as u64 != u64::from(width) * u64::from(height) * 4 {
257 return false;
258 }
259 queue.write_texture(
260 wgpu::TexelCopyTextureInfo {
261 texture: &tex.texture,
262 mip_level: 0,
263 origin: wgpu::Origin3d::ZERO,
264 aspect: wgpu::TextureAspect::All,
265 },
266 rgba,
267 wgpu::TexelCopyBufferLayout {
268 offset: 0,
269 bytes_per_row: Some(4 * width),
270 rows_per_image: Some(height),
271 },
272 wgpu::Extent3d {
273 width,
274 height,
275 depth_or_array_layers: 1,
276 },
277 );
278 true
279 }
280
281 pub fn create_from_rgba(
282 &mut self,
283 device: &wgpu::Device,
284 queue: &wgpu::Queue,
285 width: u32,
286 height: u32,
287 rgba: &[u8],
288 label: Option<&str>,
289 ) -> TextureId {
290 let size = wgpu::Extent3d {
291 width,
292 height,
293 depth_or_array_layers: 1,
294 };
295 let texture = device.create_texture(&wgpu::TextureDescriptor {
296 label,
297 size,
298 mip_level_count: 1,
299 sample_count: 1,
300 dimension: wgpu::TextureDimension::D2,
301 format: wgpu::TextureFormat::Rgba8UnormSrgb,
302 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
303 view_formats: &[],
304 });
305 queue.write_texture(
306 wgpu::TexelCopyTextureInfo {
307 texture: &texture,
308 mip_level: 0,
309 origin: wgpu::Origin3d::ZERO,
310 aspect: wgpu::TextureAspect::All,
311 },
312 rgba,
313 wgpu::TexelCopyBufferLayout {
314 offset: 0,
315 bytes_per_row: Some(4 * width),
316 rows_per_image: Some(height),
317 },
318 size,
319 );
320 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
321 let bind_group = self.create_bind_group(device, &view, TextureWrap::Clamp, label);
322
323 let id = TextureId(self.next);
324 self.next += 1;
325 self.map.insert(
326 id,
327 Texture {
328 size: [width, height],
329 bind_group,
330 texture,
331 view,
332 wrap: TextureWrap::Clamp,
333 label: label.map(str::to_owned),
334 },
335 );
336 id
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn index_matches_position_in_all() {
346 for (i, wrap) in TextureWrap::ALL.into_iter().enumerate() {
347 assert_eq!(wrap.index(), i);
348 }
349 }
350}