// 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; pub mod stats; 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}; pub use stats::{ProjectionInfo, RenderStats, SwapchainInfo}; 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 { 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, present_mode: swapchain::present_mode_name(swapchain::PRESENT_MODE), frames_presented: 0, frames_skipped: 0, last_frame_stats: None, }) } } /// 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 { 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)) }