feat: add Vulkan surface preflight to smoke runner

This commit is contained in:
2026-06-23 23:38:09 +04:00
parent 71ead678c0
commit aa2133d82b
4 changed files with 135 additions and 3 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ jobs:
--platform "${{ matrix.smoke_platform }}" --platform "${{ matrix.smoke_platform }}"
--out "target/fparkan/native-smoke/${{ runner.os }}.json" --out "target/fparkan/native-smoke/${{ runner.os }}.json"
--status blocked --status blocked
--probe-instance --probe-surface
--reason "native Vulkan smoke runner is not enabled on this CI lane yet" --reason "native Vulkan smoke runner is not enabled on this CI lane yet"
- name: Upload acceptance evidence - name: Upload acceptance evidence
if: always() if: always()
+132 -2
View File
@@ -12,8 +12,8 @@
//! Native Vulkan smoke runner entrypoint. //! Native Vulkan smoke runner entrypoint.
use fparkan_render_vulkan::{ use fparkan_render_vulkan::{
create_vulkan_instance_probe, probe_vulkan_loader, triangle_shader_manifest, create_vulkan_instance_probe, plan_vulkan_surface, probe_vulkan_loader,
validate_shader_manifest, VulkanInstanceConfig, triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
}; };
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@@ -59,6 +59,7 @@ struct SmokeOptions {
validation_error_count: Option<u32>, validation_error_count: Option<u32>,
probe_loader: bool, probe_loader: bool,
probe_instance: bool, probe_instance: bool,
probe_surface: bool,
reason: Option<String>, reason: Option<String>,
} }
@@ -72,6 +73,7 @@ impl SmokeOptions {
let mut validation_error_count = None; let mut validation_error_count = None;
let mut probe_loader = false; let mut probe_loader = false;
let mut probe_instance = false; let mut probe_instance = false;
let mut probe_surface = false;
let mut reason = None; let mut reason = None;
let mut iter = args.iter(); let mut iter = args.iter();
while let Some(arg) = iter.next() { while let Some(arg) = iter.next() {
@@ -119,6 +121,11 @@ impl SmokeOptions {
probe_loader = true; probe_loader = true;
probe_instance = true; probe_instance = true;
} }
"--probe-surface" => {
probe_loader = true;
probe_instance = true;
probe_surface = true;
}
"--reason" => { "--reason" => {
let value = iter let value = iter
.next() .next()
@@ -137,6 +144,7 @@ impl SmokeOptions {
validation_error_count, validation_error_count,
probe_loader, probe_loader,
probe_instance, probe_instance,
probe_surface,
reason, reason,
}) })
} }
@@ -156,6 +164,8 @@ struct VulkanBootstrapProbe {
instance_status: VulkanInstanceStatus, instance_status: VulkanInstanceStatus,
instance_error: Option<String>, instance_error: Option<String>,
portability_enumeration: bool, portability_enumeration: bool,
surface_status: VulkanSurfaceStatus,
surface_error: Option<String>,
} }
impl VulkanBootstrapProbe { impl VulkanBootstrapProbe {
@@ -168,6 +178,8 @@ impl VulkanBootstrapProbe {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}; };
} }
@@ -179,6 +191,8 @@ impl VulkanBootstrapProbe {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}, },
Err(err) => Self { Err(err) => Self {
loader_status: VulkanLoaderStatus::Unavailable, loader_status: VulkanLoaderStatus::Unavailable,
@@ -187,6 +201,8 @@ impl VulkanBootstrapProbe {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}, },
}; };
@@ -204,6 +220,17 @@ impl VulkanBootstrapProbe {
} }
} }
} }
if options.probe_surface && probe.instance_status == VulkanInstanceStatus::Created {
match plan_vulkan_surface(None) {
Ok(_) => {
probe.surface_status = VulkanSurfaceStatus::Planned;
}
Err(err) => {
probe.surface_status = VulkanSurfaceStatus::MissingWindowHandles;
probe.surface_error = Some(err.to_string());
}
}
}
probe probe
} }
} }
@@ -242,6 +269,23 @@ impl VulkanInstanceStatus {
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VulkanSurfaceStatus {
Skipped,
Planned,
MissingWindowHandles,
}
impl VulkanSurfaceStatus {
const fn as_str(self) -> &'static str {
match self {
Self::Skipped => "skipped",
Self::Planned => "planned",
Self::MissingWindowHandles => "missing_window_handles",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SmokePlatform { enum SmokePlatform {
Windows, Windows,
@@ -329,6 +373,11 @@ fn validate_smoke_options(
"passed native smoke report requires successful --probe-instance".to_string(), "passed native smoke report requires successful --probe-instance".to_string(),
); );
} }
if bootstrap.surface_status != VulkanSurfaceStatus::Planned {
return Err(
"passed native smoke report requires successful --probe-surface".to_string(),
);
}
} }
} }
Ok(()) Ok(())
@@ -359,6 +408,10 @@ fn render_smoke_report_json(
.instance_error .instance_error
.as_ref() .as_ref()
.map_or_else(|| "null".to_string(), |value| json_string(value)); .map_or_else(|| "null".to_string(), |value| json_string(value));
let surface_error = bootstrap
.surface_error
.as_ref()
.map_or_else(|| "null".to_string(), |value| json_string(value));
Ok(format!( Ok(format!(
concat!( concat!(
"{{\n", "{{\n",
@@ -377,6 +430,8 @@ fn render_smoke_report_json(
" \"vulkan_instance_status\": \"{}\",\n", " \"vulkan_instance_status\": \"{}\",\n",
" \"vulkan_instance_error\": {},\n", " \"vulkan_instance_error\": {},\n",
" \"vulkan_portability_enumeration\": {},\n", " \"vulkan_portability_enumeration\": {},\n",
" \"vulkan_surface_status\": \"{}\",\n",
" \"vulkan_surface_error\": {},\n",
" \"reason\": {}\n", " \"reason\": {}\n",
"}}\n" "}}\n"
), ),
@@ -399,6 +454,8 @@ fn render_smoke_report_json(
} else { } else {
"false" "false"
}, },
bootstrap.surface_status.as_str(),
surface_error,
reason reason
)) ))
} }
@@ -480,6 +537,8 @@ mod tests {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}, },
) )
} }
@@ -512,6 +571,8 @@ mod tests {
instance_status: VulkanInstanceStatus::Created, instance_status: VulkanInstanceStatus::Created,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Planned,
surface_error: None,
}, },
), ),
Err("passed native smoke report requires --frames >= 300".to_string()) Err("passed native smoke report requires --frames >= 300".to_string())
@@ -546,6 +607,8 @@ mod tests {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}, },
), ),
Err("passed native smoke report requires successful --probe-loader".to_string()) Err("passed native smoke report requires successful --probe-loader".to_string())
@@ -581,12 +644,51 @@ mod tests {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: false, portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
}, },
), ),
Err("passed native smoke report requires successful --probe-instance".to_string()) Err("passed native smoke report requires successful --probe-instance".to_string())
); );
} }
#[test]
fn rejects_passed_without_surface_probe() {
let options = SmokeOptions::parse(&strings(&[
"--platform",
"linux",
"--out",
"target/native.json",
"--status",
"passed",
"--frames",
"300",
"--resize-count",
"1",
"--validation-error-count",
"0",
"--probe-instance",
]))
.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::Created,
instance_error: None,
portability_enumeration: false,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
},
),
Err("passed native smoke report requires successful --probe-surface".to_string())
);
}
#[test] #[test]
fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> { fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> {
let options = SmokeOptions::parse(&strings(&[ let options = SmokeOptions::parse(&strings(&[
@@ -609,6 +711,11 @@ mod tests {
instance_status: VulkanInstanceStatus::Skipped, instance_status: VulkanInstanceStatus::Skipped,
instance_error: None, instance_error: None,
portability_enumeration: true, portability_enumeration: true,
surface_status: VulkanSurfaceStatus::MissingWindowHandles,
surface_error: Some(
"native window/display handles are required for Vulkan surface creation"
.to_string(),
),
}, },
)?; )?;
@@ -623,6 +730,10 @@ mod tests {
assert!(json.contains("\"vulkan_instance_status\": \"skipped\"")); assert!(json.contains("\"vulkan_instance_status\": \"skipped\""));
assert!(json.contains("\"vulkan_instance_error\": null")); assert!(json.contains("\"vulkan_instance_error\": null"));
assert!(json.contains("\"vulkan_portability_enumeration\": true")); assert!(json.contains("\"vulkan_portability_enumeration\": true"));
assert!(json.contains("\"vulkan_surface_status\": \"missing_window_handles\""));
assert!(json.contains(
"\"vulkan_surface_error\": \"native window/display handles are required for Vulkan surface creation\""
));
assert!(json.contains("\"reason\": \"runner unavailable\"")); assert!(json.contains("\"reason\": \"runner unavailable\""));
Ok(()) Ok(())
} }
@@ -641,6 +752,25 @@ mod tests {
assert!(options.probe_loader); assert!(options.probe_loader);
assert!(options.probe_instance); assert!(options.probe_instance);
assert!(!options.probe_surface);
Ok(())
}
#[test]
fn parses_surface_probe_as_instance_probe() -> Result<(), String> {
let options = SmokeOptions::parse(&strings(&[
"--platform",
"linux",
"--out",
"target/native.json",
"--probe-surface",
"--reason",
"runner unavailable",
]))?;
assert!(options.probe_loader);
assert!(options.probe_instance);
assert!(options.probe_surface);
Ok(()) Ok(())
} }
+1
View File
@@ -53,6 +53,7 @@ S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_r
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-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-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-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow 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 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 L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
1 # Acceptance coverage manifest.
53 S0-VK-023
54 S0-VK-024
55 S0-VK-025
56 S0-VK-026
57 S0-LIMIT-001
58 S0-LIMIT-002
59 L1-P1-NRES-001
+1
View File
@@ -53,6 +53,7 @@
`S0-VK-023` `S0-VK-023`
`S0-VK-024` `S0-VK-024`
`S0-VK-025` `S0-VK-025`
`S0-VK-026`
`S0-LIMIT-001` `S0-LIMIT-001`
`S0-LIMIT-002` `S0-LIMIT-002`
`L1-P1-NRES-001` `L1-P1-NRES-001`