synvael/crates/renderer/src/pipeline.rs

199 lines
8.3 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)
}
/// Size, in bytes, of one `vec4` slot of the push-constant block.
pub const VEC4_BYTES: u32 = 16;
/// Number of `vec4` slots following the MVP matrix in the push-constant block: the per-chunk offset, the fog parameters, and the sky colour.
const PUSH_CONSTANT_VEC4S: u32 = 3;
/// Shader stages that read the push-constant block.
///
/// Both stages are declared across the entire range: the vertex stage consumes the MVP and the per-chunk offset, the fragment stage the fog and sky slots. Vulkan requires the `stage_flags` given to every `cmd_push_constants` call to cover exactly the stages the layout declares for the bytes being written, so the layout and every update read this one value rather than restating the flags.
pub const PUSH_CONSTANT_STAGES: vk::ShaderStageFlags = vk::ShaderStageFlags::from_raw(
vk::ShaderStageFlags::VERTEX.as_raw() | vk::ShaderStageFlags::FRAGMENT.as_raw(),
);
/// 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 three 16-byte vec4 slots (112 bytes total, within the 128-byte guaranteed minimum).
#[expect(
clippy::expect_used,
reason = "112 bytes (Mat4 + three vec4s) is well within u32 range"
)]
let push_constant_range = vk::PushConstantRange::default()
.stage_flags(PUSH_CONSTANT_STAGES)
.offset(0)
.size(
u32::try_from(std::mem::size_of::<glam::Mat4>())
.map(|mvp| mvp + PUSH_CONSTANT_VEC4S * VEC4_BYTES)
.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.
///
/// The SPIR-V is produced from the GLSL sources by the crate's build script and embedded from `OUT_DIR`, so the modules always correspond to the shader sources present at compile time.
///
/// # 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!(concat!(env!("OUT_DIR"), "/cube.vert.spv"));
let frag_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/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))
}