diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index baa678b..b789cf9 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -113,7 +113,7 @@ struct SmokeOptions { resize_frame: u32, timeout_seconds: u64, mesh_input: MeshInput, - texture_input: Option, + texture_input: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -123,6 +123,44 @@ struct ResourceInput { name: String, } +#[derive(Clone, Debug, Eq, PartialEq)] +struct WearMaterialInput { + root: PathBuf, + archive: String, + name: String, + material_index: u16, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum TextureInput { + Direct(ResourceInput), + WearMaterial(WearMaterialInput), +} + +impl TextureInput { + fn source(&self) -> &'static str { + match self { + Self::Direct(_) => "original-texm", + Self::WearMaterial(_) => "original-wear-mat0-texm", + } + } + + fn archive(&self) -> &str { + match self { + Self::Direct(input) => &input.archive, + Self::WearMaterial(_) => "Textures.lib", + } + } + + fn requested_name(&self) -> &str { + match self { + Self::Direct(input) => &input.name, + // The selected name comes from MAT0 at load time and is not a CLI request. + Self::WearMaterial(_) => "", + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] enum MeshInput { SmokeTriangle, @@ -169,6 +207,10 @@ impl SmokeOptions { let mut texture_root = None; let mut texture_archive = None; let mut texture_name = None; + let mut wear_root = None; + let mut wear_archive = None; + let mut wear_name = None; + let mut material_index = None; let mut iter = args.iter(); while let Some(arg) = iter.next() { match arg.as_str() { @@ -242,6 +284,35 @@ impl SmokeOptions { .ok_or_else(|| "--texture-name requires a value".to_string())?, ); } + "--wear-root" => { + wear_root = Some( + iter.next() + .map(PathBuf::from) + .ok_or_else(|| "--wear-root requires a path".to_string())?, + ); + } + "--wear-archive" => { + wear_archive = Some( + iter.next() + .cloned() + .ok_or_else(|| "--wear-archive requires a value".to_string())?, + ); + } + "--wear-name" => { + wear_name = Some( + iter.next() + .cloned() + .ok_or_else(|| "--wear-name requires a value".to_string())?, + ); + } + "--material-index" => { + material_index = Some( + iter.next() + .ok_or_else(|| "--material-index requires a value".to_string())? + .parse() + .map_err(|_| "--material-index must be a u16".to_string())?, + ); + } _ => return Err(format!("unknown native smoke option: {arg}")), } } @@ -270,11 +341,11 @@ impl SmokeOptions { }; let texture_input = match (texture_root, texture_archive, texture_name) { (None, None, None) => None, - (Some(root), Some(archive), Some(name)) => Some(ResourceInput { + (Some(root), Some(archive), Some(name)) => Some(TextureInput::Direct(ResourceInput { root, archive, name, - }), + })), _ => { return Err( "--texture-root, --texture-archive and --texture-name must be supplied together" @@ -282,13 +353,36 @@ impl SmokeOptions { ); } }; + let wear_material_input = match (wear_root, wear_archive, wear_name, material_index) { + (None, None, None, None) => None, + (Some(root), Some(archive), Some(name), Some(material_index)) => { + Some(TextureInput::WearMaterial(WearMaterialInput { + root, + archive, + name, + material_index, + })) + } + _ => { + return Err( + "--wear-root, --wear-archive, --wear-name and --material-index must be supplied together" + .to_string(), + ); + } + }; + if texture_input.is_some() && wear_material_input.is_some() { + return Err( + "manual --texture-* input cannot be combined with --wear-* material input" + .to_string(), + ); + } Ok(Self { out, frames, resize_frame, timeout_seconds, mesh_input, - texture_input, + texture_input: texture_input.or(wear_material_input), }) } @@ -306,26 +400,50 @@ impl SmokeOptions { } } - fn load_texture(&self) -> Result, String> { + fn load_texture(&self) -> Result, String> { self.texture_input.as_ref().map_or(Ok(None), |input| { - let image = fparkan_inspection::load_texture_mip0_rgba8_from_root( - &input.root, - &input.archive, - &input.name, - )?; - Ok(Some(VulkanStaticTexture { - width: image.width, - height: image.height, - rgba8: image.rgba8, + let (image, selected_name) = match input { + TextureInput::Direct(input) => ( + fparkan_inspection::load_texture_mip0_rgba8_from_root( + &input.root, + &input.archive, + &input.name, + )?, + input.name.clone(), + ), + TextureInput::WearMaterial(input) => { + let selected = + fparkan_inspection::load_wear_material_texture_mip0_rgba8_from_root( + &input.root, + &input.archive, + &input.name, + input.material_index, + )?; + (selected.image, selected.texture_name) + } + }; + Ok(Some(LoadedTexture { + texture: VulkanStaticTexture { + width: image.width, + height: image.height, + rgba8: image.rgba8, + }, + selected_name, })) }) } } +#[derive(Clone, Debug)] +struct LoadedTexture { + texture: VulkanStaticTexture, + selected_name: String, +} + struct SmokeApp { options: SmokeOptions, mesh: VulkanStaticMesh, - texture: Option, + texture: Option, completed: Arc, progress: Arc, window_id: Option, @@ -370,7 +488,7 @@ impl SmokeApp { fn new( options: SmokeOptions, mesh: VulkanStaticMesh, - texture: Option, + texture: Option, completed: Arc, progress: Arc, ) -> Self { @@ -478,19 +596,29 @@ impl SmokeApp { .options .texture_input .as_ref() - .map_or("none", |_| "original-texm"), + .map_or("none", TextureInput::source), texture_archive: self .options .texture_input .as_ref() - .map_or("", |input| input.archive.as_str()), - texture_name: self - .options - .texture_input + .map_or("", TextureInput::archive), + texture_name: self.texture.as_ref().map_or_else( + || { + self.options + .texture_input + .as_ref() + .map_or("", TextureInput::requested_name) + }, + |texture| texture.selected_name.as_str(), + ), + texture_width: self + .texture .as_ref() - .map_or("", |input| input.name.as_str()), - texture_width: self.texture.as_ref().map_or(0, |texture| texture.width), - texture_height: self.texture.as_ref().map_or(0, |texture| texture.height), + .map_or(0, |texture| texture.texture.width), + texture_height: self + .texture + .as_ref() + .map_or(0, |texture| texture.texture.height), shader_manifest_hash: renderer .as_ref() .map_or("", |snapshot| snapshot.report.shader_manifest_hash.as_str()), @@ -698,15 +826,15 @@ fn render_timeout_failure_report( texture_source: options .texture_input .as_ref() - .map_or("none", |_| "original-texm"), + .map_or("none", TextureInput::source), texture_archive: options .texture_input .as_ref() - .map_or("", |input| input.archive.as_str()), + .map_or("", TextureInput::archive), texture_name: options .texture_input .as_ref() - .map_or("", |input| input.name.as_str()), + .map_or("", TextureInput::requested_name), texture_width: 0, texture_height: 0, shader_manifest_hash: "", @@ -804,7 +932,7 @@ impl ApplicationHandler for SmokeApp { render_request: RenderRequest::conservative(), enable_validation: true, mesh: self.mesh.clone(), - texture: self.texture.clone(), + texture: self.texture.as_ref().map(|texture| texture.texture.clone()), bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)), }) { Ok(renderer) => renderer, @@ -1297,12 +1425,41 @@ mod tests { assert!(matches!( parsed, Ok(SmokeOptions { - texture_input: Some(ResourceInput { archive, name, .. }), + texture_input: Some(TextureInput::Direct(ResourceInput { archive, name, .. })), .. }) if archive == "Textures.lib" && name == "DEFAULT.0" )); } + #[test] + fn parses_original_wear_material_input_as_one_atomic_request() { + let parsed = SmokeOptions::parse(&[ + "--out".to_string(), + "target/report.json".to_string(), + "--wear-root".to_string(), + "C:/GOG".to_string(), + "--wear-archive".to_string(), + "system.rlb".to_string(), + "--wear-name".to_string(), + "MODEL.WEA".to_string(), + "--material-index".to_string(), + "7".to_string(), + ]); + + assert!(matches!( + parsed, + Ok(SmokeOptions { + texture_input: Some(TextureInput::WearMaterial(WearMaterialInput { + archive, + name, + material_index, + .. + })), + .. + }) if archive == "system.rlb" && name == "MODEL.WEA" && material_index == 7 + )); + } + #[test] fn rejects_deprecated_self_assertion_flags() { for flag in [ diff --git a/docs/rendering/renderer_truth_table.md b/docs/rendering/renderer_truth_table.md index eec54d3..ee6899e 100644 --- a/docs/rendering/renderer_truth_table.md +++ b/docs/rendering/renderer_truth_table.md @@ -8,7 +8,7 @@ | Path | Native window / swapchain | Draws pixels | Uses original assets | Acceptance class | Что доказывает | Чего не доказывает | | --- | --- | --- | --- | --- | --- | --- | -| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH plus sampled TEXM | `covered-gpu` for Stage 0 smoke and explicit MSH/TEXM/descriptor bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH indexed draw; TEXM RGBA8 staging upload, `SHADER_READ_ONLY_OPTIMAL`, sampler/descriptor binding and fragment sampling | Original material mapping, exact MSH UV scale, terrain, gameplay rendering | +| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH plus sampled TEXM | `covered-gpu` for Stage 0 smoke and explicit MSH/TEXM/descriptor bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH indexed draw; TEXM RGBA8 staging upload, sampler/descriptor binding, fragment sampling and one explicitly selected WEAR/MAT0 material path | Per-batch material selection, phase animation, lightmaps, terrain, gameplay rendering | | `VulkanPlanningBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Deterministic command validation, canonical capture, frame submission planning | Любой live GPU draw, pixel parity, validation-clean asset frame | | `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels | | `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels | @@ -29,7 +29,7 @@ ## Current repository status -- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge: `fparkan-vulkan-smoke --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane, выполняет real indexed draw и загружает decoded 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. Это не WEAR/MAT0, exact material, 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`, выполняет 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. - `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 1d6e955..02e40dd 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-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH; live Win32 Vulkan indexed MSH draw/present +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-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