Skip to main content

viewport_lib/resources/mesh_sidecar/
deform.rs

1//! Per-vertex deformation sidecar storage.
2//!
3//! Hosts the `@group(2)` bind group used by every mesh-family pipeline.
4//! Each mesh's slot data is packed into a single storage buffer prefixed
5//! with `(offset, stride)` pairs per slot; shader-side reads go through
6//! the `deform_read_*` helpers in `deform.wgsl`. Meshes with no attached
7//! deformer data fall back to the renderer-owned dummy bind group at zero
8//! cost.
9
10use 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
23/// Maximum number of registered deformer slots (host range + reserved
24/// internal range). Slot indices `[0, DEFORM_INTERNAL_SLOT_START)` are
25/// available to hosts through `register_deformer`; indices
26/// `[DEFORM_INTERNAL_SLOT_START, DEFORM_SLOT_COUNT)` are reserved for the
27/// in-crate deformers (skinning, displacement) that register at renderer
28/// construction. Slot data is packed into two storage buffers (per-mesh and
29/// per-instance) so deform contributes three vertex-stage bindings total
30/// regardless of slot count.
31pub const DEFORM_SLOT_COUNT: usize = 8;
32
33/// First slot index reserved for in-crate deformers.
34pub const DEFORM_INTERNAL_SLOT_START: usize = 4;
35
36/// `vec4<f32>` count per slot inside the shared header uniform. Keep in sync
37/// with `DeformHeader` and the `slot_params` field in `deform.wgsl`.
38pub const DEFORM_PARAMS_PER_SLOT: usize = 4;
39
40/// Number of u32 words at the start of every per-mesh packed buffer
41/// reserved for slot layout: an `(offset, stride)` pair per slot.
42const SLOT_LAYOUT_WORDS: usize = DEFORM_SLOT_COUNT * 2;
43
44/// Shared header uniform; one region per slot plus a global time value.
45/// Mirrors `DeformHeader` in `deform.wgsl`.
46#[repr(C)]
47#[derive(Copy, Clone, Pod, Zeroable)]
48pub(crate) struct DeformHeader {
49    pub time_seconds: f32,
50    pub _pad: [f32; 3],
51    /// Flat array: slot `i` reads `slot_params[i * DEFORM_PARAMS_PER_SLOT .. (i+1) * DEFORM_PARAMS_PER_SLOT]`.
52    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
65/// Byte offset of slot `slot`'s `slot_params` block inside the deformation
66/// header uniform. The header layout is `[time_seconds, _pad x3]`
67/// (16 bytes) followed by 32 contiguous `vec4<f32>` slot-params entries,
68/// 4 per slot.
69pub 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/// Byte offset of slot `slot`'s parameter block inside the deformation
74/// header uniform.
75#[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/// Handle that lets a plugin update one deformer slot's `slot_params` block
81/// from a context that only has `&wgpu::Queue` (a `GpuPlugin::pre_prepare`,
82/// say). Cloning is cheap.
83#[derive(Clone)]
84pub struct DeformSlotHandle {
85    header_buffer: wgpu::Buffer,
86    slot: usize,
87}
88
89impl DeformSlotHandle {
90    /// The slot this handle writes.
91    pub fn slot(&self) -> usize {
92        self.slot
93    }
94
95    /// Write the four `vec4<f32>` parameter words for this slot. Cheap;
96    /// safe to call per frame from a plugin's `pre_prepare`.
97    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
106/// Per-(mesh, instance) deformation storage. Holds the per-instance packed
107/// slot buffer and its bind group (which also binds the owning mesh's
108/// per-mesh buffer at the same time, so the draw needs only one bind-group
109/// swap to switch instances).
110pub(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    /// Bytes currently allocated in `buffer`. Used to decide when to realloc
115    /// rather than `write_buffer` in place.
116    pub buffer_capacity: u64,
117    pub bind_group: wgpu::BindGroup,
118    /// Bit `i` set when slot `i` has per-instance data attached.
119    pub flag_bits: u32,
120}
121
122/// Per-mesh deformation storage.
123pub(crate) struct MeshDeform {
124    /// Source bytes per slot, retained so attach/detach can re-pack
125    /// without forcing the caller to re-upload other slots.
126    pub slot_data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT],
127    /// Per-slot stride in u32 words. `slot_stride[i]` is meaningful only
128    /// when `slot_data[i].is_some()`.
129    pub slot_stride: [u32; DEFORM_SLOT_COUNT],
130    /// Packed buffer: `SLOT_LAYOUT_WORDS * 4` bytes of `(offset, stride)`
131    /// header followed by tightly packed slot bytes in slot order.
132    pub buffer: wgpu::Buffer,
133    /// Bind group for draws of this mesh that have no per-instance data:
134    /// binds the per-mesh buffer at @binding(1) and the renderer's empty
135    /// instance buffer at @binding(2).
136    pub bind_group: wgpu::BindGroup,
137    /// Bit `i` set when slot `i` has per-mesh data attached.
138    pub flag_bits: u32,
139    /// Bit `i` set when *any* instance of this mesh has slot `i`
140    /// per-instance data attached. Combined with `flag_bits` at
141    /// `ObjectUniform.deform_flags` write time so the shader sees both
142    /// per-mesh and per-instance slot activity.
143    pub instance_flag_bits_union: u32,
144    pub instances: HashMap<u32, InstanceDeform>,
145}
146
147/// Renderer-side deformation state.
148pub(crate) struct DeformationState {
149    pub bind_group_layout: wgpu::BindGroupLayout,
150    pub header_buffer: wgpu::Buffer,
151    /// Empty slot-layout prefix bound when a mesh has no attached data.
152    /// `SLOT_LAYOUT_WORDS` u32s of zero. Kept alive so the dummy bind group
153    /// stays valid.
154    #[allow(dead_code)]
155    pub dummy_data_buffer: wgpu::Buffer,
156    /// Empty per-instance buffer reused by every per-mesh bind group that
157    /// has no instance data yet. Kept alive on the state so the bind group
158    /// stays valid.
159    pub dummy_instance_buffer: wgpu::Buffer,
160    /// Bind group used when a mesh has no attached deformer data. Bound by
161    /// every mesh-family draw at slot 2 to satisfy the pipeline layout
162    /// without forcing per-mesh storage allocation.
163    pub dummy_bind_group: wgpu::BindGroup,
164    pub meshes: HashMap<MeshId, MeshDeform>,
165    pub header_cpu: DeformHeader,
166    /// Currently registered deformers, in registration order.
167    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    /// Returns the bind group to use for a given mesh: the per-mesh group
259    /// when any slot is attached, otherwise the renderer-owned dummy. Used
260    /// at draw time once the registry has rebuilt the mesh pipelines to bind
261    /// group 2.
262    #[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    /// `deform_flags` value to write into the `ObjectUniform` for `mesh_id`.
271    /// Folds per-mesh and per-instance slot activity together: any slot
272    /// with data on either side is reported as live so the registered
273    /// body's flag branch executes.
274    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    /// Whether `(mesh_id, instance_id)` has any per-instance deformer data
282    /// attached. Used by the renderer to filter items out of GPU-driven
283    /// instanced batching: items with per-instance deform data need their
284    /// own bind group at draw time and cannot share an instance batch.
285    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    /// Bind group to use for a draw of `(mesh_id, instance_id)`. Falls back
300    /// to the per-mesh-only bind group when no per-instance data is
301    /// attached, and to the renderer dummy when the mesh has no data at all.
302    #[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    /// Pack the per-slot data of one mesh into a single u32 stream prefixed
320    /// by `(offset, stride)` pairs per slot.
321    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    /// Re-pack and re-upload the mesh's data, rebuilding the per-mesh bind
369    /// group and every per-instance bind group (which also binds the
370    /// per-mesh buffer). Drops the mesh entry when no slot has any data
371    /// attached on either side.
372    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        // Rebuild instance bind groups since they bind this mesh's buffer.
394        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                // SAFETY: as_entire_binding holds a borrow of the buffer,
400                // not the buffer itself; we reborrow per call.
401                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    /// Attach raw bytes to a per-mesh slot for `mesh_id`. `stride_words` is
451    /// the per-vertex stride in u32 words (must equal the registered
452    /// deformer's `per_vertex_stride / 4`).
453    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    /// Detach a per-mesh slot. Returns `true` if any data was removed.
475    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    /// Attach raw bytes to a per-(mesh, instance) slot. The data is treated
505    /// as a sequence of fixed-stride elements (e.g. mat4 joint matrices
506    /// for skinning). When the instance buffer must grow, its bind group is
507    /// rebuilt. When the existing buffer is large enough, the data is
508    /// written with `queue.write_buffer` and the bind group is reused.
509    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        // Ensure the instance entry exists with a minimal buffer.
526        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        // Mutate slot bookkeeping.
558        {
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        // Re-pack and decide between in-place write or realloc.
572        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        // Recompute the union and refresh flag bits.
631        let m = self.meshes.get_mut(&mesh_id).unwrap();
632        Self::recompute_instance_union(m);
633    }
634
635    /// Detach a per-instance slot. Returns `true` if any data was removed.
636    /// Drops the instance entry when no slot remains attached and re-packs
637    /// the instance buffer if other slots still hold data.
638    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        // Drop the mesh entirely when nothing references it.
697        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
712// ---------------------------------------------------------------------------
713// Public API on ViewportGpuResources
714// ---------------------------------------------------------------------------
715
716impl ViewportGpuResources {
717    /// Write the shared header uniform's `time_seconds` field. Cheap; safe to
718    /// call per frame.
719    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    /// Write the four `vec4<f32>` parameter words for one slot in the shared
729    /// header uniform.
730    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    /// Attach raw bytes for one deformer slot on the given mesh.
749    ///
750    /// `stride_bytes` is the per-vertex byte stride and must equal the
751    /// registered deformer's `per_vertex_stride`. The data length is
752    /// expected to be `vertex_count * stride_bytes`; the renderer does not
753    /// validate the vertex count, only that the byte length is a multiple
754    /// of `stride_bytes` and 4.
755    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    /// Detach a per-mesh slot's data. Returns `true` if any data was removed.
772    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    /// Returns `true` when the mesh has per-mesh data attached at the given
782    /// slot.
783    pub fn has_deform_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
784        self.deform.has_slot(mesh_id, slot)
785    }
786
787    /// Attach raw bytes for one deformer slot on a single instance of the
788    /// given mesh. Used for data that varies per instance (e.g. joint
789    /// palettes). `stride_bytes` is the per-element byte stride; the data
790    /// length must be a multiple of `stride_bytes` and 4.
791    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    /// Detach a per-instance slot's data. Returns `true` if any data was
817    /// removed.
818    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    /// Returns `true` when the given instance of the mesh has per-instance
831    /// data attached at the given slot.
832    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    /// Register a deformer against the mesh shader family.
842    ///
843    /// Validates the descriptor's name and allocates a slot, composes every
844    /// mesh-family base shader with the new deformer plus all previously
845    /// registered ones, and runs each composed module through wgpu's
846    /// validator. On success, the LDR and HDR `mesh.wgsl` pipelines are
847    /// rebuilt from the freshly composed source so subsequent draws run
848    /// the registered body. Other mesh-family pipelines (instanced,
849    /// shadow, outline mask, OIT) continue to run the identity path until
850    /// their factories migrate to the same rebuild path.
851    ///
852    /// On any validation failure the registration is rolled back: the
853    /// previously composed sources stay live and the returned error names
854    /// the shader that failed.
855    ///
856    /// # Errors
857    ///
858    /// - [`ViewportError::DeformNameTaken`] when `desc.name` is already
859    ///   registered.
860    /// - [`ViewportError::DeformShaderInvalid`] when `desc.name` is not a
861    ///   valid WGSL identifier, or when any composed module fails
862    ///   validation.
863    /// - [`ViewportError::DeformSlotsExhausted`] when all
864    ///   `DEFORM_SLOT_COUNT` slots are in use.
865    ///
866    /// [`ViewportError::DeformNameTaken`]: crate::error::ViewportError::DeformNameTaken
867    /// [`ViewportError::DeformShaderInvalid`]: crate::error::ViewportError::DeformShaderInvalid
868    /// [`ViewportError::DeformSlotsExhausted`]: crate::error::ViewportError::DeformSlotsExhausted
869    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    /// Register a deformer on a reserved internal slot
900    /// (`[DEFORM_INTERNAL_SLOT_START, DEFORM_SLOT_COUNT)`). Used at
901    /// renderer construction for the in-crate deformers (skinning,
902    /// displacement) so they do not consume one of the four slots exposed
903    /// to hosts. Validation, composition, and pipeline rebuild work the
904    /// same as [`Self::register_deformer`].
905    #[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    /// Number of currently registered deformers (host + internal).
937    pub fn registered_deformer_count(&self) -> usize {
938        self.deform.registrations.len()
939    }
940
941    /// Look up a registered deformer's id by its name. Returns `None` when
942    /// no deformer with that name has been registered.
943    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    /// Build a [`DeformSlotHandle`] for the given deformer. Plugins stash
952    /// the handle at registration time and use it to write that slot's
953    /// `slot_params` block from contexts (e.g. a `GpuPlugin::pre_prepare`)
954    /// that only carry `&wgpu::Queue`.
955    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    /// Re-compose every mesh-family shader and rebuild the pipelines that
963    /// draw from it. Called by `register_deformer` once a new registration
964    /// has validated; safe to call between frames with zero registrations
965    /// to reset to the identity shader. The instanced and instanced-OIT
966    /// pipelines stay on their build-time shader modules until their
967    /// factories migrate to the same rebuild flow.
968    fn rebuild_mesh_pipelines(&mut self, device: &wgpu::Device) {
969        let registrations = self.deform.registrations.clone();
970
971        // mesh.wgsl: LDR + HDR families.
972        if let Some(base) = lookup_source("mesh.wgsl") {
973            let composed = compose_shader(base, &registrations);
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        // mesh_oit.wgsl: only present after ensure_hdr_shared has been
1019        // called.
1020        if self.oit_pipeline.is_some() {
1021            if let Some(base) = lookup_source("mesh_oit.wgsl") {
1022                let composed = compose_shader(base, &registrations);
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        // shadow.wgsl: depth-only cascade pass.
1044        if let Some(base) = lookup_source("shadow.wgsl") {
1045            let composed = compose_shader(base, &registrations);
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        // outline_mask.wgsl: mask-write pass for the selection silhouette.
1065        if let Some(base) = lookup_source("outline_mask.wgsl") {
1066            let composed = compose_shader(base, &registrations);
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        // mesh_instanced.wgsl: LDR (solid + transparent), HDR (solid +
1091        // transparent + additive + premultiplied), and HDR cull. Only
1092        // present after `ensure_instanced_pipelines` / its HDR sibling /
1093        // `ensure_cull_instance_pipelines` have run.
1094        if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1095            let composed = compose_shader(base, &registrations);
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        // mesh_instanced_oit.wgsl: non-cull and cull OIT pipelines.
1159        if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1160            let composed = compose_shader(base, &registrations);
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        // Slot 0: 3 vertices, stride 1 u32 each = 12 bytes
1232        data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1233        stride[0] = 1;
1234        // Slot 2: 2 vertices, stride 2 u32 each = 16 bytes
1235        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        // Layout prefix holds an (offset, stride) pair per slot.
1242        assert_eq!(words[0], SLOT_LAYOUT_WORDS as u32); // slot 0 offset
1243        assert_eq!(words[1], 1); // slot 0 stride
1244        assert_eq!(words[2], 0); // slot 1 offset (unused)
1245        assert_eq!(words[3], 0); // slot 1 stride
1246        assert_eq!(words[4], (SLOT_LAYOUT_WORDS + 3) as u32); // slot 2 offset
1247        assert_eq!(words[5], 2); // slot 2 stride
1248        assert_eq!(words[6], 0); // slot 3 offset
1249        assert_eq!(words[7], 0); // slot 3 stride
1250        // Slot 0 data follows the prefix.
1251        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        // Slot 2 data follows slot 0.
1256        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        // 4 vertices, stride 1 word each = 16 bytes
1274        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        // 4 vertices, stride 2 words each = 32 bytes
1279        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        // Per-mesh slot 1, then per-instance slot 0.
1311        s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1312        s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1313
1314        // flag_bits reports the union of per-mesh and per-instance.
1315        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        // The instance bind group differs from the mesh bind group.
1320        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        // A draw of an instance with no per-instance data falls back to the
1325        // mesh bind group.
1326        let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1327        assert_eq!(fallback, mesh_bg);
1328
1329        // Detaching the instance slot drops the instance entry and clears
1330        // the union bit; per-mesh slot 1 remains.
1331        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    /// Registering a deformer that actually reads from `deform_data` must
1350    /// produce a rebuilt LDR `mesh.wgsl` pipeline family. The simplest
1351    /// proof: if the composed source were broken, `register_deformer`
1352    /// would fail at validation; if the rebuild path were broken (e.g.
1353    /// shader module created from stale source), this test would still
1354    /// pass because no draw is issued. So we also re-fetch the LDR
1355    /// pipelines and confirm they are not the originals that the renderer
1356    /// was constructed with.
1357    #[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        // The fields themselves moved during the swap, so the addresses
1385        // stay the same. Instead, confirm that `solid_pipeline` and
1386        // `wireframe_pipeline` are still live wgpu handles by hashing
1387        // their global_id, which is unique per device-created pipeline.
1388        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        // Renderer construction already registered the internal skinning
1410        // deformer on a reserved slot, so we record the baseline count and
1411        // expect host slots to start at 0.
1412        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}