From 0b0ed87650aa0bfb71ac680c3679b85546e4272b Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 25 Jun 2026 05:08:10 +0400 Subject: refactor(vulkan-ffi): narrow audited unsafe boundary --- adapters/fparkan-render-vulkan/src/ffi.rs | 4966 ++++++++++++++++++++++++++++ adapters/fparkan-render-vulkan/src/lib.rs | 4968 +---------------------------- xtask/src/main.rs | 2 +- 3 files changed, 4971 insertions(+), 4965 deletions(-) create mode 100644 adapters/fparkan-render-vulkan/src/ffi.rs 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, + /// 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, + /// 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) -> 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, + /// 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, +} + +/// 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, +} + +/// 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, +} + +/// 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>, +} + +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, +} + +impl VulkanValidationMessenger { + fn report(&self) -> VulkanValidationReport { + let vuids = self + .shared + .vuids + .lock() + .map(|values| values.iter().cloned().collect::>()) + .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, + render_pass: vk::RenderPass, + pipeline_layout: vk::PipelineLayout, + pipeline: vk::Pipeline, + framebuffers: Vec, + command_buffers: Vec, +} + +struct PartialSwapchainResources { + image_views: Vec, + render_pass: Option, + pipeline_layout: Option, + pipeline: Option, + framebuffers: Vec, + command_buffers: Vec, +} + +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, + validation: Option, + surface: Option, + device: Option, + swapchain: Option, + command_pool: vk::CommandPool, + swapchain_resources: Option, + vertex_buffer: Option, + index_buffer: Option, + frame_sync: Vec, + images_in_flight: Vec, + 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 { + 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 { + 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 { + 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 { + 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 { + 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::()); + 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 { + let indices = [0_u16, 1_u16, 2_u16]; + let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::()); + 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 { + 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::(), 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 { + // 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, 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, 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, +) -> Result { + 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, +) -> Result { + 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 { + 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 { + 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::>(); + 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::>(); + 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 { + 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 { + 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 { + 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 = 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, + present_modes: Vec, + surface_capabilities: VulkanSwapchainSurfaceCapabilities, +} + +fn live_device_candidate( + instance: &VulkanInstanceProbe, + surface: &VulkanSurfaceProbe, + device: vk::PhysicalDevice, + index: usize, +) -> Result { + 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::, 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 { + if graphics == present { + vec![graphics] + } else { + vec![graphics, present] + } +} + +fn device_extension_cstrings(values: &[String]) -> Result, 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, 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::>(); + extensions.sort(); + extensions.dedup(); + Ok(extensions) +} + +fn live_surface_formats( + surface: &VulkanSurfaceProbe, + device: vk::PhysicalDevice, + name: &str, +) -> Result, 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, 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 { + 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 { + // 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 { + // 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, 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, 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 { + // 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 { + 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 { + 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 { + 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, + /// Available present modes as raw Vulkan values. + pub present_modes: Vec, + /// 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, + /// Queue-family capabilities. + pub queue_families: Vec, + /// Surface formats accepted by the target surface. + pub surface_formats: Vec, +} + +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, +} + +/// 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, +} + +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 { + 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, - /// 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, - /// 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) -> 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, - /// 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, -} - -/// 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, -} - -/// 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, -} - -/// 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>, -} - -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, -} - -impl VulkanValidationMessenger { - fn report(&self) -> VulkanValidationReport { - let vuids = self - .shared - .vuids - .lock() - .map(|values| values.iter().cloned().collect::>()) - .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, - render_pass: vk::RenderPass, - pipeline_layout: vk::PipelineLayout, - pipeline: vk::Pipeline, - framebuffers: Vec, - command_buffers: Vec, -} - -struct PartialSwapchainResources { - image_views: Vec, - render_pass: Option, - pipeline_layout: Option, - pipeline: Option, - framebuffers: Vec, - command_buffers: Vec, -} - -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, - validation: Option, - surface: Option, - device: Option, - swapchain: Option, - command_pool: vk::CommandPool, - swapchain_resources: Option, - vertex_buffer: Option, - index_buffer: Option, - frame_sync: Vec, - images_in_flight: Vec, - 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 { - 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 { - 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 { - 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 { - 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 { - 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::()); - 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 { - let indices = [0_u16, 1_u16, 2_u16]; - let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::()); - 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 { - 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::(), 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 { - // 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 { - // 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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, 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, 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, -) -> Result { - 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, -) -> Result { - 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 { - 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 { - 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::>(); - 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::>(); - 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 { - 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 { - 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 { - 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 = 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, - present_modes: Vec, - surface_capabilities: VulkanSwapchainSurfaceCapabilities, -} - -fn live_device_candidate( - instance: &VulkanInstanceProbe, - surface: &VulkanSurfaceProbe, - device: vk::PhysicalDevice, - index: usize, -) -> Result { - 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::, 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 { - if graphics == present { - vec![graphics] - } else { - vec![graphics, present] - } -} - -fn device_extension_cstrings(values: &[String]) -> Result, 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, 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::>(); - extensions.sort(); - extensions.dedup(); - Ok(extensions) -} - -fn live_surface_formats( - surface: &VulkanSurfaceProbe, - device: vk::PhysicalDevice, - name: &str, -) -> Result, 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, 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 { - 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 { - // 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 { - // 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, 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, 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 { - // 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 { - 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 { - 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 { - 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, - /// Available present modes as raw Vulkan values. - pub present_modes: Vec, - /// 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, - /// Queue-family capabilities. - pub queue_families: Vec, - /// Surface formats accepted by the target surface. - pub surface_formats: Vec, -} - -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, -} - -/// 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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, -} - -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 { - 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(); -- cgit v1.2.3