fix(vulkan-capabilities): harden swapchain capability gate

This commit is contained in:
2026-06-25 05:29:10 +04:00
parent 0a78fc2460
commit 72f6c06eca
+177 -11
View File
@@ -2895,6 +2895,8 @@ fn live_device_candidate(
extensions, extensions,
queue_families, queue_families,
surface_formats: surface_formats.clone(), surface_formats: surface_formats.clone(),
present_modes: present_modes.clone(),
surface_capabilities,
}; };
let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?; let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
Ok(LiveDeviceCandidate { Ok(LiveDeviceCandidate {
@@ -3051,6 +3053,7 @@ fn live_surface_capabilities(
), ),
min_image_count: capabilities.min_image_count, min_image_count: capabilities.min_image_count,
max_image_count: capabilities.max_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, pub min_image_count: u32,
/// Maximum supported image count, or 0 when unbounded. /// Maximum supported image count, or 0 when unbounded.
pub max_image_count: u32, pub max_image_count: u32,
/// Supported swapchain image-usage flags as raw Vulkan bits.
pub supported_usage_flags: u32,
} }
/// Deterministic swapchain planning input. /// Deterministic swapchain planning input.
@@ -3737,6 +3742,10 @@ pub struct VulkanPhysicalDeviceRecord {
pub queue_families: Vec<VulkanQueueFamily>, pub queue_families: Vec<VulkanQueueFamily>,
/// Surface formats accepted by the target surface. /// Surface formats accepted by the target surface.
pub surface_formats: Vec<VulkanSurfaceFormat>, 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 { impl VulkanPhysicalDeviceRecord {
@@ -3802,6 +3811,16 @@ pub enum VulkanCapabilityError {
/// Device name that failed validation. /// Device name that failed validation.
device: String, 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 { impl std::fmt::Display for VulkanCapabilityError {
@@ -3826,6 +3845,13 @@ impl std::fmt::Display for VulkanCapabilityError {
Self::MissingSurfaceFormat { device } => { Self::MissingSurfaceFormat { device } => {
write!(f, "Vulkan device {device} has no compatible surface format") 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( fn select_surface_format(
formats: &[VulkanSurfaceFormat], formats: &[VulkanSurfaceFormat],
) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> { ) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
if let Some(format) = undefined_surface_format_override(formats) {
return Ok(format);
}
formats formats
.iter() .iter()
.copied() .copied()
@@ -3902,6 +3931,18 @@ fn select_surface_format(
.ok_or(VulkanSwapchainError::MissingSurfaceFormat) .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> { fn select_present_mode(present_modes: &[i32], preferred: i32) -> Result<i32, VulkanSwapchainError> {
if present_modes.contains(&preferred) { if present_modes.contains(&preferred) {
Ok(preferred) Ok(preferred)
@@ -4019,11 +4060,21 @@ fn validate_device(
device: device.name.clone(), device: device.name.clone(),
}); });
} }
if device.surface_formats.is_empty() { if !supports_surface_formats(device) {
return Err(VulkanCapabilityError::MissingSurfaceFormat { return Err(VulkanCapabilityError::MissingSurfaceFormat {
device: device.name.clone(), 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 (graphics_queue_family, present_queue_family) = select_queue_families(device)?;
let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION); let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION);
@@ -4050,7 +4101,8 @@ fn select_queue_families(
if let Some(unified) = device if let Some(unified) = device
.queue_families .queue_families
.iter() .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)); return Ok((unified.index, unified.index));
} }
@@ -4058,7 +4110,8 @@ fn select_queue_families(
let graphics_queue_family = device let graphics_queue_family = device
.queue_families .queue_families
.iter() .iter()
.find(|family| family.graphics) .filter(|family| family.graphics)
.min_by_key(|family| family.index)
.ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue { .ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue {
device: device.name.clone(), device: device.name.clone(),
})? })?
@@ -4066,7 +4119,8 @@ fn select_queue_families(
let present_queue_family = device let present_queue_family = device
.queue_families .queue_families
.iter() .iter()
.find(|family| family.present) .filter(|family| family.present)
.min_by_key(|family| family.index)
.ok_or_else(|| VulkanCapabilityError::NoPresentQueue { .ok_or_else(|| VulkanCapabilityError::NoPresentQueue {
device: device.name.clone(), device: device.name.clone(),
})? })?
@@ -4074,6 +4128,14 @@ fn select_queue_families(
Ok((graphics_queue_family, present_queue_family)) 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( fn score_device(
device: &VulkanPhysicalDeviceRecord, device: &VulkanPhysicalDeviceRecord,
graphics_queue_family: u32, graphics_queue_family: u32,
@@ -4453,6 +4515,53 @@ mod tests {
assert_eq!(report.enabled_extensions, vec![KHR_SWAPCHAIN_EXTENSION]); assert_eq!(report.enabled_extensions, vec![KHR_SWAPCHAIN_EXTENSION]);
} }
#[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] #[test]
fn portability_subset_is_reported_and_enabled_when_exposed() { fn portability_subset_is_reported_and_enabled_when_exposed() {
let report = select_physical_device(&[device( let report = select_physical_device(&[device(
@@ -4527,6 +4636,34 @@ mod tests {
select_physical_device(&[no_format]), select_physical_device(&[no_format]),
Err(VulkanCapabilityError::MissingSurfaceFormat { .. }) 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] #[test]
@@ -4683,6 +4820,25 @@ mod tests {
assert_eq!(plan.extent, (800, 600)); assert_eq!(plan.extent, (800, 600));
} }
#[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] #[test]
fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() { fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
let mut request = swapchain_request(); let mut request = swapchain_request();
@@ -4866,6 +5022,11 @@ mod tests {
format: vk::Format::B8G8R8A8_SRGB.as_raw(), format: vk::Format::B8G8R8A8_SRGB.as_raw(),
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.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::FIFO.as_raw(),
vk::PresentModeKHR::MAILBOX.as_raw(), vk::PresentModeKHR::MAILBOX.as_raw(),
], ],
capabilities: VulkanSwapchainSurfaceCapabilities { capabilities: default_surface_capabilities(),
current_extent: None,
min_extent: (320, 240),
max_extent: (1024, 768),
min_image_count: 2,
max_image_count: 3,
},
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(), 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(),
}
}
} }