aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs710
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs2
-rw-r--r--adapters/fparkan-render-vulkan/src/policy.rs712
3 files changed, 717 insertions, 707 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index cf6901c..e3795a2 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -27,13 +27,13 @@
//!
//! This crate is the declared low-level Vulkan boundary.
+use crate::policy::*;
use ash::{
khr::{surface, swapchain},
vk,
};
use fparkan_binary::{sha256, sha256_hex};
use fparkan_platform::NativeWindowHandles;
-use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError};
use serde::Serialize;
use std::collections::BTreeSet;
use std::ffi::{CStr, CString};
@@ -42,8 +42,6 @@ use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
/// Minimum Vulkan API version accepted by the Stage 0 backend.
pub const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
-const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
-const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset";
const KHR_PORTABILITY_ENUMERATION_EXTENSION: &str = "VK_KHR_portability_enumeration";
const EXT_DEBUG_UTILS_EXTENSION: &str = "VK_EXT_debug_utils";
const VALIDATION_LAYER_NAME: &str = "VK_LAYER_KHRONOS_validation";
@@ -3597,718 +3595,16 @@ fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]
}
}
-/// 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),
- })
-}
-
-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)
- }
-}
-
-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(
- 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,
- })
-}
-
-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)
-}
-
-fn compare_reports(
- left: &VulkanCapabilityReport,
- right: &VulkanCapabilityReport,
-) -> std::cmp::Ordering {
- left.score
- .cmp(&right.score)
- .then_with(|| right.device_name.cmp(&left.device_name))
-}
-
-/// 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}",
- )
-}
-
-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(),
- }
-}
-
-fn format_api_version(version: u32) -> String {
- format!(
- "{}.{}.{}",
- vk::api_version_major(version),
- vk::api_version_minor(version),
- vk::api_version_patch(version)
- )
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION};
use crate::*;
use fparkan_platform::RenderRequest;
- use fparkan_render::RenderBackend;
use fparkan_render::{
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
};
+ use fparkan_render::{RenderBackend, RenderError};
#[test]
fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> {
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index f5cf625..b03df88 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -3,6 +3,8 @@
mod ffi;
mod planning_backend;
+mod policy;
pub use ffi::*;
pub use planning_backend::*;
+pub use policy::*;
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))
+}