synvael/crates/renderer/src/lib.rs
Serkyo 70e08dd883 feat(renderer): implement Drop trait for resource cleanup
Ensured that the Logical Device, Debug Messenger, and Instance are destroyed in the correct reverse order when the Renderer is dropped. Added documentation for the cleanup process.
2026-04-30 14:26:18 +02:00

223 lines
8.8 KiB
Rust

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<ext::debug_utils::Instance>,
/// 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<Self, RendererError> {
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<vk::PhysicalDevice, RendererError> {
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<u32, RendererError> {
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
}