1use crate::resources::*;
2
3impl DeviceResources {
4 pub fn upload_texture(
13 &mut self,
14 device: &wgpu::Device,
15 queue: &wgpu::Queue,
16 width: u32,
17 height: u32,
18 rgba_data: &[u8],
19 ) -> crate::error::ViewportResult<crate::resources::TextureId> {
20 let id = self.begin_upload_texture(device, queue, width, height, rgba_data.to_vec())?;
28 self.drain_until(device, queue, id)?;
29 self.upload_result_texture(id)
30 }
31
32 pub fn upload_normal_map(
37 &mut self,
38 device: &wgpu::Device,
39 queue: &wgpu::Queue,
40 width: u32,
41 height: u32,
42 rgba_data: &[u8],
43 ) -> crate::error::ViewportResult<crate::resources::TextureId> {
44 let id = self.begin_upload_normal_map(device, queue, width, height, rgba_data.to_vec())?;
46 self.drain_until(device, queue, id)?;
47 self.upload_result_texture(id)
48 }
49
50 pub fn begin_upload_texture(
72 &mut self,
73 device: &wgpu::Device,
74 queue: &wgpu::Queue,
75 width: u32,
76 height: u32,
77 rgba: Vec<u8>,
78 ) -> crate::error::ViewportResult<crate::resources::JobId> {
79 let expected = (width * height * 4) as usize;
80 if rgba.len() != expected {
81 return Err(crate::error::ViewportError::InvalidTextureData {
82 expected,
83 actual: rgba.len(),
84 });
85 }
86 Ok(self.spawn_texture_upload(
87 device,
88 queue,
89 TextureUploadSpec {
90 width,
91 height,
92 format: wgpu::TextureFormat::Rgba8UnormSrgb,
93 is_normal_map: false,
94 mip_levels: vec![rgba],
95 },
96 ))
97 }
98
99 pub fn begin_upload_normal_map(
109 &mut self,
110 device: &wgpu::Device,
111 queue: &wgpu::Queue,
112 width: u32,
113 height: u32,
114 rgba: Vec<u8>,
115 ) -> crate::error::ViewportResult<crate::resources::JobId> {
116 let expected = (width * height * 4) as usize;
117 if rgba.len() != expected {
118 return Err(crate::error::ViewportError::InvalidTextureData {
119 expected,
120 actual: rgba.len(),
121 });
122 }
123 Ok(self.spawn_texture_upload(
124 device,
125 queue,
126 TextureUploadSpec {
127 width,
128 height,
129 format: wgpu::TextureFormat::Rgba8Unorm,
130 is_normal_map: true,
131 mip_levels: vec![rgba],
132 },
133 ))
134 }
135
136 pub fn upload_result_texture(
143 &mut self,
144 id: crate::resources::JobId,
145 ) -> crate::error::ViewportResult<crate::resources::TextureId> {
146 let mut map = self
147 .job_results
148 .texture
149 .lock()
150 .expect("texture result map poisoned");
151 let slot = match map.get(&id) {
152 Some(s) => s.clone(),
153 None => {
154 return Err(crate::error::ViewportError::JobResultMissing {
155 reason: "unknown id or wrong upload type",
156 });
157 }
158 };
159 match slot.take() {
160 Some(tex_id) => {
161 map.remove(&id);
162 Ok(tex_id)
163 }
164 None => Err(crate::error::ViewportError::JobNotReady),
165 }
166 }
167
168 pub fn upload_compressed_texture(
178 &mut self,
179 device: &wgpu::Device,
180 queue: &wgpu::Queue,
181 desc: CompressedTextureDesc<'_>,
182 ) -> crate::error::ViewportResult<crate::resources::TextureId> {
183 let id = self.begin_upload_compressed_texture(device, queue, desc)?;
184 self.drain_until(device, queue, id)?;
185 self.upload_result_texture(id)
186 }
187
188 pub fn begin_upload_compressed_texture(
205 &mut self,
206 device: &wgpu::Device,
207 queue: &wgpu::Queue,
208 desc: CompressedTextureDesc<'_>,
209 ) -> crate::error::ViewportResult<crate::resources::JobId> {
210 if !desc.format.is_compressed() {
211 return Err(crate::error::ViewportError::UnsupportedTextureFormat {
212 format: desc.format,
213 });
214 }
215 let (block_w, block_h) = desc.format.block_dimensions();
221 if desc.width % block_w != 0 || desc.height % block_h != 0 {
222 return Err(
223 crate::error::ViewportError::CompressedTextureNotBlockAligned {
224 width: desc.width,
225 height: desc.height,
226 block_width: block_w,
227 block_height: block_h,
228 },
229 );
230 }
231 if !supports_texture_format(device, desc.format) {
232 return Err(crate::error::ViewportError::UnsupportedTextureFormat {
233 format: desc.format,
234 });
235 }
236 if desc.mip_levels.is_empty() {
237 let (_, _, expected) = mip_block_layout(desc.format, desc.width, desc.height);
238 return Err(crate::error::ViewportError::InvalidCompressedTextureData {
239 level: 0,
240 expected,
241 actual: 0,
242 });
243 }
244 let mut mip_levels = Vec::with_capacity(desc.mip_levels.len());
247 for (level, data) in desc.mip_levels.iter().enumerate() {
248 let lw = (desc.width >> level).max(1);
249 let lh = (desc.height >> level).max(1);
250 let (_, _, expected) = mip_block_layout(desc.format, lw, lh);
251 if data.len() != expected {
252 return Err(crate::error::ViewportError::InvalidCompressedTextureData {
253 level: level as u32,
254 expected,
255 actual: data.len(),
256 });
257 }
258 mip_levels.push(data.to_vec());
259 }
260 Ok(self.spawn_texture_upload(
261 device,
262 queue,
263 TextureUploadSpec {
264 width: desc.width,
265 height: desc.height,
266 format: desc.format,
267 is_normal_map: desc.is_normal_map,
268 mip_levels,
269 },
270 ))
271 }
272
273 fn spawn_texture_upload(
277 &mut self,
278 device: &wgpu::Device,
279 queue: &wgpu::Queue,
280 spec: TextureUploadSpec,
281 ) -> crate::resources::JobId {
282 let slot = crate::resources::ResultSlot::<crate::resources::TextureId>::new();
283 let slot_for_apply = slot.clone();
284
285 let bgl = self.texture_bind_group_layout.clone();
289 let fallback_albedo_view = self.fallback_texture.view.clone();
290 let fallback_normal_view = self.fallback_normal_map_view.clone();
291 let fallback_ao_view = self.fallback_ao_map_view.clone();
292 let data_bytes: u64 = spec.mip_levels.iter().map(|l| l.len() as u64).sum();
293
294 let TextureUploadSpec {
295 width,
296 height,
297 format,
298 is_normal_map,
299 mip_levels,
300 } = spec;
301
302 let id = {
303 let mut runner = self.jobs.lock().expect("upload job runner poisoned");
304 runner.submit_with_gpu(device, queue, move |dev, q, progress| {
305 progress.set(0.2);
306 let gpu_texture = build_gpu_texture(
307 dev,
308 q,
309 width,
310 height,
311 format,
312 is_normal_map,
313 &mip_levels,
314 &bgl,
315 &fallback_albedo_view,
316 &fallback_normal_view,
317 &fallback_ao_view,
318 );
319 progress.set(0.9);
320
321 let encoder = dev.create_command_encoder(&wgpu::CommandEncoderDescriptor {
324 label: Some("async_texture_flush"),
325 });
326 let submission = q.submit(std::iter::once(encoder.finish()));
327 progress.set(1.0);
328
329 Ok(
330 crate::resources::upload_jobs::JobProduct::with_gpu_and_apply(
331 submission,
332 Box::new(move |resources: &mut DeviceResources| {
333 let tex_id = resources.content.textures.insert(gpu_texture, data_bytes);
334 slot_for_apply.set(tex_id);
335 }),
336 ),
337 )
338 })
339 };
340
341 self.job_results
342 .texture
343 .lock()
344 .expect("texture result map poisoned")
345 .insert(id, slot);
346 id
347 }
348
349 pub fn texture_memory_stats(&self) -> TextureMemoryStats {
359 TextureMemoryStats {
360 used_bytes: self.content.textures.allocated_bytes(),
361 texture_count: self.content.textures.len() as u32,
362 }
363 }
364
365 pub fn free_texture(&mut self, id: crate::resources::TextureId) -> bool {
379 if !self.content.textures.remove(id) {
380 return false;
381 }
382 self.evict_texture_bind_group_caches(id.raw());
383 true
384 }
385
386 pub fn replace_texture(
408 &mut self,
409 device: &wgpu::Device,
410 queue: &wgpu::Queue,
411 id: crate::resources::TextureId,
412 width: u32,
413 height: u32,
414 rgba_data: &[u8],
415 ) -> crate::error::ViewportResult<()> {
416 let expected = (width * height * 4) as usize;
417 if rgba_data.len() != expected {
418 return Err(crate::error::ViewportError::InvalidTextureData {
419 expected,
420 actual: rgba_data.len(),
421 });
422 }
423 let gpu_texture = build_gpu_texture(
424 device,
425 queue,
426 width,
427 height,
428 wgpu::TextureFormat::Rgba8UnormSrgb,
429 false,
430 std::slice::from_ref(&rgba_data.to_vec()),
431 &self.texture_bind_group_layout,
432 &self.fallback_texture.view,
433 &self.fallback_normal_map_view,
434 &self.fallback_ao_map_view,
435 );
436 let bytes = rgba_data.len() as u64;
437 if self
438 .content
439 .textures
440 .replace(id, gpu_texture, bytes)
441 .is_none()
442 {
443 let (index, count) = (id.index(), self.content.textures.len());
444 return Err(crate::error::ViewportError::StaleHandle { index, count });
445 }
446 self.evict_texture_bind_group_caches(id.raw());
450 Ok(())
451 }
452
453 fn evict_texture_bind_group_caches(&mut self, raw: u64) {
457 self.content
458 .material_bind_groups
459 .retain(|&(a, n, ao), _| a != raw && n != raw && ao != raw);
460 self.instancing
461 .bind_groups
462 .retain(|&(a, n, ao), _| a != raw && n != raw && ao != raw);
463
464 const INVALID_KEY: (u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64) = (
469 u64::MAX,
470 u64::MAX,
471 u64::MAX,
472 u64::MAX,
473 u64::MAX,
474 u64::MAX,
475 u64::MAX,
476 u64::MAX,
477 u64::MAX,
478 u64::MAX,
479 u64::MAX,
480 );
481 for (_, mesh) in self.mesh_store.iter_mut() {
482 let k = mesh.last_tex_key;
483 if k.0 == raw || k.1 == raw || k.2 == raw || k.7 == raw || k.8 == raw {
484 mesh.last_tex_key = INVALID_KEY;
485 }
486 }
487 }
488}
489
490struct TextureUploadSpec {
494 width: u32,
495 height: u32,
496 format: wgpu::TextureFormat,
497 is_normal_map: bool,
499 mip_levels: Vec<Vec<u8>>,
501}
502
503fn mip_block_layout(
507 format: wgpu::TextureFormat,
508 level_width: u32,
509 level_height: u32,
510) -> (u32, u32, usize) {
511 let (block_w, block_h) = format.block_dimensions();
512 let block_bytes = format
513 .block_copy_size(None)
514 .expect("texture format has no single block-copy size");
515 let blocks_x = level_width.div_ceil(block_w);
516 let blocks_y = level_height.div_ceil(block_h);
517 let bytes_per_row = blocks_x * block_bytes;
518 let total = (blocks_x * blocks_y * block_bytes) as usize;
519 (bytes_per_row, blocks_y, total)
520}
521
522#[allow(clippy::too_many_arguments)]
531fn build_gpu_texture(
532 device: &wgpu::Device,
533 queue: &wgpu::Queue,
534 width: u32,
535 height: u32,
536 format: wgpu::TextureFormat,
537 is_normal_map: bool,
538 mip_levels: &[Vec<u8>],
539 bgl: &wgpu::BindGroupLayout,
540 fallback_albedo_view: &wgpu::TextureView,
541 fallback_normal_view: &wgpu::TextureView,
542 fallback_ao_view: &wgpu::TextureView,
543) -> GpuTexture {
544 let tex_label = if is_normal_map {
545 "user_normal_map_texture"
546 } else {
547 "user_texture"
548 };
549 let mip_level_count = mip_levels.len() as u32;
550 let texture = device.create_texture(&wgpu::TextureDescriptor {
551 label: Some(tex_label),
552 size: wgpu::Extent3d {
553 width,
554 height,
555 depth_or_array_layers: 1,
556 },
557 mip_level_count,
558 sample_count: 1,
559 dimension: wgpu::TextureDimension::D2,
560 format,
561 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
562 view_formats: &[],
563 });
564 for (level, data) in mip_levels.iter().enumerate() {
567 let lw = (width >> level).max(1);
568 let lh = (height >> level).max(1);
569 let (bytes_per_row, blocks_high, _) = mip_block_layout(format, lw, lh);
570 queue.write_texture(
571 wgpu::TexelCopyTextureInfo {
572 texture: &texture,
573 mip_level: level as u32,
574 origin: wgpu::Origin3d::ZERO,
575 aspect: wgpu::TextureAspect::All,
576 },
577 data,
578 wgpu::TexelCopyBufferLayout {
579 offset: 0,
580 bytes_per_row: Some(bytes_per_row),
581 rows_per_image: Some(blocks_high),
582 },
583 wgpu::Extent3d {
584 width: lw,
585 height: lh,
586 depth_or_array_layers: 1,
587 },
588 );
589 }
590
591 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
592 let mipmap_filter = if mip_level_count > 1 {
593 wgpu::FilterMode::Linear
594 } else {
595 wgpu::FilterMode::Nearest
596 };
597 let sampler_label = if is_normal_map {
598 "user_normal_map_sampler"
599 } else {
600 "user_texture_sampler"
601 };
602 let sampler =
603 crate::resources::builders::repeat_linear_sampler(device, sampler_label, mipmap_filter);
604 let (slot0_view, slot2_view) = if is_normal_map {
605 (fallback_albedo_view, &view)
606 } else {
607 (&view, fallback_normal_view)
608 };
609 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
610 label: Some(if is_normal_map {
611 "user_normal_map_bg"
612 } else {
613 "user_texture_bg"
614 }),
615 layout: bgl,
616 entries: &[
617 wgpu::BindGroupEntry {
618 binding: 0,
619 resource: wgpu::BindingResource::TextureView(slot0_view),
620 },
621 wgpu::BindGroupEntry {
622 binding: 1,
623 resource: wgpu::BindingResource::Sampler(&sampler),
624 },
625 wgpu::BindGroupEntry {
626 binding: 2,
627 resource: wgpu::BindingResource::TextureView(slot2_view),
628 },
629 wgpu::BindGroupEntry {
630 binding: 3,
631 resource: wgpu::BindingResource::TextureView(fallback_ao_view),
632 },
633 ],
634 });
635 GpuTexture {
636 texture,
637 view,
638 sampler,
639 bind_group,
640 }
641}
642
643pub fn supports_texture_format(device: &wgpu::Device, format: wgpu::TextureFormat) -> bool {
650 device.features().contains(format.required_features())
651}
652
653pub struct CompressedTextureDesc<'a> {
666 pub width: u32,
668 pub height: u32,
670 pub format: wgpu::TextureFormat,
672 pub is_normal_map: bool,
674 pub mip_levels: &'a [&'a [u8]],
676}
677
678impl DeviceResources {
679 #[allow(dead_code)]
684 pub(crate) fn get_material_bind_group(
685 &mut self,
686 device: &wgpu::Device,
687 albedo_id: Option<crate::resources::TextureId>,
688 normal_map_id: Option<crate::resources::TextureId>,
689 ao_map_id: Option<crate::resources::TextureId>,
690 ) -> &wgpu::BindGroup {
691 let key = (
692 albedo_id.map(|t| t.raw()).unwrap_or(u64::MAX),
693 normal_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
694 ao_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
695 );
696
697 if !self.content.material_bind_groups.contains_key(&key) {
698 let albedo_view = match albedo_id {
699 Some(id) if self.content.textures.get(id).is_some() => {
700 &self.content.textures.get(id).unwrap().view
701 }
702 _ => &self.fallback_texture.view,
703 };
704 let normal_view = match normal_map_id {
705 Some(id) if self.content.textures.get(id).is_some() => {
706 &self.content.textures.get(id).unwrap().view
707 }
708 _ => &self.fallback_normal_map_view,
709 };
710 let ao_view = match ao_map_id {
711 Some(id) if self.content.textures.get(id).is_some() => {
712 &self.content.textures.get(id).unwrap().view
713 }
714 _ => &self.fallback_ao_map_view,
715 };
716
717 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
718 label: Some("material_bg"),
719 layout: &self.texture_bind_group_layout,
720 entries: &[
721 wgpu::BindGroupEntry {
722 binding: 0,
723 resource: wgpu::BindingResource::TextureView(albedo_view),
724 },
725 wgpu::BindGroupEntry {
726 binding: 1,
727 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
728 },
729 wgpu::BindGroupEntry {
730 binding: 2,
731 resource: wgpu::BindingResource::TextureView(normal_view),
732 },
733 wgpu::BindGroupEntry {
734 binding: 3,
735 resource: wgpu::BindingResource::TextureView(ao_view),
736 },
737 ],
738 });
739 self.content.material_bind_groups.insert(key, bg);
740 }
741
742 self.content.material_bind_groups.get(&key).unwrap()
743 }
744
745 pub(crate) fn update_mesh_texture_bind_group(
758 &mut self,
759 device: &wgpu::Device,
760 mesh_id: crate::resources::mesh::mesh_store::MeshId,
761 albedo_id: Option<crate::resources::TextureId>,
762 normal_map_id: Option<crate::resources::TextureId>,
763 ao_map_id: Option<crate::resources::TextureId>,
764 lut_id: Option<ColourmapId>,
765 active_attr: Option<&str>,
766 matcap_id: Option<crate::resources::MatcapId>,
767 warp_attr: Option<&str>,
768 metallic_roughness_id: Option<crate::resources::TextureId>,
769 emissive_texture_id: Option<crate::resources::TextureId>,
770 ) {
771 let hash_str = |name: &str| -> u64 {
772 use std::hash::{Hash, Hasher};
773 let mut h = std::collections::hash_map::DefaultHasher::new();
774 name.hash(&mut h);
775 h.finish()
776 };
777 let attr_hash = active_attr.map(|n| hash_str(n)).unwrap_or(u64::MAX);
778 let warp_hash = warp_attr.map(|n| hash_str(n)).unwrap_or(u64::MAX);
779
780 let (pos_override_gen, nrm_override_gen) = {
784 let Some(mesh) = self.mesh_store.get(mesh_id) else {
785 return;
786 };
787 (mesh.position_override_gen, mesh.normal_override_gen)
788 };
789
790 let key = (
791 albedo_id.map(|t| t.raw()).unwrap_or(u64::MAX),
792 normal_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
793 ao_map_id.map(|t| t.raw()).unwrap_or(u64::MAX),
794 lut_id.map(|id| id.0 as u64).unwrap_or(u64::MAX),
795 attr_hash,
796 matcap_id.map(|id| id.index as u64).unwrap_or(u64::MAX),
797 warp_hash,
798 metallic_roughness_id.map(|t| t.raw()).unwrap_or(u64::MAX),
799 emissive_texture_id.map(|t| t.raw()).unwrap_or(u64::MAX),
800 pos_override_gen,
801 nrm_override_gen,
802 );
803
804 {
805 let Some(mesh) = self.mesh_store.get(mesh_id) else {
806 return;
807 };
808 if mesh.last_tex_key == key {
809 return;
810 }
811 }
812
813 let albedo_view = match albedo_id {
814 Some(id) if self.content.textures.get(id).is_some() => {
815 &self.content.textures.get(id).unwrap().view
816 }
817 _ => &self.fallback_texture.view,
818 };
819 let normal_view = match normal_map_id {
820 Some(id) if self.content.textures.get(id).is_some() => {
821 &self.content.textures.get(id).unwrap().view
822 }
823 _ => &self.fallback_normal_map_view,
824 };
825 let ao_view = match ao_map_id {
826 Some(id) if self.content.textures.get(id).is_some() => {
827 &self.content.textures.get(id).unwrap().view
828 }
829 _ => &self.fallback_ao_map_view,
830 };
831 let lut_view = match lut_id {
832 Some(id) if id.0 < self.content.colourmap_views.len() => {
833 &self.content.colourmap_views[id.0]
834 }
835 _ => &self.content.fallback_lut_view,
836 };
837
838 let Some(mesh) = self.mesh_store.get_mut(mesh_id) else {
839 return;
840 };
841
842 let scalar_buf: &wgpu::Buffer = match active_attr {
843 Some(name) => {
844 let found_vertex = mesh.attribute_buffers.get(name);
845 let found_face = mesh.face_attribute_buffers.get(name);
846 found_vertex
847 .or(found_face)
848 .unwrap_or(&self.content.fallback_scalar_buf)
849 }
850 None => &self.content.fallback_scalar_buf,
851 };
852
853 let face_colour_buf: &wgpu::Buffer = match active_attr {
854 Some(name) => mesh
855 .face_colour_buffers
856 .get(name)
857 .unwrap_or(&self.content.fallback_face_colour_buf),
858 None => &self.content.fallback_face_colour_buf,
859 };
860
861 let matcap_view: &wgpu::TextureView = match matcap_id {
863 Some(id) if id.index < self.content.matcap_views.len() => {
864 &self.content.matcap_views[id.index]
865 }
866 _ => self
867 .content
868 .fallback_matcap_view
869 .as_ref()
870 .unwrap_or(&self.fallback_texture.view),
871 };
872
873 let warp_buf: &wgpu::Buffer = match warp_attr {
874 Some(name) => mesh
875 .vector_attribute_buffers
876 .get(name)
877 .unwrap_or(&self.content.fallback_warp_buf),
878 None => &self.content.fallback_warp_buf,
879 };
880
881 let position_override_buf: &wgpu::Buffer = mesh
882 .position_override_buffer
883 .as_ref()
884 .unwrap_or(&self.content.fallback_position_override_buf);
885 let normal_override_buf: &wgpu::Buffer = mesh
886 .normal_override_buffer
887 .as_ref()
888 .unwrap_or(&self.content.fallback_normal_override_buf);
889
890 let metallic_roughness_view: &wgpu::TextureView = match metallic_roughness_id {
891 Some(id) if self.content.textures.get(id).is_some() => {
892 &self.content.textures.get(id).unwrap().view
893 }
894 _ => &self.fallback_metallic_roughness_texture_view,
895 };
896 let emissive_view: &wgpu::TextureView = match emissive_texture_id {
897 Some(id) if self.content.textures.get(id).is_some() => {
898 &self.content.textures.get(id).unwrap().view
899 }
900 _ => &self.fallback_emissive_texture_view,
901 };
902
903 mesh.object_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
904 label: Some("object_bind_group"),
905 layout: &self.object_bind_group_layout,
906 entries: &[
907 wgpu::BindGroupEntry {
908 binding: 0,
909 resource: mesh.object_uniform_buf.as_entire_binding(),
910 },
911 wgpu::BindGroupEntry {
912 binding: 1,
913 resource: wgpu::BindingResource::TextureView(albedo_view),
914 },
915 wgpu::BindGroupEntry {
916 binding: 2,
917 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
918 },
919 wgpu::BindGroupEntry {
920 binding: 3,
921 resource: wgpu::BindingResource::TextureView(normal_view),
922 },
923 wgpu::BindGroupEntry {
924 binding: 4,
925 resource: wgpu::BindingResource::TextureView(ao_view),
926 },
927 wgpu::BindGroupEntry {
928 binding: 5,
929 resource: wgpu::BindingResource::TextureView(lut_view),
930 },
931 wgpu::BindGroupEntry {
932 binding: 6,
933 resource: scalar_buf.as_entire_binding(),
934 },
935 wgpu::BindGroupEntry {
936 binding: 7,
937 resource: wgpu::BindingResource::TextureView(matcap_view),
938 },
939 wgpu::BindGroupEntry {
940 binding: 8,
941 resource: face_colour_buf.as_entire_binding(),
942 },
943 wgpu::BindGroupEntry {
944 binding: 9,
945 resource: warp_buf.as_entire_binding(),
946 },
947 wgpu::BindGroupEntry {
948 binding: 10,
949 resource: wgpu::BindingResource::Sampler(&self.lut_sampler),
950 },
951 wgpu::BindGroupEntry {
952 binding: 11,
953 resource: wgpu::BindingResource::TextureView(metallic_roughness_view),
954 },
955 wgpu::BindGroupEntry {
956 binding: 12,
957 resource: wgpu::BindingResource::TextureView(emissive_view),
958 },
959 wgpu::BindGroupEntry {
960 binding: 13,
961 resource: position_override_buf.as_entire_binding(),
962 },
963 wgpu::BindGroupEntry {
964 binding: 14,
965 resource: normal_override_buf.as_entire_binding(),
966 },
967 ],
968 });
969 mesh.last_tex_key = key;
970 }
971
972 pub(crate) fn build_per_item_object_bind_group(
981 &self,
982 device: &wgpu::Device,
983 mesh_id: crate::resources::mesh::mesh_store::MeshId,
984 item_uniform_buf: &wgpu::Buffer,
985 albedo_id: Option<crate::resources::TextureId>,
986 normal_map_id: Option<crate::resources::TextureId>,
987 ao_map_id: Option<crate::resources::TextureId>,
988 lut_id: Option<ColourmapId>,
989 active_attr: Option<&str>,
990 matcap_id: Option<crate::resources::MatcapId>,
991 warp_attr: Option<&str>,
992 metallic_roughness_id: Option<crate::resources::TextureId>,
993 emissive_texture_id: Option<crate::resources::TextureId>,
994 prev_key: Option<u64>,
995 ) -> Option<(wgpu::BindGroup, u64)> {
996 let hash_str = |name: &str| -> u64 {
997 use std::hash::{Hash, Hasher};
998 let mut h = std::collections::hash_map::DefaultHasher::new();
999 name.hash(&mut h);
1000 h.finish()
1001 };
1002 let attr_hash = active_attr.map(|n| hash_str(n)).unwrap_or(u64::MAX);
1003 let warp_hash = warp_attr.map(|n| hash_str(n)).unwrap_or(u64::MAX);
1004
1005 let mesh = self.mesh_store.get(mesh_id)?;
1006 let pos_override_gen = mesh.position_override_gen;
1007 let nrm_override_gen = mesh.normal_override_gen;
1008
1009 let cache_key = {
1010 use std::hash::{Hash, Hasher};
1011 let mut h = std::collections::hash_map::DefaultHasher::new();
1012 mesh_id.index().hash(&mut h);
1013 albedo_id.map(|t| t.raw()).unwrap_or(u64::MAX).hash(&mut h);
1014 normal_map_id
1015 .map(|t| t.raw())
1016 .unwrap_or(u64::MAX)
1017 .hash(&mut h);
1018 ao_map_id.map(|t| t.raw()).unwrap_or(u64::MAX).hash(&mut h);
1019 lut_id
1020 .map(|id| id.0 as u64)
1021 .unwrap_or(u64::MAX)
1022 .hash(&mut h);
1023 attr_hash.hash(&mut h);
1024 matcap_id
1025 .map(|id| id.index as u64)
1026 .unwrap_or(u64::MAX)
1027 .hash(&mut h);
1028 warp_hash.hash(&mut h);
1029 metallic_roughness_id
1030 .map(|t| t.raw())
1031 .unwrap_or(u64::MAX)
1032 .hash(&mut h);
1033 emissive_texture_id
1034 .map(|t| t.raw())
1035 .unwrap_or(u64::MAX)
1036 .hash(&mut h);
1037 pos_override_gen.hash(&mut h);
1038 nrm_override_gen.hash(&mut h);
1039 h.finish()
1040 };
1041
1042 if prev_key == Some(cache_key) {
1047 return None;
1048 }
1049
1050 let albedo_view = match albedo_id {
1051 Some(id) if self.content.textures.get(id).is_some() => {
1052 &self.content.textures.get(id).unwrap().view
1053 }
1054 _ => &self.fallback_texture.view,
1055 };
1056 let normal_view = match normal_map_id {
1057 Some(id) if self.content.textures.get(id).is_some() => {
1058 &self.content.textures.get(id).unwrap().view
1059 }
1060 _ => &self.fallback_normal_map_view,
1061 };
1062 let ao_view = match ao_map_id {
1063 Some(id) if self.content.textures.get(id).is_some() => {
1064 &self.content.textures.get(id).unwrap().view
1065 }
1066 _ => &self.fallback_ao_map_view,
1067 };
1068 let lut_view = match lut_id {
1069 Some(id) if id.0 < self.content.colourmap_views.len() => {
1070 &self.content.colourmap_views[id.0]
1071 }
1072 _ => &self.content.fallback_lut_view,
1073 };
1074
1075 let scalar_buf: &wgpu::Buffer = match active_attr {
1076 Some(name) => {
1077 let found_vertex = mesh.attribute_buffers.get(name);
1078 let found_face = mesh.face_attribute_buffers.get(name);
1079 found_vertex
1080 .or(found_face)
1081 .unwrap_or(&self.content.fallback_scalar_buf)
1082 }
1083 None => &self.content.fallback_scalar_buf,
1084 };
1085
1086 let face_colour_buf: &wgpu::Buffer = match active_attr {
1087 Some(name) => mesh
1088 .face_colour_buffers
1089 .get(name)
1090 .unwrap_or(&self.content.fallback_face_colour_buf),
1091 None => &self.content.fallback_face_colour_buf,
1092 };
1093
1094 let matcap_view: &wgpu::TextureView = match matcap_id {
1095 Some(id) if id.index < self.content.matcap_views.len() => {
1096 &self.content.matcap_views[id.index]
1097 }
1098 _ => self
1099 .content
1100 .fallback_matcap_view
1101 .as_ref()
1102 .unwrap_or(&self.fallback_texture.view),
1103 };
1104
1105 let warp_buf: &wgpu::Buffer = match warp_attr {
1106 Some(name) => mesh
1107 .vector_attribute_buffers
1108 .get(name)
1109 .unwrap_or(&self.content.fallback_warp_buf),
1110 None => &self.content.fallback_warp_buf,
1111 };
1112
1113 let position_override_buf: &wgpu::Buffer = mesh
1114 .position_override_buffer
1115 .as_ref()
1116 .unwrap_or(&self.content.fallback_position_override_buf);
1117 let normal_override_buf: &wgpu::Buffer = mesh
1118 .normal_override_buffer
1119 .as_ref()
1120 .unwrap_or(&self.content.fallback_normal_override_buf);
1121
1122 let metallic_roughness_view: &wgpu::TextureView = match metallic_roughness_id {
1123 Some(id) if self.content.textures.get(id).is_some() => {
1124 &self.content.textures.get(id).unwrap().view
1125 }
1126 _ => &self.fallback_metallic_roughness_texture_view,
1127 };
1128 let emissive_view: &wgpu::TextureView = match emissive_texture_id {
1129 Some(id) if self.content.textures.get(id).is_some() => {
1130 &self.content.textures.get(id).unwrap().view
1131 }
1132 _ => &self.fallback_emissive_texture_view,
1133 };
1134
1135 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1136 label: Some("per_item_object_bind_group"),
1137 layout: &self.object_bind_group_layout,
1138 entries: &[
1139 wgpu::BindGroupEntry {
1140 binding: 0,
1141 resource: item_uniform_buf.as_entire_binding(),
1142 },
1143 wgpu::BindGroupEntry {
1144 binding: 1,
1145 resource: wgpu::BindingResource::TextureView(albedo_view),
1146 },
1147 wgpu::BindGroupEntry {
1148 binding: 2,
1149 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
1150 },
1151 wgpu::BindGroupEntry {
1152 binding: 3,
1153 resource: wgpu::BindingResource::TextureView(normal_view),
1154 },
1155 wgpu::BindGroupEntry {
1156 binding: 4,
1157 resource: wgpu::BindingResource::TextureView(ao_view),
1158 },
1159 wgpu::BindGroupEntry {
1160 binding: 5,
1161 resource: wgpu::BindingResource::TextureView(lut_view),
1162 },
1163 wgpu::BindGroupEntry {
1164 binding: 6,
1165 resource: scalar_buf.as_entire_binding(),
1166 },
1167 wgpu::BindGroupEntry {
1168 binding: 7,
1169 resource: wgpu::BindingResource::TextureView(matcap_view),
1170 },
1171 wgpu::BindGroupEntry {
1172 binding: 8,
1173 resource: face_colour_buf.as_entire_binding(),
1174 },
1175 wgpu::BindGroupEntry {
1176 binding: 9,
1177 resource: warp_buf.as_entire_binding(),
1178 },
1179 wgpu::BindGroupEntry {
1180 binding: 10,
1181 resource: wgpu::BindingResource::Sampler(&self.lut_sampler),
1182 },
1183 wgpu::BindGroupEntry {
1184 binding: 11,
1185 resource: wgpu::BindingResource::TextureView(metallic_roughness_view),
1186 },
1187 wgpu::BindGroupEntry {
1188 binding: 12,
1189 resource: wgpu::BindingResource::TextureView(emissive_view),
1190 },
1191 wgpu::BindGroupEntry {
1192 binding: 13,
1193 resource: position_override_buf.as_entire_binding(),
1194 },
1195 wgpu::BindGroupEntry {
1196 binding: 14,
1197 resource: normal_override_buf.as_entire_binding(),
1198 },
1199 ],
1200 });
1201 Some((bg, cache_key))
1202 }
1203
1204 pub fn upload_colourmap(
1209 &mut self,
1210 device: &wgpu::Device,
1211 queue: &wgpu::Queue,
1212 rgba_data: &[[u8; 4]; 256],
1213 ) -> ColourmapId {
1214 let texture = device.create_texture(&wgpu::TextureDescriptor {
1215 label: Some("lut_texture"),
1216 size: wgpu::Extent3d {
1217 width: 256,
1218 height: 1,
1219 depth_or_array_layers: 1,
1220 },
1221 mip_level_count: 1,
1222 sample_count: 1,
1223 dimension: wgpu::TextureDimension::D2,
1224 format: wgpu::TextureFormat::Rgba8Unorm,
1225 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1226 view_formats: &[],
1227 });
1228 let flat: Vec<u8> = rgba_data.iter().flat_map(|p| p.iter().copied()).collect();
1229 queue.write_texture(
1230 wgpu::TexelCopyTextureInfo {
1231 texture: &texture,
1232 mip_level: 0,
1233 origin: wgpu::Origin3d::ZERO,
1234 aspect: wgpu::TextureAspect::All,
1235 },
1236 &flat,
1237 wgpu::TexelCopyBufferLayout {
1238 offset: 0,
1239 bytes_per_row: Some(256 * 4),
1240 rows_per_image: Some(1),
1241 },
1242 wgpu::Extent3d {
1243 width: 256,
1244 height: 1,
1245 depth_or_array_layers: 1,
1246 },
1247 );
1248 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1249 let id = ColourmapId(self.content.colourmap_textures.len());
1250 self.content.colourmap_textures.push(texture);
1251 self.content.colourmap_views.push(view);
1252 self.content.colourmaps_cpu.push(*rgba_data);
1253 id
1254 }
1255
1256 pub fn get_colourmap_rgba(&self, id: ColourmapId) -> Option<&[[u8; 4]; 256]> {
1262 self.content.colourmaps_cpu.get(id.0)
1263 }
1264
1265 pub fn builtin_colourmap_id(&self, preset: BuiltinColourmap) -> ColourmapId {
1270 self.content
1271 .builtin_colourmap_ids
1272 .expect("call ensure_colourmaps_initialized before using built-in colourmaps")
1273 [preset as usize]
1274 }
1275
1276 pub fn ensure_colourmaps_initialized(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1281 if self.content.colourmaps_initialized {
1282 return;
1283 }
1284 let viridis = self.upload_colourmap(
1285 device,
1286 queue,
1287 &crate::resources::material::colourmap_data::viridis_rgba(),
1288 );
1289 let plasma = self.upload_colourmap(
1290 device,
1291 queue,
1292 &crate::resources::material::colourmap_data::plasma_rgba(),
1293 );
1294 let greyscale = self.upload_colourmap(
1295 device,
1296 queue,
1297 &crate::resources::material::colourmap_data::greyscale_rgba(),
1298 );
1299 let coolwarm = self.upload_colourmap(
1300 device,
1301 queue,
1302 &crate::resources::material::colourmap_data::coolwarm_rgba(),
1303 );
1304 let rainbow = self.upload_colourmap(
1305 device,
1306 queue,
1307 &crate::resources::material::colourmap_data::rainbow_rgba(),
1308 );
1309 let magma = self.upload_colourmap(
1310 device,
1311 queue,
1312 &crate::resources::material::colourmap_data::magma_rgba(),
1313 );
1314 let inferno = self.upload_colourmap(
1315 device,
1316 queue,
1317 &crate::resources::material::colourmap_data::inferno_rgba(),
1318 );
1319 let turbo = self.upload_colourmap(
1320 device,
1321 queue,
1322 &crate::resources::material::colourmap_data::turbo_rgba(),
1323 );
1324 let jet = self.upload_colourmap(
1325 device,
1326 queue,
1327 &crate::resources::material::colourmap_data::jet_rgba(),
1328 );
1329 let rdbu = self.upload_colourmap(
1330 device,
1331 queue,
1332 &crate::resources::material::colourmap_data::rdbu_r_rgba(),
1333 );
1334 self.content.builtin_colourmap_ids = Some([
1335 viridis, plasma, greyscale, coolwarm, rainbow, magma, inferno, turbo, jet, rdbu,
1336 ]);
1337 self.content.colourmaps_initialized = true;
1338 }
1339
1340 pub fn upload_matcap(
1355 &mut self,
1356 device: &wgpu::Device,
1357 queue: &wgpu::Queue,
1358 rgba_data: &[u8],
1359 blendable: bool,
1360 ) -> crate::error::ViewportResult<crate::resources::MatcapId> {
1361 let (width, height) = (256u32, 256u32);
1362 let expected = (width * height * 4) as usize;
1363 if rgba_data.len() != expected {
1364 return Err(crate::error::ViewportError::InvalidTextureData {
1365 expected,
1366 actual: rgba_data.len(),
1367 });
1368 }
1369
1370 let texture = device.create_texture(&wgpu::TextureDescriptor {
1371 label: Some("matcap_texture"),
1372 size: wgpu::Extent3d {
1373 width,
1374 height,
1375 depth_or_array_layers: 1,
1376 },
1377 mip_level_count: 1,
1378 sample_count: 1,
1379 dimension: wgpu::TextureDimension::D2,
1380 format: wgpu::TextureFormat::Rgba8Unorm,
1381 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1382 view_formats: &[],
1383 });
1384 queue.write_texture(
1385 wgpu::TexelCopyTextureInfo {
1386 texture: &texture,
1387 mip_level: 0,
1388 origin: wgpu::Origin3d::ZERO,
1389 aspect: wgpu::TextureAspect::All,
1390 },
1391 rgba_data,
1392 wgpu::TexelCopyBufferLayout {
1393 offset: 0,
1394 bytes_per_row: Some(width * 4),
1395 rows_per_image: Some(height),
1396 },
1397 wgpu::Extent3d {
1398 width,
1399 height,
1400 depth_or_array_layers: 1,
1401 },
1402 );
1403
1404 if self.content.matcap_sampler.is_none() {
1406 self.content.matcap_sampler = Some(crate::resources::builders::clamp_linear_sampler(
1407 device,
1408 "matcap_sampler",
1409 ));
1410 }
1411
1412 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1413 let index = self.content.matcap_textures.len();
1414 self.content.matcap_textures.push(texture);
1415 self.content.matcap_views.push(view);
1416
1417 if self.content.fallback_matcap_view.is_none() {
1420 self.content.fallback_matcap_view = Some(
1421 self.fallback_texture
1422 .texture
1423 .create_view(&wgpu::TextureViewDescriptor::default()),
1424 );
1425 }
1426
1427 tracing::debug!(matcap_index = index, blendable, "matcap uploaded");
1428 Ok(crate::resources::MatcapId { index, blendable })
1429 }
1430
1431 pub fn builtin_matcap_id(
1436 &self,
1437 preset: crate::resources::BuiltinMatcap,
1438 ) -> crate::resources::MatcapId {
1439 self.content.builtin_matcap_ids
1440 .expect("call ensure_matcaps_initialized (or run one prepare frame) before using built-in matcaps")
1441 [preset as usize]
1442 }
1443
1444 pub fn ensure_matcaps_initialized(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
1449 if self.content.matcaps_initialized {
1450 return;
1451 }
1452 use crate::resources::material::matcap_data;
1453 let clay = self
1454 .upload_matcap(device, queue, &matcap_data::clay(), true)
1455 .unwrap();
1456 let wax = self
1457 .upload_matcap(device, queue, &matcap_data::wax(), true)
1458 .unwrap();
1459 let candy = self
1460 .upload_matcap(device, queue, &matcap_data::candy(), true)
1461 .unwrap();
1462 let flat = self
1463 .upload_matcap(device, queue, &matcap_data::flat(), true)
1464 .unwrap();
1465 let ceramic = self
1466 .upload_matcap(device, queue, &matcap_data::ceramic(), false)
1467 .unwrap();
1468 let jade = self
1469 .upload_matcap(device, queue, &matcap_data::jade(), false)
1470 .unwrap();
1471 let mud = self
1472 .upload_matcap(device, queue, &matcap_data::mud(), false)
1473 .unwrap();
1474 let normal = self
1475 .upload_matcap(device, queue, &matcap_data::normal(), false)
1476 .unwrap();
1477 self.content.builtin_matcap_ids =
1478 Some([clay, wax, candy, flat, ceramic, jade, mud, normal]);
1479 self.content.matcaps_initialized = true;
1480 }
1481}
1482
1483#[cfg(test)]
1484mod async_texture_tests {
1485 use crate::DeviceResources;
1486 use crate::resources::UploadStatus;
1487
1488 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
1489 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
1490 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1491 power_preference: wgpu::PowerPreference::LowPower,
1492 compatible_surface: None,
1493 force_fallback_adapter: false,
1494 }))
1495 .ok()?;
1496 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
1497 }
1498
1499 fn try_make_bc_device() -> Option<(wgpu::Device, wgpu::Queue)> {
1503 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
1504 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1505 power_preference: wgpu::PowerPreference::LowPower,
1506 compatible_surface: None,
1507 force_fallback_adapter: false,
1508 }))
1509 .ok()?;
1510 if !adapter
1511 .features()
1512 .contains(wgpu::Features::TEXTURE_COMPRESSION_BC)
1513 {
1514 return None;
1515 }
1516 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1517 required_features: wgpu::Features::TEXTURE_COMPRESSION_BC,
1518 ..Default::default()
1519 }))
1520 .ok()
1521 }
1522
1523 fn drive_until_ready(
1524 resources: &mut DeviceResources,
1525 device: &wgpu::Device,
1526 queue: &wgpu::Queue,
1527 id: crate::resources::JobId,
1528 ) {
1529 for _ in 0..200 {
1530 resources.process_uploads(device, queue);
1531 match resources.upload_status(id) {
1532 UploadStatus::Ready => return,
1533 UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
1534 UploadStatus::Pending { .. } => {
1535 std::thread::sleep(std::time::Duration::from_millis(5));
1536 }
1537 UploadStatus::Unknown => panic!("job id disappeared"),
1538 }
1539 }
1540 panic!("texture upload did not complete in time");
1541 }
1542
1543 #[test]
1544 fn invalid_size_errors_synchronously() {
1545 let Some((device, queue)) = try_make_device() else {
1546 eprintln!("skipping: no wgpu adapter available");
1547 return;
1548 };
1549 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1550
1551 let rgba = vec![0u8; 12];
1554 let err = resources
1555 .begin_upload_texture(&device, &queue, 2, 2, rgba)
1556 .expect_err("invalid size should error");
1557 assert!(matches!(
1558 err,
1559 crate::error::ViewportError::InvalidTextureData {
1560 expected: 16,
1561 actual: 12
1562 }
1563 ));
1564 assert_eq!(resources.uploads_pending(), 0);
1565 }
1566
1567 #[test]
1568 fn begin_upload_texture_completes_and_yields_id() {
1569 let Some((device, queue)) = try_make_device() else {
1570 eprintln!("skipping: no wgpu adapter available");
1571 return;
1572 };
1573 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1574
1575 let rgba = vec![128u8; 4 * 4 * 4];
1576 let id = resources
1577 .begin_upload_texture(&device, &queue, 4, 4, rgba)
1578 .unwrap();
1579 assert_eq!(resources.uploads_pending(), 1);
1580
1581 let err = resources.upload_result_texture(id).unwrap_err();
1583 assert!(matches!(err, crate::error::ViewportError::JobNotReady));
1584
1585 drive_until_ready(&mut resources, &device, &queue, id);
1586
1587 let tex_id = resources.upload_result_texture(id).expect("ready result");
1588 assert_eq!(tex_id, crate::resources::TextureId(0));
1590
1591 let err = resources.upload_result_texture(id).unwrap_err();
1593 assert!(matches!(
1594 err,
1595 crate::error::ViewportError::JobResultMissing { .. }
1596 ));
1597 }
1598
1599 #[test]
1600 fn begin_upload_normal_map_routes_to_same_result_accessor() {
1601 let Some((device, queue)) = try_make_device() else {
1602 eprintln!("skipping: no wgpu adapter available");
1603 return;
1604 };
1605 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1606
1607 let rgba = vec![64u8; 8 * 8 * 4];
1608 let id = resources
1609 .begin_upload_normal_map(&device, &queue, 8, 8, rgba)
1610 .unwrap();
1611 drive_until_ready(&mut resources, &device, &queue, id);
1612 let tex_id = resources.upload_result_texture(id).expect("ready result");
1613 assert_eq!(tex_id, crate::resources::TextureId(0));
1614 }
1615
1616 #[test]
1617 fn sync_upload_still_works() {
1618 let Some((device, queue)) = try_make_device() else {
1619 eprintln!("skipping: no wgpu adapter available");
1620 return;
1621 };
1622 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1623
1624 let rgba = vec![200u8; 4 * 4 * 4];
1625 let tex_id = resources
1626 .upload_texture(&device, &queue, 4, 4, &rgba)
1627 .unwrap();
1628 assert_eq!(tex_id, crate::resources::TextureId(0));
1629 }
1630
1631 #[test]
1632 fn replace_texture_keeps_handle_and_updates_bytes() {
1633 let Some((device, queue)) = try_make_device() else {
1634 eprintln!("skipping: no wgpu adapter available");
1635 return;
1636 };
1637 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1638
1639 let id = resources
1640 .upload_texture(&device, &queue, 4, 4, &vec![200u8; 4 * 4 * 4])
1641 .unwrap();
1642 let bytes_4x4 = resources.resident_bytes().texture_bytes;
1643
1644 resources
1646 .replace_texture(&device, &queue, id, 8, 8, &vec![10u8; 8 * 8 * 4])
1647 .expect("replace on a live handle succeeds");
1648 assert!(resources.texture_view(id).is_some(), "handle stays valid");
1649 let bytes_8x8 = resources.resident_bytes().texture_bytes;
1650 assert!(
1651 bytes_8x8 > bytes_4x4,
1652 "replacing with a larger image must grow resident bytes"
1653 );
1654
1655 let err = resources
1657 .replace_texture(&device, &queue, id, 8, 8, &[0u8; 3])
1658 .unwrap_err();
1659 assert!(matches!(
1660 err,
1661 crate::error::ViewportError::InvalidTextureData { .. }
1662 ));
1663
1664 assert!(resources.free_texture(id));
1666 let err = resources
1667 .replace_texture(&device, &queue, id, 4, 4, &vec![0u8; 4 * 4 * 4])
1668 .unwrap_err();
1669 assert!(matches!(
1670 err,
1671 crate::error::ViewportError::StaleHandle { .. }
1672 ));
1673 }
1674
1675 #[test]
1676 fn block_layout_matches_format() {
1677 use super::mip_block_layout;
1678 use wgpu::TextureFormat as F;
1679
1680 assert_eq!(mip_block_layout(F::Rgba8Unorm, 4, 4), (16, 4, 64));
1682
1683 assert_eq!(mip_block_layout(F::Bc7RgbaUnormSrgb, 4, 4), (16, 1, 16));
1685 assert_eq!(mip_block_layout(F::Bc7RgbaUnormSrgb, 8, 8), (32, 2, 64));
1686 assert_eq!(mip_block_layout(F::Bc7RgbaUnormSrgb, 6, 6), (32, 2, 64));
1688
1689 assert_eq!(mip_block_layout(F::Bc4RUnorm, 8, 8), (16, 2, 32));
1691
1692 let astc = F::Astc {
1694 block: wgpu::AstcBlock::B8x8,
1695 channel: wgpu::AstcChannel::Unorm,
1696 };
1697 assert_eq!(mip_block_layout(astc, 16, 16), (32, 2, 64));
1698 }
1699
1700 #[test]
1701 fn block_layout_full_mip_pyramid_sum() {
1702 use super::mip_block_layout;
1703 let mut total = 0usize;
1705 let (w, h) = (64u32, 64u32);
1706 for level in 0..=6 {
1707 let lw = (w >> level).max(1);
1708 let lh = (h >> level).max(1);
1709 let (_, _, bytes) = mip_block_layout(wgpu::TextureFormat::Bc7RgbaUnormSrgb, lw, lh);
1710 total += bytes;
1711 }
1712 assert_eq!(total, 5488);
1713 }
1714
1715 #[test]
1716 fn supports_texture_format_false_without_feature() {
1717 let Some((device, _queue)) = try_make_device() else {
1718 eprintln!("skipping: no wgpu adapter available");
1719 return;
1720 };
1721 assert!(!crate::resources::supports_texture_format(
1724 &device,
1725 wgpu::TextureFormat::Bc7RgbaUnormSrgb
1726 ));
1727 assert!(crate::resources::supports_texture_format(
1729 &device,
1730 wgpu::TextureFormat::Rgba8Unorm
1731 ));
1732 }
1733
1734 #[test]
1735 fn compressed_upload_rejects_unsupported_and_non_block_formats() {
1736 let Some((device, queue)) = try_make_device() else {
1737 eprintln!("skipping: no wgpu adapter available");
1738 return;
1739 };
1740 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1741
1742 let block = vec![0u8; 16];
1744 let err = resources
1745 .begin_upload_compressed_texture(
1746 &device,
1747 &queue,
1748 crate::resources::CompressedTextureDesc {
1749 width: 4,
1750 height: 4,
1751 format: wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1752 is_normal_map: false,
1753 mip_levels: &[&block],
1754 },
1755 )
1756 .expect_err("BC7 upload without the feature should error");
1757 assert!(matches!(
1758 err,
1759 crate::error::ViewportError::UnsupportedTextureFormat { .. }
1760 ));
1761
1762 let rgba = vec![0u8; 4 * 4 * 4];
1764 let err = resources
1765 .begin_upload_compressed_texture(
1766 &device,
1767 &queue,
1768 crate::resources::CompressedTextureDesc {
1769 width: 4,
1770 height: 4,
1771 format: wgpu::TextureFormat::Rgba8Unorm,
1772 is_normal_map: false,
1773 mip_levels: &[&rgba],
1774 },
1775 )
1776 .expect_err("non-compressed format should error");
1777 assert!(matches!(
1778 err,
1779 crate::error::ViewportError::UnsupportedTextureFormat { .. }
1780 ));
1781 assert_eq!(resources.uploads_pending(), 0);
1782 }
1783
1784 #[test]
1785 fn compressed_upload_validates_level_lengths() {
1786 let Some((device, queue)) = try_make_bc_device() else {
1787 eprintln!("skipping: no adapter with TEXTURE_COMPRESSION_BC");
1788 return;
1789 };
1790 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1791
1792 let bad = vec![0u8; 15];
1795 let err = resources
1796 .begin_upload_compressed_texture(
1797 &device,
1798 &queue,
1799 crate::resources::CompressedTextureDesc {
1800 width: 4,
1801 height: 4,
1802 format: wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1803 is_normal_map: false,
1804 mip_levels: &[&bad],
1805 },
1806 )
1807 .expect_err("wrong block length should error");
1808 assert!(matches!(
1809 err,
1810 crate::error::ViewportError::InvalidCompressedTextureData {
1811 level: 0,
1812 expected: 16,
1813 actual: 15,
1814 }
1815 ));
1816
1817 let err = resources
1819 .begin_upload_compressed_texture(
1820 &device,
1821 &queue,
1822 crate::resources::CompressedTextureDesc {
1823 width: 4,
1824 height: 4,
1825 format: wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1826 is_normal_map: false,
1827 mip_levels: &[],
1828 },
1829 )
1830 .expect_err("empty mip chain should error");
1831 assert!(matches!(
1832 err,
1833 crate::error::ViewportError::InvalidCompressedTextureData { actual: 0, .. }
1834 ));
1835 assert_eq!(resources.uploads_pending(), 0);
1836 }
1837
1838 #[test]
1839 fn compressed_upload_completes_and_counts_bytes() {
1840 let Some((device, queue)) = try_make_bc_device() else {
1841 eprintln!("skipping: no adapter with TEXTURE_COMPRESSION_BC");
1842 return;
1843 };
1844 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1845
1846 let before = resources.texture_memory_stats().used_bytes;
1847 let block = vec![0u8; 16];
1850 let id = resources
1851 .begin_upload_compressed_texture(
1852 &device,
1853 &queue,
1854 crate::resources::CompressedTextureDesc {
1855 width: 4,
1856 height: 4,
1857 format: wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1858 is_normal_map: false,
1859 mip_levels: &[&block],
1860 },
1861 )
1862 .unwrap();
1863 drive_until_ready(&mut resources, &device, &queue, id);
1864 let tex_id = resources.upload_result_texture(id).expect("ready result");
1865 assert_eq!(tex_id, crate::resources::TextureId(0));
1866
1867 let stats = resources.texture_memory_stats();
1868 assert_eq!(stats.used_bytes - before, 16);
1869 assert_eq!(stats.texture_count, 1);
1870 }
1871
1872 #[test]
1873 fn compressed_upload_rejects_non_block_aligned_dimensions() {
1874 let Some((device, queue)) = try_make_device() else {
1875 eprintln!("skipping: no wgpu adapter available");
1876 return;
1877 };
1878 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
1879
1880 for (w, h) in [(6u32, 6u32), (1419, 1024), (1024, 1419)] {
1885 let blocks_x = w.div_ceil(4);
1886 let blocks_y = h.div_ceil(4);
1887 let block = vec![0u8; (blocks_x * blocks_y * 16) as usize];
1888 let err = resources
1889 .begin_upload_compressed_texture(
1890 &device,
1891 &queue,
1892 crate::resources::CompressedTextureDesc {
1893 width: w,
1894 height: h,
1895 format: wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1896 is_normal_map: false,
1897 mip_levels: &[&block],
1898 },
1899 )
1900 .expect_err("non-block-aligned dimensions must be rejected");
1901 assert!(matches!(
1902 err,
1903 crate::error::ViewportError::CompressedTextureNotBlockAligned { .. }
1904 ));
1905 }
1906 assert_eq!(resources.uploads_pending(), 0);
1907 }
1908}
1909
1910pub struct GpuTexture {
1916 pub texture: wgpu::Texture,
1918 pub view: wgpu::TextureView,
1920 pub sampler: wgpu::Sampler,
1922 pub bind_group: wgpu::BindGroup,
1924}