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 bind_group_layout: wgpu::BindGroupLayout,
150 pub header_buffer: wgpu::Buffer,
151 #[allow(dead_code)]
155 pub dummy_data_buffer: wgpu::Buffer,
156 pub dummy_instance_buffer: wgpu::Buffer,
160 pub dummy_bind_group: wgpu::BindGroup,
164 pub meshes: HashMap<MeshId, MeshDeform>,
165 pub header_cpu: DeformHeader,
166 pub registrations: Vec<StoredDeformer>,
168}
169
170impl DeformationState {
171 pub fn new(device: &wgpu::Device) -> Self {
172 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
173 label: Some("deform_bgl"),
174 entries: &[
175 wgpu::BindGroupLayoutEntry {
176 binding: 0,
177 visibility: wgpu::ShaderStages::VERTEX,
178 ty: wgpu::BindingType::Buffer {
179 ty: wgpu::BufferBindingType::Uniform,
180 has_dynamic_offset: false,
181 min_binding_size: None,
182 },
183 count: None,
184 },
185 wgpu::BindGroupLayoutEntry {
186 binding: 1,
187 visibility: wgpu::ShaderStages::VERTEX,
188 ty: wgpu::BindingType::Buffer {
189 ty: wgpu::BufferBindingType::Storage { read_only: true },
190 has_dynamic_offset: false,
191 min_binding_size: None,
192 },
193 count: None,
194 },
195 wgpu::BindGroupLayoutEntry {
196 binding: 2,
197 visibility: wgpu::ShaderStages::VERTEX,
198 ty: wgpu::BindingType::Buffer {
199 ty: wgpu::BufferBindingType::Storage { read_only: true },
200 has_dynamic_offset: false,
201 min_binding_size: None,
202 },
203 count: None,
204 },
205 ],
206 });
207
208 let header_cpu = DeformHeader::zeroed();
209 let header_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
210 label: Some("deform_header"),
211 contents: bytemuck::bytes_of(&header_cpu),
212 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
213 });
214
215 let dummy_words = vec![0u32; SLOT_LAYOUT_WORDS];
216 let dummy_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
217 label: Some("deform_dummy_data"),
218 contents: bytemuck::cast_slice(&dummy_words),
219 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
220 });
221 let dummy_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
222 label: Some("deform_dummy_instance_data"),
223 contents: bytemuck::cast_slice(&dummy_words),
224 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
225 });
226
227 let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
228 label: Some("deform_dummy_bg"),
229 layout: &bind_group_layout,
230 entries: &[
231 wgpu::BindGroupEntry {
232 binding: 0,
233 resource: header_buffer.as_entire_binding(),
234 },
235 wgpu::BindGroupEntry {
236 binding: 1,
237 resource: dummy_data_buffer.as_entire_binding(),
238 },
239 wgpu::BindGroupEntry {
240 binding: 2,
241 resource: dummy_instance_buffer.as_entire_binding(),
242 },
243 ],
244 });
245
246 Self {
247 bind_group_layout,
248 header_buffer,
249 dummy_data_buffer,
250 dummy_instance_buffer,
251 dummy_bind_group,
252 meshes: HashMap::new(),
253 header_cpu,
254 registrations: Vec::new(),
255 }
256 }
257
258 #[allow(dead_code)]
263 pub fn bind_group_for(&self, mesh_id: MeshId) -> &wgpu::BindGroup {
264 self.meshes
265 .get(&mesh_id)
266 .map(|m| &m.bind_group)
267 .unwrap_or(&self.dummy_bind_group)
268 }
269
270 pub fn flag_bits(&self, mesh_id: MeshId) -> u32 {
275 self.meshes
276 .get(&mesh_id)
277 .map(|m| m.flag_bits | m.instance_flag_bits_union)
278 .unwrap_or(0)
279 }
280
281 pub(crate) fn has_per_instance_deform_data(
286 &self,
287 mesh_id: MeshId,
288 instance_id: Option<u32>,
289 ) -> bool {
290 let Some(id) = instance_id else {
291 return false;
292 };
293 self.meshes
294 .get(&mesh_id)
295 .and_then(|m| m.instances.get(&id))
296 .is_some_and(|i| i.flag_bits != 0)
297 }
298
299 #[allow(dead_code)]
303 pub fn instance_bind_group_for(
304 &self,
305 mesh_id: MeshId,
306 instance_id: Option<u32>,
307 ) -> &wgpu::BindGroup {
308 let Some(mesh) = self.meshes.get(&mesh_id) else {
309 return &self.dummy_bind_group;
310 };
311 if let Some(id) = instance_id {
312 if let Some(inst) = mesh.instances.get(&id) {
313 return &inst.bind_group;
314 }
315 }
316 &mesh.bind_group
317 }
318
319 fn pack(
322 slot_data: &[Option<Vec<u8>>; DEFORM_SLOT_COUNT],
323 slot_stride: &[u32; DEFORM_SLOT_COUNT],
324 ) -> Vec<u32> {
325 let mut words = vec![0u32; SLOT_LAYOUT_WORDS];
326 for slot in 0..DEFORM_SLOT_COUNT {
327 if let Some(bytes) = &slot_data[slot] {
328 let offset_words = words.len() as u32;
329 words[slot * 2] = offset_words;
330 words[slot * 2 + 1] = slot_stride[slot];
331 let extra = bytes.len() / 4;
332 words.reserve(extra);
333 for chunk in bytes.chunks_exact(4) {
334 words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
335 }
336 }
337 }
338 words
339 }
340
341 fn make_bind_group(
342 &self,
343 device: &wgpu::Device,
344 label: &str,
345 mesh_buffer: &wgpu::Buffer,
346 instance_buffer: &wgpu::Buffer,
347 ) -> wgpu::BindGroup {
348 device.create_bind_group(&wgpu::BindGroupDescriptor {
349 label: Some(label),
350 layout: &self.bind_group_layout,
351 entries: &[
352 wgpu::BindGroupEntry {
353 binding: 0,
354 resource: self.header_buffer.as_entire_binding(),
355 },
356 wgpu::BindGroupEntry {
357 binding: 1,
358 resource: mesh_buffer.as_entire_binding(),
359 },
360 wgpu::BindGroupEntry {
361 binding: 2,
362 resource: instance_buffer.as_entire_binding(),
363 },
364 ],
365 })
366 }
367
368 fn refresh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
373 let Some(m) = self.meshes.get(&mesh_id) else {
374 return;
375 };
376 if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
377 self.meshes.remove(&mesh_id);
378 return;
379 }
380 let words = Self::pack(&m.slot_data, &m.slot_stride);
381 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
382 label: Some("deform_mesh_data"),
383 contents: bytemuck::cast_slice(&words),
384 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
385 });
386 let new_bg = self.make_bind_group(
387 device,
388 "deform_mesh_bg",
389 &new_buffer,
390 &self.dummy_instance_buffer,
391 );
392
393 let instance_ids: Vec<u32> =
395 self.meshes.get(&mesh_id).unwrap().instances.keys().copied().collect();
396 for id in instance_ids {
397 let inst_buf_clone = {
398 let inst = self.meshes.get(&mesh_id).unwrap().instances.get(&id).unwrap();
399 inst.buffer.clone()
402 };
403 let bg =
404 self.make_bind_group(device, "deform_mesh_instance_bg", &new_buffer, &inst_buf_clone);
405 let inst = self
406 .meshes
407 .get_mut(&mesh_id)
408 .unwrap()
409 .instances
410 .get_mut(&id)
411 .unwrap();
412 inst.bind_group = bg;
413 }
414
415 let m = self.meshes.get_mut(&mesh_id).unwrap();
416 m.buffer = new_buffer;
417 m.bind_group = new_bg;
418 }
419
420 fn ensure_mesh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
421 if self.meshes.contains_key(&mesh_id) {
422 return;
423 }
424 let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
425 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
426 label: Some("deform_mesh_data_init"),
427 contents: bytemuck::cast_slice(&init_words),
428 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
429 });
430 let bind_group = self.make_bind_group(
431 device,
432 "deform_mesh_bg",
433 &buffer,
434 &self.dummy_instance_buffer,
435 );
436 self.meshes.insert(
437 mesh_id,
438 MeshDeform {
439 slot_data: Default::default(),
440 slot_stride: [0; DEFORM_SLOT_COUNT],
441 buffer,
442 bind_group,
443 flag_bits: 0,
444 instance_flag_bits_union: 0,
445 instances: HashMap::new(),
446 },
447 );
448 }
449
450 pub fn attach_slot(
454 &mut self,
455 device: &wgpu::Device,
456 mesh_id: MeshId,
457 slot: usize,
458 stride_words: u32,
459 data: &[u8],
460 ) {
461 assert!(slot < DEFORM_SLOT_COUNT);
462 assert!(
463 data.len() % 4 == 0,
464 "deform slot data length must be a multiple of 4 bytes"
465 );
466 self.ensure_mesh(device, mesh_id);
467 let entry = self.meshes.get_mut(&mesh_id).unwrap();
468 entry.slot_data[slot] = Some(data.to_vec());
469 entry.slot_stride[slot] = stride_words;
470 entry.flag_bits |= 1u32 << slot;
471 self.refresh(device, mesh_id);
472 }
473
474 pub fn detach_slot(&mut self, device: &wgpu::Device, mesh_id: MeshId, slot: usize) -> bool {
476 assert!(slot < DEFORM_SLOT_COUNT);
477 let Some(m) = self.meshes.get_mut(&mesh_id) else {
478 return false;
479 };
480 let had = m.slot_data[slot].take().is_some();
481 if had {
482 m.slot_stride[slot] = 0;
483 m.flag_bits &= !(1u32 << slot);
484 self.refresh(device, mesh_id);
485 }
486 had
487 }
488
489 pub fn has_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
490 self.meshes
491 .get(&mesh_id)
492 .map(|m| m.slot_data[slot].is_some())
493 .unwrap_or(false)
494 }
495
496 fn recompute_instance_union(m: &mut MeshDeform) {
497 let mut union = 0u32;
498 for inst in m.instances.values() {
499 union |= inst.flag_bits;
500 }
501 m.instance_flag_bits_union = union;
502 }
503
504 pub fn attach_slot_instance(
510 &mut self,
511 device: &wgpu::Device,
512 queue: &wgpu::Queue,
513 mesh_id: MeshId,
514 instance_id: u32,
515 slot: usize,
516 stride_words: u32,
517 data: &[u8],
518 ) {
519 assert!(slot < DEFORM_SLOT_COUNT);
520 assert!(
521 data.len() % 4 == 0,
522 "deform slot data length must be a multiple of 4 bytes"
523 );
524 self.ensure_mesh(device, mesh_id);
525 let needs_insert = !self
527 .meshes
528 .get(&mesh_id)
529 .unwrap()
530 .instances
531 .contains_key(&instance_id);
532 if needs_insert {
533 let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
534 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
535 label: Some("deform_mesh_instance_data_init"),
536 contents: bytemuck::cast_slice(&init_words),
537 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
538 });
539 let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
540 let bind_group =
541 self.make_bind_group(device, "deform_mesh_instance_bg", &mesh_buf_clone, &buffer);
542 let m = self.meshes.get_mut(&mesh_id).unwrap();
543 let cap = (init_words.len() * 4) as u64;
544 m.instances.insert(
545 instance_id,
546 InstanceDeform {
547 slot_data: Default::default(),
548 slot_stride: [0; DEFORM_SLOT_COUNT],
549 buffer,
550 buffer_capacity: cap,
551 bind_group,
552 flag_bits: 0,
553 },
554 );
555 }
556
557 {
559 let inst = self
560 .meshes
561 .get_mut(&mesh_id)
562 .unwrap()
563 .instances
564 .get_mut(&instance_id)
565 .unwrap();
566 inst.slot_data[slot] = Some(data.to_vec());
567 inst.slot_stride[slot] = stride_words;
568 inst.flag_bits |= 1u32 << slot;
569 }
570
571 let (packed_words, packed_bytes_len) = {
573 let inst = self
574 .meshes
575 .get(&mesh_id)
576 .unwrap()
577 .instances
578 .get(&instance_id)
579 .unwrap();
580 let w = Self::pack(&inst.slot_data, &inst.slot_stride);
581 let len = (w.len() * 4) as u64;
582 (w, len)
583 };
584
585 let needs_realloc = {
586 let inst = self
587 .meshes
588 .get(&mesh_id)
589 .unwrap()
590 .instances
591 .get(&instance_id)
592 .unwrap();
593 packed_bytes_len > inst.buffer_capacity
594 };
595
596 if needs_realloc {
597 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
598 label: Some("deform_mesh_instance_data"),
599 contents: bytemuck::cast_slice(&packed_words),
600 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
601 });
602 let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
603 let bg = self.make_bind_group(
604 device,
605 "deform_mesh_instance_bg",
606 &mesh_buf_clone,
607 &new_buffer,
608 );
609 let inst = self
610 .meshes
611 .get_mut(&mesh_id)
612 .unwrap()
613 .instances
614 .get_mut(&instance_id)
615 .unwrap();
616 inst.buffer = new_buffer;
617 inst.buffer_capacity = packed_bytes_len;
618 inst.bind_group = bg;
619 } else {
620 let inst = self
621 .meshes
622 .get(&mesh_id)
623 .unwrap()
624 .instances
625 .get(&instance_id)
626 .unwrap();
627 queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
628 }
629
630 let m = self.meshes.get_mut(&mesh_id).unwrap();
632 Self::recompute_instance_union(m);
633 }
634
635 pub fn detach_slot_instance(
639 &mut self,
640 device: &wgpu::Device,
641 queue: &wgpu::Queue,
642 mesh_id: MeshId,
643 instance_id: u32,
644 slot: usize,
645 ) -> bool {
646 assert!(slot < DEFORM_SLOT_COUNT);
647 let Some(m) = self.meshes.get_mut(&mesh_id) else {
648 return false;
649 };
650 let Some(inst) = m.instances.get_mut(&instance_id) else {
651 return false;
652 };
653 let had = inst.slot_data[slot].take().is_some();
654 if !had {
655 return false;
656 }
657 inst.slot_stride[slot] = 0;
658 inst.flag_bits &= !(1u32 << slot);
659 let drop_instance = inst.flag_bits == 0;
660
661 if drop_instance {
662 m.instances.remove(&instance_id);
663 } else {
664 let (packed_words, packed_bytes_len) = {
665 let inst = m.instances.get(&instance_id).unwrap();
666 let w = Self::pack(&inst.slot_data, &inst.slot_stride);
667 let len = (w.len() * 4) as u64;
668 (w, len)
669 };
670 let inst = m.instances.get(&instance_id).unwrap();
671 if packed_bytes_len > inst.buffer_capacity {
672 let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
673 label: Some("deform_mesh_instance_data"),
674 contents: bytemuck::cast_slice(&packed_words),
675 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
676 });
677 let mesh_buf_clone = m.buffer.clone();
678 let bg = self.make_bind_group(
679 device,
680 "deform_mesh_instance_bg",
681 &mesh_buf_clone,
682 &new_buffer,
683 );
684 let m = self.meshes.get_mut(&mesh_id).unwrap();
685 let inst = m.instances.get_mut(&instance_id).unwrap();
686 inst.buffer = new_buffer;
687 inst.buffer_capacity = packed_bytes_len;
688 inst.bind_group = bg;
689 } else {
690 queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
691 }
692 }
693
694 let m = self.meshes.get_mut(&mesh_id).unwrap();
695 Self::recompute_instance_union(m);
696 if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
698 self.meshes.remove(&mesh_id);
699 }
700 true
701 }
702
703 pub fn has_slot_instance(&self, mesh_id: MeshId, instance_id: u32, slot: usize) -> bool {
704 self.meshes
705 .get(&mesh_id)
706 .and_then(|m| m.instances.get(&instance_id))
707 .map(|i| i.slot_data[slot].is_some())
708 .unwrap_or(false)
709 }
710}
711
712impl ViewportGpuResources {
717 pub fn set_deform_time(&mut self, queue: &wgpu::Queue, time_seconds: f32) {
720 self.deform.header_cpu.time_seconds = time_seconds;
721 queue.write_buffer(
722 &self.deform.header_buffer,
723 0,
724 bytemuck::bytes_of(&self.deform.header_cpu),
725 );
726 }
727
728 pub fn set_deform_slot_params(
731 &mut self,
732 queue: &wgpu::Queue,
733 slot: usize,
734 params: [[f32; 4]; DEFORM_PARAMS_PER_SLOT],
735 ) {
736 assert!(slot < DEFORM_SLOT_COUNT);
737 let base = slot * DEFORM_PARAMS_PER_SLOT;
738 for (i, p) in params.iter().enumerate() {
739 self.deform.header_cpu.slot_params[base + i] = *p;
740 }
741 queue.write_buffer(
742 &self.deform.header_buffer,
743 0,
744 bytemuck::bytes_of(&self.deform.header_cpu),
745 );
746 }
747
748 pub fn attach_deform_slot(
756 &mut self,
757 device: &wgpu::Device,
758 mesh_id: MeshId,
759 slot: usize,
760 stride_bytes: u32,
761 data: &[u8],
762 ) {
763 assert!(
764 stride_bytes >= 4 && stride_bytes % 4 == 0,
765 "deform slot stride must be a positive multiple of 4 bytes"
766 );
767 self.deform
768 .attach_slot(device, mesh_id, slot, stride_bytes / 4, data);
769 }
770
771 pub fn detach_deform_slot(
773 &mut self,
774 device: &wgpu::Device,
775 mesh_id: MeshId,
776 slot: usize,
777 ) -> bool {
778 self.deform.detach_slot(device, mesh_id, slot)
779 }
780
781 pub fn has_deform_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
784 self.deform.has_slot(mesh_id, slot)
785 }
786
787 pub fn attach_deform_slot_instance(
792 &mut self,
793 device: &wgpu::Device,
794 queue: &wgpu::Queue,
795 mesh_id: MeshId,
796 instance_id: u32,
797 slot: usize,
798 stride_bytes: u32,
799 data: &[u8],
800 ) {
801 assert!(
802 stride_bytes >= 4 && stride_bytes % 4 == 0,
803 "deform slot stride must be a positive multiple of 4 bytes"
804 );
805 self.deform.attach_slot_instance(
806 device,
807 queue,
808 mesh_id,
809 instance_id,
810 slot,
811 stride_bytes / 4,
812 data,
813 );
814 }
815
816 pub fn detach_deform_slot_instance(
819 &mut self,
820 device: &wgpu::Device,
821 queue: &wgpu::Queue,
822 mesh_id: MeshId,
823 instance_id: u32,
824 slot: usize,
825 ) -> bool {
826 self.deform
827 .detach_slot_instance(device, queue, mesh_id, instance_id, slot)
828 }
829
830 pub fn has_deform_slot_instance(
833 &self,
834 mesh_id: MeshId,
835 instance_id: u32,
836 slot: usize,
837 ) -> bool {
838 self.deform.has_slot_instance(mesh_id, instance_id, slot)
839 }
840
841 pub fn register_deformer(
870 &mut self,
871 device: &wgpu::Device,
872 desc: DeformerDesc,
873 ) -> ViewportResult<DeformerId> {
874 validate_name(&self.deform.registrations, desc.name)?;
875 let slot = allocate_slot(&self.deform.registrations)?;
876
877 let candidate = StoredDeformer { desc, slot };
878 let mut trial = self.deform.registrations.clone();
879 trial.push(candidate.clone());
880
881 for shader_name in MESH_FAMILY_SHADERS {
882 let Some(base) = lookup_source(shader_name) else {
883 return Err(crate::error::ViewportError::DeformShaderInvalid {
884 reason: format!(
885 "internal: shader '{shader_name}' missing from shader catalog"
886 ),
887 });
888 };
889 let composed = compose_shader(base, &trial);
890 let label = format!("deform_compose_{shader_name}");
891 validate_with_wgpu(device, &label, &composed)?;
892 }
893
894 self.deform.registrations.push(candidate);
895 self.rebuild_mesh_pipelines(device);
896 Ok(DeformerId(slot))
897 }
898
899 #[allow(dead_code)]
906 pub(crate) fn register_internal_deformer(
907 &mut self,
908 device: &wgpu::Device,
909 desc: DeformerDesc,
910 ) -> ViewportResult<DeformerId> {
911 validate_name(&self.deform.registrations, desc.name)?;
912 let slot = allocate_internal_slot(&self.deform.registrations)?;
913
914 let candidate = StoredDeformer { desc, slot };
915 let mut trial = self.deform.registrations.clone();
916 trial.push(candidate.clone());
917
918 for shader_name in MESH_FAMILY_SHADERS {
919 let Some(base) = lookup_source(shader_name) else {
920 return Err(crate::error::ViewportError::DeformShaderInvalid {
921 reason: format!(
922 "internal: shader '{shader_name}' missing from shader catalog"
923 ),
924 });
925 };
926 let composed = compose_shader(base, &trial);
927 let label = format!("deform_compose_{shader_name}");
928 validate_with_wgpu(device, &label, &composed)?;
929 }
930
931 self.deform.registrations.push(candidate);
932 self.rebuild_mesh_pipelines(device);
933 Ok(DeformerId(slot))
934 }
935
936 pub fn registered_deformer_count(&self) -> usize {
938 self.deform.registrations.len()
939 }
940
941 pub fn deformer_id_by_name(&self, name: &str) -> Option<DeformerId> {
944 self.deform
945 .registrations
946 .iter()
947 .find(|r| r.desc.name == name)
948 .map(|r| DeformerId(r.slot))
949 }
950
951 pub fn deform_slot_handle(&self, id: DeformerId) -> DeformSlotHandle {
956 DeformSlotHandle {
957 header_buffer: self.deform.header_buffer.clone(),
958 slot: id.slot(),
959 }
960 }
961
962 fn rebuild_mesh_pipelines(&mut self, device: &wgpu::Device) {
969 let registrations = self.deform.registrations.clone();
970
971 if let Some(base) = lookup_source("mesh.wgsl") {
973 let composed = compose_shader(base, ®istrations);
974 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
975 label: Some("mesh_shader_composed"),
976 source: wgpu::ShaderSource::Wgsl(composed.into()),
977 });
978
979 let ldr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
980 device,
981 "mesh_pipeline_layout",
982 &self.camera_bind_group_layout,
983 &self.object_bind_group_layout,
984 &self.deform.bind_group_layout,
985 );
986 let ldr = crate::resources::mesh_pipelines::build_ldr_mesh_pipelines(
987 device,
988 &ldr_layout,
989 &shader,
990 self.target_format,
991 self.sample_count,
992 );
993 self.solid_pipeline = ldr.solid;
994 self.solid_two_sided_pipeline = ldr.solid_two_sided;
995 self.transparent_pipeline = ldr.transparent;
996 self.wireframe_pipeline = ldr.wireframe;
997
998 if self.hdr_solid_pipeline.is_some() {
999 let hdr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1000 device,
1001 "hdr_mesh_pipeline_layout",
1002 &self.camera_bind_group_layout,
1003 &self.object_bind_group_layout,
1004 &self.deform.bind_group_layout,
1005 );
1006 let hdr = crate::resources::mesh_pipelines::build_hdr_mesh_pipelines(
1007 device,
1008 &hdr_layout,
1009 &shader,
1010 );
1011 self.hdr_solid_pipeline = Some(hdr.solid);
1012 self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1013 self.hdr_transparent_pipeline = Some(hdr.transparent);
1014 self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1015 }
1016 }
1017
1018 if self.oit_pipeline.is_some() {
1021 if let Some(base) = lookup_source("mesh_oit.wgsl") {
1022 let composed = compose_shader(base, ®istrations);
1023 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1024 label: Some("mesh_oit_shader_composed"),
1025 source: wgpu::ShaderSource::Wgsl(composed.into()),
1026 });
1027 let oit_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1028 device,
1029 "oit_pipeline_layout",
1030 &self.camera_bind_group_layout,
1031 &self.object_bind_group_layout,
1032 &self.deform.bind_group_layout,
1033 );
1034 let oit = crate::resources::mesh_pipelines::build_oit_pipeline(
1035 device,
1036 &oit_layout,
1037 &shader,
1038 );
1039 self.oit_pipeline = Some(oit);
1040 }
1041 }
1042
1043 if let Some(base) = lookup_source("shadow.wgsl") {
1045 let composed = compose_shader(base, ®istrations);
1046 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1047 label: Some("shadow_shader_composed"),
1048 source: wgpu::ShaderSource::Wgsl(composed.into()),
1049 });
1050 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1051 label: Some("shadow_pipeline_layout"),
1052 bind_group_layouts: &[
1053 &self.shadow_camera_bind_group_layout,
1054 &self.object_bind_group_layout,
1055 &self.deform.bind_group_layout,
1056 ],
1057 push_constant_ranges: &[],
1058 });
1059 self.shadow_pipeline = crate::resources::mesh_pipelines::build_shadow_pipeline(
1060 device, &layout, &shader,
1061 );
1062 }
1063
1064 if let Some(base) = lookup_source("outline_mask.wgsl") {
1066 let composed = compose_shader(base, ®istrations);
1067 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1068 label: Some("outline_mask_shader_composed"),
1069 source: wgpu::ShaderSource::Wgsl(composed.into()),
1070 });
1071 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1072 label: Some("outline_pipeline_layout"),
1073 bind_group_layouts: &[
1074 &self.camera_bind_group_layout,
1075 &self.outline_bind_group_layout,
1076 &self.deform.bind_group_layout,
1077 ],
1078 push_constant_ranges: &[],
1079 });
1080 let masks = crate::resources::mesh_pipelines::build_outline_mask_pipelines(
1081 device,
1082 &layout,
1083 &shader,
1084 wgpu::TextureFormat::R8Unorm,
1085 );
1086 self.outline_mask_pipeline = masks.mask;
1087 self.outline_mask_two_sided_pipeline = masks.mask_two_sided;
1088 }
1089
1090 if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1095 let composed = compose_shader(base, ®istrations);
1096 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1097 label: Some("mesh_instanced_shader_composed"),
1098 source: wgpu::ShaderSource::Wgsl(composed.into()),
1099 });
1100
1101 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1102 if self.solid_instanced_pipeline.is_some() {
1103 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1104 device,
1105 "instanced_pipeline_layout",
1106 &self.camera_bind_group_layout,
1107 instance_bgl,
1108 &self.deform.bind_group_layout,
1109 );
1110 let ldr = crate::resources::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1111 device,
1112 &layout,
1113 &shader,
1114 self.target_format,
1115 self.sample_count,
1116 );
1117 self.solid_instanced_pipeline = Some(ldr.solid);
1118 self.transparent_instanced_pipeline = Some(ldr.transparent);
1119 }
1120 if self.hdr_solid_instanced_pipeline.is_some() {
1121 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1122 device,
1123 "hdr_instanced_pipeline_layout",
1124 &self.camera_bind_group_layout,
1125 instance_bgl,
1126 &self.deform.bind_group_layout,
1127 );
1128 let hdr = crate::resources::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1129 device,
1130 &layout,
1131 &shader,
1132 );
1133 self.hdr_solid_instanced_pipeline = Some(hdr.solid);
1134 self.hdr_transparent_instanced_pipeline = Some(hdr.transparent);
1135 self.hdr_instanced_additive_pipeline = Some(hdr.additive);
1136 self.hdr_instanced_premultiplied_pipeline = Some(hdr.premultiplied);
1137 }
1138 }
1139 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1140 if self.hdr_solid_instanced_cull_pipeline.is_some() {
1141 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1142 device,
1143 "hdr_instanced_cull_pipeline_layout",
1144 &self.camera_bind_group_layout,
1145 cull_bgl,
1146 &self.deform.bind_group_layout,
1147 );
1148 let pl = crate::resources::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1149 device,
1150 &layout,
1151 &shader,
1152 );
1153 self.hdr_solid_instanced_cull_pipeline = Some(pl);
1154 }
1155 }
1156 }
1157
1158 if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1160 let composed = compose_shader(base, ®istrations);
1161 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1162 label: Some("mesh_instanced_oit_shader_composed"),
1163 source: wgpu::ShaderSource::Wgsl(composed.into()),
1164 });
1165 if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1166 if self.oit_instanced_pipeline.is_some() {
1167 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1168 device,
1169 "oit_instanced_pipeline_layout",
1170 &self.camera_bind_group_layout,
1171 instance_bgl,
1172 &self.deform.bind_group_layout,
1173 );
1174 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1175 device,
1176 &layout,
1177 &shader,
1178 "oit_instanced_pipeline",
1179 "vs_main",
1180 );
1181 self.oit_instanced_pipeline = Some(pl);
1182 }
1183 }
1184 if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1185 if self.oit_instanced_cull_pipeline.is_some() {
1186 let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1187 device,
1188 "oit_instanced_cull_pipeline_layout",
1189 &self.camera_bind_group_layout,
1190 cull_bgl,
1191 &self.deform.bind_group_layout,
1192 );
1193 let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1194 device,
1195 &layout,
1196 &shader,
1197 "oit_instanced_cull_pipeline",
1198 "vs_main_cull",
1199 );
1200 self.oit_instanced_cull_pipeline = Some(pl);
1201 }
1202 }
1203 }
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210
1211 fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1212 let instance = wgpu::Instance::default();
1213 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1214 power_preference: wgpu::PowerPreference::default(),
1215 force_fallback_adapter: false,
1216 compatible_surface: None,
1217 }))
1218 .ok()?;
1219 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1220 label: Some("deform_tests"),
1221 ..Default::default()
1222 }))
1223 .ok()?;
1224 Some((device, queue))
1225 }
1226
1227 #[test]
1228 fn pack_lays_out_offsets_after_slot_layout_prefix() {
1229 let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1230 let mut stride = [0u32; DEFORM_SLOT_COUNT];
1231 data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1233 stride[0] = 1;
1234 data[2] = Some(vec![
1236 10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0,
1237 ]);
1238 stride[2] = 2;
1239
1240 let words = DeformationState::pack(&data, &stride);
1241 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;
1252 assert_eq!(words[slot0_base], 1);
1253 assert_eq!(words[slot0_base + 1], 2);
1254 assert_eq!(words[slot0_base + 2], 3);
1255 let slot2_base = slot0_base + 3;
1257 assert_eq!(words[slot2_base], 10);
1258 assert_eq!(words[slot2_base + 1], 20);
1259 assert_eq!(words[slot2_base + 2], 30);
1260 assert_eq!(words[slot2_base + 3], 40);
1261 }
1262
1263 #[test]
1264 fn attach_marks_flag_bit_and_swaps_bind_group() {
1265 let Some((device, _queue)) = headless() else {
1266 return;
1267 };
1268 let mut s = DeformationState::new(&device);
1269 let mesh = MeshId(7);
1270 assert_eq!(s.flag_bits(mesh), 0);
1271 assert!(!s.has_slot(mesh, 0));
1272
1273 s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1275 assert!(s.has_slot(mesh, 0));
1276 assert_eq!(s.flag_bits(mesh), 0b0001);
1277
1278 s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1280 assert!(s.has_slot(mesh, 2));
1281 assert_eq!(s.flag_bits(mesh), 0b0101);
1282 }
1283
1284 #[test]
1285 fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1286 let Some((device, _queue)) = headless() else {
1287 return;
1288 };
1289 let mut s = DeformationState::new(&device);
1290 let mesh = MeshId(11);
1291 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1292 assert_eq!(s.flag_bits(mesh), 0b0010);
1293
1294 assert!(s.detach_slot(&device, mesh, 1));
1295 assert_eq!(s.flag_bits(mesh), 0);
1296 assert!(!s.meshes.contains_key(&mesh));
1297 assert!(!s.detach_slot(&device, mesh, 1));
1298 }
1299
1300 #[test]
1301 fn attach_slot_instance_marks_union_and_routes_bind_group() {
1302 let Some((device, queue)) = headless() else {
1303 return;
1304 };
1305 let mut s = DeformationState::new(&device);
1306 let mesh = MeshId(42);
1307 let instance = 3u32;
1308 assert_eq!(s.flag_bits(mesh), 0);
1309
1310 s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1312 s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1313
1314 assert_eq!(s.flag_bits(mesh), 0b0011);
1316 assert!(s.has_slot_instance(mesh, instance, 0));
1317 assert!(!s.has_slot_instance(mesh, instance, 1));
1318
1319 let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1321 let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1322 assert_ne!(inst_bg, mesh_bg);
1323
1324 let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1327 assert_eq!(fallback, mesh_bg);
1328
1329 assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1332 assert_eq!(s.flag_bits(mesh), 0b0010);
1333 assert!(!s.has_slot_instance(mesh, instance, 0));
1334 }
1335
1336 #[test]
1337 fn slot_index_assert_traps_out_of_range() {
1338 let Some((device, _queue)) = headless() else {
1339 return;
1340 };
1341 let mut s = DeformationState::new(&device);
1342 let result =
1343 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1344 s.attach_slot(&device, MeshId(0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1345 }));
1346 assert!(result.is_err());
1347 }
1348
1349 #[test]
1358 fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1359 use crate::renderer::ViewportRenderer;
1360 let Some((device, _queue)) = headless() else {
1361 return;
1362 };
1363 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1364
1365 let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1366 let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1367
1368 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";
1369 let desc = DeformerDesc {
1370 name: "wave",
1371 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1372 priority: 0,
1373 wgsl_body: body.to_string(),
1374 per_vertex_stride: 4,
1375 };
1376 let id = renderer
1377 .resources_mut()
1378 .register_deformer(&device, desc)
1379 .expect("register");
1380 assert_eq!(id.slot(), 0);
1381
1382 let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1383 let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1384 assert_ne!(solid_before, std::ptr::null());
1389 assert_ne!(solid_after, std::ptr::null());
1390 assert_ne!(wf_before, std::ptr::null());
1391 assert_ne!(wf_after, std::ptr::null());
1392 }
1393
1394 #[test]
1395 fn register_deformer_validates_and_assigns_slot() {
1396 use crate::renderer::ViewportRenderer;
1397 let Some((device, _queue)) = headless() else {
1398 return;
1399 };
1400 let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1401 let resources = renderer.resources_mut();
1402 let desc = DeformerDesc {
1403 name: "wind",
1404 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1405 priority: 0,
1406 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(),
1407 per_vertex_stride: 4,
1408 };
1409 let baseline = resources.registered_deformer_count();
1413 let id = resources.register_deformer(&device, desc).expect("register");
1414 assert_eq!(id.slot(), 0);
1415 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1416
1417 let dup = DeformerDesc {
1418 name: "wind",
1419 stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1420 priority: 0,
1421 wgsl_body: "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }".to_string(),
1422 per_vertex_stride: 4,
1423 };
1424 let err = resources.register_deformer(&device, dup).unwrap_err();
1425 assert!(matches!(
1426 err,
1427 crate::error::ViewportError::DeformNameTaken { .. }
1428 ));
1429 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1430
1431 let bad = DeformerDesc {
1432 name: "wave",
1433 stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1434 priority: 0,
1435 wgsl_body: "this is not valid wgsl".to_string(),
1436 per_vertex_stride: 4,
1437 };
1438 let err = resources.register_deformer(&device, bad).unwrap_err();
1439 assert!(matches!(
1440 err,
1441 crate::error::ViewportError::DeformShaderInvalid { .. }
1442 ));
1443 assert_eq!(resources.registered_deformer_count(), baseline + 1);
1444 }
1445}