refactor(vulkan-policy): extract pure swapchain and device policy

This commit is contained in:
2026-06-25 05:45:34 +04:00
parent 0de5118575
commit d0552922d9
3 changed files with 717 additions and 707 deletions
+3 -707
View File
@@ -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> {
@@ -3,6 +3,8 @@
mod ffi;
mod planning_backend;
mod policy;
pub use ffi::*;
pub use planning_backend::*;
pub use policy::*;
@@ -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))
}