use fparkan_binary::{sha256, sha256_hex}; use serde::Serialize; pub(crate) use crate::ffi::{ SPIRV_MAGIC, SPIRV_VERSION_1_0, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, }; use crate::policy::serialize_json_or_fallback; pub(crate) const SHADER_MANIFEST_SCHEMA: u32 = 2; pub(crate) const SHADER_TARGET_ENV: &str = "vulkan1.0"; pub(crate) const SHADER_COMPILER_NAME: &str = "glslangValidator"; pub(crate) const SHADER_COMPILER_VERSION: &str = "11:16.3.0"; pub(crate) const SHADER_COMPILER_BINARY_SHA256: &str = "9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"; pub(crate) const SPIRV_VALIDATOR_NAME: &str = "spirv-val"; pub(crate) const SPIRV_VALIDATOR_VERSION: &str = "SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00"; pub(crate) const SPIRV_VALIDATOR_BINARY_SHA256: &str = "f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"; pub(crate) const TRIANGLE_VERTEX_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert"; pub(crate) const TRIANGLE_VERTEX_SOURCE_SHA256: &str = "1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341"; pub(crate) const TRIANGLE_VERTEX_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert.spv"; pub(crate) 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"; pub(crate) 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, Serialize)] 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, Serialize)] #[serde(rename_all = "lowercase")] pub enum VulkanShaderStage { /// Vertex stage. Vertex, /// Fragment stage. 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, Serialize)] 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 {} /// 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())), }) } /// Renders a deterministic JSON shader manifest report. #[must_use] pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String { #[derive(Serialize)] struct ShaderManifestReportJson<'a> { schema: u32, target_env: &'a str, compiler: &'a VulkanShaderToolManifest, validator: &'a VulkanShaderToolManifest, modules: &'a [VulkanShaderModuleReport], manifest_hash: &'a str, } serialize_json_or_fallback( &ShaderManifestReportJson { schema: report.schema, target_env: report.target_env, compiler: &report.compiler, validator: &report.validator, modules: &report.modules, manifest_hash: &report.manifest_hash, }, "{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[],\"manifest_hash\":\"unknown\"}", ) } fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String { #[derive(Serialize)] struct ShaderInterfaceHashJson<'a> { stage: VulkanShaderStage, entry_point: &'a str, descriptor_sets: u32, push_constant_bytes: u32, } let normalized = serialize_json_or_fallback( &ShaderInterfaceHashJson { stage: module.stage, entry_point: module.entry_point, descriptor_sets: module.descriptor_sets, push_constant_bytes: module.push_constant_bytes, }, "{\"stage\":\"vertex\",\"entry_point\":\"main\",\"descriptor_sets\":0,\"push_constant_bytes\":0}", ); 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 } fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String { #[derive(Serialize)] struct ShaderManifestWithoutHashJson<'a> { schema: u32, target_env: &'a str, compiler: VulkanShaderToolManifest, validator: VulkanShaderToolManifest, modules: &'a [VulkanShaderModuleReport], } let json = serialize_json_or_fallback( &ShaderManifestWithoutHashJson { 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, }, "{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[]}", ); match json.strip_suffix('}') { Some(stripped) => stripped.to_string(), None => json, } }