aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 04:29:10 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:35 +0300
commit72f6c06eca47a1eceacf7f2390750599a8e2e5a3 (patch)
tree8d4820904de1eb7c612e9f866df361b236ff50d9
parent0a78fc2460cfd81dc20c9aa769275f52b8bedd64 (diff)
downloadfparkan-72f6c06eca47a1eceacf7f2390750599a8e2e5a3.tar.xz
fparkan-72f6c06eca47a1eceacf7f2390750599a8e2e5a3.zip
fix(vulkan-capabilities): harden swapchain capability gate
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs188
1 files changed, 177 insertions, 11 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index cadddfa..b29d711 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -2895,6 +2895,8 @@ fn live_device_candidate(
extensions,
queue_families,
surface_formats: surface_formats.clone(),
+ present_modes: present_modes.clone(),
+ surface_capabilities,
};
let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
Ok(LiveDeviceCandidate {
@@ -3051,6 +3053,7 @@ fn live_surface_capabilities(
),
min_image_count: capabilities.min_image_count,
max_image_count: capabilities.max_image_count,
+ supported_usage_flags: capabilities.supported_usage_flags.as_raw(),
})
}
@@ -3624,6 +3627,8 @@ pub struct VulkanSwapchainSurfaceCapabilities {
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.
@@ -3737,6 +3742,10 @@ pub struct VulkanPhysicalDeviceRecord {
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 {
@@ -3802,6 +3811,16 @@ pub enum VulkanCapabilityError {
/// 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 {
@@ -3826,6 +3845,13 @@ impl std::fmt::Display for VulkanCapabilityError {
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"
+ ),
}
}
}
@@ -3891,6 +3917,9 @@ pub fn plan_vulkan_swapchain(
fn select_surface_format(
formats: &[VulkanSurfaceFormat],
) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
+ if let Some(format) = undefined_surface_format_override(formats) {
+ return Ok(format);
+ }
formats
.iter()
.copied()
@@ -3902,6 +3931,18 @@ fn select_surface_format(
.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)
@@ -4019,11 +4060,21 @@ fn validate_device(
device: device.name.clone(),
});
}
- if device.surface_formats.is_empty() {
+ 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);
@@ -4050,7 +4101,8 @@ fn select_queue_families(
if let Some(unified) = device
.queue_families
.iter()
- .find(|family| family.graphics && family.present)
+ .filter(|family| family.graphics && family.present)
+ .min_by_key(|family| family.index)
{
return Ok((unified.index, unified.index));
}
@@ -4058,7 +4110,8 @@ fn select_queue_families(
let graphics_queue_family = device
.queue_families
.iter()
- .find(|family| family.graphics)
+ .filter(|family| family.graphics)
+ .min_by_key(|family| family.index)
.ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue {
device: device.name.clone(),
})?
@@ -4066,7 +4119,8 @@ fn select_queue_families(
let present_queue_family = device
.queue_families
.iter()
- .find(|family| family.present)
+ .filter(|family| family.present)
+ .min_by_key(|family| family.index)
.ok_or_else(|| VulkanCapabilityError::NoPresentQueue {
device: device.name.clone(),
})?
@@ -4074,6 +4128,14 @@ fn select_queue_families(
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,
@@ -4454,6 +4516,53 @@ mod tests {
}
#[test]
+ fn device_selection_skips_rejected_candidates_before_accepting_valid_gpu() {
+ let mut rejected = device("Rejected", VulkanDeviceType::DiscreteGpu, 0, true, false);
+ rejected.queue_families[0].present = false;
+ let accepted = device("Accepted", VulkanDeviceType::IntegratedGpu, 2, true, false);
+
+ let report =
+ select_physical_device(&[rejected, accepted]).expect("selected fallback device");
+
+ assert_eq!(report.device_name, "Accepted");
+ assert_eq!(report.graphics_queue_family, 2);
+ assert_eq!(report.present_queue_family, 2);
+ }
+
+ #[test]
+ fn queue_family_selection_prefers_lowest_index_unified_family() {
+ let mut candidate = device(
+ "Unified later in list",
+ VulkanDeviceType::DiscreteGpu,
+ 7,
+ true,
+ false,
+ );
+ candidate.queue_families = vec![
+ VulkanQueueFamily {
+ index: 9,
+ graphics: true,
+ present: true,
+ },
+ VulkanQueueFamily {
+ index: 3,
+ graphics: true,
+ present: true,
+ },
+ VulkanQueueFamily {
+ index: 1,
+ graphics: true,
+ present: false,
+ },
+ ];
+
+ let report = select_physical_device(&[candidate]).expect("selected unified queue");
+
+ assert_eq!(report.graphics_queue_family, 3);
+ assert_eq!(report.present_queue_family, 3);
+ }
+
+ #[test]
fn portability_subset_is_reported_and_enabled_when_exposed() {
let report = select_physical_device(&[device(
"MoltenVK",
@@ -4527,6 +4636,34 @@ mod tests {
select_physical_device(&[no_format]),
Err(VulkanCapabilityError::MissingSurfaceFormat { .. })
));
+
+ let mut no_present_mode = device(
+ "No present mode",
+ VulkanDeviceType::DiscreteGpu,
+ 0,
+ true,
+ false,
+ );
+ no_present_mode.present_modes.clear();
+ assert!(matches!(
+ select_physical_device(&[no_present_mode]),
+ Err(VulkanCapabilityError::MissingPresentMode { .. })
+ ));
+
+ let mut no_color_attachment = device(
+ "No color attachment",
+ VulkanDeviceType::DiscreteGpu,
+ 0,
+ true,
+ false,
+ );
+ no_color_attachment
+ .surface_capabilities
+ .supported_usage_flags = vk::ImageUsageFlags::TRANSFER_DST.as_raw();
+ assert!(matches!(
+ select_physical_device(&[no_color_attachment]),
+ Err(VulkanCapabilityError::MissingColorAttachmentUsage { .. })
+ ));
}
#[test]
@@ -4684,6 +4821,25 @@ mod tests {
}
#[test]
+ fn swapchain_plan_accepts_undefined_surface_format_by_picking_stage0_default() {
+ let mut request = swapchain_request();
+ request.formats = vec![VulkanSurfaceFormat {
+ format: vk::Format::UNDEFINED.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ }];
+
+ let plan = plan_vulkan_swapchain(&request).expect("swapchain plan");
+
+ assert_eq!(
+ plan.format,
+ VulkanSurfaceFormat {
+ format: vk::Format::B8G8R8A8_SRGB.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ }
+ );
+ }
+
+ #[test]
fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
let mut request = swapchain_request();
request.formats.clear();
@@ -4866,6 +5022,11 @@ mod tests {
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
}],
+ present_modes: vec![
+ vk::PresentModeKHR::FIFO.as_raw(),
+ vk::PresentModeKHR::MAILBOX.as_raw(),
+ ],
+ surface_capabilities: default_surface_capabilities(),
}
}
@@ -4886,14 +5047,19 @@ mod tests {
vk::PresentModeKHR::FIFO.as_raw(),
vk::PresentModeKHR::MAILBOX.as_raw(),
],
- capabilities: VulkanSwapchainSurfaceCapabilities {
- current_extent: None,
- min_extent: (320, 240),
- max_extent: (1024, 768),
- min_image_count: 2,
- max_image_count: 3,
- },
+ capabilities: default_surface_capabilities(),
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
}
}
+
+ fn default_surface_capabilities() -> VulkanSwapchainSurfaceCapabilities {
+ VulkanSwapchainSurfaceCapabilities {
+ current_extent: None,
+ min_extent: (320, 240),
+ max_extent: (1024, 768),
+ min_image_count: 2,
+ max_image_count: 3,
+ supported_usage_flags: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
+ }
+ }
}