feat: probe Vulkan loader boundary

This commit is contained in:
2026-06-23 22:43:54 +04:00
parent 69c032acca
commit 8ea1fd5c18
4 changed files with 132 additions and 3 deletions
+18 -2
View File
@@ -11,5 +11,21 @@ ash-window = "0.13"
fparkan-platform = { path = "../../crates/fparkan-platform" } fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-render = { path = "../../crates/fparkan-render" } fparkan-render = { path = "../../crates/fparkan-render" }
[lints] [lints.rust]
workspace = true unsafe_code = "allow"
missing_docs = "warn"
unreachable_pub = "warn"
unused_must_use = "deny"
[lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
dbg_macro = "deny"
print_stdout = "warn"
print_stderr = "warn"
lossy_float_literal = "deny"
+110 -1
View File
@@ -1,4 +1,4 @@
#![forbid(unsafe_code)] #![allow(unsafe_code)]
#![cfg_attr( #![cfg_attr(
test, test,
allow( allow(
@@ -32,6 +32,7 @@ use fparkan_platform::RenderRequest;
use fparkan_render::{ use fparkan_render::{
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
}; };
use std::ffi::CStr;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
/// Minimum Vulkan API version accepted by the Stage 0 backend. /// Minimum Vulkan API version accepted by the Stage 0 backend.
@@ -39,6 +40,87 @@ pub const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain"; const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset"; const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset";
/// Deterministic Vulkan loader probe report.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanLoaderProbeReport {
/// Report schema version.
pub schema: u32,
/// Whether the Vulkan loader was opened successfully.
pub loader_available: bool,
/// Reported loader instance API version.
pub instance_api_version: u32,
}
/// Vulkan loader bootstrap error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanLoaderError {
/// The Vulkan loader library could not be opened.
Unavailable {
/// Loader error text.
message: String,
},
}
impl std::fmt::Display for VulkanLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable { message } => {
write!(f, "Vulkan loader is unavailable: {message}")
}
}
}
}
impl std::error::Error for VulkanLoaderError {}
/// Opens the Vulkan loader and reports the supported instance API version.
///
/// # Errors
///
/// Returns [`VulkanLoaderError`] when no Vulkan loader library can be opened on
/// the host.
pub fn probe_vulkan_loader() -> Result<VulkanLoaderProbeReport, VulkanLoaderError> {
// SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
let entry = unsafe { ash::Entry::load() }.map_err(|error| VulkanLoaderError::Unavailable {
message: error.to_string(),
})?;
// SAFETY: The resolved entry only queries the loader-supported instance API version.
let version = unsafe { entry.try_enumerate_instance_version() }
.map_err(|error| VulkanLoaderError::Unavailable {
message: error.to_string(),
})?
.unwrap_or(vk::API_VERSION_1_0);
Ok(VulkanLoaderProbeReport {
schema: 1,
loader_available: true,
instance_api_version: version,
})
}
/// Returns the static Vulkan entry name used by loader probes.
#[must_use]
pub fn vulkan_entry_symbol_name() -> &'static CStr {
c"vkGetInstanceProcAddr"
}
/// Renders a deterministic JSON Vulkan loader report.
#[must_use]
pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&report.schema.to_string());
out.push_str(",\"loader_available\":");
out.push_str(if report.loader_available {
"true"
} else {
"false"
});
out.push_str(",\"instance_api\":\"");
out.push_str(&format_api_version(report.instance_api_version));
out.push_str("\"}");
out
}
/// Vulkan backend migration readiness. /// Vulkan backend migration readiness.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VulkanBackendState { pub enum VulkanBackendState {
@@ -641,6 +723,33 @@ mod tests {
); );
} }
#[test]
fn loader_probe_report_json_is_stable() {
assert_eq!(
vulkan_entry_symbol_name().to_bytes(),
b"vkGetInstanceProcAddr"
);
assert_eq!(
render_loader_probe_report_json(&VulkanLoaderProbeReport {
schema: 1,
loader_available: true,
instance_api_version: vk::API_VERSION_1_2,
}),
"{\"schema\":1,\"loader_available\":true,\"instance_api\":\"1.2.0\"}"
);
}
#[test]
fn loader_error_display_is_actionable() {
assert_eq!(
VulkanLoaderError::Unavailable {
message: "dlopen failed".to_string(),
}
.to_string(),
"Vulkan loader is unavailable: dlopen failed"
);
}
fn device( fn device(
name: &str, name: &str,
device_type: VulkanDeviceType, device_type: VulkanDeviceType,
+2
View File
@@ -26,6 +26,8 @@ S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_i
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
S0-VK-005 covered cargo test -p fparkan-render-vulkan --offline capability_report_json_is_stable S0-VK-005 covered cargo test -p fparkan-render-vulkan --offline capability_report_json_is_stable
S0-VK-006 covered cargo test -p fparkan-render-vulkan --offline loader_probe_report_json_is_stable
S0-VK-007 covered cargo xtask policy
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.
26 S0-VK-003
27 S0-VK-004
28 S0-VK-005
29 S0-VK-006
30 S0-VK-007
31 S0-LIMIT-001
32 S0-LIMIT-002
33 L1-P1-NRES-001
+2
View File
@@ -26,6 +26,8 @@
`S0-VK-003` `S0-VK-003`
`S0-VK-004` `S0-VK-004`
`S0-VK-005` `S0-VK-005`
`S0-VK-006`
`S0-VK-007`
`S0-LIMIT-001` `S0-LIMIT-001`
`S0-LIMIT-002` `S0-LIMIT-002`
`L1-P1-NRES-001` `L1-P1-NRES-001`