//! # Mesh Module — Persistent GPU Geometry Container //! //! Defines `Mesh`, a persistent GPU geometry container. Mesh data is uploaded to the GPU once at creation time //! and remains valid across all frames until dropped. Since Step 8 (DRAFT Step 8.3), a Mesh also retains the //! CPU geometry it was built from (`geometry: Arc`), giving meshes a shared, readable source of truth //! for phases such as bounding-box culling and UV access. Since Step 7, a Mesh may also hold a reference to the //! `Material` that draws it — the appearance lives on the Mesh rather than on the `Entity`. //! //! ## Architecture Notes (per ARCHI_APP.md) //! - **Identifiants**: Each Mesh is registered in Scene by string identifier, enabling dynamic access //! during the render loop without borrow checker issues. The identifier serves as the `Handle` key. //! - **Declaration Phase**: Meshes are instantiated once in the declarative phase before the render loop begins. //! - **Performance**: Multiple entities can reference the same Mesh, reducing memory footprint for repeated geometry. //! - **CPU+GPU retention (DRAFT Step 8, D5)**: `geometry` (CPU) and the vertex/index buffers (GPU) coexist. //! The GPU buffers are uploaded once at creation; the `Arc` is kept for CPU-side computations //! without re-uploading per frame. //! - **LOD packing (Step 19, D7)**: a multi-level mesh packs ALL its levels into **one** vertex buffer and //! **one** index buffer (level k lives at a byte offset), because WebGPU forbids dynamic offsets on //! vertex/index bindings — only the draw ARGS move per frame. The per-level offsets live in the //! `LodRow`s (uploaded to the GPU LOD table); the shadow/main passes always bind level 0. //! //! ## Construction (DRAFT Step 8, D4; Step 19, D6/D7) //! The single canonical constructor is [`Mesh::from_geometry_lod`] (levels + mode); [`Mesh::from_geometry`] //! is its one-level convenience wrapper. The former `Mesh::new`/`Mesh::with_material` //! (which took raw `&[Vertex]`) were removed in Step 8: the `Scene` declares meshes from a `Geometry`, and //! `Mesh` derives its interleaved vertices internally via `Geometry::to_vertices()`. use crate::math::Geometry; use crate::resources::Material; use crate::resources::Vertex; use crate::resources::uniform::LodRow; use std::sync::Arc; use wgpu::util::DeviceExt; /// How a mesh's LOD levels were produced (Step 19, D6). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum LodMode { /// No LOD: a single level (the plain `create_mesh` path). #[default] Off, /// Levels 1.. were auto-generated by quadric edge collapse (`Geometry::decimated`, D10). Auto, /// Levels 1.. were supplied explicitly (`Scene::add_mesh_lod`). Explicit, } /// Persistent GPU geometry: vertex positions, optional indices, draw call counters, and the retained /// CPU `Geometry` (Step 8). Created once via `Mesh::from_geometry()` during scene setup; referenced by /// Renderer for every frame. A Mesh optionally references the `Material` used to render it (`Option>`). /// When `material()` is `None`, the `Scene` supplies its default material at draw time (DRAFT Step 7.3.5). /// /// Since Step 19 a Mesh may carry several LOD levels: they are packed into the single vertex/index /// buffers (D7) and described by `lod_rows` (uploaded per mesh as a [`crate::resources::uniform::LodTable`]). pub struct Mesh { /// Shared CPU geometry of level 0 (the full mesh, Step 8, D5). Retained for CPU-side computation /// (bounding boxes, UV access, normal queries) and shared across meshes with identical geometry. geometry: Arc, /// GPU buffer containing the packed vertex data of ALL levels (one `Vertex` per position, levels /// concatenated in level order; level 0 first). pub vertex_buffer: wgpu::Buffer, /// Optional GPU buffer with the packed indices of all indexed levels (rebased onto the packed /// vertex layout). `None` when no level is indexed. pub index_buffer: Option, /// Number of vertices of level 0. Used as `0..num_vertices` for non-indexed draws. pub num_vertices: u32, /// Number of indices of level 0. Used as `0..num_indices` for indexed draws. pub num_indices: u32, /// The Material used to render this mesh. `None` until assigned; the Renderer falls back to the /// Scene's default material when absent (DRAFT Step 7.3.5). material: Option>, /// How the levels were produced (Step 19, D6). lod_mode: LodMode, /// The CPU geometry of every level, level 0 first (all retained; `geometry` is `lod_levels[0]`). lod_levels: Vec>, /// The packed-buffer offsets per level (mirrors the uploaded per-mesh LOD table). lod_rows: Vec, } /// Error returned by [`pack_levels`] when the packed vertex total exceeds the u16 index range. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PackError { /// Total packed vertices (all levels) — must be < 65536 for u16 rebased indices. pub total_vertices: u32, } impl std::fmt::Display for PackError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "packed LOD vertex total {} exceeds the 65535 u16 index limit", self.total_vertices ) } } impl std::error::Error for PackError {} /// Pure packing of LOD levels (Step 19, D7) — no GPU, unit-testable. /// /// Concatenates every level's interleaved vertices into one flat slice (level 0 first) and rebases /// every indexed level's indices onto the packed vertex layout. Returns the packed vertices, the /// packed indices (`None` when no level is indexed), and one [`LodRow`] per level carrying its /// **element** offsets + counts. Offsets are in ELEMENT units (vertex indices / index elements), /// not bytes: the GPU cull pass copies them straight into the WebGPU indirect draw args, whose /// `first_vertex`/`first_index` fields are element indices — the packed buffers are bound in full /// (offset 0) and only the args' first_* fields move per level. /// /// Fails with [`PackError`] when the **total** packed vertex count reaches 65536 (u16 rebased /// indices cannot address it). Callers (the Scene APIs) validate the total beforehand and report /// the error to the user; a single-level mesh can never fail (its indices are already u16). pub(crate) fn pack_levels( levels: &[Arc], ) -> Result<(Vec, Option>, Vec), PackError> { let total: u32 = levels.iter().map(|l| l.positions.len() as u32).sum(); if total >= 65536 { return Err(PackError { total_vertices: total, }); } let mut vertices: Vec = Vec::new(); let mut indices: Vec = Vec::new(); let mut any_indexed = false; let mut rows: Vec = Vec::with_capacity(levels.len()); let mut vertex_base = 0u32; // element (vertex index) base of the level in the packed buffer let mut index_base = 0u32; // element (index element) base of the level in the packed buffer for level in levels { let level_vertices = level.to_vertices(); // Element units (NOT bytes): the row's offsets feed the WebGPU indirect draw args // (first_vertex = vertex index, first_index = index element) — see the doc above. let level_vertex_offset = vertex_base; let level_vertex_count = level_vertices.len() as u32; let (level_index_offset, level_index_count) = match level.indices() { Some(data) => { any_indexed = true; for &idx in data { // Safe: the total-vertex check above guarantees no u16 overflow. indices.push(idx as u32 as u16 + vertex_base as u16); } (index_base, data.len() as u32) } None => (0, 0), }; rows.push(LodRow::new( level_vertex_offset, level_vertex_count, level_index_offset, level_index_count, )); vertex_base += level_vertex_count; index_base += level_index_count; vertices.extend(level_vertices); } Ok((vertices, any_indexed.then_some(indices), rows)) } impl Mesh { /// Canonical constructor (Step 8, D4, now D6/D7): builds the packed GPU buffers from a list of /// LOD levels (level 0 = the full mesh, always present). /// /// All levels are packed into ONE vertex buffer and ONE index buffer (D7 — WebGPU forbids dynamic /// offsets on vertex/index bindings; only the draw args move). `num_vertices`/`num_indices` /// describe **level 0** (the shadow and main passes always bind level 0); the per-level draw /// arguments are emitted by the GPU cull pass from the uploaded LOD table. pub fn from_geometry_lod( device: &wgpu::Device, levels: Vec>, material: Option>, mode: LodMode, ) -> Self { assert!(!levels.is_empty(), "a mesh needs at least level 0"); // Packing can only fail when the packed vertex total reaches 65536; the Scene APIs // validate that beforehand. A single level (from_geometry) can never fail. let (vertices, indices, rows) = pack_levels(&levels).expect("packed LOD vertex total exceeds the u16 limit"); let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Mesh Vertex Buffer (packed LOD)"), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_SRC, }); let index_buffer = indices.map(|data| { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Mesh Index Buffer (packed LOD)"), usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_SRC, contents: bytemuck::cast_slice(&data), }) }); let l0 = &levels[0]; Self { geometry: Arc::clone(l0), vertex_buffer, index_buffer, num_vertices: l0.positions.len() as u32, num_indices: l0.indices().map(|i| i.len() as u32).unwrap_or(0), material, lod_mode: mode, lod_levels: levels, lod_rows: rows, } } /// One-level convenience constructor (Step 8, D4): builds GPU buffers from a shared CPU /// `Geometry` (no LOD — `LodMode::Off`, `lod_rows` has a single row). pub fn from_geometry( device: &wgpu::Device, geometry: Arc, material: Option>, ) -> Self { Self::from_geometry_lod(device, vec![geometry], material, LodMode::Off) } /// Returns a reference to the level-0 CPU geometry this mesh was built from (Step 8, D5). /// Read-only accessor for CPU-side queries (bounding boxes, UVs, normals). pub fn geometry(&self) -> &Arc { &self.geometry } /// Returns a reference to the Material attached to this mesh, if any. /// When `None`, the Renderer falls back to the Scene's default material at draw time. pub fn material(&self) -> Option<&Arc> { self.material.as_ref() } /// Attaches (or replaces) the Material used to render this mesh. /// Called by `Scene::create_mesh` during scene setup or by advanced users linking geometry manually. pub fn set_material(&mut self, material: Arc) { self.material = Some(material); } /// How this mesh's LOD levels were produced (Step 19, D6). pub fn lod_mode(&self) -> LodMode { self.lod_mode } /// Number of LOD levels (1 for a plain mesh). pub fn num_lod_levels(&self) -> usize { self.lod_rows.len() } /// The per-level packed-buffer rows (level 0 first). pub fn lod_rows(&self) -> &[LodRow] { &self.lod_rows } /// The CPU geometry of level k (0 = full mesh). Used by `Scene::add_mesh_lod` when /// reconstructing a mesh with a modified level list. pub fn lod_levels_arc(&self, k: usize) -> Arc { Arc::clone(&self.lod_levels[k]) } /// The per-mesh GPU LOD table (uploaded once by the Renderer; read by the GPU cull pass). pub fn lod_table(&self) -> crate::resources::uniform::LodTable { crate::resources::uniform::LodTable::from_rows(&self.lod_rows) } /// Whether **level 0** is indexed (the shadow/main passes always bind level 0). pub fn l0_indexed(&self) -> bool { self.lod_rows .first() .map(|r| r.index_count > 0) .unwrap_or(false) } } #[cfg(test)] mod tests { use super::*; use crate::math::primitives; #[test] fn pack_levels_offsets_and_rebasing() { // Two levels: L0 = icosahedron (12 verts / 60 indices), L1 = decimated to 10 (welded). let l0 = Arc::new(primitives::icosphere(1.0, 0)); let l1 = Arc::new(l0.decimated(10)); let (vertices, indices, rows) = pack_levels(&[Arc::clone(&l0), Arc::clone(&l1)]).expect("small mesh packs"); // Packed vertices = L0 + L1 concatenated. assert_eq!(vertices.len(), l0.positions.len() + l1.positions.len()); // Packed indices = 60 + L1's (rebased by L0's vertex count). let packed_indices = indices.expect("both levels indexed"); assert_eq!(packed_indices.len(), 60 + l1.indices().unwrap().len()); // L0 indices unchanged (rebase base 0); L1 indices rebased by 12. for (i, idx) in l0.indices().unwrap().iter().enumerate() { assert_eq!(packed_indices[i], *idx); } for (j, idx) in l1.indices().unwrap().iter().enumerate() { assert_eq!(packed_indices[60 + j], *idx + 12); } // Rows carry the ELEMENT offsets (vertex indices / index elements, not bytes). assert_eq!(rows.len(), 2); assert_eq!(rows[0].vertex_offset, 0); assert_eq!(rows[0].vertex_count, 12); assert_eq!(rows[0].index_offset, 0); assert_eq!(rows[0].index_count, 60); assert_eq!(rows[1].vertex_offset, 12); assert_eq!(rows[1].vertex_count, l1.positions.len() as u32); assert_eq!(rows[1].index_offset, 60); assert_eq!(rows[1].index_count, l1.indices().unwrap().len() as u32); } #[test] fn pack_levels_mixed_indexedness() { // Non-indexed L0 (6 verts) + indexed L1 (welded) — the Auto-mode case from DRAFT D7. let l0 = Arc::new( Geometry::new(vec![ [0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, -2.0, 0.0], ]) .with_indices(vec![0, 1, 2, 3, 4, 5]), ); // Force L0 non-indexed: strip the indices. let l0_nonidx = Arc::new(Geometry::new(l0.positions.clone())); let l1 = Arc::new(l0.decimated(1)); // welded + indexed let (vertices, indices, rows) = pack_levels(&[Arc::clone(&l0_nonidx), Arc::clone(&l1)]).expect("small mesh packs"); assert_eq!(vertices.len(), 6 + l1.positions.len()); let packed = indices.expect("an indexed level exists"); // L0 contributes no indices; L1's are rebased by 6. assert_eq!(packed.len(), l1.indices().unwrap().len()); assert_eq!(rows[0].index_count, 0, "non-indexed L0 row"); assert_eq!(rows[1].index_offset, 0, "L1 is the first indexed level"); assert_eq!(rows[1].vertex_offset, 6, "element units, not bytes"); for (j, idx) in l1.indices().unwrap().iter().enumerate() { assert_eq!(packed[j], *idx + 6); } } #[test] fn pack_levels_all_non_indexed() { let l0 = Arc::new(Geometry::new(vec![ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], ])); let (vertices, indices, rows) = pack_levels(&[l0.clone()]).expect("small mesh packs"); assert_eq!(vertices.len(), 3); assert!(indices.is_none()); assert_eq!(rows[0].index_count, 0); } #[test] fn lod_table_from_rows() { let l0 = Arc::new(primitives::icosphere(1.0, 0)); let l1 = Arc::new(l0.decimated(10)); let (_, _, rows) = pack_levels(&[l0, l1]).expect("small mesh packs"); let table = crate::resources::uniform::LodTable::from_rows(&rows); assert_eq!(table.count, 2); assert_eq!(table.rows[0].index_count, 60); assert_eq!(table.rows[1].vertex_count, rows[1].vertex_count); } #[test] fn pack_levels_rejects_u16_overflow() { // A level with 70k vertices (indexed) cannot be packed with u16 rebased indices. let big = Arc::new( Geometry::new(vec![[0.0, 0.0, 0.0]; 70_000]).with_indices(vec![0u16; 70_000 / 3 * 3]), ); let err = pack_levels(&[big]).unwrap_err(); assert_eq!(err.total_vertices, 70_000); } }