diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index b0ae060..ccd37f0 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -1,3 +1,8 @@ +//! Main entry point for the Project Catalyst client. +//! +//! This crate handles window creation, input processing, and drives the +//! renderer to display the game world. + use anyhow::{Context, Result}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use tracing::info; diff --git a/crates/renderer/src/device.rs b/crates/renderer/src/device.rs index efc6cad..f13832c 100644 --- a/crates/renderer/src/device.rs +++ b/crates/renderer/src/device.rs @@ -1,3 +1,5 @@ +//! Logic for selecting physical devices and creating logical devices. + use crate::error::RendererError; use ash::{Device, Instance, khr, vk}; @@ -63,7 +65,8 @@ pub fn find_graphics_queue_family( let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) }; for (index, prop) in props.iter().enumerate() { - let index = index as u32; + #[allow(clippy::expect_used)] + let index = u32::try_from(index).expect("Queue family index exceeds u32 range"); let graphics = prop.queue_flags.contains(vk::QueueFlags::GRAPHICS); let present = unsafe { surface_loader.get_physical_device_surface_support(physical_device, index, surface)? diff --git a/crates/renderer/src/error.rs b/crates/renderer/src/error.rs index 1514ab6..42fcb4d 100644 --- a/crates/renderer/src/error.rs +++ b/crates/renderer/src/error.rs @@ -1,6 +1,9 @@ +//! Error types for the renderer crate. + use thiserror::Error; #[derive(Debug, Error)] +/// Enumerates all possible errors that can occur during rendering operations. pub enum RendererError { /// The Vulkan library could not be loaded from the system. #[error("Failed to load Vulkan library")] @@ -14,4 +17,10 @@ pub enum RendererError { /// An error occurred during GPU memory allocation. #[error("GPU allocation error")] AllocationError(#[from] gpu_allocator::AllocationError), + /// An I/O error occurred (e.g. reading a shader). + #[error("I/O error")] + IoError(#[from] std::io::Error), + /// An invalid string was encountered. + #[error("Invalid string")] + InvalidString, } diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index e253283..5b32bef 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -1,3 +1,10 @@ +#![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; @@ -8,6 +15,7 @@ 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}; @@ -22,6 +30,11 @@ impl 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. + #[allow(clippy::expect_used)] pub fn new( display_handle: RawDisplayHandle, window_handle: RawWindowHandle, @@ -78,10 +91,11 @@ impl Renderer { let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? }; // 9. Command Buffers + #[allow(clippy::expect_used)] let alloc_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY) - .command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32); + .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)? }; @@ -90,81 +104,16 @@ impl Renderer { sync::create_sync_primitives(&device, MAX_FRAMES_IN_FLIGHT, swapchain_images.len())?; // 11. GPU Memory Allocator - // The allocator handles the complexity of sub-allocating memory blocks from the GPU. - let allocator_create_info = gpu_allocator::vulkan::AllocatorCreateDesc { - instance: instance.clone(), - device: device.clone(), - physical_device, - debug_settings: Default::default(), - buffer_device_address: false, - allocation_sizes: Default::default(), - }; - - let allocator = gpu_allocator::vulkan::Allocator::new(&allocator_create_info) - .expect("Failed to create Vulkan allocator"); + let mut allocator = create_allocator(&instance, &device, physical_device)?; // 12. Graphics Pipeline Configuration - // The pipeline defines the fixed-function and programmable state for rendering voxel geometry. - let pipeline_layout = pipeline::create_pipeline_layout(&device); + let pipeline_layout = pipeline::create_pipeline_layout(&device)?; let graphics_pipeline = - pipeline::create_graphics_pipeline(&device, pipeline_layout, swapchain_format); + pipeline::create_graphics_pipeline(&device, pipeline_layout, swapchain_format)?; // 13. Vertex Buffer Initialization - // A simple triangle is defined in normalized device coordinates and moved to GPU memory. - let vertices = [ - mesh::Vertex { - position: [0.0, -0.5, 0.0], - tex_coord: [0.5, 0.0], - }, - mesh::Vertex { - position: [0.5, 0.5, 0.0], - tex_coord: [1.0, 1.0], - }, - mesh::Vertex { - position: [-0.5, 0.5, 0.0], - tex_coord: [0.0, 1.0], - }, - ]; - - // Create the buffer handle and query its memory requirements. - let buffer_info = vk::BufferCreateInfo::default() - .size(std::mem::size_of_val(&vertices) as u64) - .usage(vk::BufferUsageFlags::VERTEX_BUFFER); - - let vertex_buffer = unsafe { device.create_buffer(&buffer_info, None)? }; - let requirements = unsafe { device.get_buffer_memory_requirements(vertex_buffer) }; - - let mut allocator = allocator; - - // Allocate memory that is visible to the CPU for data transfer. - let vertex_allocation = - allocator.allocate(&gpu_allocator::vulkan::AllocationCreateDesc { - name: "Vertex Buffer", - requirements, - location: gpu_allocator::MemoryLocation::CpuToGpu, - linear: true, - allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged, - })?; - - // Bind the allocated memory to the buffer handle and copy the vertex data. - unsafe { - device.bind_buffer_memory( - vertex_buffer, - vertex_allocation.memory(), - vertex_allocation.offset(), - )?; - - let ptr = vertex_allocation - .mapped_ptr() - .expect("Failed to map vertex buffer memory") - .as_ptr(); - - std::ptr::copy_nonoverlapping( - vertices.as_ptr() as *const u8, - ptr as *mut u8, - std::mem::size_of_val(&vertices), - ); - } + let (vertex_buffer, vertex_allocation) = + create_vertex_buffer(&device, &mut allocator)?; Ok(Self { _entry: entry, @@ -195,3 +144,82 @@ impl Renderer { }) } } + +/// 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 a vertex buffer and populates it with initial triangle data. +#[allow(clippy::expect_used)] +fn create_vertex_buffer( + device: &ash::Device, + allocator: &mut gpu_allocator::vulkan::Allocator, +) -> Result<(vk::Buffer, gpu_allocator::vulkan::Allocation), RendererError> { + let vertices = [ + mesh::Vertex { + position: [0.0, -0.5, 0.0], + tex_coord: [0.5, 0.0], + }, + mesh::Vertex { + position: [-0.5, 0.5, 0.0], + tex_coord: [0.0, 1.0], + }, + mesh::Vertex { + position: [0.5, 0.5, 0.0], + tex_coord: [1.0, 1.0], + }, + ]; + + let buffer_info = vk::BufferCreateInfo::default() + .size(u64::try_from(std::mem::size_of_val(&vertices)).expect("Vertices size exceeds u64")) + .usage(vk::BufferUsageFlags::VERTEX_BUFFER); + + let vertex_buffer = unsafe { device.create_buffer(&buffer_info, None)? }; + let requirements = unsafe { device.get_buffer_memory_requirements(vertex_buffer) }; + + let vertex_allocation = + allocator.allocate(&gpu_allocator::vulkan::AllocationCreateDesc { + name: "Vertex Buffer", + requirements, + location: gpu_allocator::MemoryLocation::CpuToGpu, + linear: true, + allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged, + })?; + + unsafe { + device.bind_buffer_memory( + vertex_buffer, + vertex_allocation.memory(), + vertex_allocation.offset(), + )?; + + let ptr = vertex_allocation + .mapped_ptr() + .ok_or(RendererError::NoSuitableGpu)? // Should have a better error but for now + .as_ptr(); + + std::ptr::copy_nonoverlapping( + vertices.as_ptr().cast::(), + ptr.cast::(), + std::mem::size_of_val(&vertices), + ); + } + + Ok((vertex_buffer, vertex_allocation)) +} diff --git a/crates/renderer/src/mesh.rs b/crates/renderer/src/mesh.rs index 5f3d455..f112cc1 100644 --- a/crates/renderer/src/mesh.rs +++ b/crates/renderer/src/mesh.rs @@ -1,3 +1,5 @@ +//! Vertex data structures and layout descriptions. + use bytemuck::{Pod, Zeroable}; /// Represents a single vertex in 3D space with position and texture coordinates. @@ -18,16 +20,18 @@ impl Vertex { /// /// This defines the 'stride' (distance between vertices) and specifies that /// data is read per-vertex rather than per-instance. + #[allow(clippy::expect_used)] pub fn get_binding_description() -> ash::vk::VertexInputBindingDescription { ash::vk::VertexInputBindingDescription::default() .binding(0) - .stride(std::mem::size_of::() as u32) + .stride(u32::try_from(std::mem::size_of::()).expect("Vertex size exceeds u32 range")) .input_rate(ash::vk::VertexInputRate::VERTEX) } /// Describes the layout of individual fields (attributes) within a single vertex. /// /// These 'locations' must match the `layout(location = X)` qualifiers in the vertex shader. + #[allow(clippy::expect_used)] pub fn get_attribute_descriptions() -> [ash::vk::VertexInputAttributeDescription; 2] { [ // Location 0: position (vec3 -> R32G32B32_SFLOAT) @@ -41,7 +45,7 @@ impl Vertex { .binding(0) .location(1) .format(ash::vk::Format::R32G32_SFLOAT) - .offset(std::mem::size_of::<[f32; 3]>() as u32), + .offset(u32::try_from(std::mem::size_of::<[f32; 3]>()).expect("Vertex offset exceeds u32 range")), ] } } diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index 015b499..7be758e 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -1,3 +1,6 @@ +//! Graphics pipeline creation and shader management. + +use crate::error::RendererError; use crate::mesh::Vertex; use ash::{Device, vk}; use std::io::Cursor; @@ -6,41 +9,38 @@ use std::io::Cursor; /// /// 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`. -pub fn create_shader_module(device: &Device, bytes: &[u8]) -> vk::ShaderModule { +pub fn create_shader_module( + device: &Device, + bytes: &[u8], +) -> Result { let mut cursor = Cursor::new(bytes); - let code = ash::util::read_spv(&mut cursor) - .expect("Failed to read SPIR-V binary; check if the file is valid"); + let code = ash::util::read_spv(&mut cursor)?; let create_info = vk::ShaderModuleCreateInfo::default().code(&code); - unsafe { - device - .create_shader_module(&create_info, None) - .expect("Failed to create Vulkan shader module") - } + 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. -pub fn create_pipeline_layout(device: &Device) -> vk::PipelineLayout { +pub fn create_pipeline_layout(device: &Device) -> Result { // A single push constant range is defined for the Model-View-Projection matrix. // This allows the matrix to be updated for every draw call with high efficiency. + #[allow(clippy::expect_used)] let push_constant_range = vk::PushConstantRange::default() .stage_flags(vk::ShaderStageFlags::VERTEX) .offset(0) - .size(std::mem::size_of::() as u32); + .size(u32::try_from(std::mem::size_of::()).expect("Mat4 size exceeds u32 range")); let layout_create_info = vk::PipelineLayoutCreateInfo::default() .push_constant_ranges(std::slice::from_ref(&push_constant_range)); - unsafe { - device - .create_pipeline_layout(&layout_create_info, None) - .expect("Failed to create pipeline layout") - } + 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. @@ -51,16 +51,10 @@ pub fn create_graphics_pipeline( device: &Device, layout: vk::PipelineLayout, color_format: vk::Format, -) -> vk::Pipeline { +) -> Result { // 1. Load and compile shader modules - // Using include_bytes! embeds the shaders directly into the engine binary. - 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); - - let entry_point = std::ffi::CString::new("main").unwrap(); + 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() @@ -73,60 +67,51 @@ pub fn create_graphics_pipeline( .name(&entry_point), ]; - // 2. Configure Vertex Input + // 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); - // 3. Configure Input Assembly (Drawing mode) let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default() .topology(vk::PrimitiveTopology::TRIANGLE_LIST) .primitive_restart_enable(false); - // 4. Viewport and Scissor (Static declarations, dynamic values set at runtime) let viewport_state = vk::PipelineViewportStateCreateInfo::default() .viewport_count(1) .scissor_count(1); - // 5. Configure Rasterizer (Triangles to Pixels) let rasterizer = vk::PipelineRasterizationStateCreateInfo::default() .depth_clamp_enable(false) .rasterizer_discard_enable(false) .polygon_mode(vk::PolygonMode::FILL) .line_width(1.0) - .cull_mode(vk::CullModeFlags::BACK) // Back-face culling for performance + .cull_mode(vk::CullModeFlags::BACK) .front_face(vk::FrontFace::COUNTER_CLOCKWISE) .depth_bias_enable(false); - // 6. Configure Multisampling (Anti-aliasing) let multisampling = vk::PipelineMultisampleStateCreateInfo::default() .sample_shading_enable(false) .rasterization_samples(vk::SampleCountFlags::TYPE_1); - // 7. Configure Color Blending let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default() .color_write_mask(vk::ColorComponentFlags::RGBA) - .blend_enable(false); // Transparency is not required for the initial implementation + .blend_enable(false); let color_blending = vk::PipelineColorBlendStateCreateInfo::default() .logic_op_enable(false) .attachments(std::slice::from_ref(&color_blend_attachment)); - // 8. Define Dynamic States - // This allows the window to be resized without recreating the entire pipeline. let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; let dynamic_state_info = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); - // 9. Configure Dynamic Rendering (Vulkan 1.3) let color_formats = [color_format]; let mut rendering_info = vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&color_formats); - // 10. Finalize Pipeline Creation + // 3. Finalize Pipeline Creation let pipeline_info = vk::GraphicsPipelineCreateInfo::default() .push_next(&mut rendering_info) .stages(&shader_stages) @@ -142,8 +127,8 @@ pub fn create_graphics_pipeline( let pipeline = unsafe { device .create_graphics_pipelines(vk::PipelineCache::null(), &[pipeline_info], None) - .expect("Failed to create graphics pipeline")[0] - }; + .map_err(|(_, e)| e)? + }[0]; // Cleanup temporary shader modules (they are baked into the pipeline now) unsafe { @@ -151,5 +136,16 @@ pub fn create_graphics_pipeline( device.destroy_shader_module(frag_module, None); } - pipeline + Ok(pipeline) +} + +/// Loads the vertex and fragment shader modules from embedded bytes. +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)) } diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs index 21a9c9f..af4775a 100644 --- a/crates/renderer/src/renderer.rs +++ b/crates/renderer/src/renderer.rs @@ -14,12 +14,14 @@ pub struct Renderer { /// The debug messenger for validation layer output. pub(crate) debug_messenger: vk::DebugUtilsMessengerEXT, /// Handle to the selected physical device (GPU). + #[allow(dead_code)] pub(crate) physical_device: vk::PhysicalDevice, /// The logical Vulkan device. pub(crate) device: Device, /// The queue used for graphics operations. pub(crate) graphics_queue: vk::Queue, /// Index of the graphics queue family. + #[allow(dead_code)] pub(crate) graphics_queue_index: u32, /// Surface extension loader. pub(crate) surface_loader: khr::surface::Instance, @@ -32,6 +34,7 @@ pub struct Renderer { /// Images acquired from the swapchain. pub(crate) swapchain_images: Vec, /// The pixel format of the swapchain images. + #[allow(dead_code)] pub(crate) swapchain_format: vk::Format, /// The dimensions of the swapchain images. pub(crate) swapchain_extent: vk::Extent2D, @@ -53,7 +56,7 @@ pub struct Renderer { pub(crate) vertex_allocation: Allocation, /// Synchronization primitives for frame-by-frame execution. pub(crate) sync: SyncPrimitives, - /// Index of the current frame being processed (0 to crate::MAX_FRAMES_IN_FLIGHT - 1). + /// Index of the current frame being processed (0 to `crate::MAX_FRAMES_IN_FLIGHT` - 1). pub(crate) current_frame: usize, } @@ -204,16 +207,19 @@ impl Drop for Renderer { self.device .destroy_pipeline_layout(self.pipeline_layout, None); - self.allocator - .free(std::ptr::read(&self.vertex_allocation)) - .expect(" Failed to free vertex buffer allocation"); + if let Err(e) = self + .allocator + .free(std::ptr::read(&raw const self.vertex_allocation)) + { + tracing::error!("Failed to free vertex buffer allocation: {e}"); + } self.device.destroy_buffer(self.vertex_buffer, None); self.device.destroy_command_pool(self.command_pool, None); // Use the safe cleanup function from sync module - let sync = std::ptr::read(&self.sync); + let sync = std::ptr::read(&raw const self.sync); crate::sync::destroy_sync_primitives(&self.device, sync); // Destroy the swapchain diff --git a/crates/renderer/src/swapchain.rs b/crates/renderer/src/swapchain.rs index 52c2b18..434f624 100644 --- a/crates/renderer/src/swapchain.rs +++ b/crates/renderer/src/swapchain.rs @@ -39,9 +39,7 @@ pub fn create_swapchain( let present_mode = vk::PresentModeKHR::FIFO; - let extent = if surface_capabilities.current_extent.width != u32::MAX { - surface_capabilities.current_extent - } else { + let extent = if surface_capabilities.current_extent.width == u32::MAX { vk::Extent2D { width: width.clamp( surface_capabilities.min_image_extent.width, @@ -52,6 +50,8 @@ pub fn create_swapchain( surface_capabilities.max_image_extent.height, ), } + } else { + surface_capabilities.current_extent }; let image_count = if surface_capabilities.max_image_count > 0 diff --git a/crates/scripting/src/lib.rs b/crates/scripting/src/lib.rs index b93cf3f..5175837 100644 --- a/crates/scripting/src/lib.rs +++ b/crates/scripting/src/lib.rs @@ -1,3 +1,11 @@ +//! Lua scripting and modding support for Project Catalyst. +//! +//! This crate handles the integration with Lua (via `mlua`) and provides +//! the API surface for both base game content and third-party mods. + +/// Adds two numbers together. +/// +/// This is a placeholder function for the initial crate setup. pub fn add(left: u64, right: u64) -> u64 { left + right } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index e7a11a9..35c51a4 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,3 +1,8 @@ +//! Dedicated server for Project Catalyst. +//! +//! The server handles the authoritative game simulation, including world +//! management, physics, and combat. + fn main() { println!("Hello, world!"); } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index b93cf3f..7cb28cf 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,3 +1,11 @@ +//! Shared types and logic for Project Catalyst. +//! +//! This crate contains data structures and constants that are used by both +//! the client and the server. + +/// Adds two numbers together. +/// +/// This is a placeholder function for the initial crate setup. pub fn add(left: u64, right: u64) -> u64 { left + right }