// SPDX-License-Identifier: AGPL-3.0-only //! Graphics pipeline creation and shader management. use crate::error::RendererError; use crate::mesh::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`. 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. pub fn create_pipeline_layout(device: &Device) -> Result { // A single push constant range is defined for the MVP matrix, allowing it to be updated for every draw call with high efficiency. #[expect(clippy::expect_used)] let push_constant_range = vk::PushConstantRange::default() .stage_flags(vk::ShaderStageFlags::VERTEX) .offset(0) .size( u32::try_from(std::mem::size_of::()).expect("Mat4 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. pub fn create_graphics_pipeline( device: &Device, layout: vk::PipelineLayout, color_format: vk::Format, ) -> 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(vk::PolygonMode::FILL) .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(vk::CompareOp::LESS) .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. 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)) }