From b915554f3c5695bd8b9707359ffaa51ea8742341 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 07:56:49 +0400 Subject: [PATCH] feat(render): preserve original mesh batch draws --- .../fparkan-render-vulkan/src/asset_mesh.rs | 22 +++++++++- adapters/fparkan-render-vulkan/src/ffi.rs | 4 +- .../fparkan-render-vulkan/src/ffi/smoke.rs | 15 +++++-- .../src/ffi/smoke_types.rs | 41 ++++++++++++++++++- apps/fparkan-vulkan-smoke/src/main.rs | 5 +++ docs/rendering/renderer_truth_table.md | 2 +- fixtures/acceptance/coverage.tsv | 2 +- 7 files changed, 80 insertions(+), 11 deletions(-) diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index d6689b9..b90499a 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -1,6 +1,6 @@ //! Format-to-GPU geometry bridge for the initial static asset renderer. -use crate::{VulkanStaticMesh, VulkanStaticVertex}; +use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex}; use fparkan_msh::ModelAsset; /// Error returned when a validated MSH cannot enter the current static GPU path. @@ -45,7 +45,10 @@ pub fn project_msh_to_static_mesh( model: &ModelAsset, ) -> Result { let mut indices = Vec::new(); + let mut draw_ranges = Vec::with_capacity(model.batches.len()); for batch in &model.batches { + let first_index = + u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?; let start = usize::try_from(batch.index_start) .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?; let end = start @@ -62,6 +65,10 @@ pub fn project_msh_to_static_mesh( .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?); } + draw_ranges.push(VulkanStaticDrawRange { + first_index, + index_count: u32::from(batch.index_count), + }); } if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); @@ -114,7 +121,11 @@ pub fn project_msh_to_static_mesh( }) .collect(); - Ok(VulkanStaticMesh { vertices, indices }) + Ok(VulkanStaticMesh { + vertices, + indices, + draw_ranges, + }) } #[cfg(test)] @@ -165,6 +176,13 @@ mod tests { .expect("representable MSH"); assert_eq!(mesh.indices, vec![1, 2, 3]); + assert_eq!( + mesh.draw_ranges, + vec![VulkanStaticDrawRange { + first_index: 0, + index_count: 3, + }] + ); assert_eq!(mesh.vertices[1].position, [-0.8, -0.8]); assert_eq!(mesh.vertices[2].position, [0.8, -0.8]); assert_eq!(mesh.vertices[3].position, [-0.8, 0.8]); diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 315d4ff..3beedbd 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -65,8 +65,8 @@ pub use self::runtime::{ pub use self::smoke_types::{ VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, - VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticTexture, - VulkanStaticVertex, VulkanValidationReport, + VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticDrawRange, VulkanStaticMesh, + VulkanStaticTexture, VulkanStaticVertex, VulkanValidationReport, }; #[cfg(test)] use self::surface::extension_name; diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index d8cc0d5..a071e74 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -211,7 +211,7 @@ impl VulkanSmokeRenderer { vertex_buffer: Some(vertex_buffer), index_buffer: Some(index_buffer), texture, - index_count: u32::try_from(create_info.mesh.indices.len()).unwrap_or(u32::MAX), + draw_ranges: create_info.mesh.draw_ranges.clone(), frame_sync: Vec::new(), images_in_flight: Vec::new(), current_frame: 0, @@ -640,9 +640,16 @@ impl VulkanSmokeRenderer { 0, vk::IndexType::UINT16, ); - device - .device() - .cmd_draw_indexed(command_buffer, self.index_count, 1, 0, 0, 0); + for range in &self.draw_ranges { + device.device().cmd_draw_indexed( + command_buffer, + range.index_count, + 1, + range.first_index, + 0, + 0, + ); + } device.device().cmd_end_render_pass(command_buffer); } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index af84fed..0821aba 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -53,6 +53,17 @@ pub struct VulkanStaticMesh { pub vertices: Vec, /// Triangle-list indices into [`Self::vertices`]. pub indices: Vec, + /// Source-preserving triangle draw ranges in [`Self::indices`]. + pub draw_ranges: Vec, +} + +/// One indexed triangle-list draw retained from an original mesh batch. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VulkanStaticDrawRange { + /// First index in the shared index buffer. + pub first_index: u32, + /// Number of indices in this draw. + pub index_count: u32, } /// Decoded RGBA8 image accepted by the initial Vulkan texture upload path. @@ -110,6 +121,10 @@ impl VulkanStaticMesh { }, ], indices: vec![0, 1, 2], + draw_ranges: vec![VulkanStaticDrawRange { + first_index: 0, + index_count: 3, + }], } } @@ -120,6 +135,24 @@ impl VulkanStaticMesh { if self.indices.is_empty() || !self.indices.len().is_multiple_of(3) { return Err("static mesh indices must contain complete triangles"); } + if self.draw_ranges.is_empty() { + return Err("static mesh has no draw ranges"); + } + let mut expected_first = 0_u32; + for range in &self.draw_ranges { + if range.first_index != expected_first + || range.index_count == 0 + || !range.index_count.is_multiple_of(3) + { + return Err("static mesh draw ranges must be contiguous complete triangles"); + } + expected_first = expected_first + .checked_add(range.index_count) + .ok_or("static mesh draw range exceeds index count")?; + } + 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() @@ -148,14 +181,20 @@ mod static_mesh_tests { let no_vertices = VulkanStaticMesh { vertices: Vec::new(), indices: vec![0, 1, 2], + draw_ranges: VulkanStaticMesh::smoke_triangle().draw_ranges, }; let incomplete_triangle = VulkanStaticMesh { vertices: VulkanStaticMesh::smoke_triangle().vertices, indices: vec![0, 1], + draw_ranges: vec![VulkanStaticDrawRange { + first_index: 0, + index_count: 2, + }], }; let out_of_range_index = VulkanStaticMesh { vertices: VulkanStaticMesh::smoke_triangle().vertices, indices: vec![0, 1, 3], + draw_ranges: VulkanStaticMesh::smoke_triangle().draw_ranges, }; assert_eq!(no_vertices.validate(), Err("static mesh has no vertices")); @@ -383,7 +422,7 @@ pub struct VulkanSmokeRenderer { pub(super) vertex_buffer: Option, pub(super) index_buffer: Option, pub(super) texture: Option, - pub(super) index_count: u32, + pub(super) draw_ranges: Vec, pub(super) frame_sync: Vec, pub(super) images_in_flight: Vec, pub(super) current_frame: usize, diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index b789cf9..fc725d8 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -592,6 +592,7 @@ impl SmokeApp { mesh_name: self.options.mesh_input.name(), mesh_vertex_count: self.mesh.vertices.len(), mesh_index_count: self.mesh.indices.len(), + mesh_draw_count: self.mesh.draw_ranges.len(), texture_source: self .options .texture_input @@ -823,6 +824,7 @@ fn render_timeout_failure_report( mesh_name: options.mesh_input.name(), mesh_vertex_count: 0, mesh_index_count: 0, + mesh_draw_count: 0, texture_source: options .texture_input .as_ref() @@ -1065,6 +1067,7 @@ struct SmokeReport<'a> { mesh_name: &'a str, mesh_vertex_count: usize, mesh_index_count: usize, + mesh_draw_count: usize, texture_source: &'a str, texture_archive: &'a str, texture_name: &'a str, @@ -1524,6 +1527,7 @@ mod tests { mesh_name: "MTCHECK.MSH", mesh_vertex_count: 128, mesh_index_count: 252, + mesh_draw_count: 1, texture_source: "original-texm", texture_archive: "Textures.lib", texture_name: "DEFAULT.0", @@ -1554,6 +1558,7 @@ mod tests { assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\"")); assert!(json.contains("\"runner_architecture\": \"x86_64\"")); assert!(json.contains("\"mesh_source\": \"original-msh\"")); + assert!(json.contains("\"mesh_draw_count\": 1")); assert!(json.contains("\"texture_source\": \"original-texm\"")); } diff --git a/docs/rendering/renderer_truth_table.md b/docs/rendering/renderer_truth_table.md index ee6899e..1b19547 100644 --- a/docs/rendering/renderer_truth_table.md +++ b/docs/rendering/renderer_truth_table.md @@ -29,7 +29,7 @@ ## Current repository status -- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge. Помимо direct TEXM input, `fparkan-vulkan-smoke --model-root --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root --wear-archive fortif.rlb --wear-name FR_L_MTP.WEA --material-index 0` проходит `WEAR → MAT0 → Textures.lib`, выбирает `MTP_01.0`, выполняет real indexed draw и загружает selected TEXM mip-0 в device-local image. Image переходит в sampling layout, записывается в `set=0,binding=0` combined image sampler и sampled fragment shader-ом. `Res5` UV0 декодируется как signed fixed point `int16 / 1024.0`; XZ-planar UV остаётся только fallback для модели без optional stream. Это один explicit material selector, не полный per-batch/phase/lightmap/terrain или gameplay renderer. +- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge. Помимо direct TEXM input, `fparkan-vulkan-smoke --model-root --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root --wear-archive fortif.rlb --wear-name FR_L_MTP.WEA --material-index 0` проходит `WEAR → MAT0 → Textures.lib`, выбирает `MTP_01.0`, сохраняет 237 исходных MSH batch ranges и выпускает 237 indexed Vulkan draws, затем загружает selected TEXM mip-0 в device-local image. Image переходит в sampling layout, записывается в `set=0,binding=0` combined image sampler и sampled fragment shader-ом. `Res5` UV0 декодируется как signed fixed point `int16 / 1024.0`; XZ-planar UV остаётся только fallback для модели без optional stream. Все 237 draws пока используют один explicitly selected material, а не полный per-batch/phase/lightmap/terrain или gameplay renderer. - `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх synthetic window descriptor и `VulkanPlanningBackend`. - `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 02e40dd..66fa7ac 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -298,7 +298,7 @@ S3-VK-MESH-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release -- S3-VK-TEXM-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-texm-upload.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0; original TEXM 16x16 decoded RGBA8, staged into device-local image and transitioned to SHADER_READ_ONLY_OPTIMAL S3-VK-DESCRIPTOR-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-texm-sampled.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0; original TEXM is bound as set=0,binding=0 combined image sampler and sampled in fragment shader; validation_warning_count=0 and validation_error_count=0 S3-VK-PIPELINE-001 blocked awaits Stage 3 graphics pipeline key and cache implementation -S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-fr-l-mtp-wear-mat0.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root --wear-archive fortif.rlb --wear-name FR_L_MTP.WEA --material-index 0; live Win32 Vulkan indexed MSH draw with original WEAR→MAT0-selected MTP_01.0 TEXM +S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-fr-l-mtp-batches.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root --wear-archive fortif.rlb --wear-name FR_L_MTP.WEA --material-index 0; live Win32 Vulkan issues 237 source-preserving indexed batch draws for original MSH with WEAR→MAT0-selected MTP_01.0 TEXM S3-VK-DRAW-TERRAIN-001 blocked awaits Stage 3 terrain Vulkan draw path S3-VK-PIXEL-CAPTURE-001 blocked awaits Stage 3 fixed-camera pixel capture approval flow S3-VK-VALIDATION-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH; validation_warning_count=0 and validation_error_count=0