1use std::collections::HashMap;
11
12use bytemuck::{Pod, Zeroable};
13use wgpu::util::DeviceExt;
14
15use crate::error::ViewportResult;
16use crate::resources::DeviceResources;
17use crate::resources::mesh::mesh_store::MeshId;
18use crate::resources::mesh_sidecar::registry::{
19 DeformerDesc, DeformerId, MESH_FAMILY_SHADERS, StoredDeformer, allocate_internal_slot,
20 allocate_slot, compose_shader, lookup_source, validate_name, validate_with_wgpu,
21};
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 DeviceResources {
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 =
1001 crate::resources::builders::wgsl_module(device, "mesh_shader_composed", composed);
1002
1003 let ldr_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1004 device,
1005 "mesh_pipeline_layout",
1006 &self.camera_bind_group_layout,
1007 &self.object_bind_group_layout,
1008 Some(&self.deform.bind_group_layout),
1009 );
1010 let ldr = crate::resources::mesh::mesh_pipelines::build_ldr_mesh_pipelines(
1011 device,
1012 &ldr_layout,
1013 &shader,
1014 self.target_format,
1015 self.sample_count,
1016 None,
1017 );
1018 self.solid_pipeline = ldr.solid;
1019 self.solid_two_sided_pipeline = ldr.solid_two_sided;
1020 self.transparent_pipeline = ldr.transparent;
1021 self.wireframe_pipeline = ldr.wireframe;
1022
1023 if self.hdr_solid_pipeline.is_some() {
1024 let hdr_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1025 device,
1026 "hdr_mesh_pipeline_layout",
1027 &self.camera_bind_group_layout,
1028 &self.object_bind_group_layout,
1029 Some(&self.deform.bind_group_layout),
1030 );
1031 let hdr = crate::resources::mesh::mesh_pipelines::build_hdr_mesh_pipelines(
1032 device,
1033 &hdr_layout,
1034 &shader,
1035 );
1036 self.hdr_solid_pipeline = Some(hdr.solid);
1037 self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1038 self.hdr_transparent_pipeline = Some(hdr.transparent);
1039 self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1040 }
1041 }
1042
1043 if self.oit.pipeline.is_some() {
1046 if let Some(base) = lookup_source("mesh_oit.wgsl") {
1047 let composed = compose_shader(base, ®istrations);
1048 let shader = crate::resources::builders::wgsl_module(
1049 device,
1050 "mesh_oit_shader_composed",
1051 composed,
1052 );
1053 let oit_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1054 device,
1055 "oit_pipeline_layout",
1056 &self.camera_bind_group_layout,
1057 &self.object_bind_group_layout,
1058 Some(&self.deform.bind_group_layout),
1059 );
1060 let oit = crate::resources::mesh::mesh_pipelines::build_oit_pipeline(
1061 device,
1062 &oit_layout,
1063 &shader,
1064 );
1065 self.oit.pipeline = Some(oit);
1066 }
1067 }
1068
1069 if let Some(base) = lookup_source("shadow.wgsl") {
1071 let composed = compose_shader(base, ®istrations);
1072 let shader =
1073 crate::resources::builders::wgsl_module(device, "shadow_shader_composed", composed);
1074 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1075 label: Some("shadow_pipeline_layout"),
1076 bind_group_layouts: &[
1077 &self.shadow_camera_bind_group_layout,
1078 &self.object_bind_group_layout,
1079 &self.deform.bind_group_layout,
1080 ],
1081 push_constant_ranges: &[],
1082 });
1083 self.shadow_pipeline = crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
1084 device,
1085 &layout,
1086 &shader,
1087 Some(wgpu::Face::Front),
1088 None,
1089 );
1090 self.shadow_pipeline_two_sided =
1091 crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
1092 device, &layout, &shader, None, None,
1093 );
1094 }
1095
1096 if let Some(base) = lookup_source("outline_mask.wgsl") {
1098 let composed = compose_shader(base, ®istrations);
1099 let shader = crate::resources::builders::wgsl_module(
1100 device,
1101 "outline_mask_shader_composed",
1102 composed,
1103 );
1104 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1105 label: Some("outline_pipeline_layout"),
1106 bind_group_layouts: &[
1107 &self.camera_bind_group_layout,
1108 &self.outline.bind_group_layout,
1109 &self.deform.bind_group_layout,
1110 ],
1111 push_constant_ranges: &[],
1112 });
1113 let masks = crate::resources::mesh::mesh_pipelines::build_outline_mask_pipelines(
1114 device,
1115 &layout,
1116 &shader,
1117 wgpu::TextureFormat::R8Unorm,
1118 None,
1119 );
1120 self.outline.mask_pipeline = masks.mask;
1121 self.outline.mask_two_sided_pipeline = masks.mask_two_sided;
1122 }
1123
1124 if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1129 let composed = compose_shader(base, ®istrations);
1130 let shader = crate::resources::builders::wgsl_module(
1131 device,
1132 "mesh_instanced_shader_composed",
1133 composed,
1134 );
1135
1136 if let Some(instance_bgl) = self.instancing.bind_group_layout.as_ref() {
1137 if self.instancing.solid_pipeline.is_some() {
1138 let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1139 device,
1140 "instanced_pipeline_layout",
1141 &self.camera_bind_group_layout,
1142 instance_bgl,
1143 Some(&self.deform.bind_group_layout),
1144 );
1145 let ldr =
1146 crate::resources::mesh::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1147 device,
1148 &layout,
1149 &shader,
1150 self.target_format,
1151 self.sample_count,
1152 );
1153 self.instancing.solid_pipeline = Some(ldr.solid);
1154 self.instancing.solid_two_sided_pipeline = Some(ldr.solid_two_sided);
1155 self.instancing.transparent_pipeline = Some(ldr.transparent);
1156 }
1157 if self.instancing.hdr_solid_pipeline.is_some() {
1158 let layout = crate::resources::mesh::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 =
1166 crate::resources::mesh::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1167 device, &layout, &shader,
1168 );
1169 self.instancing.hdr_solid_pipeline = Some(hdr.solid);
1170 self.instancing.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1171 self.instancing.hdr_transparent_pipeline = Some(hdr.transparent);
1172 self.instancing.hdr_additive_pipeline = Some(hdr.additive);
1173 self.instancing.hdr_premultiplied_pipeline = Some(hdr.premultiplied);
1174 }
1175 }
1176 if let Some(cull_bgl) = self.cull.bind_group_layout.as_ref() {
1177 if self.cull.hdr_solid_pipeline.is_some() {
1178 let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1179 device,
1180 "hdr_instanced_cull_pipeline_layout",
1181 &self.camera_bind_group_layout,
1182 cull_bgl,
1183 Some(&self.deform.bind_group_layout),
1184 );
1185 let pl =
1186 crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1187 device, &layout, &shader,
1188 );
1189 self.cull.hdr_solid_pipeline = Some(pl);
1190 let pl_two_sided =
1191 crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_two_sided_pipeline(
1192 device, &layout, &shader,
1193 );
1194 self.cull.hdr_solid_two_sided_pipeline = Some(pl_two_sided);
1195 }
1196 }
1197 }
1198
1199 if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1201 let composed = compose_shader(base, ®istrations);
1202 let shader = crate::resources::builders::wgsl_module(
1203 device,
1204 "mesh_instanced_oit_shader_composed",
1205 composed,
1206 );
1207 if let Some(instance_bgl) = self.instancing.bind_group_layout.as_ref() {
1208 if self.oit.instanced_pipeline.is_some() {
1209 let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1210 device,
1211 "oit_instanced_pipeline_layout",
1212 &self.camera_bind_group_layout,
1213 instance_bgl,
1214 Some(&self.deform.bind_group_layout),
1215 );
1216 let pl = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
1217 device,
1218 &layout,
1219 &shader,
1220 "oit_instanced_pipeline",
1221 "vs_main",
1222 );
1223 self.oit.instanced_pipeline = Some(pl);
1224 }
1225 }
1226 if let Some(cull_bgl) = self.cull.bind_group_layout.as_ref() {
1227 if self.cull.oit_pipeline.is_some() {
1228 let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1229 device,
1230 "oit_instanced_cull_pipeline_layout",
1231 &self.camera_bind_group_layout,
1232 cull_bgl,
1233 Some(&self.deform.bind_group_layout),
1234 );
1235 let pl = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
1236 device,
1237 &layout,
1238 &shader,
1239 "oit_instanced_cull_pipeline",
1240 "vs_main_cull",
1241 );
1242 self.cull.oit_pipeline = Some(pl);
1243 }
1244 }
1245 }
1246 }
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251 use super::*;
1252
1253 fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1254 let instance = wgpu::Instance::default();
1255 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1256 power_preference: wgpu::PowerPreference::default(),
1257 force_fallback_adapter: false,
1258 compatible_surface: None,
1259 }))
1260 .ok()?;
1261 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1262 label: Some("deform_tests"),
1263 ..Default::default()
1264 }))
1265 .ok()?;
1266 Some((device, queue))
1267 }
1268
1269 #[test]
1270 fn pack_lays_out_offsets_after_slot_layout_prefix() {
1271 let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1272 let mut stride = [0u32; DEFORM_SLOT_COUNT];
1273 data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1275 stride[0] = 1;
1276 data[2] = Some(vec![10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0]);
1278 stride[2] = 2;
1279
1280 let words = DeformationState::pack(&data, &stride);
1281 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;
1292 assert_eq!(words[slot0_base], 1);
1293 assert_eq!(words[slot0_base + 1], 2);
1294 assert_eq!(words[slot0_base + 2], 3);
1295 let slot2_base = slot0_base + 3;
1297 assert_eq!(words[slot2_base], 10);
1298 assert_eq!(words[slot2_base + 1], 20);
1299 assert_eq!(words[slot2_base + 2], 30);
1300 assert_eq!(words[slot2_base + 3], 40);
1301 }
1302
1303 #[test]
1304 fn attach_marks_flag_bit_and_swaps_bind_group() {
1305 let Some((device, _queue)) = headless() else {
1306 return;
1307 };
1308 let mut s = DeformationState::new(&device);
1309 let mesh = MeshId::new(7, 0);
1310 assert_eq!(s.flag_bits(mesh), 0);
1311 assert!(!s.has_slot(mesh, 0));
1312
1313 s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1315 assert!(s.has_slot(mesh, 0));
1316 assert_eq!(s.flag_bits(mesh), 0b0001);
1317
1318 s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1320 assert!(s.has_slot(mesh, 2));
1321 assert_eq!(s.flag_bits(mesh), 0b0101);
1322 }
1323
1324 #[test]
1325 fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1326 let Some((device, _queue)) = headless() else {
1327 return;
1328 };
1329 let mut s = DeformationState::new(&device);
1330 let mesh = MeshId::new(11, 0);
1331 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1332 assert_eq!(s.flag_bits(mesh), 0b0010);
1333
1334 assert!(s.detach_slot(&device, mesh, 1));
1335 assert_eq!(s.flag_bits(mesh), 0);
1336 assert!(!s.meshes.contains_key(&mesh));
1337 assert!(!s.detach_slot(&device, mesh, 1));
1338 }
1339
1340 #[test]
1341 fn attach_slot_instance_marks_union_and_routes_bind_group() {
1342 let Some((device, queue)) = headless() else {
1343 return;
1344 };
1345 let mut s = DeformationState::new(&device);
1346 let mesh = MeshId::new(42, 0);
1347 let instance = 3u32;
1348 assert_eq!(s.flag_bits(mesh), 0);
1349
1350 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1352 s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1353
1354 assert_eq!(s.flag_bits(mesh), 0b0011);
1356 assert!(s.has_slot_instance(mesh, instance, 0));
1357 assert!(!s.has_slot_instance(mesh, instance, 1));
1358
1359 let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1361 let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1362 assert_ne!(inst_bg, mesh_bg);
1363
1364 let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1367 assert_eq!(fallback, mesh_bg);
1368
1369 assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1372 assert_eq!(s.flag_bits(mesh), 0b0010);
1373 assert!(!s.has_slot_instance(mesh, instance, 0));
1374 }
1375
1376 #[test]
1377 fn slot_index_assert_traps_out_of_range() {
1378 let Some((device, _queue)) = headless() else {
1379 return;
1380 };
1381 let mut s = DeformationState::new(&device);
1382 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1383 s.attach_slot(&device, MeshId::new(0, 0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1384 }));
1385 assert!(result.is_err());
1386 }
1387
1388 #[test]
1397 fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1398 use crate::renderer::ViewportRenderer;
1399 let Some((device, _queue)) = headless() else {
1400 return;
1401 };
1402 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1403
1404 let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1405 let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1406
1407 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";
1408 let desc = DeformerDesc {
1409 name: "wave",
1410 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1411 priority: 0,
1412 wgsl_body: body.to_string(),
1413 per_vertex_stride: 4,
1414 };
1415 let id = renderer
1416 .resources_mut()
1417 .register_deformer(&device, desc)
1418 .expect("register");
1419 assert_eq!(id.slot(), 0);
1420
1421 let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1422 let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1423 assert_ne!(solid_before, std::ptr::null());
1428 assert_ne!(solid_after, std::ptr::null());
1429 assert_ne!(wf_before, std::ptr::null());
1430 assert_ne!(wf_after, std::ptr::null());
1431 }
1432
1433 #[test]
1434 fn register_deformer_validates_and_assigns_slot() {
1435 use crate::renderer::ViewportRenderer;
1436 let Some((device, _queue)) = headless() else {
1437 return;
1438 };
1439 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1440 let resources = renderer.resources_mut();
1441 let desc = DeformerDesc {
1442 name: "wind",
1443 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1444 priority: 0,
1445 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(),
1446 per_vertex_stride: 4,
1447 };
1448 let baseline = resources.registered_deformer_count();
1452 let id = resources
1453 .register_deformer(&device, desc)
1454 .expect("register");
1455 assert_eq!(id.slot(), 0);
1456 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1457
1458 let dup = DeformerDesc {
1459 name: "wind",
1460 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1461 priority: 0,
1462 wgsl_body:
1463 "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }"
1464 .to_string(),
1465 per_vertex_stride: 4,
1466 };
1467 let err = resources.register_deformer(&device, dup).unwrap_err();
1468 assert!(matches!(
1469 err,
1470 crate::error::ViewportError::DeformNameTaken { .. }
1471 ));
1472 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1473
1474 let bad = DeformerDesc {
1475 name: "wave",
1476 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1477 priority: 0,
1478 wgsl_body: "this is not valid wgsl".to_string(),
1479 per_vertex_stride: 4,
1480 };
1481 let err = resources.register_deformer(&device, bad).unwrap_err();
1482 assert!(matches!(
1483 err,
1484 crate::error::ViewportError::DeformShaderInvalid { .. }
1485 ));
1486 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1487 }
1488}