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    /// False when the device's max_bind_groups < 3. When false, the deform
150    /// bind group layout is not included in pipeline layouts and deformer
151    /// registration is silently skipped.
152    pub enabled: bool,
153    pub bind_group_layout: wgpu::BindGroupLayout,
154    pub header_buffer: wgpu::Buffer,
155    /// Empty slot-layout prefix bound when a mesh has no attached data.
156    /// `SLOT_LAYOUT_WORDS` u32s of zero. Kept alive so the dummy bind group
157    /// stays valid.
158    #[allow(dead_code)]
159    pub dummy_data_buffer: wgpu::Buffer,
160    /// Empty per-instance buffer reused by every per-mesh bind group that
161    /// has no instance data yet. Kept alive on the state so the bind group
162    /// stays valid.
163    pub dummy_instance_buffer: wgpu::Buffer,
164    /// Bind group used when a mesh has no attached deformer data. Bound by
165    /// every mesh-family draw at slot 2 to satisfy the pipeline layout
166    /// without forcing per-mesh storage allocation.
167    pub dummy_bind_group: wgpu::BindGroup,
168    pub meshes: HashMap<MeshId, MeshDeform>,
169    pub header_cpu: DeformHeader,
170    /// Currently registered deformers, in registration order.
171    pub registrations: Vec<StoredDeformer>,
172}
173
174impl DeformationState {
175    pub fn new(device: &wgpu::Device) -> Self {
176        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
177            label: Some("deform_bgl"),
178            entries: &[
179                wgpu::BindGroupLayoutEntry {
180                    binding: 0,
181                    visibility: wgpu::ShaderStages::VERTEX,
182                    ty: wgpu::BindingType::Buffer {
183                        ty: wgpu::BufferBindingType::Uniform,
184                        has_dynamic_offset: false,
185                        min_binding_size: None,
186                    },
187                    count: None,
188                },
189                wgpu::BindGroupLayoutEntry {
190                    binding: 1,
191                    visibility: wgpu::ShaderStages::VERTEX,
192                    ty: wgpu::BindingType::Buffer {
193                        ty: wgpu::BufferBindingType::Storage { read_only: true },
194                        has_dynamic_offset: false,
195                        min_binding_size: None,
196                    },
197                    count: None,
198                },
199                wgpu::BindGroupLayoutEntry {
200                    binding: 2,
201                    visibility: wgpu::ShaderStages::VERTEX,
202                    ty: wgpu::BindingType::Buffer {
203                        ty: wgpu::BufferBindingType::Storage { read_only: true },
204                        has_dynamic_offset: false,
205                        min_binding_size: None,
206                    },
207                    count: None,
208                },
209            ],
210        });
211
212        let header_cpu = DeformHeader::zeroed();
213        let header_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
214            label: Some("deform_header"),
215            contents: bytemuck::bytes_of(&header_cpu),
216            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
217        });
218
219        let dummy_words = vec![0u32; SLOT_LAYOUT_WORDS];
220        let dummy_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
221            label: Some("deform_dummy_data"),
222            contents: bytemuck::cast_slice(&dummy_words),
223            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
224        });
225        let dummy_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
226            label: Some("deform_dummy_instance_data"),
227            contents: bytemuck::cast_slice(&dummy_words),
228            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
229        });
230
231        let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
232            label: Some("deform_dummy_bg"),
233            layout: &bind_group_layout,
234            entries: &[
235                wgpu::BindGroupEntry {
236                    binding: 0,
237                    resource: header_buffer.as_entire_binding(),
238                },
239                wgpu::BindGroupEntry {
240                    binding: 1,
241                    resource: dummy_data_buffer.as_entire_binding(),
242                },
243                wgpu::BindGroupEntry {
244                    binding: 2,
245                    resource: dummy_instance_buffer.as_entire_binding(),
246                },
247            ],
248        });
249
250        Self {
251            enabled: true,
252            bind_group_layout,
253            header_buffer,
254            dummy_data_buffer,
255            dummy_instance_buffer,
256            dummy_bind_group,
257            meshes: HashMap::new(),
258            header_cpu,
259            registrations: Vec::new(),
260        }
261    }
262
263    /// Construct a disabled `DeformationState` for devices with max_bind_groups < 3.
264    ///
265    /// The returned state still allocates a minimal BGL and dummy bind group so
266    /// that render.rs can call set_bind_group(2, ...) without crashing; those
267    /// calls target a slot not present in any pipeline layout and are ignored.
268    pub fn new_disabled(device: &wgpu::Device) -> Self {
269        let mut s = Self::new(device);
270        s.enabled = false;
271        s
272    }
273
274    /// Returns the bind group to use for a given mesh: the per-mesh group
275    /// when any slot is attached, otherwise the renderer-owned dummy. Used
276    /// at draw time once the registry has rebuilt the mesh pipelines to bind
277    /// group 2.
278    #[allow(dead_code)]
279    pub fn bind_group_for(&self, mesh_id: MeshId) -> &wgpu::BindGroup {
280        self.meshes
281            .get(&mesh_id)
282            .map(|m| &m.bind_group)
283            .unwrap_or(&self.dummy_bind_group)
284    }
285
286    /// `deform_flags` value to write into the `ObjectUniform` for `mesh_id`.
287    /// Folds per-mesh and per-instance slot activity together: any slot
288    /// with data on either side is reported as live so the registered
289    /// body's flag branch executes.
290    pub fn flag_bits(&self, mesh_id: MeshId) -> u32 {
291        self.meshes
292            .get(&mesh_id)
293            .map(|m| m.flag_bits | m.instance_flag_bits_union)
294            .unwrap_or(0)
295    }
296
297    /// Whether `(mesh_id, instance_id)` has any per-instance deformer data
298    /// attached. Used by the renderer to filter items out of GPU-driven
299    /// instanced batching: items with per-instance deform data need their
300    /// own bind group at draw time and cannot share an instance batch.
301    pub(crate) fn has_per_instance_deform_data(
302        &self,
303        mesh_id: MeshId,
304        instance_id: Option<u32>,
305    ) -> bool {
306        let Some(id) = instance_id else {
307            return false;
308        };
309        self.meshes
310            .get(&mesh_id)
311            .and_then(|m| m.instances.get(&id))
312            .is_some_and(|i| i.flag_bits != 0)
313    }
314
315    /// Bind group to use for a draw of `(mesh_id, instance_id)`. Falls back
316    /// to the per-mesh-only bind group when no per-instance data is
317    /// attached, and to the renderer dummy when the mesh has no data at all.
318    #[allow(dead_code)]
319    pub fn instance_bind_group_for(
320        &self,
321        mesh_id: MeshId,
322        instance_id: Option<u32>,
323    ) -> &wgpu::BindGroup {
324        let Some(mesh) = self.meshes.get(&mesh_id) else {
325            return &self.dummy_bind_group;
326        };
327        if let Some(id) = instance_id {
328            if let Some(inst) = mesh.instances.get(&id) {
329                return &inst.bind_group;
330            }
331        }
332        &mesh.bind_group
333    }
334
335    /// Pack the per-slot data of one mesh into a single u32 stream prefixed
336    /// by `(offset, stride)` pairs per slot.
337    fn pack(
338        slot_data: &[Option<Vec<u8>>; DEFORM_SLOT_COUNT],
339        slot_stride: &[u32; DEFORM_SLOT_COUNT],
340    ) -> Vec<u32> {
341        let mut words = vec![0u32; SLOT_LAYOUT_WORDS];
342        for slot in 0..DEFORM_SLOT_COUNT {
343            if let Some(bytes) = &slot_data[slot] {
344                let offset_words = words.len() as u32;
345                words[slot * 2] = offset_words;
346                words[slot * 2 + 1] = slot_stride[slot];
347                let extra = bytes.len() / 4;
348                words.reserve(extra);
349                for chunk in bytes.chunks_exact(4) {
350                    words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
351                }
352            }
353        }
354        words
355    }
356
357    fn make_bind_group(
358        &self,
359        device: &wgpu::Device,
360        label: &str,
361        mesh_buffer: &wgpu::Buffer,
362        instance_buffer: &wgpu::Buffer,
363    ) -> wgpu::BindGroup {
364        device.create_bind_group(&wgpu::BindGroupDescriptor {
365            label: Some(label),
366            layout: &self.bind_group_layout,
367            entries: &[
368                wgpu::BindGroupEntry {
369                    binding: 0,
370                    resource: self.header_buffer.as_entire_binding(),
371                },
372                wgpu::BindGroupEntry {
373                    binding: 1,
374                    resource: mesh_buffer.as_entire_binding(),
375                },
376                wgpu::BindGroupEntry {
377                    binding: 2,
378                    resource: instance_buffer.as_entire_binding(),
379                },
380            ],
381        })
382    }
383
384    /// Re-pack and re-upload the mesh's data, rebuilding the per-mesh bind
385    /// group and every per-instance bind group (which also binds the
386    /// per-mesh buffer). Drops the mesh entry when no slot has any data
387    /// attached on either side.
388    fn refresh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
389        let Some(m) = self.meshes.get(&mesh_id) else {
390            return;
391        };
392        if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
393            self.meshes.remove(&mesh_id);
394            return;
395        }
396        let words = Self::pack(&m.slot_data, &m.slot_stride);
397        let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
398            label: Some("deform_mesh_data"),
399            contents: bytemuck::cast_slice(&words),
400            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
401        });
402        let new_bg = self.make_bind_group(
403            device,
404            "deform_mesh_bg",
405            &new_buffer,
406            &self.dummy_instance_buffer,
407        );
408
409        // Rebuild instance bind groups since they bind this mesh's buffer.
410        let instance_ids: Vec<u32> = self
411            .meshes
412            .get(&mesh_id)
413            .unwrap()
414            .instances
415            .keys()
416            .copied()
417            .collect();
418        for id in instance_ids {
419            let inst_buf_clone = {
420                let inst = self
421                    .meshes
422                    .get(&mesh_id)
423                    .unwrap()
424                    .instances
425                    .get(&id)
426                    .unwrap();
427                // SAFETY: as_entire_binding holds a borrow of the buffer,
428                // not the buffer itself; we reborrow per call.
429                inst.buffer.clone()
430            };
431            let bg = self.make_bind_group(
432                device,
433                "deform_mesh_instance_bg",
434                &new_buffer,
435                &inst_buf_clone,
436            );
437            let inst = self
438                .meshes
439                .get_mut(&mesh_id)
440                .unwrap()
441                .instances
442                .get_mut(&id)
443                .unwrap();
444            inst.bind_group = bg;
445        }
446
447        let m = self.meshes.get_mut(&mesh_id).unwrap();
448        m.buffer = new_buffer;
449        m.bind_group = new_bg;
450    }
451
452    fn ensure_mesh(&mut self, device: &wgpu::Device, mesh_id: MeshId) {
453        if self.meshes.contains_key(&mesh_id) {
454            return;
455        }
456        let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
457        let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
458            label: Some("deform_mesh_data_init"),
459            contents: bytemuck::cast_slice(&init_words),
460            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
461        });
462        let bind_group = self.make_bind_group(
463            device,
464            "deform_mesh_bg",
465            &buffer,
466            &self.dummy_instance_buffer,
467        );
468        self.meshes.insert(
469            mesh_id,
470            MeshDeform {
471                slot_data: Default::default(),
472                slot_stride: [0; DEFORM_SLOT_COUNT],
473                buffer,
474                bind_group,
475                flag_bits: 0,
476                instance_flag_bits_union: 0,
477                instances: HashMap::new(),
478            },
479        );
480    }
481
482    /// Attach raw bytes to a per-mesh slot for `mesh_id`. `stride_words` is
483    /// the per-vertex stride in u32 words (must equal the registered
484    /// deformer's `per_vertex_stride / 4`).
485    pub fn attach_slot(
486        &mut self,
487        device: &wgpu::Device,
488        mesh_id: MeshId,
489        slot: usize,
490        stride_words: u32,
491        data: &[u8],
492    ) {
493        assert!(slot < DEFORM_SLOT_COUNT);
494        assert!(
495            data.len() % 4 == 0,
496            "deform slot data length must be a multiple of 4 bytes"
497        );
498        self.ensure_mesh(device, mesh_id);
499        let entry = self.meshes.get_mut(&mesh_id).unwrap();
500        entry.slot_data[slot] = Some(data.to_vec());
501        entry.slot_stride[slot] = stride_words;
502        entry.flag_bits |= 1u32 << slot;
503        self.refresh(device, mesh_id);
504    }
505
506    /// Detach a per-mesh slot. Returns `true` if any data was removed.
507    pub fn detach_slot(&mut self, device: &wgpu::Device, mesh_id: MeshId, slot: usize) -> bool {
508        assert!(slot < DEFORM_SLOT_COUNT);
509        let Some(m) = self.meshes.get_mut(&mesh_id) else {
510            return false;
511        };
512        let had = m.slot_data[slot].take().is_some();
513        if had {
514            m.slot_stride[slot] = 0;
515            m.flag_bits &= !(1u32 << slot);
516            self.refresh(device, mesh_id);
517        }
518        had
519    }
520
521    pub fn has_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
522        self.meshes
523            .get(&mesh_id)
524            .map(|m| m.slot_data[slot].is_some())
525            .unwrap_or(false)
526    }
527
528    fn recompute_instance_union(m: &mut MeshDeform) {
529        let mut union = 0u32;
530        for inst in m.instances.values() {
531            union |= inst.flag_bits;
532        }
533        m.instance_flag_bits_union = union;
534    }
535
536    /// Attach raw bytes to a per-(mesh, instance) slot. The data is treated
537    /// as a sequence of fixed-stride elements (e.g. mat4 joint matrices
538    /// for skinning). When the instance buffer must grow, its bind group is
539    /// rebuilt. When the existing buffer is large enough, the data is
540    /// written with `queue.write_buffer` and the bind group is reused.
541    pub fn attach_slot_instance(
542        &mut self,
543        device: &wgpu::Device,
544        queue: &wgpu::Queue,
545        mesh_id: MeshId,
546        instance_id: u32,
547        slot: usize,
548        stride_words: u32,
549        data: &[u8],
550    ) {
551        assert!(slot < DEFORM_SLOT_COUNT);
552        assert!(
553            data.len() % 4 == 0,
554            "deform slot data length must be a multiple of 4 bytes"
555        );
556        self.ensure_mesh(device, mesh_id);
557        // Ensure the instance entry exists with a minimal buffer.
558        let needs_insert = !self
559            .meshes
560            .get(&mesh_id)
561            .unwrap()
562            .instances
563            .contains_key(&instance_id);
564        if needs_insert {
565            let init_words = vec![0u32; SLOT_LAYOUT_WORDS];
566            let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
567                label: Some("deform_mesh_instance_data_init"),
568                contents: bytemuck::cast_slice(&init_words),
569                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
570            });
571            let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
572            let bind_group =
573                self.make_bind_group(device, "deform_mesh_instance_bg", &mesh_buf_clone, &buffer);
574            let m = self.meshes.get_mut(&mesh_id).unwrap();
575            let cap = (init_words.len() * 4) as u64;
576            m.instances.insert(
577                instance_id,
578                InstanceDeform {
579                    slot_data: Default::default(),
580                    slot_stride: [0; DEFORM_SLOT_COUNT],
581                    buffer,
582                    buffer_capacity: cap,
583                    bind_group,
584                    flag_bits: 0,
585                },
586            );
587        }
588
589        // Mutate slot bookkeeping.
590        {
591            let inst = self
592                .meshes
593                .get_mut(&mesh_id)
594                .unwrap()
595                .instances
596                .get_mut(&instance_id)
597                .unwrap();
598            inst.slot_data[slot] = Some(data.to_vec());
599            inst.slot_stride[slot] = stride_words;
600            inst.flag_bits |= 1u32 << slot;
601        }
602
603        // Re-pack and decide between in-place write or realloc.
604        let (packed_words, packed_bytes_len) = {
605            let inst = self
606                .meshes
607                .get(&mesh_id)
608                .unwrap()
609                .instances
610                .get(&instance_id)
611                .unwrap();
612            let w = Self::pack(&inst.slot_data, &inst.slot_stride);
613            let len = (w.len() * 4) as u64;
614            (w, len)
615        };
616
617        let needs_realloc = {
618            let inst = self
619                .meshes
620                .get(&mesh_id)
621                .unwrap()
622                .instances
623                .get(&instance_id)
624                .unwrap();
625            packed_bytes_len > inst.buffer_capacity
626        };
627
628        if needs_realloc {
629            let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
630                label: Some("deform_mesh_instance_data"),
631                contents: bytemuck::cast_slice(&packed_words),
632                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
633            });
634            let mesh_buf_clone = self.meshes.get(&mesh_id).unwrap().buffer.clone();
635            let bg = self.make_bind_group(
636                device,
637                "deform_mesh_instance_bg",
638                &mesh_buf_clone,
639                &new_buffer,
640            );
641            let inst = self
642                .meshes
643                .get_mut(&mesh_id)
644                .unwrap()
645                .instances
646                .get_mut(&instance_id)
647                .unwrap();
648            inst.buffer = new_buffer;
649            inst.buffer_capacity = packed_bytes_len;
650            inst.bind_group = bg;
651        } else {
652            let inst = self
653                .meshes
654                .get(&mesh_id)
655                .unwrap()
656                .instances
657                .get(&instance_id)
658                .unwrap();
659            queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
660        }
661
662        // Recompute the union and refresh flag bits.
663        let m = self.meshes.get_mut(&mesh_id).unwrap();
664        Self::recompute_instance_union(m);
665    }
666
667    /// Detach a per-instance slot. Returns `true` if any data was removed.
668    /// Drops the instance entry when no slot remains attached and re-packs
669    /// the instance buffer if other slots still hold data.
670    pub fn detach_slot_instance(
671        &mut self,
672        device: &wgpu::Device,
673        queue: &wgpu::Queue,
674        mesh_id: MeshId,
675        instance_id: u32,
676        slot: usize,
677    ) -> bool {
678        assert!(slot < DEFORM_SLOT_COUNT);
679        let Some(m) = self.meshes.get_mut(&mesh_id) else {
680            return false;
681        };
682        let Some(inst) = m.instances.get_mut(&instance_id) else {
683            return false;
684        };
685        let had = inst.slot_data[slot].take().is_some();
686        if !had {
687            return false;
688        }
689        inst.slot_stride[slot] = 0;
690        inst.flag_bits &= !(1u32 << slot);
691        let drop_instance = inst.flag_bits == 0;
692
693        if drop_instance {
694            m.instances.remove(&instance_id);
695        } else {
696            let (packed_words, packed_bytes_len) = {
697                let inst = m.instances.get(&instance_id).unwrap();
698                let w = Self::pack(&inst.slot_data, &inst.slot_stride);
699                let len = (w.len() * 4) as u64;
700                (w, len)
701            };
702            let inst = m.instances.get(&instance_id).unwrap();
703            if packed_bytes_len > inst.buffer_capacity {
704                let new_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
705                    label: Some("deform_mesh_instance_data"),
706                    contents: bytemuck::cast_slice(&packed_words),
707                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
708                });
709                let mesh_buf_clone = m.buffer.clone();
710                let bg = self.make_bind_group(
711                    device,
712                    "deform_mesh_instance_bg",
713                    &mesh_buf_clone,
714                    &new_buffer,
715                );
716                let m = self.meshes.get_mut(&mesh_id).unwrap();
717                let inst = m.instances.get_mut(&instance_id).unwrap();
718                inst.buffer = new_buffer;
719                inst.buffer_capacity = packed_bytes_len;
720                inst.bind_group = bg;
721            } else {
722                queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&packed_words));
723            }
724        }
725
726        let m = self.meshes.get_mut(&mesh_id).unwrap();
727        Self::recompute_instance_union(m);
728        // Drop the mesh entirely when nothing references it.
729        if m.flag_bits == 0 && m.instance_flag_bits_union == 0 {
730            self.meshes.remove(&mesh_id);
731        }
732        true
733    }
734
735    pub fn has_slot_instance(&self, mesh_id: MeshId, instance_id: u32, slot: usize) -> bool {
736        self.meshes
737            .get(&mesh_id)
738            .and_then(|m| m.instances.get(&instance_id))
739            .map(|i| i.slot_data[slot].is_some())
740            .unwrap_or(false)
741    }
742}
743
744// ---------------------------------------------------------------------------
745// Public API on ViewportGpuResources
746// ---------------------------------------------------------------------------
747
748impl ViewportGpuResources {
749    /// Write the shared header uniform's `time_seconds` field. Cheap; safe to
750    /// call per frame.
751    pub fn set_deform_time(&mut self, queue: &wgpu::Queue, time_seconds: f32) {
752        self.deform.header_cpu.time_seconds = time_seconds;
753        queue.write_buffer(
754            &self.deform.header_buffer,
755            0,
756            bytemuck::bytes_of(&self.deform.header_cpu),
757        );
758    }
759
760    /// Write the four `vec4<f32>` parameter words for one slot in the shared
761    /// header uniform.
762    pub fn set_deform_slot_params(
763        &mut self,
764        queue: &wgpu::Queue,
765        slot: usize,
766        params: [[f32; 4]; DEFORM_PARAMS_PER_SLOT],
767    ) {
768        assert!(slot < DEFORM_SLOT_COUNT);
769        let base = slot * DEFORM_PARAMS_PER_SLOT;
770        for (i, p) in params.iter().enumerate() {
771            self.deform.header_cpu.slot_params[base + i] = *p;
772        }
773        queue.write_buffer(
774            &self.deform.header_buffer,
775            0,
776            bytemuck::bytes_of(&self.deform.header_cpu),
777        );
778    }
779
780    /// Attach raw bytes for one deformer slot on the given mesh.
781    ///
782    /// `stride_bytes` is the per-vertex byte stride and must equal the
783    /// registered deformer's `per_vertex_stride`. The data length is
784    /// expected to be `vertex_count * stride_bytes`; the renderer does not
785    /// validate the vertex count, only that the byte length is a multiple
786    /// of `stride_bytes` and 4.
787    pub fn attach_deform_slot(
788        &mut self,
789        device: &wgpu::Device,
790        mesh_id: MeshId,
791        slot: usize,
792        stride_bytes: u32,
793        data: &[u8],
794    ) {
795        assert!(
796            stride_bytes >= 4 && stride_bytes % 4 == 0,
797            "deform slot stride must be a positive multiple of 4 bytes"
798        );
799        self.deform
800            .attach_slot(device, mesh_id, slot, stride_bytes / 4, data);
801    }
802
803    /// Detach a per-mesh slot's data. Returns `true` if any data was removed.
804    pub fn detach_deform_slot(
805        &mut self,
806        device: &wgpu::Device,
807        mesh_id: MeshId,
808        slot: usize,
809    ) -> bool {
810        self.deform.detach_slot(device, mesh_id, slot)
811    }
812
813    /// Returns `true` when the mesh has per-mesh data attached at the given
814    /// slot.
815    pub fn has_deform_slot(&self, mesh_id: MeshId, slot: usize) -> bool {
816        self.deform.has_slot(mesh_id, slot)
817    }
818
819    /// Attach raw bytes for one deformer slot on a single instance of the
820    /// given mesh. Used for data that varies per instance (e.g. joint
821    /// palettes). `stride_bytes` is the per-element byte stride; the data
822    /// length must be a multiple of `stride_bytes` and 4.
823    pub fn attach_deform_slot_instance(
824        &mut self,
825        device: &wgpu::Device,
826        queue: &wgpu::Queue,
827        mesh_id: MeshId,
828        instance_id: u32,
829        slot: usize,
830        stride_bytes: u32,
831        data: &[u8],
832    ) {
833        assert!(
834            stride_bytes >= 4 && stride_bytes % 4 == 0,
835            "deform slot stride must be a positive multiple of 4 bytes"
836        );
837        self.deform.attach_slot_instance(
838            device,
839            queue,
840            mesh_id,
841            instance_id,
842            slot,
843            stride_bytes / 4,
844            data,
845        );
846    }
847
848    /// Detach a per-instance slot's data. Returns `true` if any data was
849    /// removed.
850    pub fn detach_deform_slot_instance(
851        &mut self,
852        device: &wgpu::Device,
853        queue: &wgpu::Queue,
854        mesh_id: MeshId,
855        instance_id: u32,
856        slot: usize,
857    ) -> bool {
858        self.deform
859            .detach_slot_instance(device, queue, mesh_id, instance_id, slot)
860    }
861
862    /// Returns `true` when the given instance of the mesh has per-instance
863    /// data attached at the given slot.
864    pub fn has_deform_slot_instance(&self, mesh_id: MeshId, instance_id: u32, slot: usize) -> bool {
865        self.deform.has_slot_instance(mesh_id, instance_id, slot)
866    }
867
868    /// Register a deformer against the mesh shader family.
869    ///
870    /// Validates the descriptor's name and allocates a slot, composes every
871    /// mesh-family base shader with the new deformer plus all previously
872    /// registered ones, and runs each composed module through wgpu's
873    /// validator. On success, the LDR and HDR `mesh.wgsl` pipelines are
874    /// rebuilt from the freshly composed source so subsequent draws run
875    /// the registered body. Other mesh-family pipelines (instanced,
876    /// shadow, outline mask, OIT) continue to run the identity path until
877    /// their factories migrate to the same rebuild path.
878    ///
879    /// On any validation failure the registration is rolled back: the
880    /// previously composed sources stay live and the returned error names
881    /// the shader that failed.
882    ///
883    /// # Errors
884    ///
885    /// - [`ViewportError::DeformNameTaken`] when `desc.name` is already
886    ///   registered.
887    /// - [`ViewportError::DeformShaderInvalid`] when `desc.name` is not a
888    ///   valid WGSL identifier, or when any composed module fails
889    ///   validation.
890    /// - [`ViewportError::DeformSlotsExhausted`] when all
891    ///   `DEFORM_SLOT_COUNT` slots are in use.
892    ///
893    /// [`ViewportError::DeformNameTaken`]: crate::error::ViewportError::DeformNameTaken
894    /// [`ViewportError::DeformShaderInvalid`]: crate::error::ViewportError::DeformShaderInvalid
895    /// [`ViewportError::DeformSlotsExhausted`]: crate::error::ViewportError::DeformSlotsExhausted
896    pub fn register_deformer(
897        &mut self,
898        device: &wgpu::Device,
899        desc: DeformerDesc,
900    ) -> ViewportResult<DeformerId> {
901        validate_name(&self.deform.registrations, desc.name)?;
902        let slot = allocate_slot(&self.deform.registrations)?;
903
904        let candidate = StoredDeformer { desc, slot };
905        let mut trial = self.deform.registrations.clone();
906        trial.push(candidate.clone());
907
908        for shader_name in MESH_FAMILY_SHADERS {
909            let Some(base) = lookup_source(shader_name) else {
910                return Err(crate::error::ViewportError::DeformShaderInvalid {
911                    reason: format!("internal: shader '{shader_name}' missing from shader catalog"),
912                });
913            };
914            let composed = compose_shader(base, &trial);
915            let label = format!("deform_compose_{shader_name}");
916            validate_with_wgpu(device, &label, &composed)?;
917        }
918
919        self.deform.registrations.push(candidate);
920        self.rebuild_mesh_pipelines(device);
921        Ok(DeformerId(slot))
922    }
923
924    /// Register a deformer on a reserved internal slot
925    /// (`[DEFORM_INTERNAL_SLOT_START, DEFORM_SLOT_COUNT)`). Used at
926    /// renderer construction for the in-crate deformers (skinning,
927    /// displacement) so they do not consume one of the four slots exposed
928    /// to hosts. Validation, composition, and pipeline rebuild work the
929    /// same as [`Self::register_deformer`].
930    #[allow(dead_code)]
931    pub(crate) fn register_internal_deformer(
932        &mut self,
933        device: &wgpu::Device,
934        desc: DeformerDesc,
935    ) -> ViewportResult<DeformerId> {
936        validate_name(&self.deform.registrations, desc.name)?;
937        let slot = allocate_internal_slot(&self.deform.registrations)?;
938
939        let candidate = StoredDeformer { desc, slot };
940        let mut trial = self.deform.registrations.clone();
941        trial.push(candidate.clone());
942
943        for shader_name in MESH_FAMILY_SHADERS {
944            let Some(base) = lookup_source(shader_name) else {
945                return Err(crate::error::ViewportError::DeformShaderInvalid {
946                    reason: format!("internal: shader '{shader_name}' missing from shader catalog"),
947                });
948            };
949            let composed = compose_shader(base, &trial);
950            let label = format!("deform_compose_{shader_name}");
951            validate_with_wgpu(device, &label, &composed)?;
952        }
953
954        self.deform.registrations.push(candidate);
955        self.rebuild_mesh_pipelines(device);
956        Ok(DeformerId(slot))
957    }
958
959    /// Number of currently registered deformers (host + internal).
960    pub fn registered_deformer_count(&self) -> usize {
961        self.deform.registrations.len()
962    }
963
964    /// Look up a registered deformer's id by its name. Returns `None` when
965    /// no deformer with that name has been registered.
966    pub fn deformer_id_by_name(&self, name: &str) -> Option<DeformerId> {
967        self.deform
968            .registrations
969            .iter()
970            .find(|r| r.desc.name == name)
971            .map(|r| DeformerId(r.slot))
972    }
973
974    /// Build a [`DeformSlotHandle`] for the given deformer. Plugins stash
975    /// the handle at registration time and use it to write that slot's
976    /// `slot_params` block from contexts (e.g. a `GpuPlugin::pre_prepare`)
977    /// that only carry `&wgpu::Queue`.
978    pub fn deform_slot_handle(&self, id: DeformerId) -> DeformSlotHandle {
979        DeformSlotHandle {
980            header_buffer: self.deform.header_buffer.clone(),
981            slot: id.slot(),
982        }
983    }
984
985    /// Re-compose every mesh-family shader and rebuild the pipelines that
986    /// draw from it. Called by `register_deformer` once a new registration
987    /// has validated; safe to call between frames with zero registrations
988    /// to reset to the identity shader. The instanced and instanced-OIT
989    /// pipelines stay on their build-time shader modules until their
990    /// factories migrate to the same rebuild flow.
991    fn rebuild_mesh_pipelines(&mut self, device: &wgpu::Device) {
992        if !self.deform.enabled {
993            return;
994        }
995        let registrations = self.deform.registrations.clone();
996
997        // mesh.wgsl: LDR + HDR families.
998        if let Some(base) = lookup_source("mesh.wgsl") {
999            let composed = compose_shader(base, &registrations);
1000            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1001                label: Some("mesh_shader_composed"),
1002                source: wgpu::ShaderSource::Wgsl(composed.into()),
1003            });
1004
1005            let ldr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1006                device,
1007                "mesh_pipeline_layout",
1008                &self.camera_bind_group_layout,
1009                &self.object_bind_group_layout,
1010                Some(&self.deform.bind_group_layout),
1011            );
1012            let ldr = crate::resources::mesh_pipelines::build_ldr_mesh_pipelines(
1013                device,
1014                &ldr_layout,
1015                &shader,
1016                self.target_format,
1017                self.sample_count,
1018            );
1019            self.solid_pipeline = ldr.solid;
1020            self.solid_two_sided_pipeline = ldr.solid_two_sided;
1021            self.transparent_pipeline = ldr.transparent;
1022            self.wireframe_pipeline = ldr.wireframe;
1023
1024            if self.hdr_solid_pipeline.is_some() {
1025                let hdr_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1026                    device,
1027                    "hdr_mesh_pipeline_layout",
1028                    &self.camera_bind_group_layout,
1029                    &self.object_bind_group_layout,
1030                    Some(&self.deform.bind_group_layout),
1031                );
1032                let hdr = crate::resources::mesh_pipelines::build_hdr_mesh_pipelines(
1033                    device,
1034                    &hdr_layout,
1035                    &shader,
1036                );
1037                self.hdr_solid_pipeline = Some(hdr.solid);
1038                self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1039                self.hdr_transparent_pipeline = Some(hdr.transparent);
1040                self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1041            }
1042        }
1043
1044        // mesh_oit.wgsl: only present after ensure_hdr_shared has been
1045        // called.
1046        if self.oit_pipeline.is_some() {
1047            if let Some(base) = lookup_source("mesh_oit.wgsl") {
1048                let composed = compose_shader(base, &registrations);
1049                let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1050                    label: Some("mesh_oit_shader_composed"),
1051                    source: wgpu::ShaderSource::Wgsl(composed.into()),
1052                });
1053                let oit_layout = crate::resources::mesh_pipelines::mesh_pipeline_layout(
1054                    device,
1055                    "oit_pipeline_layout",
1056                    &self.camera_bind_group_layout,
1057                    &self.object_bind_group_layout,
1058                    Some(&self.deform.bind_group_layout),
1059                );
1060                let oit = crate::resources::mesh_pipelines::build_oit_pipeline(
1061                    device,
1062                    &oit_layout,
1063                    &shader,
1064                );
1065                self.oit_pipeline = Some(oit);
1066            }
1067        }
1068
1069        // shadow.wgsl: depth-only cascade pass.
1070        if let Some(base) = lookup_source("shadow.wgsl") {
1071            let composed = compose_shader(base, &registrations);
1072            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1073                label: Some("shadow_shader_composed"),
1074                source: wgpu::ShaderSource::Wgsl(composed.into()),
1075            });
1076            let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1077                label: Some("shadow_pipeline_layout"),
1078                bind_group_layouts: &[
1079                    &self.shadow_camera_bind_group_layout,
1080                    &self.object_bind_group_layout,
1081                    &self.deform.bind_group_layout,
1082                ],
1083                push_constant_ranges: &[],
1084            });
1085            self.shadow_pipeline = crate::resources::mesh_pipelines::build_shadow_pipeline(
1086                device,
1087                &layout,
1088                &shader,
1089                Some(wgpu::Face::Front),
1090            );
1091        }
1092
1093        // outline_mask.wgsl: mask-write pass for the selection silhouette.
1094        if let Some(base) = lookup_source("outline_mask.wgsl") {
1095            let composed = compose_shader(base, &registrations);
1096            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1097                label: Some("outline_mask_shader_composed"),
1098                source: wgpu::ShaderSource::Wgsl(composed.into()),
1099            });
1100            let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1101                label: Some("outline_pipeline_layout"),
1102                bind_group_layouts: &[
1103                    &self.camera_bind_group_layout,
1104                    &self.outline_bind_group_layout,
1105                    &self.deform.bind_group_layout,
1106                ],
1107                push_constant_ranges: &[],
1108            });
1109            let masks = crate::resources::mesh_pipelines::build_outline_mask_pipelines(
1110                device,
1111                &layout,
1112                &shader,
1113                wgpu::TextureFormat::R8Unorm,
1114            );
1115            self.outline_mask_pipeline = masks.mask;
1116            self.outline_mask_two_sided_pipeline = masks.mask_two_sided;
1117        }
1118
1119        // mesh_instanced.wgsl: LDR (solid + transparent), HDR (solid +
1120        // transparent + additive + premultiplied), and HDR cull. Only
1121        // present after `ensure_instanced_pipelines` / its HDR sibling /
1122        // `ensure_cull_instance_pipelines` have run.
1123        if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1124            let composed = compose_shader(base, &registrations);
1125            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1126                label: Some("mesh_instanced_shader_composed"),
1127                source: wgpu::ShaderSource::Wgsl(composed.into()),
1128            });
1129
1130            if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1131                if self.solid_instanced_pipeline.is_some() {
1132                    let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1133                        device,
1134                        "instanced_pipeline_layout",
1135                        &self.camera_bind_group_layout,
1136                        instance_bgl,
1137                        Some(&self.deform.bind_group_layout),
1138                    );
1139                    let ldr = crate::resources::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1140                        device,
1141                        &layout,
1142                        &shader,
1143                        self.target_format,
1144                        self.sample_count,
1145                    );
1146                    self.solid_instanced_pipeline = Some(ldr.solid);
1147                    self.transparent_instanced_pipeline = Some(ldr.transparent);
1148                }
1149                if self.hdr_solid_instanced_pipeline.is_some() {
1150                    let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1151                        device,
1152                        "hdr_instanced_pipeline_layout",
1153                        &self.camera_bind_group_layout,
1154                        instance_bgl,
1155                        Some(&self.deform.bind_group_layout),
1156                    );
1157                    let hdr = crate::resources::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1158                        device, &layout, &shader,
1159                    );
1160                    self.hdr_solid_instanced_pipeline = Some(hdr.solid);
1161                    self.hdr_transparent_instanced_pipeline = Some(hdr.transparent);
1162                    self.hdr_instanced_additive_pipeline = Some(hdr.additive);
1163                    self.hdr_instanced_premultiplied_pipeline = Some(hdr.premultiplied);
1164                }
1165            }
1166            if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1167                if self.hdr_solid_instanced_cull_pipeline.is_some() {
1168                    let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1169                        device,
1170                        "hdr_instanced_cull_pipeline_layout",
1171                        &self.camera_bind_group_layout,
1172                        cull_bgl,
1173                        Some(&self.deform.bind_group_layout),
1174                    );
1175                    let pl = crate::resources::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1176                        device, &layout, &shader,
1177                    );
1178                    self.hdr_solid_instanced_cull_pipeline = Some(pl);
1179                }
1180            }
1181        }
1182
1183        // mesh_instanced_oit.wgsl: non-cull and cull OIT pipelines.
1184        if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1185            let composed = compose_shader(base, &registrations);
1186            let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1187                label: Some("mesh_instanced_oit_shader_composed"),
1188                source: wgpu::ShaderSource::Wgsl(composed.into()),
1189            });
1190            if let Some(instance_bgl) = self.instance_bind_group_layout.as_ref() {
1191                if self.oit_instanced_pipeline.is_some() {
1192                    let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1193                        device,
1194                        "oit_instanced_pipeline_layout",
1195                        &self.camera_bind_group_layout,
1196                        instance_bgl,
1197                        Some(&self.deform.bind_group_layout),
1198                    );
1199                    let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1200                        device,
1201                        &layout,
1202                        &shader,
1203                        "oit_instanced_pipeline",
1204                        "vs_main",
1205                    );
1206                    self.oit_instanced_pipeline = Some(pl);
1207                }
1208            }
1209            if let Some(cull_bgl) = self.instance_cull_bind_group_layout.as_ref() {
1210                if self.oit_instanced_cull_pipeline.is_some() {
1211                    let layout = crate::resources::mesh_pipelines::instanced_pipeline_layout(
1212                        device,
1213                        "oit_instanced_cull_pipeline_layout",
1214                        &self.camera_bind_group_layout,
1215                        cull_bgl,
1216                        Some(&self.deform.bind_group_layout),
1217                    );
1218                    let pl = crate::resources::mesh_pipelines::build_oit_instanced_pipeline(
1219                        device,
1220                        &layout,
1221                        &shader,
1222                        "oit_instanced_cull_pipeline",
1223                        "vs_main_cull",
1224                    );
1225                    self.oit_instanced_cull_pipeline = Some(pl);
1226                }
1227            }
1228        }
1229    }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    use super::*;
1235
1236    fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1237        let instance = wgpu::Instance::default();
1238        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1239            power_preference: wgpu::PowerPreference::default(),
1240            force_fallback_adapter: false,
1241            compatible_surface: None,
1242        }))
1243        .ok()?;
1244        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1245            label: Some("deform_tests"),
1246            ..Default::default()
1247        }))
1248        .ok()?;
1249        Some((device, queue))
1250    }
1251
1252    #[test]
1253    fn pack_lays_out_offsets_after_slot_layout_prefix() {
1254        let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1255        let mut stride = [0u32; DEFORM_SLOT_COUNT];
1256        // Slot 0: 3 vertices, stride 1 u32 each = 12 bytes
1257        data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1258        stride[0] = 1;
1259        // Slot 2: 2 vertices, stride 2 u32 each = 16 bytes
1260        data[2] = Some(vec![10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0]);
1261        stride[2] = 2;
1262
1263        let words = DeformationState::pack(&data, &stride);
1264        // Layout prefix holds an (offset, stride) pair per slot.
1265        assert_eq!(words[0], SLOT_LAYOUT_WORDS as u32); // slot 0 offset
1266        assert_eq!(words[1], 1); // slot 0 stride
1267        assert_eq!(words[2], 0); // slot 1 offset (unused)
1268        assert_eq!(words[3], 0); // slot 1 stride
1269        assert_eq!(words[4], (SLOT_LAYOUT_WORDS + 3) as u32); // slot 2 offset
1270        assert_eq!(words[5], 2); // slot 2 stride
1271        assert_eq!(words[6], 0); // slot 3 offset
1272        assert_eq!(words[7], 0); // slot 3 stride
1273        // Slot 0 data follows the prefix.
1274        let slot0_base = SLOT_LAYOUT_WORDS;
1275        assert_eq!(words[slot0_base], 1);
1276        assert_eq!(words[slot0_base + 1], 2);
1277        assert_eq!(words[slot0_base + 2], 3);
1278        // Slot 2 data follows slot 0.
1279        let slot2_base = slot0_base + 3;
1280        assert_eq!(words[slot2_base], 10);
1281        assert_eq!(words[slot2_base + 1], 20);
1282        assert_eq!(words[slot2_base + 2], 30);
1283        assert_eq!(words[slot2_base + 3], 40);
1284    }
1285
1286    #[test]
1287    fn attach_marks_flag_bit_and_swaps_bind_group() {
1288        let Some((device, _queue)) = headless() else {
1289            return;
1290        };
1291        let mut s = DeformationState::new(&device);
1292        let mesh = MeshId(7);
1293        assert_eq!(s.flag_bits(mesh), 0);
1294        assert!(!s.has_slot(mesh, 0));
1295
1296        // 4 vertices, stride 1 word each = 16 bytes
1297        s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1298        assert!(s.has_slot(mesh, 0));
1299        assert_eq!(s.flag_bits(mesh), 0b0001);
1300
1301        // 4 vertices, stride 2 words each = 32 bytes
1302        s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1303        assert!(s.has_slot(mesh, 2));
1304        assert_eq!(s.flag_bits(mesh), 0b0101);
1305    }
1306
1307    #[test]
1308    fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1309        let Some((device, _queue)) = headless() else {
1310            return;
1311        };
1312        let mut s = DeformationState::new(&device);
1313        let mesh = MeshId(11);
1314        s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1315        assert_eq!(s.flag_bits(mesh), 0b0010);
1316
1317        assert!(s.detach_slot(&device, mesh, 1));
1318        assert_eq!(s.flag_bits(mesh), 0);
1319        assert!(!s.meshes.contains_key(&mesh));
1320        assert!(!s.detach_slot(&device, mesh, 1));
1321    }
1322
1323    #[test]
1324    fn attach_slot_instance_marks_union_and_routes_bind_group() {
1325        let Some((device, queue)) = headless() else {
1326            return;
1327        };
1328        let mut s = DeformationState::new(&device);
1329        let mesh = MeshId(42);
1330        let instance = 3u32;
1331        assert_eq!(s.flag_bits(mesh), 0);
1332
1333        // Per-mesh slot 1, then per-instance slot 0.
1334        s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1335        s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1336
1337        // flag_bits reports the union of per-mesh and per-instance.
1338        assert_eq!(s.flag_bits(mesh), 0b0011);
1339        assert!(s.has_slot_instance(mesh, instance, 0));
1340        assert!(!s.has_slot_instance(mesh, instance, 1));
1341
1342        // The instance bind group differs from the mesh bind group.
1343        let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1344        let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1345        assert_ne!(inst_bg, mesh_bg);
1346
1347        // A draw of an instance with no per-instance data falls back to the
1348        // mesh bind group.
1349        let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1350        assert_eq!(fallback, mesh_bg);
1351
1352        // Detaching the instance slot drops the instance entry and clears
1353        // the union bit; per-mesh slot 1 remains.
1354        assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1355        assert_eq!(s.flag_bits(mesh), 0b0010);
1356        assert!(!s.has_slot_instance(mesh, instance, 0));
1357    }
1358
1359    #[test]
1360    fn slot_index_assert_traps_out_of_range() {
1361        let Some((device, _queue)) = headless() else {
1362            return;
1363        };
1364        let mut s = DeformationState::new(&device);
1365        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1366            s.attach_slot(&device, MeshId(0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1367        }));
1368        assert!(result.is_err());
1369    }
1370
1371    /// Registering a deformer that actually reads from `deform_data` must
1372    /// produce a rebuilt LDR `mesh.wgsl` pipeline family. The simplest
1373    /// proof: if the composed source were broken, `register_deformer`
1374    /// would fail at validation; if the rebuild path were broken (e.g.
1375    /// shader module created from stale source), this test would still
1376    /// pass because no draw is issued. So we also re-fetch the LDR
1377    /// pipelines and confirm they are not the originals that the renderer
1378    /// was constructed with.
1379    #[test]
1380    fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1381        use crate::renderer::ViewportRenderer;
1382        let Some((device, _queue)) = headless() else {
1383            return;
1384        };
1385        let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1386
1387        let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1388        let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1389
1390        let body = "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n    var o = v;\n    if (deform_slot_stride(0u) > 0u) {\n        o.position.z = o.position.z + deform_read_f32(0u, v.vertex_index, 0u);\n    }\n    return o;\n}\n";
1391        let desc = DeformerDesc {
1392            name: "wave",
1393            stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1394            priority: 0,
1395            wgsl_body: body.to_string(),
1396            per_vertex_stride: 4,
1397        };
1398        let id = renderer
1399            .resources_mut()
1400            .register_deformer(&device, desc)
1401            .expect("register");
1402        assert_eq!(id.slot(), 0);
1403
1404        let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1405        let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1406        // The fields themselves moved during the swap, so the addresses
1407        // stay the same. Instead, confirm that `solid_pipeline` and
1408        // `wireframe_pipeline` are still live wgpu handles by hashing
1409        // their global_id, which is unique per device-created pipeline.
1410        assert_ne!(solid_before, std::ptr::null());
1411        assert_ne!(solid_after, std::ptr::null());
1412        assert_ne!(wf_before, std::ptr::null());
1413        assert_ne!(wf_after, std::ptr::null());
1414    }
1415
1416    #[test]
1417    fn register_deformer_validates_and_assigns_slot() {
1418        use crate::renderer::ViewportRenderer;
1419        let Some((device, _queue)) = headless() else {
1420            return;
1421        };
1422        let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1423        let resources = renderer.resources_mut();
1424        let desc = DeformerDesc {
1425            name: "wind",
1426            stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1427            priority: 0,
1428            wgsl_body: "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n    var o = v;\n    o.position.z = o.position.z + 0.001;\n    return o;\n}\n".to_string(),
1429            per_vertex_stride: 4,
1430        };
1431        // Renderer construction already registered the internal skinning
1432        // deformer on a reserved slot, so we record the baseline count and
1433        // expect host slots to start at 0.
1434        let baseline = resources.registered_deformer_count();
1435        let id = resources
1436            .register_deformer(&device, desc)
1437            .expect("register");
1438        assert_eq!(id.slot(), 0);
1439        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1440
1441        let dup = DeformerDesc {
1442            name: "wind",
1443            stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1444            priority: 0,
1445            wgsl_body:
1446                "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }"
1447                    .to_string(),
1448            per_vertex_stride: 4,
1449        };
1450        let err = resources.register_deformer(&device, dup).unwrap_err();
1451        assert!(matches!(
1452            err,
1453            crate::error::ViewportError::DeformNameTaken { .. }
1454        ));
1455        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1456
1457        let bad = DeformerDesc {
1458            name: "wave",
1459            stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1460            priority: 0,
1461            wgsl_body: "this is not valid wgsl".to_string(),
1462            per_vertex_stride: 4,
1463        };
1464        let err = resources.register_deformer(&device, bad).unwrap_err();
1465        assert!(matches!(
1466            err,
1467            crate::error::ViewportError::DeformShaderInvalid { .. }
1468        ));
1469        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1470    }
1471}