diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-25 07:20:47 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-25 10:45:40 +0300 |
| commit | 5aff0b64e820728c9a3469445c496c38b63ec523 (patch) | |
| tree | 97fca8488f1b9d7bc420d7d3ff00d9dc61be3131 | |
| parent | 757a975d8cad6c526f7e96cff4518434917e00fc (diff) | |
| download | fparkan-5aff0b64e820728c9a3469445c496c38b63ec523.tar.xz fparkan-5aff0b64e820728c9a3469445c496c38b63ec523.zip | |
fix(vulkan-policy): gate requested depth formats
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 7 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/capabilities.rs | 87 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/runtime.rs | 32 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 24 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 4 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 37 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/policy.rs | 72 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 2 |
8 files changed, 234 insertions, 31 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index ece45e0..386cb68 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -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, diff --git a/adapters/fparkan-render-vulkan/src/ffi/capabilities.rs b/adapters/fparkan-render-vulkan/src/ffi/capabilities.rs index 94fb3a6..c0de225 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/capabilities.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/capabilities.rs @@ -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) +} + +/// 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( +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() +} diff --git a/adapters/fparkan-render-vulkan/src/ffi/runtime.rs b/adapters/fparkan-render-vulkan/src/ffi/runtime.rs index 134bf3e..555c08c 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/runtime.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/runtime.rs @@ -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( diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 02ea0d6..0274daf 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -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(); } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 836f82d..ea4a471 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -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. diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index c242d97..891789d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -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, }; @@ -288,6 +288,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); rejected.present_modes.clear(); @@ -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(), + ], } } diff --git a/adapters/fparkan-render-vulkan/src/policy.rs b/adapters/fparkan-render-vulkan/src/policy.rs index ef9f0c4..040cefb 100644 --- a/adapters/fparkan-render-vulkan/src/policy.rs +++ b/adapters/fparkan-render-vulkan/src/policy.rs @@ -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 + ), } } } @@ -316,6 +332,20 @@ impl std::error::Error for VulkanCapabilityError {} 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, diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index f9c144c..320eb59 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -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)), }) { |
