aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 04:08:10 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:34 +0300
commit0b0ed87650aa0bfb71ac680c3679b85546e4272b (patch)
tree0fe627f26199622ddadbedaab3d685af59e37c95
parent6a6393038eb9accc908e4d54555702b426e38780 (diff)
downloadfparkan-0b0ed87650aa0bfb71ac680c3679b85546e4272b.tar.xz
fparkan-0b0ed87650aa0bfb71ac680c3679b85546e4272b.zip
refactor(vulkan-ffi): narrow audited unsafe boundary
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs4966
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs4968
-rw-r--r--xtask/src/main.rs2
3 files changed, 4971 insertions, 4965 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
new file mode 100644
index 0000000..097c5a9
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -0,0 +1,4966 @@
+#![allow(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
+#![deny(unsafe_op_in_unsafe_fn)]
+//! Vulkan adapter facade and migration-ready backend surface contract.
+//!
+//! This module intentionally keeps backend-agnostic command validation in the
+//! shared render crate while exposing deterministic lifecycle telemetry used by
+//! Stage 0 acceptance evidence.
+//!
+//! This crate is the declared low-level Vulkan boundary.
+
+use ash::{
+ khr::{surface, swapchain},
+ vk,
+};
+use fparkan_binary::{sha256, sha256_hex};
+use fparkan_platform::{NativeWindowHandles, RenderRequest};
+use fparkan_render::{
+ canonical_capture, validate_command_list, FrameOutput, RenderBackend, RenderCommand,
+ RenderCommandList, RenderError,
+};
+use std::collections::BTreeSet;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::sync::atomic::{AtomicU32, Ordering};
+use std::sync::Mutex;
+/// 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";
+const KHR_PORTABILITY_ENUMERATION_EXTENSION: &str = "VK_KHR_portability_enumeration";
+const EXT_DEBUG_UTILS_EXTENSION: &str = "VK_EXT_debug_utils";
+const VALIDATION_LAYER_NAME: &str = "VK_LAYER_KHRONOS_validation";
+const SPIRV_MAGIC: u32 = 0x0723_0203;
+const SPIRV_VERSION_1_0: u32 = 0x0001_0000;
+const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[
+ SPIRV_MAGIC,
+ SPIRV_VERSION_1_0,
+ 0x0008_000b,
+ 0x0000_0021,
+ 0x0000_0000,
+ 0x0002_0011,
+ 0x0000_0001,
+ 0x0006_000b,
+ 0x0000_0001,
+ 0x4c53_4c47,
+ 0x6474_732e,
+ 0x3035_342e,
+ 0x0000_0000,
+ 0x0003_000e,
+ 0x0000_0000,
+ 0x0000_0001,
+ 0x0009_000f,
+ 0x0000_0000,
+ 0x0000_0004,
+ 0x6e69_616d,
+ 0x0000_0000,
+ 0x0000_0009,
+ 0x0000_000b,
+ 0x0000_0013,
+ 0x0000_0018,
+ 0x0003_0003,
+ 0x0000_0002,
+ 0x0000_01c2,
+ 0x0004_0005,
+ 0x0000_0004,
+ 0x6e69_616d,
+ 0x0000_0000,
+ 0x0005_0005,
+ 0x0000_0009,
+ 0x5f74_756f,
+ 0x6f6c_6f63,
+ 0x0000_0072,
+ 0x0005_0005,
+ 0x0000_000b,
+ 0x635f_6e69,
+ 0x726f_6c6f,
+ 0x0000_0000,
+ 0x0006_0005,
+ 0x0000_0011,
+ 0x505f_6c67,
+ 0x6556_7265,
+ 0x7865_7472,
+ 0x0000_0000,
+ 0x0006_0006,
+ 0x0000_0011,
+ 0x0000_0000,
+ 0x505f_6c67,
+ 0x7469_736f,
+ 0x006e_6f69,
+ 0x0007_0006,
+ 0x0000_0011,
+ 0x0000_0001,
+ 0x505f_6c67,
+ 0x746e_696f,
+ 0x657a_6953,
+ 0x0000_0000,
+ 0x0007_0006,
+ 0x0000_0011,
+ 0x0000_0002,
+ 0x435f_6c67,
+ 0x4470_696c,
+ 0x6174_7369,
+ 0x0065_636e,
+ 0x0007_0006,
+ 0x0000_0011,
+ 0x0000_0003,
+ 0x435f_6c67,
+ 0x446c_6c75,
+ 0x6174_7369,
+ 0x0065_636e,
+ 0x0003_0005,
+ 0x0000_0013,
+ 0x0000_0000,
+ 0x0005_0005,
+ 0x0000_0018,
+ 0x705f_6e69,
+ 0x7469_736f,
+ 0x006e_6f69,
+ 0x0004_0047,
+ 0x0000_0009,
+ 0x0000_001e,
+ 0x0000_0000,
+ 0x0004_0047,
+ 0x0000_000b,
+ 0x0000_001e,
+ 0x0000_0001,
+ 0x0003_0047,
+ 0x0000_0011,
+ 0x0000_0002,
+ 0x0005_0048,
+ 0x0000_0011,
+ 0x0000_0000,
+ 0x0000_000b,
+ 0x0000_0000,
+ 0x0005_0048,
+ 0x0000_0011,
+ 0x0000_0001,
+ 0x0000_000b,
+ 0x0000_0001,
+ 0x0005_0048,
+ 0x0000_0011,
+ 0x0000_0002,
+ 0x0000_000b,
+ 0x0000_0003,
+ 0x0005_0048,
+ 0x0000_0011,
+ 0x0000_0003,
+ 0x0000_000b,
+ 0x0000_0004,
+ 0x0004_0047,
+ 0x0000_0018,
+ 0x0000_001e,
+ 0x0000_0000,
+ 0x0002_0013,
+ 0x0000_0002,
+ 0x0003_0021,
+ 0x0000_0003,
+ 0x0000_0002,
+ 0x0003_0016,
+ 0x0000_0006,
+ 0x0000_0020,
+ 0x0004_0017,
+ 0x0000_0007,
+ 0x0000_0006,
+ 0x0000_0003,
+ 0x0004_0020,
+ 0x0000_0008,
+ 0x0000_0003,
+ 0x0000_0007,
+ 0x0004_003b,
+ 0x0000_0008,
+ 0x0000_0009,
+ 0x0000_0003,
+ 0x0004_0020,
+ 0x0000_000a,
+ 0x0000_0001,
+ 0x0000_0007,
+ 0x0004_003b,
+ 0x0000_000a,
+ 0x0000_000b,
+ 0x0000_0001,
+ 0x0004_0017,
+ 0x0000_000d,
+ 0x0000_0006,
+ 0x0000_0004,
+ 0x0004_0015,
+ 0x0000_000e,
+ 0x0000_0020,
+ 0x0000_0000,
+ 0x0004_002b,
+ 0x0000_000e,
+ 0x0000_000f,
+ 0x0000_0001,
+ 0x0004_001c,
+ 0x0000_0010,
+ 0x0000_0006,
+ 0x0000_000f,
+ 0x0006_001e,
+ 0x0000_0011,
+ 0x0000_000d,
+ 0x0000_0006,
+ 0x0000_0010,
+ 0x0000_0010,
+ 0x0004_0020,
+ 0x0000_0012,
+ 0x0000_0003,
+ 0x0000_0011,
+ 0x0004_003b,
+ 0x0000_0012,
+ 0x0000_0013,
+ 0x0000_0003,
+ 0x0004_0015,
+ 0x0000_0014,
+ 0x0000_0020,
+ 0x0000_0001,
+ 0x0004_002b,
+ 0x0000_0014,
+ 0x0000_0015,
+ 0x0000_0000,
+ 0x0004_0017,
+ 0x0000_0016,
+ 0x0000_0006,
+ 0x0000_0002,
+ 0x0004_0020,
+ 0x0000_0017,
+ 0x0000_0001,
+ 0x0000_0016,
+ 0x0004_003b,
+ 0x0000_0017,
+ 0x0000_0018,
+ 0x0000_0001,
+ 0x0004_002b,
+ 0x0000_0006,
+ 0x0000_001a,
+ 0x0000_0000,
+ 0x0004_002b,
+ 0x0000_0006,
+ 0x0000_001b,
+ 0x3f80_0000,
+ 0x0004_0020,
+ 0x0000_001f,
+ 0x0000_0003,
+ 0x0000_000d,
+ 0x0005_0036,
+ 0x0000_0002,
+ 0x0000_0004,
+ 0x0000_0000,
+ 0x0000_0003,
+ 0x0002_00f8,
+ 0x0000_0005,
+ 0x0004_003d,
+ 0x0000_0007,
+ 0x0000_000c,
+ 0x0000_000b,
+ 0x0003_003e,
+ 0x0000_0009,
+ 0x0000_000c,
+ 0x0004_003d,
+ 0x0000_0016,
+ 0x0000_0019,
+ 0x0000_0018,
+ 0x0005_0051,
+ 0x0000_0006,
+ 0x0000_001c,
+ 0x0000_0019,
+ 0x0000_0000,
+ 0x0005_0051,
+ 0x0000_0006,
+ 0x0000_001d,
+ 0x0000_0019,
+ 0x0000_0001,
+ 0x0007_0050,
+ 0x0000_000d,
+ 0x0000_001e,
+ 0x0000_001c,
+ 0x0000_001d,
+ 0x0000_001a,
+ 0x0000_001b,
+ 0x0005_0041,
+ 0x0000_001f,
+ 0x0000_0020,
+ 0x0000_0013,
+ 0x0000_0015,
+ 0x0003_003e,
+ 0x0000_0020,
+ 0x0000_001e,
+ 0x0001_00fd,
+ 0x0001_0038,
+];
+const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
+ SPIRV_MAGIC,
+ SPIRV_VERSION_1_0,
+ 0x0008_000b,
+ 0x0000_0013,
+ 0x0000_0000,
+ 0x0002_0011,
+ 0x0000_0001,
+ 0x0006_000b,
+ 0x0000_0001,
+ 0x4c53_4c47,
+ 0x6474_732e,
+ 0x3035_342e,
+ 0x0000_0000,
+ 0x0003_000e,
+ 0x0000_0000,
+ 0x0000_0001,
+ 0x0007_000f,
+ 0x0000_0004,
+ 0x0000_0004,
+ 0x6e69_616d,
+ 0x0000_0000,
+ 0x0000_0009,
+ 0x0000_000c,
+ 0x0003_0010,
+ 0x0000_0004,
+ 0x0000_0007,
+ 0x0003_0003,
+ 0x0000_0002,
+ 0x0000_01c2,
+ 0x0004_0005,
+ 0x0000_0004,
+ 0x6e69_616d,
+ 0x0000_0000,
+ 0x0005_0005,
+ 0x0000_0009,
+ 0x5f74_756f,
+ 0x6f6c_6f63,
+ 0x0000_0072,
+ 0x0005_0005,
+ 0x0000_000c,
+ 0x635f_6e69,
+ 0x726f_6c6f,
+ 0x0000_0000,
+ 0x0004_0047,
+ 0x0000_0009,
+ 0x0000_001e,
+ 0x0000_0000,
+ 0x0004_0047,
+ 0x0000_000c,
+ 0x0000_001e,
+ 0x0000_0000,
+ 0x0002_0013,
+ 0x0000_0002,
+ 0x0003_0021,
+ 0x0000_0003,
+ 0x0000_0002,
+ 0x0003_0016,
+ 0x0000_0006,
+ 0x0000_0020,
+ 0x0004_0017,
+ 0x0000_0007,
+ 0x0000_0006,
+ 0x0000_0004,
+ 0x0004_0020,
+ 0x0000_0008,
+ 0x0000_0003,
+ 0x0000_0007,
+ 0x0004_003b,
+ 0x0000_0008,
+ 0x0000_0009,
+ 0x0000_0003,
+ 0x0004_0017,
+ 0x0000_000a,
+ 0x0000_0006,
+ 0x0000_0003,
+ 0x0004_0020,
+ 0x0000_000b,
+ 0x0000_0001,
+ 0x0000_000a,
+ 0x0004_003b,
+ 0x0000_000b,
+ 0x0000_000c,
+ 0x0000_0001,
+ 0x0004_002b,
+ 0x0000_0006,
+ 0x0000_000e,
+ 0x3f80_0000,
+ 0x0005_0036,
+ 0x0000_0002,
+ 0x0000_0004,
+ 0x0000_0000,
+ 0x0000_0003,
+ 0x0002_00f8,
+ 0x0000_0005,
+ 0x0004_003d,
+ 0x0000_000a,
+ 0x0000_000d,
+ 0x0000_000c,
+ 0x0005_0051,
+ 0x0000_0006,
+ 0x0000_000f,
+ 0x0000_000d,
+ 0x0000_0000,
+ 0x0005_0051,
+ 0x0000_0006,
+ 0x0000_0010,
+ 0x0000_000d,
+ 0x0000_0001,
+ 0x0005_0051,
+ 0x0000_0006,
+ 0x0000_0011,
+ 0x0000_000d,
+ 0x0000_0002,
+ 0x0007_0050,
+ 0x0000_0007,
+ 0x0000_0012,
+ 0x0000_000f,
+ 0x0000_0010,
+ 0x0000_0011,
+ 0x0000_000e,
+ 0x0003_003e,
+ 0x0000_0009,
+ 0x0000_0012,
+ 0x0001_00fd,
+ 0x0001_0038,
+];
+
+const SHADER_MANIFEST_SCHEMA: u32 = 2;
+const SHADER_TARGET_ENV: &str = "vulkan1.0";
+const SHADER_COMPILER_NAME: &str = "glslangValidator";
+const SHADER_COMPILER_VERSION: &str = "11:16.3.0";
+const SHADER_COMPILER_BINARY_SHA256: &str =
+ "9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f";
+const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
+const SPIRV_VALIDATOR_VERSION: &str = "SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00";
+const SPIRV_VALIDATOR_BINARY_SHA256: &str =
+ "f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f";
+const TRIANGLE_VERTEX_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert";
+const TRIANGLE_VERTEX_SOURCE_SHA256: &str =
+ "1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341";
+const TRIANGLE_VERTEX_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
+const TRIANGLE_VERTEX_COMPILE_COMMAND: &str = "glslangValidator -V -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
+const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str =
+ "spirv-val --target-env vulkan1.0 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
+const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag";
+const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str =
+ "f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445";
+const TRIANGLE_FRAGMENT_SPIRV_PATH: &str =
+ "adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
+const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str = "glslangValidator -V -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
+const TRIANGLE_FRAGMENT_VALIDATE_COMMAND: &str =
+ "spirv-val --target-env vulkan1.0 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
+
+/// Shader tool metadata pinned in the Stage 0 manifest.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanShaderToolManifest {
+ /// Tool executable name.
+ pub name: &'static str,
+ /// Tool version string.
+ pub version: &'static str,
+ /// Tool binary SHA-256.
+ pub binary_sha256: &'static str,
+}
+
+/// Vulkan shader stage.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanShaderStage {
+ /// Vertex stage.
+ Vertex,
+ /// Fragment stage.
+ Fragment,
+}
+
+impl VulkanShaderStage {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Vertex => "vertex",
+ Self::Fragment => "fragment",
+ }
+ }
+}
+
+/// Offline SPIR-V shader manifest entry.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanShaderModuleManifest {
+ /// Logical shader name.
+ pub name: &'static str,
+ /// Shader stage.
+ pub stage: VulkanShaderStage,
+ /// SPIR-V entry point.
+ pub entry_point: &'static str,
+ /// Descriptor set count.
+ pub descriptor_sets: u32,
+ /// Push constant byte count.
+ pub push_constant_bytes: u32,
+ /// Checked-in GLSL source path.
+ pub source_path: &'static str,
+ /// Checked-in GLSL source SHA-256.
+ pub source_sha256: &'static str,
+ /// Checked-in SPIR-V module path.
+ pub spirv_path: &'static str,
+ /// Exact offline compile command used for the checked-in SPIR-V artifact.
+ pub compile_command: &'static str,
+ /// Exact offline validation command used for the checked-in SPIR-V artifact.
+ pub validate_command: &'static str,
+ /// SPIR-V words.
+ pub words: &'static [u32],
+}
+
+/// Shader manifest validation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanShaderManifestReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Explicit Vulkan target environment for the checked-in SPIR-V.
+ pub target_env: &'static str,
+ /// Pinned compiler metadata.
+ pub compiler: VulkanShaderToolManifest,
+ /// Pinned validator metadata.
+ pub validator: VulkanShaderToolManifest,
+ /// Shader module reports.
+ pub modules: Vec<VulkanShaderModuleReport>,
+ /// Hash of the normalized shader manifest.
+ pub manifest_hash: String,
+}
+
+/// Shader module validation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanShaderModuleReport {
+ /// Logical shader name.
+ pub name: &'static str,
+ /// Shader stage.
+ pub stage: VulkanShaderStage,
+ /// SPIR-V entry point.
+ pub entry_point: &'static str,
+ /// Checked-in GLSL source path.
+ pub source_path: &'static str,
+ /// Checked-in GLSL source SHA-256.
+ pub source_sha256: &'static str,
+ /// Checked-in SPIR-V module path.
+ pub spirv_path: &'static str,
+ /// SPIR-V word count.
+ pub word_count: usize,
+ /// SPIR-V byte hash.
+ pub sha256: String,
+ /// Descriptor set count.
+ pub descriptor_sets: u32,
+ /// Push constant byte count.
+ pub push_constant_bytes: u32,
+ /// Exact offline compile command used for the checked-in SPIR-V artifact.
+ pub compile_command: &'static str,
+ /// Exact offline validation command used for the checked-in SPIR-V artifact.
+ pub validate_command: &'static str,
+ /// Stable hash of the reflected interface contract for this module.
+ pub interface_hash: String,
+}
+
+/// Shader manifest validation error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanShaderManifestError {
+ /// SPIR-V module is too short to contain a header.
+ TooShort {
+ /// Shader name.
+ name: &'static str,
+ },
+ /// SPIR-V module has an invalid magic word.
+ InvalidMagic {
+ /// Shader name.
+ name: &'static str,
+ /// Found magic word.
+ found: u32,
+ },
+ /// SPIR-V module version is below 1.0.
+ UnsupportedVersion {
+ /// Shader name.
+ name: &'static str,
+ /// Found version word.
+ found: u32,
+ },
+ /// SPIR-V module declares an invalid bound.
+ InvalidBound {
+ /// Shader name.
+ name: &'static str,
+ },
+}
+
+impl std::fmt::Display for VulkanShaderManifestError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::TooShort { name } => write!(f, "shader {name} SPIR-V module is too short"),
+ Self::InvalidMagic { name, found } => {
+ write!(f, "shader {name} has invalid SPIR-V magic 0x{found:08x}")
+ }
+ Self::UnsupportedVersion { name, found } => write!(
+ f,
+ "shader {name} has unsupported SPIR-V version 0x{found:08x}"
+ ),
+ Self::InvalidBound { name } => write!(f, "shader {name} has invalid SPIR-V bound"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanShaderManifestError {}
+
+/// Vulkan instance bootstrap configuration.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanInstanceConfig {
+ /// Application name reported to the loader.
+ pub application_name: String,
+ /// Required instance extensions, usually including surface extensions.
+ pub required_extensions: Vec<String>,
+ /// Whether `VK_KHR_portability_enumeration` and its create flag are enabled.
+ pub enable_portability_enumeration: bool,
+ /// Whether validation layers are requested.
+ pub enable_validation: bool,
+}
+
+impl VulkanInstanceConfig {
+ /// Returns a conservative instance configuration for smoke probes.
+ #[must_use]
+ pub fn smoke(application_name: impl Into<String>) -> Self {
+ Self {
+ application_name: application_name.into(),
+ required_extensions: Vec::new(),
+ enable_portability_enumeration: cfg!(target_os = "macos"),
+ enable_validation: false,
+ }
+ }
+}
+
+/// Deterministic Vulkan instance creation plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanInstancePlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Instance extensions requested at creation time.
+ pub enabled_extensions: Vec<String>,
+ /// Raw Vulkan instance creation flags.
+ pub create_flags: u32,
+ /// Whether validation was requested.
+ pub validation_requested: bool,
+}
+
+/// Created Vulkan instance probe.
+pub struct VulkanInstanceProbe {
+ entry: ash::Entry,
+ instance: ash::Instance,
+ /// Deterministic instance creation report.
+ pub report: VulkanInstancePlan,
+}
+
+impl Drop for VulkanInstanceProbe {
+ fn drop(&mut self) {
+ // SAFETY: The `Instance` was created by this probe and is destroyed once during drop.
+ unsafe { self.instance.destroy_instance(None) };
+ }
+}
+
+/// Deterministic Vulkan surface creation plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSurfacePlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Instance extensions required by the native display backend.
+ pub required_instance_extensions: Vec<String>,
+}
+
+/// Vulkan surface bootstrap error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSurfaceError {
+ /// No native raw window/display handles were available.
+ MissingNativeHandles,
+ /// Required platform surface extensions could not be enumerated.
+ RequiredExtensionsFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// A required extension pointer was not valid UTF-8.
+ InvalidExtensionName,
+ /// Surface creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanSurfaceError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::MissingNativeHandles => {
+ write!(
+ f,
+ "native window/display handles are required for Vulkan surface creation"
+ )
+ }
+ Self::RequiredExtensionsFailed { result } => write!(
+ f,
+ "failed to enumerate required Vulkan surface extensions: {result}"
+ ),
+ Self::InvalidExtensionName => {
+ write!(f, "Vulkan surface extension name is not valid UTF-8")
+ }
+ Self::CreateFailed { result } => write!(f, "Vulkan surface creation failed: {result}"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanSurfaceError {}
+
+/// Created Vulkan surface probe.
+pub struct VulkanSurfaceProbe {
+ loader: surface::Instance,
+ surface: vk::SurfaceKHR,
+ /// Deterministic surface creation report.
+ pub report: VulkanSurfacePlan,
+}
+
+impl Drop for VulkanSurfaceProbe {
+ fn drop(&mut self) {
+ // SAFETY: The `SurfaceKHR` was created by this probe and is destroyed once during drop.
+ unsafe { self.loader.destroy_surface(self.surface, None) };
+ }
+}
+
+/// Live Vulkan device/surface capability probe.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanRuntimeCapabilityProbe {
+ /// Selected device/queue capability report.
+ pub capability: VulkanCapabilityReport,
+ /// Swapchain plan built from the selected device and live surface capabilities.
+ pub swapchain: VulkanSwapchainPlan,
+}
+
+/// Created Vulkan logical device probe.
+pub struct VulkanLogicalDeviceProbe {
+ device: ash::Device,
+ physical_device: vk::PhysicalDevice,
+ /// 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) };
+ }
+}
+
+impl VulkanLogicalDeviceProbe {
+ /// Returns the graphics queue selected by the Stage 0 policy.
+ #[must_use]
+ pub fn graphics_queue(&self) -> vk::Queue {
+ // SAFETY: The queue-family index belongs to this live logical device.
+ unsafe {
+ self.device
+ .get_device_queue(self.report.graphics_queue_family, 0)
+ }
+ }
+
+ /// Returns the presentation queue selected by the Stage 0 policy.
+ #[must_use]
+ pub fn present_queue(&self) -> vk::Queue {
+ // SAFETY: The queue-family index belongs to this live logical device.
+ unsafe {
+ self.device
+ .get_device_queue(self.report.present_queue_family, 0)
+ }
+ }
+
+ /// Returns a shared reference to the live logical device.
+ #[must_use]
+ pub fn device(&self) -> &ash::Device {
+ &self.device
+ }
+}
+
+/// 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>,
+}
+
+/// Created Vulkan swapchain probe.
+pub struct VulkanSwapchainProbe {
+ loader: swapchain::Device,
+ swapchain: vk::SwapchainKHR,
+ /// Deterministic swapchain creation report.
+ pub report: VulkanSwapchainReport,
+}
+
+impl Drop for VulkanSwapchainProbe {
+ fn drop(&mut self) {
+ // SAFETY: The swapchain was created by this probe and is destroyed once during drop.
+ unsafe { self.loader.destroy_swapchain(self.swapchain, None) };
+ }
+}
+
+impl VulkanSwapchainProbe {
+ /// Returns the live swapchain handle.
+ #[must_use]
+ pub fn swapchain(&self) -> vk::SwapchainKHR {
+ self.swapchain
+ }
+
+ /// Returns the swapchain extension loader for this live swapchain.
+ #[must_use]
+ pub fn loader(&self) -> &swapchain::Device {
+ &self.loader
+ }
+}
+
+/// Creates a live native Vulkan renderer for the Stage 0 smoke loop.
+#[derive(Clone, Debug)]
+pub struct VulkanSmokeRendererCreateInfo {
+ /// Application name reported to the Vulkan loader.
+ pub application_name: String,
+ /// Native window/display handles borrowed from a live window.
+ pub native_handles: NativeWindowHandles,
+ /// Initial drawable extent.
+ pub drawable_extent: (u32, u32),
+ /// Whether validation layers must be enabled.
+ pub enable_validation: bool,
+}
+
+/// Stable smoke renderer bootstrap report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSmokeRendererReport {
+ /// Checked-in shader manifest hash used by the renderer.
+ pub shader_manifest_hash: String,
+ /// Whether portability enumeration was enabled at instance creation.
+ pub portability_enumeration: bool,
+ /// Selected device name.
+ pub device_name: String,
+ /// Graphics queue-family index.
+ pub graphics_queue_family: u32,
+ /// Present queue-family index.
+ pub present_queue_family: u32,
+ /// Enabled logical-device extension count.
+ pub enabled_extension_count: u32,
+ /// Current swapchain extent.
+ pub swapchain_extent: (u32, u32),
+ /// Current swapchain image count.
+ pub swapchain_image_count: u32,
+}
+
+/// Measured validation counters from the live smoke loop.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanValidationReport {
+ /// Validation warnings observed by the debug messenger.
+ pub warning_count: u32,
+ /// Validation errors observed by the debug messenger.
+ pub error_count: u32,
+ /// Stable sorted VUID list.
+ pub vuids: Vec<String>,
+}
+
+/// Result of one rendered smoke frame.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanSmokeFrameOutcome {
+ /// A frame was submitted and presented.
+ Presented,
+ /// Rendering was skipped because the swapchain had to be recreated.
+ Recreated,
+ /// Rendering was skipped because the drawable extent is zero.
+ ZeroExtent,
+}
+
+/// Live smoke renderer error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSmokeRendererError {
+ /// Instance bootstrap failed.
+ Instance(VulkanInstanceError),
+ /// Surface bootstrap failed.
+ Surface(VulkanSurfaceError),
+ /// Logical-device bootstrap failed.
+ LogicalDevice(VulkanLogicalDeviceError),
+ /// Swapchain bootstrap failed.
+ Swapchain(VulkanSwapchainProbeError),
+ /// Shader manifest validation failed.
+ ShaderManifest(VulkanShaderManifestError),
+ /// Vulkan operation failed.
+ VulkanOperation {
+ /// Operation context.
+ context: &'static str,
+ /// Raw Vulkan result text.
+ result: String,
+ },
+ /// No suitable memory type exists for the required properties.
+ MissingMemoryType {
+ /// Operation context.
+ context: &'static str,
+ },
+ /// Internal smoke renderer state was unexpectedly absent.
+ InvariantViolation {
+ /// Missing state context.
+ context: &'static str,
+ },
+}
+
+impl std::fmt::Display for VulkanSmokeRendererError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Instance(error) => write!(f, "{error}"),
+ Self::Surface(error) => write!(f, "{error}"),
+ Self::LogicalDevice(error) => write!(f, "{error}"),
+ Self::Swapchain(error) => write!(f, "{error}"),
+ Self::ShaderManifest(error) => write!(f, "{error}"),
+ Self::VulkanOperation { context, result } => {
+ write!(f, "{context}: {result}")
+ }
+ Self::MissingMemoryType { context } => {
+ write!(f, "{context}: no compatible Vulkan memory type")
+ }
+ Self::InvariantViolation { context } => {
+ write!(f, "renderer invariant violated: {context}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanSmokeRendererError {}
+
+struct VulkanValidationShared {
+ warning_count: AtomicU32,
+ error_count: AtomicU32,
+ vuids: Mutex<BTreeSet<String>>,
+}
+
+impl Default for VulkanValidationShared {
+ fn default() -> Self {
+ Self {
+ warning_count: AtomicU32::new(0),
+ error_count: AtomicU32::new(0),
+ vuids: Mutex::new(BTreeSet::new()),
+ }
+ }
+}
+
+struct VulkanValidationMessenger {
+ loader: ash::ext::debug_utils::Instance,
+ messenger: vk::DebugUtilsMessengerEXT,
+ shared: Box<VulkanValidationShared>,
+}
+
+impl VulkanValidationMessenger {
+ fn report(&self) -> VulkanValidationReport {
+ let vuids = self
+ .shared
+ .vuids
+ .lock()
+ .map(|values| values.iter().cloned().collect::<Vec<_>>())
+ .unwrap_or_default();
+ VulkanValidationReport {
+ warning_count: self.shared.warning_count.load(Ordering::Relaxed),
+ error_count: self.shared.error_count.load(Ordering::Relaxed),
+ vuids,
+ }
+ }
+}
+
+impl Drop for VulkanValidationMessenger {
+ fn drop(&mut self) {
+ // SAFETY: The messenger belongs to this instance-level loader and is destroyed once.
+ unsafe {
+ self.loader
+ .destroy_debug_utils_messenger(self.messenger, None);
+ };
+ }
+}
+
+unsafe extern "system" fn vulkan_validation_callback(
+ message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
+ _message_types: vk::DebugUtilsMessageTypeFlagsEXT,
+ callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
+ user_data: *mut std::ffi::c_void,
+) -> vk::Bool32 {
+ // SAFETY: The debug messenger stores a stable pointer to `VulkanValidationShared` for the messenger lifetime.
+ let Some(shared) = (unsafe { (user_data as *const VulkanValidationShared).as_ref() }) else {
+ return vk::FALSE;
+ };
+ if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
+ shared.error_count.fetch_add(1, Ordering::Relaxed);
+ } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
+ shared.warning_count.fetch_add(1, Ordering::Relaxed);
+ }
+ // SAFETY: Vulkan invokes the callback with either a null pointer or a valid callback-data payload.
+ let Some(callback_data) = (unsafe { callback_data.as_ref() }) else {
+ return vk::FALSE;
+ };
+ if let Some(vuid) = (!callback_data.p_message_id_name.is_null()).then(|| {
+ // SAFETY: `p_message_id_name` is a Vulkan-owned NUL-terminated string for the callback duration.
+ unsafe { CStr::from_ptr(callback_data.p_message_id_name) }
+ .to_string_lossy()
+ .into_owned()
+ }) {
+ if vuid.starts_with("VUID-") {
+ if let Ok(mut vuids) = shared.vuids.lock() {
+ vuids.insert(vuid);
+ }
+ }
+ }
+ vk::FALSE
+}
+
+struct VulkanAllocatedBuffer {
+ buffer: vk::Buffer,
+ memory: vk::DeviceMemory,
+}
+
+struct VulkanSwapchainResources {
+ image_views: Vec<vk::ImageView>,
+ render_pass: vk::RenderPass,
+ pipeline_layout: vk::PipelineLayout,
+ pipeline: vk::Pipeline,
+ framebuffers: Vec<vk::Framebuffer>,
+ command_buffers: Vec<vk::CommandBuffer>,
+}
+
+struct PartialSwapchainResources {
+ image_views: Vec<vk::ImageView>,
+ render_pass: Option<vk::RenderPass>,
+ pipeline_layout: Option<vk::PipelineLayout>,
+ pipeline: Option<vk::Pipeline>,
+ framebuffers: Vec<vk::Framebuffer>,
+ command_buffers: Vec<vk::CommandBuffer>,
+}
+
+struct VulkanFrameSync {
+ image_available: vk::Semaphore,
+ render_finished: vk::Semaphore,
+ fence: vk::Fence,
+}
+
+/// Live Stage 0 Vulkan triangle renderer used by the smoke app.
+pub struct VulkanSmokeRenderer {
+ instance: Option<VulkanInstanceProbe>,
+ validation: Option<VulkanValidationMessenger>,
+ surface: Option<VulkanSurfaceProbe>,
+ device: Option<VulkanLogicalDeviceProbe>,
+ swapchain: Option<VulkanSwapchainProbe>,
+ command_pool: vk::CommandPool,
+ swapchain_resources: Option<VulkanSwapchainResources>,
+ vertex_buffer: Option<VulkanAllocatedBuffer>,
+ index_buffer: Option<VulkanAllocatedBuffer>,
+ frame_sync: Vec<VulkanFrameSync>,
+ images_in_flight: Vec<vk::Fence>,
+ current_frame: usize,
+ pending_extent: Option<(u32, u32)>,
+ swapchain_recreate_count: u32,
+ report: VulkanSmokeRendererReport,
+}
+
+impl VulkanSmokeRenderer {
+ /// Creates a live Vulkan smoke renderer bound to a live native window.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
+ /// memory allocation, or synchronization resource creation fails.
+ pub fn new(
+ create_info: &VulkanSmokeRendererCreateInfo,
+ ) -> Result<Self, VulkanSmokeRendererError> {
+ let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
+ .map_err(VulkanSmokeRendererError::ShaderManifest)?;
+ let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
+ .map_err(VulkanSmokeRendererError::Surface)?;
+ let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
+ instance_config
+ .required_extensions
+ .clone_from(&surface_plan.required_instance_extensions);
+ instance_config.enable_validation = create_info.enable_validation;
+ let instance = create_vulkan_instance_probe(&instance_config)
+ .map_err(VulkanSmokeRendererError::Instance)?;
+ let validation = if create_info.enable_validation {
+ Some(create_validation_messenger(&instance)?)
+ } else {
+ None
+ };
+ let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
+ .map_err(VulkanSmokeRendererError::Surface)?;
+ let device =
+ create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
+ .map_err(VulkanSmokeRendererError::LogicalDevice)?;
+ let swapchain = create_vulkan_swapchain_probe_for_extent(
+ &instance,
+ &surface,
+ &device,
+ create_info.drawable_extent,
+ vk::SwapchainKHR::null(),
+ )
+ .map_err(VulkanSmokeRendererError::Swapchain)?;
+ let command_pool = create_command_pool(&device)?;
+ let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
+ Ok(buffer) => buffer,
+ Err(error) => {
+ // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_command_pool(command_pool, None) };
+ return Err(error);
+ }
+ };
+ let index_buffer = match create_triangle_index_buffer(&instance, &device) {
+ Ok(buffer) => buffer,
+ Err(error) => {
+ // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_command_pool(command_pool, None) };
+ destroy_allocated_buffer(&device, &vertex_buffer);
+ return Err(error);
+ }
+ };
+ let mut renderer = Self {
+ instance: Some(instance),
+ validation,
+ surface: Some(surface),
+ device: Some(device),
+ swapchain: Some(swapchain),
+ command_pool,
+ swapchain_resources: None,
+ vertex_buffer: Some(vertex_buffer),
+ index_buffer: Some(index_buffer),
+ frame_sync: Vec::new(),
+ images_in_flight: Vec::new(),
+ current_frame: 0,
+ pending_extent: None,
+ swapchain_recreate_count: 0,
+ report: VulkanSmokeRendererReport {
+ shader_manifest_hash: shader_manifest.manifest_hash.clone(),
+ portability_enumeration: instance_config.enable_portability_enumeration,
+ device_name: String::new(),
+ graphics_queue_family: 0,
+ present_queue_family: 0,
+ enabled_extension_count: 0,
+ swapchain_extent: (0, 0),
+ swapchain_image_count: 0,
+ },
+ };
+ renderer.rebuild_swapchain_resources(false)?;
+ let device_ref = renderer.device_ref()?;
+ let swapchain_ref = renderer.swapchain_ref()?;
+ renderer.report = VulkanSmokeRendererReport {
+ shader_manifest_hash: shader_manifest.manifest_hash,
+ portability_enumeration: renderer
+ .instance
+ .as_ref()
+ .is_some_and(|instance| instance.report.create_flags != 0),
+ device_name: device_ref.report.device_name.clone(),
+ graphics_queue_family: device_ref.report.graphics_queue_family,
+ present_queue_family: device_ref.report.present_queue_family,
+ enabled_extension_count: device_ref
+ .report
+ .enabled_extensions
+ .len()
+ .try_into()
+ .unwrap_or(u32::MAX),
+ swapchain_extent: swapchain_ref.report.plan.extent,
+ swapchain_image_count: swapchain_ref.report.image_count,
+ };
+ Ok(renderer)
+ }
+
+ /// Returns the current bootstrap report.
+ #[must_use]
+ pub const fn report(&self) -> &VulkanSmokeRendererReport {
+ &self.report
+ }
+
+ /// Returns measured validation counters and VUIDs.
+ #[must_use]
+ pub fn validation_report(&self) -> VulkanValidationReport {
+ self.validation.as_ref().map_or(
+ VulkanValidationReport {
+ warning_count: 0,
+ error_count: 0,
+ vuids: Vec::new(),
+ },
+ VulkanValidationMessenger::report,
+ )
+ }
+
+ /// Returns the measured swapchain recreation count.
+ #[must_use]
+ pub const fn swapchain_recreate_count(&self) -> u32 {
+ self.swapchain_recreate_count
+ }
+
+ /// Requests swapchain recreation for a new drawable extent.
+ pub fn request_resize(&mut self, extent: (u32, u32)) {
+ self.pending_extent = Some(extent);
+ }
+
+ fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
+ self.device
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "logical device",
+ })
+ }
+
+ fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
+ self.swapchain
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "swapchain",
+ })
+ }
+
+ fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
+ self.instance
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "instance",
+ })
+ }
+
+ fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
+ self.surface
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
+ }
+
+ fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
+ self.swapchain_resources
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "swapchain resources",
+ })
+ }
+
+ fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ self.vertex_buffer
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "vertex buffer",
+ })
+ }
+
+ fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ self.index_buffer
+ .as_ref()
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "index buffer",
+ })
+ }
+
+ /// Draws and presents one indexed-triangle frame.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
+ /// submission, or presentation fails.
+ #[allow(clippy::too_many_lines)]
+ pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
+ if let Some(extent) = self.pending_extent.take() {
+ if extent.0 == 0 || extent.1 == 0 {
+ self.pending_extent = Some(extent);
+ return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
+ }
+ self.recreate_swapchain(extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+
+ let sync = &self.frame_sync[self.current_frame];
+ let image_available = sync.image_available;
+ let render_finished = sync.render_finished;
+ let in_flight_fence = sync.fence;
+ // SAFETY: The fence belongs to this live logical device and is waited from one thread.
+ unsafe {
+ self.device_ref()?
+ .device()
+ .wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkWaitForFences",
+ result: format!("{error:?}"),
+ })?;
+ // SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
+ let acquire = unsafe {
+ self.swapchain_ref()?.loader().acquire_next_image(
+ self.swapchain_ref()?.swapchain(),
+ 1_000_000_000,
+ image_available,
+ vk::Fence::null(),
+ )
+ };
+ let (image_index, acquire_suboptimal) = match acquire {
+ Ok(result) => result,
+ Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+ Err(error) => {
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkAcquireNextImageKHR",
+ result: format!("{error:?}"),
+ });
+ }
+ };
+ let image_index_usize = usize::try_from(image_index).unwrap_or(0);
+ let image_fence = self.images_in_flight[image_index_usize];
+ if image_fence != vk::Fence::null() {
+ // SAFETY: The fence belongs to this renderer and can be waited independently.
+ unsafe {
+ self.device_ref()?
+ .device()
+ .wait_for_fences(&[image_fence], true, 1_000_000_000)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkWaitForFences(image)",
+ result: format!("{error:?}"),
+ })?;
+ }
+ self.images_in_flight[image_index_usize] = in_flight_fence;
+ // SAFETY: The fence belongs to this frame context and is not in use after the wait above.
+ unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
+ |error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkResetFences",
+ result: format!("{error:?}"),
+ },
+ )?;
+
+ self.record_command_buffer(image_index_usize)?;
+ let wait_semaphores = [image_available];
+ let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
+ let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
+ let signal_semaphores = [render_finished];
+ let submit_info = [vk::SubmitInfo::default()
+ .wait_semaphores(&wait_semaphores)
+ .wait_dst_stage_mask(&wait_stages)
+ .command_buffers(&command_buffers)
+ .signal_semaphores(&signal_semaphores)];
+ // SAFETY: Submission references live queue, sync objects and recorded command buffer.
+ unsafe {
+ self.device_ref()?.device().queue_submit(
+ self.device_ref()?.graphics_queue(),
+ &submit_info,
+ in_flight_fence,
+ )
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkQueueSubmit",
+ result: format!("{error:?}"),
+ })?;
+
+ let present_wait = [render_finished];
+ let swapchains = [self.swapchain_ref()?.swapchain()];
+ let image_indices = [image_index];
+ let present_info = vk::PresentInfoKHR::default()
+ .wait_semaphores(&present_wait)
+ .swapchains(&swapchains)
+ .image_indices(&image_indices);
+ // SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
+ let present_suboptimal = match unsafe {
+ self.swapchain_ref()?
+ .loader()
+ .queue_present(self.device_ref()?.present_queue(), &present_info)
+ } {
+ Ok(suboptimal) => suboptimal,
+ Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ return Ok(VulkanSmokeFrameOutcome::Recreated);
+ }
+ Err(error) => {
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkQueuePresentKHR",
+ result: format!("{error:?}"),
+ });
+ }
+ };
+
+ self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
+ if acquire_suboptimal || present_suboptimal {
+ self.recreate_swapchain(self.report.swapchain_extent)?;
+ Ok(VulkanSmokeFrameOutcome::Recreated)
+ } else {
+ Ok(VulkanSmokeFrameOutcome::Presented)
+ }
+ }
+
+ fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
+ let device = self.device_ref()?;
+ // SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
+ unsafe { device.device().device_wait_idle() }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkDeviceWaitIdle",
+ result: format!("{error:?}"),
+ }
+ })?;
+ self.pending_extent = None;
+ self.rebuild_swapchain(extent)?;
+ self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
+ Ok(())
+ }
+
+ fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
+ self.destroy_swapchain_resources();
+ let instance = self.instance_ref()?;
+ let surface = self.surface_ref()?;
+ let device = self.device_ref()?;
+ let old_swapchain = self
+ .swapchain
+ .as_ref()
+ .map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
+ let new_swapchain = create_vulkan_swapchain_probe_for_extent(
+ instance,
+ surface,
+ device,
+ extent,
+ old_swapchain,
+ )
+ .map_err(VulkanSmokeRendererError::Swapchain)?;
+ self.swapchain = Some(new_swapchain);
+ self.rebuild_swapchain_resources(true)?;
+ Ok(())
+ }
+
+ fn rebuild_swapchain_resources(
+ &mut self,
+ reuse_command_pool: bool,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ let resources = {
+ let device = self.device_ref()?;
+ let swapchain = self.swapchain_ref()?;
+ create_swapchain_resources(
+ device,
+ swapchain,
+ self.command_pool,
+ self.vertex_buffer_ref()?,
+ self.index_buffer_ref()?,
+ reuse_command_pool,
+ )?
+ };
+ let frame_sync = {
+ let device = self.device_ref()?;
+ create_frame_sync(device)?
+ };
+ let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
+ let swapchain_image_count = self.swapchain_ref()?.report.image_count;
+ self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
+ self.frame_sync = frame_sync;
+ self.report.swapchain_extent = swapchain_extent;
+ self.report.swapchain_image_count = swapchain_image_count;
+ self.swapchain_resources = Some(resources);
+ Ok(())
+ }
+
+ #[allow(clippy::too_many_lines)]
+ fn record_command_buffer(
+ &mut self,
+ image_index: usize,
+ ) -> Result<(), VulkanSmokeRendererError> {
+ let device = self.device_ref()?;
+ let swapchain = self.swapchain_ref()?;
+ let resources = self.resources_ref()?;
+ let command_buffer = resources.command_buffers[image_index];
+ // SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
+ unsafe {
+ device
+ .device()
+ .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkResetCommandBuffer",
+ result: format!("{error:?}"),
+ })?;
+ let begin_info = vk::CommandBufferBeginInfo::default();
+ // SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
+ unsafe {
+ device
+ .device()
+ .begin_command_buffer(command_buffer, &begin_info)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkBeginCommandBuffer",
+ result: format!("{error:?}"),
+ })?;
+
+ let pre_barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .subresource_range(color_subresource_range())
+ .src_access_mask(vk::AccessFlags::empty())
+ .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
+ // SAFETY: The swapchain is live and queried only to resolve the current image handles.
+ let swapchain_images = unsafe {
+ swapchain
+ .loader()
+ .get_swapchain_images(swapchain.swapchain())
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkGetSwapchainImagesKHR",
+ result: format!("{error:?}"),
+ })?;
+ let pre_barrier = pre_barrier.image(swapchain_images[image_index]);
+ // SAFETY: The barriers operate on the acquired swapchain image owned by this command buffer submission.
+ unsafe {
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[pre_barrier],
+ );
+ }
+
+ let clear_values = [vk::ClearValue {
+ color: vk::ClearColorValue {
+ float32: [0.05, 0.08, 0.11, 1.0],
+ },
+ }];
+ let render_area = vk::Rect2D {
+ offset: vk::Offset2D { x: 0, y: 0 },
+ extent: vk::Extent2D {
+ width: swapchain.report.plan.extent.0,
+ height: swapchain.report.plan.extent.1,
+ },
+ };
+ let render_pass_info = vk::RenderPassBeginInfo::default()
+ .render_pass(resources.render_pass)
+ .framebuffer(resources.framebuffers[image_index])
+ .render_area(render_area)
+ .clear_values(&clear_values);
+ // SAFETY: All commands target live frame resources owned by this renderer.
+ unsafe {
+ device.device().cmd_begin_render_pass(
+ command_buffer,
+ &render_pass_info,
+ vk::SubpassContents::INLINE,
+ );
+ device.device().cmd_bind_pipeline(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ resources.pipeline,
+ );
+ let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
+ let offsets = [0_u64];
+ device
+ .device()
+ .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
+ device.device().cmd_bind_index_buffer(
+ command_buffer,
+ self.index_buffer_ref()?.buffer,
+ 0,
+ vk::IndexType::UINT16,
+ );
+ device
+ .device()
+ .cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
+ device.device().cmd_end_render_pass(command_buffer);
+ }
+
+ let post_barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .image(swapchain_images[image_index])
+ .subresource_range(color_subresource_range())
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .dst_access_mask(vk::AccessFlags::empty());
+ // SAFETY: The post-render barrier transitions the same live swapchain image into present layout.
+ unsafe {
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::BOTTOM_OF_PIPE,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[post_barrier],
+ );
+ device.device().end_command_buffer(command_buffer)
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkEndCommandBuffer",
+ result: format!("{error:?}"),
+ })?;
+ Ok(())
+ }
+
+ fn destroy_swapchain_resources(&mut self) {
+ let Some(device) = self.device.as_ref() else {
+ return;
+ };
+ for sync in self.frame_sync.drain(..) {
+ // SAFETY: These sync objects belong to this device and are destroyed once.
+ unsafe {
+ device
+ .device()
+ .destroy_semaphore(sync.image_available, None);
+ device
+ .device()
+ .destroy_semaphore(sync.render_finished, None);
+ device.device().destroy_fence(sync.fence, None);
+ }
+ }
+ if let Some(resources) = self.swapchain_resources.take() {
+ destroy_swapchain_resources(device, self.command_pool, resources);
+ }
+ self.images_in_flight.clear();
+ self.current_frame = 0;
+ }
+
+ fn teardown(&mut self) {
+ if let Some(device) = self.device.as_ref() {
+ // SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
+ let _ = unsafe { device.device().device_wait_idle() };
+ }
+ self.destroy_swapchain_resources();
+ if let Some(device) = self.device.as_ref() {
+ if let Some(buffer) = self.index_buffer.take() {
+ // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
+ unsafe {
+ device.device().destroy_buffer(buffer.buffer, None);
+ device.device().free_memory(buffer.memory, None);
+ }
+ }
+ if let Some(buffer) = self.vertex_buffer.take() {
+ // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
+ unsafe {
+ device.device().destroy_buffer(buffer.buffer, None);
+ device.device().free_memory(buffer.memory, None);
+ }
+ }
+ // SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
+ unsafe {
+ device
+ .device()
+ .destroy_command_pool(self.command_pool, None);
+ };
+ }
+ // Drop child Vulkan owners explicitly before their parents instead of relying on field order.
+ self.swapchain.take();
+ self.device.take();
+ self.surface.take();
+ self.validation.take();
+ self.instance.take();
+ }
+}
+
+impl Drop for VulkanSmokeRenderer {
+ fn drop(&mut self) {
+ self.teardown();
+ }
+}
+
+fn create_validation_messenger(
+ instance: &VulkanInstanceProbe,
+) -> Result<VulkanValidationMessenger, VulkanSmokeRendererError> {
+ let shared = Box::new(VulkanValidationShared::default());
+ let loader = ash::ext::debug_utils::Instance::new(&instance.entry, &instance.instance);
+ let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
+ .message_severity(
+ vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
+ | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
+ )
+ .message_type(
+ vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
+ | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
+ | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
+ )
+ .pfn_user_callback(Some(vulkan_validation_callback))
+ .user_data((&raw const *shared).cast_mut().cast());
+ let messenger =
+ // SAFETY: The create info points at a stable boxed user-data allocation for the messenger lifetime.
+ unsafe { loader.create_debug_utils_messenger(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateDebugUtilsMessengerEXT",
+ result: format!("{error:?}"),
+ }
+ })?;
+ Ok(VulkanValidationMessenger {
+ loader,
+ messenger,
+ shared,
+ })
+}
+
+fn create_command_pool(
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<vk::CommandPool, VulkanSmokeRendererError> {
+ let create_info = vk::CommandPoolCreateInfo::default()
+ .queue_family_index(device.report.graphics_queue_family)
+ .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
+ // SAFETY: The queue-family index belongs to this live logical device.
+ unsafe { device.device().create_command_pool(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateCommandPool",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn create_triangle_vertex_buffer(
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ let vertices: [[f32; 5]; 3] = [
+ [0.0, -0.55, 1.0, 0.2, 0.2],
+ [0.55, 0.55, 0.2, 1.0, 0.2],
+ [-0.55, 0.55, 0.2, 0.4, 1.0],
+ ];
+ let mut bytes = Vec::with_capacity(vertices.len() * 5 * std::mem::size_of::<f32>());
+ for vertex in vertices {
+ for value in vertex {
+ bytes.extend_from_slice(&value.to_ne_bytes());
+ }
+ }
+ create_host_visible_buffer(
+ instance,
+ device,
+ &bytes,
+ vk::BufferUsageFlags::VERTEX_BUFFER,
+ "triangle vertex buffer",
+ )
+}
+
+fn create_triangle_index_buffer(
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ let indices = [0_u16, 1_u16, 2_u16];
+ let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
+ for index in indices {
+ bytes.extend_from_slice(&index.to_ne_bytes());
+ }
+ create_host_visible_buffer(
+ instance,
+ device,
+ &bytes,
+ vk::BufferUsageFlags::INDEX_BUFFER,
+ "triangle index buffer",
+ )
+}
+
+fn create_host_visible_buffer(
+ instance: &VulkanInstanceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ bytes: &[u8],
+ usage: vk::BufferUsageFlags,
+ context: &'static str,
+) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
+ let create_info = vk::BufferCreateInfo::default()
+ .size(bytes.len().try_into().unwrap_or(u64::MAX))
+ .usage(usage)
+ .sharing_mode(vk::SharingMode::EXCLUSIVE);
+ // SAFETY: The create info is stack-owned and references no external memory.
+ let buffer = unsafe { device.device().create_buffer(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context,
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The buffer belongs to this device and is queried immediately after creation.
+ let requirements = unsafe { device.device().get_buffer_memory_requirements(buffer) };
+ let Some(memory_type_index) = find_memory_type(
+ instance,
+ device.physical_device,
+ requirements.memory_type_bits,
+ vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
+ ) else {
+ // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_buffer(buffer, None) };
+ return Err(VulkanSmokeRendererError::MissingMemoryType { context });
+ };
+ let allocate_info = vk::MemoryAllocateInfo::default()
+ .allocation_size(requirements.size)
+ .memory_type_index(memory_type_index);
+ let memory =
+ // SAFETY: Allocation uses a memory type index selected from the physical-device requirements above.
+ unsafe { device.device().allocate_memory(&allocate_info, None) }.map_err(|error| {
+ // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_buffer(buffer, None) };
+ VulkanSmokeRendererError::VulkanOperation {
+ context,
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The buffer and allocation belong to the same live logical device.
+ unsafe { device.device().bind_buffer_memory(buffer, memory, 0) }.map_err(|error| {
+ // SAFETY: The buffer and allocation belong to this logical device and are destroyed on setup failure.
+ unsafe {
+ device.device().destroy_buffer(buffer, None);
+ device.device().free_memory(memory, None);
+ }
+ VulkanSmokeRendererError::VulkanOperation {
+ context,
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The allocation is HOST_VISIBLE, mapped for the full buffer size and unmapped before return.
+ let mapped = unsafe {
+ device
+ .device()
+ .map_memory(memory, 0, requirements.size, vk::MemoryMapFlags::empty())
+ }
+ .map_err(|error| {
+ // SAFETY: The buffer and allocation belong to this logical device and are destroyed on setup failure.
+ unsafe {
+ device.device().destroy_buffer(buffer, None);
+ device.device().free_memory(memory, None);
+ }
+ VulkanSmokeRendererError::VulkanOperation {
+ context,
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The mapped pointer is valid for `bytes.len()` bytes and non-overlapping with the source slice.
+ unsafe {
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
+ device.device().unmap_memory(memory);
+ }
+ Ok(VulkanAllocatedBuffer { buffer, memory })
+}
+
+fn find_memory_type(
+ instance: &VulkanInstanceProbe,
+ physical_device: vk::PhysicalDevice,
+ memory_type_bits: u32,
+ required_properties: vk::MemoryPropertyFlags,
+) -> Option<u32> {
+ // SAFETY: Physical-device memory properties are queried from a live instance-owned physical device.
+ let memory_properties = unsafe {
+ instance
+ .instance
+ .get_physical_device_memory_properties(physical_device)
+ };
+ memory_properties
+ .memory_types
+ .iter()
+ .enumerate()
+ .find_map(|(index, memory_type)| {
+ let supported = (memory_type_bits & (1_u32 << index)) != 0;
+ let has_properties = memory_type.property_flags.contains(required_properties);
+ (supported && has_properties).then(|| index.try_into().unwrap_or(u32::MAX))
+ })
+}
+
+#[allow(clippy::too_many_lines)]
+fn create_swapchain_resources(
+ device: &VulkanLogicalDeviceProbe,
+ swapchain: &VulkanSwapchainProbe,
+ command_pool: vk::CommandPool,
+ _vertex_buffer: &VulkanAllocatedBuffer,
+ _index_buffer: &VulkanAllocatedBuffer,
+ _reuse_command_pool: bool,
+) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
+ // SAFETY: The swapchain is live and owned by this renderer for the duration of the query.
+ let images = unsafe {
+ swapchain
+ .loader()
+ .get_swapchain_images(swapchain.swapchain())
+ }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkGetSwapchainImagesKHR",
+ result: format!("{error:?}"),
+ })?;
+ let mut partial = PartialSwapchainResources {
+ image_views: Vec::with_capacity(images.len()),
+ render_pass: None,
+ pipeline_layout: None,
+ pipeline: None,
+ framebuffers: Vec::with_capacity(images.len()),
+ command_buffers: Vec::new(),
+ };
+ for image in &images {
+ match create_image_view(device, *image, swapchain.report.plan.format.format) {
+ Ok(image_view) => partial.image_views.push(image_view),
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ }
+ }
+ let render_pass = match create_render_pass(device, swapchain.report.plan.format.format) {
+ Ok(render_pass) => render_pass,
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ };
+ partial.render_pass = Some(render_pass);
+ let pipeline_layout = match create_pipeline_layout(device) {
+ Ok(pipeline_layout) => pipeline_layout,
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ };
+ partial.pipeline_layout = Some(pipeline_layout);
+ let pipeline = match create_graphics_pipeline(
+ device,
+ render_pass,
+ pipeline_layout,
+ swapchain.report.plan.extent,
+ ) {
+ Ok(pipeline) => pipeline,
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ };
+ partial.pipeline = Some(pipeline);
+ for image_view in &partial.image_views {
+ match create_framebuffer(
+ device,
+ render_pass,
+ *image_view,
+ swapchain.report.plan.extent,
+ ) {
+ Ok(framebuffer) => partial.framebuffers.push(framebuffer),
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ }
+ }
+ partial.command_buffers = match allocate_command_buffers(
+ device,
+ command_pool,
+ partial.image_views.len().try_into().unwrap_or(u32::MAX),
+ ) {
+ Ok(command_buffers) => command_buffers,
+ Err(error) => {
+ destroy_partial_swapchain_resources(device, command_pool, partial);
+ return Err(error);
+ }
+ };
+ Ok(VulkanSwapchainResources {
+ image_views: partial.image_views,
+ render_pass,
+ pipeline_layout,
+ pipeline,
+ framebuffers: partial.framebuffers,
+ command_buffers: partial.command_buffers,
+ })
+}
+
+fn create_image_view(
+ device: &VulkanLogicalDeviceProbe,
+ image: vk::Image,
+ format: i32,
+) -> Result<vk::ImageView, VulkanSmokeRendererError> {
+ let create_info = vk::ImageViewCreateInfo::default()
+ .image(image)
+ .view_type(vk::ImageViewType::TYPE_2D)
+ .format(vk::Format::from_raw(format))
+ .subresource_range(color_subresource_range());
+ // SAFETY: The image comes from the live swapchain and the subresource range covers its color aspect.
+ unsafe { device.device().create_image_view(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateImageView",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn create_render_pass(
+ device: &VulkanLogicalDeviceProbe,
+ format: i32,
+) -> Result<vk::RenderPass, VulkanSmokeRendererError> {
+ let color_attachment = vk::AttachmentDescription::default()
+ .format(vk::Format::from_raw(format))
+ .samples(vk::SampleCountFlags::TYPE_1)
+ .load_op(vk::AttachmentLoadOp::CLEAR)
+ .store_op(vk::AttachmentStoreOp::STORE)
+ .initial_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
+ let color_attachment_ref = vk::AttachmentReference::default()
+ .attachment(0)
+ .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
+ let color_attachments = [color_attachment_ref];
+ let subpass = vk::SubpassDescription::default()
+ .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
+ .color_attachments(&color_attachments);
+ let dependency = vk::SubpassDependency::default()
+ .src_subpass(vk::SUBPASS_EXTERNAL)
+ .dst_subpass(0)
+ .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
+ .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
+ .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
+ let attachments = [color_attachment];
+ let subpasses = [subpass];
+ let dependencies = [dependency];
+ let create_info = vk::RenderPassCreateInfo::default()
+ .attachments(&attachments)
+ .subpasses(&subpasses)
+ .dependencies(&dependencies);
+ // SAFETY: The render-pass create info only references stack-owned descriptors.
+ unsafe { device.device().create_render_pass(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateRenderPass",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn create_pipeline_layout(
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
+ let create_info = vk::PipelineLayoutCreateInfo::default();
+ // SAFETY: The pipeline layout contains no descriptor sets or push constants.
+ unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreatePipelineLayout",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn extent_component_to_f32(value: u32) -> f32 {
+ u16::try_from(value).map_or(f32::from(u16::MAX), f32::from)
+}
+
+#[allow(clippy::too_many_lines)]
+fn create_graphics_pipeline(
+ device: &VulkanLogicalDeviceProbe,
+ render_pass: vk::RenderPass,
+ pipeline_layout: vk::PipelineLayout,
+ extent: (u32, u32),
+) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
+ let entry_point = c"main";
+ let vertex_module = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS)?;
+ let fragment_module = match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS) {
+ Ok(fragment_module) => fragment_module,
+ Err(error) => {
+ // SAFETY: The vertex shader module was created above on this logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_shader_module(vertex_module, None) };
+ return Err(error);
+ }
+ };
+ let stage_create_infos = [
+ vk::PipelineShaderStageCreateInfo::default()
+ .stage(vk::ShaderStageFlags::VERTEX)
+ .module(vertex_module)
+ .name(entry_point),
+ vk::PipelineShaderStageCreateInfo::default()
+ .stage(vk::ShaderStageFlags::FRAGMENT)
+ .module(fragment_module)
+ .name(entry_point),
+ ];
+ let binding_descriptions = [vk::VertexInputBindingDescription {
+ binding: 0,
+ stride: 20,
+ input_rate: vk::VertexInputRate::VERTEX,
+ }];
+ let attribute_descriptions = [
+ vk::VertexInputAttributeDescription {
+ location: 0,
+ binding: 0,
+ format: vk::Format::R32G32_SFLOAT,
+ offset: 0,
+ },
+ vk::VertexInputAttributeDescription {
+ location: 1,
+ binding: 0,
+ format: vk::Format::R32G32B32_SFLOAT,
+ offset: 8,
+ },
+ ];
+ let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
+ .vertex_binding_descriptions(&binding_descriptions)
+ .vertex_attribute_descriptions(&attribute_descriptions);
+ let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
+ .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
+ let viewports = [vk::Viewport {
+ x: 0.0,
+ y: 0.0,
+ width: extent_component_to_f32(extent.0),
+ height: extent_component_to_f32(extent.1),
+ min_depth: 0.0,
+ max_depth: 1.0,
+ }];
+ let scissors = [vk::Rect2D {
+ offset: vk::Offset2D { x: 0, y: 0 },
+ extent: vk::Extent2D {
+ width: extent.0,
+ height: extent.1,
+ },
+ }];
+ let viewport_state = vk::PipelineViewportStateCreateInfo::default()
+ .viewports(&viewports)
+ .scissors(&scissors);
+ let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
+ .polygon_mode(vk::PolygonMode::FILL)
+ .cull_mode(vk::CullModeFlags::BACK)
+ .front_face(vk::FrontFace::CLOCKWISE)
+ .line_width(1.0);
+ let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
+ .rasterization_samples(vk::SampleCountFlags::TYPE_1);
+ let color_blend_attachment = [vk::PipelineColorBlendAttachmentState::default()
+ .color_write_mask(
+ vk::ColorComponentFlags::R
+ | vk::ColorComponentFlags::G
+ | vk::ColorComponentFlags::B
+ | vk::ColorComponentFlags::A,
+ )];
+ let color_blend_state =
+ vk::PipelineColorBlendStateCreateInfo::default().attachments(&color_blend_attachment);
+ let create_info = [vk::GraphicsPipelineCreateInfo::default()
+ .stages(&stage_create_infos)
+ .vertex_input_state(&vertex_input_state)
+ .input_assembly_state(&input_assembly_state)
+ .viewport_state(&viewport_state)
+ .rasterization_state(&rasterization_state)
+ .multisample_state(&multisample_state)
+ .color_blend_state(&color_blend_state)
+ .layout(pipeline_layout)
+ .render_pass(render_pass)
+ .subpass(0)];
+ // SAFETY: The pipeline creation references live shader modules and stack-owned fixed-function descriptors.
+ let pipeline_result = unsafe {
+ device
+ .device()
+ .create_graphics_pipelines(vk::PipelineCache::null(), &create_info, None)
+ };
+ // SAFETY: Shader modules are no longer needed after pipeline creation completes.
+ unsafe {
+ device.device().destroy_shader_module(vertex_module, None);
+ device.device().destroy_shader_module(fragment_module, None);
+ }
+ let pipeline =
+ pipeline_result.map_err(|(_, error)| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateGraphicsPipelines",
+ result: format!("{error:?}"),
+ })?[0];
+ Ok(pipeline)
+}
+
+fn create_shader_module(
+ device: &VulkanLogicalDeviceProbe,
+ words: &[u32],
+) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
+ let create_info = vk::ShaderModuleCreateInfo::default().code(words);
+ // SAFETY: SPIR-V words are immutable and valid for the duration of the call.
+ unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateShaderModule",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn create_framebuffer(
+ device: &VulkanLogicalDeviceProbe,
+ render_pass: vk::RenderPass,
+ image_view: vk::ImageView,
+ extent: (u32, u32),
+) -> Result<vk::Framebuffer, VulkanSmokeRendererError> {
+ let attachments = [image_view];
+ let create_info = vk::FramebufferCreateInfo::default()
+ .render_pass(render_pass)
+ .attachments(&attachments)
+ .width(extent.0)
+ .height(extent.1)
+ .layers(1);
+ // SAFETY: The framebuffer attachments and render pass remain live for the duration of the call.
+ unsafe { device.device().create_framebuffer(&create_info, None) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateFramebuffer",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn allocate_command_buffers(
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ count: u32,
+) -> Result<Vec<vk::CommandBuffer>, VulkanSmokeRendererError> {
+ let allocate_info = vk::CommandBufferAllocateInfo::default()
+ .command_pool(command_pool)
+ .level(vk::CommandBufferLevel::PRIMARY)
+ .command_buffer_count(count);
+ // SAFETY: Command buffers are allocated from a live resettable pool owned by this device.
+ unsafe { device.device().allocate_command_buffers(&allocate_info) }.map_err(|error| {
+ VulkanSmokeRendererError::VulkanOperation {
+ context: "vkAllocateCommandBuffers",
+ result: format!("{error:?}"),
+ }
+ })
+}
+
+fn create_frame_sync(
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<Vec<VulkanFrameSync>, VulkanSmokeRendererError> {
+ let semaphore_info = vk::SemaphoreCreateInfo::default();
+ let fence_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED);
+ let mut sync = Vec::with_capacity(2);
+ for _ in 0..2 {
+ // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
+ let image_available = unsafe { device.device().create_semaphore(&semaphore_info, None) }
+ .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateSemaphore(image_available)",
+ result: format!("{error:?}"),
+ })?;
+ let render_finished =
+ // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
+ match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
+ Ok(render_finished) => render_finished,
+ Err(error) => {
+ destroy_frame_sync_objects(device, &sync);
+ // SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure.
+ unsafe { device.device().destroy_semaphore(image_available, None) };
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateSemaphore(render_finished)",
+ result: format!("{error:?}"),
+ });
+ }
+ };
+ let fence =
+ // SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
+ match unsafe { device.device().create_fence(&fence_info, None) } {
+ Ok(fence) => fence,
+ Err(error) => {
+ destroy_frame_sync_objects(device, &sync);
+ // SAFETY: These semaphores were created above on this logical device and are destroyed on setup failure.
+ unsafe {
+ device.device().destroy_semaphore(image_available, None);
+ device.device().destroy_semaphore(render_finished, None);
+ }
+ return Err(VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreateFence",
+ result: format!("{error:?}"),
+ });
+ }
+ };
+ sync.push(VulkanFrameSync {
+ image_available,
+ render_finished,
+ fence,
+ });
+ }
+ Ok(sync)
+}
+
+fn destroy_swapchain_resources(
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ resources: VulkanSwapchainResources,
+) {
+ // SAFETY: All swapchain-dependent objects belong to this device and are destroyed once.
+ unsafe {
+ device
+ .device()
+ .free_command_buffers(command_pool, &resources.command_buffers);
+ for framebuffer in resources.framebuffers {
+ device.device().destroy_framebuffer(framebuffer, None);
+ }
+ device.device().destroy_pipeline(resources.pipeline, None);
+ device
+ .device()
+ .destroy_pipeline_layout(resources.pipeline_layout, None);
+ device
+ .device()
+ .destroy_render_pass(resources.render_pass, None);
+ for image_view in resources.image_views {
+ device.device().destroy_image_view(image_view, None);
+ }
+ }
+}
+
+fn destroy_partial_swapchain_resources(
+ device: &VulkanLogicalDeviceProbe,
+ command_pool: vk::CommandPool,
+ resources: PartialSwapchainResources,
+) {
+ // SAFETY: All handles in this partial resource set were created on this live logical device and are destroyed once.
+ unsafe {
+ if !resources.command_buffers.is_empty() {
+ device
+ .device()
+ .free_command_buffers(command_pool, &resources.command_buffers);
+ }
+ for framebuffer in resources.framebuffers {
+ device.device().destroy_framebuffer(framebuffer, None);
+ }
+ if let Some(pipeline) = resources.pipeline {
+ device.device().destroy_pipeline(pipeline, None);
+ }
+ if let Some(pipeline_layout) = resources.pipeline_layout {
+ device
+ .device()
+ .destroy_pipeline_layout(pipeline_layout, None);
+ }
+ if let Some(render_pass) = resources.render_pass {
+ device.device().destroy_render_pass(render_pass, None);
+ }
+ for image_view in resources.image_views {
+ device.device().destroy_image_view(image_view, None);
+ }
+ }
+}
+
+fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanFrameSync]) {
+ for frame_sync in sync {
+ // SAFETY: These sync objects belong to this live logical device and are destroyed once during teardown.
+ unsafe {
+ device
+ .device()
+ .destroy_semaphore(frame_sync.image_available, None);
+ device
+ .device()
+ .destroy_semaphore(frame_sync.render_finished, None);
+ device.device().destroy_fence(frame_sync.fence, None);
+ }
+ }
+}
+
+fn destroy_allocated_buffer(device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer) {
+ // SAFETY: The buffer and allocation belong to this live logical device and are destroyed once during teardown.
+ unsafe {
+ device.device().destroy_buffer(buffer.buffer, None);
+ device.device().free_memory(buffer.memory, None);
+ }
+}
+
+fn color_subresource_range() -> vk::ImageSubresourceRange {
+ vk::ImageSubresourceRange::default()
+ .aspect_mask(vk::ImageAspectFlags::COLOR)
+ .base_mip_level(0)
+ .level_count(1)
+ .base_array_layer(0)
+ .layer_count(1)
+}
+
+/// Runtime swapchain creation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Deterministic swapchain policy used for creation.
+ pub plan: VulkanSwapchainPlan,
+ /// Number of images returned by `vkGetSwapchainImagesKHR`.
+ pub image_count: u32,
+}
+
+/// Live Vulkan device/surface capability probe error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanRuntimeCapabilityError {
+ /// Physical device enumeration failed.
+ EnumerateDevicesFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// Device extension enumeration failed.
+ EnumerateDeviceExtensionsFailed {
+ /// Device name or index context.
+ device: String,
+ /// Vulkan result.
+ result: String,
+ },
+ /// Queue-family present support query failed.
+ PresentSupportFailed {
+ /// Device name.
+ device: String,
+ /// Queue-family index.
+ queue_family: u32,
+ /// Vulkan result.
+ result: String,
+ },
+ /// Surface format query failed.
+ SurfaceFormatsFailed {
+ /// Device name.
+ device: String,
+ /// Vulkan result.
+ result: String,
+ },
+ /// Surface capability query failed.
+ SurfaceCapabilitiesFailed {
+ /// Device name.
+ device: String,
+ /// Vulkan result.
+ result: String,
+ },
+ /// Present mode query failed.
+ PresentModesFailed {
+ /// Device name.
+ device: String,
+ /// Vulkan result.
+ result: String,
+ },
+ /// No device satisfied Stage 0 capability policy.
+ Capability(VulkanCapabilityError),
+ /// Live surface capabilities could not produce a swapchain plan.
+ Swapchain(VulkanSwapchainError),
+}
+
+impl std::fmt::Display for VulkanRuntimeCapabilityError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::EnumerateDevicesFailed { result } => {
+ write!(f, "Vulkan physical device enumeration failed: {result}")
+ }
+ Self::EnumerateDeviceExtensionsFailed { device, result } => write!(
+ f,
+ "Vulkan device {device} extension enumeration failed: {result}"
+ ),
+ Self::PresentSupportFailed {
+ device,
+ queue_family,
+ result,
+ } => write!(
+ f,
+ "Vulkan device {device} queue family {queue_family} present support query failed: {result}"
+ ),
+ Self::SurfaceFormatsFailed { device, result } => write!(
+ f,
+ "Vulkan device {device} surface format query failed: {result}"
+ ),
+ Self::SurfaceCapabilitiesFailed { device, result } => write!(
+ f,
+ "Vulkan device {device} surface capabilities query failed: {result}"
+ ),
+ Self::PresentModesFailed { device, result } => write!(
+ f,
+ "Vulkan device {device} present mode query failed: {result}"
+ ),
+ Self::Capability(error) => write!(f, "{error}"),
+ Self::Swapchain(error) => write!(f, "{error}"),
+ }
+ }
+}
+
+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 {}
+
+/// Vulkan swapchain creation error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainProbeError {
+ /// Surface capability query failed.
+ SurfaceCapabilitiesFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// Swapchain creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// Swapchain image query failed.
+ ImagesFailed {
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanSwapchainProbeError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::SurfaceCapabilitiesFailed { result } => {
+ write!(f, "Vulkan surface capabilities query failed: {result}")
+ }
+ Self::CreateFailed { result } => {
+ write!(f, "Vulkan swapchain creation failed: {result}")
+ }
+ Self::ImagesFailed { result } => {
+ write!(f, "Vulkan swapchain image query failed: {result}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanSwapchainProbeError {}
+
+/// Builds a deterministic Vulkan surface plan from native window handles.
+///
+/// # Errors
+///
+/// Returns [`VulkanSurfaceError`] when no native handles exist or the platform
+/// display backend has no Vulkan surface extension mapping.
+pub fn plan_vulkan_surface(
+ handles: Option<NativeWindowHandles>,
+) -> Result<VulkanSurfacePlan, VulkanSurfaceError> {
+ let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
+ let required = ash_window::enumerate_required_extensions(handles.display).map_err(|error| {
+ VulkanSurfaceError::RequiredExtensionsFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ let mut required_instance_extensions = Vec::with_capacity(required.len());
+ for extension in required {
+ let name = extension_name(*extension)?;
+ required_instance_extensions.push(name);
+ }
+ required_instance_extensions.sort();
+ required_instance_extensions.dedup();
+ Ok(VulkanSurfacePlan {
+ schema: 1,
+ required_instance_extensions,
+ })
+}
+
+/// Creates a Vulkan surface probe from native window handles.
+///
+/// # Errors
+///
+/// Returns [`VulkanSurfaceError`] when handles are missing, required extensions
+/// cannot be planned, or `vkCreate*SurfaceKHR` fails.
+pub fn create_vulkan_surface_probe(
+ instance: &VulkanInstanceProbe,
+ handles: Option<NativeWindowHandles>,
+) -> Result<VulkanSurfaceProbe, VulkanSurfaceError> {
+ let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
+ let report = plan_vulkan_surface(Some(handles))?;
+ // SAFETY: The platform handles are only used to create a child surface owned by this probe.
+ let surface = unsafe {
+ ash_window::create_surface(
+ &instance.entry,
+ &instance.instance,
+ handles.display,
+ handles.window,
+ None,
+ )
+ }
+ .map_err(|error| VulkanSurfaceError::CreateFailed {
+ result: format!("{error:?}"),
+ })?;
+ Ok(VulkanSurfaceProbe {
+ loader: surface::Instance::new(&instance.entry, &instance.instance),
+ surface,
+ report,
+ })
+}
+
+/// Probes live Vulkan device, queue, surface and swapchain capabilities.
+///
+/// # Errors
+///
+/// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface
+/// capability queries, Stage 0 device selection, or swapchain planning fails.
+pub fn probe_vulkan_runtime_capabilities(
+ instance: &VulkanInstanceProbe,
+ 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,
+ physical_device: selected.physical_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,
+ })
+}
+
+/// Creates a Vulkan swapchain for the live logical device and surface.
+///
+/// # Errors
+///
+/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
+/// swapchain creation, or swapchain image enumeration fails.
+pub fn create_vulkan_swapchain_probe(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
+ create_vulkan_swapchain_probe_for_extent(
+ instance,
+ surface,
+ device,
+ device.runtime.swapchain.extent,
+ vk::SwapchainKHR::null(),
+ )
+}
+
+/// Creates a Vulkan swapchain for the live logical device and surface at a specific extent.
+///
+/// # Errors
+///
+/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
+/// swapchain creation, or swapchain image enumeration fails.
+pub fn create_vulkan_swapchain_probe_for_extent(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ device: &VulkanLogicalDeviceProbe,
+ drawable_extent: (u32, u32),
+ old_swapchain: vk::SwapchainKHR,
+) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
+ let raw_capabilities = {
+ // SAFETY: The physical device and surface are live query inputs and no handles are retained.
+ unsafe {
+ surface
+ .loader
+ .get_physical_device_surface_capabilities(device.physical_device, surface.surface)
+ }
+ }
+ .map_err(
+ |error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
+ result: format!("{error:?}"),
+ },
+ )?;
+ let surface_formats =
+ live_surface_formats(surface, device.physical_device, &device.report.device_name).map_err(
+ |error| VulkanSwapchainProbeError::CreateFailed {
+ result: error.to_string(),
+ },
+ )?;
+ let present_modes =
+ live_present_modes(surface, device.physical_device, &device.report.device_name).map_err(
+ |error| VulkanSwapchainProbeError::CreateFailed {
+ result: error.to_string(),
+ },
+ )?;
+ let capabilities =
+ live_surface_capabilities(surface, device.physical_device, &device.report.device_name)
+ .map_err(
+ |error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
+ result: error.to_string(),
+ },
+ )?;
+ let plan = plan_vulkan_swapchain(&VulkanSwapchainRequest {
+ drawable_extent,
+ formats: surface_formats,
+ present_modes,
+ capabilities,
+ preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
+ })
+ .map_err(|error| VulkanSwapchainProbeError::CreateFailed {
+ result: error.to_string(),
+ })?;
+ let queue_family_indices = unique_queue_families(
+ device.runtime.capability.graphics_queue_family,
+ device.runtime.capability.present_queue_family,
+ );
+ let sharing_mode = if queue_family_indices.len() > 1 {
+ vk::SharingMode::CONCURRENT
+ } else {
+ vk::SharingMode::EXCLUSIVE
+ };
+ let create_info = vk::SwapchainCreateInfoKHR::default()
+ .surface(surface.surface)
+ .min_image_count(plan.image_count)
+ .image_format(vk::Format::from_raw(plan.format.format))
+ .image_color_space(vk::ColorSpaceKHR::from_raw(plan.format.color_space))
+ .image_extent(vk::Extent2D {
+ width: plan.extent.0,
+ height: plan.extent.1,
+ })
+ .image_array_layers(1)
+ .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
+ .image_sharing_mode(sharing_mode)
+ .queue_family_indices(&queue_family_indices)
+ .pre_transform(raw_capabilities.current_transform)
+ .composite_alpha(select_composite_alpha(
+ raw_capabilities.supported_composite_alpha,
+ ))
+ .present_mode(vk::PresentModeKHR::from_raw(plan.present_mode))
+ .old_swapchain(old_swapchain)
+ .clipped(true);
+ let loader = swapchain::Device::new(&instance.instance, &device.device);
+ // SAFETY: The create info references live instance/device/surface handles for this call.
+ let swapchain = unsafe { loader.create_swapchain(&create_info, None) }.map_err(|error| {
+ VulkanSwapchainProbeError::CreateFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The swapchain was created above and the returned image handles are owned by it.
+ let images = match unsafe { loader.get_swapchain_images(swapchain) } {
+ Ok(images) => images,
+ Err(error) => {
+ // SAFETY: The swapchain was created above on this loader/device pair and is destroyed on setup failure.
+ unsafe { loader.destroy_swapchain(swapchain, None) };
+ return Err(VulkanSwapchainProbeError::ImagesFailed {
+ result: format!("{error:?}"),
+ });
+ }
+ };
+ Ok(VulkanSwapchainProbe {
+ loader,
+ swapchain,
+ report: VulkanSwapchainReport {
+ schema: 1,
+ plan,
+ image_count: images.len().try_into().unwrap_or(u32::MAX),
+ },
+ })
+}
+
+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| {
+ VulkanRuntimeCapabilityError::EnumerateDevicesFailed {
+ result: format!("{error:?}"),
+ }
+ })?
+ };
+ let mut best: Option<LiveDeviceCandidate> = None;
+ let mut last_error = None;
+ for (index, device) in devices.iter().copied().enumerate() {
+ let candidate = match live_device_candidate(instance, surface, device, index) {
+ Ok(candidate) => candidate,
+ Err(err) => {
+ last_error = Some(err);
+ continue;
+ }
+ };
+ match &best {
+ Some(existing)
+ if compare_reports(&candidate.capability, &existing.capability)
+ != std::cmp::Ordering::Greater => {}
+ _ => best = Some(candidate),
+ }
+ }
+ let best = best.ok_or_else(|| {
+ last_error.unwrap_or(VulkanRuntimeCapabilityError::Capability(
+ VulkanCapabilityError::NoPhysicalDevice,
+ ))
+ })?;
+ let swapchain = plan_vulkan_swapchain(&VulkanSwapchainRequest {
+ drawable_extent,
+ formats: best.surface_formats,
+ present_modes: best.present_modes,
+ capabilities: best.surface_capabilities,
+ preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
+ })
+ .map_err(VulkanRuntimeCapabilityError::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>,
+ surface_capabilities: VulkanSwapchainSurfaceCapabilities,
+}
+
+fn live_device_candidate(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ device: vk::PhysicalDevice,
+ index: usize,
+) -> Result<LiveDeviceCandidate, VulkanRuntimeCapabilityError> {
+ let properties = {
+ // SAFETY: `device` was returned by this live instance and the result is copied by value.
+ unsafe { instance.instance.get_physical_device_properties(device) }
+ };
+ let name = physical_device_name(&properties, index);
+ let queue_properties = {
+ // SAFETY: `device` was returned by this live instance and the result is owned by Rust.
+ unsafe {
+ instance
+ .instance
+ .get_physical_device_queue_family_properties(device)
+ }
+ };
+ let extensions = live_device_extensions(instance, device, &name)?;
+ let surface_formats = live_surface_formats(surface, device, &name)?;
+ let present_modes = live_present_modes(surface, device, &name)?;
+ let surface_capabilities = live_surface_capabilities(surface, device, &name)?;
+ let queue_families = queue_properties
+ .iter()
+ .enumerate()
+ .map(|(queue_index, properties)| {
+ let index = u32::try_from(queue_index).unwrap_or(u32::MAX);
+ let present = {
+ // SAFETY: The physical device, surface and queue-family index are live query inputs.
+ unsafe {
+ surface.loader.get_physical_device_surface_support(
+ device,
+ index,
+ surface.surface,
+ )
+ }
+ }
+ .map_err(|error| VulkanRuntimeCapabilityError::PresentSupportFailed {
+ device: name.clone(),
+ queue_family: index,
+ result: format!("{error:?}"),
+ })?;
+ Ok(VulkanQueueFamily {
+ index,
+ graphics: properties.queue_flags.contains(vk::QueueFlags::GRAPHICS),
+ present,
+ })
+ })
+ .collect::<Result<Vec<_>, VulkanRuntimeCapabilityError>>()?;
+ let record = VulkanPhysicalDeviceRecord {
+ name,
+ api_version: properties.api_version,
+ device_type: match properties.device_type {
+ vk::PhysicalDeviceType::DISCRETE_GPU => VulkanDeviceType::DiscreteGpu,
+ vk::PhysicalDeviceType::INTEGRATED_GPU => VulkanDeviceType::IntegratedGpu,
+ vk::PhysicalDeviceType::CPU => VulkanDeviceType::Cpu,
+ _ => VulkanDeviceType::Other,
+ },
+ extensions,
+ queue_families,
+ surface_formats: surface_formats.clone(),
+ };
+ let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
+ Ok(LiveDeviceCandidate {
+ physical_device: device,
+ capability,
+ surface_formats,
+ present_modes,
+ surface_capabilities,
+ })
+}
+
+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()) }
+ .to_string_lossy()
+ .trim()
+ .to_string();
+ if name.is_empty() {
+ format!("physical-device-{index}")
+ } else {
+ name
+ }
+}
+
+fn live_device_extensions(
+ instance: &VulkanInstanceProbe,
+ device: vk::PhysicalDevice,
+ name: &str,
+) -> Result<Vec<String>, VulkanRuntimeCapabilityError> {
+ let properties = {
+ // SAFETY: `device` was returned by this live instance and no borrowed data escapes.
+ unsafe {
+ instance
+ .instance
+ .enumerate_device_extension_properties(device)
+ }
+ }
+ .map_err(
+ |error| VulkanRuntimeCapabilityError::EnumerateDeviceExtensionsFailed {
+ device: name.to_string(),
+ result: format!("{error:?}"),
+ },
+ )?;
+ let mut extensions = properties
+ .iter()
+ .map(|property| {
+ // SAFETY: Vulkan extension names are fixed-size NUL-terminated C strings per the spec.
+ unsafe { CStr::from_ptr(property.extension_name.as_ptr()) }
+ .to_string_lossy()
+ .into_owned()
+ })
+ .collect::<Vec<_>>();
+ extensions.sort();
+ extensions.dedup();
+ Ok(extensions)
+}
+
+fn live_surface_formats(
+ surface: &VulkanSurfaceProbe,
+ device: vk::PhysicalDevice,
+ name: &str,
+) -> Result<Vec<VulkanSurfaceFormat>, VulkanRuntimeCapabilityError> {
+ let formats = {
+ // SAFETY: The physical device and surface are live query inputs and no handles are retained.
+ unsafe {
+ surface
+ .loader
+ .get_physical_device_surface_formats(device, surface.surface)
+ }
+ }
+ .map_err(|error| VulkanRuntimeCapabilityError::SurfaceFormatsFailed {
+ device: name.to_string(),
+ result: format!("{error:?}"),
+ })?;
+ Ok(formats
+ .into_iter()
+ .map(|format| VulkanSurfaceFormat {
+ format: format.format.as_raw(),
+ color_space: format.color_space.as_raw(),
+ })
+ .collect())
+}
+
+fn live_present_modes(
+ surface: &VulkanSurfaceProbe,
+ device: vk::PhysicalDevice,
+ name: &str,
+) -> Result<Vec<i32>, VulkanRuntimeCapabilityError> {
+ let modes = {
+ // SAFETY: The physical device and surface are live query inputs and no handles are retained.
+ unsafe {
+ surface
+ .loader
+ .get_physical_device_surface_present_modes(device, surface.surface)
+ }
+ }
+ .map_err(|error| VulkanRuntimeCapabilityError::PresentModesFailed {
+ device: name.to_string(),
+ result: format!("{error:?}"),
+ })?;
+ Ok(modes.into_iter().map(vk::PresentModeKHR::as_raw).collect())
+}
+
+fn live_surface_capabilities(
+ surface: &VulkanSurfaceProbe,
+ device: vk::PhysicalDevice,
+ name: &str,
+) -> Result<VulkanSwapchainSurfaceCapabilities, VulkanRuntimeCapabilityError> {
+ let capabilities = {
+ // SAFETY: The physical device and surface are live query inputs and no handles are retained.
+ unsafe {
+ surface
+ .loader
+ .get_physical_device_surface_capabilities(device, surface.surface)
+ }
+ }
+ .map_err(
+ |error| VulkanRuntimeCapabilityError::SurfaceCapabilitiesFailed {
+ device: name.to_string(),
+ result: format!("{error:?}"),
+ },
+ )?;
+ Ok(VulkanSwapchainSurfaceCapabilities {
+ current_extent: if capabilities.current_extent.width == u32::MAX {
+ None
+ } else {
+ Some((
+ capabilities.current_extent.width,
+ capabilities.current_extent.height,
+ ))
+ },
+ min_extent: (
+ capabilities.min_image_extent.width,
+ capabilities.min_image_extent.height,
+ ),
+ max_extent: (
+ capabilities.max_image_extent.width,
+ capabilities.max_image_extent.height,
+ ),
+ min_image_count: capabilities.min_image_count,
+ max_image_count: capabilities.max_image_count,
+ })
+}
+
+/// Renders a deterministic JSON Vulkan surface plan.
+#[must_use]
+pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&plan.schema.to_string());
+ out.push_str(",\"required_instance_extensions\":[");
+ for (index, extension) in plan.required_instance_extensions.iter().enumerate() {
+ if index > 0 {
+ out.push(',');
+ }
+ push_json_string(&mut out, extension);
+ }
+ out.push_str("]}");
+ out
+}
+
+fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
+ // SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
+ let name = unsafe { CStr::from_ptr(extension) };
+ name.to_str()
+ .map(str::to_string)
+ .map_err(|_| VulkanSurfaceError::InvalidExtensionName)
+}
+
+/// Vulkan instance bootstrap error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanInstanceError {
+ /// The Vulkan loader could not be opened.
+ Loader(VulkanLoaderError),
+ /// Application name contained an interior NUL byte.
+ InvalidApplicationName,
+ /// An extension name contained an interior NUL byte.
+ InvalidExtensionName {
+ /// Invalid extension name.
+ extension: String,
+ },
+ /// Validation layers were requested but unavailable.
+ MissingValidationLayer,
+ /// Instance creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanInstanceError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Loader(error) => write!(f, "{error}"),
+ Self::InvalidApplicationName => {
+ write!(f, "Vulkan application name contains an interior NUL byte")
+ }
+ Self::InvalidExtensionName { extension } => {
+ write!(
+ f,
+ "Vulkan instance extension name contains an interior NUL byte: {extension:?}"
+ )
+ }
+ Self::MissingValidationLayer => {
+ write!(
+ f,
+ "Vulkan validation layer VK_LAYER_KHRONOS_validation is unavailable"
+ )
+ }
+ Self::CreateFailed { result } => write!(f, "Vulkan instance creation failed: {result}"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanInstanceError {}
+
+/// Builds the deterministic instance creation plan without touching the loader.
+#[must_use]
+pub fn plan_vulkan_instance(config: &VulkanInstanceConfig) -> VulkanInstancePlan {
+ let mut enabled_extensions = config.required_extensions.clone();
+ if config.enable_validation
+ && !enabled_extensions
+ .iter()
+ .any(|extension| extension == EXT_DEBUG_UTILS_EXTENSION)
+ {
+ enabled_extensions.push(EXT_DEBUG_UTILS_EXTENSION.to_string());
+ }
+ if config.enable_portability_enumeration
+ && !enabled_extensions
+ .iter()
+ .any(|extension| extension == KHR_PORTABILITY_ENUMERATION_EXTENSION)
+ {
+ enabled_extensions.push(KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string());
+ }
+ enabled_extensions.sort();
+ enabled_extensions.dedup();
+ VulkanInstancePlan {
+ schema: 1,
+ enabled_extensions,
+ create_flags: if config.enable_portability_enumeration {
+ vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR.as_raw()
+ } else {
+ 0
+ },
+ validation_requested: config.enable_validation,
+ }
+}
+
+/// Creates a Vulkan instance probe from the supplied configuration.
+///
+/// # Errors
+///
+/// Returns [`VulkanInstanceError`] when the loader is unavailable, names are not
+/// valid C strings, or `vkCreateInstance` fails.
+pub fn create_vulkan_instance_probe(
+ config: &VulkanInstanceConfig,
+) -> Result<VulkanInstanceProbe, VulkanInstanceError> {
+ // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
+ let entry = unsafe { ash::Entry::load() }.map_err(|error| {
+ VulkanInstanceError::Loader(VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })
+ })?;
+ let app_name = CString::new(config.application_name.clone())
+ .map_err(|_| VulkanInstanceError::InvalidApplicationName)?;
+ let engine_name = c"fparkan";
+ let plan = plan_vulkan_instance(config);
+ let extension_names = cstring_vec(&plan.enabled_extensions)?;
+ let extension_ptrs = cstring_ptrs(&extension_names);
+ let layer_names = validation_layer_cstrings(&entry, config.enable_validation)?;
+ let layer_ptrs = cstring_ptrs(&layer_names);
+ let app_info = vk::ApplicationInfo::default()
+ .application_name(&app_name)
+ .application_version(0)
+ .engine_name(engine_name)
+ .engine_version(0)
+ .api_version(MIN_VULKAN_API_VERSION);
+ let create_info = vk::InstanceCreateInfo::default()
+ .application_info(&app_info)
+ .enabled_extension_names(&extension_ptrs)
+ .enabled_layer_names(&layer_ptrs)
+ .flags(vk::InstanceCreateFlags::from_raw(plan.create_flags));
+ // SAFETY: `create_info` points to stack-owned Vulkan create data that lives for the call.
+ let instance = unsafe { entry.create_instance(&create_info, None) }.map_err(|error| {
+ VulkanInstanceError::CreateFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ Ok(VulkanInstanceProbe {
+ entry,
+ instance,
+ report: plan,
+ })
+}
+
+fn validation_layer_cstrings(
+ entry: &ash::Entry,
+ enable_validation: bool,
+) -> Result<Vec<CString>, VulkanInstanceError> {
+ if !enable_validation {
+ return Ok(Vec::new());
+ }
+ let available_layers =
+ // SAFETY: Enumerating instance layers reads loader-owned immutable metadata.
+ unsafe { entry.enumerate_instance_layer_properties() }.map_err(|error| {
+ VulkanInstanceError::CreateFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ let validation_available = available_layers.iter().any(|layer| {
+ // SAFETY: Vulkan layer names are fixed-size NUL-terminated strings from the loader.
+ unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) }
+ .to_string_lossy()
+ .as_ref()
+ == VALIDATION_LAYER_NAME
+ });
+ if !validation_available {
+ return Err(VulkanInstanceError::MissingValidationLayer);
+ }
+ Ok(vec![CString::new(VALIDATION_LAYER_NAME).map_err(|_| {
+ VulkanInstanceError::InvalidApplicationName
+ })?])
+}
+
+/// Renders a deterministic JSON Vulkan instance plan.
+#[must_use]
+pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&plan.schema.to_string());
+ out.push_str(",\"create_flags\":");
+ out.push_str(&plan.create_flags.to_string());
+ out.push_str(",\"validation_requested\":");
+ out.push_str(if plan.validation_requested {
+ "true"
+ } else {
+ "false"
+ });
+ out.push_str(",\"enabled_extensions\":[");
+ for (index, extension) in plan.enabled_extensions.iter().enumerate() {
+ if index > 0 {
+ out.push(',');
+ }
+ push_json_string(&mut out, extension);
+ }
+ out.push_str("]}");
+ out
+}
+
+fn cstring_vec(values: &[String]) -> Result<Vec<CString>, VulkanInstanceError> {
+ values
+ .iter()
+ .map(|extension| {
+ CString::new(extension.as_str()).map_err(|_| {
+ VulkanInstanceError::InvalidExtensionName {
+ extension: extension.clone(),
+ }
+ })
+ })
+ .collect()
+}
+
+fn cstring_ptrs(values: &[CString]) -> Vec<*const c_char> {
+ values.iter().map(|value| value.as_ptr()).collect()
+}
+
+/// Deterministic Vulkan loader probe report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanLoaderProbeReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Whether the Vulkan loader was opened successfully.
+ pub loader_available: bool,
+ /// Reported loader instance API version.
+ pub instance_api_version: u32,
+}
+
+/// Vulkan loader bootstrap error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanLoaderError {
+ /// The Vulkan loader library could not be opened.
+ Unavailable {
+ /// Loader error text.
+ message: String,
+ },
+}
+
+impl std::fmt::Display for VulkanLoaderError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Unavailable { message } => {
+ write!(f, "Vulkan loader is unavailable: {message}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanLoaderError {}
+
+/// Opens the Vulkan loader and reports the supported instance API version.
+///
+/// # Errors
+///
+/// Returns [`VulkanLoaderError`] when no Vulkan loader library can be opened on
+/// the host.
+pub fn probe_vulkan_loader() -> Result<VulkanLoaderProbeReport, VulkanLoaderError> {
+ // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
+ let entry = unsafe { ash::Entry::load() }.map_err(|error| VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })?;
+ // SAFETY: The resolved entry only queries the loader-supported instance API version.
+ let version = unsafe { entry.try_enumerate_instance_version() }
+ .map_err(|error| VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })?
+ .unwrap_or(vk::API_VERSION_1_0);
+ Ok(VulkanLoaderProbeReport {
+ schema: 1,
+ loader_available: true,
+ instance_api_version: version,
+ })
+}
+
+/// Returns the static Vulkan entry name used by loader probes.
+#[must_use]
+pub fn vulkan_entry_symbol_name() -> &'static CStr {
+ c"vkGetInstanceProcAddr"
+}
+
+/// Renders a deterministic JSON Vulkan loader report.
+#[must_use]
+pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&report.schema.to_string());
+ out.push_str(",\"loader_available\":");
+ out.push_str(if report.loader_available {
+ "true"
+ } else {
+ "false"
+ });
+ out.push_str(",\"instance_api\":\"");
+ out.push_str(&format_api_version(report.instance_api_version));
+ out.push_str("\"}");
+ out
+}
+
+/// Returns the built-in Stage 0 indexed-triangle shader manifest.
+#[must_use]
+pub fn triangle_shader_manifest() -> Vec<VulkanShaderModuleManifest> {
+ vec![
+ VulkanShaderModuleManifest {
+ name: "triangle.vert",
+ stage: VulkanShaderStage::Vertex,
+ entry_point: "main",
+ descriptor_sets: 0,
+ push_constant_bytes: 0,
+ source_path: TRIANGLE_VERTEX_SOURCE_PATH,
+ source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
+ spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
+ compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
+ validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
+ words: TRIANGLE_VERTEX_SHADER_WORDS,
+ },
+ VulkanShaderModuleManifest {
+ name: "triangle.frag",
+ stage: VulkanShaderStage::Fragment,
+ entry_point: "main",
+ descriptor_sets: 0,
+ push_constant_bytes: 0,
+ source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
+ source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
+ spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
+ compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
+ validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
+ words: TRIANGLE_FRAGMENT_SHADER_WORDS,
+ },
+ ]
+}
+
+/// Validates shader SPIR-V containers and renders a deterministic report.
+///
+/// # Errors
+///
+/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
+/// container validation.
+pub fn validate_shader_manifest(
+ modules: &[VulkanShaderModuleManifest],
+) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
+ let mut reports = Vec::with_capacity(modules.len());
+ for module in modules {
+ validate_spirv_container(module)?;
+ let bytes = spirv_words_to_bytes(module.words);
+ reports.push(VulkanShaderModuleReport {
+ name: module.name,
+ stage: module.stage,
+ entry_point: module.entry_point,
+ source_path: module.source_path,
+ source_sha256: module.source_sha256,
+ spirv_path: module.spirv_path,
+ word_count: module.words.len(),
+ sha256: sha256_hex(&sha256(&bytes)),
+ descriptor_sets: module.descriptor_sets,
+ push_constant_bytes: module.push_constant_bytes,
+ compile_command: module.compile_command,
+ validate_command: module.validate_command,
+ interface_hash: shader_interface_hash(module),
+ });
+ }
+ let normalized = render_shader_manifest_without_hash_json(&reports);
+ Ok(VulkanShaderManifestReport {
+ schema: SHADER_MANIFEST_SCHEMA,
+ target_env: SHADER_TARGET_ENV,
+ compiler: VulkanShaderToolManifest {
+ name: SHADER_COMPILER_NAME,
+ version: SHADER_COMPILER_VERSION,
+ binary_sha256: SHADER_COMPILER_BINARY_SHA256,
+ },
+ validator: VulkanShaderToolManifest {
+ name: SPIRV_VALIDATOR_NAME,
+ version: SPIRV_VALIDATOR_VERSION,
+ binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
+ },
+ modules: reports,
+ manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
+ })
+}
+
+fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
+ let mut normalized = String::new();
+ normalized.push_str("{\"stage\":\"");
+ normalized.push_str(module.stage.as_str());
+ normalized.push_str("\",\"entry_point\":");
+ push_json_string(&mut normalized, module.entry_point);
+ normalized.push_str(",\"descriptor_sets\":");
+ normalized.push_str(&module.descriptor_sets.to_string());
+ normalized.push_str(",\"push_constant_bytes\":");
+ normalized.push_str(&module.push_constant_bytes.to_string());
+ normalized.push('}');
+ sha256_hex(&sha256(normalized.as_bytes()))
+}
+
+fn validate_spirv_container(
+ module: &VulkanShaderModuleManifest,
+) -> Result<(), VulkanShaderManifestError> {
+ if module.words.len() < 5 {
+ return Err(VulkanShaderManifestError::TooShort { name: module.name });
+ }
+ if module.words[0] != SPIRV_MAGIC {
+ return Err(VulkanShaderManifestError::InvalidMagic {
+ name: module.name,
+ found: module.words[0],
+ });
+ }
+ if module.words[1] < SPIRV_VERSION_1_0 {
+ return Err(VulkanShaderManifestError::UnsupportedVersion {
+ name: module.name,
+ found: module.words[1],
+ });
+ }
+ if module.words[3] == 0 {
+ return Err(VulkanShaderManifestError::InvalidBound { name: module.name });
+ }
+ Ok(())
+}
+
+fn spirv_words_to_bytes(words: &[u32]) -> Vec<u8> {
+ let mut out = Vec::with_capacity(words.len() * 4);
+ for word in words {
+ out.extend_from_slice(&word.to_le_bytes());
+ }
+ out
+}
+
+/// Renders a deterministic JSON shader manifest report.
+#[must_use]
+pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
+ let mut out = render_shader_manifest_without_hash_json(&report.modules);
+ out.push_str(",\"manifest_hash\":");
+ push_json_string(&mut out, &report.manifest_hash);
+ out.push('}');
+ out
+}
+
+fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&SHADER_MANIFEST_SCHEMA.to_string());
+ out.push_str(",\"target_env\":");
+ push_json_string(&mut out, SHADER_TARGET_ENV);
+ out.push_str(",\"compiler\":");
+ out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
+ name: SHADER_COMPILER_NAME,
+ version: SHADER_COMPILER_VERSION,
+ binary_sha256: SHADER_COMPILER_BINARY_SHA256,
+ }));
+ out.push_str(",\"validator\":");
+ out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
+ name: SPIRV_VALIDATOR_NAME,
+ version: SPIRV_VALIDATOR_VERSION,
+ binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
+ }));
+ out.push_str(",\"modules\":");
+ out.push_str(&render_shader_modules_json(modules));
+ out
+}
+
+fn render_shader_modules_json(modules: &[VulkanShaderModuleReport]) -> String {
+ let mut out = String::new();
+ out.push('[');
+ for (index, module) in modules.iter().enumerate() {
+ if index > 0 {
+ out.push(',');
+ }
+ out.push_str("{\"name\":");
+ push_json_string(&mut out, module.name);
+ out.push_str(",\"stage\":\"");
+ out.push_str(module.stage.as_str());
+ out.push_str("\",\"entry_point\":");
+ push_json_string(&mut out, module.entry_point);
+ out.push_str(",\"source_path\":");
+ push_json_string(&mut out, module.source_path);
+ out.push_str(",\"source_sha256\":");
+ push_json_string(&mut out, module.source_sha256);
+ out.push_str(",\"spirv_path\":");
+ push_json_string(&mut out, module.spirv_path);
+ out.push_str(",\"word_count\":");
+ out.push_str(&module.word_count.to_string());
+ out.push_str(",\"sha256\":");
+ push_json_string(&mut out, &module.sha256);
+ out.push_str(",\"descriptor_sets\":");
+ out.push_str(&module.descriptor_sets.to_string());
+ out.push_str(",\"push_constant_bytes\":");
+ out.push_str(&module.push_constant_bytes.to_string());
+ out.push_str(",\"compile_command\":");
+ push_json_string(&mut out, module.compile_command);
+ out.push_str(",\"validate_command\":");
+ push_json_string(&mut out, module.validate_command);
+ out.push_str(",\"interface_hash\":");
+ push_json_string(&mut out, &module.interface_hash);
+ out.push('}');
+ }
+ out.push(']');
+ out
+}
+
+fn render_shader_tool_json(tool: &VulkanShaderToolManifest) -> String {
+ let mut out = String::new();
+ out.push_str("{\"name\":");
+ push_json_string(&mut out, tool.name);
+ out.push_str(",\"version\":");
+ push_json_string(&mut out, tool.version);
+ out.push_str(",\"binary_sha256\":");
+ push_json_string(&mut out, tool.binary_sha256);
+ out.push('}');
+ out
+}
+
+/// Vulkan backend migration readiness.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanPlanningBackendState {
+ /// Adapter prepared and able to accept commands.
+ Ready,
+ /// Adapter is tracking a recoverable runtime surface/depth pipeline fault.
+ Degraded,
+ /// Adapter has encountered a non-recoverable error.
+ Error,
+}
+
+impl Default for VulkanPlanningBackendState {
+ fn default() -> Self {
+ Self::Degraded
+ }
+}
+
+/// 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,
+}
+
+/// Surface capabilities needed by the Stage 0 swapchain policy.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainSurfaceCapabilities {
+ /// Current surface extent, when dictated by the platform.
+ pub current_extent: Option<(u32, u32)>,
+ /// Minimum supported swapchain extent.
+ pub min_extent: (u32, u32),
+ /// Maximum supported swapchain extent.
+ pub max_extent: (u32, u32),
+ /// Minimum supported image count.
+ pub min_image_count: u32,
+ /// Maximum supported image count, or 0 when unbounded.
+ pub max_image_count: u32,
+}
+
+/// Deterministic swapchain planning input.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainRequest {
+ /// Requested drawable extent.
+ pub drawable_extent: (u32, u32),
+ /// Available surface formats.
+ pub formats: Vec<VulkanSurfaceFormat>,
+ /// Available present modes as raw Vulkan values.
+ pub present_modes: Vec<i32>,
+ /// Surface capabilities.
+ pub capabilities: VulkanSwapchainSurfaceCapabilities,
+ /// Preferred present mode.
+ pub preferred_present_mode: i32,
+}
+
+/// Deterministic swapchain plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainPlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Selected swapchain extent.
+ pub extent: (u32, u32),
+ /// Selected surface format.
+ pub format: VulkanSurfaceFormat,
+ /// Selected present mode raw Vulkan value.
+ pub present_mode: i32,
+ /// Selected image count.
+ pub image_count: u32,
+}
+
+/// Swapchain planning error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainError {
+ /// No surface format was available.
+ MissingSurfaceFormat,
+ /// No present mode was available.
+ MissingPresentMode,
+ /// Requested or current extent is empty.
+ EmptyExtent,
+}
+
+impl std::fmt::Display for VulkanSwapchainError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::MissingSurfaceFormat => write!(f, "Vulkan swapchain has no surface format"),
+ Self::MissingPresentMode => write!(f, "Vulkan swapchain has no present mode"),
+ Self::EmptyExtent => write!(f, "Vulkan swapchain extent must be non-zero"),
+ }
+ }
+}
+
+impl std::error::Error for VulkanSwapchainError {}
+
+/// Swapchain recreation reason.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainRecreationReason {
+ /// Drawable extent changed.
+ Resize,
+ /// Vulkan reported `VK_ERROR_OUT_OF_DATE_KHR`.
+ OutOfDate,
+ /// Vulkan reported `VK_SUBOPTIMAL_KHR`.
+ Suboptimal,
+}
+
+/// Deterministic swapchain recreation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainRecreationReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Recreation reason.
+ pub reason: VulkanSwapchainRecreationReason,
+ /// Previous extent.
+ pub previous_extent: (u32, u32),
+ /// Next extent.
+ pub next_extent: (u32, u32),
+}
+
+/// Deterministic frame submission plan for command buffers and sync objects.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanFrameSubmissionPlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Frames allowed in flight.
+ pub frames_in_flight: u32,
+ /// Swapchain-backed primary command buffers.
+ pub command_buffers: u32,
+ /// Binary semaphores allocated per frame.
+ pub semaphores_per_frame: u32,
+ /// Fences allocated per frame.
+ pub fences_per_frame: u32,
+ /// Draw commands encoded into the frame.
+ pub draw_count: u32,
+ /// Total indexed vertices submitted by draw commands.
+ pub indexed_vertex_count: u32,
+}
+
+/// 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;
+ let mut last_error = None;
+ for device in devices {
+ let report = match validate_device(device) {
+ Ok(report) => report,
+ Err(err) => {
+ last_error = Some(err);
+ continue;
+ }
+ };
+ match &best {
+ Some(existing) if compare_reports(&report, existing) != std::cmp::Ordering::Greater => {
+ }
+ _ => best = Some(report),
+ }
+ }
+ best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))
+}
+
+/// Builds a deterministic swapchain plan from surface capabilities.
+///
+/// # Errors
+///
+/// Returns [`VulkanSwapchainError`] when formats, present modes or extent are
+/// unusable.
+pub fn plan_vulkan_swapchain(
+ request: &VulkanSwapchainRequest,
+) -> Result<VulkanSwapchainPlan, VulkanSwapchainError> {
+ let format = select_surface_format(&request.formats)?;
+ let present_mode = select_present_mode(&request.present_modes, request.preferred_present_mode)?;
+ let extent = select_swapchain_extent(request)?;
+ if extent.0 == 0 || extent.1 == 0 {
+ return Err(VulkanSwapchainError::EmptyExtent);
+ }
+ Ok(VulkanSwapchainPlan {
+ schema: 1,
+ extent,
+ format,
+ present_mode,
+ image_count: select_image_count(request.capabilities),
+ })
+}
+
+fn select_surface_format(
+ formats: &[VulkanSurfaceFormat],
+) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
+ formats
+ .iter()
+ .copied()
+ .find(|format| {
+ format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
+ && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
+ })
+ .or_else(|| formats.first().copied())
+ .ok_or(VulkanSwapchainError::MissingSurfaceFormat)
+}
+
+fn select_present_mode(present_modes: &[i32], preferred: i32) -> Result<i32, VulkanSwapchainError> {
+ if present_modes.contains(&preferred) {
+ Ok(preferred)
+ } else if present_modes.contains(&vk::PresentModeKHR::FIFO.as_raw()) {
+ Ok(vk::PresentModeKHR::FIFO.as_raw())
+ } else {
+ present_modes
+ .first()
+ .copied()
+ .ok_or(VulkanSwapchainError::MissingPresentMode)
+ }
+}
+
+fn select_swapchain_extent(
+ request: &VulkanSwapchainRequest,
+) -> Result<(u32, u32), VulkanSwapchainError> {
+ if let Some(extent) = request.capabilities.current_extent {
+ return if extent.0 == 0 || extent.1 == 0 {
+ Err(VulkanSwapchainError::EmptyExtent)
+ } else {
+ Ok(extent)
+ };
+ }
+ let width = request.drawable_extent.0.clamp(
+ request.capabilities.min_extent.0,
+ request.capabilities.max_extent.0,
+ );
+ let height = request.drawable_extent.1.clamp(
+ request.capabilities.min_extent.1,
+ request.capabilities.max_extent.1,
+ );
+ Ok((width, height))
+}
+
+fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
+ let requested = capabilities.min_image_count.saturating_add(1).max(2);
+ if capabilities.max_image_count == 0 {
+ requested
+ } else {
+ requested.min(capabilities.max_image_count)
+ }
+}
+
+fn select_composite_alpha(supported: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR {
+ if supported.contains(vk::CompositeAlphaFlagsKHR::OPAQUE) {
+ vk::CompositeAlphaFlagsKHR::OPAQUE
+ } else if supported.contains(vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED) {
+ vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED
+ } else if supported.contains(vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED) {
+ vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED
+ } else {
+ vk::CompositeAlphaFlagsKHR::INHERIT
+ }
+}
+
+/// Builds a deterministic swapchain recreation report.
+#[must_use]
+pub const fn swapchain_recreation_report(
+ reason: VulkanSwapchainRecreationReason,
+ previous_extent: (u32, u32),
+ next_extent: (u32, u32),
+) -> VulkanSwapchainRecreationReport {
+ VulkanSwapchainRecreationReport {
+ schema: 1,
+ reason,
+ previous_extent,
+ next_extent,
+ }
+}
+
+/// Builds a deterministic frame submission plan for a validated command list.
+///
+/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer
+/// and synchronization policy can be tested without requiring a native surface.
+///
+/// # Errors
+///
+/// Returns [`RenderError`] when the command list has invalid frame framing,
+/// ordering, draw ranges, mesh bounds, or non-finite transforms.
+pub fn plan_vulkan_frame_submission(
+ swapchain: &VulkanSwapchainPlan,
+ commands: &RenderCommandList,
+) -> Result<VulkanFrameSubmissionPlan, RenderError> {
+ validate_command_list(commands)?;
+ let mut draw_count = 0_u32;
+ let mut indexed_vertex_count = 0_u32;
+ for command in &commands.commands {
+ if let RenderCommand::Draw(draw) = command {
+ draw_count = draw_count.saturating_add(1);
+ indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count);
+ }
+ }
+ Ok(VulkanFrameSubmissionPlan {
+ schema: 1,
+ frames_in_flight: swapchain.image_count.clamp(1, 2),
+ command_buffers: swapchain.image_count,
+ semaphores_per_frame: 2,
+ fences_per_frame: 1,
+ draw_count,
+ indexed_vertex_count,
+ })
+}
+
+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, present_queue_family) = select_queue_families(device)?;
+
+ 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 select_queue_families(
+ device: &VulkanPhysicalDeviceRecord,
+) -> Result<(u32, u32), VulkanCapabilityError> {
+ if let Some(unified) = device
+ .queue_families
+ .iter()
+ .find(|family| family.graphics && family.present)
+ {
+ return Ok((unified.index, unified.index));
+ }
+
+ 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;
+ Ok((graphics_queue_family, present_queue_family))
+}
+
+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
+}
+
+/// Renders a deterministic JSON swapchain plan.
+#[must_use]
+pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&plan.schema.to_string());
+ out.push_str(",\"extent\":[");
+ out.push_str(&plan.extent.0.to_string());
+ out.push(',');
+ out.push_str(&plan.extent.1.to_string());
+ out.push_str("],\"format\":");
+ out.push_str(&plan.format.format.to_string());
+ out.push_str(",\"color_space\":");
+ out.push_str(&plan.format.color_space.to_string());
+ out.push_str(",\"present_mode\":");
+ out.push_str(&plan.present_mode.to_string());
+ out.push_str(",\"image_count\":");
+ out.push_str(&plan.image_count.to_string());
+ out.push('}');
+ out
+}
+
+/// Renders a deterministic JSON swapchain recreation report.
+#[must_use]
+pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&report.schema.to_string());
+ out.push_str(",\"reason\":\"");
+ out.push_str(match report.reason {
+ VulkanSwapchainRecreationReason::Resize => "resize",
+ VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
+ VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
+ });
+ out.push_str("\",\"previous_extent\":[");
+ out.push_str(&report.previous_extent.0.to_string());
+ out.push(',');
+ out.push_str(&report.previous_extent.1.to_string());
+ out.push_str("],\"next_extent\":[");
+ out.push_str(&report.next_extent.0.to_string());
+ out.push(',');
+ out.push_str(&report.next_extent.1.to_string());
+ out.push_str("]}");
+ out
+}
+
+/// Renders a deterministic JSON frame submission plan.
+#[must_use]
+pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&plan.schema.to_string());
+ out.push_str(",\"frames_in_flight\":");
+ out.push_str(&plan.frames_in_flight.to_string());
+ out.push_str(",\"command_buffers\":");
+ out.push_str(&plan.command_buffers.to_string());
+ out.push_str(",\"semaphores_per_frame\":");
+ out.push_str(&plan.semaphores_per_frame.to_string());
+ out.push_str(",\"fences_per_frame\":");
+ out.push_str(&plan.fences_per_frame.to_string());
+ out.push_str(",\"draw_count\":");
+ out.push_str(&plan.draw_count.to_string());
+ out.push_str(",\"indexed_vertex_count\":");
+ out.push_str(&plan.indexed_vertex_count.to_string());
+ out.push('}');
+ 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 planning backend setup and frame progression.
+#[derive(Clone, Debug, PartialEq)]
+pub struct VulkanPlanningBackendReport {
+ /// Total frames executed.
+ pub frames_executed: u64,
+ /// Total command submissions.
+ pub submissions: u64,
+ /// Last command-capture byte size.
+ pub last_capture_size: usize,
+ /// Number of simulated present calls issued by the planning facade.
+ pub simulated_presents: u64,
+ /// Number of resize-driven surface plan refreshes.
+ pub resize_rebuilds: u64,
+ /// Last render request observed.
+ pub request: RenderRequest,
+ /// Last deterministic frame submission plan.
+ pub last_frame_submission: Option<VulkanFrameSubmissionPlan>,
+}
+
+impl Default for VulkanPlanningBackendReport {
+ fn default() -> Self {
+ Self {
+ frames_executed: 0,
+ submissions: 0,
+ last_capture_size: 0,
+ simulated_presents: 0,
+ resize_rebuilds: 0,
+ request: RenderRequest::conservative(),
+ last_frame_submission: None,
+ }
+ }
+}
+
+/// Vulkan planning backend façade used by the game entrypoint.
+#[derive(Debug)]
+pub struct VulkanPlanningBackend {
+ state: VulkanPlanningBackendState,
+ report: VulkanPlanningBackendReport,
+ swapchain_plan: VulkanSwapchainPlan,
+}
+
+impl Default for VulkanPlanningBackend {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl VulkanPlanningBackend {
+ /// Creates a new Vulkan planning backend façade.
+ #[must_use]
+ pub fn new() -> Self {
+ Self {
+ state: VulkanPlanningBackendState::Ready,
+ report: VulkanPlanningBackendReport::default(),
+ swapchain_plan: default_stage0_swapchain_plan(),
+ }
+ }
+
+ /// Replaces active surface/profile request.
+ pub fn set_render_request(&mut self, request: RenderRequest) {
+ self.report.request = request;
+ self.report.resize_rebuilds = self.report.resize_rebuilds.saturating_add(1);
+ }
+
+ /// Returns active render request policy.
+ #[must_use]
+ pub const fn render_request(&self) -> RenderRequest {
+ self.report.request
+ }
+
+ /// Replaces active swapchain plan used for frame submission planning.
+ pub fn set_swapchain_plan(&mut self, plan: VulkanSwapchainPlan) {
+ self.swapchain_plan = plan;
+ }
+
+ /// Returns active swapchain plan.
+ #[must_use]
+ pub const fn swapchain_plan(&self) -> &VulkanSwapchainPlan {
+ &self.swapchain_plan
+ }
+
+ /// Returns adapter state.
+ #[must_use]
+ pub const fn state(&self) -> VulkanPlanningBackendState {
+ self.state
+ }
+
+ /// Returns backend report.
+ #[must_use]
+ pub fn report(&self) -> &VulkanPlanningBackendReport {
+ &self.report
+ }
+
+ fn simulate_present(&mut self) {
+ self.report.simulated_presents = self.report.simulated_presents.saturating_add(1);
+ }
+}
+
+impl RenderBackend for VulkanPlanningBackend {
+ fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
+ if !matches!(
+ self.state,
+ VulkanPlanningBackendState::Ready | VulkanPlanningBackendState::Degraded
+ ) {
+ return Err(RenderError::InvalidRange);
+ }
+ let capture = canonical_capture(commands)?;
+ let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?;
+ self.report.frames_executed = self.report.frames_executed.saturating_add(1);
+ self.report.submissions = self.report.submissions.saturating_add(1);
+ self.report.last_capture_size = capture.len();
+ self.report.last_frame_submission = Some(frame_plan);
+ self.simulate_present();
+ Ok(FrameOutput)
+ }
+}
+
+fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan {
+ VulkanSwapchainPlan {
+ schema: 1,
+ extent: (1, 1),
+ format: VulkanSurfaceFormat {
+ format: vk::Format::B8G8R8A8_SRGB.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ },
+ present_mode: vk::PresentModeKHR::FIFO.as_raw(),
+ image_count: 2,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use fparkan_render::{
+ DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
+ };
+
+ #[test]
+ fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> {
+ let mut backend = VulkanPlanningBackend::new();
+ let request = RenderRequest::conservative();
+ backend.set_render_request(request);
+ assert_eq!(backend.render_request(), request);
+ assert_eq!(backend.report().resize_rebuilds, 1);
+
+ let commands = fparkan_render::RenderCommandList {
+ commands: vec![
+ RenderCommand::BeginFrame,
+ RenderCommand::Draw(DrawCommand {
+ id: DrawId(11),
+ phase: RenderPhase::Opaque,
+ object_id: None,
+ mesh: GpuMeshId(1),
+ material: GpuMaterialId(2),
+ transform: [1.0; 16],
+ range: IndexRange { start: 0, count: 3 },
+ stable_order: 7,
+ }),
+ RenderCommand::EndFrame,
+ ],
+ };
+
+ backend.execute(&commands)?;
+ assert_eq!(backend.state(), VulkanPlanningBackendState::Ready);
+ assert_eq!(backend.report().frames_executed, 1);
+ assert_eq!(backend.report().submissions, 1);
+ assert_eq!(backend.report().simulated_presents, 1);
+ assert!(backend.report().last_capture_size > 0);
+ assert_eq!(
+ backend.report().last_frame_submission,
+ Some(VulkanFrameSubmissionPlan {
+ schema: 1,
+ frames_in_flight: 2,
+ command_buffers: 2,
+ semaphores_per_frame: 2,
+ fences_per_frame: 1,
+ draw_count: 1,
+ indexed_vertex_count: 3,
+ })
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> {
+ let commands = fparkan_render::RenderCommandList {
+ commands: vec![
+ RenderCommand::BeginFrame,
+ RenderCommand::Draw(DrawCommand {
+ id: DrawId(11),
+ phase: RenderPhase::Opaque,
+ object_id: None,
+ mesh: GpuMeshId(1),
+ material: GpuMaterialId(2),
+ transform: [1.0; 16],
+ range: IndexRange { start: 0, count: 3 },
+ stable_order: 7,
+ }),
+ RenderCommand::EndFrame,
+ ],
+ };
+ let swapchain = VulkanSwapchainPlan {
+ image_count: 3,
+ ..default_stage0_swapchain_plan()
+ };
+
+ let plan = plan_vulkan_frame_submission(&swapchain, &commands)?;
+
+ assert_eq!(plan.frames_in_flight, 2);
+ assert_eq!(plan.command_buffers, 3);
+ assert_eq!(plan.draw_count, 1);
+ assert_eq!(plan.indexed_vertex_count, 3);
+ assert_eq!(
+ render_frame_submission_plan_json(&plan),
+ "{\"schema\":1,\"frames_in_flight\":2,\"command_buffers\":3,\"semaphores_per_frame\":2,\"fences_per_frame\":1,\"draw_count\":1,\"indexed_vertex_count\":3}"
+ );
+ 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\"]}"
+ );
+ }
+
+ #[test]
+ fn loader_probe_report_json_is_stable() {
+ assert_eq!(
+ vulkan_entry_symbol_name().to_bytes(),
+ b"vkGetInstanceProcAddr"
+ );
+ assert_eq!(
+ render_loader_probe_report_json(&VulkanLoaderProbeReport {
+ schema: 1,
+ loader_available: true,
+ instance_api_version: vk::API_VERSION_1_2,
+ }),
+ "{\"schema\":1,\"loader_available\":true,\"instance_api\":\"1.2.0\"}"
+ );
+ }
+
+ #[test]
+ fn loader_error_display_is_actionable() {
+ assert_eq!(
+ VulkanLoaderError::Unavailable {
+ message: "dlopen failed".to_string(),
+ }
+ .to_string(),
+ "Vulkan loader is unavailable: dlopen failed"
+ );
+ }
+
+ #[test]
+ fn instance_plan_is_sorted_deduplicated_and_portability_aware() {
+ let plan = plan_vulkan_instance(&VulkanInstanceConfig {
+ application_name: "FParkan".to_string(),
+ required_extensions: vec![
+ "VK_KHR_surface".to_string(),
+ KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
+ "VK_KHR_surface".to_string(),
+ ],
+ enable_portability_enumeration: true,
+ enable_validation: true,
+ });
+
+ assert_eq!(
+ render_instance_plan_json(&plan),
+ "{\"schema\":1,\"create_flags\":1,\"validation_requested\":true,\"enabled_extensions\":[\"VK_EXT_debug_utils\",\"VK_KHR_portability_enumeration\",\"VK_KHR_surface\"]}"
+ );
+ }
+
+ #[test]
+ fn instance_plan_adds_portability_extension_when_requested() {
+ let plan = plan_vulkan_instance(&VulkanInstanceConfig {
+ application_name: "FParkan".to_string(),
+ required_extensions: vec!["VK_KHR_surface".to_string()],
+ enable_portability_enumeration: true,
+ enable_validation: false,
+ });
+
+ assert_eq!(
+ plan.enabled_extensions,
+ vec![
+ KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
+ "VK_KHR_surface".to_string()
+ ]
+ );
+ assert_eq!(plan.create_flags, 1);
+ }
+
+ #[test]
+ fn invalid_instance_extension_name_is_reported_before_loader_use() {
+ assert_eq!(
+ cstring_vec(&["bad\0extension".to_string()]),
+ Err(VulkanInstanceError::InvalidExtensionName {
+ extension: "bad\0extension".to_string()
+ })
+ );
+ }
+
+ #[test]
+ fn surface_plan_requires_native_handles() {
+ assert_eq!(
+ plan_vulkan_surface(None),
+ Err(VulkanSurfaceError::MissingNativeHandles)
+ );
+ assert_eq!(
+ VulkanSurfaceError::MissingNativeHandles.to_string(),
+ "native window/display handles are required for Vulkan surface creation"
+ );
+ }
+
+ #[test]
+ fn surface_plan_json_is_stable() {
+ assert_eq!(
+ render_surface_plan_json(&VulkanSurfacePlan {
+ schema: 1,
+ required_instance_extensions: vec![
+ "VK_KHR_surface".to_string(),
+ "VK_EXT_metal_surface".to_string(),
+ ],
+ }),
+ "{\"schema\":1,\"required_instance_extensions\":[\"VK_KHR_surface\",\"VK_EXT_metal_surface\"]}"
+ );
+ }
+
+ #[test]
+ fn static_surface_extension_name_is_decoded() {
+ let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
+
+ assert_eq!(name, "VK_KHR_surface");
+ }
+
+ #[test]
+ fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() {
+ let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
+
+ assert_eq!(
+ plan.format,
+ VulkanSurfaceFormat {
+ format: vk::Format::B8G8R8A8_SRGB.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ }
+ );
+ assert_eq!(plan.present_mode, vk::PresentModeKHR::MAILBOX.as_raw());
+ assert_eq!(plan.extent, (1024, 720));
+ assert_eq!(plan.image_count, 3);
+ }
+
+ #[test]
+ fn swapchain_plan_uses_fifo_and_current_extent_fallbacks() {
+ let mut request = swapchain_request();
+ request.preferred_present_mode = vk::PresentModeKHR::IMMEDIATE.as_raw();
+ request.present_modes = vec![vk::PresentModeKHR::FIFO.as_raw()];
+ request.capabilities.current_extent = Some((800, 600));
+
+ let plan = plan_vulkan_swapchain(&request).expect("swapchain plan");
+
+ assert_eq!(plan.present_mode, vk::PresentModeKHR::FIFO.as_raw());
+ assert_eq!(plan.extent, (800, 600));
+ }
+
+ #[test]
+ fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
+ let mut request = swapchain_request();
+ request.formats.clear();
+ assert_eq!(
+ plan_vulkan_swapchain(&request),
+ Err(VulkanSwapchainError::MissingSurfaceFormat)
+ );
+
+ let mut request = swapchain_request();
+ request.present_modes.clear();
+ assert_eq!(
+ plan_vulkan_swapchain(&request),
+ Err(VulkanSwapchainError::MissingPresentMode)
+ );
+
+ let mut request = swapchain_request();
+ request.capabilities.current_extent = Some((0, 600));
+ assert_eq!(
+ plan_vulkan_swapchain(&request),
+ Err(VulkanSwapchainError::EmptyExtent)
+ );
+ }
+
+ #[test]
+ fn swapchain_plan_json_and_recreation_reports_are_stable() {
+ let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
+ assert_eq!(
+ render_swapchain_plan_json(&plan),
+ "{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3}"
+ );
+
+ let report = swapchain_recreation_report(
+ VulkanSwapchainRecreationReason::OutOfDate,
+ (1024, 720),
+ (1280, 720),
+ );
+ assert_eq!(
+ render_swapchain_recreation_report_json(&report),
+ "{\"schema\":1,\"reason\":\"out_of_date\",\"previous_extent\":[1024,720],\"next_extent\":[1280,720]}"
+ );
+ }
+
+ #[test]
+ fn triangle_shader_manifest_hashes_are_stable() {
+ let report =
+ validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
+
+ assert_eq!(report.schema, SHADER_MANIFEST_SCHEMA);
+ assert_eq!(report.target_env, SHADER_TARGET_ENV);
+ assert_eq!(
+ report.compiler,
+ VulkanShaderToolManifest {
+ name: SHADER_COMPILER_NAME,
+ version: SHADER_COMPILER_VERSION,
+ binary_sha256: SHADER_COMPILER_BINARY_SHA256,
+ }
+ );
+ assert_eq!(
+ report.validator,
+ VulkanShaderToolManifest {
+ name: SPIRV_VALIDATOR_NAME,
+ version: SPIRV_VALIDATOR_VERSION,
+ binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
+ }
+ );
+ assert_eq!(report.modules.len(), 2);
+ assert_eq!(report.modules[0].name, "triangle.vert");
+ assert_eq!(report.modules[0].stage, VulkanShaderStage::Vertex);
+ assert_eq!(report.modules[0].source_path, TRIANGLE_VERTEX_SOURCE_PATH);
+ assert_eq!(
+ report.modules[0].source_sha256,
+ TRIANGLE_VERTEX_SOURCE_SHA256
+ );
+ assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH);
+ assert_eq!(report.modules[0].word_count, 253);
+ assert_eq!(
+ report.modules[0].sha256,
+ "9023b1cc856c98ecd21755596c4e9d1e62cc63e1787f8c43ada2101544e8d0d1"
+ );
+ assert_eq!(report.modules[0].descriptor_sets, 0);
+ assert_eq!(report.modules[0].push_constant_bytes, 0);
+ assert_eq!(
+ report.modules[0].compile_command,
+ TRIANGLE_VERTEX_COMPILE_COMMAND
+ );
+ assert_eq!(
+ report.modules[0].validate_command,
+ TRIANGLE_VERTEX_VALIDATE_COMMAND
+ );
+ assert!(!report.modules[0].interface_hash.is_empty());
+ assert_eq!(
+ report.modules[1].sha256,
+ "6efe2c9716ae845c471ecbaac2c83e56a17a37dc017dd63f0a05f0d9161f44ba"
+ );
+ assert_eq!(
+ report.manifest_hash,
+ "725529e9449fa53017e7df75f3f14c76d53479a5a7617d55ec78280b3059bc44"
+ );
+ }
+
+ #[test]
+ fn shader_manifest_report_json_is_stable() {
+ let report =
+ validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
+ let json = render_shader_manifest_report_json(&report);
+
+ assert!(json.contains(SHADER_COMPILER_NAME));
+ assert!(json.contains(SPIRV_VALIDATOR_NAME));
+ assert!(json.contains(TRIANGLE_VERTEX_SOURCE_PATH));
+ assert!(json.contains(TRIANGLE_VERTEX_COMPILE_COMMAND));
+ }
+
+ #[test]
+ fn checked_in_shader_manifest_matches_generated_report() {
+ let report =
+ validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
+ assert_eq!(
+ render_shader_manifest_report_json(&report),
+ include_str!("../shaders/manifest.json").trim()
+ );
+ }
+
+ #[test]
+ fn shader_manifest_rejects_invalid_spirv_containers() {
+ let mut module = triangle_shader_manifest().remove(0);
+ module.words = &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0];
+ assert_eq!(
+ validate_shader_manifest(&[module]),
+ Err(VulkanShaderManifestError::InvalidMagic {
+ name: "triangle.vert",
+ found: 0xFFFF_FFFF,
+ })
+ );
+
+ let mut module = triangle_shader_manifest().remove(0);
+ module.words = &[SPIRV_MAGIC, 0, 0, 1, 0];
+ assert_eq!(
+ validate_shader_manifest(&[module]),
+ Err(VulkanShaderManifestError::UnsupportedVersion {
+ name: "triangle.vert",
+ found: 0,
+ })
+ );
+
+ let mut module = triangle_shader_manifest().remove(0);
+ module.words = &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0];
+ assert_eq!(
+ validate_shader_manifest(&[module]),
+ Err(VulkanShaderManifestError::InvalidBound {
+ name: "triangle.vert",
+ })
+ );
+ }
+
+ 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(),
+ }],
+ }
+ }
+
+ fn swapchain_request() -> VulkanSwapchainRequest {
+ VulkanSwapchainRequest {
+ drawable_extent: (1280, 720),
+ formats: vec![
+ VulkanSurfaceFormat {
+ format: vk::Format::R8G8B8A8_UNORM.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ },
+ VulkanSurfaceFormat {
+ format: vk::Format::B8G8R8A8_SRGB.as_raw(),
+ color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
+ },
+ ],
+ present_modes: vec![
+ vk::PresentModeKHR::FIFO.as_raw(),
+ vk::PresentModeKHR::MAILBOX.as_raw(),
+ ],
+ capabilities: VulkanSwapchainSurfaceCapabilities {
+ current_extent: None,
+ min_extent: (320, 240),
+ max_extent: (1024, 768),
+ min_image_count: 2,
+ max_image_count: 3,
+ },
+ preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
+ }
+ }
+}
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index 097c5a9..dc09c0d 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -1,4966 +1,6 @@
-#![allow(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::cast_possible_truncation,
- clippy::cast_possible_wrap,
- clippy::cast_precision_loss,
- clippy::expect_used,
- clippy::float_cmp,
- clippy::identity_op,
- clippy::too_many_lines,
- clippy::uninlined_format_args,
- clippy::map_unwrap_or,
- clippy::needless_raw_string_hashes,
- clippy::semicolon_if_nothing_returned,
- clippy::type_complexity,
- clippy::panic,
- clippy::unwrap_used
- )
-)]
-#![deny(unsafe_op_in_unsafe_fn)]
-//! Vulkan adapter facade and migration-ready backend surface contract.
-//!
-//! This module intentionally keeps backend-agnostic command validation in the
-//! shared render crate while exposing deterministic lifecycle telemetry used by
-//! Stage 0 acceptance evidence.
-//!
-//! This crate is the declared low-level Vulkan boundary.
+#![deny(unsafe_code)]
+//! Vulkan adapter public surface.
-use ash::{
- khr::{surface, swapchain},
- vk,
-};
-use fparkan_binary::{sha256, sha256_hex};
-use fparkan_platform::{NativeWindowHandles, RenderRequest};
-use fparkan_render::{
- canonical_capture, validate_command_list, FrameOutput, RenderBackend, RenderCommand,
- RenderCommandList, RenderError,
-};
-use std::collections::BTreeSet;
-use std::ffi::{CStr, CString};
-use std::os::raw::c_char;
-use std::sync::atomic::{AtomicU32, Ordering};
-use std::sync::Mutex;
-/// 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";
-const KHR_PORTABILITY_ENUMERATION_EXTENSION: &str = "VK_KHR_portability_enumeration";
-const EXT_DEBUG_UTILS_EXTENSION: &str = "VK_EXT_debug_utils";
-const VALIDATION_LAYER_NAME: &str = "VK_LAYER_KHRONOS_validation";
-const SPIRV_MAGIC: u32 = 0x0723_0203;
-const SPIRV_VERSION_1_0: u32 = 0x0001_0000;
-const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[
- SPIRV_MAGIC,
- SPIRV_VERSION_1_0,
- 0x0008_000b,
- 0x0000_0021,
- 0x0000_0000,
- 0x0002_0011,
- 0x0000_0001,
- 0x0006_000b,
- 0x0000_0001,
- 0x4c53_4c47,
- 0x6474_732e,
- 0x3035_342e,
- 0x0000_0000,
- 0x0003_000e,
- 0x0000_0000,
- 0x0000_0001,
- 0x0009_000f,
- 0x0000_0000,
- 0x0000_0004,
- 0x6e69_616d,
- 0x0000_0000,
- 0x0000_0009,
- 0x0000_000b,
- 0x0000_0013,
- 0x0000_0018,
- 0x0003_0003,
- 0x0000_0002,
- 0x0000_01c2,
- 0x0004_0005,
- 0x0000_0004,
- 0x6e69_616d,
- 0x0000_0000,
- 0x0005_0005,
- 0x0000_0009,
- 0x5f74_756f,
- 0x6f6c_6f63,
- 0x0000_0072,
- 0x0005_0005,
- 0x0000_000b,
- 0x635f_6e69,
- 0x726f_6c6f,
- 0x0000_0000,
- 0x0006_0005,
- 0x0000_0011,
- 0x505f_6c67,
- 0x6556_7265,
- 0x7865_7472,
- 0x0000_0000,
- 0x0006_0006,
- 0x0000_0011,
- 0x0000_0000,
- 0x505f_6c67,
- 0x7469_736f,
- 0x006e_6f69,
- 0x0007_0006,
- 0x0000_0011,
- 0x0000_0001,
- 0x505f_6c67,
- 0x746e_696f,
- 0x657a_6953,
- 0x0000_0000,
- 0x0007_0006,
- 0x0000_0011,
- 0x0000_0002,
- 0x435f_6c67,
- 0x4470_696c,
- 0x6174_7369,
- 0x0065_636e,
- 0x0007_0006,
- 0x0000_0011,
- 0x0000_0003,
- 0x435f_6c67,
- 0x446c_6c75,
- 0x6174_7369,
- 0x0065_636e,
- 0x0003_0005,
- 0x0000_0013,
- 0x0000_0000,
- 0x0005_0005,
- 0x0000_0018,
- 0x705f_6e69,
- 0x7469_736f,
- 0x006e_6f69,
- 0x0004_0047,
- 0x0000_0009,
- 0x0000_001e,
- 0x0000_0000,
- 0x0004_0047,
- 0x0000_000b,
- 0x0000_001e,
- 0x0000_0001,
- 0x0003_0047,
- 0x0000_0011,
- 0x0000_0002,
- 0x0005_0048,
- 0x0000_0011,
- 0x0000_0000,
- 0x0000_000b,
- 0x0000_0000,
- 0x0005_0048,
- 0x0000_0011,
- 0x0000_0001,
- 0x0000_000b,
- 0x0000_0001,
- 0x0005_0048,
- 0x0000_0011,
- 0x0000_0002,
- 0x0000_000b,
- 0x0000_0003,
- 0x0005_0048,
- 0x0000_0011,
- 0x0000_0003,
- 0x0000_000b,
- 0x0000_0004,
- 0x0004_0047,
- 0x0000_0018,
- 0x0000_001e,
- 0x0000_0000,
- 0x0002_0013,
- 0x0000_0002,
- 0x0003_0021,
- 0x0000_0003,
- 0x0000_0002,
- 0x0003_0016,
- 0x0000_0006,
- 0x0000_0020,
- 0x0004_0017,
- 0x0000_0007,
- 0x0000_0006,
- 0x0000_0003,
- 0x0004_0020,
- 0x0000_0008,
- 0x0000_0003,
- 0x0000_0007,
- 0x0004_003b,
- 0x0000_0008,
- 0x0000_0009,
- 0x0000_0003,
- 0x0004_0020,
- 0x0000_000a,
- 0x0000_0001,
- 0x0000_0007,
- 0x0004_003b,
- 0x0000_000a,
- 0x0000_000b,
- 0x0000_0001,
- 0x0004_0017,
- 0x0000_000d,
- 0x0000_0006,
- 0x0000_0004,
- 0x0004_0015,
- 0x0000_000e,
- 0x0000_0020,
- 0x0000_0000,
- 0x0004_002b,
- 0x0000_000e,
- 0x0000_000f,
- 0x0000_0001,
- 0x0004_001c,
- 0x0000_0010,
- 0x0000_0006,
- 0x0000_000f,
- 0x0006_001e,
- 0x0000_0011,
- 0x0000_000d,
- 0x0000_0006,
- 0x0000_0010,
- 0x0000_0010,
- 0x0004_0020,
- 0x0000_0012,
- 0x0000_0003,
- 0x0000_0011,
- 0x0004_003b,
- 0x0000_0012,
- 0x0000_0013,
- 0x0000_0003,
- 0x0004_0015,
- 0x0000_0014,
- 0x0000_0020,
- 0x0000_0001,
- 0x0004_002b,
- 0x0000_0014,
- 0x0000_0015,
- 0x0000_0000,
- 0x0004_0017,
- 0x0000_0016,
- 0x0000_0006,
- 0x0000_0002,
- 0x0004_0020,
- 0x0000_0017,
- 0x0000_0001,
- 0x0000_0016,
- 0x0004_003b,
- 0x0000_0017,
- 0x0000_0018,
- 0x0000_0001,
- 0x0004_002b,
- 0x0000_0006,
- 0x0000_001a,
- 0x0000_0000,
- 0x0004_002b,
- 0x0000_0006,
- 0x0000_001b,
- 0x3f80_0000,
- 0x0004_0020,
- 0x0000_001f,
- 0x0000_0003,
- 0x0000_000d,
- 0x0005_0036,
- 0x0000_0002,
- 0x0000_0004,
- 0x0000_0000,
- 0x0000_0003,
- 0x0002_00f8,
- 0x0000_0005,
- 0x0004_003d,
- 0x0000_0007,
- 0x0000_000c,
- 0x0000_000b,
- 0x0003_003e,
- 0x0000_0009,
- 0x0000_000c,
- 0x0004_003d,
- 0x0000_0016,
- 0x0000_0019,
- 0x0000_0018,
- 0x0005_0051,
- 0x0000_0006,
- 0x0000_001c,
- 0x0000_0019,
- 0x0000_0000,
- 0x0005_0051,
- 0x0000_0006,
- 0x0000_001d,
- 0x0000_0019,
- 0x0000_0001,
- 0x0007_0050,
- 0x0000_000d,
- 0x0000_001e,
- 0x0000_001c,
- 0x0000_001d,
- 0x0000_001a,
- 0x0000_001b,
- 0x0005_0041,
- 0x0000_001f,
- 0x0000_0020,
- 0x0000_0013,
- 0x0000_0015,
- 0x0003_003e,
- 0x0000_0020,
- 0x0000_001e,
- 0x0001_00fd,
- 0x0001_0038,
-];
-const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
- SPIRV_MAGIC,
- SPIRV_VERSION_1_0,
- 0x0008_000b,
- 0x0000_0013,
- 0x0000_0000,
- 0x0002_0011,
- 0x0000_0001,
- 0x0006_000b,
- 0x0000_0001,
- 0x4c53_4c47,
- 0x6474_732e,
- 0x3035_342e,
- 0x0000_0000,
- 0x0003_000e,
- 0x0000_0000,
- 0x0000_0001,
- 0x0007_000f,
- 0x0000_0004,
- 0x0000_0004,
- 0x6e69_616d,
- 0x0000_0000,
- 0x0000_0009,
- 0x0000_000c,
- 0x0003_0010,
- 0x0000_0004,
- 0x0000_0007,
- 0x0003_0003,
- 0x0000_0002,
- 0x0000_01c2,
- 0x0004_0005,
- 0x0000_0004,
- 0x6e69_616d,
- 0x0000_0000,
- 0x0005_0005,
- 0x0000_0009,
- 0x5f74_756f,
- 0x6f6c_6f63,
- 0x0000_0072,
- 0x0005_0005,
- 0x0000_000c,
- 0x635f_6e69,
- 0x726f_6c6f,
- 0x0000_0000,
- 0x0004_0047,
- 0x0000_0009,
- 0x0000_001e,
- 0x0000_0000,
- 0x0004_0047,
- 0x0000_000c,
- 0x0000_001e,
- 0x0000_0000,
- 0x0002_0013,
- 0x0000_0002,
- 0x0003_0021,
- 0x0000_0003,
- 0x0000_0002,
- 0x0003_0016,
- 0x0000_0006,
- 0x0000_0020,
- 0x0004_0017,
- 0x0000_0007,
- 0x0000_0006,
- 0x0000_0004,
- 0x0004_0020,
- 0x0000_0008,
- 0x0000_0003,
- 0x0000_0007,
- 0x0004_003b,
- 0x0000_0008,
- 0x0000_0009,
- 0x0000_0003,
- 0x0004_0017,
- 0x0000_000a,
- 0x0000_0006,
- 0x0000_0003,
- 0x0004_0020,
- 0x0000_000b,
- 0x0000_0001,
- 0x0000_000a,
- 0x0004_003b,
- 0x0000_000b,
- 0x0000_000c,
- 0x0000_0001,
- 0x0004_002b,
- 0x0000_0006,
- 0x0000_000e,
- 0x3f80_0000,
- 0x0005_0036,
- 0x0000_0002,
- 0x0000_0004,
- 0x0000_0000,
- 0x0000_0003,
- 0x0002_00f8,
- 0x0000_0005,
- 0x0004_003d,
- 0x0000_000a,
- 0x0000_000d,
- 0x0000_000c,
- 0x0005_0051,
- 0x0000_0006,
- 0x0000_000f,
- 0x0000_000d,
- 0x0000_0000,
- 0x0005_0051,
- 0x0000_0006,
- 0x0000_0010,
- 0x0000_000d,
- 0x0000_0001,
- 0x0005_0051,
- 0x0000_0006,
- 0x0000_0011,
- 0x0000_000d,
- 0x0000_0002,
- 0x0007_0050,
- 0x0000_0007,
- 0x0000_0012,
- 0x0000_000f,
- 0x0000_0010,
- 0x0000_0011,
- 0x0000_000e,
- 0x0003_003e,
- 0x0000_0009,
- 0x0000_0012,
- 0x0001_00fd,
- 0x0001_0038,
-];
+mod ffi;
-const SHADER_MANIFEST_SCHEMA: u32 = 2;
-const SHADER_TARGET_ENV: &str = "vulkan1.0";
-const SHADER_COMPILER_NAME: &str = "glslangValidator";
-const SHADER_COMPILER_VERSION: &str = "11:16.3.0";
-const SHADER_COMPILER_BINARY_SHA256: &str =
- "9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f";
-const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
-const SPIRV_VALIDATOR_VERSION: &str = "SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00";
-const SPIRV_VALIDATOR_BINARY_SHA256: &str =
- "f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f";
-const TRIANGLE_VERTEX_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert";
-const TRIANGLE_VERTEX_SOURCE_SHA256: &str =
- "1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341";
-const TRIANGLE_VERTEX_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
-const TRIANGLE_VERTEX_COMPILE_COMMAND: &str = "glslangValidator -V -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
-const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str =
- "spirv-val --target-env vulkan1.0 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
-const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag";
-const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str =
- "f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445";
-const TRIANGLE_FRAGMENT_SPIRV_PATH: &str =
- "adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
-const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str = "glslangValidator -V -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
-const TRIANGLE_FRAGMENT_VALIDATE_COMMAND: &str =
- "spirv-val --target-env vulkan1.0 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
-
-/// Shader tool metadata pinned in the Stage 0 manifest.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanShaderToolManifest {
- /// Tool executable name.
- pub name: &'static str,
- /// Tool version string.
- pub version: &'static str,
- /// Tool binary SHA-256.
- pub binary_sha256: &'static str,
-}
-
-/// Vulkan shader stage.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum VulkanShaderStage {
- /// Vertex stage.
- Vertex,
- /// Fragment stage.
- Fragment,
-}
-
-impl VulkanShaderStage {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Vertex => "vertex",
- Self::Fragment => "fragment",
- }
- }
-}
-
-/// Offline SPIR-V shader manifest entry.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanShaderModuleManifest {
- /// Logical shader name.
- pub name: &'static str,
- /// Shader stage.
- pub stage: VulkanShaderStage,
- /// SPIR-V entry point.
- pub entry_point: &'static str,
- /// Descriptor set count.
- pub descriptor_sets: u32,
- /// Push constant byte count.
- pub push_constant_bytes: u32,
- /// Checked-in GLSL source path.
- pub source_path: &'static str,
- /// Checked-in GLSL source SHA-256.
- pub source_sha256: &'static str,
- /// Checked-in SPIR-V module path.
- pub spirv_path: &'static str,
- /// Exact offline compile command used for the checked-in SPIR-V artifact.
- pub compile_command: &'static str,
- /// Exact offline validation command used for the checked-in SPIR-V artifact.
- pub validate_command: &'static str,
- /// SPIR-V words.
- pub words: &'static [u32],
-}
-
-/// Shader manifest validation report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanShaderManifestReport {
- /// Report schema version.
- pub schema: u32,
- /// Explicit Vulkan target environment for the checked-in SPIR-V.
- pub target_env: &'static str,
- /// Pinned compiler metadata.
- pub compiler: VulkanShaderToolManifest,
- /// Pinned validator metadata.
- pub validator: VulkanShaderToolManifest,
- /// Shader module reports.
- pub modules: Vec<VulkanShaderModuleReport>,
- /// Hash of the normalized shader manifest.
- pub manifest_hash: String,
-}
-
-/// Shader module validation report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanShaderModuleReport {
- /// Logical shader name.
- pub name: &'static str,
- /// Shader stage.
- pub stage: VulkanShaderStage,
- /// SPIR-V entry point.
- pub entry_point: &'static str,
- /// Checked-in GLSL source path.
- pub source_path: &'static str,
- /// Checked-in GLSL source SHA-256.
- pub source_sha256: &'static str,
- /// Checked-in SPIR-V module path.
- pub spirv_path: &'static str,
- /// SPIR-V word count.
- pub word_count: usize,
- /// SPIR-V byte hash.
- pub sha256: String,
- /// Descriptor set count.
- pub descriptor_sets: u32,
- /// Push constant byte count.
- pub push_constant_bytes: u32,
- /// Exact offline compile command used for the checked-in SPIR-V artifact.
- pub compile_command: &'static str,
- /// Exact offline validation command used for the checked-in SPIR-V artifact.
- pub validate_command: &'static str,
- /// Stable hash of the reflected interface contract for this module.
- pub interface_hash: String,
-}
-
-/// Shader manifest validation error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanShaderManifestError {
- /// SPIR-V module is too short to contain a header.
- TooShort {
- /// Shader name.
- name: &'static str,
- },
- /// SPIR-V module has an invalid magic word.
- InvalidMagic {
- /// Shader name.
- name: &'static str,
- /// Found magic word.
- found: u32,
- },
- /// SPIR-V module version is below 1.0.
- UnsupportedVersion {
- /// Shader name.
- name: &'static str,
- /// Found version word.
- found: u32,
- },
- /// SPIR-V module declares an invalid bound.
- InvalidBound {
- /// Shader name.
- name: &'static str,
- },
-}
-
-impl std::fmt::Display for VulkanShaderManifestError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::TooShort { name } => write!(f, "shader {name} SPIR-V module is too short"),
- Self::InvalidMagic { name, found } => {
- write!(f, "shader {name} has invalid SPIR-V magic 0x{found:08x}")
- }
- Self::UnsupportedVersion { name, found } => write!(
- f,
- "shader {name} has unsupported SPIR-V version 0x{found:08x}"
- ),
- Self::InvalidBound { name } => write!(f, "shader {name} has invalid SPIR-V bound"),
- }
- }
-}
-
-impl std::error::Error for VulkanShaderManifestError {}
-
-/// Vulkan instance bootstrap configuration.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanInstanceConfig {
- /// Application name reported to the loader.
- pub application_name: String,
- /// Required instance extensions, usually including surface extensions.
- pub required_extensions: Vec<String>,
- /// Whether `VK_KHR_portability_enumeration` and its create flag are enabled.
- pub enable_portability_enumeration: bool,
- /// Whether validation layers are requested.
- pub enable_validation: bool,
-}
-
-impl VulkanInstanceConfig {
- /// Returns a conservative instance configuration for smoke probes.
- #[must_use]
- pub fn smoke(application_name: impl Into<String>) -> Self {
- Self {
- application_name: application_name.into(),
- required_extensions: Vec::new(),
- enable_portability_enumeration: cfg!(target_os = "macos"),
- enable_validation: false,
- }
- }
-}
-
-/// Deterministic Vulkan instance creation plan.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanInstancePlan {
- /// Report schema version.
- pub schema: u32,
- /// Instance extensions requested at creation time.
- pub enabled_extensions: Vec<String>,
- /// Raw Vulkan instance creation flags.
- pub create_flags: u32,
- /// Whether validation was requested.
- pub validation_requested: bool,
-}
-
-/// Created Vulkan instance probe.
-pub struct VulkanInstanceProbe {
- entry: ash::Entry,
- instance: ash::Instance,
- /// Deterministic instance creation report.
- pub report: VulkanInstancePlan,
-}
-
-impl Drop for VulkanInstanceProbe {
- fn drop(&mut self) {
- // SAFETY: The `Instance` was created by this probe and is destroyed once during drop.
- unsafe { self.instance.destroy_instance(None) };
- }
-}
-
-/// Deterministic Vulkan surface creation plan.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSurfacePlan {
- /// Report schema version.
- pub schema: u32,
- /// Instance extensions required by the native display backend.
- pub required_instance_extensions: Vec<String>,
-}
-
-/// Vulkan surface bootstrap error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanSurfaceError {
- /// No native raw window/display handles were available.
- MissingNativeHandles,
- /// Required platform surface extensions could not be enumerated.
- RequiredExtensionsFailed {
- /// Vulkan result.
- result: String,
- },
- /// A required extension pointer was not valid UTF-8.
- InvalidExtensionName,
- /// Surface creation failed.
- CreateFailed {
- /// Vulkan result.
- result: String,
- },
-}
-
-impl std::fmt::Display for VulkanSurfaceError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::MissingNativeHandles => {
- write!(
- f,
- "native window/display handles are required for Vulkan surface creation"
- )
- }
- Self::RequiredExtensionsFailed { result } => write!(
- f,
- "failed to enumerate required Vulkan surface extensions: {result}"
- ),
- Self::InvalidExtensionName => {
- write!(f, "Vulkan surface extension name is not valid UTF-8")
- }
- Self::CreateFailed { result } => write!(f, "Vulkan surface creation failed: {result}"),
- }
- }
-}
-
-impl std::error::Error for VulkanSurfaceError {}
-
-/// Created Vulkan surface probe.
-pub struct VulkanSurfaceProbe {
- loader: surface::Instance,
- surface: vk::SurfaceKHR,
- /// Deterministic surface creation report.
- pub report: VulkanSurfacePlan,
-}
-
-impl Drop for VulkanSurfaceProbe {
- fn drop(&mut self) {
- // SAFETY: The `SurfaceKHR` was created by this probe and is destroyed once during drop.
- unsafe { self.loader.destroy_surface(self.surface, None) };
- }
-}
-
-/// Live Vulkan device/surface capability probe.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanRuntimeCapabilityProbe {
- /// Selected device/queue capability report.
- pub capability: VulkanCapabilityReport,
- /// Swapchain plan built from the selected device and live surface capabilities.
- pub swapchain: VulkanSwapchainPlan,
-}
-
-/// Created Vulkan logical device probe.
-pub struct VulkanLogicalDeviceProbe {
- device: ash::Device,
- physical_device: vk::PhysicalDevice,
- /// 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) };
- }
-}
-
-impl VulkanLogicalDeviceProbe {
- /// Returns the graphics queue selected by the Stage 0 policy.
- #[must_use]
- pub fn graphics_queue(&self) -> vk::Queue {
- // SAFETY: The queue-family index belongs to this live logical device.
- unsafe {
- self.device
- .get_device_queue(self.report.graphics_queue_family, 0)
- }
- }
-
- /// Returns the presentation queue selected by the Stage 0 policy.
- #[must_use]
- pub fn present_queue(&self) -> vk::Queue {
- // SAFETY: The queue-family index belongs to this live logical device.
- unsafe {
- self.device
- .get_device_queue(self.report.present_queue_family, 0)
- }
- }
-
- /// Returns a shared reference to the live logical device.
- #[must_use]
- pub fn device(&self) -> &ash::Device {
- &self.device
- }
-}
-
-/// 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>,
-}
-
-/// Created Vulkan swapchain probe.
-pub struct VulkanSwapchainProbe {
- loader: swapchain::Device,
- swapchain: vk::SwapchainKHR,
- /// Deterministic swapchain creation report.
- pub report: VulkanSwapchainReport,
-}
-
-impl Drop for VulkanSwapchainProbe {
- fn drop(&mut self) {
- // SAFETY: The swapchain was created by this probe and is destroyed once during drop.
- unsafe { self.loader.destroy_swapchain(self.swapchain, None) };
- }
-}
-
-impl VulkanSwapchainProbe {
- /// Returns the live swapchain handle.
- #[must_use]
- pub fn swapchain(&self) -> vk::SwapchainKHR {
- self.swapchain
- }
-
- /// Returns the swapchain extension loader for this live swapchain.
- #[must_use]
- pub fn loader(&self) -> &swapchain::Device {
- &self.loader
- }
-}
-
-/// Creates a live native Vulkan renderer for the Stage 0 smoke loop.
-#[derive(Clone, Debug)]
-pub struct VulkanSmokeRendererCreateInfo {
- /// Application name reported to the Vulkan loader.
- pub application_name: String,
- /// Native window/display handles borrowed from a live window.
- pub native_handles: NativeWindowHandles,
- /// Initial drawable extent.
- pub drawable_extent: (u32, u32),
- /// Whether validation layers must be enabled.
- pub enable_validation: bool,
-}
-
-/// Stable smoke renderer bootstrap report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSmokeRendererReport {
- /// Checked-in shader manifest hash used by the renderer.
- pub shader_manifest_hash: String,
- /// Whether portability enumeration was enabled at instance creation.
- pub portability_enumeration: bool,
- /// Selected device name.
- pub device_name: String,
- /// Graphics queue-family index.
- pub graphics_queue_family: u32,
- /// Present queue-family index.
- pub present_queue_family: u32,
- /// Enabled logical-device extension count.
- pub enabled_extension_count: u32,
- /// Current swapchain extent.
- pub swapchain_extent: (u32, u32),
- /// Current swapchain image count.
- pub swapchain_image_count: u32,
-}
-
-/// Measured validation counters from the live smoke loop.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanValidationReport {
- /// Validation warnings observed by the debug messenger.
- pub warning_count: u32,
- /// Validation errors observed by the debug messenger.
- pub error_count: u32,
- /// Stable sorted VUID list.
- pub vuids: Vec<String>,
-}
-
-/// Result of one rendered smoke frame.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum VulkanSmokeFrameOutcome {
- /// A frame was submitted and presented.
- Presented,
- /// Rendering was skipped because the swapchain had to be recreated.
- Recreated,
- /// Rendering was skipped because the drawable extent is zero.
- ZeroExtent,
-}
-
-/// Live smoke renderer error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanSmokeRendererError {
- /// Instance bootstrap failed.
- Instance(VulkanInstanceError),
- /// Surface bootstrap failed.
- Surface(VulkanSurfaceError),
- /// Logical-device bootstrap failed.
- LogicalDevice(VulkanLogicalDeviceError),
- /// Swapchain bootstrap failed.
- Swapchain(VulkanSwapchainProbeError),
- /// Shader manifest validation failed.
- ShaderManifest(VulkanShaderManifestError),
- /// Vulkan operation failed.
- VulkanOperation {
- /// Operation context.
- context: &'static str,
- /// Raw Vulkan result text.
- result: String,
- },
- /// No suitable memory type exists for the required properties.
- MissingMemoryType {
- /// Operation context.
- context: &'static str,
- },
- /// Internal smoke renderer state was unexpectedly absent.
- InvariantViolation {
- /// Missing state context.
- context: &'static str,
- },
-}
-
-impl std::fmt::Display for VulkanSmokeRendererError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Instance(error) => write!(f, "{error}"),
- Self::Surface(error) => write!(f, "{error}"),
- Self::LogicalDevice(error) => write!(f, "{error}"),
- Self::Swapchain(error) => write!(f, "{error}"),
- Self::ShaderManifest(error) => write!(f, "{error}"),
- Self::VulkanOperation { context, result } => {
- write!(f, "{context}: {result}")
- }
- Self::MissingMemoryType { context } => {
- write!(f, "{context}: no compatible Vulkan memory type")
- }
- Self::InvariantViolation { context } => {
- write!(f, "renderer invariant violated: {context}")
- }
- }
- }
-}
-
-impl std::error::Error for VulkanSmokeRendererError {}
-
-struct VulkanValidationShared {
- warning_count: AtomicU32,
- error_count: AtomicU32,
- vuids: Mutex<BTreeSet<String>>,
-}
-
-impl Default for VulkanValidationShared {
- fn default() -> Self {
- Self {
- warning_count: AtomicU32::new(0),
- error_count: AtomicU32::new(0),
- vuids: Mutex::new(BTreeSet::new()),
- }
- }
-}
-
-struct VulkanValidationMessenger {
- loader: ash::ext::debug_utils::Instance,
- messenger: vk::DebugUtilsMessengerEXT,
- shared: Box<VulkanValidationShared>,
-}
-
-impl VulkanValidationMessenger {
- fn report(&self) -> VulkanValidationReport {
- let vuids = self
- .shared
- .vuids
- .lock()
- .map(|values| values.iter().cloned().collect::<Vec<_>>())
- .unwrap_or_default();
- VulkanValidationReport {
- warning_count: self.shared.warning_count.load(Ordering::Relaxed),
- error_count: self.shared.error_count.load(Ordering::Relaxed),
- vuids,
- }
- }
-}
-
-impl Drop for VulkanValidationMessenger {
- fn drop(&mut self) {
- // SAFETY: The messenger belongs to this instance-level loader and is destroyed once.
- unsafe {
- self.loader
- .destroy_debug_utils_messenger(self.messenger, None);
- };
- }
-}
-
-unsafe extern "system" fn vulkan_validation_callback(
- message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
- _message_types: vk::DebugUtilsMessageTypeFlagsEXT,
- callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
- user_data: *mut std::ffi::c_void,
-) -> vk::Bool32 {
- // SAFETY: The debug messenger stores a stable pointer to `VulkanValidationShared` for the messenger lifetime.
- let Some(shared) = (unsafe { (user_data as *const VulkanValidationShared).as_ref() }) else {
- return vk::FALSE;
- };
- if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
- shared.error_count.fetch_add(1, Ordering::Relaxed);
- } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
- shared.warning_count.fetch_add(1, Ordering::Relaxed);
- }
- // SAFETY: Vulkan invokes the callback with either a null pointer or a valid callback-data payload.
- let Some(callback_data) = (unsafe { callback_data.as_ref() }) else {
- return vk::FALSE;
- };
- if let Some(vuid) = (!callback_data.p_message_id_name.is_null()).then(|| {
- // SAFETY: `p_message_id_name` is a Vulkan-owned NUL-terminated string for the callback duration.
- unsafe { CStr::from_ptr(callback_data.p_message_id_name) }
- .to_string_lossy()
- .into_owned()
- }) {
- if vuid.starts_with("VUID-") {
- if let Ok(mut vuids) = shared.vuids.lock() {
- vuids.insert(vuid);
- }
- }
- }
- vk::FALSE
-}
-
-struct VulkanAllocatedBuffer {
- buffer: vk::Buffer,
- memory: vk::DeviceMemory,
-}
-
-struct VulkanSwapchainResources {
- image_views: Vec<vk::ImageView>,
- render_pass: vk::RenderPass,
- pipeline_layout: vk::PipelineLayout,
- pipeline: vk::Pipeline,
- framebuffers: Vec<vk::Framebuffer>,
- command_buffers: Vec<vk::CommandBuffer>,
-}
-
-struct PartialSwapchainResources {
- image_views: Vec<vk::ImageView>,
- render_pass: Option<vk::RenderPass>,
- pipeline_layout: Option<vk::PipelineLayout>,
- pipeline: Option<vk::Pipeline>,
- framebuffers: Vec<vk::Framebuffer>,
- command_buffers: Vec<vk::CommandBuffer>,
-}
-
-struct VulkanFrameSync {
- image_available: vk::Semaphore,
- render_finished: vk::Semaphore,
- fence: vk::Fence,
-}
-
-/// Live Stage 0 Vulkan triangle renderer used by the smoke app.
-pub struct VulkanSmokeRenderer {
- instance: Option<VulkanInstanceProbe>,
- validation: Option<VulkanValidationMessenger>,
- surface: Option<VulkanSurfaceProbe>,
- device: Option<VulkanLogicalDeviceProbe>,
- swapchain: Option<VulkanSwapchainProbe>,
- command_pool: vk::CommandPool,
- swapchain_resources: Option<VulkanSwapchainResources>,
- vertex_buffer: Option<VulkanAllocatedBuffer>,
- index_buffer: Option<VulkanAllocatedBuffer>,
- frame_sync: Vec<VulkanFrameSync>,
- images_in_flight: Vec<vk::Fence>,
- current_frame: usize,
- pending_extent: Option<(u32, u32)>,
- swapchain_recreate_count: u32,
- report: VulkanSmokeRendererReport,
-}
-
-impl VulkanSmokeRenderer {
- /// Creates a live Vulkan smoke renderer bound to a live native window.
- ///
- /// # Errors
- ///
- /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
- /// memory allocation, or synchronization resource creation fails.
- pub fn new(
- create_info: &VulkanSmokeRendererCreateInfo,
- ) -> Result<Self, VulkanSmokeRendererError> {
- let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
- .map_err(VulkanSmokeRendererError::ShaderManifest)?;
- let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
- .map_err(VulkanSmokeRendererError::Surface)?;
- let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
- instance_config
- .required_extensions
- .clone_from(&surface_plan.required_instance_extensions);
- instance_config.enable_validation = create_info.enable_validation;
- let instance = create_vulkan_instance_probe(&instance_config)
- .map_err(VulkanSmokeRendererError::Instance)?;
- let validation = if create_info.enable_validation {
- Some(create_validation_messenger(&instance)?)
- } else {
- None
- };
- let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
- .map_err(VulkanSmokeRendererError::Surface)?;
- let device =
- create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
- .map_err(VulkanSmokeRendererError::LogicalDevice)?;
- let swapchain = create_vulkan_swapchain_probe_for_extent(
- &instance,
- &surface,
- &device,
- create_info.drawable_extent,
- vk::SwapchainKHR::null(),
- )
- .map_err(VulkanSmokeRendererError::Swapchain)?;
- let command_pool = create_command_pool(&device)?;
- let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
- Ok(buffer) => buffer,
- Err(error) => {
- // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_command_pool(command_pool, None) };
- return Err(error);
- }
- };
- let index_buffer = match create_triangle_index_buffer(&instance, &device) {
- Ok(buffer) => buffer,
- Err(error) => {
- // SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_command_pool(command_pool, None) };
- destroy_allocated_buffer(&device, &vertex_buffer);
- return Err(error);
- }
- };
- let mut renderer = Self {
- instance: Some(instance),
- validation,
- surface: Some(surface),
- device: Some(device),
- swapchain: Some(swapchain),
- command_pool,
- swapchain_resources: None,
- vertex_buffer: Some(vertex_buffer),
- index_buffer: Some(index_buffer),
- frame_sync: Vec::new(),
- images_in_flight: Vec::new(),
- current_frame: 0,
- pending_extent: None,
- swapchain_recreate_count: 0,
- report: VulkanSmokeRendererReport {
- shader_manifest_hash: shader_manifest.manifest_hash.clone(),
- portability_enumeration: instance_config.enable_portability_enumeration,
- device_name: String::new(),
- graphics_queue_family: 0,
- present_queue_family: 0,
- enabled_extension_count: 0,
- swapchain_extent: (0, 0),
- swapchain_image_count: 0,
- },
- };
- renderer.rebuild_swapchain_resources(false)?;
- let device_ref = renderer.device_ref()?;
- let swapchain_ref = renderer.swapchain_ref()?;
- renderer.report = VulkanSmokeRendererReport {
- shader_manifest_hash: shader_manifest.manifest_hash,
- portability_enumeration: renderer
- .instance
- .as_ref()
- .is_some_and(|instance| instance.report.create_flags != 0),
- device_name: device_ref.report.device_name.clone(),
- graphics_queue_family: device_ref.report.graphics_queue_family,
- present_queue_family: device_ref.report.present_queue_family,
- enabled_extension_count: device_ref
- .report
- .enabled_extensions
- .len()
- .try_into()
- .unwrap_or(u32::MAX),
- swapchain_extent: swapchain_ref.report.plan.extent,
- swapchain_image_count: swapchain_ref.report.image_count,
- };
- Ok(renderer)
- }
-
- /// Returns the current bootstrap report.
- #[must_use]
- pub const fn report(&self) -> &VulkanSmokeRendererReport {
- &self.report
- }
-
- /// Returns measured validation counters and VUIDs.
- #[must_use]
- pub fn validation_report(&self) -> VulkanValidationReport {
- self.validation.as_ref().map_or(
- VulkanValidationReport {
- warning_count: 0,
- error_count: 0,
- vuids: Vec::new(),
- },
- VulkanValidationMessenger::report,
- )
- }
-
- /// Returns the measured swapchain recreation count.
- #[must_use]
- pub const fn swapchain_recreate_count(&self) -> u32 {
- self.swapchain_recreate_count
- }
-
- /// Requests swapchain recreation for a new drawable extent.
- pub fn request_resize(&mut self, extent: (u32, u32)) {
- self.pending_extent = Some(extent);
- }
-
- fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
- self.device
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "logical device",
- })
- }
-
- fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
- self.swapchain
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "swapchain",
- })
- }
-
- fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
- self.instance
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "instance",
- })
- }
-
- fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
- self.surface
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
- }
-
- fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
- self.swapchain_resources
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "swapchain resources",
- })
- }
-
- fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- self.vertex_buffer
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "vertex buffer",
- })
- }
-
- fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- self.index_buffer
- .as_ref()
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "index buffer",
- })
- }
-
- /// Draws and presents one indexed-triangle frame.
- ///
- /// # Errors
- ///
- /// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
- /// submission, or presentation fails.
- #[allow(clippy::too_many_lines)]
- pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
- if let Some(extent) = self.pending_extent.take() {
- if extent.0 == 0 || extent.1 == 0 {
- self.pending_extent = Some(extent);
- return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
- }
- self.recreate_swapchain(extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
-
- let sync = &self.frame_sync[self.current_frame];
- let image_available = sync.image_available;
- let render_finished = sync.render_finished;
- let in_flight_fence = sync.fence;
- // SAFETY: The fence belongs to this live logical device and is waited from one thread.
- unsafe {
- self.device_ref()?
- .device()
- .wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkWaitForFences",
- result: format!("{error:?}"),
- })?;
- // SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
- let acquire = unsafe {
- self.swapchain_ref()?.loader().acquire_next_image(
- self.swapchain_ref()?.swapchain(),
- 1_000_000_000,
- image_available,
- vk::Fence::null(),
- )
- };
- let (image_index, acquire_suboptimal) = match acquire {
- Ok(result) => result,
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
- Err(error) => {
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkAcquireNextImageKHR",
- result: format!("{error:?}"),
- });
- }
- };
- let image_index_usize = usize::try_from(image_index).unwrap_or(0);
- let image_fence = self.images_in_flight[image_index_usize];
- if image_fence != vk::Fence::null() {
- // SAFETY: The fence belongs to this renderer and can be waited independently.
- unsafe {
- self.device_ref()?
- .device()
- .wait_for_fences(&[image_fence], true, 1_000_000_000)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkWaitForFences(image)",
- result: format!("{error:?}"),
- })?;
- }
- self.images_in_flight[image_index_usize] = in_flight_fence;
- // SAFETY: The fence belongs to this frame context and is not in use after the wait above.
- unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
- |error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkResetFences",
- result: format!("{error:?}"),
- },
- )?;
-
- self.record_command_buffer(image_index_usize)?;
- let wait_semaphores = [image_available];
- let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
- let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
- let signal_semaphores = [render_finished];
- let submit_info = [vk::SubmitInfo::default()
- .wait_semaphores(&wait_semaphores)
- .wait_dst_stage_mask(&wait_stages)
- .command_buffers(&command_buffers)
- .signal_semaphores(&signal_semaphores)];
- // SAFETY: Submission references live queue, sync objects and recorded command buffer.
- unsafe {
- self.device_ref()?.device().queue_submit(
- self.device_ref()?.graphics_queue(),
- &submit_info,
- in_flight_fence,
- )
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkQueueSubmit",
- result: format!("{error:?}"),
- })?;
-
- let present_wait = [render_finished];
- let swapchains = [self.swapchain_ref()?.swapchain()];
- let image_indices = [image_index];
- let present_info = vk::PresentInfoKHR::default()
- .wait_semaphores(&present_wait)
- .swapchains(&swapchains)
- .image_indices(&image_indices);
- // SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
- let present_suboptimal = match unsafe {
- self.swapchain_ref()?
- .loader()
- .queue_present(self.device_ref()?.present_queue(), &present_info)
- } {
- Ok(suboptimal) => suboptimal,
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- return Ok(VulkanSmokeFrameOutcome::Recreated);
- }
- Err(error) => {
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkQueuePresentKHR",
- result: format!("{error:?}"),
- });
- }
- };
-
- self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
- if acquire_suboptimal || present_suboptimal {
- self.recreate_swapchain(self.report.swapchain_extent)?;
- Ok(VulkanSmokeFrameOutcome::Recreated)
- } else {
- Ok(VulkanSmokeFrameOutcome::Presented)
- }
- }
-
- fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
- let device = self.device_ref()?;
- // SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
- unsafe { device.device().device_wait_idle() }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle",
- result: format!("{error:?}"),
- }
- })?;
- self.pending_extent = None;
- self.rebuild_swapchain(extent)?;
- self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
- Ok(())
- }
-
- fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
- self.destroy_swapchain_resources();
- let instance = self.instance_ref()?;
- let surface = self.surface_ref()?;
- let device = self.device_ref()?;
- let old_swapchain = self
- .swapchain
- .as_ref()
- .map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
- let new_swapchain = create_vulkan_swapchain_probe_for_extent(
- instance,
- surface,
- device,
- extent,
- old_swapchain,
- )
- .map_err(VulkanSmokeRendererError::Swapchain)?;
- self.swapchain = Some(new_swapchain);
- self.rebuild_swapchain_resources(true)?;
- Ok(())
- }
-
- fn rebuild_swapchain_resources(
- &mut self,
- reuse_command_pool: bool,
- ) -> Result<(), VulkanSmokeRendererError> {
- let resources = {
- let device = self.device_ref()?;
- let swapchain = self.swapchain_ref()?;
- create_swapchain_resources(
- device,
- swapchain,
- self.command_pool,
- self.vertex_buffer_ref()?,
- self.index_buffer_ref()?,
- reuse_command_pool,
- )?
- };
- let frame_sync = {
- let device = self.device_ref()?;
- create_frame_sync(device)?
- };
- let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
- let swapchain_image_count = self.swapchain_ref()?.report.image_count;
- self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
- self.frame_sync = frame_sync;
- self.report.swapchain_extent = swapchain_extent;
- self.report.swapchain_image_count = swapchain_image_count;
- self.swapchain_resources = Some(resources);
- Ok(())
- }
-
- #[allow(clippy::too_many_lines)]
- fn record_command_buffer(
- &mut self,
- image_index: usize,
- ) -> Result<(), VulkanSmokeRendererError> {
- let device = self.device_ref()?;
- let swapchain = self.swapchain_ref()?;
- let resources = self.resources_ref()?;
- let command_buffer = resources.command_buffers[image_index];
- // SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
- unsafe {
- device
- .device()
- .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkResetCommandBuffer",
- result: format!("{error:?}"),
- })?;
- let begin_info = vk::CommandBufferBeginInfo::default();
- // SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
- unsafe {
- device
- .device()
- .begin_command_buffer(command_buffer, &begin_info)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkBeginCommandBuffer",
- result: format!("{error:?}"),
- })?;
-
- let pre_barrier = vk::ImageMemoryBarrier::default()
- .old_layout(vk::ImageLayout::PRESENT_SRC_KHR)
- .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .subresource_range(color_subresource_range())
- .src_access_mask(vk::AccessFlags::empty())
- .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
- // SAFETY: The swapchain is live and queried only to resolve the current image handles.
- let swapchain_images = unsafe {
- swapchain
- .loader()
- .get_swapchain_images(swapchain.swapchain())
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkGetSwapchainImagesKHR",
- result: format!("{error:?}"),
- })?;
- let pre_barrier = pre_barrier.image(swapchain_images[image_index]);
- // SAFETY: The barriers operate on the acquired swapchain image owned by this command buffer submission.
- unsafe {
- device.device().cmd_pipeline_barrier(
- command_buffer,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[pre_barrier],
- );
- }
-
- let clear_values = [vk::ClearValue {
- color: vk::ClearColorValue {
- float32: [0.05, 0.08, 0.11, 1.0],
- },
- }];
- let render_area = vk::Rect2D {
- offset: vk::Offset2D { x: 0, y: 0 },
- extent: vk::Extent2D {
- width: swapchain.report.plan.extent.0,
- height: swapchain.report.plan.extent.1,
- },
- };
- let render_pass_info = vk::RenderPassBeginInfo::default()
- .render_pass(resources.render_pass)
- .framebuffer(resources.framebuffers[image_index])
- .render_area(render_area)
- .clear_values(&clear_values);
- // SAFETY: All commands target live frame resources owned by this renderer.
- unsafe {
- device.device().cmd_begin_render_pass(
- command_buffer,
- &render_pass_info,
- vk::SubpassContents::INLINE,
- );
- device.device().cmd_bind_pipeline(
- command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- resources.pipeline,
- );
- let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
- let offsets = [0_u64];
- device
- .device()
- .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
- device.device().cmd_bind_index_buffer(
- command_buffer,
- self.index_buffer_ref()?.buffer,
- 0,
- vk::IndexType::UINT16,
- );
- device
- .device()
- .cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
- device.device().cmd_end_render_pass(command_buffer);
- }
-
- let post_barrier = vk::ImageMemoryBarrier::default()
- .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
- .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .image(swapchain_images[image_index])
- .subresource_range(color_subresource_range())
- .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
- .dst_access_mask(vk::AccessFlags::empty());
- // SAFETY: The post-render barrier transitions the same live swapchain image into present layout.
- unsafe {
- device.device().cmd_pipeline_barrier(
- command_buffer,
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
- vk::PipelineStageFlags::BOTTOM_OF_PIPE,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[post_barrier],
- );
- device.device().end_command_buffer(command_buffer)
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkEndCommandBuffer",
- result: format!("{error:?}"),
- })?;
- Ok(())
- }
-
- fn destroy_swapchain_resources(&mut self) {
- let Some(device) = self.device.as_ref() else {
- return;
- };
- for sync in self.frame_sync.drain(..) {
- // SAFETY: These sync objects belong to this device and are destroyed once.
- unsafe {
- device
- .device()
- .destroy_semaphore(sync.image_available, None);
- device
- .device()
- .destroy_semaphore(sync.render_finished, None);
- device.device().destroy_fence(sync.fence, None);
- }
- }
- if let Some(resources) = self.swapchain_resources.take() {
- destroy_swapchain_resources(device, self.command_pool, resources);
- }
- self.images_in_flight.clear();
- self.current_frame = 0;
- }
-
- fn teardown(&mut self) {
- if let Some(device) = self.device.as_ref() {
- // SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
- let _ = unsafe { device.device().device_wait_idle() };
- }
- self.destroy_swapchain_resources();
- if let Some(device) = self.device.as_ref() {
- if let Some(buffer) = self.index_buffer.take() {
- // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
- unsafe {
- device.device().destroy_buffer(buffer.buffer, None);
- device.device().free_memory(buffer.memory, None);
- }
- }
- if let Some(buffer) = self.vertex_buffer.take() {
- // SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
- unsafe {
- device.device().destroy_buffer(buffer.buffer, None);
- device.device().free_memory(buffer.memory, None);
- }
- }
- // SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
- unsafe {
- device
- .device()
- .destroy_command_pool(self.command_pool, None);
- };
- }
- // Drop child Vulkan owners explicitly before their parents instead of relying on field order.
- self.swapchain.take();
- self.device.take();
- self.surface.take();
- self.validation.take();
- self.instance.take();
- }
-}
-
-impl Drop for VulkanSmokeRenderer {
- fn drop(&mut self) {
- self.teardown();
- }
-}
-
-fn create_validation_messenger(
- instance: &VulkanInstanceProbe,
-) -> Result<VulkanValidationMessenger, VulkanSmokeRendererError> {
- let shared = Box::new(VulkanValidationShared::default());
- let loader = ash::ext::debug_utils::Instance::new(&instance.entry, &instance.instance);
- let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
- .message_severity(
- vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
- | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
- )
- .message_type(
- vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
- | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
- | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
- )
- .pfn_user_callback(Some(vulkan_validation_callback))
- .user_data((&raw const *shared).cast_mut().cast());
- let messenger =
- // SAFETY: The create info points at a stable boxed user-data allocation for the messenger lifetime.
- unsafe { loader.create_debug_utils_messenger(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateDebugUtilsMessengerEXT",
- result: format!("{error:?}"),
- }
- })?;
- Ok(VulkanValidationMessenger {
- loader,
- messenger,
- shared,
- })
-}
-
-fn create_command_pool(
- device: &VulkanLogicalDeviceProbe,
-) -> Result<vk::CommandPool, VulkanSmokeRendererError> {
- let create_info = vk::CommandPoolCreateInfo::default()
- .queue_family_index(device.report.graphics_queue_family)
- .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
- // SAFETY: The queue-family index belongs to this live logical device.
- unsafe { device.device().create_command_pool(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateCommandPool",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn create_triangle_vertex_buffer(
- instance: &VulkanInstanceProbe,
- device: &VulkanLogicalDeviceProbe,
-) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- let vertices: [[f32; 5]; 3] = [
- [0.0, -0.55, 1.0, 0.2, 0.2],
- [0.55, 0.55, 0.2, 1.0, 0.2],
- [-0.55, 0.55, 0.2, 0.4, 1.0],
- ];
- let mut bytes = Vec::with_capacity(vertices.len() * 5 * std::mem::size_of::<f32>());
- for vertex in vertices {
- for value in vertex {
- bytes.extend_from_slice(&value.to_ne_bytes());
- }
- }
- create_host_visible_buffer(
- instance,
- device,
- &bytes,
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "triangle vertex buffer",
- )
-}
-
-fn create_triangle_index_buffer(
- instance: &VulkanInstanceProbe,
- device: &VulkanLogicalDeviceProbe,
-) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- let indices = [0_u16, 1_u16, 2_u16];
- let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
- for index in indices {
- bytes.extend_from_slice(&index.to_ne_bytes());
- }
- create_host_visible_buffer(
- instance,
- device,
- &bytes,
- vk::BufferUsageFlags::INDEX_BUFFER,
- "triangle index buffer",
- )
-}
-
-fn create_host_visible_buffer(
- instance: &VulkanInstanceProbe,
- device: &VulkanLogicalDeviceProbe,
- bytes: &[u8],
- usage: vk::BufferUsageFlags,
- context: &'static str,
-) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
- let create_info = vk::BufferCreateInfo::default()
- .size(bytes.len().try_into().unwrap_or(u64::MAX))
- .usage(usage)
- .sharing_mode(vk::SharingMode::EXCLUSIVE);
- // SAFETY: The create info is stack-owned and references no external memory.
- let buffer = unsafe { device.device().create_buffer(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context,
- result: format!("{error:?}"),
- }
- })?;
- // SAFETY: The buffer belongs to this device and is queried immediately after creation.
- let requirements = unsafe { device.device().get_buffer_memory_requirements(buffer) };
- let Some(memory_type_index) = find_memory_type(
- instance,
- device.physical_device,
- requirements.memory_type_bits,
- vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
- ) else {
- // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_buffer(buffer, None) };
- return Err(VulkanSmokeRendererError::MissingMemoryType { context });
- };
- let allocate_info = vk::MemoryAllocateInfo::default()
- .allocation_size(requirements.size)
- .memory_type_index(memory_type_index);
- let memory =
- // SAFETY: Allocation uses a memory type index selected from the physical-device requirements above.
- unsafe { device.device().allocate_memory(&allocate_info, None) }.map_err(|error| {
- // SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_buffer(buffer, None) };
- VulkanSmokeRendererError::VulkanOperation {
- context,
- result: format!("{error:?}"),
- }
- })?;
- // SAFETY: The buffer and allocation belong to the same live logical device.
- unsafe { device.device().bind_buffer_memory(buffer, memory, 0) }.map_err(|error| {
- // SAFETY: The buffer and allocation belong to this logical device and are destroyed on setup failure.
- unsafe {
- device.device().destroy_buffer(buffer, None);
- device.device().free_memory(memory, None);
- }
- VulkanSmokeRendererError::VulkanOperation {
- context,
- result: format!("{error:?}"),
- }
- })?;
- // SAFETY: The allocation is HOST_VISIBLE, mapped for the full buffer size and unmapped before return.
- let mapped = unsafe {
- device
- .device()
- .map_memory(memory, 0, requirements.size, vk::MemoryMapFlags::empty())
- }
- .map_err(|error| {
- // SAFETY: The buffer and allocation belong to this logical device and are destroyed on setup failure.
- unsafe {
- device.device().destroy_buffer(buffer, None);
- device.device().free_memory(memory, None);
- }
- VulkanSmokeRendererError::VulkanOperation {
- context,
- result: format!("{error:?}"),
- }
- })?;
- // SAFETY: The mapped pointer is valid for `bytes.len()` bytes and non-overlapping with the source slice.
- unsafe {
- std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
- device.device().unmap_memory(memory);
- }
- Ok(VulkanAllocatedBuffer { buffer, memory })
-}
-
-fn find_memory_type(
- instance: &VulkanInstanceProbe,
- physical_device: vk::PhysicalDevice,
- memory_type_bits: u32,
- required_properties: vk::MemoryPropertyFlags,
-) -> Option<u32> {
- // SAFETY: Physical-device memory properties are queried from a live instance-owned physical device.
- let memory_properties = unsafe {
- instance
- .instance
- .get_physical_device_memory_properties(physical_device)
- };
- memory_properties
- .memory_types
- .iter()
- .enumerate()
- .find_map(|(index, memory_type)| {
- let supported = (memory_type_bits & (1_u32 << index)) != 0;
- let has_properties = memory_type.property_flags.contains(required_properties);
- (supported && has_properties).then(|| index.try_into().unwrap_or(u32::MAX))
- })
-}
-
-#[allow(clippy::too_many_lines)]
-fn create_swapchain_resources(
- device: &VulkanLogicalDeviceProbe,
- swapchain: &VulkanSwapchainProbe,
- command_pool: vk::CommandPool,
- _vertex_buffer: &VulkanAllocatedBuffer,
- _index_buffer: &VulkanAllocatedBuffer,
- _reuse_command_pool: bool,
-) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
- // SAFETY: The swapchain is live and owned by this renderer for the duration of the query.
- let images = unsafe {
- swapchain
- .loader()
- .get_swapchain_images(swapchain.swapchain())
- }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkGetSwapchainImagesKHR",
- result: format!("{error:?}"),
- })?;
- let mut partial = PartialSwapchainResources {
- image_views: Vec::with_capacity(images.len()),
- render_pass: None,
- pipeline_layout: None,
- pipeline: None,
- framebuffers: Vec::with_capacity(images.len()),
- command_buffers: Vec::new(),
- };
- for image in &images {
- match create_image_view(device, *image, swapchain.report.plan.format.format) {
- Ok(image_view) => partial.image_views.push(image_view),
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- }
- }
- let render_pass = match create_render_pass(device, swapchain.report.plan.format.format) {
- Ok(render_pass) => render_pass,
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- };
- partial.render_pass = Some(render_pass);
- let pipeline_layout = match create_pipeline_layout(device) {
- Ok(pipeline_layout) => pipeline_layout,
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- };
- partial.pipeline_layout = Some(pipeline_layout);
- let pipeline = match create_graphics_pipeline(
- device,
- render_pass,
- pipeline_layout,
- swapchain.report.plan.extent,
- ) {
- Ok(pipeline) => pipeline,
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- };
- partial.pipeline = Some(pipeline);
- for image_view in &partial.image_views {
- match create_framebuffer(
- device,
- render_pass,
- *image_view,
- swapchain.report.plan.extent,
- ) {
- Ok(framebuffer) => partial.framebuffers.push(framebuffer),
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- }
- }
- partial.command_buffers = match allocate_command_buffers(
- device,
- command_pool,
- partial.image_views.len().try_into().unwrap_or(u32::MAX),
- ) {
- Ok(command_buffers) => command_buffers,
- Err(error) => {
- destroy_partial_swapchain_resources(device, command_pool, partial);
- return Err(error);
- }
- };
- Ok(VulkanSwapchainResources {
- image_views: partial.image_views,
- render_pass,
- pipeline_layout,
- pipeline,
- framebuffers: partial.framebuffers,
- command_buffers: partial.command_buffers,
- })
-}
-
-fn create_image_view(
- device: &VulkanLogicalDeviceProbe,
- image: vk::Image,
- format: i32,
-) -> Result<vk::ImageView, VulkanSmokeRendererError> {
- let create_info = vk::ImageViewCreateInfo::default()
- .image(image)
- .view_type(vk::ImageViewType::TYPE_2D)
- .format(vk::Format::from_raw(format))
- .subresource_range(color_subresource_range());
- // SAFETY: The image comes from the live swapchain and the subresource range covers its color aspect.
- unsafe { device.device().create_image_view(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateImageView",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn create_render_pass(
- device: &VulkanLogicalDeviceProbe,
- format: i32,
-) -> Result<vk::RenderPass, VulkanSmokeRendererError> {
- let color_attachment = vk::AttachmentDescription::default()
- .format(vk::Format::from_raw(format))
- .samples(vk::SampleCountFlags::TYPE_1)
- .load_op(vk::AttachmentLoadOp::CLEAR)
- .store_op(vk::AttachmentStoreOp::STORE)
- .initial_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
- .final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
- let color_attachment_ref = vk::AttachmentReference::default()
- .attachment(0)
- .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
- let color_attachments = [color_attachment_ref];
- let subpass = vk::SubpassDescription::default()
- .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
- .color_attachments(&color_attachments);
- let dependency = vk::SubpassDependency::default()
- .src_subpass(vk::SUBPASS_EXTERNAL)
- .dst_subpass(0)
- .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
- .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
- .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
- let attachments = [color_attachment];
- let subpasses = [subpass];
- let dependencies = [dependency];
- let create_info = vk::RenderPassCreateInfo::default()
- .attachments(&attachments)
- .subpasses(&subpasses)
- .dependencies(&dependencies);
- // SAFETY: The render-pass create info only references stack-owned descriptors.
- unsafe { device.device().create_render_pass(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateRenderPass",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn create_pipeline_layout(
- device: &VulkanLogicalDeviceProbe,
-) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
- let create_info = vk::PipelineLayoutCreateInfo::default();
- // SAFETY: The pipeline layout contains no descriptor sets or push constants.
- unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreatePipelineLayout",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn extent_component_to_f32(value: u32) -> f32 {
- u16::try_from(value).map_or(f32::from(u16::MAX), f32::from)
-}
-
-#[allow(clippy::too_many_lines)]
-fn create_graphics_pipeline(
- device: &VulkanLogicalDeviceProbe,
- render_pass: vk::RenderPass,
- pipeline_layout: vk::PipelineLayout,
- extent: (u32, u32),
-) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
- let entry_point = c"main";
- let vertex_module = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS)?;
- let fragment_module = match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS) {
- Ok(fragment_module) => fragment_module,
- Err(error) => {
- // SAFETY: The vertex shader module was created above on this logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_shader_module(vertex_module, None) };
- return Err(error);
- }
- };
- let stage_create_infos = [
- vk::PipelineShaderStageCreateInfo::default()
- .stage(vk::ShaderStageFlags::VERTEX)
- .module(vertex_module)
- .name(entry_point),
- vk::PipelineShaderStageCreateInfo::default()
- .stage(vk::ShaderStageFlags::FRAGMENT)
- .module(fragment_module)
- .name(entry_point),
- ];
- let binding_descriptions = [vk::VertexInputBindingDescription {
- binding: 0,
- stride: 20,
- input_rate: vk::VertexInputRate::VERTEX,
- }];
- let attribute_descriptions = [
- vk::VertexInputAttributeDescription {
- location: 0,
- binding: 0,
- format: vk::Format::R32G32_SFLOAT,
- offset: 0,
- },
- vk::VertexInputAttributeDescription {
- location: 1,
- binding: 0,
- format: vk::Format::R32G32B32_SFLOAT,
- offset: 8,
- },
- ];
- let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
- .vertex_binding_descriptions(&binding_descriptions)
- .vertex_attribute_descriptions(&attribute_descriptions);
- let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
- .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
- let viewports = [vk::Viewport {
- x: 0.0,
- y: 0.0,
- width: extent_component_to_f32(extent.0),
- height: extent_component_to_f32(extent.1),
- min_depth: 0.0,
- max_depth: 1.0,
- }];
- let scissors = [vk::Rect2D {
- offset: vk::Offset2D { x: 0, y: 0 },
- extent: vk::Extent2D {
- width: extent.0,
- height: extent.1,
- },
- }];
- let viewport_state = vk::PipelineViewportStateCreateInfo::default()
- .viewports(&viewports)
- .scissors(&scissors);
- let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
- .polygon_mode(vk::PolygonMode::FILL)
- .cull_mode(vk::CullModeFlags::BACK)
- .front_face(vk::FrontFace::CLOCKWISE)
- .line_width(1.0);
- let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
- .rasterization_samples(vk::SampleCountFlags::TYPE_1);
- let color_blend_attachment = [vk::PipelineColorBlendAttachmentState::default()
- .color_write_mask(
- vk::ColorComponentFlags::R
- | vk::ColorComponentFlags::G
- | vk::ColorComponentFlags::B
- | vk::ColorComponentFlags::A,
- )];
- let color_blend_state =
- vk::PipelineColorBlendStateCreateInfo::default().attachments(&color_blend_attachment);
- let create_info = [vk::GraphicsPipelineCreateInfo::default()
- .stages(&stage_create_infos)
- .vertex_input_state(&vertex_input_state)
- .input_assembly_state(&input_assembly_state)
- .viewport_state(&viewport_state)
- .rasterization_state(&rasterization_state)
- .multisample_state(&multisample_state)
- .color_blend_state(&color_blend_state)
- .layout(pipeline_layout)
- .render_pass(render_pass)
- .subpass(0)];
- // SAFETY: The pipeline creation references live shader modules and stack-owned fixed-function descriptors.
- let pipeline_result = unsafe {
- device
- .device()
- .create_graphics_pipelines(vk::PipelineCache::null(), &create_info, None)
- };
- // SAFETY: Shader modules are no longer needed after pipeline creation completes.
- unsafe {
- device.device().destroy_shader_module(vertex_module, None);
- device.device().destroy_shader_module(fragment_module, None);
- }
- let pipeline =
- pipeline_result.map_err(|(_, error)| VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateGraphicsPipelines",
- result: format!("{error:?}"),
- })?[0];
- Ok(pipeline)
-}
-
-fn create_shader_module(
- device: &VulkanLogicalDeviceProbe,
- words: &[u32],
-) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
- let create_info = vk::ShaderModuleCreateInfo::default().code(words);
- // SAFETY: SPIR-V words are immutable and valid for the duration of the call.
- unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateShaderModule",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn create_framebuffer(
- device: &VulkanLogicalDeviceProbe,
- render_pass: vk::RenderPass,
- image_view: vk::ImageView,
- extent: (u32, u32),
-) -> Result<vk::Framebuffer, VulkanSmokeRendererError> {
- let attachments = [image_view];
- let create_info = vk::FramebufferCreateInfo::default()
- .render_pass(render_pass)
- .attachments(&attachments)
- .width(extent.0)
- .height(extent.1)
- .layers(1);
- // SAFETY: The framebuffer attachments and render pass remain live for the duration of the call.
- unsafe { device.device().create_framebuffer(&create_info, None) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateFramebuffer",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn allocate_command_buffers(
- device: &VulkanLogicalDeviceProbe,
- command_pool: vk::CommandPool,
- count: u32,
-) -> Result<Vec<vk::CommandBuffer>, VulkanSmokeRendererError> {
- let allocate_info = vk::CommandBufferAllocateInfo::default()
- .command_pool(command_pool)
- .level(vk::CommandBufferLevel::PRIMARY)
- .command_buffer_count(count);
- // SAFETY: Command buffers are allocated from a live resettable pool owned by this device.
- unsafe { device.device().allocate_command_buffers(&allocate_info) }.map_err(|error| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkAllocateCommandBuffers",
- result: format!("{error:?}"),
- }
- })
-}
-
-fn create_frame_sync(
- device: &VulkanLogicalDeviceProbe,
-) -> Result<Vec<VulkanFrameSync>, VulkanSmokeRendererError> {
- let semaphore_info = vk::SemaphoreCreateInfo::default();
- let fence_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED);
- let mut sync = Vec::with_capacity(2);
- for _ in 0..2 {
- // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
- let image_available = unsafe { device.device().create_semaphore(&semaphore_info, None) }
- .map_err(|error| VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateSemaphore(image_available)",
- result: format!("{error:?}"),
- })?;
- let render_finished =
- // SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
- match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
- Ok(render_finished) => render_finished,
- Err(error) => {
- destroy_frame_sync_objects(device, &sync);
- // SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure.
- unsafe { device.device().destroy_semaphore(image_available, None) };
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateSemaphore(render_finished)",
- result: format!("{error:?}"),
- });
- }
- };
- let fence =
- // SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
- match unsafe { device.device().create_fence(&fence_info, None) } {
- Ok(fence) => fence,
- Err(error) => {
- destroy_frame_sync_objects(device, &sync);
- // SAFETY: These semaphores were created above on this logical device and are destroyed on setup failure.
- unsafe {
- device.device().destroy_semaphore(image_available, None);
- device.device().destroy_semaphore(render_finished, None);
- }
- return Err(VulkanSmokeRendererError::VulkanOperation {
- context: "vkCreateFence",
- result: format!("{error:?}"),
- });
- }
- };
- sync.push(VulkanFrameSync {
- image_available,
- render_finished,
- fence,
- });
- }
- Ok(sync)
-}
-
-fn destroy_swapchain_resources(
- device: &VulkanLogicalDeviceProbe,
- command_pool: vk::CommandPool,
- resources: VulkanSwapchainResources,
-) {
- // SAFETY: All swapchain-dependent objects belong to this device and are destroyed once.
- unsafe {
- device
- .device()
- .free_command_buffers(command_pool, &resources.command_buffers);
- for framebuffer in resources.framebuffers {
- device.device().destroy_framebuffer(framebuffer, None);
- }
- device.device().destroy_pipeline(resources.pipeline, None);
- device
- .device()
- .destroy_pipeline_layout(resources.pipeline_layout, None);
- device
- .device()
- .destroy_render_pass(resources.render_pass, None);
- for image_view in resources.image_views {
- device.device().destroy_image_view(image_view, None);
- }
- }
-}
-
-fn destroy_partial_swapchain_resources(
- device: &VulkanLogicalDeviceProbe,
- command_pool: vk::CommandPool,
- resources: PartialSwapchainResources,
-) {
- // SAFETY: All handles in this partial resource set were created on this live logical device and are destroyed once.
- unsafe {
- if !resources.command_buffers.is_empty() {
- device
- .device()
- .free_command_buffers(command_pool, &resources.command_buffers);
- }
- for framebuffer in resources.framebuffers {
- device.device().destroy_framebuffer(framebuffer, None);
- }
- if let Some(pipeline) = resources.pipeline {
- device.device().destroy_pipeline(pipeline, None);
- }
- if let Some(pipeline_layout) = resources.pipeline_layout {
- device
- .device()
- .destroy_pipeline_layout(pipeline_layout, None);
- }
- if let Some(render_pass) = resources.render_pass {
- device.device().destroy_render_pass(render_pass, None);
- }
- for image_view in resources.image_views {
- device.device().destroy_image_view(image_view, None);
- }
- }
-}
-
-fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanFrameSync]) {
- for frame_sync in sync {
- // SAFETY: These sync objects belong to this live logical device and are destroyed once during teardown.
- unsafe {
- device
- .device()
- .destroy_semaphore(frame_sync.image_available, None);
- device
- .device()
- .destroy_semaphore(frame_sync.render_finished, None);
- device.device().destroy_fence(frame_sync.fence, None);
- }
- }
-}
-
-fn destroy_allocated_buffer(device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer) {
- // SAFETY: The buffer and allocation belong to this live logical device and are destroyed once during teardown.
- unsafe {
- device.device().destroy_buffer(buffer.buffer, None);
- device.device().free_memory(buffer.memory, None);
- }
-}
-
-fn color_subresource_range() -> vk::ImageSubresourceRange {
- vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .base_mip_level(0)
- .level_count(1)
- .base_array_layer(0)
- .layer_count(1)
-}
-
-/// Runtime swapchain creation report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSwapchainReport {
- /// Report schema version.
- pub schema: u32,
- /// Deterministic swapchain policy used for creation.
- pub plan: VulkanSwapchainPlan,
- /// Number of images returned by `vkGetSwapchainImagesKHR`.
- pub image_count: u32,
-}
-
-/// Live Vulkan device/surface capability probe error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanRuntimeCapabilityError {
- /// Physical device enumeration failed.
- EnumerateDevicesFailed {
- /// Vulkan result.
- result: String,
- },
- /// Device extension enumeration failed.
- EnumerateDeviceExtensionsFailed {
- /// Device name or index context.
- device: String,
- /// Vulkan result.
- result: String,
- },
- /// Queue-family present support query failed.
- PresentSupportFailed {
- /// Device name.
- device: String,
- /// Queue-family index.
- queue_family: u32,
- /// Vulkan result.
- result: String,
- },
- /// Surface format query failed.
- SurfaceFormatsFailed {
- /// Device name.
- device: String,
- /// Vulkan result.
- result: String,
- },
- /// Surface capability query failed.
- SurfaceCapabilitiesFailed {
- /// Device name.
- device: String,
- /// Vulkan result.
- result: String,
- },
- /// Present mode query failed.
- PresentModesFailed {
- /// Device name.
- device: String,
- /// Vulkan result.
- result: String,
- },
- /// No device satisfied Stage 0 capability policy.
- Capability(VulkanCapabilityError),
- /// Live surface capabilities could not produce a swapchain plan.
- Swapchain(VulkanSwapchainError),
-}
-
-impl std::fmt::Display for VulkanRuntimeCapabilityError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::EnumerateDevicesFailed { result } => {
- write!(f, "Vulkan physical device enumeration failed: {result}")
- }
- Self::EnumerateDeviceExtensionsFailed { device, result } => write!(
- f,
- "Vulkan device {device} extension enumeration failed: {result}"
- ),
- Self::PresentSupportFailed {
- device,
- queue_family,
- result,
- } => write!(
- f,
- "Vulkan device {device} queue family {queue_family} present support query failed: {result}"
- ),
- Self::SurfaceFormatsFailed { device, result } => write!(
- f,
- "Vulkan device {device} surface format query failed: {result}"
- ),
- Self::SurfaceCapabilitiesFailed { device, result } => write!(
- f,
- "Vulkan device {device} surface capabilities query failed: {result}"
- ),
- Self::PresentModesFailed { device, result } => write!(
- f,
- "Vulkan device {device} present mode query failed: {result}"
- ),
- Self::Capability(error) => write!(f, "{error}"),
- Self::Swapchain(error) => write!(f, "{error}"),
- }
- }
-}
-
-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 {}
-
-/// Vulkan swapchain creation error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanSwapchainProbeError {
- /// Surface capability query failed.
- SurfaceCapabilitiesFailed {
- /// Vulkan result.
- result: String,
- },
- /// Swapchain creation failed.
- CreateFailed {
- /// Vulkan result.
- result: String,
- },
- /// Swapchain image query failed.
- ImagesFailed {
- /// Vulkan result.
- result: String,
- },
-}
-
-impl std::fmt::Display for VulkanSwapchainProbeError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::SurfaceCapabilitiesFailed { result } => {
- write!(f, "Vulkan surface capabilities query failed: {result}")
- }
- Self::CreateFailed { result } => {
- write!(f, "Vulkan swapchain creation failed: {result}")
- }
- Self::ImagesFailed { result } => {
- write!(f, "Vulkan swapchain image query failed: {result}")
- }
- }
- }
-}
-
-impl std::error::Error for VulkanSwapchainProbeError {}
-
-/// Builds a deterministic Vulkan surface plan from native window handles.
-///
-/// # Errors
-///
-/// Returns [`VulkanSurfaceError`] when no native handles exist or the platform
-/// display backend has no Vulkan surface extension mapping.
-pub fn plan_vulkan_surface(
- handles: Option<NativeWindowHandles>,
-) -> Result<VulkanSurfacePlan, VulkanSurfaceError> {
- let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
- let required = ash_window::enumerate_required_extensions(handles.display).map_err(|error| {
- VulkanSurfaceError::RequiredExtensionsFailed {
- result: format!("{error:?}"),
- }
- })?;
- let mut required_instance_extensions = Vec::with_capacity(required.len());
- for extension in required {
- let name = extension_name(*extension)?;
- required_instance_extensions.push(name);
- }
- required_instance_extensions.sort();
- required_instance_extensions.dedup();
- Ok(VulkanSurfacePlan {
- schema: 1,
- required_instance_extensions,
- })
-}
-
-/// Creates a Vulkan surface probe from native window handles.
-///
-/// # Errors
-///
-/// Returns [`VulkanSurfaceError`] when handles are missing, required extensions
-/// cannot be planned, or `vkCreate*SurfaceKHR` fails.
-pub fn create_vulkan_surface_probe(
- instance: &VulkanInstanceProbe,
- handles: Option<NativeWindowHandles>,
-) -> Result<VulkanSurfaceProbe, VulkanSurfaceError> {
- let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
- let report = plan_vulkan_surface(Some(handles))?;
- // SAFETY: The platform handles are only used to create a child surface owned by this probe.
- let surface = unsafe {
- ash_window::create_surface(
- &instance.entry,
- &instance.instance,
- handles.display,
- handles.window,
- None,
- )
- }
- .map_err(|error| VulkanSurfaceError::CreateFailed {
- result: format!("{error:?}"),
- })?;
- Ok(VulkanSurfaceProbe {
- loader: surface::Instance::new(&instance.entry, &instance.instance),
- surface,
- report,
- })
-}
-
-/// Probes live Vulkan device, queue, surface and swapchain capabilities.
-///
-/// # Errors
-///
-/// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface
-/// capability queries, Stage 0 device selection, or swapchain planning fails.
-pub fn probe_vulkan_runtime_capabilities(
- instance: &VulkanInstanceProbe,
- 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,
- physical_device: selected.physical_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,
- })
-}
-
-/// Creates a Vulkan swapchain for the live logical device and surface.
-///
-/// # Errors
-///
-/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
-/// swapchain creation, or swapchain image enumeration fails.
-pub fn create_vulkan_swapchain_probe(
- instance: &VulkanInstanceProbe,
- surface: &VulkanSurfaceProbe,
- device: &VulkanLogicalDeviceProbe,
-) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
- create_vulkan_swapchain_probe_for_extent(
- instance,
- surface,
- device,
- device.runtime.swapchain.extent,
- vk::SwapchainKHR::null(),
- )
-}
-
-/// Creates a Vulkan swapchain for the live logical device and surface at a specific extent.
-///
-/// # Errors
-///
-/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
-/// swapchain creation, or swapchain image enumeration fails.
-pub fn create_vulkan_swapchain_probe_for_extent(
- instance: &VulkanInstanceProbe,
- surface: &VulkanSurfaceProbe,
- device: &VulkanLogicalDeviceProbe,
- drawable_extent: (u32, u32),
- old_swapchain: vk::SwapchainKHR,
-) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
- let raw_capabilities = {
- // SAFETY: The physical device and surface are live query inputs and no handles are retained.
- unsafe {
- surface
- .loader
- .get_physical_device_surface_capabilities(device.physical_device, surface.surface)
- }
- }
- .map_err(
- |error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
- result: format!("{error:?}"),
- },
- )?;
- let surface_formats =
- live_surface_formats(surface, device.physical_device, &device.report.device_name).map_err(
- |error| VulkanSwapchainProbeError::CreateFailed {
- result: error.to_string(),
- },
- )?;
- let present_modes =
- live_present_modes(surface, device.physical_device, &device.report.device_name).map_err(
- |error| VulkanSwapchainProbeError::CreateFailed {
- result: error.to_string(),
- },
- )?;
- let capabilities =
- live_surface_capabilities(surface, device.physical_device, &device.report.device_name)
- .map_err(
- |error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
- result: error.to_string(),
- },
- )?;
- let plan = plan_vulkan_swapchain(&VulkanSwapchainRequest {
- drawable_extent,
- formats: surface_formats,
- present_modes,
- capabilities,
- preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
- })
- .map_err(|error| VulkanSwapchainProbeError::CreateFailed {
- result: error.to_string(),
- })?;
- let queue_family_indices = unique_queue_families(
- device.runtime.capability.graphics_queue_family,
- device.runtime.capability.present_queue_family,
- );
- let sharing_mode = if queue_family_indices.len() > 1 {
- vk::SharingMode::CONCURRENT
- } else {
- vk::SharingMode::EXCLUSIVE
- };
- let create_info = vk::SwapchainCreateInfoKHR::default()
- .surface(surface.surface)
- .min_image_count(plan.image_count)
- .image_format(vk::Format::from_raw(plan.format.format))
- .image_color_space(vk::ColorSpaceKHR::from_raw(plan.format.color_space))
- .image_extent(vk::Extent2D {
- width: plan.extent.0,
- height: plan.extent.1,
- })
- .image_array_layers(1)
- .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
- .image_sharing_mode(sharing_mode)
- .queue_family_indices(&queue_family_indices)
- .pre_transform(raw_capabilities.current_transform)
- .composite_alpha(select_composite_alpha(
- raw_capabilities.supported_composite_alpha,
- ))
- .present_mode(vk::PresentModeKHR::from_raw(plan.present_mode))
- .old_swapchain(old_swapchain)
- .clipped(true);
- let loader = swapchain::Device::new(&instance.instance, &device.device);
- // SAFETY: The create info references live instance/device/surface handles for this call.
- let swapchain = unsafe { loader.create_swapchain(&create_info, None) }.map_err(|error| {
- VulkanSwapchainProbeError::CreateFailed {
- result: format!("{error:?}"),
- }
- })?;
- // SAFETY: The swapchain was created above and the returned image handles are owned by it.
- let images = match unsafe { loader.get_swapchain_images(swapchain) } {
- Ok(images) => images,
- Err(error) => {
- // SAFETY: The swapchain was created above on this loader/device pair and is destroyed on setup failure.
- unsafe { loader.destroy_swapchain(swapchain, None) };
- return Err(VulkanSwapchainProbeError::ImagesFailed {
- result: format!("{error:?}"),
- });
- }
- };
- Ok(VulkanSwapchainProbe {
- loader,
- swapchain,
- report: VulkanSwapchainReport {
- schema: 1,
- plan,
- image_count: images.len().try_into().unwrap_or(u32::MAX),
- },
- })
-}
-
-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| {
- VulkanRuntimeCapabilityError::EnumerateDevicesFailed {
- result: format!("{error:?}"),
- }
- })?
- };
- let mut best: Option<LiveDeviceCandidate> = None;
- let mut last_error = None;
- for (index, device) in devices.iter().copied().enumerate() {
- let candidate = match live_device_candidate(instance, surface, device, index) {
- Ok(candidate) => candidate,
- Err(err) => {
- last_error = Some(err);
- continue;
- }
- };
- match &best {
- Some(existing)
- if compare_reports(&candidate.capability, &existing.capability)
- != std::cmp::Ordering::Greater => {}
- _ => best = Some(candidate),
- }
- }
- let best = best.ok_or_else(|| {
- last_error.unwrap_or(VulkanRuntimeCapabilityError::Capability(
- VulkanCapabilityError::NoPhysicalDevice,
- ))
- })?;
- let swapchain = plan_vulkan_swapchain(&VulkanSwapchainRequest {
- drawable_extent,
- formats: best.surface_formats,
- present_modes: best.present_modes,
- capabilities: best.surface_capabilities,
- preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
- })
- .map_err(VulkanRuntimeCapabilityError::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>,
- surface_capabilities: VulkanSwapchainSurfaceCapabilities,
-}
-
-fn live_device_candidate(
- instance: &VulkanInstanceProbe,
- surface: &VulkanSurfaceProbe,
- device: vk::PhysicalDevice,
- index: usize,
-) -> Result<LiveDeviceCandidate, VulkanRuntimeCapabilityError> {
- let properties = {
- // SAFETY: `device` was returned by this live instance and the result is copied by value.
- unsafe { instance.instance.get_physical_device_properties(device) }
- };
- let name = physical_device_name(&properties, index);
- let queue_properties = {
- // SAFETY: `device` was returned by this live instance and the result is owned by Rust.
- unsafe {
- instance
- .instance
- .get_physical_device_queue_family_properties(device)
- }
- };
- let extensions = live_device_extensions(instance, device, &name)?;
- let surface_formats = live_surface_formats(surface, device, &name)?;
- let present_modes = live_present_modes(surface, device, &name)?;
- let surface_capabilities = live_surface_capabilities(surface, device, &name)?;
- let queue_families = queue_properties
- .iter()
- .enumerate()
- .map(|(queue_index, properties)| {
- let index = u32::try_from(queue_index).unwrap_or(u32::MAX);
- let present = {
- // SAFETY: The physical device, surface and queue-family index are live query inputs.
- unsafe {
- surface.loader.get_physical_device_surface_support(
- device,
- index,
- surface.surface,
- )
- }
- }
- .map_err(|error| VulkanRuntimeCapabilityError::PresentSupportFailed {
- device: name.clone(),
- queue_family: index,
- result: format!("{error:?}"),
- })?;
- Ok(VulkanQueueFamily {
- index,
- graphics: properties.queue_flags.contains(vk::QueueFlags::GRAPHICS),
- present,
- })
- })
- .collect::<Result<Vec<_>, VulkanRuntimeCapabilityError>>()?;
- let record = VulkanPhysicalDeviceRecord {
- name,
- api_version: properties.api_version,
- device_type: match properties.device_type {
- vk::PhysicalDeviceType::DISCRETE_GPU => VulkanDeviceType::DiscreteGpu,
- vk::PhysicalDeviceType::INTEGRATED_GPU => VulkanDeviceType::IntegratedGpu,
- vk::PhysicalDeviceType::CPU => VulkanDeviceType::Cpu,
- _ => VulkanDeviceType::Other,
- },
- extensions,
- queue_families,
- surface_formats: surface_formats.clone(),
- };
- let capability = validate_device(&record).map_err(VulkanRuntimeCapabilityError::Capability)?;
- Ok(LiveDeviceCandidate {
- physical_device: device,
- capability,
- surface_formats,
- present_modes,
- surface_capabilities,
- })
-}
-
-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()) }
- .to_string_lossy()
- .trim()
- .to_string();
- if name.is_empty() {
- format!("physical-device-{index}")
- } else {
- name
- }
-}
-
-fn live_device_extensions(
- instance: &VulkanInstanceProbe,
- device: vk::PhysicalDevice,
- name: &str,
-) -> Result<Vec<String>, VulkanRuntimeCapabilityError> {
- let properties = {
- // SAFETY: `device` was returned by this live instance and no borrowed data escapes.
- unsafe {
- instance
- .instance
- .enumerate_device_extension_properties(device)
- }
- }
- .map_err(
- |error| VulkanRuntimeCapabilityError::EnumerateDeviceExtensionsFailed {
- device: name.to_string(),
- result: format!("{error:?}"),
- },
- )?;
- let mut extensions = properties
- .iter()
- .map(|property| {
- // SAFETY: Vulkan extension names are fixed-size NUL-terminated C strings per the spec.
- unsafe { CStr::from_ptr(property.extension_name.as_ptr()) }
- .to_string_lossy()
- .into_owned()
- })
- .collect::<Vec<_>>();
- extensions.sort();
- extensions.dedup();
- Ok(extensions)
-}
-
-fn live_surface_formats(
- surface: &VulkanSurfaceProbe,
- device: vk::PhysicalDevice,
- name: &str,
-) -> Result<Vec<VulkanSurfaceFormat>, VulkanRuntimeCapabilityError> {
- let formats = {
- // SAFETY: The physical device and surface are live query inputs and no handles are retained.
- unsafe {
- surface
- .loader
- .get_physical_device_surface_formats(device, surface.surface)
- }
- }
- .map_err(|error| VulkanRuntimeCapabilityError::SurfaceFormatsFailed {
- device: name.to_string(),
- result: format!("{error:?}"),
- })?;
- Ok(formats
- .into_iter()
- .map(|format| VulkanSurfaceFormat {
- format: format.format.as_raw(),
- color_space: format.color_space.as_raw(),
- })
- .collect())
-}
-
-fn live_present_modes(
- surface: &VulkanSurfaceProbe,
- device: vk::PhysicalDevice,
- name: &str,
-) -> Result<Vec<i32>, VulkanRuntimeCapabilityError> {
- let modes = {
- // SAFETY: The physical device and surface are live query inputs and no handles are retained.
- unsafe {
- surface
- .loader
- .get_physical_device_surface_present_modes(device, surface.surface)
- }
- }
- .map_err(|error| VulkanRuntimeCapabilityError::PresentModesFailed {
- device: name.to_string(),
- result: format!("{error:?}"),
- })?;
- Ok(modes.into_iter().map(vk::PresentModeKHR::as_raw).collect())
-}
-
-fn live_surface_capabilities(
- surface: &VulkanSurfaceProbe,
- device: vk::PhysicalDevice,
- name: &str,
-) -> Result<VulkanSwapchainSurfaceCapabilities, VulkanRuntimeCapabilityError> {
- let capabilities = {
- // SAFETY: The physical device and surface are live query inputs and no handles are retained.
- unsafe {
- surface
- .loader
- .get_physical_device_surface_capabilities(device, surface.surface)
- }
- }
- .map_err(
- |error| VulkanRuntimeCapabilityError::SurfaceCapabilitiesFailed {
- device: name.to_string(),
- result: format!("{error:?}"),
- },
- )?;
- Ok(VulkanSwapchainSurfaceCapabilities {
- current_extent: if capabilities.current_extent.width == u32::MAX {
- None
- } else {
- Some((
- capabilities.current_extent.width,
- capabilities.current_extent.height,
- ))
- },
- min_extent: (
- capabilities.min_image_extent.width,
- capabilities.min_image_extent.height,
- ),
- max_extent: (
- capabilities.max_image_extent.width,
- capabilities.max_image_extent.height,
- ),
- min_image_count: capabilities.min_image_count,
- max_image_count: capabilities.max_image_count,
- })
-}
-
-/// Renders a deterministic JSON Vulkan surface plan.
-#[must_use]
-pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&plan.schema.to_string());
- out.push_str(",\"required_instance_extensions\":[");
- for (index, extension) in plan.required_instance_extensions.iter().enumerate() {
- if index > 0 {
- out.push(',');
- }
- push_json_string(&mut out, extension);
- }
- out.push_str("]}");
- out
-}
-
-fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
- // SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
- let name = unsafe { CStr::from_ptr(extension) };
- name.to_str()
- .map(str::to_string)
- .map_err(|_| VulkanSurfaceError::InvalidExtensionName)
-}
-
-/// Vulkan instance bootstrap error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanInstanceError {
- /// The Vulkan loader could not be opened.
- Loader(VulkanLoaderError),
- /// Application name contained an interior NUL byte.
- InvalidApplicationName,
- /// An extension name contained an interior NUL byte.
- InvalidExtensionName {
- /// Invalid extension name.
- extension: String,
- },
- /// Validation layers were requested but unavailable.
- MissingValidationLayer,
- /// Instance creation failed.
- CreateFailed {
- /// Vulkan result.
- result: String,
- },
-}
-
-impl std::fmt::Display for VulkanInstanceError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Loader(error) => write!(f, "{error}"),
- Self::InvalidApplicationName => {
- write!(f, "Vulkan application name contains an interior NUL byte")
- }
- Self::InvalidExtensionName { extension } => {
- write!(
- f,
- "Vulkan instance extension name contains an interior NUL byte: {extension:?}"
- )
- }
- Self::MissingValidationLayer => {
- write!(
- f,
- "Vulkan validation layer VK_LAYER_KHRONOS_validation is unavailable"
- )
- }
- Self::CreateFailed { result } => write!(f, "Vulkan instance creation failed: {result}"),
- }
- }
-}
-
-impl std::error::Error for VulkanInstanceError {}
-
-/// Builds the deterministic instance creation plan without touching the loader.
-#[must_use]
-pub fn plan_vulkan_instance(config: &VulkanInstanceConfig) -> VulkanInstancePlan {
- let mut enabled_extensions = config.required_extensions.clone();
- if config.enable_validation
- && !enabled_extensions
- .iter()
- .any(|extension| extension == EXT_DEBUG_UTILS_EXTENSION)
- {
- enabled_extensions.push(EXT_DEBUG_UTILS_EXTENSION.to_string());
- }
- if config.enable_portability_enumeration
- && !enabled_extensions
- .iter()
- .any(|extension| extension == KHR_PORTABILITY_ENUMERATION_EXTENSION)
- {
- enabled_extensions.push(KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string());
- }
- enabled_extensions.sort();
- enabled_extensions.dedup();
- VulkanInstancePlan {
- schema: 1,
- enabled_extensions,
- create_flags: if config.enable_portability_enumeration {
- vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR.as_raw()
- } else {
- 0
- },
- validation_requested: config.enable_validation,
- }
-}
-
-/// Creates a Vulkan instance probe from the supplied configuration.
-///
-/// # Errors
-///
-/// Returns [`VulkanInstanceError`] when the loader is unavailable, names are not
-/// valid C strings, or `vkCreateInstance` fails.
-pub fn create_vulkan_instance_probe(
- config: &VulkanInstanceConfig,
-) -> Result<VulkanInstanceProbe, VulkanInstanceError> {
- // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
- let entry = unsafe { ash::Entry::load() }.map_err(|error| {
- VulkanInstanceError::Loader(VulkanLoaderError::Unavailable {
- message: error.to_string(),
- })
- })?;
- let app_name = CString::new(config.application_name.clone())
- .map_err(|_| VulkanInstanceError::InvalidApplicationName)?;
- let engine_name = c"fparkan";
- let plan = plan_vulkan_instance(config);
- let extension_names = cstring_vec(&plan.enabled_extensions)?;
- let extension_ptrs = cstring_ptrs(&extension_names);
- let layer_names = validation_layer_cstrings(&entry, config.enable_validation)?;
- let layer_ptrs = cstring_ptrs(&layer_names);
- let app_info = vk::ApplicationInfo::default()
- .application_name(&app_name)
- .application_version(0)
- .engine_name(engine_name)
- .engine_version(0)
- .api_version(MIN_VULKAN_API_VERSION);
- let create_info = vk::InstanceCreateInfo::default()
- .application_info(&app_info)
- .enabled_extension_names(&extension_ptrs)
- .enabled_layer_names(&layer_ptrs)
- .flags(vk::InstanceCreateFlags::from_raw(plan.create_flags));
- // SAFETY: `create_info` points to stack-owned Vulkan create data that lives for the call.
- let instance = unsafe { entry.create_instance(&create_info, None) }.map_err(|error| {
- VulkanInstanceError::CreateFailed {
- result: format!("{error:?}"),
- }
- })?;
- Ok(VulkanInstanceProbe {
- entry,
- instance,
- report: plan,
- })
-}
-
-fn validation_layer_cstrings(
- entry: &ash::Entry,
- enable_validation: bool,
-) -> Result<Vec<CString>, VulkanInstanceError> {
- if !enable_validation {
- return Ok(Vec::new());
- }
- let available_layers =
- // SAFETY: Enumerating instance layers reads loader-owned immutable metadata.
- unsafe { entry.enumerate_instance_layer_properties() }.map_err(|error| {
- VulkanInstanceError::CreateFailed {
- result: format!("{error:?}"),
- }
- })?;
- let validation_available = available_layers.iter().any(|layer| {
- // SAFETY: Vulkan layer names are fixed-size NUL-terminated strings from the loader.
- unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) }
- .to_string_lossy()
- .as_ref()
- == VALIDATION_LAYER_NAME
- });
- if !validation_available {
- return Err(VulkanInstanceError::MissingValidationLayer);
- }
- Ok(vec![CString::new(VALIDATION_LAYER_NAME).map_err(|_| {
- VulkanInstanceError::InvalidApplicationName
- })?])
-}
-
-/// Renders a deterministic JSON Vulkan instance plan.
-#[must_use]
-pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&plan.schema.to_string());
- out.push_str(",\"create_flags\":");
- out.push_str(&plan.create_flags.to_string());
- out.push_str(",\"validation_requested\":");
- out.push_str(if plan.validation_requested {
- "true"
- } else {
- "false"
- });
- out.push_str(",\"enabled_extensions\":[");
- for (index, extension) in plan.enabled_extensions.iter().enumerate() {
- if index > 0 {
- out.push(',');
- }
- push_json_string(&mut out, extension);
- }
- out.push_str("]}");
- out
-}
-
-fn cstring_vec(values: &[String]) -> Result<Vec<CString>, VulkanInstanceError> {
- values
- .iter()
- .map(|extension| {
- CString::new(extension.as_str()).map_err(|_| {
- VulkanInstanceError::InvalidExtensionName {
- extension: extension.clone(),
- }
- })
- })
- .collect()
-}
-
-fn cstring_ptrs(values: &[CString]) -> Vec<*const c_char> {
- values.iter().map(|value| value.as_ptr()).collect()
-}
-
-/// Deterministic Vulkan loader probe report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanLoaderProbeReport {
- /// Report schema version.
- pub schema: u32,
- /// Whether the Vulkan loader was opened successfully.
- pub loader_available: bool,
- /// Reported loader instance API version.
- pub instance_api_version: u32,
-}
-
-/// Vulkan loader bootstrap error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanLoaderError {
- /// The Vulkan loader library could not be opened.
- Unavailable {
- /// Loader error text.
- message: String,
- },
-}
-
-impl std::fmt::Display for VulkanLoaderError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Unavailable { message } => {
- write!(f, "Vulkan loader is unavailable: {message}")
- }
- }
- }
-}
-
-impl std::error::Error for VulkanLoaderError {}
-
-/// Opens the Vulkan loader and reports the supported instance API version.
-///
-/// # Errors
-///
-/// Returns [`VulkanLoaderError`] when no Vulkan loader library can be opened on
-/// the host.
-pub fn probe_vulkan_loader() -> Result<VulkanLoaderProbeReport, VulkanLoaderError> {
- // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
- let entry = unsafe { ash::Entry::load() }.map_err(|error| VulkanLoaderError::Unavailable {
- message: error.to_string(),
- })?;
- // SAFETY: The resolved entry only queries the loader-supported instance API version.
- let version = unsafe { entry.try_enumerate_instance_version() }
- .map_err(|error| VulkanLoaderError::Unavailable {
- message: error.to_string(),
- })?
- .unwrap_or(vk::API_VERSION_1_0);
- Ok(VulkanLoaderProbeReport {
- schema: 1,
- loader_available: true,
- instance_api_version: version,
- })
-}
-
-/// Returns the static Vulkan entry name used by loader probes.
-#[must_use]
-pub fn vulkan_entry_symbol_name() -> &'static CStr {
- c"vkGetInstanceProcAddr"
-}
-
-/// Renders a deterministic JSON Vulkan loader report.
-#[must_use]
-pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&report.schema.to_string());
- out.push_str(",\"loader_available\":");
- out.push_str(if report.loader_available {
- "true"
- } else {
- "false"
- });
- out.push_str(",\"instance_api\":\"");
- out.push_str(&format_api_version(report.instance_api_version));
- out.push_str("\"}");
- out
-}
-
-/// Returns the built-in Stage 0 indexed-triangle shader manifest.
-#[must_use]
-pub fn triangle_shader_manifest() -> Vec<VulkanShaderModuleManifest> {
- vec![
- VulkanShaderModuleManifest {
- name: "triangle.vert",
- stage: VulkanShaderStage::Vertex,
- entry_point: "main",
- descriptor_sets: 0,
- push_constant_bytes: 0,
- source_path: TRIANGLE_VERTEX_SOURCE_PATH,
- source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
- spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
- compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
- validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
- words: TRIANGLE_VERTEX_SHADER_WORDS,
- },
- VulkanShaderModuleManifest {
- name: "triangle.frag",
- stage: VulkanShaderStage::Fragment,
- entry_point: "main",
- descriptor_sets: 0,
- push_constant_bytes: 0,
- source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
- source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
- spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
- compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
- validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
- words: TRIANGLE_FRAGMENT_SHADER_WORDS,
- },
- ]
-}
-
-/// Validates shader SPIR-V containers and renders a deterministic report.
-///
-/// # Errors
-///
-/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
-/// container validation.
-pub fn validate_shader_manifest(
- modules: &[VulkanShaderModuleManifest],
-) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
- let mut reports = Vec::with_capacity(modules.len());
- for module in modules {
- validate_spirv_container(module)?;
- let bytes = spirv_words_to_bytes(module.words);
- reports.push(VulkanShaderModuleReport {
- name: module.name,
- stage: module.stage,
- entry_point: module.entry_point,
- source_path: module.source_path,
- source_sha256: module.source_sha256,
- spirv_path: module.spirv_path,
- word_count: module.words.len(),
- sha256: sha256_hex(&sha256(&bytes)),
- descriptor_sets: module.descriptor_sets,
- push_constant_bytes: module.push_constant_bytes,
- compile_command: module.compile_command,
- validate_command: module.validate_command,
- interface_hash: shader_interface_hash(module),
- });
- }
- let normalized = render_shader_manifest_without_hash_json(&reports);
- Ok(VulkanShaderManifestReport {
- schema: SHADER_MANIFEST_SCHEMA,
- target_env: SHADER_TARGET_ENV,
- compiler: VulkanShaderToolManifest {
- name: SHADER_COMPILER_NAME,
- version: SHADER_COMPILER_VERSION,
- binary_sha256: SHADER_COMPILER_BINARY_SHA256,
- },
- validator: VulkanShaderToolManifest {
- name: SPIRV_VALIDATOR_NAME,
- version: SPIRV_VALIDATOR_VERSION,
- binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
- },
- modules: reports,
- manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
- })
-}
-
-fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
- let mut normalized = String::new();
- normalized.push_str("{\"stage\":\"");
- normalized.push_str(module.stage.as_str());
- normalized.push_str("\",\"entry_point\":");
- push_json_string(&mut normalized, module.entry_point);
- normalized.push_str(",\"descriptor_sets\":");
- normalized.push_str(&module.descriptor_sets.to_string());
- normalized.push_str(",\"push_constant_bytes\":");
- normalized.push_str(&module.push_constant_bytes.to_string());
- normalized.push('}');
- sha256_hex(&sha256(normalized.as_bytes()))
-}
-
-fn validate_spirv_container(
- module: &VulkanShaderModuleManifest,
-) -> Result<(), VulkanShaderManifestError> {
- if module.words.len() < 5 {
- return Err(VulkanShaderManifestError::TooShort { name: module.name });
- }
- if module.words[0] != SPIRV_MAGIC {
- return Err(VulkanShaderManifestError::InvalidMagic {
- name: module.name,
- found: module.words[0],
- });
- }
- if module.words[1] < SPIRV_VERSION_1_0 {
- return Err(VulkanShaderManifestError::UnsupportedVersion {
- name: module.name,
- found: module.words[1],
- });
- }
- if module.words[3] == 0 {
- return Err(VulkanShaderManifestError::InvalidBound { name: module.name });
- }
- Ok(())
-}
-
-fn spirv_words_to_bytes(words: &[u32]) -> Vec<u8> {
- let mut out = Vec::with_capacity(words.len() * 4);
- for word in words {
- out.extend_from_slice(&word.to_le_bytes());
- }
- out
-}
-
-/// Renders a deterministic JSON shader manifest report.
-#[must_use]
-pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
- let mut out = render_shader_manifest_without_hash_json(&report.modules);
- out.push_str(",\"manifest_hash\":");
- push_json_string(&mut out, &report.manifest_hash);
- out.push('}');
- out
-}
-
-fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&SHADER_MANIFEST_SCHEMA.to_string());
- out.push_str(",\"target_env\":");
- push_json_string(&mut out, SHADER_TARGET_ENV);
- out.push_str(",\"compiler\":");
- out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
- name: SHADER_COMPILER_NAME,
- version: SHADER_COMPILER_VERSION,
- binary_sha256: SHADER_COMPILER_BINARY_SHA256,
- }));
- out.push_str(",\"validator\":");
- out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
- name: SPIRV_VALIDATOR_NAME,
- version: SPIRV_VALIDATOR_VERSION,
- binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
- }));
- out.push_str(",\"modules\":");
- out.push_str(&render_shader_modules_json(modules));
- out
-}
-
-fn render_shader_modules_json(modules: &[VulkanShaderModuleReport]) -> String {
- let mut out = String::new();
- out.push('[');
- for (index, module) in modules.iter().enumerate() {
- if index > 0 {
- out.push(',');
- }
- out.push_str("{\"name\":");
- push_json_string(&mut out, module.name);
- out.push_str(",\"stage\":\"");
- out.push_str(module.stage.as_str());
- out.push_str("\",\"entry_point\":");
- push_json_string(&mut out, module.entry_point);
- out.push_str(",\"source_path\":");
- push_json_string(&mut out, module.source_path);
- out.push_str(",\"source_sha256\":");
- push_json_string(&mut out, module.source_sha256);
- out.push_str(",\"spirv_path\":");
- push_json_string(&mut out, module.spirv_path);
- out.push_str(",\"word_count\":");
- out.push_str(&module.word_count.to_string());
- out.push_str(",\"sha256\":");
- push_json_string(&mut out, &module.sha256);
- out.push_str(",\"descriptor_sets\":");
- out.push_str(&module.descriptor_sets.to_string());
- out.push_str(",\"push_constant_bytes\":");
- out.push_str(&module.push_constant_bytes.to_string());
- out.push_str(",\"compile_command\":");
- push_json_string(&mut out, module.compile_command);
- out.push_str(",\"validate_command\":");
- push_json_string(&mut out, module.validate_command);
- out.push_str(",\"interface_hash\":");
- push_json_string(&mut out, &module.interface_hash);
- out.push('}');
- }
- out.push(']');
- out
-}
-
-fn render_shader_tool_json(tool: &VulkanShaderToolManifest) -> String {
- let mut out = String::new();
- out.push_str("{\"name\":");
- push_json_string(&mut out, tool.name);
- out.push_str(",\"version\":");
- push_json_string(&mut out, tool.version);
- out.push_str(",\"binary_sha256\":");
- push_json_string(&mut out, tool.binary_sha256);
- out.push('}');
- out
-}
-
-/// Vulkan backend migration readiness.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum VulkanPlanningBackendState {
- /// Adapter prepared and able to accept commands.
- Ready,
- /// Adapter is tracking a recoverable runtime surface/depth pipeline fault.
- Degraded,
- /// Adapter has encountered a non-recoverable error.
- Error,
-}
-
-impl Default for VulkanPlanningBackendState {
- fn default() -> Self {
- Self::Degraded
- }
-}
-
-/// 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,
-}
-
-/// Surface capabilities needed by the Stage 0 swapchain policy.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct VulkanSwapchainSurfaceCapabilities {
- /// Current surface extent, when dictated by the platform.
- pub current_extent: Option<(u32, u32)>,
- /// Minimum supported swapchain extent.
- pub min_extent: (u32, u32),
- /// Maximum supported swapchain extent.
- pub max_extent: (u32, u32),
- /// Minimum supported image count.
- pub min_image_count: u32,
- /// Maximum supported image count, or 0 when unbounded.
- pub max_image_count: u32,
-}
-
-/// Deterministic swapchain planning input.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSwapchainRequest {
- /// Requested drawable extent.
- pub drawable_extent: (u32, u32),
- /// Available surface formats.
- pub formats: Vec<VulkanSurfaceFormat>,
- /// Available present modes as raw Vulkan values.
- pub present_modes: Vec<i32>,
- /// Surface capabilities.
- pub capabilities: VulkanSwapchainSurfaceCapabilities,
- /// Preferred present mode.
- pub preferred_present_mode: i32,
-}
-
-/// Deterministic swapchain plan.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSwapchainPlan {
- /// Report schema version.
- pub schema: u32,
- /// Selected swapchain extent.
- pub extent: (u32, u32),
- /// Selected surface format.
- pub format: VulkanSurfaceFormat,
- /// Selected present mode raw Vulkan value.
- pub present_mode: i32,
- /// Selected image count.
- pub image_count: u32,
-}
-
-/// Swapchain planning error.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum VulkanSwapchainError {
- /// No surface format was available.
- MissingSurfaceFormat,
- /// No present mode was available.
- MissingPresentMode,
- /// Requested or current extent is empty.
- EmptyExtent,
-}
-
-impl std::fmt::Display for VulkanSwapchainError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::MissingSurfaceFormat => write!(f, "Vulkan swapchain has no surface format"),
- Self::MissingPresentMode => write!(f, "Vulkan swapchain has no present mode"),
- Self::EmptyExtent => write!(f, "Vulkan swapchain extent must be non-zero"),
- }
- }
-}
-
-impl std::error::Error for VulkanSwapchainError {}
-
-/// Swapchain recreation reason.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum VulkanSwapchainRecreationReason {
- /// Drawable extent changed.
- Resize,
- /// Vulkan reported `VK_ERROR_OUT_OF_DATE_KHR`.
- OutOfDate,
- /// Vulkan reported `VK_SUBOPTIMAL_KHR`.
- Suboptimal,
-}
-
-/// Deterministic swapchain recreation report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanSwapchainRecreationReport {
- /// Report schema version.
- pub schema: u32,
- /// Recreation reason.
- pub reason: VulkanSwapchainRecreationReason,
- /// Previous extent.
- pub previous_extent: (u32, u32),
- /// Next extent.
- pub next_extent: (u32, u32),
-}
-
-/// Deterministic frame submission plan for command buffers and sync objects.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct VulkanFrameSubmissionPlan {
- /// Report schema version.
- pub schema: u32,
- /// Frames allowed in flight.
- pub frames_in_flight: u32,
- /// Swapchain-backed primary command buffers.
- pub command_buffers: u32,
- /// Binary semaphores allocated per frame.
- pub semaphores_per_frame: u32,
- /// Fences allocated per frame.
- pub fences_per_frame: u32,
- /// Draw commands encoded into the frame.
- pub draw_count: u32,
- /// Total indexed vertices submitted by draw commands.
- pub indexed_vertex_count: u32,
-}
-
-/// 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;
- let mut last_error = None;
- for device in devices {
- let report = match validate_device(device) {
- Ok(report) => report,
- Err(err) => {
- last_error = Some(err);
- continue;
- }
- };
- match &best {
- Some(existing) if compare_reports(&report, existing) != std::cmp::Ordering::Greater => {
- }
- _ => best = Some(report),
- }
- }
- best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))
-}
-
-/// Builds a deterministic swapchain plan from surface capabilities.
-///
-/// # Errors
-///
-/// Returns [`VulkanSwapchainError`] when formats, present modes or extent are
-/// unusable.
-pub fn plan_vulkan_swapchain(
- request: &VulkanSwapchainRequest,
-) -> Result<VulkanSwapchainPlan, VulkanSwapchainError> {
- let format = select_surface_format(&request.formats)?;
- let present_mode = select_present_mode(&request.present_modes, request.preferred_present_mode)?;
- let extent = select_swapchain_extent(request)?;
- if extent.0 == 0 || extent.1 == 0 {
- return Err(VulkanSwapchainError::EmptyExtent);
- }
- Ok(VulkanSwapchainPlan {
- schema: 1,
- extent,
- format,
- present_mode,
- image_count: select_image_count(request.capabilities),
- })
-}
-
-fn select_surface_format(
- formats: &[VulkanSurfaceFormat],
-) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
- formats
- .iter()
- .copied()
- .find(|format| {
- format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
- && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
- })
- .or_else(|| formats.first().copied())
- .ok_or(VulkanSwapchainError::MissingSurfaceFormat)
-}
-
-fn select_present_mode(present_modes: &[i32], preferred: i32) -> Result<i32, VulkanSwapchainError> {
- if present_modes.contains(&preferred) {
- Ok(preferred)
- } else if present_modes.contains(&vk::PresentModeKHR::FIFO.as_raw()) {
- Ok(vk::PresentModeKHR::FIFO.as_raw())
- } else {
- present_modes
- .first()
- .copied()
- .ok_or(VulkanSwapchainError::MissingPresentMode)
- }
-}
-
-fn select_swapchain_extent(
- request: &VulkanSwapchainRequest,
-) -> Result<(u32, u32), VulkanSwapchainError> {
- if let Some(extent) = request.capabilities.current_extent {
- return if extent.0 == 0 || extent.1 == 0 {
- Err(VulkanSwapchainError::EmptyExtent)
- } else {
- Ok(extent)
- };
- }
- let width = request.drawable_extent.0.clamp(
- request.capabilities.min_extent.0,
- request.capabilities.max_extent.0,
- );
- let height = request.drawable_extent.1.clamp(
- request.capabilities.min_extent.1,
- request.capabilities.max_extent.1,
- );
- Ok((width, height))
-}
-
-fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
- let requested = capabilities.min_image_count.saturating_add(1).max(2);
- if capabilities.max_image_count == 0 {
- requested
- } else {
- requested.min(capabilities.max_image_count)
- }
-}
-
-fn select_composite_alpha(supported: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR {
- if supported.contains(vk::CompositeAlphaFlagsKHR::OPAQUE) {
- vk::CompositeAlphaFlagsKHR::OPAQUE
- } else if supported.contains(vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED) {
- vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED
- } else if supported.contains(vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED) {
- vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED
- } else {
- vk::CompositeAlphaFlagsKHR::INHERIT
- }
-}
-
-/// Builds a deterministic swapchain recreation report.
-#[must_use]
-pub const fn swapchain_recreation_report(
- reason: VulkanSwapchainRecreationReason,
- previous_extent: (u32, u32),
- next_extent: (u32, u32),
-) -> VulkanSwapchainRecreationReport {
- VulkanSwapchainRecreationReport {
- schema: 1,
- reason,
- previous_extent,
- next_extent,
- }
-}
-
-/// Builds a deterministic frame submission plan for a validated command list.
-///
-/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer
-/// and synchronization policy can be tested without requiring a native surface.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when the command list has invalid frame framing,
-/// ordering, draw ranges, mesh bounds, or non-finite transforms.
-pub fn plan_vulkan_frame_submission(
- swapchain: &VulkanSwapchainPlan,
- commands: &RenderCommandList,
-) -> Result<VulkanFrameSubmissionPlan, RenderError> {
- validate_command_list(commands)?;
- let mut draw_count = 0_u32;
- let mut indexed_vertex_count = 0_u32;
- for command in &commands.commands {
- if let RenderCommand::Draw(draw) = command {
- draw_count = draw_count.saturating_add(1);
- indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count);
- }
- }
- Ok(VulkanFrameSubmissionPlan {
- schema: 1,
- frames_in_flight: swapchain.image_count.clamp(1, 2),
- command_buffers: swapchain.image_count,
- semaphores_per_frame: 2,
- fences_per_frame: 1,
- draw_count,
- indexed_vertex_count,
- })
-}
-
-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, present_queue_family) = select_queue_families(device)?;
-
- 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 select_queue_families(
- device: &VulkanPhysicalDeviceRecord,
-) -> Result<(u32, u32), VulkanCapabilityError> {
- if let Some(unified) = device
- .queue_families
- .iter()
- .find(|family| family.graphics && family.present)
- {
- return Ok((unified.index, unified.index));
- }
-
- 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;
- Ok((graphics_queue_family, present_queue_family))
-}
-
-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
-}
-
-/// Renders a deterministic JSON swapchain plan.
-#[must_use]
-pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&plan.schema.to_string());
- out.push_str(",\"extent\":[");
- out.push_str(&plan.extent.0.to_string());
- out.push(',');
- out.push_str(&plan.extent.1.to_string());
- out.push_str("],\"format\":");
- out.push_str(&plan.format.format.to_string());
- out.push_str(",\"color_space\":");
- out.push_str(&plan.format.color_space.to_string());
- out.push_str(",\"present_mode\":");
- out.push_str(&plan.present_mode.to_string());
- out.push_str(",\"image_count\":");
- out.push_str(&plan.image_count.to_string());
- out.push('}');
- out
-}
-
-/// Renders a deterministic JSON swapchain recreation report.
-#[must_use]
-pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&report.schema.to_string());
- out.push_str(",\"reason\":\"");
- out.push_str(match report.reason {
- VulkanSwapchainRecreationReason::Resize => "resize",
- VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
- VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
- });
- out.push_str("\",\"previous_extent\":[");
- out.push_str(&report.previous_extent.0.to_string());
- out.push(',');
- out.push_str(&report.previous_extent.1.to_string());
- out.push_str("],\"next_extent\":[");
- out.push_str(&report.next_extent.0.to_string());
- out.push(',');
- out.push_str(&report.next_extent.1.to_string());
- out.push_str("]}");
- out
-}
-
-/// Renders a deterministic JSON frame submission plan.
-#[must_use]
-pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
- let mut out = String::new();
- out.push_str("{\"schema\":");
- out.push_str(&plan.schema.to_string());
- out.push_str(",\"frames_in_flight\":");
- out.push_str(&plan.frames_in_flight.to_string());
- out.push_str(",\"command_buffers\":");
- out.push_str(&plan.command_buffers.to_string());
- out.push_str(",\"semaphores_per_frame\":");
- out.push_str(&plan.semaphores_per_frame.to_string());
- out.push_str(",\"fences_per_frame\":");
- out.push_str(&plan.fences_per_frame.to_string());
- out.push_str(",\"draw_count\":");
- out.push_str(&plan.draw_count.to_string());
- out.push_str(",\"indexed_vertex_count\":");
- out.push_str(&plan.indexed_vertex_count.to_string());
- out.push('}');
- 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 planning backend setup and frame progression.
-#[derive(Clone, Debug, PartialEq)]
-pub struct VulkanPlanningBackendReport {
- /// Total frames executed.
- pub frames_executed: u64,
- /// Total command submissions.
- pub submissions: u64,
- /// Last command-capture byte size.
- pub last_capture_size: usize,
- /// Number of simulated present calls issued by the planning facade.
- pub simulated_presents: u64,
- /// Number of resize-driven surface plan refreshes.
- pub resize_rebuilds: u64,
- /// Last render request observed.
- pub request: RenderRequest,
- /// Last deterministic frame submission plan.
- pub last_frame_submission: Option<VulkanFrameSubmissionPlan>,
-}
-
-impl Default for VulkanPlanningBackendReport {
- fn default() -> Self {
- Self {
- frames_executed: 0,
- submissions: 0,
- last_capture_size: 0,
- simulated_presents: 0,
- resize_rebuilds: 0,
- request: RenderRequest::conservative(),
- last_frame_submission: None,
- }
- }
-}
-
-/// Vulkan planning backend façade used by the game entrypoint.
-#[derive(Debug)]
-pub struct VulkanPlanningBackend {
- state: VulkanPlanningBackendState,
- report: VulkanPlanningBackendReport,
- swapchain_plan: VulkanSwapchainPlan,
-}
-
-impl Default for VulkanPlanningBackend {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl VulkanPlanningBackend {
- /// Creates a new Vulkan planning backend façade.
- #[must_use]
- pub fn new() -> Self {
- Self {
- state: VulkanPlanningBackendState::Ready,
- report: VulkanPlanningBackendReport::default(),
- swapchain_plan: default_stage0_swapchain_plan(),
- }
- }
-
- /// Replaces active surface/profile request.
- pub fn set_render_request(&mut self, request: RenderRequest) {
- self.report.request = request;
- self.report.resize_rebuilds = self.report.resize_rebuilds.saturating_add(1);
- }
-
- /// Returns active render request policy.
- #[must_use]
- pub const fn render_request(&self) -> RenderRequest {
- self.report.request
- }
-
- /// Replaces active swapchain plan used for frame submission planning.
- pub fn set_swapchain_plan(&mut self, plan: VulkanSwapchainPlan) {
- self.swapchain_plan = plan;
- }
-
- /// Returns active swapchain plan.
- #[must_use]
- pub const fn swapchain_plan(&self) -> &VulkanSwapchainPlan {
- &self.swapchain_plan
- }
-
- /// Returns adapter state.
- #[must_use]
- pub const fn state(&self) -> VulkanPlanningBackendState {
- self.state
- }
-
- /// Returns backend report.
- #[must_use]
- pub fn report(&self) -> &VulkanPlanningBackendReport {
- &self.report
- }
-
- fn simulate_present(&mut self) {
- self.report.simulated_presents = self.report.simulated_presents.saturating_add(1);
- }
-}
-
-impl RenderBackend for VulkanPlanningBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- if !matches!(
- self.state,
- VulkanPlanningBackendState::Ready | VulkanPlanningBackendState::Degraded
- ) {
- return Err(RenderError::InvalidRange);
- }
- let capture = canonical_capture(commands)?;
- let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?;
- self.report.frames_executed = self.report.frames_executed.saturating_add(1);
- self.report.submissions = self.report.submissions.saturating_add(1);
- self.report.last_capture_size = capture.len();
- self.report.last_frame_submission = Some(frame_plan);
- self.simulate_present();
- Ok(FrameOutput)
- }
-}
-
-fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan {
- VulkanSwapchainPlan {
- schema: 1,
- extent: (1, 1),
- format: VulkanSurfaceFormat {
- format: vk::Format::B8G8R8A8_SRGB.as_raw(),
- color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
- },
- present_mode: vk::PresentModeKHR::FIFO.as_raw(),
- image_count: 2,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use fparkan_render::{
- DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
- };
-
- #[test]
- fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> {
- let mut backend = VulkanPlanningBackend::new();
- let request = RenderRequest::conservative();
- backend.set_render_request(request);
- assert_eq!(backend.render_request(), request);
- assert_eq!(backend.report().resize_rebuilds, 1);
-
- let commands = fparkan_render::RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(11),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(2),
- transform: [1.0; 16],
- range: IndexRange { start: 0, count: 3 },
- stable_order: 7,
- }),
- RenderCommand::EndFrame,
- ],
- };
-
- backend.execute(&commands)?;
- assert_eq!(backend.state(), VulkanPlanningBackendState::Ready);
- assert_eq!(backend.report().frames_executed, 1);
- assert_eq!(backend.report().submissions, 1);
- assert_eq!(backend.report().simulated_presents, 1);
- assert!(backend.report().last_capture_size > 0);
- assert_eq!(
- backend.report().last_frame_submission,
- Some(VulkanFrameSubmissionPlan {
- schema: 1,
- frames_in_flight: 2,
- command_buffers: 2,
- semaphores_per_frame: 2,
- fences_per_frame: 1,
- draw_count: 1,
- indexed_vertex_count: 3,
- })
- );
- Ok(())
- }
-
- #[test]
- fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> {
- let commands = fparkan_render::RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(11),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(2),
- transform: [1.0; 16],
- range: IndexRange { start: 0, count: 3 },
- stable_order: 7,
- }),
- RenderCommand::EndFrame,
- ],
- };
- let swapchain = VulkanSwapchainPlan {
- image_count: 3,
- ..default_stage0_swapchain_plan()
- };
-
- let plan = plan_vulkan_frame_submission(&swapchain, &commands)?;
-
- assert_eq!(plan.frames_in_flight, 2);
- assert_eq!(plan.command_buffers, 3);
- assert_eq!(plan.draw_count, 1);
- assert_eq!(plan.indexed_vertex_count, 3);
- assert_eq!(
- render_frame_submission_plan_json(&plan),
- "{\"schema\":1,\"frames_in_flight\":2,\"command_buffers\":3,\"semaphores_per_frame\":2,\"fences_per_frame\":1,\"draw_count\":1,\"indexed_vertex_count\":3}"
- );
- 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\"]}"
- );
- }
-
- #[test]
- fn loader_probe_report_json_is_stable() {
- assert_eq!(
- vulkan_entry_symbol_name().to_bytes(),
- b"vkGetInstanceProcAddr"
- );
- assert_eq!(
- render_loader_probe_report_json(&VulkanLoaderProbeReport {
- schema: 1,
- loader_available: true,
- instance_api_version: vk::API_VERSION_1_2,
- }),
- "{\"schema\":1,\"loader_available\":true,\"instance_api\":\"1.2.0\"}"
- );
- }
-
- #[test]
- fn loader_error_display_is_actionable() {
- assert_eq!(
- VulkanLoaderError::Unavailable {
- message: "dlopen failed".to_string(),
- }
- .to_string(),
- "Vulkan loader is unavailable: dlopen failed"
- );
- }
-
- #[test]
- fn instance_plan_is_sorted_deduplicated_and_portability_aware() {
- let plan = plan_vulkan_instance(&VulkanInstanceConfig {
- application_name: "FParkan".to_string(),
- required_extensions: vec![
- "VK_KHR_surface".to_string(),
- KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
- "VK_KHR_surface".to_string(),
- ],
- enable_portability_enumeration: true,
- enable_validation: true,
- });
-
- assert_eq!(
- render_instance_plan_json(&plan),
- "{\"schema\":1,\"create_flags\":1,\"validation_requested\":true,\"enabled_extensions\":[\"VK_EXT_debug_utils\",\"VK_KHR_portability_enumeration\",\"VK_KHR_surface\"]}"
- );
- }
-
- #[test]
- fn instance_plan_adds_portability_extension_when_requested() {
- let plan = plan_vulkan_instance(&VulkanInstanceConfig {
- application_name: "FParkan".to_string(),
- required_extensions: vec!["VK_KHR_surface".to_string()],
- enable_portability_enumeration: true,
- enable_validation: false,
- });
-
- assert_eq!(
- plan.enabled_extensions,
- vec![
- KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
- "VK_KHR_surface".to_string()
- ]
- );
- assert_eq!(plan.create_flags, 1);
- }
-
- #[test]
- fn invalid_instance_extension_name_is_reported_before_loader_use() {
- assert_eq!(
- cstring_vec(&["bad\0extension".to_string()]),
- Err(VulkanInstanceError::InvalidExtensionName {
- extension: "bad\0extension".to_string()
- })
- );
- }
-
- #[test]
- fn surface_plan_requires_native_handles() {
- assert_eq!(
- plan_vulkan_surface(None),
- Err(VulkanSurfaceError::MissingNativeHandles)
- );
- assert_eq!(
- VulkanSurfaceError::MissingNativeHandles.to_string(),
- "native window/display handles are required for Vulkan surface creation"
- );
- }
-
- #[test]
- fn surface_plan_json_is_stable() {
- assert_eq!(
- render_surface_plan_json(&VulkanSurfacePlan {
- schema: 1,
- required_instance_extensions: vec![
- "VK_KHR_surface".to_string(),
- "VK_EXT_metal_surface".to_string(),
- ],
- }),
- "{\"schema\":1,\"required_instance_extensions\":[\"VK_KHR_surface\",\"VK_EXT_metal_surface\"]}"
- );
- }
-
- #[test]
- fn static_surface_extension_name_is_decoded() {
- let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
-
- assert_eq!(name, "VK_KHR_surface");
- }
-
- #[test]
- fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() {
- let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
-
- assert_eq!(
- plan.format,
- VulkanSurfaceFormat {
- format: vk::Format::B8G8R8A8_SRGB.as_raw(),
- color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
- }
- );
- assert_eq!(plan.present_mode, vk::PresentModeKHR::MAILBOX.as_raw());
- assert_eq!(plan.extent, (1024, 720));
- assert_eq!(plan.image_count, 3);
- }
-
- #[test]
- fn swapchain_plan_uses_fifo_and_current_extent_fallbacks() {
- let mut request = swapchain_request();
- request.preferred_present_mode = vk::PresentModeKHR::IMMEDIATE.as_raw();
- request.present_modes = vec![vk::PresentModeKHR::FIFO.as_raw()];
- request.capabilities.current_extent = Some((800, 600));
-
- let plan = plan_vulkan_swapchain(&request).expect("swapchain plan");
-
- assert_eq!(plan.present_mode, vk::PresentModeKHR::FIFO.as_raw());
- assert_eq!(plan.extent, (800, 600));
- }
-
- #[test]
- fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
- let mut request = swapchain_request();
- request.formats.clear();
- assert_eq!(
- plan_vulkan_swapchain(&request),
- Err(VulkanSwapchainError::MissingSurfaceFormat)
- );
-
- let mut request = swapchain_request();
- request.present_modes.clear();
- assert_eq!(
- plan_vulkan_swapchain(&request),
- Err(VulkanSwapchainError::MissingPresentMode)
- );
-
- let mut request = swapchain_request();
- request.capabilities.current_extent = Some((0, 600));
- assert_eq!(
- plan_vulkan_swapchain(&request),
- Err(VulkanSwapchainError::EmptyExtent)
- );
- }
-
- #[test]
- fn swapchain_plan_json_and_recreation_reports_are_stable() {
- let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
- assert_eq!(
- render_swapchain_plan_json(&plan),
- "{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3}"
- );
-
- let report = swapchain_recreation_report(
- VulkanSwapchainRecreationReason::OutOfDate,
- (1024, 720),
- (1280, 720),
- );
- assert_eq!(
- render_swapchain_recreation_report_json(&report),
- "{\"schema\":1,\"reason\":\"out_of_date\",\"previous_extent\":[1024,720],\"next_extent\":[1280,720]}"
- );
- }
-
- #[test]
- fn triangle_shader_manifest_hashes_are_stable() {
- let report =
- validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
-
- assert_eq!(report.schema, SHADER_MANIFEST_SCHEMA);
- assert_eq!(report.target_env, SHADER_TARGET_ENV);
- assert_eq!(
- report.compiler,
- VulkanShaderToolManifest {
- name: SHADER_COMPILER_NAME,
- version: SHADER_COMPILER_VERSION,
- binary_sha256: SHADER_COMPILER_BINARY_SHA256,
- }
- );
- assert_eq!(
- report.validator,
- VulkanShaderToolManifest {
- name: SPIRV_VALIDATOR_NAME,
- version: SPIRV_VALIDATOR_VERSION,
- binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
- }
- );
- assert_eq!(report.modules.len(), 2);
- assert_eq!(report.modules[0].name, "triangle.vert");
- assert_eq!(report.modules[0].stage, VulkanShaderStage::Vertex);
- assert_eq!(report.modules[0].source_path, TRIANGLE_VERTEX_SOURCE_PATH);
- assert_eq!(
- report.modules[0].source_sha256,
- TRIANGLE_VERTEX_SOURCE_SHA256
- );
- assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH);
- assert_eq!(report.modules[0].word_count, 253);
- assert_eq!(
- report.modules[0].sha256,
- "9023b1cc856c98ecd21755596c4e9d1e62cc63e1787f8c43ada2101544e8d0d1"
- );
- assert_eq!(report.modules[0].descriptor_sets, 0);
- assert_eq!(report.modules[0].push_constant_bytes, 0);
- assert_eq!(
- report.modules[0].compile_command,
- TRIANGLE_VERTEX_COMPILE_COMMAND
- );
- assert_eq!(
- report.modules[0].validate_command,
- TRIANGLE_VERTEX_VALIDATE_COMMAND
- );
- assert!(!report.modules[0].interface_hash.is_empty());
- assert_eq!(
- report.modules[1].sha256,
- "6efe2c9716ae845c471ecbaac2c83e56a17a37dc017dd63f0a05f0d9161f44ba"
- );
- assert_eq!(
- report.manifest_hash,
- "725529e9449fa53017e7df75f3f14c76d53479a5a7617d55ec78280b3059bc44"
- );
- }
-
- #[test]
- fn shader_manifest_report_json_is_stable() {
- let report =
- validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
- let json = render_shader_manifest_report_json(&report);
-
- assert!(json.contains(SHADER_COMPILER_NAME));
- assert!(json.contains(SPIRV_VALIDATOR_NAME));
- assert!(json.contains(TRIANGLE_VERTEX_SOURCE_PATH));
- assert!(json.contains(TRIANGLE_VERTEX_COMPILE_COMMAND));
- }
-
- #[test]
- fn checked_in_shader_manifest_matches_generated_report() {
- let report =
- validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
- assert_eq!(
- render_shader_manifest_report_json(&report),
- include_str!("../shaders/manifest.json").trim()
- );
- }
-
- #[test]
- fn shader_manifest_rejects_invalid_spirv_containers() {
- let mut module = triangle_shader_manifest().remove(0);
- module.words = &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0];
- assert_eq!(
- validate_shader_manifest(&[module]),
- Err(VulkanShaderManifestError::InvalidMagic {
- name: "triangle.vert",
- found: 0xFFFF_FFFF,
- })
- );
-
- let mut module = triangle_shader_manifest().remove(0);
- module.words = &[SPIRV_MAGIC, 0, 0, 1, 0];
- assert_eq!(
- validate_shader_manifest(&[module]),
- Err(VulkanShaderManifestError::UnsupportedVersion {
- name: "triangle.vert",
- found: 0,
- })
- );
-
- let mut module = triangle_shader_manifest().remove(0);
- module.words = &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0];
- assert_eq!(
- validate_shader_manifest(&[module]),
- Err(VulkanShaderManifestError::InvalidBound {
- name: "triangle.vert",
- })
- );
- }
-
- 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(),
- }],
- }
- }
-
- fn swapchain_request() -> VulkanSwapchainRequest {
- VulkanSwapchainRequest {
- drawable_extent: (1280, 720),
- formats: vec![
- VulkanSurfaceFormat {
- format: vk::Format::R8G8B8A8_UNORM.as_raw(),
- color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
- },
- VulkanSurfaceFormat {
- format: vk::Format::B8G8R8A8_SRGB.as_raw(),
- color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
- },
- ],
- present_modes: vec![
- vk::PresentModeKHR::FIFO.as_raw(),
- vk::PresentModeKHR::MAILBOX.as_raw(),
- ],
- capabilities: VulkanSwapchainSurfaceCapabilities {
- current_extent: None,
- min_extent: (320, 240),
- max_extent: (1024, 768),
- min_image_count: 2,
- max_image_count: 3,
- },
- preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
- }
- }
-}
+pub use ffi::*;
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 609cbfa..816e56b 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1240,7 +1240,7 @@ fn has_safety_comment(line: &str) -> bool {
line.contains("SAFETY:")
}
-const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &["adapters/fparkan-render-vulkan/src/lib.rs"];
+const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &["adapters/fparkan-render-vulkan/src/ffi.rs"];
fn is_audited_unsafe_source(path: &Path) -> bool {
let as_path = path.as_os_str().to_string_lossy();