fix(vulkan-capabilities): harden swapchain capability gate
This commit is contained in:
@@ -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(),
|
||||||
|
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_surface_capabilities() -> VulkanSwapchainSurfaceCapabilities {
|
||||||
|
VulkanSwapchainSurfaceCapabilities {
|
||||||
current_extent: None,
|
current_extent: None,
|
||||||
min_extent: (320, 240),
|
min_extent: (320, 240),
|
||||||
max_extent: (1024, 768),
|
max_extent: (1024, 768),
|
||||||
min_image_count: 2,
|
min_image_count: 2,
|
||||||
max_image_count: 3,
|
max_image_count: 3,
|
||||||
},
|
supported_usage_flags: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
|
||||||
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user