pub mod error; use std::ffi::{CStr, c_char}; use ash::{Entry, Instance, ext, vk, Device}; use tracing::{debug, error, info, warn}; pub use error::RendererError; /// The core renderer structure holding the Vulkan instance and debug resources. pub struct Renderer { /// The entry point to the Vulkan library. _entry: Entry, /// The Vulkan instance handle. instance: Instance, /// The debug utils instance, if enabled (debug builds only). debug_utils: Option, /// The debug messenger handle, if enabled (debug builds only). debug_messenger: vk::DebugUtilsMessengerEXT, /// The selected physical device (GPU). physical_device: vk::PhysicalDevice, /// The logical device handle. device: Device, /// The handle to the graphics queue. graphics_queue: vk::Queue, /// The index of the graphics queue family. graphics_queue_index: u32, } 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. /// /// `required_extensions` are raw C-strings (pointers) provided by the windowing system. pub fn new(required_extensions: &[*const c_char]) -> Result { let entry = unsafe { Entry::load() }?; // Prepare extensions and layers let mut extensions: Vec<*const c_char> = required_extensions.to_vec(); let mut layers: Vec<*const c_char> = Vec::new(); // Enable debug extensions and validation layers in debug builds #[cfg(debug_assertions)] { extensions.push(ext::debug_utils::NAME.as_ptr()); layers.push(c"VK_LAYER_KHRONOS_validation".as_ptr()); } let app_info = vk::ApplicationInfo::default() .api_version(vk::API_VERSION_1_3); let create_info = vk::InstanceCreateInfo::default() .application_info(&app_info) .enabled_extension_names(&extensions) .enabled_layer_names(&layers); // Create the Vulkan instance let instance = unsafe { entry.create_instance(&create_info, None)? }; // Setup debug messenger for validation layer feedback #[cfg(debug_assertions)] let (debug_utils, debug_messenger) = { let debug_info = vk::DebugUtilsMessengerCreateInfoEXT::default() .message_severity( vk::DebugUtilsMessageSeverityFlagsEXT::WARNING | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR ) .message_type( vk::DebugUtilsMessageTypeFlagsEXT::GENERAL | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE ) .pfn_user_callback(Some(vulkan_debug_callback)); let utils = ext::debug_utils::Instance::new(&entry, &instance); let messenger = unsafe { utils.create_debug_utils_messenger(&debug_info, None)? }; (Some(utils), messenger) }; #[cfg(not(debug_assertions))] let (debug_utils, debug_messenger) = (None, vk::DebugUtilsMessengerEXT::null()); let physical_device = Self::pick_physical_device(&instance)?; // Find the graphics queue and create the logical device let graphics_queue_index = Self::find_graphics_queue_family(&instance, physical_device)?; let (device, graphics_queue) = Self::create_logical_device(&instance, physical_device, graphics_queue_index)?; Ok(Self { _entry: entry, instance, debug_utils, debug_messenger, physical_device, device, graphics_queue, graphics_queue_index, }) } /// Selects the best physical device (GPU) available on the system. /// /// This function filters for devices supporting graphics operations and scores /// them based on their type, preferring discrete GPUs. fn pick_physical_device(instance: &Instance) -> Result { let devices = unsafe { instance.enumerate_physical_devices()? }; let selected = devices.into_iter() .filter(|&device| { // Check if the device has a queue family that supports graphics let props = unsafe { instance.get_physical_device_queue_family_properties(device) }; props.iter().any(|p| p.queue_flags.contains(vk::QueueFlags::GRAPHICS)) }) .max_by_key(|&device| { // Score the device based on its type let props = unsafe { instance.get_physical_device_properties(device) }; match props.device_type { vk::PhysicalDeviceType::DISCRETE_GPU => 1000, vk::PhysicalDeviceType::INTEGRATED_GPU => 100, _ => 1, } }) .ok_or(RendererError::NoSuitableGpu)?; let props = unsafe { instance.get_physical_device_properties(selected) }; info!("Selected GPU: {:?}", unsafe { CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy() }); Ok(selected) } /// Finds the index of the first queue family that supports graphics operations. fn find_graphics_queue_family( instance: &Instance, physical_device: vk::PhysicalDevice, ) -> Result { let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) }; props.iter().enumerate() .find(|(_, p)| p.queue_flags.contains(vk::QueueFlags::GRAPHICS)) .map(|(i, _)| i as u32) .ok_or(RendererError::NoSuitableGpu) } /// Creates a logical device and retrieves the handle to its graphics queue. fn create_logical_device( instance: &Instance, physical_device: vk::PhysicalDevice, queue_index: u32, ) -> Result<(Device, vk::Queue), RendererError> { // Request a single queue from the graphics family let queue_priorities = [1.0]; let queue_info = vk::DeviceQueueCreateInfo::default() .queue_family_index(queue_index) .queue_priorities(&queue_priorities); // Enable modern Vulkan 1.3 features let mut features_13 = vk::PhysicalDeviceVulkan13Features::default() .dynamic_rendering(true) // Removes the need for RenderPasses/Framebuffers .synchronization2(true); // Cleaner memory barriers and synchronization // We need the swapchain extension to present images to the window let device_extensions = [ash::khr::swapchain::NAME.as_ptr()]; let create_info = vk::DeviceCreateInfo::default() .queue_create_infos(std::slice::from_ref(&queue_info)) .enabled_extension_names(&device_extensions) .push_next(&mut features_13); let device = unsafe { instance.create_device(physical_device, &create_info, None)? }; let graphics_queue = unsafe { device.get_device_queue(queue_index, 0) }; Ok((device, graphics_queue)) } } /// Ensures all Vulkan resources are destroyed in the correct order. impl Drop for Renderer { fn drop(&mut self) { unsafe { self.device.destroy_device(None); if let Some(utils) = &self.debug_utils { utils.destroy_debug_utils_messenger(self.debug_messenger, None); } self.instance.destroy_instance(None); } } } /// The callback function invoked by Vulkan's validation layers. /// /// This function translates Vulkan debug messages into `tracing` events. unsafe extern "system" fn vulkan_debug_callback( message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, _message_type: vk::DebugUtilsMessageTypeFlagsEXT, p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, _user_data: *mut std::ffi::c_void, ) -> vk::Bool32 { // Safety: p_callback_data is guaranteed to be valid by the Vulkan spec let callback_data = unsafe { *p_callback_data }; // Convert raw C string to Rust string for logging let message = if callback_data.p_message.is_null() { "".into() } else { unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } }; // Route severity to matching tracing macro match message_severity { vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => debug!("{message}"), vk::DebugUtilsMessageSeverityFlagsEXT::INFO => info!("{message}"), vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => warn!("{message}"), vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => error!("{message}"), _ => info!("{message}"), } // TRUE aborts the Vulkan call that triggered the warning vk::FALSE }