fix(vulkan-policy): gate requested depth formats

This commit is contained in:
2026-06-25 08:20:47 +04:00
parent 757a975d8c
commit 5aff0b64e8
8 changed files with 234 additions and 31 deletions
+4 -3
View File
@@ -38,7 +38,8 @@ mod swapchain_resources;
mod validation;
pub use self::capabilities::{
probe_vulkan_runtime_capabilities, VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
};
pub use self::instance::{
create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
@@ -54,8 +55,8 @@ use self::resources::{
VulkanFrameSync,
};
pub use self::runtime::{
create_vulkan_logical_device_probe, VulkanLogicalDeviceError, VulkanLogicalDeviceProbe,
VulkanLogicalDeviceReport,
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanLogicalDeviceReport,
};
pub use self::smoke_types::{
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
@@ -1,11 +1,12 @@
#![allow(unsafe_code)]
use ash::vk;
use fparkan_platform::RenderRequest;
use std::ffi::CStr;
use super::{VulkanInstanceProbe, VulkanSurfaceProbe};
use crate::policy::{
compare_reports, plan_vulkan_swapchain, validate_device, VulkanCapabilityError,
compare_reports, plan_vulkan_swapchain, validate_device_for_request, VulkanCapabilityError,
VulkanCapabilityReport, VulkanDeviceType, VulkanPhysicalDeviceRecord, VulkanQueueFamily,
VulkanSurfaceFormat, VulkanSwapchainError, VulkanSwapchainPlan, VulkanSwapchainRequest,
VulkanSwapchainSurfaceCapabilities,
@@ -133,14 +134,42 @@ pub fn probe_vulkan_runtime_capabilities(
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
) -> Result<VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError> {
let selected = select_live_device_candidate(instance, surface, drawable_extent)?;
let selected = select_live_device_candidate_for_request(
instance,
surface,
drawable_extent,
&RenderRequest::conservative(),
)?;
Ok(selected.runtime)
}
pub(super) fn select_live_device_candidate(
/// Probes live Vulkan device, queue, surface and swapchain capabilities for a
/// specific Stage 0 render request.
///
/// # Errors
///
/// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface
/// capability queries, Stage 0 device selection, or swapchain planning fails.
pub fn probe_vulkan_runtime_capabilities_for_request(
instance: &VulkanInstanceProbe,
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
render_request: &RenderRequest,
) -> Result<VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError> {
let selected = select_live_device_candidate_for_request(
instance,
surface,
drawable_extent,
render_request,
)?;
Ok(selected.runtime)
}
pub(super) fn select_live_device_candidate_for_request(
instance: &VulkanInstanceProbe,
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
render_request: &RenderRequest,
) -> Result<SelectedLiveDevice, VulkanRuntimeCapabilityError> {
let devices = {
// SAFETY: The Vulkan instance is live for this query and no handles are retained.
@@ -151,13 +180,14 @@ pub(super) fn select_live_device_candidate(
let mut best: Option<LiveDeviceCandidate> = None;
let mut last_error = None;
for (index, device) in devices.iter().copied().enumerate() {
let candidate = match live_device_candidate(instance, surface, device, index) {
Ok(candidate) => candidate,
Err(err) => {
last_error = Some(err);
continue;
}
};
let candidate =
match live_device_candidate(instance, surface, device, index, render_request) {
Ok(candidate) => candidate,
Err(err) => {
last_error = Some(err);
continue;
}
};
match &best {
Some(existing)
if compare_reports(&candidate.capability, &existing.capability)
@@ -192,6 +222,7 @@ fn live_device_candidate(
surface: &VulkanSurfaceProbe,
device: vk::PhysicalDevice,
index: usize,
render_request: &RenderRequest,
) -> Result<LiveDeviceCandidate, VulkanRuntimeCapabilityError> {
let properties = {
// SAFETY: `device` was returned by this live instance and the result is copied by value.
@@ -210,6 +241,7 @@ fn live_device_candidate(
let surface_formats = live_surface_formats(surface, device, &name)?;
let present_modes = live_present_modes(surface, device, &name)?;
let surface_capabilities = live_surface_capabilities(surface, device, &name)?;
let supported_depth_stencil_formats = live_depth_stencil_formats(instance, device);
let queue_families = queue_properties
.iter()
.enumerate()
@@ -251,8 +283,10 @@ fn live_device_candidate(
surface_formats: surface_formats.clone(),
present_modes: present_modes.clone(),
surface_capabilities,
supported_depth_stencil_formats,
};
let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
let capability = validate_device_for_request(&record, render_request)
.map_err(VulkanRuntimeCapabilityError::Capability)?;
Ok(LiveDeviceCandidate {
physical_device: device,
capability,
@@ -403,3 +437,34 @@ pub(super) fn live_surface_capabilities(
supported_usage_flags: capabilities.supported_usage_flags.as_raw(),
})
}
fn live_depth_stencil_formats(
instance: &VulkanInstanceProbe,
device: vk::PhysicalDevice,
) -> Vec<i32> {
[
vk::Format::D16_UNORM,
vk::Format::X8_D24_UNORM_PACK32,
vk::Format::D32_SFLOAT,
vk::Format::S8_UINT,
vk::Format::D16_UNORM_S8_UINT,
vk::Format::D24_UNORM_S8_UINT,
vk::Format::D32_SFLOAT_S8_UINT,
]
.into_iter()
.filter(|format| {
let properties = {
// SAFETY: `device` belongs to `instance`; format-property queries copy data by value.
unsafe {
instance
.instance
.get_physical_device_format_properties(device, *format)
}
};
properties
.optimal_tiling_features
.contains(vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT)
})
.map(vk::Format::as_raw)
.collect()
}
@@ -1,10 +1,11 @@
#![allow(unsafe_code)]
use ash::vk;
use fparkan_platform::RenderRequest;
use std::ffi::CString;
use super::capabilities::{
select_live_device_candidate, unique_queue_families, VulkanRuntimeCapabilityError,
select_live_device_candidate_for_request, unique_queue_families, VulkanRuntimeCapabilityError,
VulkanRuntimeCapabilityProbe,
};
use super::{VulkanInstanceProbe, VulkanSurfaceProbe};
@@ -125,8 +126,33 @@ pub fn create_vulkan_logical_device_probe(
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
) -> Result<VulkanLogicalDeviceProbe, VulkanLogicalDeviceError> {
let selected = select_live_device_candidate(instance, surface, drawable_extent)
.map_err(VulkanLogicalDeviceError::Runtime)?;
create_vulkan_logical_device_probe_for_request(
instance,
surface,
drawable_extent,
&RenderRequest::conservative(),
)
}
/// Creates a Vulkan logical device for a specific Stage 0 render request.
///
/// # Errors
///
/// Returns [`VulkanLogicalDeviceError`] when runtime capability probing fails,
/// device extension names are invalid, or `vkCreateDevice` fails.
pub fn create_vulkan_logical_device_probe_for_request(
instance: &VulkanInstanceProbe,
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
render_request: &RenderRequest,
) -> Result<VulkanLogicalDeviceProbe, VulkanLogicalDeviceError> {
let selected = select_live_device_candidate_for_request(
instance,
surface,
drawable_extent,
render_request,
)
.map_err(VulkanLogicalDeviceError::Runtime)?;
let capability = &selected.runtime.capability;
let queue_priorities = [1.0_f32];
let queue_families = unique_queue_families(
+14 -10
View File
@@ -5,13 +5,13 @@ use ash::vk;
use super::{
create_command_pool, create_frame_sync, create_swapchain_resources,
create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer,
destroy_swapchain_resources, plan_vulkan_surface, VulkanAllocatedBuffer, VulkanInstanceConfig,
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport,
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
VulkanValidationReport,
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
VulkanAllocatedBuffer, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSurfaceProbe, VulkanSwapchainProbe,
VulkanSwapchainResources, VulkanValidationMessenger, VulkanValidationReport,
};
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
@@ -106,9 +106,13 @@ impl VulkanSmokeRenderer {
if let Some(progress) = bootstrap_progress {
progress.mark_surface_created();
}
let device =
create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
.map_err(VulkanSmokeRendererError::LogicalDevice)?;
let device = create_vulkan_logical_device_probe_for_request(
&instance,
&surface,
create_info.drawable_extent,
&create_info.render_request,
)
.map_err(VulkanSmokeRendererError::LogicalDevice)?;
if let Some(progress) = bootstrap_progress {
progress.mark_logical_device_created();
}
@@ -1,5 +1,5 @@
use ash::vk;
use fparkan_platform::NativeWindowHandles;
use fparkan_platform::{NativeWindowHandles, RenderRequest};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
@@ -20,6 +20,8 @@ pub struct VulkanSmokeRendererCreateInfo {
pub native_handles: NativeWindowHandles,
/// Initial drawable extent.
pub drawable_extent: (u32, u32),
/// Stage 0 render request used for capability gating.
pub render_request: RenderRequest,
/// Whether validation layers must be enabled.
pub enable_validation: bool,
/// Optional shared bootstrap progress tracker for failure evidence.
@@ -8,7 +8,7 @@ use crate::shader_manifest::{
TRIANGLE_VERTEX_SPIRV_PATH, TRIANGLE_VERTEX_VALIDATE_COMMAND,
};
use crate::*;
use fparkan_platform::RenderRequest;
use fparkan_platform::{DepthStencilSupport, RenderRequest};
use fparkan_render::{
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
};
@@ -287,6 +287,36 @@ fn rejects_missing_graphics_present_swapchain_and_format() {
));
}
#[test]
fn capability_gate_rejects_devices_without_requested_depth_stencil_support() {
let mut no_depth = device("No depth", VulkanDeviceType::DiscreteGpu, 0, true, false);
no_depth.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()];
assert!(matches!(
select_physical_device(&[no_depth]),
Err(VulkanCapabilityError::MissingDepthStencilFormat { .. })
));
}
#[test]
fn capability_gate_respects_request_specific_depth_profiles() {
let mut no_stencil = device("No stencil", VulkanDeviceType::DiscreteGpu, 0, true, false);
no_stencil.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()];
let relaxed_request = RenderRequest {
depth: DepthStencilSupport {
depth_bits: 32,
stencil_bits: 0,
},
..RenderRequest::conservative()
};
let report = select_physical_device_for_request(&[no_stencil], &relaxed_request)
.expect("selected device for depth-only request");
assert_eq!(report.device_name, "No stencil");
assert!(report.rejected_devices.is_empty());
}
#[test]
fn capability_report_json_is_stable() {
let mut rejected = device("Rejected", VulkanDeviceType::IntegratedGpu, 0, true, false);
@@ -660,6 +690,11 @@ fn device(
vk::PresentModeKHR::MAILBOX.as_raw(),
],
surface_capabilities: default_surface_capabilities(),
supported_depth_stencil_formats: vec![
vk::Format::D24_UNORM_S8_UINT.as_raw(),
vk::Format::D32_SFLOAT_S8_UINT.as_raw(),
vk::Format::D32_SFLOAT.as_raw(),
],
}
}
+70 -2
View File
@@ -1,4 +1,5 @@
use ash::vk;
use fparkan_platform::{DepthStencilSupport, RenderRequest};
use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError};
use serde::Serialize;
@@ -182,6 +183,8 @@ pub struct VulkanPhysicalDeviceRecord {
pub present_modes: Vec<i32>,
/// Surface capabilities accepted by the target surface.
pub surface_capabilities: VulkanSwapchainSurfaceCapabilities,
/// Depth/stencil attachment formats supported by the device.
pub supported_depth_stencil_formats: Vec<i32>,
}
impl VulkanPhysicalDeviceRecord {
@@ -270,6 +273,13 @@ pub enum VulkanCapabilityError {
/// Device name that failed validation.
device: String,
},
/// No compatible depth/stencil attachment format exists for the render request.
MissingDepthStencilFormat {
/// Device name that failed validation.
device: String,
/// Requested depth/stencil profile.
requested: DepthStencilSupport,
},
}
impl std::fmt::Display for VulkanCapabilityError {
@@ -301,6 +311,12 @@ impl std::fmt::Display for VulkanCapabilityError {
f,
"Vulkan device {device} surface does not support COLOR_ATTACHMENT usage"
),
Self::MissingDepthStencilFormat { device, requested } => write!(
f,
"Vulkan device {device} lacks a depth/stencil attachment format for {}-bit depth and {}-bit stencil",
requested.depth_bits,
requested.stencil_bits
),
}
}
}
@@ -315,6 +331,20 @@ impl std::error::Error for VulkanCapabilityError {}
/// API version, queue, swapchain-extension and surface-format requirements.
pub fn select_physical_device(
devices: &[VulkanPhysicalDeviceRecord],
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
select_physical_device_for_request(devices, &RenderRequest::conservative())
}
/// Selects a Vulkan physical device for a specific Stage 0 render request.
///
/// # Errors
///
/// Returns [`VulkanCapabilityError`] when no candidate satisfies the minimum
/// API version, queue, swapchain-extension, surface-format or depth/stencil
/// requirements for the requested profile.
pub fn select_physical_device_for_request(
devices: &[VulkanPhysicalDeviceRecord],
render_request: &RenderRequest,
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
if devices.is_empty() {
return Err(VulkanCapabilityError::NoPhysicalDevice);
@@ -324,7 +354,7 @@ pub fn select_physical_device(
let mut rejected_devices = Vec::new();
let mut last_error = None;
for device in devices {
let report = match validate_device(device) {
let report = match validate_device_for_request(device, render_request) {
Ok(report) => report,
Err(err) => {
rejected_devices.push(rejected_device_report(device, &err));
@@ -611,8 +641,9 @@ fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
}
}
pub(crate) fn validate_device(
pub(crate) fn validate_device_for_request(
device: &VulkanPhysicalDeviceRecord,
render_request: &RenderRequest,
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
if device.api_version < MIN_VULKAN_API_VERSION {
return Err(VulkanCapabilityError::ApiVersionTooLow {
@@ -640,6 +671,12 @@ pub(crate) fn validate_device(
device: device.name.clone(),
});
}
if !supports_depth_stencil_request(device, render_request.depth) {
return Err(VulkanCapabilityError::MissingDepthStencilFormat {
device: device.name.clone(),
requested: render_request.depth,
});
}
let (graphics_queue_family, present_queue_family) = select_queue_families(device)?;
let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION);
@@ -684,6 +721,7 @@ const fn capability_error_code(error: &VulkanCapabilityError) -> &'static str {
VulkanCapabilityError::MissingColorAttachmentUsage { .. } => {
"missing_color_attachment_usage"
}
VulkanCapabilityError::MissingDepthStencilFormat { .. } => "missing_depth_stencil_format",
}
}
@@ -728,6 +766,36 @@ fn supports_color_attachment_usage(capabilities: VulkanSwapchainSurfaceCapabilit
capabilities.supported_usage_flags & vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw() != 0
}
fn supports_depth_stencil_request(
device: &VulkanPhysicalDeviceRecord,
depth: DepthStencilSupport,
) -> bool {
if depth.depth_bits == 0 && depth.stencil_bits == 0 {
return true;
}
required_depth_stencil_formats(depth).iter().any(|format| {
device
.supported_depth_stencil_formats
.contains(&format.as_raw())
})
}
fn required_depth_stencil_formats(depth: DepthStencilSupport) -> &'static [vk::Format] {
match (depth.depth_bits, depth.stencil_bits) {
(0, 0) => &[],
(16, 0) => &[vk::Format::D16_UNORM, vk::Format::D32_SFLOAT],
(24, 0) => &[vk::Format::X8_D24_UNORM_PACK32, vk::Format::D32_SFLOAT],
(32, 0) => &[vk::Format::D32_SFLOAT],
(16, 8) => &[vk::Format::D16_UNORM_S8_UINT, vk::Format::D24_UNORM_S8_UINT],
(24, 8) => &[
vk::Format::D24_UNORM_S8_UINT,
vk::Format::D32_SFLOAT_S8_UINT,
],
(32, 8) => &[vk::Format::D32_SFLOAT_S8_UINT],
_ => &[],
}
}
fn score_device(
device: &VulkanPhysicalDeviceRecord,
graphics_queue_family: u32,
+2
View File
@@ -11,6 +11,7 @@
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! Native Vulkan smoke runner entrypoint.
use fparkan_platform::RenderRequest;
use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{
VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
@@ -524,6 +525,7 @@ impl ApplicationHandler for SmokeApp {
application_name: "fparkan-vulkan-smoke".to_string(),
native_handles,
drawable_extent: (size.width.max(1), size.height.max(1)),
render_request: RenderRequest::conservative(),
enable_validation: true,
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
}) {