diff options
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 23 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/policy.rs | 51 |
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> { |
