feat(vulkan): draw original terrain geometry
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 2m11s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 08:46:16 +04:00
parent 4feada1bf4
commit b1408b0907
9 changed files with 221 additions and 13 deletions
Generated
+1
View File
@@ -565,6 +565,7 @@ dependencies = [
"fparkan-msh", "fparkan-msh",
"fparkan-platform", "fparkan-platform",
"fparkan-render", "fparkan-render",
"fparkan-terrain-format",
"serde", "serde",
"serde_json", "serde_json",
] ]
+1 -1
View File
@@ -33,7 +33,7 @@ Open source проект с реализацией компонентов игр
- `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry. - `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry.
- `VulkanPlanningBackend` и `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. - `VulkanPlanningBackend` и `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей.
- `fparkan-viewer` пока является инспектором ассетов. Stage 3 GPU vertical slice для оригинального `MSH`/`Texm`/`WEAR`/`MAT0`/terrain еще не закрыт. - `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты.
- Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence). - Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence).
## Тестирование ## Тестирование
@@ -13,6 +13,7 @@ fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" } fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" }
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" } fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" } fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
fparkan-terrain-format = { path = "../../crates/fparkan-terrain-format", version = "0.1.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@@ -3,6 +3,7 @@
use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex}; use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
use fparkan_msh::ModelAsset; use fparkan_msh::ModelAsset;
use fparkan_render::LegacyPipelineState; use fparkan_render::LegacyPipelineState;
use fparkan_terrain_format::LandMeshDocument;
/// 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.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -132,10 +133,108 @@ pub fn project_msh_to_static_mesh(
}) })
} }
/// Projects validated `Land.msh` terrain geometry into the static Vulkan path.
///
/// This bridge preserves the source triangle order from `TerrainFace28` and
/// consumes its validated position and UV0 streams. It is deliberately a
/// geometry-only terrain slice: source slot/material selection, camera
/// transforms, fog and surface-mask shading need their own evidence before
/// they can be modeled as renderer state.
///
/// # Errors
///
/// Returns [`VulkanAssetMeshError`] if the terrain has no triangles, contains
/// non-finite positions, has no usable XZ extent, or references data outside
/// the current static Vulkan input contract.
pub fn project_land_msh_to_static_mesh(
terrain: &LandMeshDocument,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
let mut indices = Vec::with_capacity(
terrain
.faces
.len()
.checked_mul(3)
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
);
for face in &terrain.faces {
indices.extend(face.vertices);
}
if indices.is_empty() {
return Err(VulkanAssetMeshError::EmptyGeometry);
}
let (min_x, max_x, min_z, max_z) = static_mesh_xz_bounds(&terrain.positions, &indices)?;
let extent = (max_x - min_x).max(max_z - min_z);
if !extent.is_finite() || extent <= f32::EPSILON {
return Err(VulkanAssetMeshError::DegenerateViewExtent);
}
let center_x = (min_x + max_x) * 0.5;
let center_z = (min_z + max_z) * 0.5;
let scale = 1.6 / extent;
let vertices = terrain
.positions
.iter()
.enumerate()
.map(|(index, position)| VulkanStaticVertex {
position: [
(position[0] - center_x) * scale,
(position[2] - center_z) * scale,
],
color: [0.31, 0.58, 0.27],
uv: terrain.uv0.get(index).map_or(
[
(position[0] - center_x) / extent + 0.5,
(position[2] - center_z) / extent + 0.5,
],
|uv| [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0],
),
})
.collect();
Ok(VulkanStaticMesh {
vertices,
indices,
draw_ranges: vec![VulkanStaticDrawRange {
first_index: 0,
index_count: u32::try_from(terrain.faces.len())
.map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?
.checked_mul(3)
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
material_index: 0,
pipeline_state: LegacyPipelineState::default(),
alpha_test_reference: 0,
}],
})
}
fn static_mesh_xz_bounds(
positions: &[[f32; 3]],
indices: &[u16],
) -> Result<(f32, f32, f32, f32), VulkanAssetMeshError> {
let mut min_x = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut min_z = f32::INFINITY;
let mut max_z = f32::NEG_INFINITY;
for &index in indices {
let position = positions
.get(usize::from(index))
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
if !position.iter().all(|value| value.is_finite()) {
return Err(VulkanAssetMeshError::NonFinitePosition);
}
min_x = min_x.min(position[0]);
max_x = max_x.max(position[0]);
min_z = min_z.min(position[2]);
max_z = max_z.max(position[2]);
}
Ok((min_x, max_x, min_z, max_z))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use fparkan_msh::{Batch, ModelAsset}; use fparkan_msh::{Batch, ModelAsset};
use fparkan_terrain_format::{FullSurfaceMask, TerrainFace28, TerrainSlotTable};
fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset { fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset {
ModelAsset { ModelAsset {
@@ -247,4 +346,50 @@ mod tests {
] ]
); );
} }
#[test]
fn projects_land_faces_in_source_order_with_packed_uv0() {
let terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
header_raw: Vec::new(),
slots_raw: Vec::new(),
},
positions: vec![
[-2.0, 5.0, -1.0],
[2.0, 3.0, -1.0],
[-2.0, 9.0, 3.0],
[2.0, 1.0, 3.0],
],
normals: Vec::new(),
uv0: vec![[1024, -512], [0, 2048], [-1024, 512], [512, 0]],
accelerator: Vec::new(),
aux14: Vec::new(),
aux18: Vec::new(),
faces: vec![terrain_face([0, 1, 2]), terrain_face([1, 3, 2])],
};
let mesh = project_land_msh_to_static_mesh(&terrain).expect("representable terrain");
assert_eq!(mesh.indices, vec![0, 1, 2, 1, 3, 2]);
assert_eq!(mesh.draw_ranges.len(), 1);
assert_eq!(mesh.draw_ranges[0].index_count, 6);
assert_eq!(mesh.vertices[0].position, [-0.8, -0.8]);
assert_eq!(mesh.vertices[3].position, [0.8, 0.8]);
assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]);
assert_eq!(mesh.vertices[2].uv, [-1.0, 0.5]);
}
fn terrain_face(vertices: [u16; 3]) -> TerrainFace28 {
TerrainFace28 {
flags: FullSurfaceMask(0),
material_tag: 0,
aux_tag: 0,
vertices,
neighbors: [None; 3],
tail_raw: [0; 8],
raw: [0; 28],
}
}
} }
+3 -1
View File
@@ -39,7 +39,9 @@ mod swapchain;
mod swapchain_resources; mod swapchain_resources;
mod validation; mod validation;
pub use self::asset_mesh::{project_msh_to_static_mesh, VulkanAssetMeshError}; pub use self::asset_mesh::{
project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanAssetMeshError,
};
pub use self::capabilities::{ pub use self::capabilities::{
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request, probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
+46 -9
View File
@@ -14,10 +14,10 @@
use fparkan_platform::RenderRequest; use fparkan_platform::RenderRequest;
use fparkan_platform_winit::{window_native_handles, WinitWindowPlan}; use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{ use fparkan_render_vulkan::{
project_msh_to_static_mesh, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanSmokeBootstrapProgress,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanSmokeShutdownReport, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMaterial, VulkanStaticMesh,
VulkanValidationReport, VulkanStaticTexture, VulkanValidationReport,
}; };
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
@@ -179,6 +179,9 @@ enum MeshInput {
archive: String, archive: String,
name: String, name: String,
}, },
LandMsh {
path: PathBuf,
},
} }
impl MeshInput { impl MeshInput {
@@ -186,13 +189,14 @@ impl MeshInput {
match self { match self {
Self::SmokeTriangle => "synthetic-smoke-triangle", Self::SmokeTriangle => "synthetic-smoke-triangle",
Self::Msh { .. } => "original-msh", Self::Msh { .. } => "original-msh",
Self::LandMsh { .. } => "original-land-msh",
} }
} }
fn archive(&self) -> &str { fn archive(&self) -> &str {
match self { match self {
Self::SmokeTriangle => "",
Self::Msh { archive, .. } => archive, Self::Msh { archive, .. } => archive,
Self::SmokeTriangle | Self::LandMsh { .. } => "",
} }
} }
@@ -200,6 +204,7 @@ impl MeshInput {
match self { match self {
Self::SmokeTriangle => "", Self::SmokeTriangle => "",
Self::Msh { name, .. } => name, Self::Msh { name, .. } => name,
Self::LandMsh { .. } => "Land.msh",
} }
} }
} }
@@ -214,6 +219,7 @@ impl SmokeOptions {
let mut model_root = None; let mut model_root = None;
let mut model_archive = None; let mut model_archive = None;
let mut model_name = None; let mut model_name = None;
let mut terrain_path = None;
let mut texture_root = None; let mut texture_root = None;
let mut texture_archive = None; let mut texture_archive = None;
let mut texture_name = None; let mut texture_name = None;
@@ -273,6 +279,13 @@ impl SmokeOptions {
.ok_or_else(|| "--model-name requires a value".to_string())?, .ok_or_else(|| "--model-name requires a value".to_string())?,
); );
} }
"--terrain-path" => {
terrain_path = Some(
iter.next()
.map(PathBuf::from)
.ok_or_else(|| "--terrain-path requires a path".to_string())?,
);
}
"--texture-root" => { "--texture-root" => {
texture_root = Some( texture_root = Some(
iter.next() iter.next()
@@ -335,19 +348,21 @@ impl SmokeOptions {
if timeout_seconds == 0 { if timeout_seconds == 0 {
return Err("native smoke requires --timeout-seconds >= 1".to_string()); return Err("native smoke requires --timeout-seconds >= 1".to_string());
} }
let mesh_input = match (model_root, model_archive, model_name) { let mesh_input = match (terrain_path, model_root, model_archive, model_name) {
(None, None, None) => MeshInput::SmokeTriangle, (Some(path), None, None, None) => MeshInput::LandMsh { path },
(Some(root), Some(archive), Some(name)) => MeshInput::Msh { (None, None, None, None) => MeshInput::SmokeTriangle,
(None, Some(root), Some(archive), Some(name)) => MeshInput::Msh {
root, root,
archive, archive,
name, name,
}, },
_ => { (None, _, _, _) => {
return Err( return Err(
"--model-root, --model-archive and --model-name must be supplied together" "--model-root, --model-archive and --model-name must be supplied together"
.to_string(), .to_string(),
); );
} }
_ => return Err("--terrain-path cannot be combined with --model-*".to_string()),
}; };
let texture_input = match (texture_root, texture_archive, texture_name) { let texture_input = match (texture_root, texture_archive, texture_name) {
(None, None, None) => None, (None, None, None) => None,
@@ -414,6 +429,10 @@ impl SmokeOptions {
let model = fparkan_inspection::load_model_from_root(root, archive, name)?; let model = fparkan_inspection::load_model_from_root(root, archive, name)?;
project_msh_to_static_mesh(&model).map_err(|err| err.to_string()) project_msh_to_static_mesh(&model).map_err(|err| err.to_string())
} }
MeshInput::LandMsh { path } => {
let terrain = fparkan_inspection::load_land_msh_from_path(path)?;
project_land_msh_to_static_mesh(&terrain).map_err(|err| err.to_string())
}
} }
} }
@@ -1487,6 +1506,24 @@ mod tests {
)); ));
} }
#[test]
fn parses_original_land_msh_as_direct_terrain_input() {
let parsed = SmokeOptions::parse(&[
"--out".to_string(),
"target/report.json".to_string(),
"--terrain-path".to_string(),
"C:/GOG/DATA/MAPS/11/Land.msh".to_string(),
]);
assert!(matches!(
parsed,
Ok(SmokeOptions {
mesh_input: MeshInput::LandMsh { path },
..
}) if path == std::path::Path::new("C:/GOG/DATA/MAPS/11/Land.msh")
));
}
#[test] #[test]
fn rejects_partial_original_msh_input() { fn rejects_partial_original_msh_input() {
let parsed = SmokeOptions::parse(&[ let parsed = SmokeOptions::parse(&[
+14 -1
View File
@@ -29,7 +29,7 @@ use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
use fparkan_path::{normalize_relative, PathPolicy}; use fparkan_path::{normalize_relative, PathPolicy};
use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository}; use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_rsli::decode as decode_rsli; use fparkan_rsli::decode as decode_rsli;
use fparkan_terrain_format::{decode_land_map, decode_land_msh}; use fparkan_terrain_format::{decode_land_map, decode_land_msh, LandMeshDocument};
use fparkan_texm::decode_texm; use fparkan_texm::decode_texm;
use fparkan_vfs::{DirectoryVfs, Vfs}; use fparkan_vfs::{DirectoryVfs, Vfs};
use std::fs; use std::fs;
@@ -437,6 +437,19 @@ pub fn inspect_land_file(path: &Path, kind: LandFileKind) -> Result<MapInspectio
} }
} }
/// Loads and validates a standalone `Land.msh` file.
///
/// # Errors
///
/// Returns a human-readable error if the file cannot be read, decoded as `NRes`,
/// or decoded as the specialized terrain mesh format.
pub fn load_land_msh_from_path(path: &Path) -> Result<LandMeshDocument, String> {
let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
let document = decode_nres(Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible)
.map_err(|err| err.to_string())?;
decode_land_msh(&document).map_err(|err| err.to_string())
}
fn inspect_land_msh(document: &NresDocument) -> Result<MapInspection, String> { fn inspect_land_msh(document: &NresDocument) -> Result<MapInspection, String> {
let land_msh = decode_land_msh(document).map_err(|err| err.to_string())?; let land_msh = decode_land_msh(document).map_err(|err| err.to_string())?;
Ok(MapInspection { Ok(MapInspection {
+9
View File
@@ -859,6 +859,15 @@ dynamic material data и не входит в `PipelineKey`. Также не у
дополнительного дизассемблирования. Это частично реализованная compatibility дополнительного дизассемблирования. Это частично реализованная compatibility
boundary, а не заявление о готовой parity fixed-function state. boundary, а не заявление о готовой parity fixed-function state.
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
static vertex/index upload path. Для текущего диагностического viewer XZ bounds
нормализуются в clip-space, packed UV0 декодируется как signed `int16 / 1024`.
Один static draw range намеренно не трактует `material_tag`, surface mask, slots
или auxiliary streams как material/pipeline state: их связь с original terrain
renderer пока не доказана. GOG `DATA/MAPS/11/Land.msh` дал 6 458 vertices и
24 672 indices в 300-frame native Vulkan run без validation warning/error.
Выбор depth/stencil attachment отделён от renderer lifetime: Выбор depth/stencil attachment отделён от renderer lifetime:
`select_depth_stencil_attachment_format` применяет тот же фиксированный порядок `select_depth_stencil_attachment_format` применяет тот же фиксированный порядок
форматов, что и capability gate, к фактически поддерживаемому списку GPU. форматов, что и capability gate, к фактически поддерживаемому списку GPU.
+1 -1
View File
@@ -299,7 +299,7 @@ S3-VK-TEXM-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --
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 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-alpha-test-smoke.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; PipelineKey selects a deduplicated live Vulkan pipeline per static draw range; capability-selected depth attachment supports TestWrite/TestReadOnly; fragment alpha test receives per-range dynamic cutoff through push constants; baseline key is live-proven, while Batch20/MAT0 state/reference mapping remains an evidence gap S3-VK-PIPELINE-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-alpha-test-smoke.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; PipelineKey selects a deduplicated live Vulkan pipeline per static draw range; capability-selected depth attachment supports TestWrite/TestReadOnly; fragment alpha test receives per-range dynamic cutoff through push constants; baseline key is live-proven, while Batch20/MAT0 state/reference mapping remains an evidence gap
S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-fr-l-mtp-per-batch-materials.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; live Win32 Vulkan issues 237 source-preserving indexed batch draws and binds the deduplicated WEAR→MAT0 material descriptor selected by each Batch20.material_index (this corpus model has selector 0 only) S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-fr-l-mtp-per-batch-materials.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; live Win32 Vulkan issues 237 source-preserving indexed batch draws and binds the deduplicated WEAR→MAT0 material descriptor selected by each Batch20.material_index (this corpus model has selector 0 only)
S3-VK-DRAW-TERRAIN-001 blocked awaits Stage 3 terrain Vulkan draw path S3-VK-DRAW-TERRAIN-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-land-11-smoke.json --frames 300 --resize-frame 120 --timeout-seconds 90 --terrain-path <GOG_ROOT>/DATA/MAPS/11/Land.msh --texture-root <GOG_ROOT> --texture-archive Textures.lib --texture-name DEFAULT.0; validated Land.msh TerrainFace28 order becomes one live indexed Vulkan terrain draw (6458 vertices, 24672 indices); terrain material/slot/camera semantics remain an evidence gap
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
S3-GL-001 omitted legacy OpenGL adapters are outside the Windows-only Vulkan renderer scope S3-GL-001 omitted legacy OpenGL adapters are outside the Windows-only Vulkan renderer scope
1 # Acceptance coverage manifest.
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
305 S3-GL-001