diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 1 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 28 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/instance.rs | 44 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 3 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/surface.rs | 20 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 6 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 280 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/lib.rs | 6 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/planning_backend.rs | 168 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/policy.rs | 164 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/shader.rs | 82 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/shader_manifest.rs | 399 |
14 files changed, 119 insertions, 1106 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 79a42a4..236361b 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -439,7 +439,6 @@ fn project_msh_to_static_mesh_in_world_space_with_node_poses( }) } -#[allow(clippy::too_many_arguments)] fn append_node_fallback_batch( model: &ModelAsset, batch: &fparkan_msh::Batch, diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 79ca919..640551a 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -1,22 +1,3 @@ -#![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. //! @@ -57,9 +38,8 @@ pub use self::capabilities::{ }; pub use self::instance::{ create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader, - render_instance_plan_json, render_loader_probe_report_json, vulkan_entry_symbol_name, - VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan, VulkanInstanceProbe, - VulkanLoaderError, VulkanLoaderProbeReport, + vulkan_entry_symbol_name, VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan, + VulkanInstanceProbe, VulkanLoaderError, VulkanLoaderProbeReport, }; #[cfg(test)] use self::instance::{cstring_vec, ensure_instance_extensions_available}; @@ -84,8 +64,8 @@ pub use self::smoke_types::{ #[cfg(test)] use self::surface::extension_name; pub use self::surface::{ - create_vulkan_surface_probe, plan_vulkan_surface, render_surface_plan_json, VulkanSurfaceError, - VulkanSurfacePlan, VulkanSurfaceProbe, + create_vulkan_surface_probe, plan_vulkan_surface, VulkanSurfaceError, VulkanSurfacePlan, + VulkanSurfaceProbe, }; pub use self::swapchain::{ create_vulkan_swapchain_probe, create_vulkan_swapchain_probe_for_extent, VulkanSwapchainProbe, diff --git a/adapters/fparkan-render-vulkan/src/ffi/instance.rs b/adapters/fparkan-render-vulkan/src/ffi/instance.rs index 42b9bdb..463fa03 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/instance.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/instance.rs @@ -1,7 +1,6 @@ #![allow(unsafe_code)] use ash::vk; -use serde::Serialize; use std::collections::BTreeSet; use std::ffi::{CStr, CString}; use std::os::raw::c_char; @@ -10,7 +9,6 @@ use super::{ EXT_DEBUG_UTILS_EXTENSION, KHR_PORTABILITY_ENUMERATION_EXTENSION, MIN_VULKAN_API_VERSION, VALIDATION_LAYER_NAME, }; -use crate::policy::{format_api_version, serialize_json_or_fallback}; /// Vulkan instance bootstrap configuration. #[derive(Clone, Debug, Eq, PartialEq)] @@ -236,28 +234,6 @@ pub fn create_vulkan_instance_probe( }) } -/// Renders a deterministic JSON Vulkan instance plan. -#[must_use] -pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String { - #[derive(Serialize)] - struct InstancePlanJson<'a> { - schema: u32, - create_flags: u32, - validation_requested: bool, - enabled_extensions: &'a [String], - } - - serialize_json_or_fallback( - &InstancePlanJson { - schema: plan.schema, - create_flags: plan.create_flags, - validation_requested: plan.validation_requested, - enabled_extensions: &plan.enabled_extensions, - }, - "{\"schema\":0,\"create_flags\":0,\"validation_requested\":false,\"enabled_extensions\":[]}", - ) -} - /// Opens the Vulkan loader and reports the supported instance API version. /// /// # Errors @@ -288,26 +264,6 @@ 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 { - #[derive(Serialize)] - struct LoaderProbeReportJson { - schema: u32, - loader_available: bool, - instance_api: String, - } - - serialize_json_or_fallback( - &LoaderProbeReportJson { - schema: report.schema, - loader_available: report.loader_available, - instance_api: format_api_version(report.instance_api_version), - }, - "{\"schema\":0,\"loader_available\":false,\"instance_api\":\"0.0.0\"}", - ) -} - fn available_instance_extensions(entry: &ash::Entry) -> Result<Vec<String>, VulkanInstanceError> { let available_extensions = // SAFETY: Enumerating instance extensions reads loader-owned immutable metadata. diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index 1d38503..f1fd579 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -26,7 +26,6 @@ pub(super) struct VulkanDepthAttachment { pub(super) format: vk::Format, } -#[allow(clippy::too_many_lines)] pub(super) fn create_depth_attachment( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, @@ -249,7 +248,6 @@ pub(super) fn create_static_mesh_index_buffer( ) } -#[allow(clippy::too_many_lines)] pub(super) fn create_static_texture_image( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, @@ -376,7 +374,6 @@ pub(super) fn create_static_texture_image( }) } -#[allow(clippy::too_many_lines)] fn upload_static_texture( device: &VulkanLogicalDeviceProbe, command_pool: vk::CommandPool, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index e41f0f9..e2f6c34 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -17,7 +17,7 @@ use super::{ VulkanValidationReport, }; use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION; -use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest}; +use crate::shader::validate_triangle_shaders; #[cfg(test)] fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>( @@ -103,7 +103,7 @@ impl VulkanSmokeRenderer { /// /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation, /// memory allocation, or synchronization resource creation fails. - #[allow(clippy::too_many_lines)] + pub fn new( create_info: &VulkanSmokeRendererCreateInfo, ) -> Result<Self, VulkanSmokeRendererError> { @@ -120,8 +120,7 @@ impl VulkanSmokeRenderer { resolve_draw_texture_indices(&create_info.mesh.draw_ranges, &create_info.materials) .map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?; let bootstrap_progress = create_info.bootstrap_progress.as_ref(); - let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) - .map_err(VulkanSmokeRendererError::ShaderManifest)?; + validate_triangle_shaders().map_err(VulkanSmokeRendererError::Shader)?; 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); @@ -240,7 +239,6 @@ impl VulkanSmokeRenderer { pending_extent: None, swapchain_recreate_count: 0, report: VulkanSmokeRendererReport { - shader_manifest_hash: shader_manifest.manifest_hash.clone(), portability_enumeration: instance_config.enable_portability_enumeration, portability_subset_enabled: false, device_name: String::new(), @@ -260,7 +258,6 @@ impl VulkanSmokeRenderer { 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() @@ -426,7 +423,7 @@ impl VulkanSmokeRenderer { /// /// Returns [`VulkanSmokeRendererError`] when synchronization, command recording, /// submission, or presentation fails. - #[allow(clippy::too_many_lines)] + pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> { if let Some(extent) = self.pending_extent.take() { if extent.0 == 0 || extent.1 == 0 { @@ -632,7 +629,6 @@ impl VulkanSmokeRenderer { Ok(()) } - #[allow(clippy::too_many_lines)] fn record_command_buffer( &mut self, image_index: usize, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 4686eaf..217df3f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -10,7 +10,7 @@ use super::{ VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources, VulkanValidationMessenger, }; -use crate::shader_manifest::VulkanShaderManifestError; +use crate::shader::VulkanShaderError; /// Creates a live native Vulkan renderer for the Stage 0 smoke loop. #[derive(Clone, Debug)] @@ -536,7 +536,7 @@ impl VulkanSmokeBootstrapProgress { /// Stable snapshot of measured bootstrap progress. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[allow(clippy::struct_excessive_bools)] + pub struct VulkanSmokeBootstrapSnapshot { /// Whether the Vulkan loader was resolved. pub loader_available: bool, @@ -562,8 +562,6 @@ const BOOTSTRAP_SWAPCHAIN_CREATED: u8 = 1 << 5; /// 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, /// Whether the logical device enabled `VK_KHR_portability_subset`. @@ -652,8 +650,8 @@ pub enum VulkanSmokeRendererError { LogicalDevice(VulkanLogicalDeviceError), /// Swapchain bootstrap failed. Swapchain(VulkanSwapchainProbeError), - /// Shader manifest validation failed. - ShaderManifest(VulkanShaderManifestError), + /// Checked-in shader validation failed. + Shader(VulkanShaderError), /// Vulkan operation failed. VulkanOperation { /// Operation context. @@ -695,7 +693,7 @@ impl std::fmt::Display for VulkanSmokeRendererError { 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::Shader(error) => write!(f, "{error}"), Self::VulkanOperation { context, result } => { write!(f, "{context}: {result:?}") } diff --git a/adapters/fparkan-render-vulkan/src/ffi/surface.rs b/adapters/fparkan-render-vulkan/src/ffi/surface.rs index 8fc9c5a..65a8b7f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/surface.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/surface.rs @@ -2,12 +2,10 @@ use ash::{khr::surface, vk}; use fparkan_platform::NativeWindowHandles; -use serde::Serialize; use std::ffi::CStr; use std::os::raw::c_char; use super::VulkanInstanceProbe; -use crate::policy::serialize_json_or_fallback; /// Deterministic Vulkan surface creation plan. #[derive(Clone, Debug, Eq, PartialEq)] @@ -132,24 +130,6 @@ pub fn create_vulkan_surface_probe( }) } -/// Renders a deterministic JSON Vulkan surface plan. -#[must_use] -pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String { - #[derive(Serialize)] - struct SurfacePlanJson<'a> { - schema: u32, - required_instance_extensions: &'a [String], - } - - serialize_json_or_fallback( - &SurfacePlanJson { - schema: plan.schema, - required_instance_extensions: &plan.required_instance_extensions, - }, - "{\"schema\":0,\"required_instance_extensions\":[]}", - ) -} - pub(super) fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> { // SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names. let name = unsafe { CStr::from_ptr(extension) }; diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index 7558db6..c175f0d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -45,7 +45,6 @@ struct PartialSwapchainResources { command_buffers: Vec<vk::CommandBuffer>, } -#[allow(clippy::too_many_arguments, clippy::too_many_lines)] pub(super) fn create_swapchain_resources( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, @@ -205,7 +204,6 @@ fn create_swapchain_image_views( Ok(image_views) } -#[allow(clippy::type_complexity)] fn create_swapchain_pipeline_bundle( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, @@ -433,7 +431,6 @@ fn create_pipeline_layout( }) } -#[allow(clippy::too_many_lines)] fn create_texture_descriptor_bundle( device: &VulkanLogicalDeviceProbe, textures: &[VulkanAllocatedImage], @@ -554,12 +551,10 @@ fn create_texture_descriptor_bundle( Ok((layout, pool, descriptor_sets, sampler)) } -#[allow(clippy::cast_precision_loss)] fn extent_component_to_f32(value: u32) -> f32 { value as f32 } -#[allow(clippy::too_many_lines)] fn create_graphics_pipeline_cache( device: &VulkanLogicalDeviceProbe, render_pass: vk::RenderPass, @@ -596,7 +591,6 @@ fn create_graphics_pipeline_cache( Ok(pipelines) } -#[allow(clippy::too_many_lines)] fn create_graphics_pipeline( device: &VulkanLogicalDeviceProbe, render_pass: vk::RenderPass, diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index a842c73..740c13d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -1,114 +1,9 @@ use super::*; +use crate::ffi::{SPIRV_MAGIC, SPIRV_VERSION_1_0}; 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::shader::{validate_spirv_container, validate_triangle_shaders, VulkanShaderError}; use crate::*; use fparkan_platform::{DepthStencilSupport, RenderRequest}; -use fparkan_render::{ - DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, LegacyPipelineState, RenderCommand, - RenderPhase, -}; -use fparkan_render::{RenderBackend, RenderError}; - -#[test] -fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> { - let mut backend = VulkanPlanningBackend::new(); - let request = RenderRequest { - presentation: fparkan_platform::PresentationMode::Immediate, - ..RenderRequest::conservative() - }; - backend.set_render_request(request); - assert_eq!(backend.render_request(), request); - assert_eq!(backend.report().request.current_request, request); - assert_eq!(backend.report().request.request_updates, 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), - pipeline_key: LegacyPipelineState::default().into(), - transform: [1.0; 16], - range: IndexRange { start: 0, count: 3 }, - stable_order: 7, - }), - RenderCommand::EndFrame, - ], - }; - - backend.execute(&commands)?; - assert_eq!(backend.state(), VulkanPlanningBackendState::Configured); - assert_eq!(backend.report().execution.planned_frames, 1); - assert_eq!(backend.report().execution.submission_plans, 1); - assert_eq!(backend.report().execution.simulated_presents, 1); - assert!(backend.report().execution.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), - pipeline_key: LegacyPipelineState::default().into(), - transform: [1.0; 16], - range: IndexRange { start: 0, count: 3 }, - stable_order: 7, - }), - RenderCommand::EndFrame, - ], - }; - let swapchain = 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: 3, - image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), - }; - - 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() { @@ -391,38 +286,6 @@ fn capability_report_preserves_informational_sampled_formats_and_limits() { } #[test] -fn capability_report_json_is_stable() { - let mut rejected = device("Rejected", VulkanDeviceType::IntegratedGpu, 0, true, false); - rejected.present_modes.clear(); - let report = select_physical_device(&[ - rejected, - 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\"],\"informational_capabilities\":{\"sampled_color_formats\":[50],\"sampled_depth_formats\":[126],\"limits\":{\"max_image_dimension_2d\":4096,\"max_sampler_allocation_count\":4096,\"max_per_stage_descriptor_samplers\":16,\"max_bound_descriptor_sets\":4}},\"rejected_devices\":[{\"device_name\":\"Rejected\",\"reason_code\":\"missing_present_mode\",\"reason\":\"Vulkan device Rejected has no supported present mode\"}]}" - ); -} - -#[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 { @@ -446,9 +309,15 @@ fn instance_plan_is_sorted_deduplicated_and_portability_aware() { enable_validation: true, }); + assert_eq!(plan.create_flags, 1); + assert!(plan.validation_requested); 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\"]}" + plan.enabled_extensions, + vec![ + "VK_EXT_debug_utils".to_string(), + KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(), + "VK_KHR_surface".to_string(), + ] ); } @@ -510,20 +379,6 @@ fn surface_plan_requires_native_handles() { } #[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"); @@ -620,128 +475,31 @@ fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() { } #[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,\"image_usage\":16}" - ); - - 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, 358); - assert_eq!( - report.modules[0].sha256, - "4e2051ac43b933a57e5557e596bcc95952b4efbfcb45315180f89581e094398d" - ); - assert_eq!(report.modules[0].descriptor_sets, 0); - assert_eq!(report.modules[0].push_constant_bytes, 64); - 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, - "8ab7bf835e166892b04b10fa1100258daf355d2f693ad311d014dbee22de5c7b" - ); - assert_eq!( - report.manifest_hash, - "cf471972978ddb5c8919711ad64c93d0d64e18b1f590a89ce014f1e2a743cfb5" - ); -} - -#[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() - ); +fn checked_in_shaders_have_valid_spirv_containers() { + assert_eq!(validate_triangle_shaders(), Ok(())); } #[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]; +fn shader_container_rejects_invalid_spirv() { assert_eq!( - validate_shader_manifest(&[module]), - Err(VulkanShaderManifestError::InvalidMagic { + validate_spirv_container("triangle.vert", &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0]), + Err(VulkanShaderError::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 { + validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, 0, 0, 1, 0]), + Err(VulkanShaderError::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 { + validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0]), + Err(VulkanShaderError::InvalidBound { name: "triangle.vert", }) ); diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs index b039cd1..a5beafd 100644 --- a/adapters/fparkan-render-vulkan/src/lib.rs +++ b/adapters/fparkan-render-vulkan/src/lib.rs @@ -2,11 +2,9 @@ //! Vulkan adapter public surface. mod ffi; -mod planning_backend; mod policy; -mod shader_manifest; +mod shader; pub use ffi::*; -pub use planning_backend::*; pub use policy::*; -pub use shader_manifest::*; +pub use shader::VulkanShaderError; diff --git a/adapters/fparkan-render-vulkan/src/planning_backend.rs b/adapters/fparkan-render-vulkan/src/planning_backend.rs deleted file mode 100644 index 9a91712..0000000 --- a/adapters/fparkan-render-vulkan/src/planning_backend.rs +++ /dev/null @@ -1,168 +0,0 @@ -use ash::vk; -use fparkan_platform::RenderRequest; -use fparkan_render::{ - canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, -}; - -use crate::{ - plan_vulkan_frame_submission, VulkanFrameSubmissionPlan, VulkanSurfaceFormat, - VulkanSwapchainPlan, -}; - -/// Vulkan backend migration readiness. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum VulkanPlanningBackendState { - /// Planning facade is configured and able to accept command lists. - #[default] - Configured, - /// Adapter is tracking a recoverable runtime surface/depth pipeline fault. - Degraded, - /// Adapter has encountered a non-recoverable error. - Error, -} - -/// Diagnostics for planning-facade request tracking. -#[derive(Clone, Debug, PartialEq)] -pub struct VulkanPlanningRequestReport { - /// Last render request observed by the planning facade. - pub current_request: RenderRequest, - /// Number of meaningful request updates applied to the facade. - pub request_updates: u64, -} - -impl Default for VulkanPlanningRequestReport { - fn default() -> Self { - Self { - current_request: RenderRequest::conservative(), - request_updates: 0, - } - } -} - -/// Diagnostics for planning-facade execution telemetry. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct VulkanPlanningExecutionReport { - /// Total frames planned by the facade. - pub planned_frames: u64, - /// Total frame-submission plans emitted by the facade. - pub submission_plans: 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, -} - -/// Diagnostics for Vulkan planning backend setup and frame progression. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct VulkanPlanningBackendReport { - /// Request-tracking telemetry. - pub request: VulkanPlanningRequestReport, - /// Execution-planning telemetry. - pub execution: VulkanPlanningExecutionReport, - /// Last deterministic frame submission plan. - pub last_frame_submission: Option<VulkanFrameSubmissionPlan>, -} - -/// Vulkan planning backend facade 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 facade. - #[must_use] - pub fn new() -> Self { - Self { - state: VulkanPlanningBackendState::Configured, - report: VulkanPlanningBackendReport::default(), - swapchain_plan: default_stage0_swapchain_plan(), - } - } - - /// Replaces active surface/profile request. - pub fn set_render_request(&mut self, request: RenderRequest) { - if self.report.request.current_request != request { - self.report.request.current_request = request; - self.report.request.request_updates = - self.report.request.request_updates.saturating_add(1); - } - } - - /// Returns active render request policy. - #[must_use] - pub const fn render_request(&self) -> RenderRequest { - self.report.request.current_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.execution.simulated_presents = - self.report.execution.simulated_presents.saturating_add(1); - } -} - -impl RenderBackend for VulkanPlanningBackend { - fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> { - if !matches!( - self.state, - VulkanPlanningBackendState::Configured | VulkanPlanningBackendState::Degraded - ) { - return Err(RenderError::InvalidRange); - } - let capture = canonical_capture(commands)?; - let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?; - self.report.execution.planned_frames = - self.report.execution.planned_frames.saturating_add(1); - self.report.execution.submission_plans = - self.report.execution.submission_plans.saturating_add(1); - self.report.execution.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, - image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), - } -} diff --git a/adapters/fparkan-render-vulkan/src/policy.rs b/adapters/fparkan-render-vulkan/src/policy.rs index c79b8dc..ce7b59e 100644 --- a/adapters/fparkan-render-vulkan/src/policy.rs +++ b/adapters/fparkan-render-vulkan/src/policy.rs @@ -1,7 +1,5 @@ use ash::vk; use fparkan_platform::{DepthStencilSupport, RenderRequest}; -use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError}; -use serde::Serialize; const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1; pub(crate) const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain"; @@ -147,25 +145,6 @@ pub struct VulkanSwapchainRecreationReport { pub next_extent: (u32, u32), } -/// Deterministic frame submission plan for command buffers and sync objects. -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -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 { @@ -204,7 +183,7 @@ impl VulkanPhysicalDeviceRecord { } /// Informational device limits relevant to future Stage 0 capability growth. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct VulkanDeviceLimits { /// Maximum 2D image dimension supported by the device. pub max_image_dimension_2d: u32, @@ -217,7 +196,7 @@ pub struct VulkanDeviceLimits { } /// Informational capabilities preserved in deterministic Stage 0 reports. -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanInformationalCapabilities { /// Color formats that support sampled-image usage. pub sampled_color_formats: Vec<i32>, @@ -253,7 +232,7 @@ pub struct VulkanCapabilityReport { } /// Deterministic rejection reason for an unsuitable physical device. -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanRejectedDeviceReport { /// Human-readable device name. pub device_name: String, @@ -446,136 +425,6 @@ pub const fn swapchain_recreation_report( } } -/// Builds a deterministic frame submission plan for a validated command list. -/// -/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer -/// and synchronization policy can be tested without requiring a native surface. -/// -/// # Errors -/// -/// Returns [`RenderError`] when the command list has invalid frame framing, -/// ordering, draw ranges, mesh bounds, or non-finite transforms. -pub fn plan_vulkan_frame_submission( - swapchain: &VulkanSwapchainPlan, - commands: &RenderCommandList, -) -> Result<VulkanFrameSubmissionPlan, RenderError> { - validate_command_list(commands)?; - let mut draw_count = 0_u32; - let mut indexed_vertex_count = 0_u32; - for command in &commands.commands { - if let RenderCommand::Draw(draw) = command { - draw_count = draw_count.saturating_add(1); - indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count); - } - } - Ok(VulkanFrameSubmissionPlan { - schema: 1, - frames_in_flight: swapchain.image_count.clamp(1, 2), - command_buffers: swapchain.image_count, - semaphores_per_frame: 2, - fences_per_frame: 1, - draw_count, - indexed_vertex_count, - }) -} - -/// Renders a deterministic JSON capability report. -#[must_use] -pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String { - #[derive(Serialize)] - struct CapabilityReportJson<'a> { - schema: u32, - vulkan_api: String, - device_name: &'a str, - score: i32, - graphics_queue_family: u32, - present_queue_family: u32, - portability_subset: bool, - enabled_extensions: &'a [String], - informational_capabilities: &'a VulkanInformationalCapabilities, - rejected_devices: &'a [VulkanRejectedDeviceReport], - } - - serialize_json_or_fallback( - &CapabilityReportJson { - schema: report.schema, - vulkan_api: format_api_version(report.vulkan_api_version), - device_name: &report.device_name, - score: report.score, - graphics_queue_family: report.graphics_queue_family, - present_queue_family: report.present_queue_family, - portability_subset: report.portability_subset, - enabled_extensions: &report.enabled_extensions, - informational_capabilities: &report.informational_capabilities, - rejected_devices: &report.rejected_devices, - }, - "{\"schema\":0,\"vulkan_api\":\"0.0.0\",\"device_name\":\"unknown\",\"score\":0,\"graphics_queue_family\":0,\"present_queue_family\":0,\"portability_subset\":false,\"enabled_extensions\":[],\"informational_capabilities\":{\"sampled_color_formats\":[],\"sampled_depth_formats\":[],\"limits\":{\"max_image_dimension_2d\":0,\"max_sampler_allocation_count\":0,\"max_per_stage_descriptor_samplers\":0,\"max_bound_descriptor_sets\":0}},\"rejected_devices\":[]}", - ) -} - -/// Renders a deterministic JSON swapchain plan. -#[must_use] -pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String { - #[derive(Serialize)] - struct SwapchainPlanJson { - schema: u32, - extent: [u32; 2], - format: i32, - color_space: i32, - present_mode: i32, - image_count: u32, - image_usage: u32, - } - - serialize_json_or_fallback( - &SwapchainPlanJson { - schema: plan.schema, - extent: [plan.extent.0, plan.extent.1], - format: plan.format.format, - color_space: plan.format.color_space, - present_mode: plan.present_mode, - image_count: plan.image_count, - image_usage: plan.image_usage, - }, - "{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0,\"image_usage\":0}", - ) -} - -/// Renders a deterministic JSON swapchain recreation report. -#[must_use] -pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String { - #[derive(Serialize)] - struct SwapchainRecreationReportJson<'a> { - schema: u32, - reason: &'a str, - previous_extent: [u32; 2], - next_extent: [u32; 2], - } - - serialize_json_or_fallback( - &SwapchainRecreationReportJson { - schema: report.schema, - reason: match report.reason { - VulkanSwapchainRecreationReason::Resize => "resize", - VulkanSwapchainRecreationReason::OutOfDate => "out_of_date", - VulkanSwapchainRecreationReason::Suboptimal => "suboptimal", - }, - previous_extent: [report.previous_extent.0, report.previous_extent.1], - next_extent: [report.next_extent.0, report.next_extent.1], - }, - "{\"schema\":0,\"reason\":\"unknown\",\"previous_extent\":[0,0],\"next_extent\":[0,0]}", - ) -} - -/// Renders a deterministic JSON frame submission plan. -#[must_use] -pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String { - serialize_json_or_fallback( - plan, - "{\"schema\":0,\"frames_in_flight\":0,\"command_buffers\":0,\"semaphores_per_frame\":0,\"fences_per_frame\":0,\"draw_count\":0,\"indexed_vertex_count\":0}", - ) -} - pub(crate) fn select_composite_alpha( supported: vk::CompositeAlphaFlagsKHR, ) -> vk::CompositeAlphaFlagsKHR { @@ -590,13 +439,6 @@ pub(crate) fn select_composite_alpha( } } -pub(crate) fn serialize_json_or_fallback<T: Serialize>(value: &T, fallback: &str) -> String { - match serde_json::to_string(value) { - Ok(json) => json, - Err(_) => fallback.to_string(), - } -} - pub(crate) fn format_api_version(version: u32) -> String { format!( "{}.{}.{}", diff --git a/adapters/fparkan-render-vulkan/src/shader.rs b/adapters/fparkan-render-vulkan/src/shader.rs new file mode 100644 index 0000000..2e3184a --- /dev/null +++ b/adapters/fparkan-render-vulkan/src/shader.rs @@ -0,0 +1,82 @@ +use crate::ffi::{ + SPIRV_MAGIC, SPIRV_VERSION_1_0, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, +}; + +/// Error found while checking a checked-in SPIR-V module. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VulkanShaderError { + /// 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 VulkanShaderError { + 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 VulkanShaderError {} + +/// Checks the checked-in triangle shader containers before creating a pipeline. +pub(crate) fn validate_triangle_shaders() -> Result<(), VulkanShaderError> { + validate_spirv_container("triangle.vert", TRIANGLE_VERTEX_SHADER_WORDS)?; + validate_spirv_container("triangle.frag", TRIANGLE_FRAGMENT_SHADER_WORDS)?; + Ok(()) +} + +pub(crate) fn validate_spirv_container( + name: &'static str, + words: &[u32], +) -> Result<(), VulkanShaderError> { + if words.len() < 5 { + return Err(VulkanShaderError::TooShort { name }); + } + if words[0] != SPIRV_MAGIC { + return Err(VulkanShaderError::InvalidMagic { + name, + found: words[0], + }); + } + if words[1] < SPIRV_VERSION_1_0 { + return Err(VulkanShaderError::UnsupportedVersion { + name, + found: words[1], + }); + } + if words[3] == 0 { + return Err(VulkanShaderError::InvalidBound { name }); + } + Ok(()) +} diff --git a/adapters/fparkan-render-vulkan/src/shader_manifest.rs b/adapters/fparkan-render-vulkan/src/shader_manifest.rs deleted file mode 100644 index be86e50..0000000 --- a/adapters/fparkan-render-vulkan/src/shader_manifest.rs +++ /dev/null @@ -1,399 +0,0 @@ -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.1"; -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 = - "fe3721202477220d2d9677b6572d7f3baecb374e94f04eea9d7286ed5e98b6c9"; -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 --target-env vulkan1.1 -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.1 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 = - "fb3ed435af48e4fb4a817f160b467f7a561c7c547c87b6f0c1e89f5f58af7329"; -const TRIANGLE_FRAGMENT_SPIRV_PATH: &str = - "adapters/fparkan-render-vulkan/shaders/triangle.frag.spv"; -const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str = - "glslangValidator -V --target-env vulkan1.1 -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.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv"; - -fn shader_compiler_name() -> &'static str { - option_env!("FPARKAN_BUILD_SHADER_COMPILER_NAME").unwrap_or(SHADER_COMPILER_NAME) -} - -fn shader_compiler_version() -> &'static str { - option_env!("FPARKAN_BUILD_SHADER_COMPILER_VERSION").unwrap_or(SHADER_COMPILER_VERSION) -} - -fn shader_compiler_binary_sha256() -> &'static str { - option_env!("FPARKAN_BUILD_SHADER_COMPILER_SHA256").unwrap_or(SHADER_COMPILER_BINARY_SHA256) -} - -fn spirv_validator_name() -> &'static str { - option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_NAME").unwrap_or(SPIRV_VALIDATOR_NAME) -} - -fn spirv_validator_version() -> &'static str { - option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_VERSION").unwrap_or(SPIRV_VALIDATOR_VERSION) -} - -fn spirv_validator_binary_sha256() -> &'static str { - option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_SHA256").unwrap_or(SPIRV_VALIDATOR_BINARY_SHA256) -} - -/// 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: 64, - 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: 1, - push_constant_bytes: 68, - 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, - } -} |
