aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/policy.rs
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/policy.rs')
-rw-r--r--adapters/fparkan-render-vulkan/src/policy.rs712
1 files changed, 712 insertions, 0 deletions
diff --git a/adapters/fparkan-render-vulkan/src/policy.rs b/adapters/fparkan-render-vulkan/src/policy.rs
new file mode 100644
index 0000000..9e77e57
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/policy.rs
@@ -0,0 +1,712 @@
+use ash::vk;
+use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError};
+use serde::Serialize;
+
+const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
+pub(crate) const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
+pub(crate) const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset";
+
+/// Synthetic physical-device type used by deterministic capability scoring.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanDeviceType {
+ /// Discrete GPU.
+ DiscreteGpu,
+ /// Integrated GPU.
+ IntegratedGpu,
+ /// CPU or software Vulkan implementation.
+ Cpu,
+ /// Other or unknown implementation.
+ Other,
+}
+
+impl VulkanDeviceType {
+ const fn score_bonus(self) -> i32 {
+ match self {
+ Self::DiscreteGpu => 1_000,
+ Self::IntegratedGpu => 700,
+ Self::Cpu => 100,
+ Self::Other => 10,
+ }
+ }
+}
+
+/// Queue-family capabilities needed by the Stage 0 renderer.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct VulkanQueueFamily {
+ /// Stable queue-family index.
+ pub index: u32,
+ /// Whether the family supports graphics commands.
+ pub graphics: bool,
+ /// Whether the family supports presentation for the target surface.
+ pub present: bool,
+}
+
+/// Surface format capability needed by the Stage 0 swapchain policy.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct VulkanSurfaceFormat {
+ /// Vulkan format numeric value.
+ pub format: i32,
+ /// Vulkan color-space numeric value.
+ pub color_space: i32,
+}
+
+/// Surface capabilities needed by the Stage 0 swapchain policy.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainSurfaceCapabilities {
+ /// Current surface extent, when dictated by the platform.
+ pub current_extent: Option<(u32, u32)>,
+ /// Minimum supported swapchain extent.
+ pub min_extent: (u32, u32),
+ /// Maximum supported swapchain extent.
+ pub max_extent: (u32, u32),
+ /// Minimum supported image count.
+ pub min_image_count: u32,
+ /// Maximum supported image count, or 0 when unbounded.
+ pub max_image_count: u32,
+ /// Supported swapchain image-usage flags as raw Vulkan bits.
+ pub supported_usage_flags: u32,
+}
+
+/// Deterministic swapchain planning input.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainRequest {
+ /// Requested drawable extent.
+ pub drawable_extent: (u32, u32),
+ /// Available surface formats.
+ pub formats: Vec<VulkanSurfaceFormat>,
+ /// Available present modes as raw Vulkan values.
+ pub present_modes: Vec<i32>,
+ /// Surface capabilities.
+ pub capabilities: VulkanSwapchainSurfaceCapabilities,
+ /// Preferred present mode.
+ pub preferred_present_mode: i32,
+}
+
+/// Deterministic swapchain plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainPlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Selected swapchain extent.
+ pub extent: (u32, u32),
+ /// Selected surface format.
+ pub format: VulkanSurfaceFormat,
+ /// Selected present mode raw Vulkan value.
+ pub present_mode: i32,
+ /// Selected image count.
+ pub image_count: u32,
+}
+
+/// Swapchain planning error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainError {
+ /// No surface format was available.
+ MissingSurfaceFormat,
+ /// No present mode was available.
+ MissingPresentMode,
+ /// Requested or current extent is empty.
+ EmptyExtent,
+}
+
+impl std::fmt::Display for VulkanSwapchainError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::MissingSurfaceFormat => write!(f, "Vulkan swapchain has no surface format"),
+ Self::MissingPresentMode => write!(f, "Vulkan swapchain has no present mode"),
+ Self::EmptyExtent => write!(f, "Vulkan swapchain extent must be non-zero"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanSwapchainError {}
+
+/// Swapchain recreation reason.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainRecreationReason {
+ /// Drawable extent changed.
+ Resize,
+ /// Vulkan reported `VK_ERROR_OUT_OF_DATE_KHR`.
+ OutOfDate,
+ /// Vulkan reported `VK_SUBOPTIMAL_KHR`.
+ Suboptimal,
+}
+
+/// Deterministic swapchain recreation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainRecreationReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Recreation reason.
+ pub reason: VulkanSwapchainRecreationReason,
+ /// Previous extent.
+ pub previous_extent: (u32, u32),
+ /// Next extent.
+ pub next_extent: (u32, u32),
+}
+
+/// Deterministic frame submission plan for command buffers and sync objects.
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+pub struct VulkanFrameSubmissionPlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Frames allowed in flight.
+ pub frames_in_flight: u32,
+ /// Swapchain-backed primary command buffers.
+ pub command_buffers: u32,
+ /// Binary semaphores allocated per frame.
+ pub semaphores_per_frame: u32,
+ /// Fences allocated per frame.
+ pub fences_per_frame: u32,
+ /// Draw commands encoded into the frame.
+ pub draw_count: u32,
+ /// Total indexed vertices submitted by draw commands.
+ pub indexed_vertex_count: u32,
+}
+
+/// Synthetic physical-device capabilities used by negative tests and reports.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanPhysicalDeviceRecord {
+ /// Human-readable device name.
+ pub name: String,
+ /// Reported Vulkan API version.
+ pub api_version: u32,
+ /// Device class.
+ pub device_type: VulkanDeviceType,
+ /// Supported device-extension names.
+ pub extensions: Vec<String>,
+ /// Queue-family capabilities.
+ pub queue_families: Vec<VulkanQueueFamily>,
+ /// Surface formats accepted by the target surface.
+ pub surface_formats: Vec<VulkanSurfaceFormat>,
+ /// Present modes accepted by the target surface.
+ pub present_modes: Vec<i32>,
+ /// Surface capabilities accepted by the target surface.
+ pub surface_capabilities: VulkanSwapchainSurfaceCapabilities,
+}
+
+impl VulkanPhysicalDeviceRecord {
+ /// Returns whether the device supports an extension name.
+ #[must_use]
+ pub fn supports_extension(&self, extension: &str) -> bool {
+ self.extensions
+ .iter()
+ .any(|candidate| candidate == extension)
+ }
+}
+
+/// Selected device and queue capability report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanCapabilityReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Selected device name.
+ pub device_name: String,
+ /// Selected Vulkan API version.
+ pub vulkan_api_version: u32,
+ /// Deterministic score used for device selection.
+ pub score: i32,
+ /// Graphics queue family index.
+ pub graphics_queue_family: u32,
+ /// Present queue family index.
+ pub present_queue_family: u32,
+ /// Whether portability subset is enabled for the selected device.
+ pub portability_subset: bool,
+ /// Enabled device extensions.
+ pub enabled_extensions: Vec<String>,
+}
+
+/// Vulkan capability selection error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanCapabilityError {
+ /// No physical devices were available.
+ NoPhysicalDevice,
+ /// Device API version is lower than the Stage 0 minimum.
+ ApiVersionTooLow {
+ /// Required Vulkan API version.
+ required: u32,
+ /// Reported Vulkan API version.
+ found: u32,
+ },
+ /// Required graphics queue is unavailable.
+ NoGraphicsQueue {
+ /// Device name that failed validation.
+ device: String,
+ },
+ /// Required present queue is unavailable.
+ NoPresentQueue {
+ /// Device name that failed validation.
+ device: String,
+ },
+ /// Swapchain device extension is unavailable.
+ MissingSwapchainExtension {
+ /// Device name that failed validation.
+ device: String,
+ },
+ /// No compatible surface format exists.
+ MissingSurfaceFormat {
+ /// Device name that failed validation.
+ device: String,
+ },
+ /// No present mode is available for the target surface.
+ MissingPresentMode {
+ /// Device name that failed validation.
+ device: String,
+ },
+ /// Swapchain images cannot be used as color attachments.
+ MissingColorAttachmentUsage {
+ /// Device name that failed validation.
+ device: String,
+ },
+}
+
+impl std::fmt::Display for VulkanCapabilityError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::NoPhysicalDevice => write!(f, "no Vulkan physical device available"),
+ Self::ApiVersionTooLow { required, found } => write!(
+ f,
+ "Vulkan API version too low: required {}, found {}",
+ format_api_version(*required),
+ format_api_version(*found)
+ ),
+ Self::NoGraphicsQueue { device } => {
+ write!(f, "Vulkan device {device} has no graphics queue")
+ }
+ Self::NoPresentQueue { device } => {
+ write!(f, "Vulkan device {device} has no present queue")
+ }
+ Self::MissingSwapchainExtension { device } => {
+ write!(f, "Vulkan device {device} lacks {KHR_SWAPCHAIN_EXTENSION}")
+ }
+ Self::MissingSurfaceFormat { device } => {
+ write!(f, "Vulkan device {device} has no compatible surface format")
+ }
+ Self::MissingPresentMode { device } => {
+ write!(f, "Vulkan device {device} has no supported present mode")
+ }
+ Self::MissingColorAttachmentUsage { device } => write!(
+ f,
+ "Vulkan device {device} surface does not support COLOR_ATTACHMENT usage"
+ ),
+ }
+ }
+}
+
+impl std::error::Error for VulkanCapabilityError {}
+
+/// Selects a Vulkan physical device using deterministic Stage 0 policy.
+///
+/// # Errors
+///
+/// Returns [`VulkanCapabilityError`] when no candidate satisfies the minimum
+/// API version, queue, swapchain-extension and surface-format requirements.
+pub fn select_physical_device(
+ devices: &[VulkanPhysicalDeviceRecord],
+) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
+ if devices.is_empty() {
+ return Err(VulkanCapabilityError::NoPhysicalDevice);
+ }
+
+ let mut best = None;
+ let mut last_error = None;
+ for device in devices {
+ let report = match validate_device(device) {
+ Ok(report) => report,
+ Err(err) => {
+ last_error = Some(err);
+ continue;
+ }
+ };
+ match &best {
+ Some(existing) if compare_reports(&report, existing) != std::cmp::Ordering::Greater => {
+ }
+ _ => best = Some(report),
+ }
+ }
+ best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))
+}
+
+/// Builds a deterministic swapchain plan from surface capabilities.
+///
+/// # Errors
+///
+/// Returns [`VulkanSwapchainError`] when formats, present modes or extent are
+/// unusable.
+pub fn plan_vulkan_swapchain(
+ request: &VulkanSwapchainRequest,
+) -> Result<VulkanSwapchainPlan, VulkanSwapchainError> {
+ let format = select_surface_format(&request.formats)?;
+ let present_mode = select_present_mode(&request.present_modes, request.preferred_present_mode)?;
+ let extent = select_swapchain_extent(request)?;
+ if extent.0 == 0 || extent.1 == 0 {
+ return Err(VulkanSwapchainError::EmptyExtent);
+ }
+ Ok(VulkanSwapchainPlan {
+ schema: 1,
+ extent,
+ format,
+ present_mode,
+ image_count: select_image_count(request.capabilities),
+ })
+}
+
+/// Builds a deterministic swapchain recreation report.
+#[must_use]
+pub const fn swapchain_recreation_report(
+ reason: VulkanSwapchainRecreationReason,
+ previous_extent: (u32, u32),
+ next_extent: (u32, u32),
+) -> VulkanSwapchainRecreationReport {
+ VulkanSwapchainRecreationReport {
+ schema: 1,
+ reason,
+ previous_extent,
+ next_extent,
+ }
+}
+
+/// Builds a deterministic frame submission plan for a validated command list.
+///
+/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer
+/// and synchronization policy can be tested without requiring a native surface.
+///
+/// # Errors
+///
+/// Returns [`RenderError`] when the command list has invalid frame framing,
+/// ordering, draw ranges, mesh bounds, or non-finite transforms.
+pub fn plan_vulkan_frame_submission(
+ swapchain: &VulkanSwapchainPlan,
+ commands: &RenderCommandList,
+) -> Result<VulkanFrameSubmissionPlan, RenderError> {
+ validate_command_list(commands)?;
+ let mut draw_count = 0_u32;
+ let mut indexed_vertex_count = 0_u32;
+ for command in &commands.commands {
+ if let RenderCommand::Draw(draw) = command {
+ draw_count = draw_count.saturating_add(1);
+ indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count);
+ }
+ }
+ Ok(VulkanFrameSubmissionPlan {
+ schema: 1,
+ frames_in_flight: swapchain.image_count.clamp(1, 2),
+ command_buffers: swapchain.image_count,
+ semaphores_per_frame: 2,
+ fences_per_frame: 1,
+ draw_count,
+ indexed_vertex_count,
+ })
+}
+
+/// Renders a deterministic JSON capability report.
+#[must_use]
+pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String {
+ #[derive(Serialize)]
+ struct CapabilityReportJson<'a> {
+ schema: u32,
+ vulkan_api: String,
+ device_name: &'a str,
+ score: i32,
+ graphics_queue_family: u32,
+ present_queue_family: u32,
+ portability_subset: bool,
+ enabled_extensions: &'a [String],
+ }
+
+ serialize_json_or_fallback(
+ &CapabilityReportJson {
+ schema: report.schema,
+ vulkan_api: format_api_version(report.vulkan_api_version),
+ device_name: &report.device_name,
+ score: report.score,
+ graphics_queue_family: report.graphics_queue_family,
+ present_queue_family: report.present_queue_family,
+ portability_subset: report.portability_subset,
+ enabled_extensions: &report.enabled_extensions,
+ },
+ "{\"schema\":0,\"vulkan_api\":\"0.0.0\",\"device_name\":\"unknown\",\"score\":0,\"graphics_queue_family\":0,\"present_queue_family\":0,\"portability_subset\":false,\"enabled_extensions\":[]}",
+ )
+}
+
+/// Renders a deterministic JSON swapchain plan.
+#[must_use]
+pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
+ #[derive(Serialize)]
+ struct SwapchainPlanJson {
+ schema: u32,
+ extent: [u32; 2],
+ format: i32,
+ color_space: i32,
+ present_mode: i32,
+ image_count: u32,
+ }
+
+ serialize_json_or_fallback(
+ &SwapchainPlanJson {
+ schema: plan.schema,
+ extent: [plan.extent.0, plan.extent.1],
+ format: plan.format.format,
+ color_space: plan.format.color_space,
+ present_mode: plan.present_mode,
+ image_count: plan.image_count,
+ },
+ "{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0}",
+ )
+}
+
+/// Renders a deterministic JSON swapchain recreation report.
+#[must_use]
+pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
+ #[derive(Serialize)]
+ struct SwapchainRecreationReportJson<'a> {
+ schema: u32,
+ reason: &'a str,
+ previous_extent: [u32; 2],
+ next_extent: [u32; 2],
+ }
+
+ serialize_json_or_fallback(
+ &SwapchainRecreationReportJson {
+ schema: report.schema,
+ reason: match report.reason {
+ VulkanSwapchainRecreationReason::Resize => "resize",
+ VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
+ VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
+ },
+ previous_extent: [report.previous_extent.0, report.previous_extent.1],
+ next_extent: [report.next_extent.0, report.next_extent.1],
+ },
+ "{\"schema\":0,\"reason\":\"unknown\",\"previous_extent\":[0,0],\"next_extent\":[0,0]}",
+ )
+}
+
+/// Renders a deterministic JSON frame submission plan.
+#[must_use]
+pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
+ serialize_json_or_fallback(
+ plan,
+ "{\"schema\":0,\"frames_in_flight\":0,\"command_buffers\":0,\"semaphores_per_frame\":0,\"fences_per_frame\":0,\"draw_count\":0,\"indexed_vertex_count\":0}",
+ )
+}
+
+pub(crate) 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
+ }
+}
+
+pub(crate) fn serialize_json_or_fallback<T: Serialize>(value: &T, fallback: &str) -> String {
+ match serde_json::to_string(value) {
+ Ok(json) => json,
+ Err(_) => fallback.to_string(),
+ }
+}
+
+pub(crate) fn format_api_version(version: u32) -> String {
+ format!(
+ "{}.{}.{}",
+ vk::api_version_major(version),
+ vk::api_version_minor(version),
+ vk::api_version_patch(version)
+ )
+}
+
+fn select_surface_format(
+ formats: &[VulkanSurfaceFormat],
+) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
+ if let Some(format) = undefined_surface_format_override(formats) {
+ return Ok(format);
+ }
+ formats
+ .iter()
+ .copied()
+ .find(|format| {
+ format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
+ && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
+ })
+ .or_else(|| formats.first().copied())
+ .ok_or(VulkanSwapchainError::MissingSurfaceFormat)
+}
+
+fn undefined_surface_format_override(
+ formats: &[VulkanSurfaceFormat],
+) -> Option<VulkanSurfaceFormat> {
+ match formats {
+ [format] if format.format == vk::Format::UNDEFINED.as_raw() => Some(VulkanSurfaceFormat {
+ format: vk::Format::B8G8R8A8_SRGB.as_raw(),
+ color_space: format.color_space,
+ }),
+ _ => None,
+ }
+}
+
+fn select_present_mode(present_modes: &[i32], preferred: i32) -> Result<i32, VulkanSwapchainError> {
+ if present_modes.contains(&preferred) {
+ Ok(preferred)
+ } else if present_modes.contains(&vk::PresentModeKHR::FIFO.as_raw()) {
+ Ok(vk::PresentModeKHR::FIFO.as_raw())
+ } else {
+ present_modes
+ .first()
+ .copied()
+ .ok_or(VulkanSwapchainError::MissingPresentMode)
+ }
+}
+
+fn select_swapchain_extent(
+ request: &VulkanSwapchainRequest,
+) -> Result<(u32, u32), VulkanSwapchainError> {
+ if let Some(extent) = request.capabilities.current_extent {
+ return if extent.0 == 0 || extent.1 == 0 {
+ Err(VulkanSwapchainError::EmptyExtent)
+ } else {
+ Ok(extent)
+ };
+ }
+ let width = request.drawable_extent.0.clamp(
+ request.capabilities.min_extent.0,
+ request.capabilities.max_extent.0,
+ );
+ let height = request.drawable_extent.1.clamp(
+ request.capabilities.min_extent.1,
+ request.capabilities.max_extent.1,
+ );
+ Ok((width, height))
+}
+
+fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
+ let requested = capabilities.min_image_count.saturating_add(1).max(2);
+ if capabilities.max_image_count == 0 {
+ requested
+ } else {
+ requested.min(capabilities.max_image_count)
+ }
+}
+
+pub(crate) fn validate_device(
+ device: &VulkanPhysicalDeviceRecord,
+) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
+ if device.api_version < MIN_VULKAN_API_VERSION {
+ return Err(VulkanCapabilityError::ApiVersionTooLow {
+ required: MIN_VULKAN_API_VERSION,
+ found: device.api_version,
+ });
+ }
+ if !device.supports_extension(KHR_SWAPCHAIN_EXTENSION) {
+ return Err(VulkanCapabilityError::MissingSwapchainExtension {
+ device: device.name.clone(),
+ });
+ }
+ if !supports_surface_formats(device) {
+ return Err(VulkanCapabilityError::MissingSurfaceFormat {
+ device: device.name.clone(),
+ });
+ }
+ if device.present_modes.is_empty() {
+ return Err(VulkanCapabilityError::MissingPresentMode {
+ device: device.name.clone(),
+ });
+ }
+ if !supports_color_attachment_usage(device.surface_capabilities) {
+ return Err(VulkanCapabilityError::MissingColorAttachmentUsage {
+ device: device.name.clone(),
+ });
+ }
+ let (graphics_queue_family, present_queue_family) = select_queue_families(device)?;
+
+ let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION);
+ let mut enabled_extensions = vec![KHR_SWAPCHAIN_EXTENSION.to_string()];
+ if portability_subset {
+ enabled_extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string());
+ }
+
+ Ok(VulkanCapabilityReport {
+ schema: 1,
+ device_name: device.name.clone(),
+ vulkan_api_version: device.api_version,
+ score: score_device(device, graphics_queue_family, present_queue_family),
+ graphics_queue_family,
+ present_queue_family,
+ portability_subset,
+ enabled_extensions,
+ })
+}
+
+fn select_queue_families(
+ device: &VulkanPhysicalDeviceRecord,
+) -> Result<(u32, u32), VulkanCapabilityError> {
+ if let Some(unified) = device
+ .queue_families
+ .iter()
+ .filter(|family| family.graphics && family.present)
+ .min_by_key(|family| family.index)
+ {
+ return Ok((unified.index, unified.index));
+ }
+
+ let graphics_queue_family = device
+ .queue_families
+ .iter()
+ .filter(|family| family.graphics)
+ .min_by_key(|family| family.index)
+ .ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue {
+ device: device.name.clone(),
+ })?
+ .index;
+ let present_queue_family = device
+ .queue_families
+ .iter()
+ .filter(|family| family.present)
+ .min_by_key(|family| family.index)
+ .ok_or_else(|| VulkanCapabilityError::NoPresentQueue {
+ device: device.name.clone(),
+ })?
+ .index;
+ Ok((graphics_queue_family, present_queue_family))
+}
+
+fn supports_surface_formats(device: &VulkanPhysicalDeviceRecord) -> bool {
+ !device.surface_formats.is_empty()
+}
+
+fn supports_color_attachment_usage(capabilities: VulkanSwapchainSurfaceCapabilities) -> bool {
+ capabilities.supported_usage_flags & vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw() != 0
+}
+
+fn score_device(
+ device: &VulkanPhysicalDeviceRecord,
+ graphics_queue_family: u32,
+ present_queue_family: u32,
+) -> i32 {
+ let unified_queue_bonus = if graphics_queue_family == present_queue_family {
+ 100
+ } else {
+ 0
+ };
+ let portability_penalty = if device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION) {
+ -50
+ } else {
+ 0
+ };
+ device.device_type.score_bonus()
+ + unified_queue_bonus
+ + portability_penalty
+ + i32::try_from(device.surface_formats.len()).unwrap_or(i32::MAX)
+}
+
+pub(crate) fn compare_reports(
+ left: &VulkanCapabilityReport,
+ right: &VulkanCapabilityReport,
+) -> std::cmp::Ordering {
+ left.score
+ .cmp(&right.score)
+ .then_with(|| right.device_name.cmp(&left.device_name))
+}