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.
223 lines
8.8 KiB
Rust
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
|
|
} |