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 #[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 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 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}