Synvael/crates/renderer/src/lib.rs
Serkyo 5ee9fc1bd6 feat(renderer): implement surface creation and support checks
Created the OS surface and ensured that the selected GPU and queue family support presentation to that surface. Updated Drop to properly destroy the surface and improved documentation.
2026-04-30 14:56:45 +02:00

268 lines
11 KiB
Rust

pub mod error;
use std::ffi::{CStr, c_char};
use ash::{Device, Entry, Instance, ext, khr, vk};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
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,
surface_loader: khr::surface::Instance,
surface: vk::SurfaceKHR,
}
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.
///
/// `display_handle` and `window_handle` are used to create the OS-specific surface.
/// `required_extensions` are raw C-strings (pointers) provided by the windowing system.
pub fn new(
display_handle: RawDisplayHandle,
window_handle: RawWindowHandle,
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());
// Create the OS surface
let surface = unsafe {
ash_window::create_surface(&entry, &instance, display_handle, window_handle, None)?
};
let surface_loader = khr::surface::Instance::new(&entry, &instance);
// Pick a physical device that supports our surface
let physical_device = Self::pick_physical_device(&instance, &surface_loader, surface)?;
// Find the graphics queue family (which must also support presentation)
let graphics_queue_index = Self::find_graphics_queue_family(&instance, physical_device, &surface_loader, surface)?;
// Create the logical 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,
surface_loader,
surface,
})
}
/// 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,
surface_loader: &khr::surface::Instance,
surface: vk::SurfaceKHR,
) -> 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 AND presentation
let props = unsafe { instance.get_physical_device_queue_family_properties(device) };
props.iter().enumerate().any(|(i, p)| {
let supports_graphics = p.queue_flags.contains(vk::QueueFlags::GRAPHICS);
let supports_present = unsafe {
surface_loader.get_physical_device_surface_support(device, i as u32, surface)
.unwrap_or(false)
};
supports_graphics && supports_present
})
})
.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 both graphics and presentation.
fn find_graphics_queue_family(
instance: &Instance,
physical_device: vk::PhysicalDevice,
surface_loader: &khr::surface::Instance,
surface: vk::SurfaceKHR,
) -> Result<u32, RendererError> {
let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) };
props.iter().enumerate()
.find(|&(i, p)| {
let supports_graphics = p.queue_flags.contains(vk::QueueFlags::GRAPHICS);
let supports_present = unsafe {
surface_loader.get_physical_device_surface_support(physical_device, i as u32, surface)
.unwrap_or(false)
};
supports_graphics && supports_present
})
.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 = [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 {
// 1. Destroy the logical device first
self.device.destroy_device(None);
// 2. Destroy the surface
self.surface_loader.destroy_surface(self.surface, None);
// 3. Destroy the debug messenger if we created one
if let Some(utils) = &self.debug_utils {
utils.destroy_debug_utils_messenger(self.debug_messenger, None);
}
// 4. Finally, destroy the instance
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
}