feat: create Vulkan swapchain probe
This commit is contained in:
@@ -27,7 +27,10 @@
|
||||
//!
|
||||
//! This crate is the declared low-level Vulkan boundary.
|
||||
|
||||
use ash::{khr::surface, vk};
|
||||
use ash::{
|
||||
khr::{surface, swapchain},
|
||||
vk,
|
||||
};
|
||||
use fparkan_binary::{sha256, sha256_hex};
|
||||
use fparkan_platform::{NativeWindowHandles, RenderRequest};
|
||||
use fparkan_render::{
|
||||
@@ -323,6 +326,7 @@ pub struct VulkanRuntimeCapabilityProbe {
|
||||
/// Created Vulkan logical device probe.
|
||||
pub struct VulkanLogicalDeviceProbe {
|
||||
device: ash::Device,
|
||||
physical_device: vk::PhysicalDevice,
|
||||
/// Runtime capability report used for device selection.
|
||||
pub runtime: VulkanRuntimeCapabilityProbe,
|
||||
/// Deterministic logical device creation report.
|
||||
@@ -351,6 +355,32 @@ pub struct VulkanLogicalDeviceReport {
|
||||
pub enabled_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Created Vulkan swapchain probe.
|
||||
pub struct VulkanSwapchainProbe {
|
||||
loader: swapchain::Device,
|
||||
swapchain: vk::SwapchainKHR,
|
||||
/// Deterministic swapchain creation report.
|
||||
pub report: VulkanSwapchainReport,
|
||||
}
|
||||
|
||||
impl Drop for VulkanSwapchainProbe {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: The swapchain was created by this probe and is destroyed once during drop.
|
||||
unsafe { self.loader.destroy_swapchain(self.swapchain, None) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime swapchain creation report.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanSwapchainReport {
|
||||
/// Report schema version.
|
||||
pub schema: u32,
|
||||
/// Deterministic swapchain policy used for creation.
|
||||
pub plan: VulkanSwapchainPlan,
|
||||
/// Number of images returned by `vkGetSwapchainImagesKHR`.
|
||||
pub image_count: u32,
|
||||
}
|
||||
|
||||
/// Live Vulkan device/surface capability probe error.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum VulkanRuntimeCapabilityError {
|
||||
@@ -479,6 +509,44 @@ impl std::fmt::Display for VulkanLogicalDeviceError {
|
||||
|
||||
impl std::error::Error for VulkanLogicalDeviceError {}
|
||||
|
||||
/// Vulkan swapchain creation error.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum VulkanSwapchainProbeError {
|
||||
/// Surface capability query failed.
|
||||
SurfaceCapabilitiesFailed {
|
||||
/// Vulkan result.
|
||||
result: String,
|
||||
},
|
||||
/// Swapchain creation failed.
|
||||
CreateFailed {
|
||||
/// Vulkan result.
|
||||
result: String,
|
||||
},
|
||||
/// Swapchain image query failed.
|
||||
ImagesFailed {
|
||||
/// Vulkan result.
|
||||
result: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VulkanSwapchainProbeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::SurfaceCapabilitiesFailed { result } => {
|
||||
write!(f, "Vulkan surface capabilities query failed: {result}")
|
||||
}
|
||||
Self::CreateFailed { result } => {
|
||||
write!(f, "Vulkan swapchain creation failed: {result}")
|
||||
}
|
||||
Self::ImagesFailed { result } => {
|
||||
write!(f, "Vulkan swapchain image query failed: {result}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VulkanSwapchainProbeError {}
|
||||
|
||||
/// Builds a deterministic Vulkan surface plan from native window handles.
|
||||
///
|
||||
/// # Errors
|
||||
@@ -606,6 +674,7 @@ pub fn create_vulkan_logical_device_probe(
|
||||
let _present_queue = unsafe { device.get_device_queue(capability.present_queue_family, 0) };
|
||||
Ok(VulkanLogicalDeviceProbe {
|
||||
device,
|
||||
physical_device: selected.physical_device,
|
||||
report: VulkanLogicalDeviceReport {
|
||||
schema: 1,
|
||||
device_name: capability.device_name.clone(),
|
||||
@@ -617,6 +686,83 @@ pub fn create_vulkan_logical_device_probe(
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a Vulkan swapchain for the live logical device and surface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
|
||||
/// swapchain creation, or swapchain image enumeration fails.
|
||||
pub fn create_vulkan_swapchain_probe(
|
||||
instance: &VulkanInstanceProbe,
|
||||
surface: &VulkanSurfaceProbe,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
|
||||
let raw_capabilities = {
|
||||
// SAFETY: The physical device and surface are live query inputs and no handles are retained.
|
||||
unsafe {
|
||||
surface
|
||||
.loader
|
||||
.get_physical_device_surface_capabilities(device.physical_device, surface.surface)
|
||||
}
|
||||
}
|
||||
.map_err(
|
||||
|error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
|
||||
result: format!("{error:?}"),
|
||||
},
|
||||
)?;
|
||||
let plan = &device.runtime.swapchain;
|
||||
let queue_family_indices = unique_queue_families(
|
||||
device.runtime.capability.graphics_queue_family,
|
||||
device.runtime.capability.present_queue_family,
|
||||
);
|
||||
let sharing_mode = if queue_family_indices.len() > 1 {
|
||||
vk::SharingMode::CONCURRENT
|
||||
} else {
|
||||
vk::SharingMode::EXCLUSIVE
|
||||
};
|
||||
let create_info = vk::SwapchainCreateInfoKHR::default()
|
||||
.surface(surface.surface)
|
||||
.min_image_count(plan.image_count)
|
||||
.image_format(vk::Format::from_raw(plan.format.format))
|
||||
.image_color_space(vk::ColorSpaceKHR::from_raw(plan.format.color_space))
|
||||
.image_extent(vk::Extent2D {
|
||||
width: plan.extent.0,
|
||||
height: plan.extent.1,
|
||||
})
|
||||
.image_array_layers(1)
|
||||
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||
.image_sharing_mode(sharing_mode)
|
||||
.queue_family_indices(&queue_family_indices)
|
||||
.pre_transform(raw_capabilities.current_transform)
|
||||
.composite_alpha(select_composite_alpha(
|
||||
raw_capabilities.supported_composite_alpha,
|
||||
))
|
||||
.present_mode(vk::PresentModeKHR::from_raw(plan.present_mode))
|
||||
.clipped(true);
|
||||
let loader = swapchain::Device::new(&instance.instance, &device.device);
|
||||
// SAFETY: The create info references live instance/device/surface handles for this call.
|
||||
let swapchain = unsafe { loader.create_swapchain(&create_info, None) }.map_err(|error| {
|
||||
VulkanSwapchainProbeError::CreateFailed {
|
||||
result: format!("{error:?}"),
|
||||
}
|
||||
})?;
|
||||
// SAFETY: The swapchain was created above and the returned image handles are owned by it.
|
||||
let images = unsafe { loader.get_swapchain_images(swapchain) }.map_err(|error| {
|
||||
VulkanSwapchainProbeError::ImagesFailed {
|
||||
result: format!("{error:?}"),
|
||||
}
|
||||
})?;
|
||||
Ok(VulkanSwapchainProbe {
|
||||
loader,
|
||||
swapchain,
|
||||
report: VulkanSwapchainReport {
|
||||
schema: 1,
|
||||
plan: plan.clone(),
|
||||
image_count: images.len().try_into().unwrap_or(u32::MAX),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn select_live_device_candidate(
|
||||
instance: &VulkanInstanceProbe,
|
||||
surface: &VulkanSurfaceProbe,
|
||||
@@ -1665,6 +1811,18 @@ fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_composite_alpha(supported: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR {
|
||||
if supported.contains(vk::CompositeAlphaFlagsKHR::OPAQUE) {
|
||||
vk::CompositeAlphaFlagsKHR::OPAQUE
|
||||
} else if supported.contains(vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED) {
|
||||
vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED
|
||||
} else if supported.contains(vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED) {
|
||||
vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED
|
||||
} else {
|
||||
vk::CompositeAlphaFlagsKHR::INHERIT
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a deterministic swapchain recreation report.
|
||||
#[must_use]
|
||||
pub const fn swapchain_recreation_report(
|
||||
|
||||
@@ -15,8 +15,9 @@ use fparkan_platform::{NativeWindowHandles, WindowPort};
|
||||
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
|
||||
use fparkan_render_vulkan::{
|
||||
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
|
||||
probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
|
||||
VulkanInstanceProbe, VulkanLogicalDeviceProbe,
|
||||
create_vulkan_swapchain_probe, probe_vulkan_loader, triangle_shader_manifest,
|
||||
validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
|
||||
VulkanSwapchainProbe,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -424,7 +425,14 @@ impl VulkanBootstrapProbe {
|
||||
self.window_height.unwrap_or(1).max(1),
|
||||
),
|
||||
) {
|
||||
Ok(device) => self.record_logical_device_probe(&device),
|
||||
Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) {
|
||||
Ok(swapchain) => self.record_swapchain_probe(&device, &swapchain),
|
||||
Err(err) => {
|
||||
self.record_logical_device_probe(&device);
|
||||
self.swapchain_status = VulkanSwapchainStatus::Failed;
|
||||
self.swapchain_error = Some(err.to_string());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
self.device_status = VulkanDeviceStatus::Failed;
|
||||
self.device_error = Some(err.to_string());
|
||||
@@ -450,11 +458,22 @@ impl VulkanBootstrapProbe {
|
||||
.try_into()
|
||||
.unwrap_or(u32::MAX),
|
||||
);
|
||||
self.swapchain_status = VulkanSwapchainStatus::Planned;
|
||||
self.swapchain_width = Some(device.runtime.swapchain.extent.0);
|
||||
self.swapchain_height = Some(device.runtime.swapchain.extent.1);
|
||||
self.swapchain_image_count = Some(device.runtime.swapchain.image_count);
|
||||
}
|
||||
|
||||
fn record_swapchain_probe(
|
||||
&mut self,
|
||||
device: &VulkanLogicalDeviceProbe,
|
||||
swapchain: &VulkanSwapchainProbe,
|
||||
) {
|
||||
self.record_logical_device_probe(device);
|
||||
self.swapchain_status = VulkanSwapchainStatus::Created;
|
||||
self.swapchain_width = Some(swapchain.report.plan.extent.0);
|
||||
self.swapchain_height = Some(swapchain.report.plan.extent.1);
|
||||
self.swapchain_image_count = Some(swapchain.report.image_count);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -564,7 +583,7 @@ impl VulkanLogicalDeviceStatus {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum VulkanSwapchainStatus {
|
||||
Skipped,
|
||||
Planned,
|
||||
Created,
|
||||
Failed,
|
||||
}
|
||||
|
||||
@@ -572,7 +591,7 @@ impl VulkanSwapchainStatus {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Skipped => "skipped",
|
||||
Self::Planned => "planned",
|
||||
Self::Created => "created",
|
||||
Self::Failed => "failed",
|
||||
}
|
||||
}
|
||||
@@ -691,9 +710,9 @@ fn validate_smoke_options(
|
||||
"passed native smoke report requires created Vulkan logical device".to_string(),
|
||||
);
|
||||
}
|
||||
if bootstrap.swapchain_status != VulkanSwapchainStatus::Planned {
|
||||
if bootstrap.swapchain_status != VulkanSwapchainStatus::Created {
|
||||
return Err(
|
||||
"passed native smoke report requires planned Vulkan swapchain".to_string(),
|
||||
"passed native smoke report requires created Vulkan swapchain".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -953,7 +972,7 @@ mod tests {
|
||||
logical_device_present_queue_family: Some(0),
|
||||
logical_device_enabled_extension_count: Some(1),
|
||||
logical_device_error: None,
|
||||
swapchain_status: VulkanSwapchainStatus::Planned,
|
||||
swapchain_status: VulkanSwapchainStatus::Created,
|
||||
swapchain_width: Some(1280),
|
||||
swapchain_height: Some(720),
|
||||
swapchain_image_count: Some(3),
|
||||
@@ -1340,7 +1359,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_passed_without_planned_swapchain() {
|
||||
fn rejects_passed_without_created_swapchain() {
|
||||
let options = SmokeOptions::parse(&strings(&[
|
||||
"--platform",
|
||||
"linux",
|
||||
@@ -1365,11 +1384,11 @@ mod tests {
|
||||
&options,
|
||||
&VulkanBootstrapProbe {
|
||||
swapchain_status: VulkanSwapchainStatus::Failed,
|
||||
swapchain_error: Some("Vulkan swapchain has no surface format".to_string()),
|
||||
swapchain_error: Some("Vulkan swapchain creation failed".to_string()),
|
||||
..probe_fixture()
|
||||
},
|
||||
),
|
||||
Err("passed native smoke report requires planned Vulkan swapchain".to_string())
|
||||
Err("passed native smoke report requires created Vulkan swapchain".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +61,9 @@ S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_hos
|
||||
S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
||||
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
|
||||
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
|
||||
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_planned_swapchain
|
||||
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
|
||||
S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
|
||||
S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
||||
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
|
||||
|
||||
|
@@ -63,6 +63,7 @@
|
||||
`S0-VK-031`
|
||||
`S0-VK-032`
|
||||
`S0-VK-033`
|
||||
`S0-VK-034`
|
||||
`S0-LIMIT-001`
|
||||
`S0-LIMIT-002`
|
||||
`L1-P1-NRES-001`
|
||||
|
||||
+2
-2
@@ -1553,7 +1553,7 @@ fn validate_native_smoke_report(
|
||||
platform,
|
||||
report,
|
||||
"vulkan_swapchain_status",
|
||||
"planned",
|
||||
"created",
|
||||
failures,
|
||||
);
|
||||
expect_u64_at_least(platform, report, "frames", 300, failures);
|
||||
@@ -2292,7 +2292,7 @@ mod tests {
|
||||
"vulkan_logical_device_graphics_queue_family": 0,
|
||||
"vulkan_logical_device_present_queue_family": 0,
|
||||
"vulkan_logical_device_enabled_extension_count": 1,
|
||||
"vulkan_swapchain_status": "planned",
|
||||
"vulkan_swapchain_status": "created",
|
||||
"vulkan_swapchain_width": 1280,
|
||||
"vulkan_swapchain_height": 720,
|
||||
"vulkan_swapchain_image_count": 3
|
||||
|
||||
Reference in New Issue
Block a user