1use std::collections::HashMap;
11
12use bytemuck::{Pod, Zeroable};
13use wgpu::util::DeviceExt;
14
15use crate::error::ViewportResult;
16use crate::resources::ViewportGpuResources;
17use crate::resources::mesh_sidecar::registry::{
18 DeformerDesc, DeformerId, MESH_FAMILY_SHADERS, StoredDeformer, allocate_internal_slot,
19 allocate_slot, compose_shader, lookup_source, validate_name, validate_with_wgpu,
20};
21use crate::resources::mesh_store::MeshId;
22
23pub const DEFORM_SLOT_COUNT: usize = 8;
32
33pub const DEFORM_INTERNAL_SLOT_START: usize = 4;
35
36pub const DEFORM_PARAMS_PER_SLOT: usize = 4;
39
40const SLOT_LAYOUT_WORDS: usize = DEFORM_SLOT_COUNT * 2;
43
44#[repr(C)]
47#[derive(Copy, Clone, Pod, Zeroable)]
48pub(crate) struct DeformHeader {
49 pub time_seconds: f32,
50 pub _pad: [f32; 3],
51 pub slot_params: [[f32; 4]; DEFORM_SLOT_COUNT * DEFORM_PARAMS_PER_SLOT],
53}
54
55impl DeformHeader {
56 pub fn zeroed() -> Self {
57 Self {
58 time_seconds: 0.0,
59 _pad: [0.0; 3],
60 slot_params: [[0.0; 4]; DEFORM_SLOT_COUNT * DEFORM_PARAMS_PER_SLOT],
61 }
62 }
63}
64
65pub const DEFORM_SLOT_PARAMS_BYTES: u64 =
70 (DEFORM_PARAMS_PER_SLOT * std::mem::size_of::<[f32; 4]>()) as u64;
71const DEFORM_HEADER_PREFIX_BYTES: u64 = 16;
72
73#[inline]
76pub const fn deform_slot_params_byte_offset(slot: usize) -> u64 {
77 DEFORM_HEADER_PREFIX_BYTES + (slot as u64) * DEFORM_SLOT_PARAMS_BYTES
78}
79
80#[derive(Clone)]
84pub struct DeformSlotHandle {
85 header_buffer: wgpu::Buffer,
86 slot: usize,
87}
88
89impl DeformSlotHandle {
90 pub fn slot(&self) -> usize {
92 self.slot
93 }
94
95 pub fn write(&self, queue: &wgpu::Queue, params: &[[f32; 4]; DEFORM_PARAMS_PER_SLOT]) {
98 queue.write_buffer(
99 &self.header_buffer,
100 deform_slot_params_byte_offset(self.slot),
101 bytemuck::cast_slice(params),
102 );
103 }
104}
105
106pub(crate) struct InstanceDeform {
111 pub slot_data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT],
112 pub slot_stride: [u32; DEFORM_SLOT_COUNT],
113 pub buffer: wgpu::Buffer,
114 pub buffer_capacity: u64,
117 pub bind_group: wgpu::BindGroup,
118 pub flag_bits: u32,
120}
121
122pub(crate) struct MeshDeform {
124 pub slot_data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT],
127 pub slot_stride: [u32; DEFORM_SLOT_COUNT],
130 pub buffer: wgpu::Buffer,
133 pub bind_group: wgpu::BindGroup,
137 pub flag_bits: u32,
139 pub instance_flag_bits_union: u32,
144 pub instances: HashMap<u32, InstanceDeform>,
145}
146
147pub(crate) struct DeformationState {
149 pub enabled: bool,
153 pub bind_group_layout: wgpu::BindGroupLayout,
154 pub header_buffer: wgpu::Buffer,
155 #[allow(dead_code)]
159 pub dummy_data_buffer: wgpu::Buffer,
160 pub dummy_instance_buffer: wgpu::Buffer,
164 pub dummy_bind_group: wgpu::BindGroup,
168 pub meshes: HashMap<MeshId, MeshDeform>,
169 pub header_cpu: DeformHeader,
170 pub registrations: Vec<StoredDeformer>,
172}
173
174impl DeformationState {
175 pub fn new(device: &wgpu::Device) -> Self {
176 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
177 label: Some("deform_bgl"),
178 entries: &[
179 wgpu::BindGroupLayoutEntry {
180 binding: 0,
181 visibility: wgpu::ShaderStages::VERTEX,
182 ty: wgpu::BindingType::Buffer {
183 ty: wgpu::BufferBindingType::Uniform,
184 has_dynamic_offset: false,
185 min_binding_size: None,
186 },
187 count: None,
188 },
189 wgpu::BindGroupLayoutEntry {
190 binding: 1,
191 visibility: wgpu::ShaderStages::VERTEX,
192 ty: wgpu::BindingType::Buffer {
193 ty: wgpu::BufferBindingType::Storage { read_only: true },
194 has_dynamic_offset: false,
195 min_binding_size: None,
196 },
197 count: None,
198 },
199 wgpu::BindGroupLayoutEntry {
200 binding: 2,
201 visibility: wgpu::ShaderStages::VERTEX,
202 ty: wgpu::BindingType::Buffer {
203 ty: wgpu::BufferBindingType::Storage { read_only: true },
204 has_dynamic_offset: false,
205 min_binding_size: None,
206 },
207 count: None,
208 },
209 ],
210 });
211
212 let header_cpu = DeformHeader::zeroed();
213 let header_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
214 label: Some("deform_header"),
215 contents: bytemuck::bytes_of(&header_cpu),
216 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
217 });
218
219 let dummy_words = vec![0u32; SLOT_LAYOUT_WORDS];
220 let dummy_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
221 label: Some("deform_dummy_data"),
222 contents: bytemuck::cast_slice(&dummy_words),
223 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
224 });
225 let dummy_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
226 label: Some("deform_dummy_instance_data"),
227 contents: bytemuck::cast_slice(&dummy_words),
228 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
229 });
230
231 let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
232 label: Some("deform_dummy_bg"),
233 layout: &bind_group_layout,
234 entries: &[
235 wgpu::BindGroupEntry {
236 binding: 0,
237 resource: header_buffer.as_entire_binding(),
238 },
239 wgpu::BindGroupEntry {
240 binding: 1,
241 resource: dummy_data_buffer.as_entire_binding(),
242 },
243 wgpu::BindGroupEntry {
244 binding: 2,
245 resource: dummy_instance_buffer.as_entire_binding(),
246 },
247 ],
248 });
249
250 Self {
251 enabled: true,
252 bind_group_layout,
253 header_buffer,
254 dummy_data_buffer,
255 dummy_instance_buffer,
256 dummy_bind_group,
257 meshes: HashMap::new(),
258 header_cpu,
259 registrations: Vec::new(),
260 }
261 }
262
263 pub fn new_disabled(device: &wgpu::Device) -> Self {
269 let mut s = Self::new(device);
270 s.enabled = false;
271 s
272 }
273
274 #[allow(dead_code)]
279 pub fn bind_group_for(&self, mesh_id: MeshId) -> &wgpu::BindGroup {
280 self.meshes
281 .get(&mesh_id)
282 .map(|m| &m.bind_group)
283 .unwrap_or(&self.dummy_bind_group)
284 }
285
286 pub fn flag_bits(&self, mesh_id: MeshId) -> u32 {
291 self.meshes
292 .get(&mesh_id)
293 .map(|m| m.flag_bits | m.instance_flag_bits_union)
294 .unwrap_or(0)
295 }
296
297 pub(crate) fn has_per_instance_deform_data(
302 &self,
303 mesh_id: MeshId,
304 instance_id: Option<u32>,
305 ) -> bool {
306 let Some(id) = instance_id else {
307 return false;
308 };
309 self.meshes
310 .get(&mesh_id)
311 .and_then(|m| m.instances.get(&id))
312 .is_some_and(|i| i.flag_bits != 0)
313 }
314
315 #[allow(dead_code)]
319 pub fn instance_bind_group_for(
320 &self,
321 mesh_id: MeshId,
322 instance_id: Option<u32>,
323 ) -> &wgpu::BindGroup {
324 let Some(mesh) = self.meshes.get(&mesh_id) else {
325 return &self.dummy_bind_group;
326 };
327 if let Some(id) = instance_id {
328 if let Some(inst) = mesh.instances.get(&id) {
329 return &inst.bind_group;
330 }
331 }
332 &mesh.bind_group
333 }
334
335 fn pack(
338 slot_data: &[Option<Vec<u8>>; DEFORM_SLOT_COUNT],
339 slot_stride: &[u32; DEFORM_SLOT_COUNT],
340 ) -> Vec<u32> {
341 let mut words = vec![0u32; SLOT_LAYOUT_WORDS];
342 for slot in 0..DEFORM_SLOT_COUNT {
343 if let Some(bytes) = &slot_data[slot] {
344 let offset_words = words.len() as u32;
345 words[slot * 2] = offset_words;
346 words[slot * 2 + 1] = slot_stride[slot];
347 let extra = bytes.len() / 4;
348 words.reserve(extra);
349 for chunk in bytes.chunks_exact(4) {
350 words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
351 }
352 }
353 }
354 words
355 }
356
357 fn make_bind_group(
358 &self,
359 device: &wgpu::Device,
360 label: &str,
361 mesh_buffer: &wgpu::Buffer,
362 instance_buffer: &wgpu::Buffer,
363 ) -> wgpu::BindGroup {
364 device.create_bind_group(&wgpu::BindGroupDescriptor {
365 label: Some(label),
366 layout: &self.bind_group_layout,
367 entries: &[
368 wgpu::BindGroupEntry {
369 binding: 0,
370 resource: self.header_buffer.as_entire_binding(),
371 },
372 wgpu::BindGroupEntry {
373 binding: 1,
374 resource: mesh_buffer.as_entire_binding(),
375 },
376 wgpu::BindGroupEntry {
377 binding: 2,
378 resource: instance_buffer.as_entire_binding(),
379 },
380 ],
381 })
382 }
383
384 fn refresh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
389 let Some(m) = self.meshes.get(&mesh_id) else {
390 return;
391 };
392 if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
393 self.meshes.remove(&mesh_id);
394 return;
395 }
396 let words = Self::pack(&m.slot_data, &m.slot_stride);
397 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
398 label: Some("deform_mesh_data"),
399 contents: bytemuck::cast_slice(&words),
400 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
401 });
402 let new_bg = self.make_bind_group(
403 device,
404 "deform_mesh_bg",
405 &new_buffer,
406 &self.dummy_instance_buffer,
407 );
408
409 let instance_ids: Vec<u32> = self
411 .meshes
412 .get(&mesh_id)
413 .unwrap()
414 .instances
415 .keys()
416 .copied()
417 .collect();
418 for id in instance_ids {
419 let inst_buf_clone = {
420 let inst = self
421 .meshes
422 .get(&mesh_id)
423 .unwrap()
424 .instances
425 .get(&id)
426 .unwrap();
427 inst.buffer.clone()
430 };
431 let bg = self.make_bind_group(
432 device,
433 "deform_mesh_instance_bg",
434 &new_buffer,
435 &inst_buf_clone,
436 );
437 let inst = self
438 .meshes
439 .get_mut(&mesh_id)
440 .unwrap()
441 .instances
442 .get_mut(&id)
443 .unwrap();
444 inst.bind_group = bg;
445 }
446
447 let m = self.meshes.get_mut(&mesh_id).unwrap();
448 m.buffer = new_buffer;
449 m.bind_group = new_bg;
450 }
451
452 fn ensure_mesh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
453 if self.meshes.contains_key(&mesh_id) {
454 return;
455 }
456 let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
457 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
458 label: Some("deform_mesh_data_init"),
459 contents: bytemuck::cast_slice(&init_words),
460 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
461 });
462 let bind_group = self.make_bind_group(
463 device,
464 "deform_mesh_bg",
465 &buffer,
466 &self.dummy_instance_buffer,
467 );
468 self.meshes.insert(
469 mesh_id,
470 MeshDeform {
471 slot_data: Default::default(),
472 slot_stride: [0; DEFORM_SLOT_COUNT],
473 buffer,
474 bind_group,
475 flag_bits: 0,
476 instance_flag_bits_union: 0,
477 instances: HashMap::new(),
478 },
479 );
480 }
481
482 pub fn attach_slot(
486 &mut self,
487 device: &wgpu::Device,
488 mesh_id: MeshId,
489 slot: usize,
490 stride_words: u32,
491 data: &[u8],
492 ) {
493 assert!(slot < DEFORM_SLOT_COUNT);
494 assert!(
495 data.len() % 4 == 0,
496 "deform slot data length must be a multiple of 4 bytes"
497 );
498 self.ensure_mesh(device, mesh_id);
499 let entry = self.meshes.get_mut(&mesh_id).unwrap();
500 entry.slot_data[slot] = Some(data.to_vec());
501 entry.slot_stride[slot] = stride_words;
502 entry.flag_bits |= 1u32 << slot;
503 self.refresh(device, mesh_id);
504 }
505
506 pub fn detach_slot(&mut self, device: &wgpu::Device, mesh_id: MeshId, slot: usize) -> bool {
508 assert!(slot < DEFORM_SLOT_COUNT);
509 let Some(m) = self.meshes.get_mut(&mesh_id) else {
510 return false;
511 };
512 let had = m.slot_data[slot].take().is_some();
513 if had {
514 m.slot_stride[slot] = 0;
515 m.flag_bits &= !(1u32 << slot);
516 self.refresh(device, mesh_id);
517 }
518 had
519 }
520
521 pub fn has_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
522 self.meshes
523 .get(&mesh_id)
524 .map(|m| m.slot_data[slot].is_some())
525 .unwrap_or(false)
526 }
527
528 fn recompute_instance_union(m: &mut MeshDeform) {
529 let mut union = 0u32;
530 for inst in m.instances.values() {
531 union |= inst.flag_bits;
532 }
533 m.instance_flag_bits_union = union;
534 }
535
536 pub fn attach_slot_instance(
542 &mut self,
543 device: &wgpu::Device,
544 queue: &wgpu::Queue,
545 mesh_id: MeshId,
546 instance_id: u32,
547 slot: usize,
548 stride_words: u32,
549 data: &[u8],
550 ) {
551 assert!(slot < DEFORM_SLOT_COUNT);
552 assert!(
553 data.len() % 4 == 0,
554 "deform slot data length must be a multiple of 4 bytes"
555 );
556 self.ensure_mesh(device, mesh_id);
557 let needs_insert = !self
559 .meshes
560 .get(&mesh_id)
561 .unwrap()
562 .instances
563 .contains_key(&instance_id);
564 if needs_insert {
565 let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
566 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
567 label: Some("deform_mesh_instance_data_init"),
568 contents: bytemuck::cast_slice(&init_words),
569 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
570 });
571 let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
572 let bind_group =
573 self.make_bind_group(device, "deform_mesh_instance_bg", &mesh_buf_clone, &buffer);
574 let m = self.meshes.get_mut(&mesh_id).unwrap();
575 let cap = (init_words.len() * 4) as u64;
576 m.instances.insert(
577 instance_id,
578 InstanceDeform {
579 slot_data: Default::default(),
580 slot_stride: [0; DEFORM_SLOT_COUNT],
581 buffer,
582 buffer_capacity: cap,
583 bind_group,
584 flag_bits: 0,
585 },
586 );
587 }
588
589 {
591 let inst = self
592 .meshes
593 .get_mut(&mesh_id)
594 .unwrap()
595 .instances
596 .get_mut(&instance_id)
597 .unwrap();
598 inst.slot_data[slot] = Some(data.to_vec());
599 inst.slot_stride[slot] = stride_words;
600 inst.flag_bits |= 1u32 << slot;
601 }
602
603 let (packed_words, packed_bytes_len) = {
605 let inst = self
606 .meshes
607 .get(&mesh_id)
608 .unwrap()
609 .instances
610 .get(&instance_id)
611 .unwrap();
612 let w = Self::pack(&inst.slot_data, &inst.slot_stride);
613 let len = (w.len() * 4) as u64;
614 (w, len)
615 };
616
617 let needs_realloc = {
618 let inst = self
619 .meshes
620 .get(&mesh_id)
621 .unwrap()
622 .instances
623 .get(&instance_id)
624 .unwrap();
625 packed_bytes_len > inst.buffer_capacity
626 };
627
628 if needs_realloc {
629 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
630 label: Some("deform_mesh_instance_data"),
631 contents: bytemuck::cast_slice(&packed_words),
632 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
633 });
634 let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
635 let bg = self.make_bind_group(
636 device,
637 "deform_mesh_instance_bg",
638 &mesh_buf_clone,
639 &new_buffer,
640 );
641 let inst = self
642 .meshes
643 .get_mut(&mesh_id)
644 .unwrap()
645 .instances
646 .get_mut(&instance_id)
647 .unwrap();
648 inst.buffer = new_buffer;
649 inst.buffer_capacity = packed_bytes_len;
650 inst.bind_group = bg;
651 } else {
652 let inst = self
653 .meshes
654 .get(&mesh_id)
655 .unwrap()
656 .instances
657 .get(&instance_id)
658 .unwrap();
659 queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
660 }
661
662 let m = self.meshes.get_mut(&mesh_id).unwrap();
664 Self::recompute_instance_union(m);
665 }
666
667 pub fn detach_slot_instance(
671 &mut self,
672 device: &wgpu::Device,
673 queue: &wgpu::Queue,
674 mesh_id: MeshId,
675 instance_id: u32,
676 slot: usize,
677 ) -> bool {
678 assert!(slot < DEFORM_SLOT_COUNT);
679 let Some(m) = self.meshes.get_mut(&mesh_id) else {
680 return false;
681 };
682 let Some(inst) = m.instances.get_mut(&instance_id) else {
683 return false;
684 };
685 let had = inst.slot_data[slot].take().is_some();
686 if !had {
687 return false;
688 }
689 inst.slot_stride[slot] = 0;
690 inst.flag_bits &= !(1u32 << slot);
691 let drop_instance = inst.flag_bits == 0;
692
693 if drop_instance {
694 m.instances.remove(&instance_id);
695 } else {
696 let (packed_words, packed_bytes_len) = {
697 let inst = m.instances.get(&instance_id).unwrap();
698 let w = Self::pack(&inst.slot_data, &inst.slot_stride);
699 let len = (w.len() * 4) as u64;
700 (w, len)
701 };
702 let inst = m.instances.get(&instance_id).unwrap();
703 if packed_bytes_len > inst.buffer_capacity {
704 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
705 label: Some("deform_mesh_instance_data"),
706 contents: bytemuck::cast_slice(&packed_words),
707 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
708 });
709 let mesh_buf_clone = m.buffer.clone();
710 let bg = self.make_bind_group(
711 device,
712 "deform_mesh_instance_bg",
713 &mesh_buf_clone,
714 &new_buffer,
715 );
716 let m = self.meshes.get_mut(&mesh_id).unwrap();
717 let inst = m.instances.get_mut(&instance_id).unwrap();
718 inst.buffer = new_buffer;
719 inst.buffer_capacity = packed_bytes_len;
720 inst.bind_group = bg;
721 } else {
722 queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
723 }
724 }
725
726 let m = self.meshes.get_mut(&mesh_id).unwrap();
727 Self::recompute_instance_union(m);
728 if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
730 self.meshes.remove(&mesh_id);
731 }
732 true
733 }
734
735 pub fn has_slot_instance(&self, mesh_id: MeshId, instance_id: u32, slot: usize) -> bool {
736 self.meshes
737 .get(&mesh_id)
738 .and_then(|m| m.instances.get(&instance_id))
739 .map(|i| i.slot_data[slot].is_some())
740 .unwrap_or(false)
741 }
742}
743
744impl ViewportGpuResources {
749 pub fn set_deform_time(&mut self, queue: &wgpu::Queue, time_seconds: f32) {
752 self.deform.header_cpu.time_seconds = time_seconds;
753 queue.write_buffer(
754 &self.deform.header_buffer,
755 0,
756 bytemuck::bytes_of(&self.deform.header_cpu),
757 );
758 }
759
760 pub fn set_deform_slot_params(
763 &mut self,
764 queue: &wgpu::Queue,
765 slot: usize,
766 params: [[f32; 4]; DEFORM_PARAMS_PER_SLOT],
767 ) {
768 assert!(slot < DEFORM_SLOT_COUNT);
769 let base = slot * DEFORM_PARAMS_PER_SLOT;
770 for (i, p) in params.iter().enumerate() {
771 self.deform.header_cpu.slot_params[base + i] = *p;
772 }
773 queue.write_buffer(
774 &self.deform.header_buffer,
775 0,
776 bytemuck::bytes_of(&self.deform.header_cpu),
777 );
778 }
779
780 pub fn attach_deform_slot(
788 &mut self,
789 device: &wgpu::Device,
790 mesh_id: MeshId,
791 slot: usize,
792 stride_bytes: u32,
793 data: &[u8],
794 ) {
795 assert!(
796 stride_bytes >= 4 && stride_bytes % 4 == 0,
797 "deform slot stride must be a positive multiple of 4 bytes"
798 );
799 self.deform
800 .attach_slot(device, mesh_id, slot, stride_bytes / 4, data);
801 }
802
803 pub fn detach_deform_slot(
805 &mut self,
806 device: &wgpu::Device,
807 mesh_id: MeshId,
808 slot: usize,
809 ) -> bool {
810 self.deform.detach_slot(device, mesh_id, slot)
811 }
812
813 pub fn has_deform_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
816 self.deform.has_slot(mesh_id, slot)
817 }
818
819 pub fn attach_deform_slot_instance(
824 &mut self,
825 device: &wgpu::Device,
826 queue: &wgpu::Queue,
827 mesh_id: MeshId,
828 instance_id: u32,
829 slot: usize,
830 stride_bytes: u32,
831 data: &[u8],
832 ) {
833 assert!(
834 stride_bytes >= 4 && stride_bytes % 4 == 0,
835 "deform slot stride must be a positive multiple of 4 bytes"
836 );
837 self.deform.attach_slot_instance(
838 device,
839 queue,
840 mesh_id,
841 instance_id,
842 slot,
843 stride_bytes / 4,
844 data,
845 );
846 }
847
848 pub fn detach_deform_slot_instance(
851 &mut self,
852 device: &wgpu::Device,
853 queue: &wgpu::Queue,
854 mesh_id: MeshId,
855 instance_id: u32,
856 slot: usize,
857 ) -> bool {
858 self.deform
859 .detach_slot_instance(device, queue, mesh_id, instance_id, slot)
860 }
861
862 pub fn has_deform_slot_instance(&self, mesh_id: MeshId, instance_id: u32, slot: usize) -> bool {
865 self.deform.has_slot_instance(mesh_id, instance_id, slot)
866 }
867
868 pub fn register_deformer(
897 &mut self,
898 device: &wgpu::Device,
899 desc: DeformerDesc,
900 ) -> ViewportResult<DeformerId> {
901 validate_name(&self.deform.registrations, desc.name)?;
902 let slot = allocate_slot(&self.deform.registrations)?;
903
904 let candidate = StoredDeformer { desc, slot };
905 let mut trial = self.deform.registrations.clone();
906 trial.push(candidate.clone());
907
908 for shader_name in MESH_FAMILY_SHADERS {
909 let Some(base) = lookup_source(shader_name) else {
910 return Err(crate::error::ViewportError::DeformShaderInvalid {
911 reason: format!("internal: shader '{shader_name}' missing from shader catalog"),
912 });
913 };
914 let composed = compose_shader(base, &trial);
915 let label = format!("deform_compose_{shader_name}");
916 validate_with_wgpu(device, &label, &composed)?;
917 }
918
919 self.deform.registrations.push(candidate);
920 self.rebuild_mesh_pipelines(device);
921 Ok(DeformerId(slot))
922 }
923
924 #[allow(dead_code)]
931 pub(crate) fn register_internal_deformer(
932 &mut self,
933 device: &wgpu::Device,
934 desc: DeformerDesc,
935 ) -> ViewportResult<DeformerId> {
936 validate_name(&self.deform.registrations, desc.name)?;
937 let slot = allocate_internal_slot(&self.deform.registrations)?;
938
939 let candidate = StoredDeformer { desc, slot };
940 let mut trial = self.deform.registrations.clone();
941 trial.push(candidate.clone());
942
943 for shader_name in MESH_FAMILY_SHADERS {
944 let Some(base) = lookup_source(shader_name) else {
945 return Err(crate::error::ViewportError::DeformShaderInvalid {
946 reason: format!("internal: shader '{shader_name}' missing from shader catalog"),
947 });
948 };
949 let composed = compose_shader(base, &trial);
950 let label = format!("deform_compose_{shader_name}");
951 validate_with_wgpu(device, &label, &composed)?;
952 }
953
954 self.deform.registrations.push(candidate);
955 self.rebuild_mesh_pipelines(device);
956 Ok(DeformerId(slot))
957 }
958
959 pub fn registered_deformer_count(&self) -> usize {
961 self.deform.registrations.len()
962 }
963
964 pub fn deformer_id_by_name(&self, name: &str) -> Option<DeformerId> {
967 self.deform
968 .registrations
969 .iter()
970 .find(|r| r.desc.name == name)
971 .map(|r| DeformerId(r.slot))
972 }
973
974 pub fn deform_slot_handle(&self, id: DeformerId) -> DeformSlotHandle {
979 DeformSlotHandle {
980 header_buffer: self.deform.header_buffer.clone(),
981 slot: id.slot(),
982 }
983 }
984
985 fn rebuild_mesh_pipelines(&mut self, device: &wgpu::Device) {
992 if !self.deform.enabled {
993 return;
994 }
995 let registrations = self.deform.registrations.clone();
996
997 if let Some(base) = lookup_source("mesh.wgsl") {
999 let composed = compose_shader(base, ®istrations);
1000 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1001 label: Some("mesh_shader_composed"),
1002 source: wgpu::ShaderSource::Wgsl(composed.into()),
1003 });
1004
1005 let ldr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1006 device,
1007 "mesh_pipeline_layout",
1008 &self.camera_bind_group_layout,
1009 &self.object_bind_group_layout,
1010 Some(&self.deform.bind_group_layout),
1011 );
1012 let ldr = crate::resources::mesh_pipelines::build_ldr_mesh_pipelines(
1013 device,
1014 &ldr_layout,
1015 &shader,
1016 self.target_format,
1017 self.sample_count,
1018 None,
1019 );
1020 self.solid_pipeline = ldr.solid;
1021 self.solid_two_sided_pipeline = ldr.solid_two_sided;
1022 self.transparent_pipeline = ldr.transparent;
1023 self.wireframe_pipeline = ldr.wireframe;
1024
1025 if self.hdr_solid_pipeline.is_some() {
1026 let hdr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1027 device,
1028 "hdr_mesh_pipeline_layout",
1029 &self.camera_bind_group_layout,
1030 &self.object_bind_group_layout,
1031 Some(&self.deform.bind_group_layout),
1032 );
1033 let hdr = crate::resources::mesh_pipelines::build_hdr_mesh_pipelines(
1034 device,
1035 &hdr_layout,
1036 &shader,
1037 );
1038 self.hdr_solid_pipeline = Some(hdr.solid);
1039 self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1040 self.hdr_transparent_pipeline = Some(hdr.transparent);
1041 self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1042 }
1043 }
1044
1045 if self.oit_pipeline.is_some() {
1048 if let Some(base) = lookup_source("mesh_oit.wgsl") {
1049 let composed = compose_shader(base, ®istrations);
1050 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1051 label: Some("mesh_oit_shader_composed"),
1052 source: wgpu::ShaderSource::Wgsl(composed.into()),
1053 });
1054 let oit_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1055 device,
1056 "oit_pipeline_layout",
1057 &self.camera_bind_group_layout,
1058 &self.object_bind_group_layout,
1059 Some(&self.deform.bind_group_layout),
1060 );
1061 let oit = crate::resources::mesh_pipelines::build_oit_pipeline(
1062 device,
1063 &oit_layout,
1064 &shader,
1065 );
1066 self.oit_pipeline = Some(oit);
1067 }
1068 }
1069
1070 if let Some(base) = lookup_source("shadow.wgsl") {
1072 let composed = compose_shader(base, ®istrations);
1073 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1074 label: Some("shadow_shader_composed"),
1075 source: wgpu::ShaderSource::Wgsl(composed.into()),
1076 });
1077 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1078 label: Some("shadow_pipeline_layout"),
1079 bind_group_layouts: &[
1080 &self.shadow_camera_bind_group_layout,
1081 &self.object_bind_group_layout,
1082 &self.deform.bind_group_layout,
1083 ],
1084 push_constant_ranges: &[],
1085 });
1086 self.shadow_pipeline = crate::resources::mesh_pipelines::build_shadow_pipeline(
1087 device,
1088 &layout,
1089 &shader,
1090 Some(wgpu::Face::Front),
1091 None,
1092 );
1093 self.shadow_pipeline_two_sided =
1094 crate::resources::mesh_pipelines::build_shadow_pipeline(
1095 device, &layout, &shader, None, None,
1096 );
1097 }
1098
1099 if let Some(base) = lookup_source("outline_mask.wgsl") {
1101 let composed = compose_shader(base, ®istrations);
1102 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1103 label: Some("outline_mask_shader_composed"),
1104 source: wgpu::ShaderSource::Wgsl(composed.into()),
1105 });
1106 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1107 label: Some("outline_pipeline_layout"),
1108 bind_group_layouts: &[
1109 &self.camera_bind_group_layout,
1110 &self.outline_bind_group_layout,
1111 &self.deform.bind_group_layout,
1112 ],
1113 push_constant_ranges: &[],
1114 });
1115 let masks = crate::resources::mesh_pipelines::build_outline_mask_pipelines(
1116 device,
1117 &layout,
1118 &shader,
1119 wgpu::TextureFormat::R8Unorm,
1120 None,
1121 );
1122 self.outline_mask_pipeline = masks.mask;
1123 self.outline_mask_two_sided_pipeline = masks.mask_two_sided;
1124 }
1125
1126 if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1131 let composed = compose_shader(base, ®istrations);
1132 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1133 label: Some("mesh_instanced_shader_composed"),
1134 source: wgpu::ShaderSource::Wgsl(composed.into()),
1135 });
1136
1137 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1138 if self.solid_instanced_pipeline.is_some() {
1139 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1140 device,
1141 "instanced_pipeline_layout",
1142 &self.camera_bind_group_layout,
1143 instance_bgl,
1144 Some(&self.deform.bind_group_layout),
1145 );
1146 let ldr = crate::resources::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1147 device,
1148 &layout,
1149 &shader,
1150 self.target_format,
1151 self.sample_count,
1152 );
1153 self.solid_instanced_pipeline = Some(ldr.solid);
1154 self.solid_two_sided_instanced_pipeline = Some(ldr.solid_two_sided);
1155 self.transparent_instanced_pipeline = Some(ldr.transparent);
1156 }
1157 if self.hdr_solid_instanced_pipeline.is_some() {
1158 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1159 device,
1160 "hdr_instanced_pipeline_layout",
1161 &self.camera_bind_group_layout,
1162 instance_bgl,
1163 Some(&self.deform.bind_group_layout),
1164 );
1165 let hdr = crate::resources::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1166 device, &layout, &shader,
1167 );
1168 self.hdr_solid_instanced_pipeline = Some(hdr.solid);
1169 self.hdr_solid_two_sided_instanced_pipeline = Some(hdr.solid_two_sided);
1170 self.hdr_transparent_instanced_pipeline = Some(hdr.transparent);
1171 self.hdr_instanced_additive_pipeline = Some(hdr.additive);
1172 self.hdr_instanced_premultiplied_pipeline = Some(hdr.premultiplied);
1173 }
1174 }
1175 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1176 if self.hdr_solid_instanced_cull_pipeline.is_some() {
1177 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1178 device,
1179 "hdr_instanced_cull_pipeline_layout",
1180 &self.camera_bind_group_layout,
1181 cull_bgl,
1182 Some(&self.deform.bind_group_layout),
1183 );
1184 let pl = crate::resources::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1185 device, &layout, &shader,
1186 );
1187 self.hdr_solid_instanced_cull_pipeline = Some(pl);
1188 let pl_two_sided =
1189 crate::resources::mesh_pipelines::build_hdr_instanced_cull_two_sided_pipeline(
1190 device, &layout, &shader,
1191 );
1192 self.hdr_solid_instanced_cull_two_sided_pipeline = Some(pl_two_sided);
1193 }
1194 }
1195 }
1196
1197 if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1199 let composed = compose_shader(base, ®istrations);
1200 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1201 label: Some("mesh_instanced_oit_shader_composed"),
1202 source: wgpu::ShaderSource::Wgsl(composed.into()),
1203 });
1204 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1205 if self.oit_instanced_pipeline.is_some() {
1206 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1207 device,
1208 "oit_instanced_pipeline_layout",
1209 &self.camera_bind_group_layout,
1210 instance_bgl,
1211 Some(&self.deform.bind_group_layout),
1212 );
1213 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1214 device,
1215 &layout,
1216 &shader,
1217 "oit_instanced_pipeline",
1218 "vs_main",
1219 );
1220 self.oit_instanced_pipeline = Some(pl);
1221 }
1222 }
1223 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1224 if self.oit_instanced_cull_pipeline.is_some() {
1225 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1226 device,
1227 "oit_instanced_cull_pipeline_layout",
1228 &self.camera_bind_group_layout,
1229 cull_bgl,
1230 Some(&self.deform.bind_group_layout),
1231 );
1232 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1233 device,
1234 &layout,
1235 &shader,
1236 "oit_instanced_cull_pipeline",
1237 "vs_main_cull",
1238 );
1239 self.oit_instanced_cull_pipeline = Some(pl);
1240 }
1241 }
1242 }
1243 }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248 use super::*;
1249
1250 fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1251 let instance = wgpu::Instance::default();
1252 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1253 power_preference: wgpu::PowerPreference::default(),
1254 force_fallback_adapter: false,
1255 compatible_surface: None,
1256 }))
1257 .ok()?;
1258 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1259 label: Some("deform_tests"),
1260 ..Default::default()
1261 }))
1262 .ok()?;
1263 Some((device, queue))
1264 }
1265
1266 #[test]
1267 fn pack_lays_out_offsets_after_slot_layout_prefix() {
1268 let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1269 let mut stride = [0u32; DEFORM_SLOT_COUNT];
1270 data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1272 stride[0] = 1;
1273 data[2] = Some(vec![10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0]);
1275 stride[2] = 2;
1276
1277 let words = DeformationState::pack(&data, &stride);
1278 assert_eq!(words[0], SLOT_LAYOUT_WORDS as u32); assert_eq!(words[1], 1); assert_eq!(words[2], 0); assert_eq!(words[3], 0); assert_eq!(words[4], (SLOT_LAYOUT_WORDS + 3) as u32); assert_eq!(words[5], 2); assert_eq!(words[6], 0); assert_eq!(words[7], 0); let slot0_base = SLOT_LAYOUT_WORDS;
1289 assert_eq!(words[slot0_base], 1);
1290 assert_eq!(words[slot0_base + 1], 2);
1291 assert_eq!(words[slot0_base + 2], 3);
1292 let slot2_base = slot0_base + 3;
1294 assert_eq!(words[slot2_base], 10);
1295 assert_eq!(words[slot2_base + 1], 20);
1296 assert_eq!(words[slot2_base + 2], 30);
1297 assert_eq!(words[slot2_base + 3], 40);
1298 }
1299
1300 #[test]
1301 fn attach_marks_flag_bit_and_swaps_bind_group() {
1302 let Some((device, _queue)) = headless() else {
1303 return;
1304 };
1305 let mut s = DeformationState::new(&device);
1306 let mesh = MeshId(7);
1307 assert_eq!(s.flag_bits(mesh), 0);
1308 assert!(!s.has_slot(mesh, 0));
1309
1310 s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1312 assert!(s.has_slot(mesh, 0));
1313 assert_eq!(s.flag_bits(mesh), 0b0001);
1314
1315 s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1317 assert!(s.has_slot(mesh, 2));
1318 assert_eq!(s.flag_bits(mesh), 0b0101);
1319 }
1320
1321 #[test]
1322 fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1323 let Some((device, _queue)) = headless() else {
1324 return;
1325 };
1326 let mut s = DeformationState::new(&device);
1327 let mesh = MeshId(11);
1328 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1329 assert_eq!(s.flag_bits(mesh), 0b0010);
1330
1331 assert!(s.detach_slot(&device, mesh, 1));
1332 assert_eq!(s.flag_bits(mesh), 0);
1333 assert!(!s.meshes.contains_key(&mesh));
1334 assert!(!s.detach_slot(&device, mesh, 1));
1335 }
1336
1337 #[test]
1338 fn attach_slot_instance_marks_union_and_routes_bind_group() {
1339 let Some((device, queue)) = headless() else {
1340 return;
1341 };
1342 let mut s = DeformationState::new(&device);
1343 let mesh = MeshId(42);
1344 let instance = 3u32;
1345 assert_eq!(s.flag_bits(mesh), 0);
1346
1347 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1349 s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1350
1351 assert_eq!(s.flag_bits(mesh), 0b0011);
1353 assert!(s.has_slot_instance(mesh, instance, 0));
1354 assert!(!s.has_slot_instance(mesh, instance, 1));
1355
1356 let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1358 let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1359 assert_ne!(inst_bg, mesh_bg);
1360
1361 let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1364 assert_eq!(fallback, mesh_bg);
1365
1366 assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1369 assert_eq!(s.flag_bits(mesh), 0b0010);
1370 assert!(!s.has_slot_instance(mesh, instance, 0));
1371 }
1372
1373 #[test]
1374 fn slot_index_assert_traps_out_of_range() {
1375 let Some((device, _queue)) = headless() else {
1376 return;
1377 };
1378 let mut s = DeformationState::new(&device);
1379 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1380 s.attach_slot(&device, MeshId(0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1381 }));
1382 assert!(result.is_err());
1383 }
1384
1385 #[test]
1394 fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1395 use crate::renderer::ViewportRenderer;
1396 let Some((device, _queue)) = headless() else {
1397 return;
1398 };
1399 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1400
1401 let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1402 let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1403
1404 let body = "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n var o = v;\n if (deform_slot_stride(0u) > 0u) {\n o.position.z = o.position.z + deform_read_f32(0u, v.vertex_index, 0u);\n }\n return o;\n}\n";
1405 let desc = DeformerDesc {
1406 name: "wave",
1407 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1408 priority: 0,
1409 wgsl_body: body.to_string(),
1410 per_vertex_stride: 4,
1411 };
1412 let id = renderer
1413 .resources_mut()
1414 .register_deformer(&device, desc)
1415 .expect("register");
1416 assert_eq!(id.slot(), 0);
1417
1418 let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1419 let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1420 assert_ne!(solid_before, std::ptr::null());
1425 assert_ne!(solid_after, std::ptr::null());
1426 assert_ne!(wf_before, std::ptr::null());
1427 assert_ne!(wf_after, std::ptr::null());
1428 }
1429
1430 #[test]
1431 fn register_deformer_validates_and_assigns_slot() {
1432 use crate::renderer::ViewportRenderer;
1433 let Some((device, _queue)) = headless() else {
1434 return;
1435 };
1436 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1437 let resources = renderer.resources_mut();
1438 let desc = DeformerDesc {
1439 name: "wind",
1440 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1441 priority: 0,
1442 wgsl_body: "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n var o = v;\n o.position.z = o.position.z + 0.001;\n return o;\n}\n".to_string(),
1443 per_vertex_stride: 4,
1444 };
1445 let baseline = resources.registered_deformer_count();
1449 let id = resources
1450 .register_deformer(&device, desc)
1451 .expect("register");
1452 assert_eq!(id.slot(), 0);
1453 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1454
1455 let dup = DeformerDesc {
1456 name: "wind",
1457 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1458 priority: 0,
1459 wgsl_body:
1460 "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }"
1461 .to_string(),
1462 per_vertex_stride: 4,
1463 };
1464 let err = resources.register_deformer(&device, dup).unwrap_err();
1465 assert!(matches!(
1466 err,
1467 crate::error::ViewportError::DeformNameTaken { .. }
1468 ));
1469 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1470
1471 let bad = DeformerDesc {
1472 name: "wave",
1473 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1474 priority: 0,
1475 wgsl_body: "this is not valid wgsl".to_string(),
1476 per_vertex_stride: 4,
1477 };
1478 let err = resources.register_deformer(&device, bad).unwrap_err();
1479 assert!(matches!(
1480 err,
1481 crate::error::ViewportError::DeformShaderInvalid { .. }
1482 ));
1483 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1484 }
1485}