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 );
1019 self.solid_pipeline = ldr.solid;
1020 self.solid_two_sided_pipeline = ldr.solid_two_sided;
1021 self.transparent_pipeline = ldr.transparent;
1022 self.wireframe_pipeline = ldr.wireframe;
1023
1024 if self.hdr_solid_pipeline.is_some() {
1025 let hdr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1026 device,
1027 "hdr_mesh_pipeline_layout",
1028 &self.camera_bind_group_layout,
1029 &self.object_bind_group_layout,
1030 Some(&self.deform.bind_group_layout),
1031 );
1032 let hdr = crate::resources::mesh_pipelines::build_hdr_mesh_pipelines(
1033 device,
1034 &hdr_layout,
1035 &shader,
1036 );
1037 self.hdr_solid_pipeline = Some(hdr.solid);
1038 self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1039 self.hdr_transparent_pipeline = Some(hdr.transparent);
1040 self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1041 }
1042 }
1043
1044 if self.oit_pipeline.is_some() {
1047 if let Some(base) = lookup_source("mesh_oit.wgsl") {
1048 let composed = compose_shader(base, ®istrations);
1049 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1050 label: Some("mesh_oit_shader_composed"),
1051 source: wgpu::ShaderSource::Wgsl(composed.into()),
1052 });
1053 let oit_layout = crate::resources::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_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 = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1073 label: Some("shadow_shader_composed"),
1074 source: wgpu::ShaderSource::Wgsl(composed.into()),
1075 });
1076 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1077 label: Some("shadow_pipeline_layout"),
1078 bind_group_layouts: &[
1079 &self.shadow_camera_bind_group_layout,
1080 &self.object_bind_group_layout,
1081 &self.deform.bind_group_layout,
1082 ],
1083 push_constant_ranges: &[],
1084 });
1085 self.shadow_pipeline = crate::resources::mesh_pipelines::build_shadow_pipeline(
1086 device,
1087 &layout,
1088 &shader,
1089 Some(wgpu::Face::Front),
1090 );
1091 }
1092
1093 if let Some(base) = lookup_source("outline_mask.wgsl") {
1095 let composed = compose_shader(base, ®istrations);
1096 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1097 label: Some("outline_mask_shader_composed"),
1098 source: wgpu::ShaderSource::Wgsl(composed.into()),
1099 });
1100 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1101 label: Some("outline_pipeline_layout"),
1102 bind_group_layouts: &[
1103 &self.camera_bind_group_layout,
1104 &self.outline_bind_group_layout,
1105 &self.deform.bind_group_layout,
1106 ],
1107 push_constant_ranges: &[],
1108 });
1109 let masks = crate::resources::mesh_pipelines::build_outline_mask_pipelines(
1110 device,
1111 &layout,
1112 &shader,
1113 wgpu::TextureFormat::R8Unorm,
1114 );
1115 self.outline_mask_pipeline = masks.mask;
1116 self.outline_mask_two_sided_pipeline = masks.mask_two_sided;
1117 }
1118
1119 if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1124 let composed = compose_shader(base, ®istrations);
1125 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1126 label: Some("mesh_instanced_shader_composed"),
1127 source: wgpu::ShaderSource::Wgsl(composed.into()),
1128 });
1129
1130 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1131 if self.solid_instanced_pipeline.is_some() {
1132 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1133 device,
1134 "instanced_pipeline_layout",
1135 &self.camera_bind_group_layout,
1136 instance_bgl,
1137 Some(&self.deform.bind_group_layout),
1138 );
1139 let ldr = crate::resources::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1140 device,
1141 &layout,
1142 &shader,
1143 self.target_format,
1144 self.sample_count,
1145 );
1146 self.solid_instanced_pipeline = Some(ldr.solid);
1147 self.transparent_instanced_pipeline = Some(ldr.transparent);
1148 }
1149 if self.hdr_solid_instanced_pipeline.is_some() {
1150 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1151 device,
1152 "hdr_instanced_pipeline_layout",
1153 &self.camera_bind_group_layout,
1154 instance_bgl,
1155 Some(&self.deform.bind_group_layout),
1156 );
1157 let hdr = crate::resources::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1158 device, &layout, &shader,
1159 );
1160 self.hdr_solid_instanced_pipeline = Some(hdr.solid);
1161 self.hdr_transparent_instanced_pipeline = Some(hdr.transparent);
1162 self.hdr_instanced_additive_pipeline = Some(hdr.additive);
1163 self.hdr_instanced_premultiplied_pipeline = Some(hdr.premultiplied);
1164 }
1165 }
1166 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1167 if self.hdr_solid_instanced_cull_pipeline.is_some() {
1168 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1169 device,
1170 "hdr_instanced_cull_pipeline_layout",
1171 &self.camera_bind_group_layout,
1172 cull_bgl,
1173 Some(&self.deform.bind_group_layout),
1174 );
1175 let pl = crate::resources::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1176 device, &layout, &shader,
1177 );
1178 self.hdr_solid_instanced_cull_pipeline = Some(pl);
1179 }
1180 }
1181 }
1182
1183 if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1185 let composed = compose_shader(base, ®istrations);
1186 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1187 label: Some("mesh_instanced_oit_shader_composed"),
1188 source: wgpu::ShaderSource::Wgsl(composed.into()),
1189 });
1190 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1191 if self.oit_instanced_pipeline.is_some() {
1192 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1193 device,
1194 "oit_instanced_pipeline_layout",
1195 &self.camera_bind_group_layout,
1196 instance_bgl,
1197 Some(&self.deform.bind_group_layout),
1198 );
1199 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1200 device,
1201 &layout,
1202 &shader,
1203 "oit_instanced_pipeline",
1204 "vs_main",
1205 );
1206 self.oit_instanced_pipeline = Some(pl);
1207 }
1208 }
1209 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1210 if self.oit_instanced_cull_pipeline.is_some() {
1211 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1212 device,
1213 "oit_instanced_cull_pipeline_layout",
1214 &self.camera_bind_group_layout,
1215 cull_bgl,
1216 Some(&self.deform.bind_group_layout),
1217 );
1218 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1219 device,
1220 &layout,
1221 &shader,
1222 "oit_instanced_cull_pipeline",
1223 "vs_main_cull",
1224 );
1225 self.oit_instanced_cull_pipeline = Some(pl);
1226 }
1227 }
1228 }
1229 }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235
1236 fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1237 let instance = wgpu::Instance::default();
1238 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1239 power_preference: wgpu::PowerPreference::default(),
1240 force_fallback_adapter: false,
1241 compatible_surface: None,
1242 }))
1243 .ok()?;
1244 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1245 label: Some("deform_tests"),
1246 ..Default::default()
1247 }))
1248 .ok()?;
1249 Some((device, queue))
1250 }
1251
1252 #[test]
1253 fn pack_lays_out_offsets_after_slot_layout_prefix() {
1254 let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1255 let mut stride = [0u32; DEFORM_SLOT_COUNT];
1256 data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1258 stride[0] = 1;
1259 data[2] = Some(vec![10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0]);
1261 stride[2] = 2;
1262
1263 let words = DeformationState::pack(&data, &stride);
1264 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;
1275 assert_eq!(words[slot0_base], 1);
1276 assert_eq!(words[slot0_base + 1], 2);
1277 assert_eq!(words[slot0_base + 2], 3);
1278 let slot2_base = slot0_base + 3;
1280 assert_eq!(words[slot2_base], 10);
1281 assert_eq!(words[slot2_base + 1], 20);
1282 assert_eq!(words[slot2_base + 2], 30);
1283 assert_eq!(words[slot2_base + 3], 40);
1284 }
1285
1286 #[test]
1287 fn attach_marks_flag_bit_and_swaps_bind_group() {
1288 let Some((device, _queue)) = headless() else {
1289 return;
1290 };
1291 let mut s = DeformationState::new(&device);
1292 let mesh = MeshId(7);
1293 assert_eq!(s.flag_bits(mesh), 0);
1294 assert!(!s.has_slot(mesh, 0));
1295
1296 s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1298 assert!(s.has_slot(mesh, 0));
1299 assert_eq!(s.flag_bits(mesh), 0b0001);
1300
1301 s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1303 assert!(s.has_slot(mesh, 2));
1304 assert_eq!(s.flag_bits(mesh), 0b0101);
1305 }
1306
1307 #[test]
1308 fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1309 let Some((device, _queue)) = headless() else {
1310 return;
1311 };
1312 let mut s = DeformationState::new(&device);
1313 let mesh = MeshId(11);
1314 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1315 assert_eq!(s.flag_bits(mesh), 0b0010);
1316
1317 assert!(s.detach_slot(&device, mesh, 1));
1318 assert_eq!(s.flag_bits(mesh), 0);
1319 assert!(!s.meshes.contains_key(&mesh));
1320 assert!(!s.detach_slot(&device, mesh, 1));
1321 }
1322
1323 #[test]
1324 fn attach_slot_instance_marks_union_and_routes_bind_group() {
1325 let Some((device, queue)) = headless() else {
1326 return;
1327 };
1328 let mut s = DeformationState::new(&device);
1329 let mesh = MeshId(42);
1330 let instance = 3u32;
1331 assert_eq!(s.flag_bits(mesh), 0);
1332
1333 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1335 s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1336
1337 assert_eq!(s.flag_bits(mesh), 0b0011);
1339 assert!(s.has_slot_instance(mesh, instance, 0));
1340 assert!(!s.has_slot_instance(mesh, instance, 1));
1341
1342 let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1344 let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1345 assert_ne!(inst_bg, mesh_bg);
1346
1347 let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1350 assert_eq!(fallback, mesh_bg);
1351
1352 assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1355 assert_eq!(s.flag_bits(mesh), 0b0010);
1356 assert!(!s.has_slot_instance(mesh, instance, 0));
1357 }
1358
1359 #[test]
1360 fn slot_index_assert_traps_out_of_range() {
1361 let Some((device, _queue)) = headless() else {
1362 return;
1363 };
1364 let mut s = DeformationState::new(&device);
1365 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1366 s.attach_slot(&device, MeshId(0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1367 }));
1368 assert!(result.is_err());
1369 }
1370
1371 #[test]
1380 fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1381 use crate::renderer::ViewportRenderer;
1382 let Some((device, _queue)) = headless() else {
1383 return;
1384 };
1385 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1386
1387 let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1388 let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1389
1390 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";
1391 let desc = DeformerDesc {
1392 name: "wave",
1393 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1394 priority: 0,
1395 wgsl_body: body.to_string(),
1396 per_vertex_stride: 4,
1397 };
1398 let id = renderer
1399 .resources_mut()
1400 .register_deformer(&device, desc)
1401 .expect("register");
1402 assert_eq!(id.slot(), 0);
1403
1404 let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1405 let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1406 assert_ne!(solid_before, std::ptr::null());
1411 assert_ne!(solid_after, std::ptr::null());
1412 assert_ne!(wf_before, std::ptr::null());
1413 assert_ne!(wf_after, std::ptr::null());
1414 }
1415
1416 #[test]
1417 fn register_deformer_validates_and_assigns_slot() {
1418 use crate::renderer::ViewportRenderer;
1419 let Some((device, _queue)) = headless() else {
1420 return;
1421 };
1422 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1423 let resources = renderer.resources_mut();
1424 let desc = DeformerDesc {
1425 name: "wind",
1426 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1427 priority: 0,
1428 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(),
1429 per_vertex_stride: 4,
1430 };
1431 let baseline = resources.registered_deformer_count();
1435 let id = resources
1436 .register_deformer(&device, desc)
1437 .expect("register");
1438 assert_eq!(id.slot(), 0);
1439 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1440
1441 let dup = DeformerDesc {
1442 name: "wind",
1443 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1444 priority: 0,
1445 wgsl_body:
1446 "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }"
1447 .to_string(),
1448 per_vertex_stride: 4,
1449 };
1450 let err = resources.register_deformer(&device, dup).unwrap_err();
1451 assert!(matches!(
1452 err,
1453 crate::error::ViewportError::DeformNameTaken { .. }
1454 ));
1455 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1456
1457 let bad = DeformerDesc {
1458 name: "wave",
1459 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1460 priority: 0,
1461 wgsl_body: "this is not valid wgsl".to_string(),
1462 per_vertex_stride: 4,
1463 };
1464 let err = resources.register_deformer(&device, bad).unwrap_err();
1465 assert!(matches!(
1466 err,
1467 crate::error::ViewportError::DeformShaderInvalid { .. }
1468 ));
1469 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1470 }
1471}