aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 06:55:08 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:39 +0300
commite79d26ea68a562c95c13405516547acbbc23cefd (patch)
treeec42ca0595207960ee7568b1663e43de5018a50f
parente3c74485f1d3d2aff94de2a12486cf34c4bce0ed (diff)
downloadfparkan-e79d26ea68a562c95c13405516547acbbc23cefd.tar.xz
fparkan-e79d26ea68a562c95c13405516547acbbc23cefd.zip
feat(vulkan-policy): report rejected device diagnostics
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs23
-rw-r--r--adapters/fparkan-render-vulkan/src/policy.rs51
2 files changed, 64 insertions, 10 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
index 524dbcb..c242d97 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
@@ -140,6 +140,14 @@ fn device_selection_skips_rejected_candidates_before_accepting_valid_gpu() {
assert_eq!(report.device_name, "Accepted");
assert_eq!(report.graphics_queue_family, 2);
assert_eq!(report.present_queue_family, 2);
+ assert_eq!(
+ report.rejected_devices,
+ vec![VulkanRejectedDeviceReport {
+ device_name: "Rejected".to_string(),
+ reason_code: "no_present_queue",
+ reason: "Vulkan device Rejected has no present queue".to_string(),
+ }]
+ );
}
#[test]
@@ -281,18 +289,17 @@ fn rejects_missing_graphics_present_swapchain_and_format() {
#[test]
fn capability_report_json_is_stable() {
- let report = select_physical_device(&[device(
- "GPU \"A\"",
- VulkanDeviceType::DiscreteGpu,
- 3,
- true,
- false,
- )])
+ let mut rejected = device("Rejected", VulkanDeviceType::IntegratedGpu, 0, true, false);
+ rejected.present_modes.clear();
+ let report = select_physical_device(&[
+ rejected,
+ device("GPU \"A\"", VulkanDeviceType::DiscreteGpu, 3, true, false),
+ ])
.expect("selected device");
assert_eq!(
render_capability_report_json(&report),
- "{\"schema\":1,\"vulkan_api\":\"1.1.0\",\"device_name\":\"GPU \\\"A\\\"\",\"score\":1101,\"graphics_queue_family\":3,\"present_queue_family\":3,\"portability_subset\":false,\"enabled_extensions\":[\"VK_KHR_swapchain\"]}"
+ "{\"schema\":1,\"vulkan_api\":\"1.1.0\",\"device_name\":\"GPU \\\"A\\\"\",\"score\":1101,\"graphics_queue_family\":3,\"present_queue_family\":3,\"portability_subset\":false,\"enabled_extensions\":[\"VK_KHR_swapchain\"],\"rejected_devices\":[{\"device_name\":\"Rejected\",\"reason_code\":\"missing_present_mode\",\"reason\":\"Vulkan device Rejected has no supported present mode\"}]}"
);
}
diff --git a/adapters/fparkan-render-vulkan/src/policy.rs b/adapters/fparkan-render-vulkan/src/policy.rs
index 9e77e57..ef9f0c4 100644
--- a/adapters/fparkan-render-vulkan/src/policy.rs
+++ b/adapters/fparkan-render-vulkan/src/policy.rs
@@ -213,6 +213,19 @@ pub struct VulkanCapabilityReport {
pub portability_subset: bool,
/// Enabled device extensions.
pub enabled_extensions: Vec<String>,
+ /// Devices rejected by deterministic Stage 0 capability validation.
+ pub rejected_devices: Vec<VulkanRejectedDeviceReport>,
+}
+
+/// Deterministic rejection reason for an unsuitable physical device.
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+pub struct VulkanRejectedDeviceReport {
+ /// Human-readable device name.
+ pub device_name: String,
+ /// Stable machine-readable rejection code.
+ pub reason_code: &'static str,
+ /// Actionable rejection summary.
+ pub reason: String,
}
/// Vulkan capability selection error.
@@ -308,11 +321,13 @@ pub fn select_physical_device(
}
let mut best = None;
+ let mut rejected_devices = Vec::new();
let mut last_error = None;
for device in devices {
let report = match validate_device(device) {
Ok(report) => report,
Err(err) => {
+ rejected_devices.push(rejected_device_report(device, &err));
last_error = Some(err);
continue;
}
@@ -323,7 +338,10 @@ pub fn select_physical_device(
_ => best = Some(report),
}
}
- best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))
+ let mut best =
+ best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))?;
+ best.rejected_devices = rejected_devices;
+ Ok(best)
}
/// Builds a deterministic swapchain plan from surface capabilities.
@@ -411,6 +429,7 @@ pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String
present_queue_family: u32,
portability_subset: bool,
enabled_extensions: &'a [String],
+ rejected_devices: &'a [VulkanRejectedDeviceReport],
}
serialize_json_or_fallback(
@@ -423,8 +442,9 @@ pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String
present_queue_family: report.present_queue_family,
portability_subset: report.portability_subset,
enabled_extensions: &report.enabled_extensions,
+ rejected_devices: &report.rejected_devices,
},
- "{\"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\":[]}",
+ "{\"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\":[],\"rejected_devices\":[]}",
)
}
@@ -637,9 +657,36 @@ pub(crate) fn validate_device(
present_queue_family,
portability_subset,
enabled_extensions,
+ rejected_devices: Vec::new(),
})
}
+fn rejected_device_report(
+ device: &VulkanPhysicalDeviceRecord,
+ error: &VulkanCapabilityError,
+) -> VulkanRejectedDeviceReport {
+ VulkanRejectedDeviceReport {
+ device_name: device.name.clone(),
+ reason_code: capability_error_code(error),
+ reason: error.to_string(),
+ }
+}
+
+const fn capability_error_code(error: &VulkanCapabilityError) -> &'static str {
+ match error {
+ VulkanCapabilityError::NoPhysicalDevice => "no_physical_device",
+ VulkanCapabilityError::ApiVersionTooLow { .. } => "api_version_too_low",
+ VulkanCapabilityError::NoGraphicsQueue { .. } => "no_graphics_queue",
+ VulkanCapabilityError::NoPresentQueue { .. } => "no_present_queue",
+ VulkanCapabilityError::MissingSwapchainExtension { .. } => "missing_swapchain_extension",
+ VulkanCapabilityError::MissingSurfaceFormat { .. } => "missing_surface_format",
+ VulkanCapabilityError::MissingPresentMode { .. } => "missing_present_mode",
+ VulkanCapabilityError::MissingColorAttachmentUsage { .. } => {
+ "missing_color_attachment_usage"
+ }
+ }
+}
+
fn select_queue_families(
device: &VulkanPhysicalDeviceRecord,
) -> Result<(u32, u32), VulkanCapabilityError> {