feat(render): preserve original mesh batch draws
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 2m7s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 07:56:49 +04:00
parent 116e7716b9
commit b915554f3c
7 changed files with 80 additions and 11 deletions
@@ -1,6 +1,6 @@
//! Format-to-GPU geometry bridge for the initial static asset renderer. //! Format-to-GPU geometry bridge for the initial static asset renderer.
use crate::{VulkanStaticMesh, VulkanStaticVertex}; use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
use fparkan_msh::ModelAsset; use fparkan_msh::ModelAsset;
/// Error returned when a validated MSH cannot enter the current static GPU path. /// 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, model: &ModelAsset,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { ) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
let mut indices = Vec::new(); let mut indices = Vec::new();
let mut draw_ranges = Vec::with_capacity(model.batches.len());
for batch in &model.batches { 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) let start = usize::try_from(batch.index_start)
.map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?; .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
let end = start let end = start
@@ -62,6 +65,10 @@ pub fn project_msh_to_static_mesh(
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?; .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
indices.push(u16::try_from(index).map_err(|_| 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() { if indices.is_empty() {
return Err(VulkanAssetMeshError::EmptyGeometry); return Err(VulkanAssetMeshError::EmptyGeometry);
@@ -114,7 +121,11 @@ pub fn project_msh_to_static_mesh(
}) })
.collect(); .collect();
Ok(VulkanStaticMesh { vertices, indices }) Ok(VulkanStaticMesh {
vertices,
indices,
draw_ranges,
})
} }
#[cfg(test)] #[cfg(test)]
@@ -165,6 +176,13 @@ mod tests {
.expect("representable MSH"); .expect("representable MSH");
assert_eq!(mesh.indices, vec![1, 2, 3]); 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[1].position, [-0.8, -0.8]);
assert_eq!(mesh.vertices[2].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]); assert_eq!(mesh.vertices[3].position, [-0.8, 0.8]);
+2 -2
View File
@@ -65,8 +65,8 @@ pub use self::runtime::{
pub use self::smoke_types::{ pub use self::smoke_types::{
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticTexture, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticDrawRange, VulkanStaticMesh,
VulkanStaticVertex, VulkanValidationReport, VulkanStaticTexture, VulkanStaticVertex, VulkanValidationReport,
}; };
#[cfg(test)] #[cfg(test)]
use self::surface::extension_name; use self::surface::extension_name;
@@ -211,7 +211,7 @@ impl VulkanSmokeRenderer {
vertex_buffer: Some(vertex_buffer), vertex_buffer: Some(vertex_buffer),
index_buffer: Some(index_buffer), index_buffer: Some(index_buffer),
texture, 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(), frame_sync: Vec::new(),
images_in_flight: Vec::new(), images_in_flight: Vec::new(),
current_frame: 0, current_frame: 0,
@@ -640,9 +640,16 @@ impl VulkanSmokeRenderer {
0, 0,
vk::IndexType::UINT16, vk::IndexType::UINT16,
); );
device for range in &self.draw_ranges {
.device() device.device().cmd_draw_indexed(
.cmd_draw_indexed(command_buffer, self.index_count, 1, 0, 0, 0); command_buffer,
range.index_count,
1,
range.first_index,
0,
0,
);
}
device.device().cmd_end_render_pass(command_buffer); device.device().cmd_end_render_pass(command_buffer);
} }
@@ -53,6 +53,17 @@ pub struct VulkanStaticMesh {
pub vertices: Vec<VulkanStaticVertex>, pub vertices: Vec<VulkanStaticVertex>,
/// Triangle-list indices into [`Self::vertices`]. /// Triangle-list indices into [`Self::vertices`].
pub indices: Vec<u16>, pub indices: Vec<u16>,
/// Source-preserving triangle draw ranges in [`Self::indices`].
pub draw_ranges: Vec<VulkanStaticDrawRange>,
}
/// 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. /// Decoded RGBA8 image accepted by the initial Vulkan texture upload path.
@@ -110,6 +121,10 @@ impl VulkanStaticMesh {
}, },
], ],
indices: vec![0, 1, 2], 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) { if self.indices.is_empty() || !self.indices.len().is_multiple_of(3) {
return Err("static mesh indices must contain complete triangles"); 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 if self
.indices .indices
.iter() .iter()
@@ -148,14 +181,20 @@ mod static_mesh_tests {
let no_vertices = VulkanStaticMesh { let no_vertices = VulkanStaticMesh {
vertices: Vec::new(), vertices: Vec::new(),
indices: vec![0, 1, 2], indices: vec![0, 1, 2],
draw_ranges: VulkanStaticMesh::smoke_triangle().draw_ranges,
}; };
let incomplete_triangle = VulkanStaticMesh { let incomplete_triangle = VulkanStaticMesh {
vertices: VulkanStaticMesh::smoke_triangle().vertices, vertices: VulkanStaticMesh::smoke_triangle().vertices,
indices: vec![0, 1], indices: vec![0, 1],
draw_ranges: vec![VulkanStaticDrawRange {
first_index: 0,
index_count: 2,
}],
}; };
let out_of_range_index = VulkanStaticMesh { let out_of_range_index = VulkanStaticMesh {
vertices: VulkanStaticMesh::smoke_triangle().vertices, vertices: VulkanStaticMesh::smoke_triangle().vertices,
indices: vec![0, 1, 3], indices: vec![0, 1, 3],
draw_ranges: VulkanStaticMesh::smoke_triangle().draw_ranges,
}; };
assert_eq!(no_vertices.validate(), Err("static mesh has no vertices")); assert_eq!(no_vertices.validate(), Err("static mesh has no vertices"));
@@ -383,7 +422,7 @@ pub struct VulkanSmokeRenderer {
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>, pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
pub(super) index_buffer: Option<VulkanAllocatedBuffer>, pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
pub(super) texture: Option<VulkanAllocatedImage>, pub(super) texture: Option<VulkanAllocatedImage>,
pub(super) index_count: u32, pub(super) draw_ranges: Vec<VulkanStaticDrawRange>,
pub(super) frame_sync: Vec<VulkanFrameSync>, pub(super) frame_sync: Vec<VulkanFrameSync>,
pub(super) images_in_flight: Vec<vk::Fence>, pub(super) images_in_flight: Vec<vk::Fence>,
pub(super) current_frame: usize, pub(super) current_frame: usize,
+5
View File
@@ -592,6 +592,7 @@ impl SmokeApp {
mesh_name: self.options.mesh_input.name(), mesh_name: self.options.mesh_input.name(),
mesh_vertex_count: self.mesh.vertices.len(), mesh_vertex_count: self.mesh.vertices.len(),
mesh_index_count: self.mesh.indices.len(), mesh_index_count: self.mesh.indices.len(),
mesh_draw_count: self.mesh.draw_ranges.len(),
texture_source: self texture_source: self
.options .options
.texture_input .texture_input
@@ -823,6 +824,7 @@ fn render_timeout_failure_report(
mesh_name: options.mesh_input.name(), mesh_name: options.mesh_input.name(),
mesh_vertex_count: 0, mesh_vertex_count: 0,
mesh_index_count: 0, mesh_index_count: 0,
mesh_draw_count: 0,
texture_source: options texture_source: options
.texture_input .texture_input
.as_ref() .as_ref()
@@ -1065,6 +1067,7 @@ struct SmokeReport<'a> {
mesh_name: &'a str, mesh_name: &'a str,
mesh_vertex_count: usize, mesh_vertex_count: usize,
mesh_index_count: usize, mesh_index_count: usize,
mesh_draw_count: usize,
texture_source: &'a str, texture_source: &'a str,
texture_archive: &'a str, texture_archive: &'a str,
texture_name: &'a str, texture_name: &'a str,
@@ -1524,6 +1527,7 @@ mod tests {
mesh_name: "MTCHECK.MSH", mesh_name: "MTCHECK.MSH",
mesh_vertex_count: 128, mesh_vertex_count: 128,
mesh_index_count: 252, mesh_index_count: 252,
mesh_draw_count: 1,
texture_source: "original-texm", texture_source: "original-texm",
texture_archive: "Textures.lib", texture_archive: "Textures.lib",
texture_name: "DEFAULT.0", texture_name: "DEFAULT.0",
@@ -1554,6 +1558,7 @@ mod tests {
assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\"")); assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\""));
assert!(json.contains("\"runner_architecture\": \"x86_64\"")); assert!(json.contains("\"runner_architecture\": \"x86_64\""));
assert!(json.contains("\"mesh_source\": \"original-msh\"")); assert!(json.contains("\"mesh_source\": \"original-msh\""));
assert!(json.contains("\"mesh_draw_count\": 1"));
assert!(json.contains("\"texture_source\": \"original-texm\"")); assert!(json.contains("\"texture_source\": \"original-texm\""));
} }
+1 -1
View File
@@ -29,7 +29,7 @@
## Current repository status ## Current repository status
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge. Помимо direct TEXM input, `fparkan-vulkan-smoke --model-root <GOG_ROOT> --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root <GOG_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 <GOG_ROOT> --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root <GOG_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 поверх - `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх
synthetic window descriptor и `VulkanPlanningBackend`. synthetic window descriptor и `VulkanPlanningBackend`.
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan - `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
+1 -1
View File
@@ -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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_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-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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_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-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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_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-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 <GOG_ROOT> --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root <GOG_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 <GOG_ROOT> --model-archive fortif.rlb --model-name FR_L_MTP.msh --wear-root <GOG_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-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-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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; validation_warning_count=0 and validation_error_count=0 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 <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; validation_warning_count=0 and validation_error_count=0
1 # Acceptance coverage manifest.
298 S3-VK-TEXM-UPLOAD-001
299 S3-VK-DESCRIPTOR-001
300 S3-VK-PIPELINE-001
301 S3-VK-DRAW-MODEL-001
302 S3-VK-DRAW-TERRAIN-001
303 S3-VK-PIXEL-CAPTURE-001
304 S3-VK-VALIDATION-001