aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 23:14:26 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 23:14:26 +0300
commit159731664fae9ea3f08ec594985b82248988732d (patch)
tree3c590f6f20517d2ec599e1dddded48305b6baef6
parente6b7fa189642bf432dd2bbcf1bcff659bd794750 (diff)
downloadfparkan-159731664fae9ea3f08ec594985b82248988732d.tar.xz
fparkan-159731664fae9ea3f08ec594985b82248988732d.zip
feat: probe Vulkan logical device creation
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs173
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs167
-rw-r--r--fixtures/acceptance/coverage.tsv1
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
-rw-r--r--xtask/src/main.rs32
5 files changed, 354 insertions, 20 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index 38d521d..d2345e1 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -320,6 +320,37 @@ pub struct VulkanRuntimeCapabilityProbe {
pub swapchain: VulkanSwapchainPlan,
}
+/// Created Vulkan logical device probe.
+pub struct VulkanLogicalDeviceProbe {
+ device: ash::Device,
+ /// Runtime capability report used for device selection.
+ pub runtime: VulkanRuntimeCapabilityProbe,
+ /// Deterministic logical device creation report.
+ pub report: VulkanLogicalDeviceReport,
+}
+
+impl Drop for VulkanLogicalDeviceProbe {
+ fn drop(&mut self) {
+ // SAFETY: The logical device was created by this probe and is destroyed once during drop.
+ unsafe { self.device.destroy_device(None) };
+ }
+}
+
+/// Logical device creation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanLogicalDeviceReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Selected physical device name.
+ pub device_name: String,
+ /// Graphics queue-family index used by the logical device.
+ pub graphics_queue_family: u32,
+ /// Present queue-family index used by the logical device.
+ pub present_queue_family: u32,
+ /// Enabled device extensions.
+ pub enabled_extensions: Vec<String>,
+}
+
/// Live Vulkan device/surface capability probe error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanRuntimeCapabilityError {
@@ -409,6 +440,45 @@ impl std::fmt::Display for VulkanRuntimeCapabilityError {
impl std::error::Error for VulkanRuntimeCapabilityError {}
+/// Vulkan logical device creation error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanLogicalDeviceError {
+ /// Runtime capability probing failed.
+ Runtime(VulkanRuntimeCapabilityError),
+ /// Device extension name contained an interior NUL byte.
+ InvalidExtensionName {
+ /// Invalid extension name.
+ extension: String,
+ },
+ /// Logical device creation failed.
+ CreateFailed {
+ /// Selected device name.
+ device: String,
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanLogicalDeviceError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Runtime(error) => write!(f, "{error}"),
+ Self::InvalidExtensionName { extension } => write!(
+ f,
+ "Vulkan device extension name contains an interior NUL byte: {extension:?}"
+ ),
+ Self::CreateFailed { device, result } => {
+ write!(
+ f,
+ "Vulkan logical device creation failed for {device}: {result}"
+ )
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanLogicalDeviceError {}
+
/// Builds a deterministic Vulkan surface plan from native window handles.
///
/// # Errors
@@ -480,6 +550,78 @@ pub fn probe_vulkan_runtime_capabilities(
surface: &VulkanSurfaceProbe,
drawable_extent: (u32, u32),
) -> Result<VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError> {
+ let selected = select_live_device_candidate(instance, surface, drawable_extent)?;
+ Ok(selected.runtime)
+}
+
+/// Creates a Vulkan logical device for the selected live surface-capable device.
+///
+/// # Errors
+///
+/// Returns [`VulkanLogicalDeviceError`] when runtime capability probing fails,
+/// device extension names are invalid, or `vkCreateDevice` fails.
+pub fn create_vulkan_logical_device_probe(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ drawable_extent: (u32, u32),
+) -> Result<VulkanLogicalDeviceProbe, VulkanLogicalDeviceError> {
+ let selected = select_live_device_candidate(instance, surface, drawable_extent)
+ .map_err(VulkanLogicalDeviceError::Runtime)?;
+ let capability = &selected.runtime.capability;
+ let queue_priorities = [1.0_f32];
+ let queue_families = unique_queue_families(
+ capability.graphics_queue_family,
+ capability.present_queue_family,
+ );
+ let queue_infos = queue_families
+ .iter()
+ .map(|queue_family| {
+ vk::DeviceQueueCreateInfo::default()
+ .queue_family_index(*queue_family)
+ .queue_priorities(&queue_priorities)
+ })
+ .collect::<Vec<_>>();
+ let extension_names = device_extension_cstrings(&capability.enabled_extensions)
+ .map_err(|extension| VulkanLogicalDeviceError::InvalidExtensionName { extension })?;
+ let extension_ptrs = extension_names
+ .iter()
+ .map(|extension| extension.as_ptr())
+ .collect::<Vec<_>>();
+ let create_info = vk::DeviceCreateInfo::default()
+ .queue_create_infos(&queue_infos)
+ .enabled_extension_names(&extension_ptrs);
+ // SAFETY: `selected.physical_device` belongs to `instance`; create data lives for the call.
+ let device = unsafe {
+ instance
+ .instance
+ .create_device(selected.physical_device, &create_info, None)
+ }
+ .map_err(|error| VulkanLogicalDeviceError::CreateFailed {
+ device: capability.device_name.clone(),
+ result: format!("{error:?}"),
+ })?;
+ // SAFETY: Queue family indices came from validated live queue families requested above.
+ let _graphics_queue = unsafe { device.get_device_queue(capability.graphics_queue_family, 0) };
+ // SAFETY: Queue family indices came from validated live queue families requested above.
+ let _present_queue = unsafe { device.get_device_queue(capability.present_queue_family, 0) };
+ Ok(VulkanLogicalDeviceProbe {
+ device,
+ report: VulkanLogicalDeviceReport {
+ schema: 1,
+ device_name: capability.device_name.clone(),
+ graphics_queue_family: capability.graphics_queue_family,
+ present_queue_family: capability.present_queue_family,
+ enabled_extensions: capability.enabled_extensions.clone(),
+ },
+ runtime: selected.runtime,
+ })
+}
+
+fn select_live_device_candidate(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ drawable_extent: (u32, u32),
+) -> Result<SelectedLiveDevice, VulkanRuntimeCapabilityError> {
let devices = {
// SAFETY: The Vulkan instance is live for this query and no handles are retained.
unsafe { instance.instance.enumerate_physical_devices() }.map_err(|error| {
@@ -509,13 +651,22 @@ pub fn probe_vulkan_runtime_capabilities(
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
})
.map_err(VulkanRuntimeCapabilityError::Swapchain)?;
- Ok(VulkanRuntimeCapabilityProbe {
- capability: best.capability,
- swapchain,
+ Ok(SelectedLiveDevice {
+ physical_device: best.physical_device,
+ runtime: VulkanRuntimeCapabilityProbe {
+ capability: best.capability,
+ swapchain,
+ },
})
}
+struct SelectedLiveDevice {
+ physical_device: vk::PhysicalDevice,
+ runtime: VulkanRuntimeCapabilityProbe,
+}
+
struct LiveDeviceCandidate {
+ physical_device: vk::PhysicalDevice,
capability: VulkanCapabilityReport,
surface_formats: Vec<VulkanSurfaceFormat>,
present_modes: Vec<i32>,
@@ -587,6 +738,7 @@ fn live_device_candidate(
};
let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
Ok(LiveDeviceCandidate {
+ physical_device: device,
capability,
surface_formats,
present_modes,
@@ -594,6 +746,21 @@ fn live_device_candidate(
})
}
+fn unique_queue_families(graphics: u32, present: u32) -> Vec<u32> {
+ if graphics == present {
+ vec![graphics]
+ } else {
+ vec![graphics, present]
+ }
+}
+
+fn device_extension_cstrings(values: &[String]) -> Result<Vec<CString>, String> {
+ values
+ .iter()
+ .map(|extension| CString::new(extension.as_str()).map_err(|_| extension.clone()))
+ .collect()
+}
+
fn physical_device_name(properties: &vk::PhysicalDeviceProperties, index: usize) -> String {
// SAFETY: Vulkan device names are fixed-size NUL-terminated C strings per the spec.
let name = unsafe { CStr::from_ptr(properties.device_name.as_ptr()) }
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index da8d68f..9425c31 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -14,9 +14,9 @@
use fparkan_platform::{NativeWindowHandles, WindowPort};
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
use fparkan_render_vulkan::{
- create_vulkan_instance_probe, create_vulkan_surface_probe, probe_vulkan_loader,
- probe_vulkan_runtime_capabilities, triangle_shader_manifest, validate_shader_manifest,
- VulkanInstanceConfig, VulkanInstanceProbe, VulkanRuntimeCapabilityProbe,
+ create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
+ probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
+ VulkanInstanceProbe, VulkanLogicalDeviceProbe,
};
use std::path::PathBuf;
use std::process::Command;
@@ -209,6 +209,11 @@ struct VulkanBootstrapProbe {
device_status: VulkanDeviceStatus,
device_name: Option<String>,
device_error: Option<String>,
+ logical_device_status: VulkanLogicalDeviceStatus,
+ logical_device_graphics_queue_family: Option<u32>,
+ logical_device_present_queue_family: Option<u32>,
+ logical_device_enabled_extension_count: Option<u32>,
+ logical_device_error: Option<String>,
swapchain_status: VulkanSwapchainStatus,
swapchain_width: Option<u32>,
swapchain_height: Option<u32>,
@@ -246,6 +251,11 @@ impl VulkanBootstrapProbe {
device_status: VulkanDeviceStatus::Skipped,
device_name: None,
device_error: None,
+ logical_device_status: VulkanLogicalDeviceStatus::Skipped,
+ logical_device_graphics_queue_family: None,
+ logical_device_present_queue_family: None,
+ logical_device_enabled_extension_count: None,
+ logical_device_error: None,
swapchain_status: VulkanSwapchainStatus::Skipped,
swapchain_width: None,
swapchain_height: None,
@@ -272,6 +282,11 @@ impl VulkanBootstrapProbe {
device_status: VulkanDeviceStatus::Skipped,
device_name: None,
device_error: None,
+ logical_device_status: VulkanLogicalDeviceStatus::Skipped,
+ logical_device_graphics_queue_family: None,
+ logical_device_present_queue_family: None,
+ logical_device_enabled_extension_count: None,
+ logical_device_error: None,
swapchain_status: VulkanSwapchainStatus::Skipped,
swapchain_width: None,
swapchain_height: None,
@@ -294,6 +309,11 @@ impl VulkanBootstrapProbe {
device_status: VulkanDeviceStatus::Skipped,
device_name: None,
device_error: None,
+ logical_device_status: VulkanLogicalDeviceStatus::Skipped,
+ logical_device_graphics_queue_family: None,
+ logical_device_present_queue_family: None,
+ logical_device_enabled_extension_count: None,
+ logical_device_error: None,
swapchain_status: VulkanSwapchainStatus::Skipped,
swapchain_width: None,
swapchain_height: None,
@@ -392,10 +412,11 @@ impl VulkanBootstrapProbe {
let Some(instance) = instance else {
self.device_status = VulkanDeviceStatus::Failed;
self.device_error = Some("Vulkan instance probe was not retained".to_string());
+ self.logical_device_status = VulkanLogicalDeviceStatus::Skipped;
self.swapchain_status = VulkanSwapchainStatus::Skipped;
return;
};
- match probe_vulkan_runtime_capabilities(
+ match create_vulkan_logical_device_probe(
instance,
surface,
(
@@ -403,23 +424,36 @@ impl VulkanBootstrapProbe {
self.window_height.unwrap_or(1).max(1),
),
) {
- Ok(runtime) => self.record_runtime_capabilities(runtime),
+ Ok(device) => self.record_logical_device_probe(&device),
Err(err) => {
self.device_status = VulkanDeviceStatus::Failed;
self.device_error = Some(err.to_string());
+ self.logical_device_status = VulkanLogicalDeviceStatus::Failed;
+ self.logical_device_error = Some(err.to_string());
self.swapchain_status = VulkanSwapchainStatus::Failed;
self.swapchain_error = Some(err.to_string());
}
}
}
- fn record_runtime_capabilities(&mut self, runtime: VulkanRuntimeCapabilityProbe) {
+ fn record_logical_device_probe(&mut self, device: &VulkanLogicalDeviceProbe) {
self.device_status = VulkanDeviceStatus::Selected;
- self.device_name = Some(runtime.capability.device_name);
+ self.device_name = Some(device.runtime.capability.device_name.clone());
+ self.logical_device_status = VulkanLogicalDeviceStatus::Created;
+ self.logical_device_graphics_queue_family = Some(device.report.graphics_queue_family);
+ self.logical_device_present_queue_family = Some(device.report.present_queue_family);
+ self.logical_device_enabled_extension_count = Some(
+ device
+ .report
+ .enabled_extensions
+ .len()
+ .try_into()
+ .unwrap_or(u32::MAX),
+ );
self.swapchain_status = VulkanSwapchainStatus::Planned;
- self.swapchain_width = Some(runtime.swapchain.extent.0);
- self.swapchain_height = Some(runtime.swapchain.extent.1);
- self.swapchain_image_count = Some(runtime.swapchain.image_count);
+ self.swapchain_width = Some(device.runtime.swapchain.extent.0);
+ self.swapchain_height = Some(device.runtime.swapchain.extent.1);
+ self.swapchain_image_count = Some(device.runtime.swapchain.image_count);
}
}
@@ -511,6 +545,23 @@ impl VulkanDeviceStatus {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum VulkanLogicalDeviceStatus {
+ Skipped,
+ Created,
+ Failed,
+}
+
+impl VulkanLogicalDeviceStatus {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Skipped => "skipped",
+ Self::Created => "created",
+ Self::Failed => "failed",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VulkanSwapchainStatus {
Skipped,
Planned,
@@ -635,6 +686,11 @@ fn validate_smoke_options(
"passed native smoke report requires selected Vulkan device".to_string()
);
}
+ if bootstrap.logical_device_status != VulkanLogicalDeviceStatus::Created {
+ return Err(
+ "passed native smoke report requires created Vulkan logical device".to_string(),
+ );
+ }
if bootstrap.swapchain_status != VulkanSwapchainStatus::Planned {
return Err(
"passed native smoke report requires planned Vulkan swapchain".to_string(),
@@ -651,7 +707,17 @@ fn render_smoke_report_json(
) -> Result<String, String> {
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
.map_err(|err| format!("shader manifest: {err}"))?;
- Ok(render_json_object(&[
+ let mut fields = base_smoke_report_fields(options, &shader_manifest.manifest_hash);
+ fields.extend(vulkan_bootstrap_fields(bootstrap));
+ fields.push(("reason", optional_string(options.reason.as_deref())));
+ Ok(render_json_object(&fields))
+}
+
+fn base_smoke_report_fields(
+ options: &SmokeOptions,
+ shader_manifest_hash: &str,
+) -> Vec<(&'static str, String)> {
+ vec![
("schema_version", json_string(SCHEMA_VERSION)),
("commit_sha", json_string(&current_git_commit_sha())),
("rust_toolchain", json_string(RUST_TOOLCHAIN)),
@@ -668,10 +734,12 @@ fn render_smoke_report_json(
"validation_error_count",
optional_u32(options.validation_error_count),
),
- (
- "shader_manifest_hash",
- json_string(&shader_manifest.manifest_hash),
- ),
+ ("shader_manifest_hash", json_string(shader_manifest_hash)),
+ ]
+}
+
+fn vulkan_bootstrap_fields(bootstrap: &VulkanBootstrapProbe) -> Vec<(&'static str, String)> {
+ vec![
(
"vulkan_loader_status",
json_string(bootstrap.loader_status.as_str()),
@@ -727,6 +795,26 @@ fn render_smoke_report_json(
optional_string(bootstrap.device_error.as_deref()),
),
(
+ "vulkan_logical_device_status",
+ json_string(bootstrap.logical_device_status.as_str()),
+ ),
+ (
+ "vulkan_logical_device_graphics_queue_family",
+ optional_u32(bootstrap.logical_device_graphics_queue_family),
+ ),
+ (
+ "vulkan_logical_device_present_queue_family",
+ optional_u32(bootstrap.logical_device_present_queue_family),
+ ),
+ (
+ "vulkan_logical_device_enabled_extension_count",
+ optional_u32(bootstrap.logical_device_enabled_extension_count),
+ ),
+ (
+ "vulkan_logical_device_error",
+ optional_string(bootstrap.logical_device_error.as_deref()),
+ ),
+ (
"vulkan_swapchain_status",
json_string(bootstrap.swapchain_status.as_str()),
),
@@ -746,8 +834,7 @@ fn render_smoke_report_json(
"vulkan_swapchain_error",
optional_string(bootstrap.swapchain_error.as_deref()),
),
- ("reason", optional_string(options.reason.as_deref())),
- ]))
+ ]
}
fn render_json_object(fields: &[(&str, String)]) -> String {
@@ -861,6 +948,11 @@ mod tests {
device_status: VulkanDeviceStatus::Selected,
device_name: Some("Stage 0 GPU".to_string()),
device_error: None,
+ logical_device_status: VulkanLogicalDeviceStatus::Created,
+ logical_device_graphics_queue_family: Some(0),
+ logical_device_present_queue_family: Some(0),
+ logical_device_enabled_extension_count: Some(1),
+ logical_device_error: None,
swapchain_status: VulkanSwapchainStatus::Planned,
swapchain_width: Some(1280),
swapchain_height: Some(720),
@@ -1282,6 +1374,40 @@ mod tests {
}
#[test]
+ fn rejects_passed_without_created_logical_device() {
+ let options = SmokeOptions::parse(&strings(&[
+ "--platform",
+ "linux",
+ "--out",
+ "target/native.json",
+ "--status",
+ "passed",
+ "--frames",
+ "300",
+ "--resize-count",
+ "1",
+ "--swapchain-recreate-count",
+ "1",
+ "--validation-error-count",
+ "0",
+ "--probe-surface",
+ ]))
+ .expect("options");
+
+ assert_eq!(
+ validate_smoke_options(
+ &options,
+ &VulkanBootstrapProbe {
+ logical_device_status: VulkanLogicalDeviceStatus::Failed,
+ logical_device_error: Some("Vulkan logical device creation failed".to_string()),
+ ..probe_fixture()
+ },
+ ),
+ Err("passed native smoke report requires created Vulkan logical device".to_string())
+ );
+ }
+
+ #[test]
fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> {
let options = SmokeOptions::parse(&strings(&[
"--platform",
@@ -1315,6 +1441,11 @@ mod tests {
device_status: VulkanDeviceStatus::Skipped,
device_name: None,
device_error: None,
+ logical_device_status: VulkanLogicalDeviceStatus::Skipped,
+ logical_device_graphics_queue_family: None,
+ logical_device_present_queue_family: None,
+ logical_device_enabled_extension_count: None,
+ logical_device_error: None,
swapchain_status: VulkanSwapchainStatus::Skipped,
swapchain_width: None,
swapchain_height: None,
@@ -1346,6 +1477,8 @@ mod tests {
));
assert!(json.contains("\"vulkan_device_status\": \"skipped\""));
assert!(json.contains("\"vulkan_device_name\": null"));
+ assert!(json.contains("\"vulkan_logical_device_status\": \"skipped\""));
+ assert!(json.contains("\"vulkan_logical_device_graphics_queue_family\": null"));
assert!(json.contains("\"vulkan_swapchain_status\": \"skipped\""));
assert!(json.contains("\"vulkan_swapchain_width\": null"));
assert!(json.contains("\"reason\": \"runner unavailable\""));
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index 7c8e40d..ed9a090 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -62,6 +62,7 @@ S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_compl
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_planned_swapchain
+S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md
index 5a3c19d..6fc3239 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -62,6 +62,7 @@
`S0-VK-030`
`S0-VK-031`
`S0-VK-032`
+`S0-VK-033`
`S0-LIMIT-001`
`S0-LIMIT-002`
`L1-P1-NRES-001`
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 9ca8da9..4babad2 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1545,6 +1545,13 @@ fn validate_native_smoke_report(
expect_string_field(
platform,
report,
+ "vulkan_logical_device_status",
+ "created",
+ failures,
+ );
+ expect_string_field(
+ platform,
+ report,
"vulkan_swapchain_status",
"planned",
failures,
@@ -1558,6 +1565,27 @@ fn validate_native_smoke_report(
expect_nonempty_string(platform, report, "target_triple", failures);
expect_nonempty_string(platform, report, "shader_manifest_hash", failures);
expect_nonempty_string(platform, report, "vulkan_device_name", failures);
+ expect_u64_at_least(
+ platform,
+ report,
+ "vulkan_logical_device_enabled_extension_count",
+ 1,
+ failures,
+ );
+ expect_u64_at_least(
+ platform,
+ report,
+ "vulkan_logical_device_graphics_queue_family",
+ 0,
+ failures,
+ );
+ expect_u64_at_least(
+ platform,
+ report,
+ "vulkan_logical_device_present_queue_family",
+ 0,
+ failures,
+ );
expect_u64_at_least(platform, report, "vulkan_swapchain_width", 1, failures);
expect_u64_at_least(platform, report, "vulkan_swapchain_height", 1, failures);
expect_u64_at_least(
@@ -2260,6 +2288,10 @@ mod tests {
"vulkan_surface_status": "created",
"vulkan_device_status": "selected",
"vulkan_device_name": format!("{platform} GPU"),
+ "vulkan_logical_device_status": "created",
+ "vulkan_logical_device_graphics_queue_family": 0,
+ "vulkan_logical_device_present_queue_family": 0,
+ "vulkan_logical_device_enabled_extension_count": 1,
"vulkan_swapchain_status": "planned",
"vulkan_swapchain_width": 1280,
"vulkan_swapchain_height": 720,