// 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 instance; pub mod mesh; mod pipeline; mod renderer; mod surface; mod swapchain; mod sync; /// 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::Renderer; use crate::mesh::Vertex; 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. /// /// # Panics /// /// Panics if `MAX_FRAMES_IN_FLIGHT` or vertex data sizes exceed `u32`/`u64` limits. // 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)] 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)?; let graphics_pipeline = pipeline::create_graphics_pipeline(&device, pipeline_layout, swapchain_format)?; let (vertex_buffer, vertex_allocation, index_buffer, index_allocation) = create_geometry(&device, &mut allocator)?; let index_count = 36; 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), index_buffer, index_allocation: Some(index_allocation), index_count, depth_image, depth_allocation: Some(depth_allocation), depth_image_view, pipeline_layout, graphics_pipeline, vertex_buffer, vertex_allocation: Some(vertex_allocation), sync: Some(sync), current_frame: 0, }) } } /// Creates a GPU memory allocator. 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 3D geometry buffers (vertex and index) for a cube. fn create_geometry( device: &ash::Device, allocator: &mut Allocator, ) -> Result<(vk::Buffer, Allocation, vk::Buffer, Allocation), RendererError> { let vertices = [ // Front face Vertex { position: [-0.5, -0.5, 0.5], color: [1.0, 0.0, 0.0], }, Vertex { position: [0.5, -0.5, 0.5], color: [0.0, 1.0, 0.0], }, Vertex { position: [0.5, 0.5, 0.5], color: [0.0, 0.0, 1.0], }, Vertex { position: [-0.5, 0.5, 0.5], color: [1.0, 1.0, 1.0], }, // Back face Vertex { position: [-0.5, -0.5, -0.5], color: [1.0, 0.0, 0.0], }, Vertex { position: [0.5, -0.5, -0.5], color: [0.0, 1.0, 0.0], }, Vertex { position: [0.5, 0.5, -0.5], color: [0.0, 0.0, 1.0], }, Vertex { position: [-0.5, 0.5, -0.5], color: [1.0, 1.0, 1.0], }, ]; let indices: [u32; 36] = [ 0, 1, 2, 2, 3, 0, // front 1, 5, 6, 6, 2, 1, // right 7, 6, 5, 5, 4, 7, // back 4, 0, 3, 3, 7, 4, // left 4, 5, 1, 1, 0, 4, // bottom 3, 2, 6, 6, 7, 3, // top ]; let (vertex_buffer, vertex_allocation) = create_gpu_buffer( device, allocator, bytemuck::cast_slice(&vertices), vk::BufferUsageFlags::VERTEX_BUFFER, "Vertex Buffer", )?; let (index_buffer, index_allocation) = create_gpu_buffer( device, allocator, bytemuck::cast_slice(&indices), vk::BufferUsageFlags::INDEX_BUFFER, "Index Buffer", )?; Ok(( vertex_buffer, vertex_allocation, index_buffer, index_allocation, )) } /// Creates the depth buffer resources (image, memory, and view). 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. 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)) }