Synvael/crates/renderer/src/lib.rs

310 lines
10 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
#![allow(unsafe_code)]
//! Voxel rendering engine using Vulkan 1.3 and dynamic rendering.
//!
//! This crate provides the core `Renderer` structure and associated types
//! for handling GPU resources and drawing operations.
mod device;
pub mod error;
mod frustum;
mod instance;
pub mod meshing;
mod pipeline;
mod renderer;
mod surface;
mod swapchain;
mod sync;
pub mod vertex;
/// The maximum number of frames that can be processed by the GPU and CPU simultaneously.
pub const MAX_FRAMES_IN_FLIGHT: usize = 3;
use ash::{Entry, vk};
use gpu_allocator::vulkan::{Allocation, Allocator};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use std::ffi::c_char;
pub use error::RendererError;
pub use renderer::{MeshKey, RasterPass, RenderMode, Renderer};
use std::collections::HashMap;
impl Renderer {
/// Initializes the Vulkan renderer.
///
/// This function loads the Vulkan library, creates an instance, selects a GPU,
/// and initializes a logical device with a graphics queue.
///
/// # Errors
///
/// Returns [`RendererError`] if any initialization step fails: loading Vulkan, creating the instance, surface, device, swapchain, pipeline, allocator, or initial geometry.
///
/// # Panics
///
/// Panics if `MAX_FRAMES_IN_FLIGHT` exceeds `u32`'s range.
// TODO: partial-construction leak. Each `?` below early-returns and leaks every Vulkan resource created so far; only a fully successful `new` reaches `Drop for Renderer`. Once the renderer grows more state, wrap each resource in an RAII guard so failure paths tear them down too.
pub fn new(
display_handle: RawDisplayHandle,
window_handle: RawWindowHandle,
width: u32,
height: u32,
required_extensions: &[*const c_char],
) -> Result<Self, RendererError> {
let entry = unsafe { Entry::load() }?;
// 1. Instance and Debug Messenger
let (instance, debug_utils, debug_messenger) =
instance::create_instance(&entry, required_extensions)?;
// 2. Surface
let (surface_loader, surface) =
surface::create_surface(&entry, &instance, display_handle, window_handle)?;
// 3. Physical Device (GPU)
let physical_device = device::pick_physical_device(&instance, &surface_loader, surface)?;
// 4. Graphics Queue Index
let graphics_queue_index = device::find_graphics_queue_family(
&instance,
physical_device,
&surface_loader,
surface,
)?;
// 5. Logical Device and Queue
let (device, graphics_queue) =
device::create_logical_device(&instance, physical_device, graphics_queue_index)?;
// 6. Swapchain
let (swapchain_loader, swapchain, swapchain_images, swapchain_format, swapchain_extent) =
swapchain::create_swapchain(
&instance,
physical_device,
&device,
&surface_loader,
surface,
width,
height,
)?;
// 7. Image Views
let image_views =
swapchain::create_image_views(&device, &swapchain_images, swapchain_format)?;
// 8. Command Pool
let pool_create_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(graphics_queue_index)
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? };
// 9. Command Buffers
#[expect(
clippy::expect_used,
reason = "MAX_FRAMES_IN_FLIGHT is a small compile-time constant"
)]
let alloc_info = vk::CommandBufferAllocateInfo::default()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(
u32::try_from(MAX_FRAMES_IN_FLIGHT).expect("MAX_FRAMES_IN_FLIGHT exceeds u32"),
);
let command_buffers = unsafe { device.allocate_command_buffers(&alloc_info)? };
// 10. Synchronization Primitives
let sync =
sync::create_sync_primitives(&device, MAX_FRAMES_IN_FLIGHT, swapchain_images.len())?;
// 11. GPU Memory Allocator
let mut allocator = create_allocator(&instance, &device, physical_device)?;
// 12. Graphics Pipeline Configuration
let pipeline_layout = pipeline::create_pipeline_layout(&device)?;
// One pipeline per raster pass, built up front so selecting a mode is a bind-time choice rather than a pipeline compilation stall. All variants share `pipeline_layout`; only their rasterisation and depth-compare state differs.
let mut pipelines = [vk::Pipeline::null(); RasterPass::COUNT];
for pass in RasterPass::ALL {
pipelines[pass.index()] = pipeline::create_graphics_pipeline(
&device,
pipeline_layout,
swapchain_format,
pass.polygon_mode(),
pass.depth_compare_op(),
)?;
}
let (depth_image, depth_allocation, depth_image_view) =
create_depth_resources(&device, &mut allocator, swapchain_extent)?;
Ok(Self {
_entry: entry,
instance,
debug_utils,
debug_messenger,
physical_device,
device,
graphics_queue,
graphics_queue_index,
surface_loader,
surface,
swapchain_loader,
swapchain,
swapchain_images,
swapchain_format,
swapchain_extent,
swapchain_image_views: image_views,
command_pool,
command_buffers,
allocator: Some(allocator),
chunk_meshes: HashMap::new(),
depth_image,
depth_allocation: Some(depth_allocation),
depth_image_view,
pipeline_layout,
pipelines,
render_mode: RenderMode::default(),
sync: Some(sync),
current_frame: 0,
})
}
}
/// Creates a GPU memory allocator.
///
/// # Errors
///
/// Returns [`RendererError::AllocationError`] if the allocator cannot be initialized.
fn create_allocator(
instance: &ash::Instance,
device: &ash::Device,
physical_device: vk::PhysicalDevice,
) -> Result<gpu_allocator::vulkan::Allocator, RendererError> {
let allocator_create_info = gpu_allocator::vulkan::AllocatorCreateDesc {
instance: instance.clone(),
device: device.clone(),
physical_device,
debug_settings: gpu_allocator::AllocatorDebugSettings::default(),
buffer_device_address: false,
allocation_sizes: gpu_allocator::AllocationSizes::default(),
};
let allocator = gpu_allocator::vulkan::Allocator::new(&allocator_create_info)
.map_err(RendererError::AllocationError)?;
Ok(allocator)
}
/// Creates the depth buffer resources (image, memory, and view).
///
/// # Errors
///
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the depth image or its view cannot be created.
fn create_depth_resources(
device: &ash::Device,
allocator: &mut Allocator,
extent: vk::Extent2D,
) -> Result<(vk::Image, Allocation, vk::ImageView), RendererError> {
let depth_format = vk::Format::D32_SFLOAT;
let image_create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(depth_format)
.extent(vk::Extent3D {
width: extent.width,
height: extent.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED);
let depth_image = unsafe { device.create_image(&image_create_info, None)? };
let requirements = unsafe { device.get_image_memory_requirements(depth_image) };
let depth_allocation = allocator.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
name: "Depth Image",
requirements,
location: gpu_allocator::MemoryLocation::GpuOnly,
linear: false,
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
})?;
unsafe {
device.bind_image_memory(
depth_image,
depth_allocation.memory(),
depth_allocation.offset(),
)?;
}
let view_create_info = vk::ImageViewCreateInfo::default()
.image(depth_image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(depth_format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::DEPTH,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let depth_image_view = unsafe { device.create_image_view(&view_create_info, None)? };
Ok((depth_image, depth_allocation, depth_image_view))
}
/// Helper function to create and populate a GPU buffer.
///
/// # Errors
///
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the buffer cannot be created or bound.
fn create_gpu_buffer(
device: &ash::Device,
allocator: &mut Allocator,
data: &[u8],
usage: vk::BufferUsageFlags,
name: &str,
) -> Result<(vk::Buffer, Allocation), RendererError> {
let size = data.len() as u64;
let buffer_info = vk::BufferCreateInfo::default()
.size(size)
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.create_buffer(&buffer_info, None)? };
let requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
let allocation = allocator.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
name,
requirements,
location: gpu_allocator::MemoryLocation::CpuToGpu,
linear: true,
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
})?;
unsafe {
device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())?;
}
let ptr = allocation
.mapped_ptr()
.ok_or(RendererError::NoSuitableGpu)?
.as_ptr();
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr.cast(), data.len());
}
Ok((buffer, allocation))
}