diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-23 22:33:42 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-23 22:33:42 +0300 |
| commit | f15ea95bf296056a7cea26a29509d9a3ef185eb7 (patch) | |
| tree | d0bae03a75d524b0aca001be08e5449b95d6d5bd | |
| parent | 99bcbf388f08eaed16726bccedbda80353c207e3 (diff) | |
| download | fparkan-f15ea95bf296056a7cea26a29509d9a3ef185eb7.tar.xz fparkan-f15ea95bf296056a7cea26a29509d9a3ef185eb7.zip | |
feat: add Vulkan loader probe to smoke runner
| -rw-r--r-- | .github/workflows/ci.yml | 1 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 172 | ||||
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 3 | ||||
| -rw-r--r-- | fixtures/acceptance/stage_0_2_roadmap.md | 1 |
4 files changed, 167 insertions, 10 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a5e83e..b3aa187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: --platform "${{ matrix.smoke_platform }}" --out "target/fparkan/native-smoke/${{ runner.os }}.json" --status blocked + --probe-loader --reason "native Vulkan smoke runner is not enabled on this CI lane yet" - name: Upload acceptance evidence if: always() diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index dded309..bd7efe7 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -11,7 +11,9 @@ #![allow(clippy::print_stderr, clippy::print_stdout)] //! Native Vulkan smoke runner entrypoint. -use fparkan_render_vulkan::{triangle_shader_manifest, validate_shader_manifest}; +use fparkan_render_vulkan::{ + probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest, +}; use std::path::PathBuf; use std::process::Command; @@ -35,8 +37,9 @@ fn main() { fn run(args: &[String]) -> Result<String, String> { let options = SmokeOptions::parse(args)?; - validate_smoke_options(&options)?; - let report = render_smoke_report_json(&options)?; + let bootstrap = VulkanBootstrapProbe::run(&options); + validate_smoke_options(&options, &bootstrap)?; + let report = render_smoke_report_json(&options, &bootstrap)?; if let Some(parent) = options.out.parent() { std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; } @@ -53,6 +56,7 @@ struct SmokeOptions { frames: u32, resize_count: u32, validation_error_count: Option<u32>, + probe_loader: bool, reason: Option<String>, } @@ -64,6 +68,7 @@ impl SmokeOptions { let mut frames = 0; let mut resize_count = 0; let mut validation_error_count = None; + let mut probe_loader = false; let mut reason = None; let mut iter = args.iter(); while let Some(arg) = iter.next() { @@ -104,6 +109,9 @@ impl SmokeOptions { .ok_or_else(|| "--validation-error-count requires a value".to_string())?; validation_error_count = Some(parse_u32("--validation-error-count", value)?); } + "--probe-loader" => { + probe_loader = true; + } "--reason" => { let value = iter .next() @@ -120,6 +128,7 @@ impl SmokeOptions { frames, resize_count, validation_error_count, + probe_loader, reason, }) } @@ -131,6 +140,55 @@ fn parse_u32(name: &str, value: &str) -> Result<u32, String> { .map_err(|_| format!("invalid {name} value: {value}")) } +#[derive(Clone, Debug, Eq, PartialEq)] +struct VulkanBootstrapProbe { + loader_status: VulkanLoaderStatus, + instance_api: Option<String>, + error: Option<String>, +} + +impl VulkanBootstrapProbe { + fn run(options: &SmokeOptions) -> Self { + if !options.probe_loader { + return Self { + loader_status: VulkanLoaderStatus::Skipped, + instance_api: None, + error: None, + }; + } + + match probe_vulkan_loader() { + Ok(report) => Self { + loader_status: VulkanLoaderStatus::Available, + instance_api: Some(format_api_version(report.instance_api_version)), + error: None, + }, + Err(err) => Self { + loader_status: VulkanLoaderStatus::Unavailable, + instance_api: None, + error: Some(err.to_string()), + }, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum VulkanLoaderStatus { + Skipped, + Available, + Unavailable, +} + +impl VulkanLoaderStatus { + const fn as_str(self) -> &'static str { + match self { + Self::Skipped => "skipped", + Self::Available => "available", + Self::Unavailable => "unavailable", + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SmokePlatform { Windows, @@ -180,7 +238,10 @@ impl SmokeStatus { } } -fn validate_smoke_options(options: &SmokeOptions) -> Result<(), String> { +fn validate_smoke_options( + options: &SmokeOptions, + bootstrap: &VulkanBootstrapProbe, +) -> Result<(), String> { match options.status { SmokeStatus::Blocked => { if options @@ -205,12 +266,20 @@ fn validate_smoke_options(options: &SmokeOptions) -> Result<(), String> { "passed native smoke report requires --validation-error-count 0".to_string(), ); } + if bootstrap.loader_status != VulkanLoaderStatus::Available { + return Err( + "passed native smoke report requires successful --probe-loader".to_string(), + ); + } } } Ok(()) } -fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> { +fn render_smoke_report_json( + options: &SmokeOptions, + bootstrap: &VulkanBootstrapProbe, +) -> Result<String, String> { let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) .map_err(|err| format!("shader manifest: {err}"))?; let validation_error_count = options @@ -220,6 +289,14 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> { .reason .as_ref() .map_or_else(|| "null".to_string(), |value| json_string(value)); + let instance_api = bootstrap + .instance_api + .as_ref() + .map_or_else(|| "null".to_string(), |value| json_string(value)); + let bootstrap_error = bootstrap + .error + .as_ref() + .map_or_else(|| "null".to_string(), |value| json_string(value)); Ok(format!( concat!( "{{\n", @@ -232,6 +309,9 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> { " \"resize_count\": {},\n", " \"validation_error_count\": {},\n", " \"shader_manifest_hash\": \"{}\",\n", + " \"vulkan_loader_status\": \"{}\",\n", + " \"vulkan_instance_api\": {},\n", + " \"vulkan_bootstrap_error\": {},\n", " \"reason\": {}\n", "}}\n" ), @@ -244,10 +324,20 @@ fn render_smoke_report_json(options: &SmokeOptions) -> Result<String, String> { options.resize_count, validation_error_count, json_escape(&shader_manifest.manifest_hash), + bootstrap.loader_status.as_str(), + instance_api, + bootstrap_error, reason )) } +fn format_api_version(version: u32) -> String { + let major = version >> 22; + let minor = (version >> 12) & 0x03ff; + let patch = version & 0x0fff; + format!("{major}.{minor}.{patch}") +} + fn current_git_commit_sha() -> String { Command::new("git") .args(["rev-parse", "HEAD"]) @@ -300,14 +390,23 @@ mod tests { "target/native.json", "--status", "blocked", + "--probe-loader", "--reason", "runner unavailable", ]))?; assert_eq!(options.platform, SmokePlatform::Linux); assert_eq!(options.status, SmokeStatus::Blocked); + assert!(options.probe_loader); assert_eq!(options.reason.as_deref(), Some("runner unavailable")); - validate_smoke_options(&options) + validate_smoke_options( + &options, + &VulkanBootstrapProbe { + loader_status: VulkanLoaderStatus::Unavailable, + instance_api: None, + error: Some("Vulkan loader is unavailable".to_string()), + }, + ) } #[test] @@ -329,13 +428,51 @@ mod tests { .expect("options"); assert_eq!( - validate_smoke_options(&options), + validate_smoke_options( + &options, + &VulkanBootstrapProbe { + loader_status: VulkanLoaderStatus::Available, + instance_api: Some("1.3.0".to_string()), + error: None, + }, + ), Err("passed native smoke report requires --frames >= 300".to_string()) ); } #[test] - fn blocked_report_includes_shader_manifest_hash() -> Result<(), String> { + fn rejects_passed_without_loader_probe() { + let options = SmokeOptions::parse(&strings(&[ + "--platform", + "linux", + "--out", + "target/native.json", + "--status", + "passed", + "--frames", + "300", + "--resize-count", + "1", + "--validation-error-count", + "0", + ])) + .expect("options"); + + assert_eq!( + validate_smoke_options( + &options, + &VulkanBootstrapProbe { + loader_status: VulkanLoaderStatus::Skipped, + instance_api: None, + error: None, + }, + ), + Err("passed native smoke report requires successful --probe-loader".to_string()) + ); + } + + #[test] + fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> { let options = SmokeOptions::parse(&strings(&[ "--platform", "macos", @@ -347,13 +484,30 @@ mod tests { "runner unavailable", ]))?; - let json = render_smoke_report_json(&options)?; + let json = render_smoke_report_json( + &options, + &VulkanBootstrapProbe { + loader_status: VulkanLoaderStatus::Unavailable, + instance_api: None, + error: Some("Vulkan loader is unavailable: dlopen failed".to_string()), + }, + )?; assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\"")); assert!(json.contains("\"platform\": \"macos\"")); assert!(json.contains("\"status\": \"blocked\"")); assert!(json.contains("\"shader_manifest_hash\": \"")); + assert!(json.contains("\"vulkan_loader_status\": \"unavailable\"")); + assert!(json.contains("\"vulkan_instance_api\": null")); + assert!(json.contains( + "\"vulkan_bootstrap_error\": \"Vulkan loader is unavailable: dlopen failed\"" + )); assert!(json.contains("\"reason\": \"runner unavailable\"")); Ok(()) } + + #[test] + fn formats_vulkan_api_version() { + assert_eq!(format_api_version((1 << 22) | (3 << 12) | 280), "1.3.280"); + } } diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 4c39deb..45cdf67 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -50,7 +50,8 @@ S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_ S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents -S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_hash +S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status +S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md index baa33b6..619104d 100644 --- a/fixtures/acceptance/stage_0_2_roadmap.md +++ b/fixtures/acceptance/stage_0_2_roadmap.md @@ -51,6 +51,7 @@ `S0-VK-021` `S0-VK-022` `S0-VK-023` +`S0-VK-024` `S0-LIMIT-001` `S0-LIMIT-002` `L1-P1-NRES-001` |
