diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 7a2f8ba..d74e79c 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -251,7 +251,7 @@ pub fn project_land_msh_to_static_mesh( .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, ); for face in &terrain.faces { - indices.extend(face.vertices); + indices.extend(face.vertices.map(u32::from)); } if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); @@ -280,7 +280,7 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame( .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, ); for face in &terrain.faces { - indices.extend(face.vertices); + indices.extend(face.vertices.map(u32::from)); } if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); @@ -357,7 +357,7 @@ pub fn project_land_msh_to_static_mesh_in_world_space( }) } -fn static_terrain_indices(terrain: &LandMeshDocument) -> Result, VulkanAssetMeshError> { +fn static_terrain_indices(terrain: &LandMeshDocument) -> Result, VulkanAssetMeshError> { let mut indices = Vec::with_capacity( terrain .faces @@ -366,7 +366,7 @@ fn static_terrain_indices(terrain: &LandMeshDocument) -> Result, Vulkan .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, ); for face in &terrain.faces { - indices.extend(face.vertices); + indices.extend(face.vertices.map(u32::from)); } if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); @@ -391,7 +391,7 @@ fn static_terrain_draw_ranges( fn static_model_indices_and_ranges( model: &ModelAsset, -) -> Result<(Vec, Vec), VulkanAssetMeshError> { +) -> Result<(Vec, Vec), VulkanAssetMeshError> { let mut indices = Vec::new(); let mut draw_ranges = Vec::with_capacity(model.batches.len()); for batch in &model.batches { @@ -411,7 +411,7 @@ fn static_model_indices_and_ranges( .base_vertex .checked_add(u32::from(raw_index)) .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; - indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?); + indices.push(index); } draw_ranges.push(VulkanStaticDrawRange { first_index, @@ -429,7 +429,7 @@ fn static_model_indices_and_ranges( fn static_mesh_xy_bounds( positions: &[[f32; 3]], - indices: &[u16], + indices: &[u32], ) -> Result<(f32, f32, f32, f32), VulkanAssetMeshError> { let mut min_x = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; @@ -437,7 +437,7 @@ fn static_mesh_xy_bounds( let mut max_y = f32::NEG_INFINITY; for &index in indices { let position = positions - .get(usize::from(index)) + .get(usize::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?) .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; if !position.iter().all(|value| value.is_finite()) { return Err(VulkanAssetMeshError::NonFinitePosition); diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index b7e84bd..1d38503 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -236,7 +236,7 @@ pub(super) fn create_static_mesh_index_buffer( device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { - let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::()); + let mut bytes = Vec::with_capacity(mesh.indices.len() * std::mem::size_of::()); for &index in &mesh.indices { bytes.extend_from_slice(&index.to_ne_bytes()); } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 63066b4..f7c72b6 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -701,7 +701,7 @@ impl VulkanSmokeRenderer { command_buffer, self.index_buffer_ref()?.buffer, 0, - vk::IndexType::UINT16, + vk::IndexType::UINT32, ); let clip_from_world = matrix_bytes(self.camera.clip_from_world); device.device().cmd_push_constants( diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 47cab7d..2ca517b 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -126,7 +126,7 @@ pub struct VulkanStaticMesh { /// Vertex data in pipeline order. pub vertices: Vec, /// Triangle-list indices into [`Self::vertices`]. - pub indices: Vec, + pub indices: Vec, /// Source-preserving triangle draw ranges in [`Self::indices`]. pub draw_ranges: Vec, } @@ -296,11 +296,11 @@ impl VulkanStaticMesh { if usize::try_from(expected_first).ok() != Some(self.indices.len()) { return Err("static mesh draw ranges must cover all indices"); } - if self - .indices - .iter() - .any(|&index| usize::from(index) >= self.vertices.len()) - { + if self.indices.iter().any(|&index| { + usize::try_from(index) + .ok() + .is_none_or(|index| index >= self.vertices.len()) + }) { return Err("static mesh index exceeds vertex count"); } Ok(()) diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index af5d7ed..850b509 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -465,28 +465,24 @@ fn append_static_preview_component( component: VulkanStaticMesh, selector_remap: &[(u16, u16)], ) -> Result<(), String> { - let vertex_base = u16::try_from(target.vertices.len()).map_err(|_| { - "static preview exceeds the available 16-bit vertex index space".to_string() - })?; + let vertex_base = u32::try_from(target.vertices.len()) + .map_err(|_| "static preview vertex count exceeds u32".to_string())?; let first_index_base = u32::try_from(target.indices.len()) .map_err(|_| "static preview index count exceeds u32".to_string())?; target .vertices .len() .checked_add(component.vertices.len()) - .filter(|count| *count <= usize::from(u16::MAX) + 1) - .ok_or_else(|| { - "static preview exceeds the available 16-bit vertex index space".to_string() - })?; + .ok_or_else(|| "static preview vertex count exceeds addressable memory".to_string())?; target.vertices.extend(component.vertices); target.indices.extend( component .indices .into_iter() .map(|index| { - index.checked_add(vertex_base).ok_or_else(|| { - "static preview exceeds the available 16-bit vertex index space".to_string() - }) + index + .checked_add(vertex_base) + .ok_or_else(|| "static preview vertex index exceeds u32".to_string()) }) .collect::, _>>()?, ); @@ -1292,6 +1288,29 @@ mod tests { Ok(()) } + #[test] + fn static_preview_component_merge_keeps_indices_above_u16() -> Result<(), String> { + let vertex = fparkan_render_vulkan::VulkanStaticVertex { + position: [0.0, 0.0, 0.0], + color: [1.0, 1.0, 1.0], + uv: [0.0, 0.0], + }; + let mut merged = VulkanStaticMesh { + vertices: vec![vertex; usize::from(u16::MAX) + 1], + indices: Vec::new(), + draw_ranges: Vec::new(), + }; + + append_static_preview_component( + &mut merged, + VulkanStaticMesh::smoke_triangle(), + &[(0, 0)], + )?; + + assert_eq!(merged.indices, vec![65_536, 65_537, 65_538]); + Ok(()) + } + #[test] fn render_commands_follow_snapshot_order() -> Result<(), String> { let snapshot = WorldSnapshot { diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 543af27..e24db94 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1475,6 +1475,18 @@ is a real original-asset rendering milestone, but not pixel parity or gameplay camera parity: it intentionally renders the bounded first preview root and uses a one-time offline camera capture. +Expanding that same preview to all eight AutoDemo mission objects exposed a +different, renderer-local limit: the merged static scene crossed 65,535 +vertices before any Vulkan command was recorded. `VulkanStaticMesh` now uses a +32-bit GPU index buffer (`VK_INDEX_TYPE_UINT32`), while source MSH/TerrainFace28 +indices remain decoded in their original 16-bit representations and are widened +only at the static-render bridge. A regression test crosses the former boundary. +The full captured-camera AutoDemo preview completed at 13.369 seconds and +rendered three 1280×720 native frames: 8 mission objects, 66 mesh components, +67 material descriptors, and zero Vulkan validation warnings or errors. It is +still a static source-world preview: raw mission orientation, terrain material +selection, lighting and animation have not yet been claimed as parity. + A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive window titled `Parkan. Железная Стратегия`. A read-only probe then requested `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known