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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
|
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]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -104,6 +124,12 @@ version = "2.13.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -230,6 +256,36 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
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]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -618,6 +674,8 @@ dependencies = [
|
|||||||
name = "fparkan-render-vulkan"
|
name = "fparkan-render-vulkan"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ash",
|
||||||
|
"ash-window",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
]
|
]
|
||||||
@@ -1015,6 +1073,15 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
@@ -1101,6 +1168,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc-sys"
|
name = "objc-sys"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@@ -1453,6 +1529,18 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ash = "0.38"
|
||||||
|
ash-window = "0.13"
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render" }
|
fparkan-render = { path = "../../crates/fparkan-render" }
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,18 @@
|
|||||||
//!
|
//!
|
||||||
//! This crate is the declared low-level Vulkan boundary.
|
//! This crate is the declared low-level Vulkan boundary.
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
use fparkan_platform::RenderRequest;
|
use fparkan_platform::RenderRequest;
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
||||||
};
|
};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
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.
|
/// Vulkan backend migration readiness.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum VulkanBackendState {
|
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.
|
/// Diagnostics for Vulkan backend setup and frame progression.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct VulkanBackendReport {
|
pub struct VulkanBackendReport {
|
||||||
@@ -194,4 +524,151 @@ mod tests {
|
|||||||
assert!(backend.report().last_capture_size > 0);
|
assert!(backend.report().last_capture_size > 0);
|
||||||
Ok(())
|
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-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-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-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-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
|
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
|
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
|
||||||
|
|||||||
|
@@ -21,7 +21,11 @@
|
|||||||
`S0-CORPUS-006`
|
`S0-CORPUS-006`
|
||||||
`S0-CLI-001`
|
`S0-CLI-001`
|
||||||
`S0-CLI-002`
|
`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-001`
|
||||||
`S0-LIMIT-002`
|
`S0-LIMIT-002`
|
||||||
`L1-P1-NRES-001`
|
`L1-P1-NRES-001`
|
||||||
|
|||||||
Reference in New Issue
Block a user