1use super::decode_rgba_image_file;
2use std::sync::Arc;
3
4const SAMPLER_LINEAR_REPEAT: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
12 label: Some("linear_repeat_sampler"),
13 address_mode_u: wgpu::AddressMode::Repeat,
14 address_mode_v: wgpu::AddressMode::Repeat,
15 address_mode_w: wgpu::AddressMode::Repeat,
16 mag_filter: wgpu::FilterMode::Linear,
17 min_filter: wgpu::FilterMode::Linear,
18 mipmap_filter: wgpu::FilterMode::Nearest, lod_min_clamp: 0.0,
20 lod_max_clamp: 0.0,
21 compare: None,
22 anisotropy_clamp: 1,
23 border_color: None,
24};
25
26const SAMPLER_NEAREST_REPEAT: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
28 label: Some("nearest_repeat_sampler"),
29 address_mode_u: wgpu::AddressMode::Repeat,
30 address_mode_v: wgpu::AddressMode::Repeat,
31 address_mode_w: wgpu::AddressMode::Repeat,
32 mag_filter: wgpu::FilterMode::Nearest,
33 min_filter: wgpu::FilterMode::Nearest,
34 mipmap_filter: wgpu::FilterMode::Nearest,
35 lod_min_clamp: 0.0,
36 lod_max_clamp: 0.0,
37 compare: None,
38 anisotropy_clamp: 1,
39 border_color: None,
40};
41
42impl super::AssetManager {
47 fn resolve_texture_cache_key(&self, path_or_uuid: &str) -> Result<(String, String), String> {
56 let resolved = self.resolve_path_from_meta_source(path_or_uuid)?;
57 let cache_key = self
58 .get_uuid(&resolved)
59 .map(|id| id.to_string())
60 .unwrap_or_else(|| resolved.clone());
61 Ok((resolved, cache_key))
62 }
63
64 pub fn install_decoded_material_texture(
76 &mut self,
77 device: &wgpu::Device,
78 queue: &wgpu::Queue,
79 layout: &wgpu::BindGroupLayout,
80 cache_key: &str,
81 rgba: &[u8],
82 width: u32,
83 height: u32,
84 ) -> Result<Arc<wgpu::BindGroup>, String> {
85 if width == 0 || height == 0 {
87 return Err(format!(
88 "Cannot create texture with zero dimension: {width}×{height} (key={cache_key})"
89 ));
90 }
91
92 let expected = (width as usize)
93 .saturating_mul(height as usize)
94 .saturating_mul(4);
95
96 if rgba.len() != expected {
97 return Err(format!(
98 "RGBA size mismatch for '{cache_key}': got {} bytes, expected {expected} \
99 ({width}×{height}×4)",
100 rgba.len()
101 ));
102 }
103
104 let texture_size = wgpu::Extent3d {
105 width,
106 height,
107 depth_or_array_layers: 1,
108 };
109
110 let texture = device.create_texture(&wgpu::TextureDescriptor {
111 size: texture_size,
112 mip_level_count: 1,
113 sample_count: 1,
114 dimension: wgpu::TextureDimension::D2,
115 format: wgpu::TextureFormat::Rgba8UnormSrgb,
116 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
117 label: Some(cache_key),
118 view_formats: &[],
119 });
120
121 queue.write_texture(
122 wgpu::ImageCopyTexture {
123 texture: &texture,
124 mip_level: 0,
125 origin: wgpu::Origin3d::ZERO,
126 aspect: wgpu::TextureAspect::All,
127 },
128 rgba,
129 wgpu::ImageDataLayout {
130 offset: 0,
131 bytes_per_row: Some(4 * width),
132 rows_per_image: Some(height),
133 },
134 texture_size,
135 );
136
137 let bg = self.build_bind_group(device, &texture, layout, &SAMPLER_LINEAR_REPEAT, cache_key);
138 self.texture_cache.insert(cache_key.to_string(), bg.clone());
139 Ok(bg)
140 }
141
142 pub fn load_material_texture(
151 &mut self,
152 device: &wgpu::Device,
153 queue: &wgpu::Queue,
154 layout: &wgpu::BindGroupLayout,
155 path_or_uuid: &str,
156 ) -> Result<Arc<wgpu::BindGroup>, String> {
157 let (resolved_path, cache_key) = self.resolve_texture_cache_key(path_or_uuid)?;
158
159 if let Some(cached) = self.texture_cache.get(&cache_key) {
160 return Ok(cached.clone());
161 }
162
163 let (rgba, w, h) = self.decode_texture_rgba(&resolved_path)?;
164 self.install_decoded_material_texture(device, queue, layout, &cache_key, &rgba, w, h)
165 }
166
167 pub fn reload_material_texture(
171 &mut self,
172 device: &wgpu::Device,
173 queue: &wgpu::Queue,
174 layout: &wgpu::BindGroupLayout,
175 path_or_uuid: &str,
176 ) -> Result<Arc<wgpu::BindGroup>, String> {
177 let (_, cache_key) = self.resolve_texture_cache_key(path_or_uuid)?;
179 self.texture_cache.remove(&cache_key);
180 self.load_material_texture(device, queue, layout, path_or_uuid)
181 }
182
183 pub fn create_white_texture(
189 &mut self,
190 device: &wgpu::Device,
191 queue: &wgpu::Queue,
192 layout: &wgpu::BindGroupLayout,
193 ) -> Arc<wgpu::BindGroup> {
194 const KEY: &str = "__white_fallback_texture__";
195
196 if let Some(cached) = self.texture_cache.get(KEY) {
197 return cached.clone();
198 }
199
200 let bg = self.upload_solid_1x1(device, queue, layout, [255, 255, 255, 255], KEY);
201 self.texture_cache.insert(KEY.to_string(), bg.clone());
202 bg
203 }
204
205 pub fn create_checkerboard_texture(
210 &mut self,
211 device: &wgpu::Device,
212 queue: &wgpu::Queue,
213 layout: &wgpu::BindGroupLayout,
214 ) -> Arc<wgpu::BindGroup> {
215 const KEY: &str = "__checkerboard_texture__";
216 const SIZE: u32 = 256;
217 const CELL: u32 = 32; if let Some(cached) = self.texture_cache.get(KEY) {
220 return cached.clone();
221 }
222
223 let mut pixels = vec![0u8; (SIZE * SIZE * 4) as usize];
224 for y in 0..SIZE {
225 for x in 0..SIZE {
226 let light = ((x / CELL) + (y / CELL)).is_multiple_of(2);
227 let luma = if light { 200u8 } else { 50u8 };
228 let base = ((y * SIZE + x) * 4) as usize;
229 pixels[base] = luma;
230 pixels[base + 1] = luma;
231 pixels[base + 2] = luma;
232 pixels[base + 3] = 255;
233 }
234 }
235
236 self.install_decoded_material_texture(device, queue, layout, KEY, &pixels, SIZE, SIZE)
239 .expect("checkerboard texture creation must not fail")
240 }
241
242 fn decode_texture_rgba(&self, resolved_path: &str) -> Result<(Vec<u8>, u32, u32), String> {
246 if let Some(data) = self.embedded_assets.get(resolved_path) {
247 let img = image::load_from_memory(data)
248 .map_err(|e| format!("Embedded texture decode failed ({resolved_path}): {e}"))?
249 .to_rgba8();
250 let (w, h) = img.dimensions();
251 return Ok((img.into_raw(), w, h));
252 }
253
254 decode_rgba_image_file(resolved_path)
255 }
256
257 fn upload_solid_1x1(
262 &self,
263 device: &wgpu::Device,
264 queue: &wgpu::Queue,
265 layout: &wgpu::BindGroupLayout,
266 pixel: [u8; 4],
267 label: &str,
268 ) -> Arc<wgpu::BindGroup> {
269 let size = wgpu::Extent3d {
270 width: 1,
271 height: 1,
272 depth_or_array_layers: 1,
273 };
274
275 let texture = device.create_texture(&wgpu::TextureDescriptor {
276 size,
277 mip_level_count: 1,
278 sample_count: 1,
279 dimension: wgpu::TextureDimension::D2,
280 format: wgpu::TextureFormat::Rgba8UnormSrgb,
281 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
282 label: Some(label),
283 view_formats: &[],
284 });
285
286 queue.write_texture(
287 wgpu::ImageCopyTexture {
288 texture: &texture,
289 mip_level: 0,
290 origin: wgpu::Origin3d::ZERO,
291 aspect: wgpu::TextureAspect::All,
292 },
293 &pixel,
294 wgpu::ImageDataLayout {
295 offset: 0,
296 bytes_per_row: Some(4),
297 rows_per_image: Some(1),
298 },
299 size,
300 );
301
302 self.build_bind_group(device, &texture, layout, &SAMPLER_NEAREST_REPEAT, label)
303 }
304
305 fn build_bind_group(
310 &self,
311 device: &wgpu::Device,
312 texture: &wgpu::Texture,
313 layout: &wgpu::BindGroupLayout,
314 sampler_desc: &wgpu::SamplerDescriptor,
315 label: &str,
316 ) -> Arc<wgpu::BindGroup> {
317 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
318 let sampler = device.create_sampler(sampler_desc);
319
320 Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
321 label: Some(label),
322 layout,
323 entries: &[
324 wgpu::BindGroupEntry {
325 binding: 0,
326 resource: wgpu::BindingResource::TextureView(&view),
327 },
328 wgpu::BindGroupEntry {
329 binding: 1,
330 resource: wgpu::BindingResource::Sampler(&sampler),
331 },
332 ],
333 }))
334 }
335}