fix(vulkan-capabilities): harden swapchain capability gate
This commit is contained in:
@@ -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,
|
||||
@@ -4453,6 +4515,53 @@ mod tests {
|
||||
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]
|
||||
fn portability_subset_is_reported_and_enabled_when_exposed() {
|
||||
let report = select_physical_device(&[device(
|
||||
@@ -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]
|
||||
@@ -4683,6 +4820,25 @@ mod tests {
|
||||
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]
|
||||
fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
|
||||
let mut request = swapchain_request();
|
||||
@@ -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 {
|
||||
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,
|
||||
},
|
||||
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||
supported_usage_flags: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user