Synvael/crates/renderer/src/pipeline.rs

183 lines
7.2 KiB
Rust

// 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<vk::ShaderModule, RendererError> {
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<vk::PipelineLayout, RendererError> {
// 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::<glam::Mat4>() + 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<vk::Pipeline, RendererError> {
// 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))
}