From 8296400cad77f842b1ba7f5ea871eee7ff045a19 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 09:15:23 +0400 Subject: [PATCH] fix(vulkan): validate readback formats --- apps/fparkan-vulkan-smoke/src/main.rs | 45 +++++++++++++++++++++++++-- docs/tomes/05-render.md | 3 ++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index 498febb..d4782ff 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -111,6 +111,13 @@ fn compare_readback_files( expected: &std::path::Path, actual: &std::path::Path, ) -> Result<(), String> { + let expected_format = readback_format_from_path(expected)?; + let actual_format = readback_format_from_path(actual)?; + if expected_format != actual_format { + return Err(format!( + "pixel readback format mismatch: expected={expected_format} actual={actual_format}" + )); + } let expected_bytes = std::fs::read(expected).map_err(|err| format!("{}: {err}", expected.display()))?; let actual_bytes = @@ -135,6 +142,34 @@ fn compare_readback_files( Ok(()) } +fn readback_format_from_path(path: &std::path::Path) -> Result { + let name = path + .file_name() + .and_then(std::ffi::OsStr::to_str) + .ok_or_else(|| { + format!( + "pixel readback artifact has no UTF-8 file name: {}", + path.display() + ) + })?; + let prefix = ".readback-vkformat-"; + let raw = name + .strip_suffix(".raw") + .and_then(|stem| stem.rsplit_once(prefix).map(|(_, raw)| raw)) + .ok_or_else(|| { + format!( + "pixel readback artifact lacks format identity: {}", + path.display() + ) + })?; + raw.parse::().map_err(|_| { + format!( + "pixel readback artifact has invalid format identity: {}", + path.display() + ) + }) +} + #[derive(Clone, Debug, Eq, PartialEq)] struct SmokeOptions { out: PathBuf, @@ -1894,8 +1929,8 @@ mod tests { fn readback_comparator_accepts_exact_bytes_and_reports_first_difference() { let root = std::env::temp_dir().join(format!("fparkan-readback-{}", std::process::id())); std::fs::create_dir_all(&root).expect("temp dir"); - let expected = root.join("expected.raw"); - let actual = root.join("actual.raw"); + let expected = root.join("expected.readback-vkformat-50.raw"); + let actual = root.join("actual.readback-vkformat-50.raw"); std::fs::write(&expected, [1_u8, 2, 3]).expect("expected"); std::fs::write(&actual, [1_u8, 2, 3]).expect("actual"); assert_eq!(compare_readback_files(&expected, &actual), Ok(())); @@ -1904,6 +1939,12 @@ mod tests { compare_readback_files(&expected, &actual), Err("pixel readback mismatch at byte 1: expected=2 actual=9".to_string()) ); + let incompatible = root.join("incompatible.readback-vkformat-37.raw"); + std::fs::write(&incompatible, [1_u8, 2, 3]).expect("incompatible actual"); + assert_eq!( + compare_readback_files(&expected, &incompatible), + Err("pixel readback format mismatch: expected=50 actual=37".to_string()) + ); std::fs::remove_dir_all(root).expect("cleanup"); } diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 0e2f411..8e35420 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -905,6 +905,9 @@ Smoke option `--expected-readback ` выполняет exact byte comparis readback и завершает run с первым differing byte или length mismatch. Fresh GOG run с предыдущим format-50 artifact прошёл этот gate. Это regression contract нашего static Vulkan viewer, не comparison с оригинальным renderer. +Comparator прежде сравнивает `vkformat` identity из имен обоих artifacts и +отклоняет mismatch до byte scan; raw blobs разных Vulkan formats нельзя +считать сопоставимыми без явной conversion contract. `Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28` сохраняет source triangle order, а его positions и packed UV0 попадают в тот же