// SPDX-License-Identifier: AGPL-3.0-only //! Graphics pipeline creation and shader management. use crate::error::RendererError; use crate::vertex::Vertex; use ash::{Device, vk}; use std::io::Cursor; /// Helper to load SPIR-V bytes and create a Vulkan Shader Module. /// /// Vulkan expects shader code to be 32-bit aligned; `ash::util::read_spv` is used to correctly interpret the raw bytes as a slice of `u32`. /// /// # Errors /// /// Returns [`RendererError::IoError`] if `bytes` is not valid, 32-bit-aligned SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device. pub fn create_shader_module( device: &Device, bytes: &[u8], ) -> Result { let mut cursor = Cursor::new(bytes); let code = ash::util::read_spv(&mut cursor)?; let create_info = vk::ShaderModuleCreateInfo::default().code(&code); let module = unsafe { device.create_shader_module(&create_info, None)? }; Ok(module) } /// Defines the 'interface' of the pipeline (what data we can pass to the shaders). /// /// This layout defines any push constants or descriptor sets (textures/UBOs) accessed by the shaders during execution. /// /// # Errors /// /// Returns [`RendererError::VulkanError`] if the device fails to create the pipeline layout. pub fn create_pipeline_layout(device: &Device) -> Result { // The push-constant range covers the 64-byte MVP matrix followed by a 16-byte vec4 per-chunk world offset (80 bytes total, within the 128-byte guaranteed minimum). #[expect( clippy::expect_used, reason = "80 bytes (Mat4 + vec4) is well within u32 range" )] let push_constant_range = vk::PushConstantRange::default() .stage_flags(vk::ShaderStageFlags::VERTEX) .offset(0) .size( u32::try_from(std::mem::size_of::() + std::mem::size_of::<[f32; 4]>()) .expect("push-constant size exceeds u32 range"), ); let layout_create_info = vk::PipelineLayoutCreateInfo::default() .push_constant_ranges(std::slice::from_ref(&push_constant_range)); let layout = unsafe { device.create_pipeline_layout(&layout_create_info, None)? }; Ok(layout) } /// Creates a Graphics Pipeline for voxel rendering using Vulkan 1.3 Dynamic Rendering. /// /// The pipeline encapsulates the entire state of the GPU for a specific draw operation, including shader stages, vertex input layout, rasterization settings, and blending. /// /// # Errors /// /// Returns [`RendererError::InvalidString`] if the shader entry-point name cannot be built, [`RendererError::IoError`] if an embedded shader is not valid SPIR-V, or [`RendererError::VulkanError`] if shader-module or pipeline creation fails on the device. pub fn create_graphics_pipeline( device: &Device, layout: vk::PipelineLayout, color_format: vk::Format, polygon_mode: vk::PolygonMode, depth_compare_op: vk::CompareOp, ) -> Result { // 1. Load and compile shader modules let (vert_module, frag_module) = load_shader_modules(device)?; let entry_point = std::ffi::CString::new("main").map_err(|_| RendererError::InvalidString)?; let shader_stages = [ vk::PipelineShaderStageCreateInfo::default() .stage(vk::ShaderStageFlags::VERTEX) .module(vert_module) .name(&entry_point), vk::PipelineShaderStageCreateInfo::default() .stage(vk::ShaderStageFlags::FRAGMENT) .module(frag_module) .name(&entry_point), ]; // 2. Configure Fixed-Function States let binding_descriptions = [Vertex::get_binding_description()]; let attribute_descriptions = Vertex::get_attribute_descriptions(); let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default() .vertex_binding_descriptions(&binding_descriptions) .vertex_attribute_descriptions(&attribute_descriptions); let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default() .topology(vk::PrimitiveTopology::TRIANGLE_LIST) .primitive_restart_enable(false); let viewport_state = vk::PipelineViewportStateCreateInfo::default() .viewport_count(1) .scissor_count(1); let rasterizer = vk::PipelineRasterizationStateCreateInfo::default() .depth_clamp_enable(false) .rasterizer_discard_enable(false) .polygon_mode(polygon_mode) .line_width(1.0) .cull_mode(vk::CullModeFlags::BACK) .front_face(vk::FrontFace::COUNTER_CLOCKWISE) .depth_bias_enable(false); let multisampling = vk::PipelineMultisampleStateCreateInfo::default() .sample_shading_enable(false) .rasterization_samples(vk::SampleCountFlags::TYPE_1); let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default() .color_write_mask(vk::ColorComponentFlags::RGBA) .blend_enable(false); let color_blending = vk::PipelineColorBlendStateCreateInfo::default() .logic_op_enable(false) .attachments(std::slice::from_ref(&color_blend_attachment)); let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; let dynamic_state_info = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); let color_formats = [color_format]; let mut rendering_info = vk::PipelineRenderingCreateInfo::default() .color_attachment_formats(&color_formats) .depth_attachment_format(vk::Format::D32_SFLOAT); let depth_stencil_state = &vk::PipelineDepthStencilStateCreateInfo::default() .depth_test_enable(true) .depth_write_enable(true) .depth_compare_op(depth_compare_op) .depth_bounds_test_enable(false) .stencil_test_enable(false); // 3. Finalize Pipeline Creation let pipeline_info = vk::GraphicsPipelineCreateInfo::default() .push_next(&mut rendering_info) .stages(&shader_stages) .vertex_input_state(&vertex_input_info) .input_assembly_state(&input_assembly) .viewport_state(&viewport_state) .rasterization_state(&rasterizer) .multisample_state(&multisampling) .color_blend_state(&color_blending) .dynamic_state(&dynamic_state_info) .layout(layout) .depth_stencil_state(depth_stencil_state); let result = unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), &[pipeline_info], None) }; unsafe { device.destroy_shader_module(vert_module, None); device.destroy_shader_module(frag_module, None); } let pipeline = result.map_err(|(_, e)| e)?[0]; Ok(pipeline) } /// Loads the vertex and fragment shader modules from embedded bytes. /// /// # Errors /// /// Returns [`RendererError::IoError`] if an embedded shader is not valid SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device. fn load_shader_modules( device: &Device, ) -> Result<(vk::ShaderModule, vk::ShaderModule), RendererError> { let vert_bytes = include_bytes!("../../../assets/shaders/cube.vert.spv"); let frag_bytes = include_bytes!("../../../assets/shaders/cube.frag.spv"); let vert_module = create_shader_module(device, vert_bytes)?; let frag_module = create_shader_module(device, frag_bytes)?; Ok((vert_module, frag_module)) }