#![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_for_request, VulkanCapabilityError, VulkanCapabilityReport, VulkanDeviceLimits, VulkanDeviceType, VulkanPhysicalDeviceRecord, VulkanQueueFamily, VulkanSurfaceFormat, VulkanSwapchainError, VulkanSwapchainPlan, VulkanSwapchainRequest, VulkanSwapchainSurfaceCapabilities, }; /// Live Vulkan device/surface capability probe. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanRuntimeCapabilityProbe { /// Selected device/queue capability report. pub capability: VulkanCapabilityReport, /// Swapchain plan built from the selected device and live surface capabilities. pub swapchain: VulkanSwapchainPlan, } /// Live Vulkan device/surface capability probe error. #[derive(Clone, Debug, Eq, PartialEq)] pub enum VulkanRuntimeCapabilityError { /// Physical device enumeration failed. EnumerateDevicesFailed { /// Vulkan result. result: vk::Result, }, /// Device extension enumeration failed. EnumerateDeviceExtensionsFailed { /// Device name or index context. device: String, /// Vulkan result. result: vk::Result, }, /// Queue-family present support query failed. PresentSupportFailed { /// Device name. device: String, /// Queue-family index. queue_family: u32, /// Vulkan result. result: vk::Result, }, /// Surface format query failed. SurfaceFormatsFailed { /// Device name. device: String, /// Vulkan result. result: vk::Result, }, /// Surface capability query failed. SurfaceCapabilitiesFailed { /// Device name. device: String, /// Vulkan result. result: vk::Result, }, /// Present mode query failed. PresentModesFailed { /// Device name. device: String, /// Vulkan result. result: vk::Result, }, /// No device satisfied Stage 0 capability policy. Capability(VulkanCapabilityError), /// Live surface capabilities could not produce a swapchain plan. Swapchain(VulkanSwapchainError), } impl std::fmt::Display for VulkanRuntimeCapabilityError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EnumerateDevicesFailed { result } => { write!(f, "Vulkan physical device enumeration failed: {result:?}") } Self::EnumerateDeviceExtensionsFailed { device, result } => write!( f, "Vulkan device {device} extension enumeration failed: {result:?}" ), Self::PresentSupportFailed { device, queue_family, result, } => write!( f, "Vulkan device {device} queue family {queue_family} present support query failed: {result:?}" ), Self::SurfaceFormatsFailed { device, result } => write!( f, "Vulkan device {device} surface format query failed: {result:?}" ), Self::SurfaceCapabilitiesFailed { device, result } => write!( f, "Vulkan device {device} surface capabilities query failed: {result:?}" ), Self::PresentModesFailed { device, result } => write!( f, "Vulkan device {device} present mode query failed: {result:?}" ), Self::Capability(error) => write!(f, "{error}"), Self::Swapchain(error) => write!(f, "{error}"), } } } impl std::error::Error for VulkanRuntimeCapabilityError {} pub(super) struct SelectedLiveDevice { pub(super) physical_device: vk::PhysicalDevice, pub(super) runtime: VulkanRuntimeCapabilityProbe, } struct LiveDeviceCandidate { physical_device: vk::PhysicalDevice, capability: VulkanCapabilityReport, surface_formats: Vec, present_modes: Vec, surface_capabilities: VulkanSwapchainSurfaceCapabilities, } /// Probes live Vulkan device, queue, surface and swapchain capabilities. /// /// # Errors /// /// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface /// capability queries, Stage 0 device selection, or swapchain planning fails. pub fn probe_vulkan_runtime_capabilities( instance: &VulkanInstanceProbe, surface: &VulkanSurfaceProbe, drawable_extent: (u32, u32), ) -> Result { 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 { 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 { let devices = { // SAFETY: The Vulkan instance is live for this query and no handles are retained. unsafe { instance.instance.enumerate_physical_devices() }.map_err(|error| { VulkanRuntimeCapabilityError::EnumerateDevicesFailed { result: error } })? }; let mut best: Option = None; let mut last_error = None; for (index, device) in devices.iter().copied().enumerate() { 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) != std::cmp::Ordering::Greater => {} _ => best = Some(candidate), } } let best = best.ok_or_else(|| { last_error.unwrap_or(VulkanRuntimeCapabilityError::Capability( VulkanCapabilityError::NoPhysicalDevice, )) })?; let swapchain = plan_vulkan_swapchain(&VulkanSwapchainRequest { drawable_extent, formats: best.surface_formats, present_modes: best.present_modes, capabilities: best.surface_capabilities, preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(), }) .map_err(VulkanRuntimeCapabilityError::Swapchain)?; Ok(SelectedLiveDevice { physical_device: best.physical_device, runtime: VulkanRuntimeCapabilityProbe { capability: best.capability, swapchain, }, }) } fn live_device_candidate( instance: &VulkanInstanceProbe, surface: &VulkanSurfaceProbe, device: vk::PhysicalDevice, index: usize, render_request: &RenderRequest, ) -> Result { let properties = { // SAFETY: `device` was returned by this live instance and the result is copied by value. unsafe { instance.instance.get_physical_device_properties(device) } }; let name = physical_device_name(&properties, index); let queue_properties = { // SAFETY: `device` was returned by this live instance and the result is owned by Rust. unsafe { instance .instance .get_physical_device_queue_family_properties(device) } }; let extensions = live_device_extensions(instance, device, &name)?; 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 sampled_image_formats = live_sampled_image_formats(instance, device); let queue_families = queue_properties .iter() .enumerate() .map(|(queue_index, properties)| { let index = u32::try_from(queue_index).unwrap_or(u32::MAX); let present = { // SAFETY: The physical device, surface and queue-family index are live query inputs. unsafe { surface.loader.get_physical_device_surface_support( device, index, surface.surface, ) } } .map_err(|error| VulkanRuntimeCapabilityError::PresentSupportFailed { device: name.clone(), queue_family: index, result: error, })?; Ok(VulkanQueueFamily { index, graphics: properties.queue_flags.contains(vk::QueueFlags::GRAPHICS), present, }) }) .collect::, VulkanRuntimeCapabilityError>>()?; let record = VulkanPhysicalDeviceRecord { name, api_version: properties.api_version, device_type: match properties.device_type { vk::PhysicalDeviceType::DISCRETE_GPU => VulkanDeviceType::DiscreteGpu, vk::PhysicalDeviceType::INTEGRATED_GPU => VulkanDeviceType::IntegratedGpu, vk::PhysicalDeviceType::CPU => VulkanDeviceType::Cpu, _ => VulkanDeviceType::Other, }, extensions, queue_families, surface_formats: surface_formats.clone(), present_modes: present_modes.clone(), surface_capabilities, supported_depth_stencil_formats, sampled_image_formats, limits: VulkanDeviceLimits { max_image_dimension_2d: properties.limits.max_image_dimension2_d, max_sampler_allocation_count: properties.limits.max_sampler_allocation_count, max_per_stage_descriptor_samplers: properties.limits.max_per_stage_descriptor_samplers, max_bound_descriptor_sets: properties.limits.max_bound_descriptor_sets, }, }; let capability = validate_device_for_request(&record, render_request) .map_err(VulkanRuntimeCapabilityError::Capability)?; Ok(LiveDeviceCandidate { physical_device: device, capability, surface_formats, present_modes, surface_capabilities, }) } pub(super) fn unique_queue_families(graphics: u32, present: u32) -> Vec { if graphics == present { vec![graphics] } else { vec![graphics, present] } } fn physical_device_name(properties: &vk::PhysicalDeviceProperties, index: usize) -> String { // SAFETY: Vulkan device names are fixed-size NUL-terminated C strings per the spec. let name = unsafe { CStr::from_ptr(properties.device_name.as_ptr()) } .to_string_lossy() .trim() .to_string(); if name.is_empty() { format!("physical-device-{index}") } else { name } } fn live_device_extensions( instance: &VulkanInstanceProbe, device: vk::PhysicalDevice, name: &str, ) -> Result, VulkanRuntimeCapabilityError> { let properties = { // SAFETY: `device` was returned by this live instance and no borrowed data escapes. unsafe { instance .instance .enumerate_device_extension_properties(device) } } .map_err( |error| VulkanRuntimeCapabilityError::EnumerateDeviceExtensionsFailed { device: name.to_string(), result: error, }, )?; let mut extensions = properties .iter() .map(|property| { // SAFETY: Vulkan extension names are fixed-size NUL-terminated C strings per the spec. unsafe { CStr::from_ptr(property.extension_name.as_ptr()) } .to_string_lossy() .into_owned() }) .collect::>(); extensions.sort(); extensions.dedup(); Ok(extensions) } pub(super) fn live_surface_formats( surface: &VulkanSurfaceProbe, device: vk::PhysicalDevice, name: &str, ) -> Result, VulkanRuntimeCapabilityError> { let formats = { // SAFETY: The physical device and surface are live query inputs and no handles are retained. unsafe { surface .loader .get_physical_device_surface_formats(device, surface.surface) } } .map_err(|error| VulkanRuntimeCapabilityError::SurfaceFormatsFailed { device: name.to_string(), result: error, })?; Ok(formats .into_iter() .map(|format| VulkanSurfaceFormat { format: format.format.as_raw(), color_space: format.color_space.as_raw(), }) .collect()) } pub(super) fn live_present_modes( surface: &VulkanSurfaceProbe, device: vk::PhysicalDevice, name: &str, ) -> Result, VulkanRuntimeCapabilityError> { let modes = { // SAFETY: The physical device and surface are live query inputs and no handles are retained. unsafe { surface .loader .get_physical_device_surface_present_modes(device, surface.surface) } } .map_err(|error| VulkanRuntimeCapabilityError::PresentModesFailed { device: name.to_string(), result: error, })?; Ok(modes.into_iter().map(vk::PresentModeKHR::as_raw).collect()) } pub(super) fn live_surface_capabilities( surface: &VulkanSurfaceProbe, device: vk::PhysicalDevice, name: &str, ) -> Result { let 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, surface.surface) } } .map_err( |error| VulkanRuntimeCapabilityError::SurfaceCapabilitiesFailed { device: name.to_string(), result: error, }, )?; Ok(VulkanSwapchainSurfaceCapabilities { current_extent: if capabilities.current_extent.width == u32::MAX { None } else { Some(( capabilities.current_extent.width, capabilities.current_extent.height, )) }, min_extent: ( capabilities.min_image_extent.width, capabilities.min_image_extent.height, ), max_extent: ( capabilities.max_image_extent.width, capabilities.max_image_extent.height, ), min_image_count: capabilities.min_image_count, max_image_count: capabilities.max_image_count, supported_usage_flags: capabilities.supported_usage_flags.as_raw(), }) } fn live_depth_stencil_formats( instance: &VulkanInstanceProbe, device: vk::PhysicalDevice, ) -> Vec { [ 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() } fn live_sampled_image_formats( instance: &VulkanInstanceProbe, device: vk::PhysicalDevice, ) -> Vec { [ vk::Format::R8G8B8A8_SRGB, vk::Format::B8G8R8A8_SRGB, vk::Format::D16_UNORM, vk::Format::D32_SFLOAT, 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::SAMPLED_IMAGE) }) .map(vk::Format::as_raw) .collect() }