feat: add Vulkan capability selection boundary
This commit is contained in:
Generated
+88
@@ -80,6 +80,26 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
|
||||
|
||||
[[package]]
|
||||
name = "ash"
|
||||
version = "0.38.0+1.3.281"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ash-window"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"raw-window-handle",
|
||||
"raw-window-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -104,6 +124,12 @@ version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -230,6 +256,36 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -618,6 +674,8 @@ dependencies = [
|
||||
name = "fparkan-render-vulkan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
"fparkan-platform",
|
||||
"fparkan-render",
|
||||
]
|
||||
@@ -1015,6 +1073,15 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
@@ -1101,6 +1168,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -1453,6 +1529,18 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-metal"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
"objc",
|
||||
"raw-window-handle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
|
||||
@@ -6,6 +6,8 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ash = "0.38"
|
||||
ash-window = "0.13"
|
||||
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||
fparkan-render = { path = "../../crates/fparkan-render" }
|
||||
|
||||
|
||||
@@ -27,12 +27,18 @@
|
||||
//!
|
||||
//! This crate is the declared low-level Vulkan boundary.
|
||||
|
||||
use ash::vk;
|
||||
use fparkan_platform::RenderRequest;
|
||||
use fparkan_render::{
|
||||
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
||||
};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Minimum Vulkan API version accepted by the Stage 0 backend.
|
||||
pub const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
|
||||
const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
|
||||
const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset";
|
||||
|
||||
/// Vulkan backend migration readiness.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum VulkanBackendState {
|
||||
@@ -50,6 +56,330 @@ impl Default for VulkanBackendState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetic physical-device type used by deterministic capability scoring.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum VulkanDeviceType {
|
||||
/// Discrete GPU.
|
||||
DiscreteGpu,
|
||||
/// Integrated GPU.
|
||||
IntegratedGpu,
|
||||
/// CPU or software Vulkan implementation.
|
||||
Cpu,
|
||||
/// Other or unknown implementation.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl VulkanDeviceType {
|
||||
const fn score_bonus(self) -> i32 {
|
||||
match self {
|
||||
Self::DiscreteGpu => 1_000,
|
||||
Self::IntegratedGpu => 700,
|
||||
Self::Cpu => 100,
|
||||
Self::Other => 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue-family capabilities needed by the Stage 0 renderer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanQueueFamily {
|
||||
/// Stable queue-family index.
|
||||
pub index: u32,
|
||||
/// Whether the family supports graphics commands.
|
||||
pub graphics: bool,
|
||||
/// Whether the family supports presentation for the target surface.
|
||||
pub present: bool,
|
||||
}
|
||||
|
||||
/// Surface format capability needed by the Stage 0 swapchain policy.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanSurfaceFormat {
|
||||
/// Vulkan format numeric value.
|
||||
pub format: i32,
|
||||
/// Vulkan color-space numeric value.
|
||||
pub color_space: i32,
|
||||
}
|
||||
|
||||
/// Synthetic physical-device capabilities used by negative tests and reports.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanPhysicalDeviceRecord {
|
||||
/// Human-readable device name.
|
||||
pub name: String,
|
||||
/// Reported Vulkan API version.
|
||||
pub api_version: u32,
|
||||
/// Device class.
|
||||
pub device_type: VulkanDeviceType,
|
||||
/// Supported device-extension names.
|
||||
pub extensions: Vec<String>,
|
||||
/// Queue-family capabilities.
|
||||
pub queue_families: Vec<VulkanQueueFamily>,
|
||||
/// Surface formats accepted by the target surface.
|
||||
pub surface_formats: Vec<VulkanSurfaceFormat>,
|
||||
}
|
||||
|
||||
impl VulkanPhysicalDeviceRecord {
|
||||
/// Returns whether the device supports an extension name.
|
||||
#[must_use]
|
||||
pub fn supports_extension(&self, extension: &str) -> bool {
|
||||
self.extensions
|
||||
.iter()
|
||||
.any(|candidate| candidate == extension)
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected device and queue capability report.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct VulkanCapabilityReport {
|
||||
/// Report schema version.
|
||||
pub schema: u32,
|
||||
/// Selected device name.
|
||||
pub device_name: String,
|
||||
/// Selected Vulkan API version.
|
||||
pub vulkan_api_version: u32,
|
||||
/// Deterministic score used for device selection.
|
||||
pub score: i32,
|
||||
/// Graphics queue family index.
|
||||
pub graphics_queue_family: u32,
|
||||
/// Present queue family index.
|
||||
pub present_queue_family: u32,
|
||||
/// Whether portability subset is enabled for the selected device.
|
||||
pub portability_subset: bool,
|
||||
/// Enabled device extensions.
|
||||
pub enabled_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Vulkan capability selection error.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum VulkanCapabilityError {
|
||||
/// No physical devices were available.
|
||||
NoPhysicalDevice,
|
||||
/// Device API version is lower than the Stage 0 minimum.
|
||||
ApiVersionTooLow {
|
||||
/// Required Vulkan API version.
|
||||
required: u32,
|
||||
/// Reported Vulkan API version.
|
||||
found: u32,
|
||||
},
|
||||
/// Required graphics queue is unavailable.
|
||||
NoGraphicsQueue {
|
||||
/// Device name that failed validation.
|
||||
device: String,
|
||||
},
|
||||
/// Required present queue is unavailable.
|
||||
NoPresentQueue {
|
||||
/// Device name that failed validation.
|
||||
device: String,
|
||||
},
|
||||
/// Swapchain device extension is unavailable.
|
||||
MissingSwapchainExtension {
|
||||
/// Device name that failed validation.
|
||||
device: String,
|
||||
},
|
||||
/// No compatible surface format exists.
|
||||
MissingSurfaceFormat {
|
||||
/// Device name that failed validation.
|
||||
device: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VulkanCapabilityError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoPhysicalDevice => write!(f, "no Vulkan physical device available"),
|
||||
Self::ApiVersionTooLow { required, found } => write!(
|
||||
f,
|
||||
"Vulkan API version too low: required {}, found {}",
|
||||
format_api_version(*required),
|
||||
format_api_version(*found)
|
||||
),
|
||||
Self::NoGraphicsQueue { device } => {
|
||||
write!(f, "Vulkan device {device} has no graphics queue")
|
||||
}
|
||||
Self::NoPresentQueue { device } => {
|
||||
write!(f, "Vulkan device {device} has no present queue")
|
||||
}
|
||||
Self::MissingSwapchainExtension { device } => {
|
||||
write!(f, "Vulkan device {device} lacks {KHR_SWAPCHAIN_EXTENSION}")
|
||||
}
|
||||
Self::MissingSurfaceFormat { device } => {
|
||||
write!(f, "Vulkan device {device} has no compatible surface format")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VulkanCapabilityError {}
|
||||
|
||||
/// Selects a Vulkan physical device using deterministic Stage 0 policy.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`VulkanCapabilityError`] when no candidate satisfies the minimum
|
||||
/// API version, queue, swapchain-extension and surface-format requirements.
|
||||
pub fn select_physical_device(
|
||||
devices: &[VulkanPhysicalDeviceRecord],
|
||||
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
|
||||
if devices.is_empty() {
|
||||
return Err(VulkanCapabilityError::NoPhysicalDevice);
|
||||
}
|
||||
|
||||
let mut best = None;
|
||||
for device in devices {
|
||||
let report = validate_device(device)?;
|
||||
match &best {
|
||||
Some(existing) if compare_reports(&report, existing) != std::cmp::Ordering::Greater => {
|
||||
}
|
||||
_ => best = Some(report),
|
||||
}
|
||||
}
|
||||
best.ok_or(VulkanCapabilityError::NoPhysicalDevice)
|
||||
}
|
||||
|
||||
fn validate_device(
|
||||
device: &VulkanPhysicalDeviceRecord,
|
||||
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
|
||||
if device.api_version < MIN_VULKAN_API_VERSION {
|
||||
return Err(VulkanCapabilityError::ApiVersionTooLow {
|
||||
required: MIN_VULKAN_API_VERSION,
|
||||
found: device.api_version,
|
||||
});
|
||||
}
|
||||
if !device.supports_extension(KHR_SWAPCHAIN_EXTENSION) {
|
||||
return Err(VulkanCapabilityError::MissingSwapchainExtension {
|
||||
device: device.name.clone(),
|
||||
});
|
||||
}
|
||||
if device.surface_formats.is_empty() {
|
||||
return Err(VulkanCapabilityError::MissingSurfaceFormat {
|
||||
device: device.name.clone(),
|
||||
});
|
||||
}
|
||||
let graphics_queue_family = device
|
||||
.queue_families
|
||||
.iter()
|
||||
.find(|family| family.graphics)
|
||||
.ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue {
|
||||
device: device.name.clone(),
|
||||
})?
|
||||
.index;
|
||||
let present_queue_family = device
|
||||
.queue_families
|
||||
.iter()
|
||||
.find(|family| family.present)
|
||||
.ok_or_else(|| VulkanCapabilityError::NoPresentQueue {
|
||||
device: device.name.clone(),
|
||||
})?
|
||||
.index;
|
||||
|
||||
let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION);
|
||||
let mut enabled_extensions = vec![KHR_SWAPCHAIN_EXTENSION.to_string()];
|
||||
if portability_subset {
|
||||
enabled_extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string());
|
||||
}
|
||||
|
||||
Ok(VulkanCapabilityReport {
|
||||
schema: 1,
|
||||
device_name: device.name.clone(),
|
||||
vulkan_api_version: device.api_version,
|
||||
score: score_device(device, graphics_queue_family, present_queue_family),
|
||||
graphics_queue_family,
|
||||
present_queue_family,
|
||||
portability_subset,
|
||||
enabled_extensions,
|
||||
})
|
||||
}
|
||||
|
||||
fn score_device(
|
||||
device: &VulkanPhysicalDeviceRecord,
|
||||
graphics_queue_family: u32,
|
||||
present_queue_family: u32,
|
||||
) -> i32 {
|
||||
let unified_queue_bonus = if graphics_queue_family == present_queue_family {
|
||||
100
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let portability_penalty = if device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION) {
|
||||
-50
|
||||
} else {
|
||||
0
|
||||
};
|
||||
device.device_type.score_bonus()
|
||||
+ unified_queue_bonus
|
||||
+ portability_penalty
|
||||
+ i32::try_from(device.surface_formats.len()).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
fn compare_reports(
|
||||
left: &VulkanCapabilityReport,
|
||||
right: &VulkanCapabilityReport,
|
||||
) -> std::cmp::Ordering {
|
||||
left.score
|
||||
.cmp(&right.score)
|
||||
.then_with(|| right.device_name.cmp(&left.device_name))
|
||||
}
|
||||
|
||||
/// Renders a deterministic JSON capability report.
|
||||
#[must_use]
|
||||
pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("{\"schema\":");
|
||||
out.push_str(&report.schema.to_string());
|
||||
out.push_str(",\"vulkan_api\":\"");
|
||||
out.push_str(&format_api_version(report.vulkan_api_version));
|
||||
out.push_str("\",\"device_name\":");
|
||||
push_json_string(&mut out, &report.device_name);
|
||||
out.push_str(",\"score\":");
|
||||
out.push_str(&report.score.to_string());
|
||||
out.push_str(",\"graphics_queue_family\":");
|
||||
out.push_str(&report.graphics_queue_family.to_string());
|
||||
out.push_str(",\"present_queue_family\":");
|
||||
out.push_str(&report.present_queue_family.to_string());
|
||||
out.push_str(",\"portability_subset\":");
|
||||
out.push_str(if report.portability_subset {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
});
|
||||
out.push_str(",\"enabled_extensions\":[");
|
||||
for (index, extension) in report.enabled_extensions.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
push_json_string(&mut out, extension);
|
||||
}
|
||||
out.push_str("]}");
|
||||
out
|
||||
}
|
||||
|
||||
fn format_api_version(version: u32) -> String {
|
||||
format!(
|
||||
"{}.{}.{}",
|
||||
vk::api_version_major(version),
|
||||
vk::api_version_minor(version),
|
||||
vk::api_version_patch(version)
|
||||
)
|
||||
}
|
||||
|
||||
fn push_json_string(out: &mut String, value: &str) {
|
||||
out.push('"');
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if c.is_control() => {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(out, "\\u{:04x}", c as u32);
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
/// Diagnostics for Vulkan backend setup and frame progression.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VulkanBackendReport {
|
||||
@@ -194,4 +524,151 @@ mod tests {
|
||||
assert!(backend.report().last_capture_size > 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_scoring_is_deterministic_and_prefers_discrete_unified_queue() {
|
||||
let devices = vec![
|
||||
device("SwiftShader", VulkanDeviceType::Cpu, 0, true, false),
|
||||
device("Discrete", VulkanDeviceType::DiscreteGpu, 1, true, false),
|
||||
device(
|
||||
"Integrated",
|
||||
VulkanDeviceType::IntegratedGpu,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
let report = select_physical_device(&devices).expect("selected device");
|
||||
|
||||
assert_eq!(report.device_name, "Discrete");
|
||||
assert_eq!(report.graphics_queue_family, 1);
|
||||
assert_eq!(report.present_queue_family, 1);
|
||||
assert!(!report.portability_subset);
|
||||
assert_eq!(report.enabled_extensions, vec![KHR_SWAPCHAIN_EXTENSION]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portability_subset_is_reported_and_enabled_when_exposed() {
|
||||
let report = select_physical_device(&[device(
|
||||
"MoltenVK",
|
||||
VulkanDeviceType::IntegratedGpu,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
)])
|
||||
.expect("selected device");
|
||||
|
||||
assert!(report.portability_subset);
|
||||
assert_eq!(
|
||||
report.enabled_extensions,
|
||||
vec![
|
||||
KHR_SWAPCHAIN_EXTENSION.to_string(),
|
||||
KHR_PORTABILITY_SUBSET_EXTENSION.to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_loader_candidates_are_reported() {
|
||||
assert_eq!(
|
||||
select_physical_device(&[]),
|
||||
Err(VulkanCapabilityError::NoPhysicalDevice)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_low_api_version() {
|
||||
let mut candidate = device("Old GPU", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||
candidate.api_version = vk::API_VERSION_1_0;
|
||||
|
||||
assert!(matches!(
|
||||
select_physical_device(&[candidate]),
|
||||
Err(VulkanCapabilityError::ApiVersionTooLow { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_graphics_present_swapchain_and_format() {
|
||||
let mut no_graphics = device("No graphics", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||
no_graphics.queue_families[0].graphics = false;
|
||||
assert!(matches!(
|
||||
select_physical_device(&[no_graphics]),
|
||||
Err(VulkanCapabilityError::NoGraphicsQueue { .. })
|
||||
));
|
||||
|
||||
let mut no_present = device("No present", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||
no_present.queue_families[0].present = false;
|
||||
assert!(matches!(
|
||||
select_physical_device(&[no_present]),
|
||||
Err(VulkanCapabilityError::NoPresentQueue { .. })
|
||||
));
|
||||
|
||||
let no_swapchain = device(
|
||||
"No swapchain",
|
||||
VulkanDeviceType::DiscreteGpu,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert!(matches!(
|
||||
select_physical_device(&[no_swapchain]),
|
||||
Err(VulkanCapabilityError::MissingSwapchainExtension { .. })
|
||||
));
|
||||
|
||||
let mut no_format = device("No format", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||
no_format.surface_formats.clear();
|
||||
assert!(matches!(
|
||||
select_physical_device(&[no_format]),
|
||||
Err(VulkanCapabilityError::MissingSurfaceFormat { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_report_json_is_stable() {
|
||||
let report = select_physical_device(&[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\"]}"
|
||||
);
|
||||
}
|
||||
|
||||
fn device(
|
||||
name: &str,
|
||||
device_type: VulkanDeviceType,
|
||||
queue_index: u32,
|
||||
swapchain: bool,
|
||||
portability_subset: bool,
|
||||
) -> VulkanPhysicalDeviceRecord {
|
||||
let mut extensions = Vec::new();
|
||||
if swapchain {
|
||||
extensions.push(KHR_SWAPCHAIN_EXTENSION.to_string());
|
||||
}
|
||||
if portability_subset {
|
||||
extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string());
|
||||
}
|
||||
VulkanPhysicalDeviceRecord {
|
||||
name: name.to_string(),
|
||||
api_version: MIN_VULKAN_API_VERSION,
|
||||
device_type,
|
||||
extensions,
|
||||
queue_families: vec![VulkanQueueFamily {
|
||||
index: queue_index,
|
||||
graphics: true,
|
||||
present: true,
|
||||
}],
|
||||
surface_formats: vec![VulkanSurfaceFormat {
|
||||
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ S0-CORPUS-005 covered cargo test -p fparkan-corpus --offline fingerprint_changes
|
||||
S0-CORPUS-006 covered cargo test -p fparkan-corpus --offline atomic_report_write
|
||||
S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_mapped
|
||||
S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version
|
||||
S0-GL-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
||||
S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
||||
S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
|
||||
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
|
||||
S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
|
||||
S0-VK-005 covered cargo test -p fparkan-render-vulkan --offline capability_report_json_is_stable
|
||||
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
|
||||
|
||||
|
@@ -21,7 +21,11 @@
|
||||
`S0-CORPUS-006`
|
||||
`S0-CLI-001`
|
||||
`S0-CLI-002`
|
||||
`S0-GL-001`
|
||||
`S0-VK-001`
|
||||
`S0-VK-002`
|
||||
`S0-VK-003`
|
||||
`S0-VK-004`
|
||||
`S0-VK-005`
|
||||
`S0-LIMIT-001`
|
||||
`S0-LIMIT-002`
|
||||
`L1-P1-NRES-001`
|
||||
|
||||
Reference in New Issue
Block a user