aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 05:00:03 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:35 +0300
commitce3e5ad813fcd5bb1c4de9ce51c603e110412cc9 (patch)
tree8254f882aca27cfc7b5588fd89ab48a211b2085e
parentd0552922d9f0fc1ac95fc3ab2009672a39bf0f0c (diff)
downloadfparkan-ce3e5ad813fcd5bb1c4de9ce51c603e110412cc9.tar.xz
fparkan-ce3e5ad813fcd5bb1c4de9ce51c603e110412cc9.zip
refactor(vulkan-shaders): extract manifest validation module
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs383
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs2
-rw-r--r--adapters/fparkan-render-vulkan/src/shader_manifest.rs375
3 files changed, 392 insertions, 368 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index e3795a2..b897a03 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -28,11 +28,13 @@
//! This crate is the declared low-level Vulkan boundary.
use crate::policy::*;
+use crate::shader_manifest::{
+ triangle_shader_manifest, validate_shader_manifest, VulkanShaderManifestError,
+};
use ash::{
khr::{surface, swapchain},
vk,
};
-use fparkan_binary::{sha256, sha256_hex};
use fparkan_platform::NativeWindowHandles;
use serde::Serialize;
use std::collections::BTreeSet;
@@ -45,9 +47,9 @@ pub const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
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] = &[
+pub(crate) const SPIRV_MAGIC: u32 = 0x0723_0203;
+pub(crate) const SPIRV_VERSION_1_0: u32 = 0x0001_0000;
+pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[
SPIRV_MAGIC,
SPIRV_VERSION_1_0,
0x0008_000b,
@@ -302,7 +304,7 @@ const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[
0x0001_00fd,
0x0001_0038,
];
-const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
+pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
SPIRV_MAGIC,
SPIRV_VERSION_1_0,
0x0008_000b,
@@ -430,175 +432,6 @@ const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
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, 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<VulkanShaderModuleReport>,
- /// 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 {}
-
/// Vulkan instance bootstrap configuration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanInstanceConfig {
@@ -3401,204 +3234,18 @@ pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> Stri
)
}
-/// Returns the built-in Stage 0 indexed-triangle shader manifest.
-#[must_use]
-pub fn triangle_shader_manifest() -> Vec<VulkanShaderModuleManifest> {
- vec![
- VulkanShaderModuleManifest {
- name: "triangle.vert",
- stage: VulkanShaderStage::Vertex,
- entry_point: "main",
- descriptor_sets: 0,
- push_constant_bytes: 0,
- source_path: TRIANGLE_VERTEX_SOURCE_PATH,
- source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
- spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
- compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
- validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
- words: TRIANGLE_VERTEX_SHADER_WORDS,
- },
- VulkanShaderModuleManifest {
- name: "triangle.frag",
- stage: VulkanShaderStage::Fragment,
- entry_point: "main",
- descriptor_sets: 0,
- push_constant_bytes: 0,
- source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
- source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
- spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
- compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
- validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
- words: TRIANGLE_FRAGMENT_SHADER_WORDS,
- },
- ]
-}
-
-/// Validates shader SPIR-V containers and renders a deterministic report.
-///
-/// # Errors
-///
-/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
-/// container validation.
-pub fn validate_shader_manifest(
- modules: &[VulkanShaderModuleManifest],
-) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
- let mut reports = Vec::with_capacity(modules.len());
- for module in modules {
- validate_spirv_container(module)?;
- let bytes = spirv_words_to_bytes(module.words);
- reports.push(VulkanShaderModuleReport {
- name: module.name,
- stage: module.stage,
- entry_point: module.entry_point,
- source_path: module.source_path,
- source_sha256: module.source_sha256,
- spirv_path: module.spirv_path,
- word_count: module.words.len(),
- sha256: sha256_hex(&sha256(&bytes)),
- descriptor_sets: module.descriptor_sets,
- push_constant_bytes: module.push_constant_bytes,
- compile_command: module.compile_command,
- validate_command: module.validate_command,
- interface_hash: shader_interface_hash(module),
- });
- }
- let normalized = render_shader_manifest_without_hash_json(&reports);
- Ok(VulkanShaderManifestReport {
- schema: SHADER_MANIFEST_SCHEMA,
- target_env: SHADER_TARGET_ENV,
- compiler: VulkanShaderToolManifest {
- name: SHADER_COMPILER_NAME,
- version: SHADER_COMPILER_VERSION,
- binary_sha256: SHADER_COMPILER_BINARY_SHA256,
- },
- validator: VulkanShaderToolManifest {
- name: SPIRV_VALIDATOR_NAME,
- version: SPIRV_VALIDATOR_VERSION,
- binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
- },
- modules: reports,
- manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
- })
-}
-
-fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
- #[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<u8> {
- let mut out = Vec::with_capacity(words.len() * 4);
- for word in words {
- out.extend_from_slice(&word.to_le_bytes());
- }
- out
-}
-
-/// Renders a deterministic JSON shader manifest report.
-#[must_use]
-pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
- #[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 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,
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION};
+ use crate::shader_manifest::{
+ SHADER_COMPILER_BINARY_SHA256, SHADER_COMPILER_NAME, SHADER_COMPILER_VERSION,
+ SHADER_MANIFEST_SCHEMA, SHADER_TARGET_ENV, SPIRV_MAGIC, SPIRV_VALIDATOR_BINARY_SHA256,
+ SPIRV_VALIDATOR_NAME, SPIRV_VALIDATOR_VERSION, SPIRV_VERSION_1_0,
+ TRIANGLE_VERTEX_COMPILE_COMMAND, TRIANGLE_VERTEX_SOURCE_PATH,
+ TRIANGLE_VERTEX_SOURCE_SHA256, TRIANGLE_VERTEX_SPIRV_PATH,
+ TRIANGLE_VERTEX_VALIDATE_COMMAND,
+ };
use crate::*;
use fparkan_platform::RenderRequest;
use fparkan_render::{
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index b03df88..b039cd1 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -4,7 +4,9 @@
mod ffi;
mod planning_backend;
mod policy;
+mod shader_manifest;
pub use ffi::*;
pub use planning_backend::*;
pub use policy::*;
+pub use shader_manifest::*;
diff --git a/adapters/fparkan-render-vulkan/src/shader_manifest.rs b/adapters/fparkan-render-vulkan/src/shader_manifest.rs
new file mode 100644
index 0000000..5c589cb
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/shader_manifest.rs
@@ -0,0 +1,375 @@
+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<VulkanShaderModuleReport>,
+ /// 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<VulkanShaderModuleManifest> {
+ vec![
+ VulkanShaderModuleManifest {
+ name: "triangle.vert",
+ stage: VulkanShaderStage::Vertex,
+ entry_point: "main",
+ descriptor_sets: 0,
+ push_constant_bytes: 0,
+ source_path: TRIANGLE_VERTEX_SOURCE_PATH,
+ source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
+ spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
+ compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
+ validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
+ words: TRIANGLE_VERTEX_SHADER_WORDS,
+ },
+ VulkanShaderModuleManifest {
+ name: "triangle.frag",
+ stage: VulkanShaderStage::Fragment,
+ entry_point: "main",
+ descriptor_sets: 0,
+ push_constant_bytes: 0,
+ source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
+ source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
+ spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
+ compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
+ validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
+ words: TRIANGLE_FRAGMENT_SHADER_WORDS,
+ },
+ ]
+}
+
+/// Validates shader SPIR-V containers and renders a deterministic report.
+///
+/// # Errors
+///
+/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
+/// container validation.
+pub fn validate_shader_manifest(
+ modules: &[VulkanShaderModuleManifest],
+) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
+ let mut reports = Vec::with_capacity(modules.len());
+ for module in modules {
+ validate_spirv_container(module)?;
+ let bytes = spirv_words_to_bytes(module.words);
+ reports.push(VulkanShaderModuleReport {
+ name: module.name,
+ stage: module.stage,
+ entry_point: module.entry_point,
+ source_path: module.source_path,
+ source_sha256: module.source_sha256,
+ spirv_path: module.spirv_path,
+ word_count: module.words.len(),
+ sha256: sha256_hex(&sha256(&bytes)),
+ descriptor_sets: module.descriptor_sets,
+ push_constant_bytes: module.push_constant_bytes,
+ compile_command: module.compile_command,
+ validate_command: module.validate_command,
+ interface_hash: shader_interface_hash(module),
+ });
+ }
+ let normalized = render_shader_manifest_without_hash_json(&reports);
+ Ok(VulkanShaderManifestReport {
+ schema: SHADER_MANIFEST_SCHEMA,
+ target_env: SHADER_TARGET_ENV,
+ compiler: VulkanShaderToolManifest {
+ name: SHADER_COMPILER_NAME,
+ version: SHADER_COMPILER_VERSION,
+ binary_sha256: SHADER_COMPILER_BINARY_SHA256,
+ },
+ validator: VulkanShaderToolManifest {
+ name: SPIRV_VALIDATOR_NAME,
+ version: SPIRV_VALIDATOR_VERSION,
+ binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
+ },
+ modules: reports,
+ manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
+ })
+}
+
+/// 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<u8> {
+ 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,
+ }
+}