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::DeviceResources;
17use crate::resources::mesh::mesh_store::MeshId;
18use crate::resources::mesh_sidecar::registry::{
19    DeformerDesc, DeformerId, MESH_FAMILY_SHADERS, StoredDeformer, allocate_internal_slot,
20    allocate_slot, compose_shader, lookup_source, validate_name, validate_with_wgpu,
21};
22
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 DeviceResources
746// ---------------------------------------------------------------------------
747
748impl DeviceResources {
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 =
1001                crate::resources::builders::wgsl_module(device, "mesh_shader_composed", composed);
1002
1003            let ldr_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1004                device,
1005                "mesh_pipeline_layout",
1006                &self.camera_bind_group_layout,
1007                &self.object_bind_group_layout,
1008                Some(&self.deform.bind_group_layout),
1009            );
1010            let ldr = crate::resources::mesh::mesh_pipelines::build_ldr_mesh_pipelines(
1011                device,
1012                &ldr_layout,
1013                &shader,
1014                self.target_format,
1015                self.sample_count,
1016                None,
1017            );
1018            self.solid_pipeline = ldr.solid;
1019            self.solid_two_sided_pipeline = ldr.solid_two_sided;
1020            self.transparent_pipeline = ldr.transparent;
1021            self.wireframe_pipeline = ldr.wireframe;
1022
1023            if self.hdr_solid_pipeline.is_some() {
1024                let hdr_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1025                    device,
1026                    "hdr_mesh_pipeline_layout",
1027                    &self.camera_bind_group_layout,
1028                    &self.object_bind_group_layout,
1029                    Some(&self.deform.bind_group_layout),
1030                );
1031                let hdr = crate::resources::mesh::mesh_pipelines::build_hdr_mesh_pipelines(
1032                    device,
1033                    &hdr_layout,
1034                    &shader,
1035                );
1036                self.hdr_solid_pipeline = Some(hdr.solid);
1037                self.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1038                self.hdr_transparent_pipeline = Some(hdr.transparent);
1039                self.hdr_wireframe_pipeline = Some(hdr.wireframe);
1040            }
1041        }
1042
1043        // mesh_oit.wgsl: only present after ensure_hdr_shared has been
1044        // called.
1045        if self.oit.pipeline.is_some() {
1046            if let Some(base) = lookup_source("mesh_oit.wgsl") {
1047                let composed = compose_shader(base, &registrations);
1048                let shader = crate::resources::builders::wgsl_module(
1049                    device,
1050                    "mesh_oit_shader_composed",
1051                    composed,
1052                );
1053                let oit_layout = crate::resources::mesh::mesh_pipelines::mesh_pipeline_layout(
1054                    device,
1055                    "oit_pipeline_layout",
1056                    &self.camera_bind_group_layout,
1057                    &self.object_bind_group_layout,
1058                    Some(&self.deform.bind_group_layout),
1059                );
1060                let oit = crate::resources::mesh::mesh_pipelines::build_oit_pipeline(
1061                    device,
1062                    &oit_layout,
1063                    &shader,
1064                );
1065                self.oit.pipeline = Some(oit);
1066            }
1067        }
1068
1069        // 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 =
1073                crate::resources::builders::wgsl_module(device, "shadow_shader_composed", composed);
1074            let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1075                label: Some("shadow_pipeline_layout"),
1076                bind_group_layouts: &[
1077                    &self.shadow_camera_bind_group_layout,
1078                    &self.object_bind_group_layout,
1079                    &self.deform.bind_group_layout,
1080                ],
1081                push_constant_ranges: &[],
1082            });
1083            self.shadow_pipeline = crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
1084                device,
1085                &layout,
1086                &shader,
1087                Some(wgpu::Face::Front),
1088                None,
1089            );
1090            self.shadow_pipeline_two_sided =
1091                crate::resources::mesh::mesh_pipelines::build_shadow_pipeline(
1092                    device, &layout, &shader, None, None,
1093                );
1094        }
1095
1096        // outline_mask.wgsl: mask-write pass for the selection silhouette.
1097        if let Some(base) = lookup_source("outline_mask.wgsl") {
1098            let composed = compose_shader(base, &registrations);
1099            let shader = crate::resources::builders::wgsl_module(
1100                device,
1101                "outline_mask_shader_composed",
1102                composed,
1103            );
1104            let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1105                label: Some("outline_pipeline_layout"),
1106                bind_group_layouts: &[
1107                    &self.camera_bind_group_layout,
1108                    &self.outline.bind_group_layout,
1109                    &self.deform.bind_group_layout,
1110                ],
1111                push_constant_ranges: &[],
1112            });
1113            let masks = crate::resources::mesh::mesh_pipelines::build_outline_mask_pipelines(
1114                device,
1115                &layout,
1116                &shader,
1117                wgpu::TextureFormat::R8Unorm,
1118                None,
1119            );
1120            self.outline.mask_pipeline = masks.mask;
1121            self.outline.mask_two_sided_pipeline = masks.mask_two_sided;
1122        }
1123
1124        // mesh_instanced.wgsl: LDR (solid + transparent), HDR (solid +
1125        // transparent + additive + premultiplied), and HDR cull. Only
1126        // present after `ensure_instanced_pipelines` / its HDR sibling /
1127        // `ensure_cull_instance_pipelines` have run.
1128        if let Some(base) = lookup_source("mesh_instanced.wgsl") {
1129            let composed = compose_shader(base, &registrations);
1130            let shader = crate::resources::builders::wgsl_module(
1131                device,
1132                "mesh_instanced_shader_composed",
1133                composed,
1134            );
1135
1136            if let Some(instance_bgl) = self.instancing.bind_group_layout.as_ref() {
1137                if self.instancing.solid_pipeline.is_some() {
1138                    let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1139                        device,
1140                        "instanced_pipeline_layout",
1141                        &self.camera_bind_group_layout,
1142                        instance_bgl,
1143                        Some(&self.deform.bind_group_layout),
1144                    );
1145                    let ldr =
1146                        crate::resources::mesh::mesh_pipelines::build_ldr_instanced_mesh_pipelines(
1147                            device,
1148                            &layout,
1149                            &shader,
1150                            self.target_format,
1151                            self.sample_count,
1152                        );
1153                    self.instancing.solid_pipeline = Some(ldr.solid);
1154                    self.instancing.solid_two_sided_pipeline = Some(ldr.solid_two_sided);
1155                    self.instancing.transparent_pipeline = Some(ldr.transparent);
1156                }
1157                if self.instancing.hdr_solid_pipeline.is_some() {
1158                    let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1159                        device,
1160                        "hdr_instanced_pipeline_layout",
1161                        &self.camera_bind_group_layout,
1162                        instance_bgl,
1163                        Some(&self.deform.bind_group_layout),
1164                    );
1165                    let hdr =
1166                        crate::resources::mesh::mesh_pipelines::build_hdr_instanced_mesh_pipelines(
1167                            device, &layout, &shader,
1168                        );
1169                    self.instancing.hdr_solid_pipeline = Some(hdr.solid);
1170                    self.instancing.hdr_solid_two_sided_pipeline = Some(hdr.solid_two_sided);
1171                    self.instancing.hdr_transparent_pipeline = Some(hdr.transparent);
1172                    self.instancing.hdr_additive_pipeline = Some(hdr.additive);
1173                    self.instancing.hdr_premultiplied_pipeline = Some(hdr.premultiplied);
1174                }
1175            }
1176            if let Some(cull_bgl) = self.cull.bind_group_layout.as_ref() {
1177                if self.cull.hdr_solid_pipeline.is_some() {
1178                    let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1179                        device,
1180                        "hdr_instanced_cull_pipeline_layout",
1181                        &self.camera_bind_group_layout,
1182                        cull_bgl,
1183                        Some(&self.deform.bind_group_layout),
1184                    );
1185                    let pl =
1186                        crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_pipeline(
1187                            device, &layout, &shader,
1188                        );
1189                    self.cull.hdr_solid_pipeline = Some(pl);
1190                    let pl_two_sided =
1191                        crate::resources::mesh::mesh_pipelines::build_hdr_instanced_cull_two_sided_pipeline(
1192                            device, &layout, &shader,
1193                        );
1194                    self.cull.hdr_solid_two_sided_pipeline = Some(pl_two_sided);
1195                }
1196            }
1197        }
1198
1199        // mesh_instanced_oit.wgsl: non-cull and cull OIT pipelines.
1200        if let Some(base) = lookup_source("mesh_instanced_oit.wgsl") {
1201            let composed = compose_shader(base, &registrations);
1202            let shader = crate::resources::builders::wgsl_module(
1203                device,
1204                "mesh_instanced_oit_shader_composed",
1205                composed,
1206            );
1207            if let Some(instance_bgl) = self.instancing.bind_group_layout.as_ref() {
1208                if self.oit.instanced_pipeline.is_some() {
1209                    let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1210                        device,
1211                        "oit_instanced_pipeline_layout",
1212                        &self.camera_bind_group_layout,
1213                        instance_bgl,
1214                        Some(&self.deform.bind_group_layout),
1215                    );
1216                    let pl = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
1217                        device,
1218                        &layout,
1219                        &shader,
1220                        "oit_instanced_pipeline",
1221                        "vs_main",
1222                    );
1223                    self.oit.instanced_pipeline = Some(pl);
1224                }
1225            }
1226            if let Some(cull_bgl) = self.cull.bind_group_layout.as_ref() {
1227                if self.cull.oit_pipeline.is_some() {
1228                    let layout = crate::resources::mesh::mesh_pipelines::instanced_pipeline_layout(
1229                        device,
1230                        "oit_instanced_cull_pipeline_layout",
1231                        &self.camera_bind_group_layout,
1232                        cull_bgl,
1233                        Some(&self.deform.bind_group_layout),
1234                    );
1235                    let pl = crate::resources::mesh::mesh_pipelines::build_oit_instanced_pipeline(
1236                        device,
1237                        &layout,
1238                        &shader,
1239                        "oit_instanced_cull_pipeline",
1240                        "vs_main_cull",
1241                    );
1242                    self.cull.oit_pipeline = Some(pl);
1243                }
1244            }
1245        }
1246    }
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251    use super::*;
1252
1253    fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
1254        let instance = wgpu::Instance::default();
1255        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1256            power_preference: wgpu::PowerPreference::default(),
1257            force_fallback_adapter: false,
1258            compatible_surface: None,
1259        }))
1260        .ok()?;
1261        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1262            label: Some("deform_tests"),
1263            ..Default::default()
1264        }))
1265        .ok()?;
1266        Some((device, queue))
1267    }
1268
1269    #[test]
1270    fn pack_lays_out_offsets_after_slot_layout_prefix() {
1271        let mut data: [Option<Vec<u8>>; DEFORM_SLOT_COUNT] = Default::default();
1272        let mut stride = [0u32; DEFORM_SLOT_COUNT];
1273        // Slot 0: 3 vertices, stride 1 u32 each = 12 bytes
1274        data[0] = Some(vec![1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
1275        stride[0] = 1;
1276        // Slot 2: 2 vertices, stride 2 u32 each = 16 bytes
1277        data[2] = Some(vec![10, 0, 0, 0, 20, 0, 0, 0, 30, 0, 0, 0, 40, 0, 0, 0]);
1278        stride[2] = 2;
1279
1280        let words = DeformationState::pack(&data, &stride);
1281        // Layout prefix holds an (offset, stride) pair per slot.
1282        assert_eq!(words[0], SLOT_LAYOUT_WORDS as u32); // slot 0 offset
1283        assert_eq!(words[1], 1); // slot 0 stride
1284        assert_eq!(words[2], 0); // slot 1 offset (unused)
1285        assert_eq!(words[3], 0); // slot 1 stride
1286        assert_eq!(words[4], (SLOT_LAYOUT_WORDS + 3) as u32); // slot 2 offset
1287        assert_eq!(words[5], 2); // slot 2 stride
1288        assert_eq!(words[6], 0); // slot 3 offset
1289        assert_eq!(words[7], 0); // slot 3 stride
1290        // Slot 0 data follows the prefix.
1291        let slot0_base = SLOT_LAYOUT_WORDS;
1292        assert_eq!(words[slot0_base], 1);
1293        assert_eq!(words[slot0_base + 1], 2);
1294        assert_eq!(words[slot0_base + 2], 3);
1295        // Slot 2 data follows slot 0.
1296        let slot2_base = slot0_base + 3;
1297        assert_eq!(words[slot2_base], 10);
1298        assert_eq!(words[slot2_base + 1], 20);
1299        assert_eq!(words[slot2_base + 2], 30);
1300        assert_eq!(words[slot2_base + 3], 40);
1301    }
1302
1303    #[test]
1304    fn attach_marks_flag_bit_and_swaps_bind_group() {
1305        let Some((device, _queue)) = headless() else {
1306            return;
1307        };
1308        let mut s = DeformationState::new(&device);
1309        let mesh = MeshId::new(7, 0);
1310        assert_eq!(s.flag_bits(mesh), 0);
1311        assert!(!s.has_slot(mesh, 0));
1312
1313        // 4 vertices, stride 1 word each = 16 bytes
1314        s.attach_slot(&device, mesh, 0, 1, &[0u8; 16]);
1315        assert!(s.has_slot(mesh, 0));
1316        assert_eq!(s.flag_bits(mesh), 0b0001);
1317
1318        // 4 vertices, stride 2 words each = 32 bytes
1319        s.attach_slot(&device, mesh, 2, 2, &[0u8; 32]);
1320        assert!(s.has_slot(mesh, 2));
1321        assert_eq!(s.flag_bits(mesh), 0b0101);
1322    }
1323
1324    #[test]
1325    fn detach_clears_flag_bit_and_drops_entry_when_empty() {
1326        let Some((device, _queue)) = headless() else {
1327            return;
1328        };
1329        let mut s = DeformationState::new(&device);
1330        let mesh = MeshId::new(11, 0);
1331        s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1332        assert_eq!(s.flag_bits(mesh), 0b0010);
1333
1334        assert!(s.detach_slot(&device, mesh, 1));
1335        assert_eq!(s.flag_bits(mesh), 0);
1336        assert!(!s.meshes.contains_key(&mesh));
1337        assert!(!s.detach_slot(&device, mesh, 1));
1338    }
1339
1340    #[test]
1341    fn attach_slot_instance_marks_union_and_routes_bind_group() {
1342        let Some((device, queue)) = headless() else {
1343            return;
1344        };
1345        let mut s = DeformationState::new(&device);
1346        let mesh = MeshId::new(42, 0);
1347        let instance = 3u32;
1348        assert_eq!(s.flag_bits(mesh), 0);
1349
1350        // Per-mesh slot 1, then per-instance slot 0.
1351        s.attach_slot(&device, mesh, 1, 1, &[0u8; 16]);
1352        s.attach_slot_instance(&device, &queue, mesh, instance, 0, 16, &[0u8; 64]);
1353
1354        // flag_bits reports the union of per-mesh and per-instance.
1355        assert_eq!(s.flag_bits(mesh), 0b0011);
1356        assert!(s.has_slot_instance(mesh, instance, 0));
1357        assert!(!s.has_slot_instance(mesh, instance, 1));
1358
1359        // The instance bind group differs from the mesh bind group.
1360        let inst_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(instance));
1361        let mesh_bg: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, None);
1362        assert_ne!(inst_bg, mesh_bg);
1363
1364        // A draw of an instance with no per-instance data falls back to the
1365        // mesh bind group.
1366        let fallback: *const wgpu::BindGroup = s.instance_bind_group_for(mesh, Some(99));
1367        assert_eq!(fallback, mesh_bg);
1368
1369        // Detaching the instance slot drops the instance entry and clears
1370        // the union bit; per-mesh slot 1 remains.
1371        assert!(s.detach_slot_instance(&device, &queue, mesh, instance, 0));
1372        assert_eq!(s.flag_bits(mesh), 0b0010);
1373        assert!(!s.has_slot_instance(mesh, instance, 0));
1374    }
1375
1376    #[test]
1377    fn slot_index_assert_traps_out_of_range() {
1378        let Some((device, _queue)) = headless() else {
1379            return;
1380        };
1381        let mut s = DeformationState::new(&device);
1382        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1383            s.attach_slot(&device, MeshId::new(0, 0), DEFORM_SLOT_COUNT, 1, &[0u8; 4])
1384        }));
1385        assert!(result.is_err());
1386    }
1387
1388    /// Registering a deformer that actually reads from `deform_data` must
1389    /// produce a rebuilt LDR `mesh.wgsl` pipeline family. The simplest
1390    /// proof: if the composed source were broken, `register_deformer`
1391    /// would fail at validation; if the rebuild path were broken (e.g.
1392    /// shader module created from stale source), this test would still
1393    /// pass because no draw is issued. So we also re-fetch the LDR
1394    /// pipelines and confirm they are not the originals that the renderer
1395    /// was constructed with.
1396    #[test]
1397    fn register_deformer_rebuilds_ldr_mesh_pipelines() {
1398        use crate::renderer::ViewportRenderer;
1399        let Some((device, _queue)) = headless() else {
1400            return;
1401        };
1402        let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1403
1404        let solid_before: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1405        let wf_before: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1406
1407        let body = "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n    var o = v;\n    if (deform_slot_stride(0u) > 0u) {\n        o.position.z = o.position.z + deform_read_f32(0u, v.vertex_index, 0u);\n    }\n    return o;\n}\n";
1408        let desc = DeformerDesc {
1409            name: "wave",
1410            stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1411            priority: 0,
1412            wgsl_body: body.to_string(),
1413            per_vertex_stride: 4,
1414        };
1415        let id = renderer
1416            .resources_mut()
1417            .register_deformer(&device, desc)
1418            .expect("register");
1419        assert_eq!(id.slot(), 0);
1420
1421        let solid_after: *const wgpu::RenderPipeline = &renderer.resources().solid_pipeline;
1422        let wf_after: *const wgpu::RenderPipeline = &renderer.resources().wireframe_pipeline;
1423        // The fields themselves moved during the swap, so the addresses
1424        // stay the same. Instead, confirm that `solid_pipeline` and
1425        // `wireframe_pipeline` are still live wgpu handles by hashing
1426        // their global_id, which is unique per device-created pipeline.
1427        assert_ne!(solid_before, std::ptr::null());
1428        assert_ne!(solid_after, std::ptr::null());
1429        assert_ne!(wf_before, std::ptr::null());
1430        assert_ne!(wf_after, std::ptr::null());
1431    }
1432
1433    #[test]
1434    fn register_deformer_validates_and_assigns_slot() {
1435        use crate::renderer::ViewportRenderer;
1436        let Some((device, _queue)) = headless() else {
1437            return;
1438        };
1439        let mut renderer = ViewportRenderer::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
1440        let resources = renderer.resources_mut();
1441        let desc = DeformerDesc {
1442            name: "wind",
1443            stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1444            priority: 0,
1445            wgsl_body: "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n    var o = v;\n    o.position.z = o.position.z + 0.001;\n    return o;\n}\n".to_string(),
1446            per_vertex_stride: 4,
1447        };
1448        // Renderer construction already registered the internal skinning
1449        // deformer on a reserved slot, so we record the baseline count and
1450        // expect host slots to start at 0.
1451        let baseline = resources.registered_deformer_count();
1452        let id = resources
1453            .register_deformer(&device, desc)
1454            .expect("register");
1455        assert_eq!(id.slot(), 0);
1456        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1457
1458        let dup = DeformerDesc {
1459            name: "wind",
1460            stage: crate::resources::mesh_sidecar::registry::DeformStage::ObjectSpace,
1461            priority: 0,
1462            wgsl_body:
1463                "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex { return v; }"
1464                    .to_string(),
1465            per_vertex_stride: 4,
1466        };
1467        let err = resources.register_deformer(&device, dup).unwrap_err();
1468        assert!(matches!(
1469            err,
1470            crate::error::ViewportError::DeformNameTaken { .. }
1471        ));
1472        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1473
1474        let bad = DeformerDesc {
1475            name: "wave",
1476            stage: crate::resources::mesh_sidecar::registry::DeformStage::WorldSpace,
1477            priority: 0,
1478            wgsl_body: "this is not valid wgsl".to_string(),
1479            per_vertex_stride: 4,
1480        };
1481        let err = resources.register_deformer(&device, bad).unwrap_err();
1482        assert!(matches!(
1483            err,
1484            crate::error::ViewportError::DeformShaderInvalid { .. }
1485        ));
1486        assert_eq!(resources.registered_deformer_count(), baseline + 1);
1487    }
1488}