aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 22:35:41 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 22:35:41 +0300
commit71ead678c05d2165fc0b6a291ca6d1e25c2a11e0 (patch)
treefbe72fe613d530eb121e9b30ab5644f462fe930c
parentf15ea95bf296056a7cea26a29509d9a3ef185eb7 (diff)
downloadfparkan-71ead678c05d2165fc0b6a291ca6d1e25c2a11e0.tar.xz
fparkan-71ead678c05d2165fc0b6a291ca6d1e25c2a11e0.zip
feat: add Vulkan instance probe to smoke runner
-rw-r--r--.github/workflows/ci.yml2
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs172
-rw-r--r--fixtures/acceptance/coverage.tsv1
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
4 files changed, 158 insertions, 18 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b3aa187..fd41ff0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,7 +77,7 @@ jobs:
--platform "${{ matrix.smoke_platform }}"
--out "target/fparkan/native-smoke/${{ runner.os }}.json"
--status blocked
- --probe-loader
+ --probe-instance
--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 bd7efe7..7e47581 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -12,7 +12,8 @@
//! Native Vulkan smoke runner entrypoint.
use fparkan_render_vulkan::{
- probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest,
+ create_vulkan_instance_probe, probe_vulkan_loader, triangle_shader_manifest,
+ validate_shader_manifest, VulkanInstanceConfig,
};
use std::path::PathBuf;
use std::process::Command;
@@ -57,6 +58,7 @@ struct SmokeOptions {
resize_count: u32,
validation_error_count: Option<u32>,
probe_loader: bool,
+ probe_instance: bool,
reason: Option<String>,
}
@@ -69,6 +71,7 @@ impl SmokeOptions {
let mut resize_count = 0;
let mut validation_error_count = None;
let mut probe_loader = false;
+ let mut probe_instance = false;
let mut reason = None;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
@@ -112,6 +115,10 @@ impl SmokeOptions {
"--probe-loader" => {
probe_loader = true;
}
+ "--probe-instance" => {
+ probe_loader = true;
+ probe_instance = true;
+ }
"--reason" => {
let value = iter
.next()
@@ -129,6 +136,7 @@ impl SmokeOptions {
resize_count,
validation_error_count,
probe_loader,
+ probe_instance,
reason,
})
}
@@ -144,7 +152,10 @@ fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
struct VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus,
instance_api: Option<String>,
- error: Option<String>,
+ loader_error: Option<String>,
+ instance_status: VulkanInstanceStatus,
+ instance_error: Option<String>,
+ portability_enumeration: bool,
}
impl VulkanBootstrapProbe {
@@ -153,22 +164,47 @@ impl VulkanBootstrapProbe {
return Self {
loader_status: VulkanLoaderStatus::Skipped,
instance_api: None,
- error: None,
+ loader_error: None,
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
};
}
- match probe_vulkan_loader() {
+ let mut probe = match probe_vulkan_loader() {
Ok(report) => Self {
loader_status: VulkanLoaderStatus::Available,
instance_api: Some(format_api_version(report.instance_api_version)),
- error: None,
+ loader_error: None,
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
},
Err(err) => Self {
loader_status: VulkanLoaderStatus::Unavailable,
instance_api: None,
- error: Some(err.to_string()),
+ loader_error: Some(err.to_string()),
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
},
+ };
+
+ if options.probe_instance && probe.loader_status == VulkanLoaderStatus::Available {
+ let config = VulkanInstanceConfig::smoke("fparkan-vulkan-smoke");
+ probe.portability_enumeration = config.enable_portability_enumeration;
+ match create_vulkan_instance_probe(&config) {
+ Ok(instance) => {
+ probe.instance_status = VulkanInstanceStatus::Created;
+ probe.portability_enumeration = instance.report.create_flags != 0;
+ }
+ Err(err) => {
+ probe.instance_status = VulkanInstanceStatus::Failed;
+ probe.instance_error = Some(err.to_string());
+ }
+ }
}
+ probe
}
}
@@ -190,6 +226,23 @@ impl VulkanLoaderStatus {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum VulkanInstanceStatus {
+ Skipped,
+ Created,
+ Failed,
+}
+
+impl VulkanInstanceStatus {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Skipped => "skipped",
+ Self::Created => "created",
+ Self::Failed => "failed",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SmokePlatform {
Windows,
Linux,
@@ -271,6 +324,11 @@ fn validate_smoke_options(
"passed native smoke report requires successful --probe-loader".to_string(),
);
}
+ if bootstrap.instance_status != VulkanInstanceStatus::Created {
+ return Err(
+ "passed native smoke report requires successful --probe-instance".to_string(),
+ );
+ }
}
}
Ok(())
@@ -293,8 +351,12 @@ fn render_smoke_report_json(
.instance_api
.as_ref()
.map_or_else(|| "null".to_string(), |value| json_string(value));
- let bootstrap_error = bootstrap
- .error
+ let loader_error = bootstrap
+ .loader_error
+ .as_ref()
+ .map_or_else(|| "null".to_string(), |value| json_string(value));
+ let instance_error = bootstrap
+ .instance_error
.as_ref()
.map_or_else(|| "null".to_string(), |value| json_string(value));
Ok(format!(
@@ -311,7 +373,10 @@ fn render_smoke_report_json(
" \"shader_manifest_hash\": \"{}\",\n",
" \"vulkan_loader_status\": \"{}\",\n",
" \"vulkan_instance_api\": {},\n",
- " \"vulkan_bootstrap_error\": {},\n",
+ " \"vulkan_loader_error\": {},\n",
+ " \"vulkan_instance_status\": \"{}\",\n",
+ " \"vulkan_instance_error\": {},\n",
+ " \"vulkan_portability_enumeration\": {},\n",
" \"reason\": {}\n",
"}}\n"
),
@@ -326,7 +391,14 @@ fn render_smoke_report_json(
json_escape(&shader_manifest.manifest_hash),
bootstrap.loader_status.as_str(),
instance_api,
- bootstrap_error,
+ loader_error,
+ bootstrap.instance_status.as_str(),
+ instance_error,
+ if bootstrap.portability_enumeration {
+ "true"
+ } else {
+ "false"
+ },
reason
))
}
@@ -404,7 +476,10 @@ mod tests {
&VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus::Unavailable,
instance_api: None,
- error: Some("Vulkan loader is unavailable".to_string()),
+ loader_error: Some("Vulkan loader is unavailable".to_string()),
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
},
)
}
@@ -433,7 +508,10 @@ mod tests {
&VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus::Available,
instance_api: Some("1.3.0".to_string()),
- error: None,
+ loader_error: None,
+ instance_status: VulkanInstanceStatus::Created,
+ instance_error: None,
+ portability_enumeration: false,
},
),
Err("passed native smoke report requires --frames >= 300".to_string())
@@ -464,7 +542,10 @@ mod tests {
&VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus::Skipped,
instance_api: None,
- error: None,
+ loader_error: None,
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
},
),
Err("passed native smoke report requires successful --probe-loader".to_string())
@@ -472,6 +553,41 @@ mod tests {
}
#[test]
+ fn rejects_passed_without_instance_probe() {
+ let options = SmokeOptions::parse(&strings(&[
+ "--platform",
+ "linux",
+ "--out",
+ "target/native.json",
+ "--status",
+ "passed",
+ "--frames",
+ "300",
+ "--resize-count",
+ "1",
+ "--validation-error-count",
+ "0",
+ "--probe-loader",
+ ]))
+ .expect("options");
+
+ assert_eq!(
+ validate_smoke_options(
+ &options,
+ &VulkanBootstrapProbe {
+ loader_status: VulkanLoaderStatus::Available,
+ instance_api: Some("1.3.0".to_string()),
+ loader_error: None,
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: false,
+ },
+ ),
+ Err("passed native smoke report requires successful --probe-instance".to_string())
+ );
+ }
+
+ #[test]
fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> {
let options = SmokeOptions::parse(&strings(&[
"--platform",
@@ -489,7 +605,10 @@ mod tests {
&VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus::Unavailable,
instance_api: None,
- error: Some("Vulkan loader is unavailable: dlopen failed".to_string()),
+ loader_error: Some("Vulkan loader is unavailable: dlopen failed".to_string()),
+ instance_status: VulkanInstanceStatus::Skipped,
+ instance_error: None,
+ portability_enumeration: true,
},
)?;
@@ -499,14 +618,33 @@ mod tests {
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("\"vulkan_loader_error\": \"Vulkan loader is unavailable: dlopen failed\""));
+ assert!(json.contains("\"vulkan_instance_status\": \"skipped\""));
+ assert!(json.contains("\"vulkan_instance_error\": null"));
+ assert!(json.contains("\"vulkan_portability_enumeration\": true"));
assert!(json.contains("\"reason\": \"runner unavailable\""));
Ok(())
}
#[test]
+ fn parses_instance_probe_as_loader_probe() -> Result<(), String> {
+ let options = SmokeOptions::parse(&strings(&[
+ "--platform",
+ "linux",
+ "--out",
+ "target/native.json",
+ "--probe-instance",
+ "--reason",
+ "runner unavailable",
+ ]))?;
+
+ assert!(options.probe_loader);
+ assert!(options.probe_instance);
+ 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 45cdf67..233572a 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -52,6 +52,7 @@ S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission
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_and_bootstrap_status
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
+S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
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 619104d..6ccfd72 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -52,6 +52,7 @@
`S0-VK-022`
`S0-VK-023`
`S0-VK-024`
+`S0-VK-025`
`S0-LIMIT-001`
`S0-LIMIT-002`
`L1-P1-NRES-001`