Skip to main content

awsm_renderer/
meshes.rs

1//! Mesh storage and GPU buffer management.
2
3pub mod buffer_info;
4pub mod error;
5pub mod geometry;
6pub mod mesh;
7pub mod meta;
8pub mod morphs;
9#[cfg(feature = "lod")]
10pub mod skin_lod;
11pub mod skins;
12
13use std::collections::HashMap;
14
15use awsm_renderer_core::buffers::{BufferDescriptor, BufferUsage};
16use awsm_renderer_core::renderer::AwsmRendererWebGpu;
17use glam::Mat4;
18use slotmap::{new_key_type, DenseSlotMap, SecondaryMap};
19
20use crate::bind_groups::{BindGroupCreate, BindGroups};
21use crate::bounds::Aabb;
22use crate::buffer::dynamic_storage::DynamicStorageBuffer;
23use crate::instances::Instances;
24use crate::materials::Materials;
25use crate::meshes::buffer_info::MeshBufferVertexInfo;
26use crate::transforms::{Transform, TransformKey, Transforms};
27use crate::{AwsmRenderer, AwsmRendererLogging};
28use buffer_info::{MeshBufferInfoKey, MeshBufferInfos};
29use meta::{MeshMeta, MESH_META_INITIAL_CAPACITY};
30use skins::{SkinKey, Skins};
31
32use error::{AwsmMeshError, Result};
33use mesh::{BillboardMode, Mesh};
34use morphs::{GeometryMorphKey, MaterialMorphKey, Morphs};
35
36impl AwsmRenderer {
37    /// Duplicates a mesh into an existing transform key and mirrors transparent pass pipeline state.
38    pub fn duplicate_mesh_with_transform(
39        &mut self,
40        mesh_key: MeshKey,
41        new_transform_key: TransformKey,
42    ) -> crate::error::Result<MeshKey> {
43        let new_mesh_key = self.meshes.duplicate_with_transform(
44            mesh_key,
45            new_transform_key,
46            &self.materials,
47            &self.transforms,
48        )?;
49
50        self.render_passes
51            .material_transparent
52            .pipelines
53            .clone_render_pipeline_key(mesh_key, new_mesh_key);
54
55        self.sync_spatial_for_mesh(new_mesh_key);
56
57        Ok(new_mesh_key)
58    }
59
60    /// Clones a mesh and its current transform under the same parent.
61    pub fn clone_mesh(&mut self, mesh_key: MeshKey) -> crate::error::Result<MeshKey> {
62        let transform_key = self.meshes.get(mesh_key)?.transform_key;
63        let local_transform = self.transforms.get_local(transform_key)?.clone();
64        let parent_transform = self.transforms.get_parent(transform_key).ok();
65        let new_transform_key = self.transforms.insert(local_transform, parent_transform);
66
67        self.duplicate_mesh_with_transform(mesh_key, new_transform_key)
68    }
69
70    /// Duplicates all meshes that share a transform, returning the new transform and mesh keys.
71    ///
72    /// Transparent pass pipeline mappings are copied per duplicated mesh.
73    pub fn duplicate_meshes_by_transform_key(
74        &mut self,
75        transform_key: TransformKey,
76    ) -> crate::error::Result<(TransformKey, Vec<MeshKey>)> {
77        let source_mesh_keys = self
78            .meshes
79            .keys_by_transform_key(transform_key)
80            .cloned()
81            .ok_or(AwsmMeshError::TransformHasNoMeshes(transform_key))?;
82
83        let (new_transform_key, new_mesh_keys) = self.meshes.duplicate_by_transform_key(
84            transform_key,
85            &self.materials,
86            &mut self.transforms,
87        )?;
88
89        for (source_mesh_key, new_mesh_key) in source_mesh_keys
90            .into_iter()
91            .zip(new_mesh_keys.iter().copied())
92        {
93            self.render_passes
94                .material_transparent
95                .pipelines
96                .clone_render_pipeline_key(source_mesh_key, new_mesh_key);
97            self.sync_spatial_for_mesh(new_mesh_key);
98        }
99
100        Ok((new_transform_key, new_mesh_keys))
101    }
102
103    /// Sets mesh visibility state.
104    pub fn set_mesh_hidden(&mut self, mesh_key: MeshKey, hidden: bool) -> crate::error::Result<()> {
105        let mesh = self.meshes.get_mut(mesh_key)?;
106        mesh.hidden = hidden;
107        self.sync_spatial_for_mesh(mesh_key);
108        Ok(())
109    }
110
111    /// Routes the mesh through the HUD render pass so it draws on top of
112    /// world geometry. Used by editor overlay primitives (gizmos, point
113    /// handles) that need to remain visible regardless of occluding meshes.
114    pub fn set_mesh_hud(&mut self, mesh_key: MeshKey, hud: bool) -> crate::error::Result<()> {
115        let mesh = self.meshes.get_mut(mesh_key)?;
116        mesh.hud = hud;
117        if hud {
118            // T2.6: first HUD usage flips the sticky flag so the next
119            // `RenderTextures::views` allocates the HUD depth
120            // attachment. Stays true for the renderer's lifetime —
121            // a single HUD transition is cheap and the alternative
122            // would be re-shrinking on every HUD toggle.
123            self.meshes.mark_hud_used();
124        }
125        self.sync_spatial_for_mesh(mesh_key);
126        Ok(())
127    }
128
129    /// Reassign the material a mesh references. The previous material is left
130    /// in the materials map for reuse; callers may remove it via the
131    /// `materials` API if they're sure nothing else references it.
132    ///
133    /// Refreshes the mesh's metadata in the meta buffer so the visibility-
134    /// buffer compute pass picks up the new material on the next frame.
135    pub fn set_mesh_material(
136        &mut self,
137        mesh_key: MeshKey,
138        new_material_key: crate::materials::MaterialKey,
139    ) -> crate::error::Result<()> {
140        let mesh = self.meshes.get_mut(mesh_key)?;
141        mesh.material_key = new_material_key;
142        self.meshes
143            .refresh_meta_for_mesh_public(mesh_key, &self.materials, &self.transforms)?;
144        Ok(())
145    }
146
147    /// Sets (or clears) the cheap-material variant for a mesh. The
148    /// cheap variant takes over the mesh's shading whenever last-frame
149    /// coverage drops below the threshold (per-mesh
150    /// `cheap_material_pixel_threshold`, falling back to the
151    /// renderer's `default_cheap_material_pixel_threshold`).
152    ///
153    /// Constraint (validated here): the cheap material MUST share the
154    /// authored material's [`crate::materials::MaterialShaderId`] AND its
155    /// [`is_transparency_pass`](crate::materials::Material::is_transparency_pass) classification. The per-frame routing
156    /// in `Meshes::refresh_cheap_material_routing` only swaps the
157    /// GPU-side `material_offset` — it doesn't migrate the mesh
158    /// between the opaque / transparent renderable pools or rebuild
159    /// the opaque-compute pipeline key. A mismatched pair would
160    /// either silently no-op the cheap path (same-pass, different
161    /// shader_id → wrong compute kernel) or, worse, run a transparent
162    /// material through the opaque pipeline → layout mismatch /
163    /// garbage shading. Returns `IncompatibleCheapMaterial` rather
164    /// than swallowing the mistake.
165    ///
166    /// Pass `cheap_material_key = None` to clear an existing cheap
167    /// variant. The next frame's `refresh_cheap_material_routing`
168    /// re-patches the mesh's `material_offset` back to the authored
169    /// material.
170    pub fn set_mesh_cheap_material(
171        &mut self,
172        mesh_key: MeshKey,
173        cheap_material_key: Option<crate::materials::MaterialKey>,
174        cheap_material_pixel_threshold: Option<u32>,
175    ) -> crate::error::Result<()> {
176        let authored_material = {
177            let mesh = self.meshes.get(mesh_key)?;
178            mesh.material_key
179        };
180        if let Some(cheap) = cheap_material_key {
181            let authored_shader = self.materials.shader_id(authored_material);
182            let cheap_shader = self.materials.shader_id(cheap);
183            if authored_shader != cheap_shader {
184                return Err(crate::meshes::AwsmMeshError::IncompatibleCheapMaterial {
185                    authored: authored_material,
186                    cheap,
187                    reason: format!(
188                        "shader_id mismatch (authored {authored_shader:?} vs cheap {cheap_shader:?}) — \
189                         the per-frame routing only swaps material_offset; cross-shader cheap variants \
190                         need a separate pipeline + render pool migration that isn't wired."
191                    ),
192                }
193                .into());
194            }
195            let authored_blend = self.materials.is_transparency_pass(authored_material);
196            let cheap_blend = self.materials.is_transparency_pass(cheap);
197            if authored_blend != cheap_blend {
198                return Err(crate::meshes::AwsmMeshError::IncompatibleCheapMaterial {
199                    authored: authored_material,
200                    cheap,
201                    reason: format!(
202                        "transparency-pass classification mismatch (authored opaque?={} vs cheap opaque?={}) — \
203                         a cheap variant on the opposite pass would land in the wrong renderable list.",
204                        !authored_blend, !cheap_blend
205                    ),
206                }
207                .into());
208            }
209        }
210        let mesh = self.meshes.get_mut(mesh_key)?;
211        mesh.cheap_material_key = cheap_material_key;
212        mesh.cheap_material_pixel_threshold = cheap_material_pixel_threshold;
213        Ok(())
214    }
215
216    /// Removes all meshes under a transform and clears any pass-local mesh state.
217    pub fn remove_meshes_by_transform_key(&mut self, transform_key: TransformKey) -> Vec<MeshKey> {
218        let mesh_keys = self
219            .meshes
220            .keys_by_transform_key(transform_key)
221            .cloned()
222            .unwrap_or_default();
223
224        if mesh_keys.is_empty() {
225            return mesh_keys;
226        }
227
228        self.meshes.remove_by_transform_key(transform_key);
229
230        for mesh_key in &mesh_keys {
231            self.render_passes
232                .material_transparent
233                .pipelines
234                .remove_render_pipeline_key(*mesh_key);
235            self.drop_spatial_for_mesh(*mesh_key);
236            self.drop_cluster_lod_for_mesh(*mesh_key);
237        }
238
239        mesh_keys
240    }
241
242    /// Removes one mesh and clears any pass-local mesh state.
243    pub fn remove_mesh(&mut self, mesh_key: MeshKey) -> bool {
244        let removed = self.meshes.remove(mesh_key).is_some();
245
246        if removed {
247            self.render_passes
248                .material_transparent
249                .pipelines
250                .remove_render_pipeline_key(mesh_key);
251            self.drop_spatial_for_mesh(mesh_key);
252            self.drop_cluster_lod_for_mesh(mesh_key);
253        }
254
255        removed
256    }
257
258    /// Drop any cluster-LOD ("nanite") render state keyed by this mesh — so a
259    /// removed cluster render mesh `M` doesn't leave a dangling
260    /// [`crate::render_passes::cluster_lod::ClusterMeshState`] whose per-frame
261    /// paging/cut would then hit `MeshNotFound`. No-op without the `lod` feature or
262    /// when `mesh_key` isn't a cluster render mesh.
263    #[cfg(feature = "lod")]
264    fn drop_cluster_lod_for_mesh(&mut self, mesh_key: MeshKey) {
265        if let Some(pass) = self.render_passes.cluster_lod.as_mut() {
266            pass.remove_mesh(mesh_key);
267        }
268    }
269    #[cfg(not(feature = "lod"))]
270    fn drop_cluster_lod_for_mesh(&mut self, _mesh_key: MeshKey) {}
271
272    /// Splits a mesh out to a new transform key.
273    pub fn split_mesh(&mut self, mesh_key: MeshKey) -> crate::error::Result<TransformKey> {
274        let new_transform_key =
275            self.meshes
276                .split_mesh(mesh_key, &mut self.transforms, &self.materials)?;
277        self.sync_spatial_for_mesh(mesh_key);
278        Ok(new_transform_key)
279    }
280
281    /// Splits all meshes under a transform into new transform keys.
282    pub fn split_meshes_by_transform_key(
283        &mut self,
284        transform_key: TransformKey,
285    ) -> crate::error::Result<Vec<(MeshKey, TransformKey)>> {
286        let result = self.meshes.split_meshes_by_transform_key(
287            transform_key,
288            &mut self.transforms,
289            &self.materials,
290        )?;
291        for (mesh_key, _) in &result {
292            self.sync_spatial_for_mesh(*mesh_key);
293        }
294        Ok(result)
295    }
296
297    /// Joins meshes under a shared transform, optionally overriding the transform.
298    pub fn join_meshes(
299        &mut self,
300        mesh_keys: &[MeshKey],
301        transform_override: Option<Transform>,
302    ) -> crate::error::Result<(TransformKey, Vec<MeshKey>)> {
303        let (new_transform_key, moved) = self.meshes.join_meshes(
304            mesh_keys,
305            &mut self.transforms,
306            &self.materials,
307            transform_override,
308        )?;
309        for mesh_key in &moved {
310            self.sync_spatial_for_mesh(*mesh_key);
311        }
312        Ok((new_transform_key, moved))
313    }
314
315    /// Enables GPU instancing for an opaque mesh — sync because the
316    /// transparent pipeline rebuild is unnecessary when the mesh doesn't
317    /// flow through the transparent pass. Use `enable_mesh_instancing` for
318    /// meshes that may also render via the transparent pipeline.
319    pub fn enable_mesh_instancing_opaque(
320        &mut self,
321        mesh_key: MeshKey,
322        transforms: &[Transform],
323    ) -> crate::error::Result<()> {
324        let transform_key = self.meshes.get(mesh_key)?.transform_key;
325        if transforms.is_empty() {
326            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
327        }
328        {
329            let mesh = self.meshes.get_mut(mesh_key)?;
330            if mesh.instanced {
331                return Err(AwsmMeshError::InstancingAlreadyEnabled(mesh_key).into());
332            }
333            mesh.instanced = true;
334        }
335        self.instances.transform_insert(transform_key, transforms)?;
336        Ok(())
337    }
338
339    /// Enables GPU instancing for a mesh with explicit instance transforms.
340    pub async fn enable_mesh_instancing(
341        &mut self,
342        mesh_key: MeshKey,
343        transforms: &[Transform],
344    ) -> crate::error::Result<()> {
345        let buffer_info_key = self.meshes.buffer_info_key(mesh_key)?;
346        let transform_key = self.meshes.get(mesh_key)?.transform_key;
347        if transforms.is_empty() {
348            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
349        }
350        {
351            let mesh = self.meshes.get_mut(mesh_key)?;
352            if mesh.instanced {
353                return Err(AwsmMeshError::InstancingAlreadyEnabled(mesh_key).into());
354            }
355            mesh.instanced = true;
356        }
357
358        self.instances.transform_insert(transform_key, transforms)?;
359
360        let mesh = self.meshes.get(mesh_key)?;
361        // Only transparent-pass meshes get a transparent pipeline (see the
362        // matching guard in `add_raw_mesh`): an opaque dynamic material's
363        // author WGSL targets the opaque contract and can't compile against
364        // the transparent fragment.
365        if !self.materials.is_transparency_pass(mesh.material_key) {
366            return Ok(());
367        }
368        let writes_depth = self.materials.transparent_writes_depth(mesh.material_key);
369        let (mat_base, mat_pbr_features) = self.materials.transparent_variant(mesh.material_key);
370        let dynamic_shader_id = matches!(mat_base, crate::dynamic_materials::ShadingBase::Custom)
371            .then(|| self.materials.shader_id(mesh.material_key));
372        let dynamic_shader =
373            dynamic_shader_id.and_then(|id| self.dynamic_materials.shader_info_for(id));
374        let dynamic_vertex_shader =
375            dynamic_shader_id.and_then(|id| self.dynamic_materials.vertex_shader_info_for(id));
376        self.render_passes
377            .material_transparent
378            .pipelines
379            .set_render_pipeline_key(
380                &self.gpu,
381                mesh,
382                mesh_key,
383                buffer_info_key,
384                &mut self.shaders,
385                &mut self.pipelines,
386                &self.render_passes.material_transparent.bind_groups,
387                &self.pipeline_layouts,
388                &self.meshes.buffer_infos,
389                &self.anti_aliasing,
390                &self.textures,
391                &self.render_textures.formats,
392                writes_depth,
393                mat_base,
394                mat_pbr_features,
395                dynamic_shader_id,
396                dynamic_shader,
397                dynamic_vertex_shader,
398            )
399            .await?;
400
401        Ok(())
402    }
403
404    /// Replaces all instance transforms for an instanced mesh.
405    pub fn set_mesh_instances(
406        &mut self,
407        mesh_key: MeshKey,
408        transforms: &[Transform],
409    ) -> crate::error::Result<()> {
410        if transforms.is_empty() {
411            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
412        }
413        let mesh = self.meshes.get(mesh_key)?;
414        if !mesh.instanced {
415            return Err(AwsmMeshError::InstancingNotEnabled(mesh_key).into());
416        }
417
418        // In-place when the count is unchanged (the per-frame particle path
419        // allocates nothing); insert on shape changes.
420        self.instances
421            .transform_write_all(mesh.transform_key, transforms)?;
422
423        Ok(())
424    }
425
426    /// Sets the per-mesh camera-facing billboard mode and refreshes geometry
427    /// meta so the next frame's vertex shader picks up the new mode.
428    pub fn set_mesh_billboard_mode(
429        &mut self,
430        mesh_key: MeshKey,
431        mode: BillboardMode,
432    ) -> crate::error::Result<()> {
433        if let Ok(mesh) = self.meshes.get_mut(mesh_key) {
434            mesh.billboard_mode = mode;
435        } else {
436            return Err(AwsmMeshError::MeshNotFound(mesh_key).into());
437        }
438        self.meshes
439            .refresh_meta_for_mesh_public(mesh_key, &self.materials, &self.transforms)?;
440        Ok(())
441    }
442
443    /// Writes per-instance attributes (color + alpha + size) for every mesh
444    /// sharing the given transform key, and refreshes those meshes' geometry
445    /// meta so the shading pass picks up the new `instance_attr_base`.
446    ///
447    /// The number of `attrs` must match the number of transforms previously
448    /// written via `set_mesh_instances` / `transform_insert`. Mismatches
449    /// (including the case where no transforms exist yet for the key)
450    /// return `AwsmMeshError::InstanceAttrCountMismatch` — silently
451    /// accepting a shorter slice would leave the shader reading past the
452    /// logical attr range into zero-fill / neighbor allocations and tint
453    /// the trailing instances with garbage.
454    pub fn set_mesh_instance_attrs(
455        &mut self,
456        transform_key: TransformKey,
457        attrs: &[crate::instances::InstanceAttr],
458    ) -> crate::error::Result<()> {
459        let transforms = self
460            .instances
461            .transform_instance_count(transform_key)
462            .unwrap_or(0);
463        if transforms != attrs.len() {
464            return Err(AwsmMeshError::InstanceAttrCountMismatch {
465                transform_key,
466                attrs: attrs.len(),
467                transforms,
468            }
469            .into());
470        }
471        self.instances.attribute_write_all(transform_key, attrs)?;
472
473        let base = self
474            .instances
475            .attribute_buffer_offset(transform_key)
476            .map(|off| (off / crate::instances::InstanceAttr::BYTE_SIZE) as u32)
477            .unwrap_or(u32::MAX);
478
479        let mesh_keys: Vec<MeshKey> = self
480            .meshes
481            .keys_by_transform_key(transform_key)
482            .cloned()
483            .unwrap_or_default();
484
485        for mesh_key in mesh_keys {
486            if let Ok(mesh) = self.meshes.get_mut(mesh_key) {
487                mesh.instance_attr_base = base;
488            }
489            self.meshes.refresh_meta_for_mesh_public(
490                mesh_key,
491                &self.materials,
492                &self.transforms,
493            )?;
494        }
495
496        Ok(())
497    }
498
499    /// Appends a single instance transform to an instanced mesh.
500    pub fn append_mesh_instance(
501        &mut self,
502        mesh_key: MeshKey,
503        transform: Transform,
504    ) -> crate::error::Result<usize> {
505        let start_index = self.append_mesh_instances(mesh_key, &[transform])?;
506        Ok(start_index)
507    }
508
509    /// Appends instance transforms to an instanced mesh. Keeps any
510    /// already-bound per-instance attributes extended in lockstep with
511    /// default `InstanceAttr` entries so the shading pass's
512    /// `instance_attrs[base + instance_index]` lookup never reads past
513    /// the logical slice.
514    pub fn append_mesh_instances(
515        &mut self,
516        mesh_key: MeshKey,
517        transforms: &[Transform],
518    ) -> crate::error::Result<usize> {
519        if transforms.is_empty() {
520            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
521        }
522
523        let mesh = self.meshes.get(mesh_key)?;
524        if !mesh.instanced {
525            return Err(AwsmMeshError::InstancingNotEnabled(mesh_key).into());
526        }
527        let transform_key = mesh.transform_key;
528        if self
529            .instances
530            .transform_instance_count(transform_key)
531            .is_none()
532        {
533            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
534        }
535
536        let start_index = self.instances.transform_extend(transform_key, transforms)?;
537        self.instances
538            .attribute_extend_with_default(transform_key, transforms.len())?;
539        Ok(start_index)
540    }
541
542    /// Reserves additional instance slots for an instanced mesh. Mirrors
543    /// `append_mesh_instances` for attrs: if attrs are already bound,
544    /// extend with defaults so the invariant holds even when reserved
545    /// slots are written via `attribute_update` directly.
546    pub fn reserve_mesh_instances(
547        &mut self,
548        mesh_key: MeshKey,
549        additional: usize,
550    ) -> crate::error::Result<usize> {
551        let mesh = self.meshes.get(mesh_key)?;
552        if !mesh.instanced {
553            return Err(AwsmMeshError::InstancingNotEnabled(mesh_key).into());
554        }
555        let transform_key = mesh.transform_key;
556        if self
557            .instances
558            .transform_instance_count(transform_key)
559            .is_none()
560        {
561            return Err(AwsmMeshError::InstancingMissingTransforms(mesh_key).into());
562        }
563
564        let start_index = self
565            .instances
566            .transform_reserve(transform_key, additional)?;
567        self.instances
568            .attribute_extend_with_default(transform_key, additional)?;
569        Ok(start_index)
570    }
571}
572
573/// Shared mesh resource data and buffer offsets.
574#[derive(Debug, Clone)]
575pub struct MeshResource {
576    pub buffer_info_key: MeshBufferInfoKey,
577    pub visibility_geometry_data_offset: Option<usize>,
578    pub transparency_geometry_data_offset: Option<usize>,
579    pub custom_attribute_data_offset: usize,
580    pub custom_attribute_index_offset: usize,
581    pub aabb: Option<Aabb>,
582    pub geometry_morph_key: Option<GeometryMorphKey>,
583    pub material_morph_key: Option<MaterialMorphKey>,
584    pub skin_key: Option<SkinKey>,
585    pub refcount: usize,
586}
587
588/// Mesh list with shared resources and GPU buffers.
589pub struct Meshes {
590    list: DenseSlotMap<MeshKey, Mesh>,
591    resources: DenseSlotMap<MeshResourceKey, MeshResource>,
592    /// Registered geometry sources (§1 ②). CPU-only retained source, held from
593    /// `register_geometry` until its first `commit_load` packs+uploads the kinds
594    /// its bound materials need, then dropped. The transaction's geometry-dedup
595    /// unit — many meshes share one [`GeometryKey`]. (Populated/consumed by later
596    /// steps; for now the registry exists in parallel to the legacy `insert` path.)
597    geometries: DenseSlotMap<geometry::GeometryKey, geometry::GeometrySource>,
598    /// Which geometry each pending/committed mesh binds to (§1 ③). Set by
599    /// `add_mesh`; read by `resolve_geometry` to wire the shared resource.
600    mesh_to_geometry: SecondaryMap<MeshKey, geometry::GeometryKey>,
601    /// Reverse: the meshes bound to each registered geometry, so `resolve_geometry`
602    /// can union their materials' kinds + wire them all to the one shared resource.
603    geometry_to_meshes: SecondaryMap<geometry::GeometryKey, Vec<MeshKey>>,
604    mesh_to_resource: SecondaryMap<MeshKey, MeshResourceKey>,
605    transform_to_meshes: SecondaryMap<TransformKey, Vec<MeshKey>>,
606    // Merged geometry pool: one allocation per mesh holds
607    // [visibility_data || custom_attribute_index || custom_attribute_data]
608    // contiguously. Per-mesh sub-offsets live in `MeshResource`. Bound
609    // once as `visibility_data` on the opaque compute pass and reused as
610    // a vertex/index buffer by the geometry + transparent passes.
611    mesh_geometry_pool_buffers: DynamicStorageBuffer<MeshResourceKey>,
612    mesh_geometry_pool_gpu_buffer: web_sys::GpuBuffer,
613    mesh_geometry_pool_dirty: bool,
614    // visibility geometry index buffers (position, triangle-id, barycentric, etc.)
615    visibility_geometry_index_buffers: DynamicStorageBuffer<MeshResourceKey>,
616    visibility_geometry_index_gpu_buffer: web_sys::GpuBuffer,
617    visibility_geometry_index_dirty: bool,
618    // transparency geometry data buffers (position, etc.)
619    transparency_geometry_data_buffers: DynamicStorageBuffer<MeshResourceKey>,
620    transparency_geometry_data_gpu_buffer: web_sys::GpuBuffer,
621    transparency_geometry_data_dirty: bool,
622    mesh_geometry_pool_uploader: crate::buffer::mapped_uploader::MappedUploader,
623    visibility_geometry_index_uploader: crate::buffer::mapped_uploader::MappedUploader,
624    transparency_geometry_data_uploader: crate::buffer::mapped_uploader::MappedUploader,
625    // buffer infos
626    pub buffer_infos: MeshBufferInfos,
627    // meta
628    pub meta: MeshMeta,
629    // morphs and skins
630    pub morphs: Morphs,
631    pub skins: Skins,
632    /// Last-frame effective `MaterialKey` per mesh — the value
633    /// `Mesh::effective_material_key` resolved to. Used by
634    /// `refresh_cheap_material_routing` to detect coverage-cross-threshold
635    /// transitions and patch `MaterialMeshMeta.material_offset` only on
636    /// the meshes that actually crossed; steady-state writes are O(0)
637    /// even when every mesh has a cheap variant authored.
638    last_effective_material: SecondaryMap<MeshKey, crate::materials::MaterialKey>,
639    /// Per-skin "frames-since-last-frame-with-coverage > 0" counter,
640    /// driving the coverage-skin-skip grace period in `update_world`.
641    ///
642    /// Tracks: while ANY consumer mesh of a skin had non-zero coverage
643    /// last frame, the counter resets to 0. When every consumer hit
644    /// zero coverage, the counter increments. The skip gate only
645    /// fires once the counter clears the grace threshold AND no
646    /// consumer mesh sits inside the camera frustum (the BVH override).
647    ///
648    /// Default 0 = never-observed-skin / fresh-insertion = "still in
649    /// grace period" so the very first frame after a skin's meshes
650    /// materialise, skinning runs normally (no rest-pose pop).
651    skin_zero_coverage_grace: SecondaryMap<SkinKey, u32>,
652    /// Scratch inverted-index `skin_key → Vec<MeshKey>` reused across
653    /// `update_world` invocations. The outer HashMap is cleared (not
654    /// dropped) at the start of each frame so the bucket-storage Vec
655    /// allocations stick around — steady-state per-frame cost drops
656    /// to "rebucket meshes that have a skin", with zero heap traffic
657    /// once the map's capacity has stabilised. A persistent inverted
658    /// index maintained on mesh insert/remove would be marginally
659    /// faster still, but every mesh-mutation path would have to
660    /// remember to keep it in sync; reuse-and-clear gets us the bulk
661    /// of the win with none of the maintenance burden.
662    skin_consumers_scratch: HashMap<SkinKey, Vec<MeshKey>>,
663    /// T2.6 sticky flag — set to `true` the first time any mesh
664    /// transitions to the HUD render group, and never cleared. The
665    /// HUD depth texture is allocated only when this flag is true,
666    /// saving a full-screen Depth32/Depth24 attachment on builds
667    /// that never use HUD overlays (the library / game default).
668    has_seen_hud: bool,
669    /// Monotonic counter bumped every time a mesh enters the HUD render
670    /// group (post-insert `set_mesh_hud(true)` or insert-with-`hud`).
671    /// The per-frame transparent/HUD pipeline-resolve kick in `render()`
672    /// folds this into its dirty signal so a freshly-inserted HUD
673    /// overlay (editor gizmo, in-game HUD primitive) gets its transparent
674    /// pipeline variant resolved on the next frame — even when the
675    /// texture-pool shape hasn't changed. Stays at 0 for builds that
676    /// never use HUD, so the resolve kick early-outs and they pay
677    /// nothing.
678    hud_revision: u64,
679}
680impl Meshes {
681    // Initial sizes assume ~1000 vertices per mesh
682    // but this is just an allocation, can be divided many ways
683    const INDICES_INITIAL_SIZE: usize = MESH_META_INITIAL_CAPACITY * 3 * 1000;
684
685    const VISIBILITY_GEOMETRY_INITIAL_SIZE: usize =
686        Self::INDICES_INITIAL_SIZE * MeshBufferVertexInfo::VISIBILITY_GEOMETRY_BYTE_SIZE;
687
688    const TRANSPARENCY_GEOMETRY_INITIAL_SIZE: usize =
689        Self::INDICES_INITIAL_SIZE * MeshBufferVertexInfo::TRANSPARENCY_GEOMETRY_BYTE_SIZE;
690
691    // Attribute data is much smaller - only custom attributes (UVs, colors, joints, weights).
692    // Estimate: 2 UV sets (8 bytes each) = 16 bytes per vertex as a reasonable starting point.
693    // For textureless models this will be 0, but buffer will grow as needed.
694    const ATTRIBUTE_DATA_INITIAL_SIZE: usize = Self::INDICES_INITIAL_SIZE * 16;
695
696    // Merged pool capacity = vis_data + attr_index + attr_data
697    // (visibility-byte stride 56 + index stride 4 + attr stride 16).
698    const MESH_GEOMETRY_POOL_INITIAL_SIZE: usize = Self::VISIBILITY_GEOMETRY_INITIAL_SIZE
699        + Self::INDICES_INITIAL_SIZE
700        + Self::ATTRIBUTE_DATA_INITIAL_SIZE;
701
702    /// Creates mesh storage and GPU buffers.
703    pub fn new(gpu: &AwsmRendererWebGpu) -> Result<Self> {
704        Ok(Self {
705            list: DenseSlotMap::with_key(),
706            resources: DenseSlotMap::with_key(),
707            geometries: DenseSlotMap::with_key(),
708            mesh_to_geometry: SecondaryMap::new(),
709            geometry_to_meshes: SecondaryMap::new(),
710            mesh_to_resource: SecondaryMap::new(),
711            transform_to_meshes: SecondaryMap::new(),
712            buffer_infos: MeshBufferInfos::new(),
713            // Merged geometry pool: vis_data + attr_index + attr_data per mesh.
714            mesh_geometry_pool_buffers: DynamicStorageBuffer::new(
715                Self::MESH_GEOMETRY_POOL_INITIAL_SIZE,
716                Some("MeshGeometryPool".to_string()),
717            ),
718            mesh_geometry_pool_gpu_buffer: gpu.create_buffer(
719                &BufferDescriptor::new(
720                    Some("MeshGeometryPool"),
721                    Self::MESH_GEOMETRY_POOL_INITIAL_SIZE,
722                    BufferUsage::new()
723                        .with_copy_dst()
724                        .with_vertex()
725                        .with_storage()
726                        .with_index(),
727                )
728                .into(),
729            )?,
730            mesh_geometry_pool_dirty: true,
731            // visibility index
732            visibility_geometry_index_buffers: DynamicStorageBuffer::new(
733                Self::INDICES_INITIAL_SIZE,
734                Some("MeshVisibilityIndex".to_string()),
735            ),
736            visibility_geometry_index_gpu_buffer: gpu.create_buffer(
737                &BufferDescriptor::new(
738                    Some("MeshVisibilityIndex"),
739                    Self::INDICES_INITIAL_SIZE,
740                    BufferUsage::new().with_copy_dst().with_index(),
741                )
742                .into(),
743            )?,
744            visibility_geometry_index_dirty: true,
745            // transparency geometry
746            transparency_geometry_data_buffers: DynamicStorageBuffer::new(
747                Self::TRANSPARENCY_GEOMETRY_INITIAL_SIZE,
748                Some("MeshTransparencyData".to_string()),
749            ),
750            transparency_geometry_data_gpu_buffer: gpu.create_buffer(
751                &BufferDescriptor::new(
752                    Some("MeshTransparencyData"),
753                    Self::TRANSPARENCY_GEOMETRY_INITIAL_SIZE,
754                    BufferUsage::new().with_copy_dst().with_vertex(),
755                )
756                .into(),
757            )?,
758            transparency_geometry_data_dirty: true,
759            mesh_geometry_pool_uploader: crate::buffer::mapped_uploader::MappedUploader::new(
760                "MeshGeometryPool",
761            ),
762            visibility_geometry_index_uploader: crate::buffer::mapped_uploader::MappedUploader::new(
763                "MeshVisibilityIndex",
764            ),
765            transparency_geometry_data_uploader:
766                crate::buffer::mapped_uploader::MappedUploader::new("MeshTransparencyData"),
767            meta: MeshMeta::new(gpu)?,
768            // attribute morphs and skins
769            morphs: Morphs::new(gpu)?,
770            skins: Skins::new(gpu)?,
771            last_effective_material: SecondaryMap::new(),
772            skin_zero_coverage_grace: SecondaryMap::new(),
773            skin_consumers_scratch: HashMap::new(),
774            has_seen_hud: false,
775            hud_revision: 0,
776        })
777    }
778
779    /// Has any mesh ever been routed through the HUD render group?
780    /// Sticky-true; used by `RenderTextures::views` to defer
781    /// allocation of the HUD depth attachment until a HUD renderable
782    /// actually exists. Builds that never insert a HUD mesh (the
783    /// library / game default) save a full-screen depth attachment.
784    pub fn has_seen_hud(&self) -> bool {
785        self.has_seen_hud
786    }
787
788    /// Revision counter that bumps whenever a mesh enters the HUD render
789    /// group. The `render()` transparent/HUD resolve kick watches this
790    /// (together with the texture-pool shape) to decide when to
791    /// re-resolve HUD meshes' transparent pipeline variants.
792    pub fn hud_revision(&self) -> u64 {
793        self.hud_revision
794    }
795
796    /// Internal: stickily mark that HUD rendering is now in use, and
797    /// bump the HUD revision so the per-frame resolve kick re-checks.
798    /// Called from the public `set_mesh_hud(.., true)` and any other
799    /// insertion path that places a mesh into the HUD group from
800    /// scratch.
801    pub(crate) fn mark_hud_used(&mut self) {
802        self.has_seen_hud = true;
803        self.hud_revision = self.hud_revision.wrapping_add(1);
804    }
805
806    /// Walk every mesh with a `cheap_material_key` authored and patch
807    /// its `MaterialMeshMeta.material_offset` to point at the
808    /// *effective* material for this frame (cheap when coverage is
809    /// below threshold, authored otherwise). Idempotent — the
810    /// `last_effective_material` sidecar tracks the last patched
811    /// value, so meshes whose effective key didn't change generate no
812    /// GPU writes.
813    ///
814    /// Safety: the cheap-material compatibility constraint (same
815    /// `MaterialShaderId` AND same `is_transparency_pass()`
816    /// classification) is enforced by
817    /// [`AwsmRenderer::set_mesh_cheap_material`] at authoring time —
818    /// it returns `AwsmMeshError::IncompatibleCheapMaterial` on any
819    /// pair that would violate it. This per-frame refresh therefore
820    /// only swaps `material_offset`, not pipeline keys or pass-list
821    /// membership. Without that constraint a cross-pass cheap
822    /// material would land in the wrong renderable list relative to
823    /// the meta data the shader reads and either silently no-op the
824    /// cheap path or, worse, route a transparent material through
825    /// the opaque pipeline. There is no separate validation helper —
826    /// the public setter IS the validation entrypoint.
827    ///
828    /// Called once per frame from `AwsmRenderer::render` after
829    /// `coverage.ingest` and before `meshes.meta.write_gpu`.
830    pub fn refresh_cheap_material_routing(
831        &mut self,
832        materials: &crate::materials::Materials,
833        coverage: &crate::coverage::MeshCoverage,
834        default_threshold: u32,
835    ) -> Result<()> {
836        // Two-pass to avoid the immutable-borrow-of-self vs
837        // mutable-borrow-of-self.meta conflict — gather updates first,
838        // then apply.
839        let mut updates: Vec<(MeshKey, u32, crate::materials::MaterialKey)> = Vec::new();
840        for (mesh_key, mesh) in self.list.iter() {
841            if mesh.cheap_material_key.is_none() {
842                continue;
843            }
844            let effective = mesh.effective_material_key(mesh_key, coverage, default_threshold);
845            let last = self.last_effective_material.get(mesh_key).copied();
846            if last == Some(effective) {
847                continue;
848            }
849            // Resolve to GPU offset now (still inside the immutable
850            // borrow) so the update step doesn't need `materials`
851            // access — keeps the mutation set tiny.
852            let offset = materials.buffer_offset(effective)? as u32;
853            updates.push((mesh_key, offset, effective));
854        }
855        for (mesh_key, offset, effective) in updates {
856            // Only cache the patched value when the meta-buffer write
857            // actually went through. `set_material_offset` returns
858            // `false` when the mesh has no material-meta slot (either
859            // never registered or removed between the gather and apply
860            // phase — possible if a sync action races this routine).
861            // Updating `last_effective_material` unconditionally would
862            // suppress every future patch for that mesh: the next
863            // gather pass sees `last == effective` and skips it, so the
864            // GPU `material_offset` stays at whatever it was when the
865            // slot was last alive (frequently a stale, just-recycled
866            // index).
867            if self.meta.set_material_offset(mesh_key, offset) {
868                self.last_effective_material.insert(mesh_key, effective);
869            } else {
870                tracing::debug!(
871                    "refresh_cheap_material_routing: mesh {mesh_key:?} has no material-meta slot; \
872                     skipping cache update so the next gather pass retries"
873                );
874            }
875        }
876        Ok(())
877    }
878
879    /// Mapped-ring upload telemetry for mesh-side per-frame buffers.
880    /// Aggregates the three internal uploaders (geometry pool,
881    /// visibility index, transparency data) into one rollup.
882    pub fn upload_stats(&self) -> crate::buffer::mapped_staging_ring::UploadStats {
883        let mut s = self.mesh_geometry_pool_uploader.stats();
884        let b = self.visibility_geometry_index_uploader.stats();
885        let c = self.transparency_geometry_data_uploader.stats();
886        s.peak_ring_depth_used = s
887            .peak_ring_depth_used
888            .max(b.peak_ring_depth_used)
889            .max(c.peak_ring_depth_used);
890        s.fallback_count += b.fallback_count + c.fallback_count;
891        s.map_async_wait_ms += b.map_async_wait_ms + c.map_async_wait_ms;
892        s.bytes_uploaded_via_ring += b.bytes_uploaded_via_ring + c.bytes_uploaded_via_ring;
893        s.bytes_uploaded_via_fallback +=
894            b.bytes_uploaded_via_fallback + c.bytes_uploaded_via_fallback;
895        s.bytes_uploaded_via_writebuffer +=
896            b.bytes_uploaded_via_writebuffer + c.bytes_uploaded_via_writebuffer;
897        s.resize_count += b.resize_count + c.resize_count;
898        s
899    }
900
901    /// Register a [`geometry::GeometrySource`] (§1 ②) — CPU-only, NO GPU upload.
902    /// Returns a [`geometry::GeometryKey`] that meshes bind to via `add_mesh`; the
903    /// per-pass representations are packed+uploaded at the next `commit_load` from
904    /// the union of bound materials, then the source is dropped. Many meshes may
905    /// share one key (geometry dedup).
906    pub fn register_geometry(&mut self, source: geometry::GeometrySource) -> geometry::GeometryKey {
907        self.geometries.insert(source)
908    }
909
910    /// The retained source for a registered geometry, while it's still present
911    /// (i.e. before its first commit consumes + frees it). `None` after that, or
912    /// for an unknown key.
913    pub fn geometry_source(&self, key: geometry::GeometryKey) -> Option<&geometry::GeometrySource> {
914        self.geometries.get(key)
915    }
916
917    /// Number of registered geometries still holding source (drives the
918    /// `UploadingGeometry` progress count).
919    pub fn geometry_count(&self) -> usize {
920        self.geometries.len()
921    }
922
923    /// Bind an already-built [`Mesh`] to a registered geometry (the deferred
924    /// "append" — `add_mesh`'s storage half). Inserts the mesh (sync MeshKey),
925    /// fills its world AABB from the geometry source, and records the binding maps.
926    /// NO GPU upload, NO resource, NO meta — those happen at the next
927    /// `resolve_geometry` (commit). Returns the MeshKey.
928    pub(crate) fn bind_mesh(
929        &mut self,
930        mut mesh: Mesh,
931        geometry_key: geometry::GeometryKey,
932    ) -> Result<MeshKey> {
933        let source = self
934            .geometries
935            .get(geometry_key)
936            .ok_or(AwsmMeshError::GeometryNotFound(geometry_key))?;
937        if mesh.world_aabb.is_none() {
938            mesh.world_aabb = source.aabb.clone();
939        }
940        let mesh_key = self.list.insert(mesh);
941        self.mesh_to_geometry.insert(mesh_key, geometry_key);
942        self.geometry_to_meshes
943            .entry(geometry_key)
944            .unwrap()
945            .or_default()
946            .push(mesh_key);
947        Ok(mesh_key)
948    }
949
950    /// THE geometry commit (`commit_load` phase 0, §2): for every registered
951    /// geometry, derive + upload the representation(s) the union of its bound
952    /// materials needs — ONCE each — into a single shared resource, wire every
953    /// bound mesh to it, then FREE the source. Returns the wired mesh keys (for the
954    /// caller to sync into the spatial index). Idempotent on an empty registry.
955    ///
956    /// Reuses the existing `insert_resource` (upload) + `wire_instance` (per-mesh)
957    /// plumbing — the only new logic is the union-of-kinds + pack-from-source. A
958    /// geometry registered but never bound is simply dropped (source freed).
959    pub(crate) fn resolve_geometry(
960        &mut self,
961        materials: &Materials,
962        transforms: &Transforms,
963    ) -> Result<Vec<MeshKey>> {
964        let geometry_keys: Vec<geometry::GeometryKey> = self.geometries.keys().collect();
965        let mut wired = Vec::new();
966        for gkey in geometry_keys {
967            wired.extend(self.resolve_one(gkey, materials, transforms)?);
968        }
969        Ok(wired)
970    }
971
972    /// Resolve a SINGLE registered geometry: pack the representation(s) the union
973    /// of its bound materials needs (once each), upload into one shared resource,
974    /// wire every bound mesh, and free the source. Returns the wired mesh keys
975    /// (empty if the geometry is unknown or unbound). Shared by the commit-time
976    /// `resolve_geometry` (all pending) and the eager `add_raw_mesh` (its one
977    /// geometry — so a one-off raw mesh draws immediately, sync, without a commit).
978    pub(crate) fn resolve_one(
979        &mut self,
980        gkey: geometry::GeometryKey,
981        materials: &Materials,
982        transforms: &Transforms,
983    ) -> Result<Vec<MeshKey>> {
984        use crate::meshes::geometry::{geometry_kind, GeometryReps};
985
986        // Take the source OUT (frees it — §1 ②) so the uploads below can borrow
987        // `&mut self` without aliasing the registry. `remove` is unconditional, and
988        // the returned `source` is dropped at the end of this scope — so after a
989        // resolve the registry holds NO `GeometrySource` for `gkey` (source-freed
990        // invariant, §7). The only retained CPU state is the GPU offsets + layout.
991        let Some(source) = self.geometries.remove(gkey) else {
992            return Ok(Vec::new());
993        };
994        let bound = self.geometry_to_meshes.remove(gkey).unwrap_or_default();
995        if bound.is_empty() {
996            return Ok(Vec::new()); // registered but never bound — nothing to build.
997        }
998        let mut wired = Vec::new();
999
1000        {
1001            // UNION the kinds over the bound meshes' materials → the distinct reps to
1002            // build, ONCE each (dedup, §7 — see `GeometryReps::count`). Same fold the
1003            // `reps_union_dedups_to_distinct_kinds_not_instance_count` test pins.
1004            let reps = GeometryReps::from_kinds(bound.iter().filter_map(|&mk| {
1005                let mesh = self.list.get(mk)?;
1006                let material = materials.get(mesh.material_key).ok()?;
1007                Some(geometry_kind(material, mesh.hud))
1008            }));
1009            let want_visibility = reps.visibility;
1010            let want_transparency = reps.transparency;
1011            // Tangents are generated once iff ANY bound material samples a normal map.
1012            let want_tangents = bound.iter().any(|&mk| {
1013                self.list
1014                    .get(mk)
1015                    .and_then(|mesh| materials.get(mesh.material_key).ok())
1016                    .is_some_and(crate::raw_mesh::material_wants_tangents)
1017            });
1018
1019            // Tangents (commit-time): prefer AUTHORED tangents (e.g. glTF TANGENT);
1020            // else generate via MikkTSpace iff a bound material samples a normal map
1021            // (gated — see §6 step 2). `None` ⇒ the packer's synthetic fallback.
1022            let tangents = source.tangents.clone().or_else(|| {
1023                if want_tangents {
1024                    source.uvs0.as_ref().and_then(|uvs| {
1025                        awsm_renderer_tangents::generate_tangents(
1026                            &source.positions,
1027                            &source.normals,
1028                            uvs,
1029                            &source.indices,
1030                        )
1031                    })
1032                } else {
1033                    None
1034                }
1035            });
1036
1037            // Pack exactly the needed representation(s), once each.
1038            let visibility_bytes = want_visibility.then(|| {
1039                crate::mesh_pack::pack_visibility_bytes(
1040                    &source.positions,
1041                    &source.normals,
1042                    tangents.as_deref(),
1043                    &source.indices,
1044                    source.front_face,
1045                )
1046            });
1047            let transparency_bytes = want_transparency.then(|| {
1048                crate::mesh_pack::pack_transparency_bytes(
1049                    &source.positions,
1050                    &source.normals,
1051                    tangents.as_deref(),
1052                    source.vertex_count(),
1053                )
1054            });
1055
1056            // Rebuild the layout descriptor from the source (vis/transp Some/None
1057            // matches what we packed; triangles from the source attributes).
1058            let triangle_count = source.triangle_count();
1059            let buffer_info = buffer_info::MeshBufferInfo {
1060                visibility_geometry_vertex: visibility_bytes.as_ref().map(|_| {
1061                    MeshBufferVertexInfo {
1062                        count: triangle_count * 3,
1063                    }
1064                }),
1065                transparency_geometry_vertex: transparency_bytes.as_ref().map(|_| {
1066                    MeshBufferVertexInfo {
1067                        count: source.vertex_count(),
1068                    }
1069                }),
1070                triangles: buffer_info::MeshBufferTriangleInfo {
1071                    count: triangle_count,
1072                    vertex_attribute_indices: buffer_info::MeshBufferAttributeIndexInfo {
1073                        count: triangle_count * 3,
1074                    },
1075                    vertex_attributes: source.vertex_attributes.clone(),
1076                    vertex_attributes_size: source.custom_attribute_bytes.len(),
1077                    triangle_data: buffer_info::MeshBufferTriangleDataInfo {
1078                        size_per_triangle: 12,
1079                        total_size: triangle_count * 12,
1080                    },
1081                },
1082                // Morph/skin layout travels with the geometry source (deltas are
1083                // kind-independent). `None` for the raw path; the glTF decoder fills
1084                // these when it produces morphed/skinned geometry (§6 step 5).
1085                geometry_morph: source.geometry_morph_info.clone(),
1086                material_morph: source.material_morph_info.clone(),
1087                skin: source.skin_info.clone(),
1088            };
1089            let buffer_info_key = self.buffer_infos.insert(buffer_info);
1090
1091            // ONE shared upload for this geometry; refcount = number of bound meshes.
1092            let resource_key = self.insert_resource(
1093                buffer_info_key,
1094                visibility_bytes.as_deref(),
1095                transparency_bytes.as_deref(),
1096                &source.custom_attribute_bytes,
1097                &source.attribute_index_bytes,
1098                source.aabb.clone(),
1099                source.geometry_morph_key,
1100                source.material_morph_key,
1101                source.skin_key,
1102            )?;
1103            if let Some(resource) = self.resources.get_mut(resource_key) {
1104                resource.refcount = bound.len();
1105            }
1106
1107            // Wire every bound mesh to the shared resource (flags + meta).
1108            for &mk in &bound {
1109                self.wire_instance(mk, resource_key, materials, transforms)?;
1110                self.mesh_to_geometry.remove(mk);
1111                wired.push(mk);
1112            }
1113        }
1114
1115        Ok(wired)
1116    }
1117
1118    // (The legacy eager `insert` / `insert_public` entry points are gone — all
1119    // geometry now flows through `register_geometry` → `add_mesh` → `resolve_one`
1120    // (commit), §1. `insert_resource` below is the shared upload primitive that
1121    // `resolve_one` calls per (geometry, kind); `insert_instance` wires a
1122    // duplicated instance to an already-built resource.)
1123
1124    fn insert_resource(
1125        &mut self,
1126        buffer_info_key: MeshBufferInfoKey,
1127        visibility_geometry_data: Option<&[u8]>,
1128        transparency_geometry_data: Option<&[u8]>,
1129        attribute_data: &[u8],
1130        attribute_index: &[u8],
1131        aabb: Option<Aabb>,
1132        geometry_morph_key: Option<GeometryMorphKey>,
1133        material_morph_key: Option<MaterialMorphKey>,
1134        skin_key: Option<SkinKey>,
1135    ) -> Result<MeshResourceKey> {
1136        let buffer_info = self.buffer_infos.get(buffer_info_key)?;
1137
1138        // Pre-validate geometry buffer info before any mutation.
1139        if visibility_geometry_data.is_some() && buffer_info.visibility_geometry_vertex.is_none() {
1140            return Err(AwsmMeshError::VisibilityGeometryBufferInfoNotFound(
1141                buffer_info_key,
1142            ));
1143        }
1144
1145        let resource_key = self.resources.insert(MeshResource {
1146            buffer_info_key,
1147            visibility_geometry_data_offset: None,
1148            transparency_geometry_data_offset: None,
1149            custom_attribute_data_offset: 0,
1150            custom_attribute_index_offset: 0,
1151            aabb,
1152            geometry_morph_key,
1153            material_morph_key,
1154            skin_key,
1155            refcount: 1,
1156        });
1157
1158        // Perform all fallible buffer updates in one pass so we can roll back on error.
1159        // The merged geometry pool holds [vis_data || attr_index || attr_data] per mesh
1160        // in one allocation; per-section offsets are computed from the section sizes.
1161        let vis_data_len = visibility_geometry_data.map(|d| d.len()).unwrap_or(0);
1162        let attr_index_len = attribute_index.len();
1163        let offsets_result: Result<(Option<usize>, Option<usize>, usize, usize)> = (|| {
1164            if let Some(geometry_data) = visibility_geometry_data {
1165                let vertex_info = buffer_info
1166                    .visibility_geometry_vertex
1167                    .as_ref()
1168                    .expect("visibility_geometry_vertex presence pre-validated");
1169                let mut geometry_index = Vec::new();
1170                for i in 0..vertex_info.count {
1171                    geometry_index.extend_from_slice(&(i as u32).to_le_bytes());
1172                }
1173                self.visibility_geometry_index_buffers
1174                    .update(resource_key, &geometry_index)
1175                    .map_err(|e| {
1176                        AwsmMeshError::BufferCapacityOverflow(format!(
1177                            "visibility geometry index: {e}"
1178                        ))
1179                    })?;
1180                self.visibility_geometry_index_dirty = true;
1181
1182                let mut combined =
1183                    Vec::with_capacity(geometry_data.len() + attr_index_len + attribute_data.len());
1184                combined.extend_from_slice(geometry_data);
1185                combined.extend_from_slice(attribute_index);
1186                combined.extend_from_slice(attribute_data);
1187                let base = self
1188                    .mesh_geometry_pool_buffers
1189                    .update(resource_key, &combined)
1190                    .map_err(|e| {
1191                        AwsmMeshError::BufferCapacityOverflow(format!("mesh geometry pool: {e}"))
1192                    })?;
1193                self.mesh_geometry_pool_dirty = true;
1194
1195                let visibility_offset = Some(base);
1196                let custom_attribute_indices_offset = base + vis_data_len;
1197                let custom_attribute_data_offset = base + vis_data_len + attr_index_len;
1198
1199                let transparency_offset = match transparency_geometry_data {
1200                    Some(geometry_data) => {
1201                        let offset = self
1202                            .transparency_geometry_data_buffers
1203                            .update(resource_key, geometry_data)
1204                            .map_err(|e| {
1205                                AwsmMeshError::BufferCapacityOverflow(format!(
1206                                    "transparency geometry data: {e}"
1207                                ))
1208                            })?;
1209                        self.transparency_geometry_data_dirty = true;
1210                        Some(offset)
1211                    }
1212                    None => None,
1213                };
1214
1215                Ok((
1216                    visibility_offset,
1217                    transparency_offset,
1218                    custom_attribute_indices_offset,
1219                    custom_attribute_data_offset,
1220                ))
1221            } else {
1222                let mut combined = Vec::with_capacity(attr_index_len + attribute_data.len());
1223                combined.extend_from_slice(attribute_index);
1224                combined.extend_from_slice(attribute_data);
1225                let base = self
1226                    .mesh_geometry_pool_buffers
1227                    .update(resource_key, &combined)
1228                    .map_err(|e| {
1229                        AwsmMeshError::BufferCapacityOverflow(format!("mesh geometry pool: {e}"))
1230                    })?;
1231                self.mesh_geometry_pool_dirty = true;
1232
1233                let custom_attribute_indices_offset = base;
1234                let custom_attribute_data_offset = base + attr_index_len;
1235
1236                let transparency_offset = match transparency_geometry_data {
1237                    Some(geometry_data) => {
1238                        let offset = self
1239                            .transparency_geometry_data_buffers
1240                            .update(resource_key, geometry_data)
1241                            .map_err(|e| {
1242                                AwsmMeshError::BufferCapacityOverflow(format!(
1243                                    "transparency geometry data: {e}"
1244                                ))
1245                            })?;
1246                        self.transparency_geometry_data_dirty = true;
1247                        Some(offset)
1248                    }
1249                    None => None,
1250                };
1251
1252                Ok((
1253                    None,
1254                    transparency_offset,
1255                    custom_attribute_indices_offset,
1256                    custom_attribute_data_offset,
1257                ))
1258            }
1259        })();
1260
1261        let (
1262            visibility_geometry_data_offset,
1263            transparency_geometry_data_offset,
1264            custom_attribute_indices_offset,
1265            custom_attribute_data_offset,
1266        ) = match offsets_result {
1267            Ok(offsets) => offsets,
1268            Err(e) => {
1269                // Roll back any partial buffer allocations and the resource entry itself.
1270                self.visibility_geometry_index_buffers.remove(resource_key);
1271                self.mesh_geometry_pool_buffers.remove(resource_key);
1272                self.transparency_geometry_data_buffers.remove(resource_key);
1273                self.resources.remove(resource_key);
1274                return Err(e);
1275            }
1276        };
1277
1278        // KEEP THIS AROUND FOR DEBUGGING
1279        // Very helpful - shows all the non-position vertex attributes and triangle indices
1280        // tracing::info!(
1281        //     "attribute indices: {:?}",
1282        //     buffer_info
1283        //         .triangles
1284        //         .vertex_attribute_indices
1285        //         .debug_to_vec(attribute_index)
1286        // );
1287        // for attr in buffer_info.triangles.vertex_attributes.iter() {
1288        //     tracing::info!(
1289        //         "attribute data {:?}: {:?}",
1290        //         attr,
1291        //         buffer_info
1292        //             .triangles
1293        //             .debug_get_attribute_vec_f32(attr, attribute_data)
1294        //     );
1295        // }
1296
1297        // for attr in buffer_info.triangles.vertex_attributes.iter() {
1298        //     match attr {
1299        //         crate::mesh::MeshBufferVertexAttributeInfo::Custom(
1300        //             crate::mesh::MeshBufferCustomVertexAttributeInfo::Colors { .. },
1301        //         ) => {
1302        //             tracing::info!(
1303        //                 "attribute data {:?}: {:?}",
1304        //                 attr,
1305        //                 buffer_info
1306        //                     .triangles
1307        //                     .debug_get_attribute_vec_f32(attr, attribute_data)
1308        //             );
1309        //         }
1310        //         _ => {}
1311        //     }
1312        // }
1313
1314        if let Some(resource) = self.resources.get_mut(resource_key) {
1315            resource.visibility_geometry_data_offset = visibility_geometry_data_offset;
1316            resource.transparency_geometry_data_offset = transparency_geometry_data_offset;
1317            resource.custom_attribute_data_offset = custom_attribute_data_offset;
1318            resource.custom_attribute_index_offset = custom_attribute_indices_offset;
1319        }
1320
1321        Ok(resource_key)
1322    }
1323
1324    fn insert_instance(
1325        &mut self,
1326        mesh: Mesh,
1327        resource_key: MeshResourceKey,
1328        materials: &Materials,
1329        transforms: &Transforms,
1330    ) -> Result<MeshKey> {
1331        let mesh_key = self.list.insert(mesh);
1332        self.wire_instance(mesh_key, resource_key, materials, transforms)?;
1333        Ok(mesh_key)
1334    }
1335
1336    /// Wires an ALREADY-inserted mesh (in `self.list`) to its shared GPU
1337    /// `resource_key`: derives the pass-routing flags from the resource's
1338    /// representation offsets, fills the world AABB, registers the HUD/transform
1339    /// bookkeeping, and writes the per-mesh meta. The single choke point shared by
1340    /// the legacy `insert` path (via `insert_instance`) AND the geometry-transaction
1341    /// `resolve_geometry`, which binds N meshes to one resource. Bumps no refcount —
1342    /// the caller owns that (each `insert` resource starts at 1; `resolve_geometry`
1343    /// sets it to the bound-mesh count).
1344    fn wire_instance(
1345        &mut self,
1346        mesh_key: MeshKey,
1347        resource_key: MeshResourceKey,
1348        materials: &Materials,
1349        transforms: &Transforms,
1350    ) -> Result<()> {
1351        let (
1352            resource_aabb,
1353            buffer_info_key,
1354            visibility_geometry_data_offset,
1355            transparency_geometry_data_offset,
1356            custom_attribute_index_offset,
1357            custom_attribute_data_offset,
1358            geometry_morph_key,
1359            material_morph_key,
1360            skin_key,
1361        ) = {
1362            let resource = self
1363                .resources
1364                .get(resource_key)
1365                .ok_or(AwsmMeshError::ResourceNotFound(resource_key))?;
1366            (
1367                resource.aabb.clone(),
1368                resource.buffer_info_key,
1369                resource.visibility_geometry_data_offset,
1370                resource.transparency_geometry_data_offset,
1371                resource.custom_attribute_index_offset,
1372                resource.custom_attribute_data_offset,
1373                resource.geometry_morph_key,
1374                resource.material_morph_key,
1375                resource.skin_key,
1376            )
1377        };
1378
1379        let transform_key = {
1380            let mesh = self
1381                .list
1382                .get_mut(mesh_key)
1383                .ok_or(AwsmMeshError::MeshNotFound(mesh_key))?;
1384            // Flags DERIVED from the resource's actual representation offsets — the
1385            // routing flags can never disagree with the uploaded buffers.
1386            mesh.has_visibility_geometry = visibility_geometry_data_offset.is_some();
1387            mesh.has_transparency_geometry = transparency_geometry_data_offset.is_some();
1388            if mesh.world_aabb.is_none() {
1389                mesh.world_aabb = resource_aabb;
1390            }
1391            // T2.6: catch the insert-with-`hud: true` path too — either route into
1392            // the HUD render group trips the sticky flag so the HUD depth attachment
1393            // lands by the next render frame.
1394            if mesh.hud {
1395                self.has_seen_hud = true;
1396                self.hud_revision = self.hud_revision.wrapping_add(1);
1397            }
1398            mesh.transform_key
1399        };
1400
1401        self.mesh_to_resource.insert(mesh_key, resource_key);
1402        self.transform_to_meshes
1403            .entry(transform_key)
1404            .unwrap()
1405            .or_default()
1406            .push(mesh_key);
1407
1408        let mesh = self
1409            .list
1410            .get(mesh_key)
1411            .ok_or(AwsmMeshError::MeshNotFound(mesh_key))?
1412            .clone();
1413        let buffer_info = self.buffer_infos.get(buffer_info_key)?;
1414        self.meta.insert(
1415            mesh_key,
1416            &mesh,
1417            buffer_info,
1418            visibility_geometry_data_offset,
1419            custom_attribute_index_offset,
1420            custom_attribute_data_offset,
1421            geometry_morph_key,
1422            material_morph_key,
1423            skin_key,
1424            materials,
1425            transforms,
1426            &self.morphs,
1427            &self.skins,
1428        )?;
1429
1430        Ok(())
1431    }
1432
1433    /// Duplicates a mesh instance and assigns a new transform key.
1434    ///
1435    /// This low-level API only duplicates mesh storage state. Callers that need pass-specific
1436    /// renderer mappings (for example transparent material pipeline keys) should use
1437    /// `AwsmRenderer::duplicate_mesh_with_transform`.
1438    pub(crate) fn duplicate_with_transform(
1439        &mut self,
1440        mesh_key: MeshKey,
1441        new_transform_key: TransformKey,
1442        materials: &Materials,
1443        transforms: &Transforms,
1444    ) -> Result<MeshKey> {
1445        let mesh = self.get(mesh_key)?.clone();
1446        let resource_key = self.resource_key(mesh_key)?;
1447        let resource_aabb = {
1448            let resource = self
1449                .resources
1450                .get_mut(resource_key)
1451                .ok_or(AwsmMeshError::ResourceNotFound(resource_key))?;
1452            resource.refcount += 1;
1453            resource.aabb.clone()
1454        };
1455
1456        // Pre-transform the AABB into the new transform's world space.
1457        // `update_world` only refreshes meshes whose transform key is
1458        // currently dirty — but a duplicated mesh is often re-parented
1459        // under a transform whose dirty flag has long since cleared,
1460        // so without this the world_aabb stays at the local-space AABB
1461        // and consumers (frustum culling, selection bboxes, gizmo
1462        // centering) see an unrotated, unscaled box.
1463        let world_aabb = match (
1464            resource_aabb.as_ref(),
1465            transforms.get_world(new_transform_key).ok(),
1466        ) {
1467            (Some(aabb), Some(world_mat)) => Some(aabb.transformed(world_mat)),
1468            (Some(aabb), None) => Some(aabb.clone()),
1469            (None, _) => None,
1470        };
1471
1472        let mut new_mesh = mesh.clone();
1473        new_mesh.transform_key = new_transform_key;
1474        new_mesh.world_aabb = world_aabb;
1475
1476        self.insert_instance(new_mesh, resource_key, materials, transforms)
1477    }
1478
1479    /// Duplicates all meshes under a transform into a new transform key.
1480    pub(crate) fn duplicate_by_transform_key(
1481        &mut self,
1482        transform_key: TransformKey,
1483        materials: &Materials,
1484        transforms: &mut Transforms,
1485    ) -> Result<(TransformKey, Vec<MeshKey>)> {
1486        let mesh_keys = self
1487            .transform_to_meshes
1488            .get(transform_key)
1489            .cloned()
1490            .ok_or(AwsmMeshError::TransformHasNoMeshes(transform_key))?;
1491
1492        if mesh_keys.is_empty() {
1493            return Err(AwsmMeshError::TransformHasNoMeshes(transform_key));
1494        }
1495
1496        for mesh_key in &mesh_keys {
1497            if self.get(*mesh_key)?.instanced {
1498                return Err(AwsmMeshError::InstancedMeshUnsupported(*mesh_key));
1499            }
1500        }
1501
1502        let new_transform_key = transforms.duplicate(transform_key)?;
1503
1504        let mut new_mesh_keys = Vec::with_capacity(mesh_keys.len());
1505        for mesh_key in mesh_keys {
1506            let new_mesh_key =
1507                self.duplicate_with_transform(mesh_key, new_transform_key, materials, transforms)?;
1508            new_mesh_keys.push(new_mesh_key);
1509        }
1510
1511        Ok((new_transform_key, new_mesh_keys))
1512    }
1513
1514    /// Splits a mesh into a new transform key so it can move independently.
1515    pub(crate) fn split_mesh(
1516        &mut self,
1517        mesh_key: MeshKey,
1518        transforms: &mut Transforms,
1519        materials: &Materials,
1520    ) -> Result<TransformKey> {
1521        let old_transform_key = self.get(mesh_key)?.transform_key;
1522        if self.get(mesh_key)?.instanced {
1523            return Err(AwsmMeshError::InstancedMeshUnsupported(mesh_key));
1524        }
1525
1526        let new_transform_key = transforms.duplicate(old_transform_key)?;
1527
1528        self.update_mesh_transform(
1529            mesh_key,
1530            old_transform_key,
1531            new_transform_key,
1532            materials,
1533            transforms,
1534        )?;
1535
1536        Ok(new_transform_key)
1537    }
1538
1539    /// Splits all meshes under a transform into independent transforms.
1540    pub(crate) fn split_meshes_by_transform_key(
1541        &mut self,
1542        transform_key: TransformKey,
1543        transforms: &mut Transforms,
1544        materials: &Materials,
1545    ) -> Result<Vec<(MeshKey, TransformKey)>> {
1546        let mesh_keys = self
1547            .transform_to_meshes
1548            .get(transform_key)
1549            .cloned()
1550            .ok_or(AwsmMeshError::TransformHasNoMeshes(transform_key))?;
1551
1552        if mesh_keys.is_empty() {
1553            return Err(AwsmMeshError::TransformHasNoMeshes(transform_key));
1554        }
1555
1556        let mut out = Vec::with_capacity(mesh_keys.len());
1557        for mesh_key in mesh_keys {
1558            let new_transform_key = self.split_mesh(mesh_key, transforms, materials)?;
1559            out.push((mesh_key, new_transform_key));
1560        }
1561
1562        Ok(out)
1563    }
1564
1565    /// Joins multiple meshes under a single transform key.
1566    pub(crate) fn join_meshes(
1567        &mut self,
1568        mesh_keys: &[MeshKey],
1569        transforms: &mut Transforms,
1570        materials: &Materials,
1571        transform_override: Option<Transform>,
1572    ) -> Result<(TransformKey, Vec<MeshKey>)> {
1573        if mesh_keys.is_empty() {
1574            return Err(AwsmMeshError::MeshListEmpty);
1575        }
1576
1577        for mesh_key in mesh_keys {
1578            if self.get(*mesh_key)?.instanced {
1579                return Err(AwsmMeshError::InstancedMeshUnsupported(*mesh_key));
1580            }
1581        }
1582
1583        let mut common_parent = None;
1584        for (index, mesh_key) in mesh_keys.iter().enumerate() {
1585            let mesh = self.get(*mesh_key)?;
1586            let parent = transforms.get_parent(mesh.transform_key).ok();
1587            if index == 0 {
1588                common_parent = parent;
1589            } else if common_parent != parent {
1590                common_parent = None;
1591                break;
1592            }
1593        }
1594
1595        let new_local = match transform_override {
1596            Some(transform) => transform,
1597            None => {
1598                let mut center_sum = glam::Vec3::ZERO;
1599                for mesh_key in mesh_keys {
1600                    let mesh = self.get(*mesh_key)?;
1601                    let center = mesh
1602                        .world_aabb
1603                        .as_ref()
1604                        .map(|aabb| aabb.center())
1605                        .or_else(|| {
1606                            transforms
1607                                .get_world(mesh.transform_key)
1608                                .ok()
1609                                .map(|m| m.w_axis.truncate())
1610                        })
1611                        .unwrap_or(glam::Vec3::ZERO);
1612                    center_sum += center;
1613                }
1614                let centroid_world = center_sum / mesh_keys.len() as f32;
1615                let local_translation = match common_parent {
1616                    Some(parent_key) => transforms
1617                        .get_world(parent_key)
1618                        .ok()
1619                        .map(|m| m.inverse().transform_point3(centroid_world))
1620                        .unwrap_or(centroid_world),
1621                    None => centroid_world,
1622                };
1623                Transform::IDENTITY.with_translation(local_translation)
1624            }
1625        };
1626
1627        let new_transform_key = transforms.insert(new_local, common_parent);
1628
1629        let moved = mesh_keys.to_vec();
1630        for mesh_key in &moved {
1631            let old_transform_key = self.get(*mesh_key)?.transform_key;
1632            self.update_mesh_transform(
1633                *mesh_key,
1634                old_transform_key,
1635                new_transform_key,
1636                materials,
1637                transforms,
1638            )?;
1639        }
1640
1641        Ok((new_transform_key, moved))
1642    }
1643
1644    /// Updates world-space AABBs for meshes affected by dirty transforms or instances.
1645    ///
1646    /// Returns every mesh key whose `world_aabb` was potentially refreshed
1647    /// this call. The caller (currently `AwsmRenderer::update_transforms`)
1648    /// uses the list to mirror the new AABBs into the spatial index.
1649    pub fn update_world(
1650        &mut self,
1651        dirty_transforms: HashMap<TransformKey, Mat4>,
1652        dirty_instances: &std::collections::HashSet<TransformKey>,
1653        transforms: &Transforms,
1654        instances: &Instances,
1655        frame_index: u64,
1656        // Coverage data is consulted at gate time. Empty = consumers
1657        // fall through to their conservative defaults (always update),
1658        // so the parameter is harmless when the GPU coverage pass
1659        // isn't wired yet on the producer side.
1660        coverage: &crate::coverage::MeshCoverage,
1661        // Current camera frustum, if any. The coverage-driven
1662        // skin-skip uses this as a "BVH-visible override": a skin
1663        // whose consumer meshes' world AABBs are all *out of frustum*
1664        // is genuinely off-screen; if any AABB is in-frustum the
1665        // skin is likely about to (or already) disocclude, so we
1666        // continue animating to dodge rest-pose pop-in. `None` is
1667        // treated conservatively (assume in-frustum, never skip
1668        // via coverage) — used by first-frame paths that don't
1669        // have a camera matrix yet.
1670        frustum: Option<&crate::frustum::Frustum>,
1671    ) -> Vec<MeshKey> {
1672        let mut update_keys = std::collections::HashSet::new();
1673        update_keys.extend(dirty_transforms.keys().copied());
1674        update_keys.extend(dirty_instances.iter().copied());
1675
1676        let mut touched = Vec::new();
1677
1678        // This doesn't mark anything as dirty, it just updates the world AABB for frustum culling and depth sorting
1679        for transform_key in update_keys {
1680            let world_mat = dirty_transforms
1681                .get(&transform_key)
1682                .copied()
1683                .or_else(|| transforms.get_world(transform_key).ok().copied());
1684
1685            let world_mat = match world_mat {
1686                Some(mat) => mat,
1687                None => continue,
1688            };
1689
1690            if let Some(mesh_keys) = self.transform_to_meshes.get(transform_key) {
1691                for mesh_key in mesh_keys {
1692                    let resource_aabb = self
1693                        .resource(*mesh_key)
1694                        .ok()
1695                        .and_then(|resource| resource.aabb.clone());
1696
1697                    let world_aabb = match resource_aabb {
1698                        Some(aabb) => {
1699                            let mesh = match self.list.get(*mesh_key) {
1700                                Some(mesh) => mesh,
1701                                None => continue,
1702                            };
1703
1704                            if mesh.instanced {
1705                                match instances.transform_list(mesh.transform_key) {
1706                                    Some(transforms_list) if !transforms_list.is_empty() => {
1707                                        let first = world_mat * transforms_list[0].to_matrix();
1708                                        let mut combined = aabb.transformed(&first);
1709                                        for transform in &transforms_list[1..] {
1710                                            let world = world_mat * transform.to_matrix();
1711                                            let transformed = aabb.transformed(&world);
1712                                            combined.extend(&transformed);
1713                                        }
1714                                        Some(combined)
1715                                    }
1716                                    _ => None,
1717                                }
1718                            } else {
1719                                Some(aabb.transformed(&world_mat))
1720                            }
1721                        }
1722                        None => None,
1723                    };
1724
1725                    if let Some(mesh) = self.list.get_mut(*mesh_key) {
1726                        mesh.world_aabb = world_aabb;
1727                    }
1728                    touched.push(*mesh_key);
1729                }
1730            }
1731        }
1732
1733        // Skin-skip gate. Two layers compose:
1734        //
1735        //   1. `skin_update_period` cadence — purely period-based, no
1736        //      coverage / frustum input. A `period = 4` skin only
1737        //      updates on frames whose `frame_index % 4 == 0`. Drives
1738        //      the distance-LOD skinning policy.
1739        //
1740        //   2. Coverage-driven skip with grace period + BVH override.
1741        //      Layer (1) gates *cadence*; this gates *visibility*. A
1742        //      skin every one of whose consumer meshes had coverage = 0
1743        //      last frame AND whose AABBs all sit outside the camera
1744        //      frustum is genuinely off-screen → safe to skip.
1745        //
1746        //      The grace period (`SKIN_COVERAGE_GRACE_FRAMES`) protects
1747        //      multi-primitive characters (e.g. BrainStem's 59
1748        //      primitives sharing one skeleton) where one submesh
1749        //      briefly self-occludes another for a frame or two. Without
1750        //      grace, that submesh freezes in its last-skinned pose;
1751        //      when the occluder moves and reveals it, it pops into
1752        //      view in rest pose. The grace counter lets the skin
1753        //      keep animating for the first ~2 frames of zero coverage
1754        //      so a brief self-occlusion doesn't propagate.
1755        //
1756        //      The BVH override (`frustum.intersects_aabb`) catches the
1757        //      complementary case: a skin re-entering the frustum is
1758        //      about to disocclude, so we resume animation immediately
1759        //      regardless of last-frame coverage.
1760        //
1761        // `cheap_material_pixel_threshold` (the other coverage consumer)
1762        // doesn't suffer from rest-pose persistence — it just picks
1763        // a cheaper shader on the next frame.
1764        const SKIN_COVERAGE_GRACE_FRAMES: u32 = 2;
1765        let mut skip_skins: std::collections::HashSet<SkinKey> = std::collections::HashSet::new();
1766        // Build the inverted index skin_key → Vec<MeshKey> once so the
1767        // per-skin BVH / coverage walk is O(meshes) total instead of
1768        // O(skins × meshes). Empty entries are fine (a skin with no
1769        // consumer meshes can't show pop-in by definition).
1770        //
1771        // `skin_consumers_scratch` is a persistent field on `Meshes`
1772        // that's `clear()`-ed (not dropped) here so the outer HashMap
1773        // and each bucket Vec keep their capacities frame-to-frame.
1774        // Steady-state heap traffic drops to ~zero once the map sizes
1775        // up; we still pay one rebucket walk per frame, which is the
1776        // O(meshes) cost the original comment promised. The
1777        // disjoint-field destructure scopes the mutable borrow to
1778        // this block; downstream code then re-borrows the scratch
1779        // immutably via `&self.skin_consumers_scratch`.
1780        {
1781            let Self {
1782                list,
1783                mesh_to_resource,
1784                resources,
1785                skin_consumers_scratch,
1786                ..
1787            } = self;
1788            for bucket in skin_consumers_scratch.values_mut() {
1789                bucket.clear();
1790            }
1791            for (mesh_key, _mesh) in list.iter() {
1792                let Some(resource_key) = mesh_to_resource.get(mesh_key).copied() else {
1793                    continue;
1794                };
1795                let Some(resource) = resources.get(resource_key) else {
1796                    continue;
1797                };
1798                if let Some(skin_key) = resource.skin_key {
1799                    skin_consumers_scratch
1800                        .entry(skin_key)
1801                        .or_default()
1802                        .push(mesh_key);
1803                }
1804            }
1805        }
1806        let skin_consumers: &HashMap<SkinKey, Vec<MeshKey>> = &self.skin_consumers_scratch;
1807
1808        if frame_index > 0 {
1809            let skin_keys: Vec<SkinKey> = self.skins.iter_skin_keys().collect();
1810            for skin_key in skin_keys {
1811                // Layer 1: cadence gate.
1812                if !self.skin_should_update_this_frame(skin_key, frame_index) {
1813                    skip_skins.insert(skin_key);
1814                    continue;
1815                }
1816
1817                // Layer 2: coverage + BVH + grace.
1818                let consumers = skin_consumers
1819                    .get(&skin_key)
1820                    .map(Vec::as_slice)
1821                    .unwrap_or(&[]);
1822
1823                // A skin with no live consumers — its meshes were
1824                // removed but the skin slot lingers. No pop-in
1825                // possible, but no work to do either; the cadence
1826                // gate already lets it run if `period` allows, and
1827                // the skip below would also fire (no visible consumers,
1828                // no in-frustum consumers, grace counter expired
1829                // immediately since there's nothing to keep the
1830                // counter at 0). The default branch handles it
1831                // cleanly so we don't need a special case here.
1832                let any_visible_last_frame = consumers
1833                    .iter()
1834                    .any(|mk| !coverage.is_below_threshold(*mk, 1));
1835
1836                let any_in_frustum = match frustum {
1837                    None => true, // conservative: assume in-frustum
1838                    Some(f) => consumers.iter().any(|mk| {
1839                        self.list
1840                            .get(*mk)
1841                            .and_then(|m| m.world_aabb.as_ref())
1842                            .map(|aabb| f.intersects_aabb(aabb))
1843                            .unwrap_or(true)
1844                    }),
1845                };
1846
1847                // Grace counter: reset to 0 the moment ANY consumer
1848                // showed coverage last frame; otherwise increment
1849                // (saturating so it never wraps).
1850                let grace = if any_visible_last_frame {
1851                    0
1852                } else {
1853                    self.skin_zero_coverage_grace
1854                        .get(skin_key)
1855                        .copied()
1856                        .unwrap_or(0)
1857                        .saturating_add(1)
1858                };
1859                self.skin_zero_coverage_grace.insert(skin_key, grace);
1860
1861                // Skip only when (a) the BVH agrees the skin is
1862                // off-screen AND (b) coverage has been zero for long
1863                // enough that a brief self-occlusion is unlikely to
1864                // be the cause AND (c) no consumer showed coverage
1865                // last frame.
1866                if !any_visible_last_frame && !any_in_frustum && grace > SKIN_COVERAGE_GRACE_FRAMES
1867                {
1868                    skip_skins.insert(skin_key);
1869                }
1870            }
1871        }
1872
1873        // This does update the GPU as dirty, bit skins manage their own GPU dirty state
1874        self.skins
1875            .update_transforms(dirty_transforms, transforms, |skin_key| {
1876                !skip_skins.contains(&skin_key)
1877            });
1878
1879        touched
1880    }
1881
1882    fn update_mesh_transform(
1883        &mut self,
1884        mesh_key: MeshKey,
1885        old_transform_key: TransformKey,
1886        new_transform_key: TransformKey,
1887        materials: &Materials,
1888        transforms: &Transforms,
1889    ) -> Result<()> {
1890        let resource_aabb = self.resource(mesh_key).ok().and_then(|r| r.aabb.clone());
1891
1892        // Same reason as `duplicate_with_transform`: pre-transform into
1893        // world space rather than leave the AABB local — the new
1894        // transform key may not be dirty when the next `update_world`
1895        // runs.
1896        let world_aabb = match (
1897            resource_aabb.as_ref(),
1898            transforms.get_world(new_transform_key).ok(),
1899        ) {
1900            (Some(aabb), Some(world_mat)) => Some(aabb.transformed(world_mat)),
1901            (Some(aabb), None) => Some(aabb.clone()),
1902            (None, _) => None,
1903        };
1904
1905        if let Some(mesh) = self.list.get_mut(mesh_key) {
1906            mesh.transform_key = new_transform_key;
1907            mesh.world_aabb = world_aabb;
1908        }
1909
1910        if let Some(meshes) = self.transform_to_meshes.get_mut(old_transform_key) {
1911            meshes.retain(|&key| key != mesh_key);
1912        }
1913        if let Some(meshes) = self.transform_to_meshes.get(old_transform_key) {
1914            if meshes.is_empty() {
1915                self.transform_to_meshes.remove(old_transform_key);
1916            }
1917        }
1918
1919        if let Some(meshes) = self.transform_to_meshes.get_mut(new_transform_key) {
1920            meshes.push(mesh_key);
1921        } else {
1922            self.transform_to_meshes
1923                .insert(new_transform_key, vec![mesh_key]);
1924        }
1925
1926        self.refresh_meta_for_mesh(mesh_key, materials, transforms)?;
1927
1928        Ok(())
1929    }
1930
1931    /// Public wrapper around `refresh_meta_for_mesh` for the `set_mesh_material`
1932    /// path on `AwsmRenderer`.
1933    pub fn refresh_meta_for_mesh_public(
1934        &mut self,
1935        mesh_key: MeshKey,
1936        materials: &Materials,
1937        transforms: &Transforms,
1938    ) -> Result<()> {
1939        self.refresh_meta_for_mesh(mesh_key, materials, transforms)
1940    }
1941
1942    fn refresh_meta_for_mesh(
1943        &mut self,
1944        mesh_key: MeshKey,
1945        materials: &Materials,
1946        transforms: &Transforms,
1947    ) -> Result<()> {
1948        let mesh = self
1949            .list
1950            .get(mesh_key)
1951            .ok_or(AwsmMeshError::MeshNotFound(mesh_key))?;
1952
1953        let (
1954            buffer_info_key,
1955            visibility_geometry_data_offset,
1956            custom_attribute_index_offset,
1957            custom_attribute_data_offset,
1958            geometry_morph_key,
1959            material_morph_key,
1960            skin_key,
1961        ) = {
1962            let resource = self.resource(mesh_key)?;
1963            (
1964                resource.buffer_info_key,
1965                resource.visibility_geometry_data_offset,
1966                resource.custom_attribute_index_offset,
1967                resource.custom_attribute_data_offset,
1968                resource.geometry_morph_key,
1969                resource.material_morph_key,
1970                resource.skin_key,
1971            )
1972        };
1973
1974        let buffer_info = self.buffer_infos.get(buffer_info_key)?;
1975
1976        self.meta.insert(
1977            mesh_key,
1978            mesh,
1979            buffer_info,
1980            visibility_geometry_data_offset,
1981            custom_attribute_index_offset,
1982            custom_attribute_data_offset,
1983            geometry_morph_key,
1984            material_morph_key,
1985            skin_key,
1986            materials,
1987            transforms,
1988            &self.morphs,
1989            &self.skins,
1990        )?;
1991
1992        Ok(())
1993    }
1994
1995    /// Returns mesh keys associated with a transform key.
1996    pub fn keys_by_transform_key(&self, transform_key: TransformKey) -> Option<&Vec<MeshKey>> {
1997        self.transform_to_meshes.get(transform_key)
1998    }
1999
2000    /// Whether this mesh is skinned (its shared resource carries a `SkinKey`).
2001    /// Skinned meshes can't be naively duplicated/hidden by editor tooling:
2002    /// the per-frame joint-matrix update is driven through the live mesh, so a
2003    /// duplicate (or a hidden original) loses its skinning. Callers use this to
2004    /// leave skinned meshes rendering in place.
2005    pub fn mesh_is_skinned(&self, mesh_key: MeshKey) -> bool {
2006        self.resource_key(mesh_key)
2007            .ok()
2008            .and_then(|rk| self.resources.get(rk))
2009            .map(|r| r.skin_key.is_some())
2010            .unwrap_or(false)
2011    }
2012
2013    /// The number of triangles in this mesh's geometry, if known. Editor tooling
2014    /// uses this to report a selection's real triangle count.
2015    pub fn mesh_triangle_count(&self, mesh_key: MeshKey) -> Option<usize> {
2016        let resource_key = self.resource_key(mesh_key).ok()?;
2017        let resource = self.resources.get(resource_key)?;
2018        let buffer_info = self.buffer_infos.get(resource.buffer_info_key).ok()?;
2019        Some(buffer_info.triangles.count)
2020    }
2021
2022    /// Iterates over all mesh keys.
2023    pub fn keys(&self) -> impl Iterator<Item = MeshKey> + '_ {
2024        self.list.keys()
2025    }
2026
2027    /// Returns the resource key for a mesh.
2028    pub fn resource_key(&self, mesh_key: MeshKey) -> Result<MeshResourceKey> {
2029        self.mesh_to_resource
2030            .get(mesh_key)
2031            .copied()
2032            .ok_or(AwsmMeshError::MeshNotFound(mesh_key))
2033    }
2034
2035    /// Returns the buffer info key for a mesh.
2036    pub fn buffer_info_key(&self, mesh_key: MeshKey) -> Result<MeshBufferInfoKey> {
2037        Ok(self.resource(mesh_key)?.buffer_info_key)
2038    }
2039
2040    /// Returns the buffer info for a mesh.
2041    pub fn buffer_info(&self, mesh_key: MeshKey) -> Result<&buffer_info::MeshBufferInfo> {
2042        let buffer_info_key = self.buffer_info_key(mesh_key)?;
2043        self.buffer_infos.get(buffer_info_key)
2044    }
2045
2046    /// Returns the mesh resource referenced by a mesh key.
2047    pub fn resource(&self, mesh_key: MeshKey) -> Result<&MeshResource> {
2048        let resource_key = self.resource_key(mesh_key)?;
2049        self.resources
2050            .get(resource_key)
2051            .ok_or(AwsmMeshError::ResourceNotFound(resource_key))
2052    }
2053
2054    /// Convenience accessor for the optional `SkinKey` on a mesh resource.
2055    /// Returns `None` if the mesh has no resource or no skin. Used by the
2056    /// spatial-index auto-flagger to route skinned meshes through the
2057    /// dynamic sidecar.
2058    pub fn mesh_skin_key(&self, mesh_key: MeshKey) -> Option<Option<SkinKey>> {
2059        self.resource(mesh_key).ok().map(|r| r.skin_key)
2060    }
2061
2062    /// Convenience accessor for the optional `GeometryMorphKey` on a mesh
2063    /// resource. Returns `None` if the mesh has no resource or no geometry
2064    /// morph targets. Used by the editor's animation bridge to resolve a
2065    /// morph-weight animation track (which names a node) to the renderer
2066    /// morph-weight set it drives.
2067    pub fn geometry_morph_key_for_mesh(&self, mesh_key: MeshKey) -> Option<GeometryMorphKey> {
2068        self.resource(mesh_key)
2069            .ok()
2070            .and_then(|r| r.geometry_morph_key)
2071    }
2072
2073    /// Material-morph counterpart of [`Self::geometry_morph_key_for_mesh`] —
2074    /// `None` if the mesh has no resource or no material (UV/color) morph
2075    /// targets. Used by the editor's live `SetMorphWeight` path so a weight
2076    /// poke drives BOTH morph buffers, exactly like a morph animation track.
2077    pub fn material_morph_key_for_mesh(&self, mesh_key: MeshKey) -> Option<MaterialMorphKey> {
2078        self.resource(mesh_key)
2079            .ok()
2080            .and_then(|r| r.material_morph_key)
2081    }
2082
2083    /// Smallest `skin_update_period` across every mesh that references
2084    /// `skin_key`. Used by the per-frame skinning-LOD gate: a skin is
2085    /// updated this frame if ANY of its consumer meshes wants the
2086    /// update, which is the conservative choice for shared skeletons.
2087    /// Returns `1` if no meshes reference the skin (forces an update
2088    /// if anything dirties the joints).
2089    pub fn skin_smallest_period(&self, skin_key: SkinKey) -> u8 {
2090        let mut min_period: u8 = u8::MAX;
2091        for (mesh_key, mesh) in self.iter() {
2092            let same_skin = self
2093                .resource(mesh_key)
2094                .ok()
2095                .and_then(|r| r.skin_key)
2096                .map(|k| k == skin_key)
2097                .unwrap_or(false);
2098            if !same_skin {
2099                continue;
2100            }
2101            min_period = min_period.min(mesh.skin_update_period.max(1));
2102        }
2103        if min_period == u8::MAX {
2104            1
2105        } else {
2106            min_period
2107        }
2108    }
2109
2110    /// Coverage gate for skinning skip. Returns true if
2111    /// EVERY mesh that references `skin_key` had zero pixels last frame.
2112    /// One non-zero consumer is enough to keep the skin updating.
2113    pub fn skin_all_consumers_zero_coverage(
2114        &self,
2115        skin_key: SkinKey,
2116        coverage: &crate::coverage::MeshCoverage,
2117    ) -> bool {
2118        let mut had_any_consumer = false;
2119        for (mesh_key, _mesh) in self.iter() {
2120            let same_skin = self
2121                .resource(mesh_key)
2122                .ok()
2123                .and_then(|r| r.skin_key)
2124                .map(|k| k == skin_key)
2125                .unwrap_or(false);
2126            if !same_skin {
2127                continue;
2128            }
2129            had_any_consumer = true;
2130            if coverage.is_visible_last_frame(mesh_key) {
2131                return false;
2132            }
2133        }
2134        // If no consumers exist, the skin isn't actually rendered —
2135        // skipping it is fine.
2136        had_any_consumer
2137    }
2138
2139    /// Whether the skin should run its per-joint matrix refresh on this
2140    /// frame, given the renderer-wide `frame_index`. A skin updates if
2141    /// `frame_index % min_period == 0`. Always updates on the first
2142    /// frame after a load (frame_index == 0) so the initial pose lands.
2143    pub fn skin_should_update_this_frame(&self, skin_key: SkinKey, frame_index: u64) -> bool {
2144        let period = self.skin_smallest_period(skin_key).max(1) as u64;
2145        if period == 1 || frame_index == 0 {
2146            return true;
2147        }
2148        frame_index % period == 0
2149    }
2150
2151    /// Returns the merged geometry pool GPU buffer. All three per-mesh
2152    /// sections — visibility, attribute indices, attribute data —
2153    /// live in this one buffer; per-mesh sub-offsets in `MeshResource`
2154    /// (visibility/custom_attribute_index/custom_attribute_data_offset)
2155    /// say where each section starts.
2156    pub fn mesh_geometry_pool_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2157        &self.mesh_geometry_pool_gpu_buffer
2158    }
2159
2160    /// Returns the merged geometry pool GPU buffer used by the opaque
2161    /// compute pass for visibility-data reads. Same handle as
2162    /// [`Self::mesh_geometry_pool_gpu_buffer`] — `visibility_data` in
2163    /// WGSL is now a view over the pool.
2164    pub fn visibility_geometry_data_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2165        &self.mesh_geometry_pool_gpu_buffer
2166    }
2167    /// Returns the offset into the merged geometry pool where this mesh's
2168    /// visibility-data section starts.
2169    pub fn visibility_geometry_data_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2170        let resource_key = self.resource_key(key)?;
2171        self.resources
2172            .get(resource_key)
2173            .and_then(|r| r.visibility_geometry_data_offset)
2174            .ok_or(AwsmMeshError::VisibilityGeometryBufferNotFound(key))
2175    }
2176
2177    /// Returns the GPU buffer for visibility geometry indices.
2178    pub fn visibility_geometry_index_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2179        &self.visibility_geometry_index_gpu_buffer
2180    }
2181    /// Returns the offset into visibility geometry indices for a mesh.
2182    pub fn visibility_geometry_index_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2183        let resource_key = self.resource_key(key)?;
2184        self.visibility_geometry_index_buffers
2185            .offset(resource_key)
2186            .ok_or(AwsmMeshError::VisibilityGeometryBufferNotFound(key))
2187    }
2188
2189    /// Returns the merged geometry pool — custom attribute data is a
2190    /// section inside it.
2191    pub fn custom_attribute_data_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2192        &self.mesh_geometry_pool_gpu_buffer
2193    }
2194    /// Returns the offset into the pool where this mesh's custom-attribute
2195    /// data section starts.
2196    pub fn custom_attribute_data_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2197        let resource_key = self.resource_key(key)?;
2198        self.resources
2199            .get(resource_key)
2200            .map(|r| r.custom_attribute_data_offset)
2201            .ok_or(AwsmMeshError::CustomAttributeBufferNotFound(key))
2202    }
2203
2204    /// Returns the GPU buffer for transparency geometry vertex data.
2205    pub fn transparency_geometry_data_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2206        &self.transparency_geometry_data_gpu_buffer
2207    }
2208    /// Returns the offset into transparency geometry data for a mesh.
2209    pub fn transparency_geometry_data_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2210        let resource_key = self.resource_key(key)?;
2211        self.transparency_geometry_data_buffers
2212            .offset(resource_key)
2213            .ok_or(AwsmMeshError::TransparencyGeometryBufferNotFound(key))
2214    }
2215    /// Returns the merged geometry pool used as the transparent draw's
2216    /// index buffer.
2217    pub fn transparency_geometry_index_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2218        &self.mesh_geometry_pool_gpu_buffer
2219    }
2220    /// Returns the offset into the pool where this mesh's attribute-index
2221    /// section starts — reused as the transparent path's index-buffer
2222    /// offset.
2223    pub fn transparency_geometry_index_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2224        let resource_key = self.resource_key(key)?;
2225        self.resources
2226            .get(resource_key)
2227            .map(|r| r.custom_attribute_index_offset)
2228            .ok_or(AwsmMeshError::CustomAttributeBufferNotFound(key))
2229    }
2230
2231    /// Returns the merged geometry pool — custom attribute indices are a
2232    /// section inside it.
2233    pub fn custom_attribute_index_gpu_buffer(&self) -> &web_sys::GpuBuffer {
2234        &self.mesh_geometry_pool_gpu_buffer
2235    }
2236    /// Returns the offset into the pool where this mesh's custom-attribute
2237    /// index section starts.
2238    pub fn custom_attribute_index_buffer_offset(&self, key: MeshKey) -> Result<usize> {
2239        let resource_key = self.resource_key(key)?;
2240        self.resources
2241            .get(resource_key)
2242            .map(|r| r.custom_attribute_index_offset)
2243            .ok_or(AwsmMeshError::CustomAttributeBufferNotFound(key))
2244    }
2245
2246    /// Total number of `Mesh` entries (including hidden / non-renderable).
2247    /// Used as an upper bound when sizing per-mesh GPU buffers before the
2248    /// per-frame renderables list is collected.
2249    pub fn len(&self) -> usize {
2250        self.list.len()
2251    }
2252
2253    /// True when there are no `Mesh` entries.
2254    pub fn is_empty(&self) -> bool {
2255        self.list.is_empty()
2256    }
2257
2258    /// Iterates over meshes and their keys.
2259    pub fn iter(&self) -> impl Iterator<Item = (MeshKey, &Mesh)> {
2260        self.list.iter()
2261    }
2262
2263    /// Total triangles across all **visible** (`!hidden`) meshes — the geometry
2264    /// the frame submits, counting each mesh once. Discrete LOD reduces this by
2265    /// swapping an instance to a coarser level (fewer indices); it's the
2266    /// deterministic before/after metric for the LOD perf win (independent of
2267    /// GPU timers / vsync). Each mesh is counted once, so it reflects per-mesh
2268    /// LOD, not instance multiplicity.
2269    pub fn visible_triangle_count(&self) -> u64 {
2270        self.list
2271            .iter()
2272            .filter(|(_, m)| !m.hidden)
2273            .filter_map(|(k, _)| self.buffer_info(k).ok())
2274            .map(|info| (info.triangles.vertex_attribute_indices.count / 3) as u64)
2275            .sum()
2276    }
2277
2278    /// Walk every mesh key and apply `gate_fn(mesh_key) -> u32` to
2279    /// `MeshMeta::set_shadow_receiver_gate`. Exists so the caller
2280    /// doesn't have to materialise a `Vec<MeshKey>` per frame just to
2281    /// step around the `&self.list` vs `&mut self.meta` split borrow
2282    /// — both fields are disjoint sub-borrows of `self`, so we do the
2283    /// split here and walk `self.list.keys()` in place. The cached
2284    /// last-frame-gate inside `MeshMeta::set_shadow_receiver_gate`
2285    /// keeps the per-call cost effectively `Cell::get + compare` on
2286    /// unchanged meshes; per-frame allocation drops to zero.
2287    pub fn update_shadow_receiver_gates<F: FnMut(MeshKey) -> u32>(&mut self, mut gate_fn: F) {
2288        for mesh_key in self.list.keys() {
2289            let gate = gate_fn(mesh_key);
2290            self.meta.set_shadow_receiver_gate(mesh_key, gate);
2291        }
2292    }
2293
2294    /// Returns a mesh by key.
2295    pub fn get(&self, mesh_key: MeshKey) -> Result<&Mesh> {
2296        self.list
2297            .get(mesh_key)
2298            .ok_or(AwsmMeshError::MeshNotFound(mesh_key))
2299    }
2300
2301    /// Returns a mutable mesh by key.
2302    pub(crate) fn get_mut(&mut self, mesh_key: MeshKey) -> Result<&mut Mesh> {
2303        self.list
2304            .get_mut(mesh_key)
2305            .ok_or(AwsmMeshError::MeshNotFound(mesh_key))
2306    }
2307
2308    /// Removes all meshes that share the given transform key.
2309    pub(crate) fn remove_by_transform_key(
2310        &mut self,
2311        transform_key: TransformKey,
2312    ) -> Option<Vec<Mesh>> {
2313        if let Some(mesh_keys) = self.transform_to_meshes.get(transform_key).cloned() {
2314            let mut removed_meshes = Vec::with_capacity(mesh_keys.capacity());
2315            for mesh_key in mesh_keys.iter() {
2316                if let Some(mesh) = self.remove(*mesh_key) {
2317                    removed_meshes.push(mesh);
2318                }
2319            }
2320            Some(removed_meshes)
2321        } else {
2322            None
2323        }
2324    }
2325    /// Removes a mesh by key and returns it if found.
2326    pub(crate) fn remove(&mut self, mesh_key: MeshKey) -> Option<Mesh> {
2327        if let Some(mesh) = self.list.remove(mesh_key) {
2328            self.meta.remove(mesh_key);
2329            // Drop the cheap-material LOD cache entry so a recycled
2330            // MeshKey can't inherit a stale "effective_material was X"
2331            // hit (which would suppress the first frame's patch).
2332            self.last_effective_material.remove(mesh_key);
2333
2334            if let Some(meshes) = self.transform_to_meshes.get_mut(mesh.transform_key) {
2335                meshes.retain(|&key| key != mesh_key)
2336            }
2337
2338            if let Some(resource_key) = self.mesh_to_resource.remove(mesh_key) {
2339                let should_remove_resource = match self.resources.get_mut(resource_key) {
2340                    Some(resource) => {
2341                        if resource.refcount > 1 {
2342                            resource.refcount -= 1;
2343                            false
2344                        } else {
2345                            true
2346                        }
2347                    }
2348                    None => false,
2349                };
2350
2351                if should_remove_resource {
2352                    if let Some(resource) = self.resources.remove(resource_key) {
2353                        self.mesh_geometry_pool_buffers.remove(resource_key);
2354                        self.visibility_geometry_index_buffers.remove(resource_key);
2355                        self.transparency_geometry_data_buffers.remove(resource_key);
2356
2357                        self.mesh_geometry_pool_dirty = true;
2358                        self.visibility_geometry_index_dirty = true;
2359                        self.transparency_geometry_data_dirty = true;
2360
2361                        if self.buffer_infos.remove(resource.buffer_info_key).is_some() {
2362                            self.mesh_geometry_pool_dirty = true;
2363                            self.visibility_geometry_index_dirty = true;
2364                            self.transparency_geometry_data_dirty = true;
2365                        }
2366
2367                        if let Some(morph_key) = resource.geometry_morph_key {
2368                            self.morphs.geometry.remove(morph_key);
2369                        }
2370
2371                        if let Some(morph_key) = resource.material_morph_key {
2372                            self.morphs.material.remove(morph_key);
2373                        }
2374
2375                        if let Some(skin_key) = resource.skin_key {
2376                            self.skins.remove(skin_key, None);
2377                            // Drop the grace-period cache entry so a
2378                            // recycled SkinKey can't inherit a stale
2379                            // "out-of-frustum for N frames" counter.
2380                            self.skin_zero_coverage_grace.remove(skin_key);
2381                        }
2382                    }
2383                }
2384            }
2385
2386            Some(mesh)
2387        } else {
2388            None
2389        }
2390    }
2391
2392    /// Writes dirty mesh buffers to the GPU and updates bind groups.
2393    pub fn write_gpu(
2394        &mut self,
2395        logging: &AwsmRendererLogging,
2396        gpu: &AwsmRendererWebGpu,
2397        bind_groups: &mut BindGroups,
2398    ) -> Result<()> {
2399        let any_dirty = self.mesh_geometry_pool_dirty
2400            || self.visibility_geometry_index_dirty
2401            || self.transparency_geometry_data_dirty;
2402
2403        if any_dirty {
2404            let _maybe_span_guard = if logging.render_timings.sub_frame() {
2405                Some(tracing::span!(tracing::Level::INFO, "Mesh GPU write").entered())
2406            } else {
2407                None
2408            };
2409
2410            if self.mesh_geometry_pool_dirty {
2411                let mut resized = false;
2412                if let Some(new_size) = self.mesh_geometry_pool_buffers.take_gpu_needs_resize() {
2413                    self.mesh_geometry_pool_gpu_buffer = gpu.create_buffer(
2414                        &BufferDescriptor::new(
2415                            Some("MeshGeometryPool"),
2416                            new_size,
2417                            BufferUsage::new()
2418                                .with_copy_dst()
2419                                .with_vertex()
2420                                .with_storage()
2421                                .with_index(),
2422                        )
2423                        .into(),
2424                    )?;
2425                    bind_groups.mark_create(BindGroupCreate::MeshGeometryPoolResize);
2426                    resized = true;
2427                }
2428                if resized {
2429                    self.mesh_geometry_pool_buffers.clear_dirty_ranges();
2430                    gpu.write_buffer(
2431                        &self.mesh_geometry_pool_gpu_buffer,
2432                        None,
2433                        self.mesh_geometry_pool_buffers.raw_slice(),
2434                        None,
2435                        None,
2436                    )?;
2437                } else {
2438                    let ranges = self.mesh_geometry_pool_buffers.take_dirty_ranges();
2439                    self.mesh_geometry_pool_uploader.write_dirty_ranges(
2440                        gpu,
2441                        &self.mesh_geometry_pool_gpu_buffer,
2442                        self.mesh_geometry_pool_buffers.raw_slice().len(),
2443                        self.mesh_geometry_pool_buffers.raw_slice(),
2444                        &ranges,
2445                    )?;
2446                }
2447            }
2448
2449            if self.visibility_geometry_index_dirty {
2450                let mut resized = false;
2451                if let Some(new_size) = self
2452                    .visibility_geometry_index_buffers
2453                    .take_gpu_needs_resize()
2454                {
2455                    self.visibility_geometry_index_gpu_buffer = gpu.create_buffer(
2456                        &BufferDescriptor::new(
2457                            Some("MeshVisibilityIndex"),
2458                            new_size,
2459                            BufferUsage::new().with_copy_dst().with_index(),
2460                        )
2461                        .into(),
2462                    )?;
2463                    resized = true;
2464                }
2465                if resized {
2466                    self.visibility_geometry_index_buffers.clear_dirty_ranges();
2467                    gpu.write_buffer(
2468                        &self.visibility_geometry_index_gpu_buffer,
2469                        None,
2470                        self.visibility_geometry_index_buffers.raw_slice(),
2471                        None,
2472                        None,
2473                    )?;
2474                } else {
2475                    let ranges = self.visibility_geometry_index_buffers.take_dirty_ranges();
2476                    self.visibility_geometry_index_uploader.write_dirty_ranges(
2477                        gpu,
2478                        &self.visibility_geometry_index_gpu_buffer,
2479                        self.visibility_geometry_index_buffers.raw_slice().len(),
2480                        self.visibility_geometry_index_buffers.raw_slice(),
2481                        &ranges,
2482                    )?;
2483                }
2484            }
2485
2486            if self.transparency_geometry_data_dirty {
2487                let mut resized = false;
2488                if let Some(new_size) = self
2489                    .transparency_geometry_data_buffers
2490                    .take_gpu_needs_resize()
2491                {
2492                    self.transparency_geometry_data_gpu_buffer = gpu.create_buffer(
2493                        &BufferDescriptor::new(
2494                            Some("MeshTransparencyGeometryData"),
2495                            new_size,
2496                            BufferUsage::new()
2497                                .with_copy_dst()
2498                                .with_vertex()
2499                                .with_storage(),
2500                        )
2501                        .into(),
2502                    )?;
2503                    resized = true;
2504                }
2505                if resized {
2506                    self.transparency_geometry_data_buffers.clear_dirty_ranges();
2507                    gpu.write_buffer(
2508                        &self.transparency_geometry_data_gpu_buffer,
2509                        None,
2510                        self.transparency_geometry_data_buffers.raw_slice(),
2511                        None,
2512                        None,
2513                    )?;
2514                } else {
2515                    let ranges = self.transparency_geometry_data_buffers.take_dirty_ranges();
2516                    self.transparency_geometry_data_uploader
2517                        .write_dirty_ranges(
2518                            gpu,
2519                            &self.transparency_geometry_data_gpu_buffer,
2520                            self.transparency_geometry_data_buffers.raw_slice().len(),
2521                            self.transparency_geometry_data_buffers.raw_slice(),
2522                            &ranges,
2523                        )?;
2524                }
2525            }
2526
2527            self.mesh_geometry_pool_dirty = false;
2528            self.visibility_geometry_index_dirty = false;
2529            self.transparency_geometry_data_dirty = false;
2530        }
2531
2532        Ok(())
2533    }
2534}
2535
2536impl Drop for Meshes {
2537    fn drop(&mut self) {
2538        self.mesh_geometry_pool_gpu_buffer.destroy();
2539        self.visibility_geometry_index_gpu_buffer.destroy();
2540        self.transparency_geometry_data_gpu_buffer.destroy();
2541    }
2542}
2543
2544new_key_type! {
2545    /// Opaque key for mesh instances.
2546    pub struct MeshKey;
2547    /// Opaque key for shared mesh resources.
2548    pub struct MeshResourceKey;
2549}