diff --git a/Cargo.lock b/Cargo.lock index 277d568..fbed88c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -144,6 +144,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "bytes" @@ -437,6 +451,28 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "presser", + "thiserror 2.0.18", + "windows", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -701,6 +737,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -710,10 +755,10 @@ dependencies = [ "bitflags 2.11.1", "block2", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] @@ -725,9 +770,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -737,8 +782,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -749,8 +794,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -760,9 +805,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -772,9 +817,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ "block2", - "objc2", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -793,7 +838,17 @@ dependencies = [ "block2", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", ] [[package]] @@ -803,9 +858,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -816,8 +871,19 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -828,9 +894,9 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -839,8 +905,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -851,12 +917,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -871,8 +937,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -883,9 +949,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.1", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -971,6 +1037,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1055,6 +1127,9 @@ version = "0.1.0" dependencies = [ "ash", "ash-window", + "bytemuck", + "glam", + "gpu-allocator", "raw-window-handle", "thiserror 2.0.18", "tracing", @@ -1673,12 +1748,107 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1722,6 +1892,15 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1793,9 +1972,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index 27e7b4e..96b65d7 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -9,3 +9,6 @@ ash-window = "0.13.0" raw-window-handle = "0.6.2" thiserror = "2.0.18" tracing = "0.1.44" +gpu-allocator = "0.28.0" +bytemuck = { version = "1.21", features = ["derive"] } +glam = "0.29" diff --git a/crates/renderer/src/device.rs b/crates/renderer/src/device.rs new file mode 100644 index 0000000..e572d16 --- /dev/null +++ b/crates/renderer/src/device.rs @@ -0,0 +1,98 @@ +use ash::{Device, Instance, khr, vk}; +use crate::error::RendererError; + +/// Picks a physical device (GPU) that supports the required features and extensions. +pub fn pick_physical_device( + instance: &Instance, + surface_loader: &khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> Result { + let devices = unsafe { instance.enumerate_physical_devices()? }; + + for device in devices { + if is_device_suitable(instance, device, surface_loader, surface) { + let props = unsafe { instance.get_physical_device_properties(device) }; + let name = unsafe { std::ffi::CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy() }; + tracing::info!("Selected GPU: \"{name}\""); + return Ok(device); + } + } + + Err(RendererError::NoSuitableGpu) +} + +/// Creates a logical device and retrieves the graphics queue. +pub fn create_logical_device( + instance: &Instance, + physical_device: vk::PhysicalDevice, + queue_family_index: u32, +) -> Result<(Device, vk::Queue), RendererError> { + let priorities = [1.0]; + let queue_info = vk::DeviceQueueCreateInfo::default() + .queue_family_index(queue_family_index) + .queue_priorities(&priorities); + + let device_extensions = [khr::swapchain::NAME.as_ptr()]; + + // Enable Vulkan 1.3 features + let mut synchronization2_features = vk::PhysicalDeviceSynchronization2Features::default() + .synchronization2(true); + let mut dynamic_rendering_features = vk::PhysicalDeviceDynamicRenderingFeatures::default() + .dynamic_rendering(true); + + let create_info = vk::DeviceCreateInfo::default() + .queue_create_infos(std::slice::from_ref(&queue_info)) + .enabled_extension_names(&device_extensions) + .push_next(&mut synchronization2_features) + .push_next(&mut dynamic_rendering_features); + + let device = unsafe { instance.create_device(physical_device, &create_info, None)? }; + let graphics_queue = unsafe { device.get_device_queue(queue_family_index, 0) }; + + Ok((device, graphics_queue)) +} + +/// Finds a queue family that supports both graphics commands and presentation. +pub fn find_graphics_queue_family( + instance: &Instance, + physical_device: vk::PhysicalDevice, + surface_loader: &khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> Result { + let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) }; + + for (index, prop) in props.iter().enumerate() { + let index = index as u32; + let graphics = prop.queue_flags.contains(vk::QueueFlags::GRAPHICS); + let present = unsafe { + surface_loader.get_physical_device_surface_support(physical_device, index, surface)? + }; + + if graphics && present { + return Ok(index); + } + } + + Err(RendererError::NoSuitableGpu) +} + +fn is_device_suitable( + instance: &Instance, + device: vk::PhysicalDevice, + surface_loader: &khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> bool { + let extensions = unsafe { instance.enumerate_device_extension_properties(device).unwrap_or_default() }; + let has_swapchain = extensions.iter().any(|ext| { + unsafe { std::ffi::CStr::from_ptr(ext.extension_name.as_ptr()) == khr::swapchain::NAME } + }); + + let formats = unsafe { + surface_loader.get_physical_device_surface_formats(device, surface).unwrap_or_default() + }; + let present_modes = unsafe { + surface_loader.get_physical_device_surface_present_modes(device, surface).unwrap_or_default() + }; + + has_swapchain && !formats.is_empty() && !present_modes.is_empty() +} diff --git a/crates/renderer/src/instance.rs b/crates/renderer/src/instance.rs new file mode 100644 index 0000000..0446e7a --- /dev/null +++ b/crates/renderer/src/instance.rs @@ -0,0 +1,80 @@ +use std::ffi::{CStr, c_char}; +use ash::{Entry, Instance, ext, vk}; +use tracing::{debug, error, info, warn}; +use crate::error::RendererError; + +/// Creates a Vulkan instance and optionally a debug messenger. +pub fn create_instance( + entry: &Entry, + required_extensions: &[*const c_char], +) -> Result<(Instance, Option, vk::DebugUtilsMessengerEXT), RendererError> { + let mut extensions = required_extensions.to_vec(); + let mut layers = Vec::new(); + + #[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); + + let instance = unsafe { entry.create_instance(&create_info, None)? }; + + #[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()); + + Ok((instance, debug_utils, debug_messenger)) +} + +/// The callback function invoked by Vulkan's validation layers. +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 { + let callback_data = unsafe { *p_callback_data }; + + let message = if callback_data.p_message.is_null() { + "".into() + } else { + unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } + }; + + 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}"), + } + + vk::FALSE +} diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 82f112f..4ec90a9 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -1,70 +1,23 @@ pub mod error; +mod instance; +mod device; +mod surface; +mod swapchain; +mod sync; +mod renderer; -use std::ffi::{CStr, c_char}; -use ash::{Device, Entry, Instance, ext, khr, vk}; +use std::ffi::c_char; +use ash::{Entry, 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, - /// 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, - /// The surface loader instance. - surface_loader: khr::surface::Instance, - /// The window surface handle. - surface: vk::SurfaceKHR, - /// The swapchain loader instance. - swapchain_loader: khr::swapchain::Device, - /// The swapchain handle. - swapchain: vk::SwapchainKHR, - /// The images retrieved from the swapchain. - swapchain_images: Vec, - /// The pixel format of the swapchain images. - swapchain_format: vk::Format, - /// The resolution of the swapchain images. - swapchain_extent: vk::Extent2D, - /// The views into the swapchain images. - swapchain_image_views: Vec, - /// The pool used to allocate command buffers. - command_pool: vk::CommandPool, - /// The buffers used to record GPU commands (one per frame in flight). - command_buffers: Vec, - /// Semaphores signaled when an image is acquired (one per frame in flight). - image_available_semaphores: Vec, - /// Semaphores signaled when rendering is complete (one per frame in flight). - render_finished_semaphores: Vec, - /// Fences signaled when the GPU has finished a frame (one per frame in flight). - in_flight_fences: Vec, - /// Tracks which frame is using which swapchain image (one per swapchain image). - images_in_flight: Vec, - /// The index of the frame currently being processed (0..MAX_FRAMES_IN_FLIGHT). - current_frame: usize, -} +pub use renderer::{Renderer, MAX_FRAMES_IN_FLIGHT}; 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, @@ -74,117 +27,51 @@ impl Renderer { ) -> 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(); + // 1. Instance and Debug Messenger + let (instance, debug_utils, debug_messenger) = + instance::create_instance(&entry, required_extensions)?; - // 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()); - } + // 2. Surface + let (surface_loader, surface) = + surface::create_surface(&entry, &instance, display_handle, window_handle)?; - let app_info = vk::ApplicationInfo::default() - .api_version(vk::API_VERSION_1_3); + // 3. Physical Device (GPU) + let physical_device = + device::pick_physical_device(&instance, &surface_loader, surface)?; - 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 the 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)?; + // 4. Graphics Queue Index + let graphics_queue_index = + device::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)?; + // 5. Logical Device and Queue + let (device, graphics_queue) = + device::create_logical_device(&instance, physical_device, graphics_queue_index)?; - // Create the swapchain + // 6. Swapchain let (swapchain_loader, swapchain, swapchain_images, swapchain_format, swapchain_extent) = - Self::create_swapchain(&instance, physical_device, &device, &surface_loader, surface, width, height)?; + swapchain::create_swapchain(&instance, physical_device, &device, &surface_loader, surface, width, height)?; - // Create image views for the swapchain images - let image_views = Self::create_image_views(&device, &swapchain_images, swapchain_format)?; + // 7. Image Views + let image_views = + swapchain::create_image_views(&device, &swapchain_images, swapchain_format)?; - // Create the command pool + // 8. Command Pool let pool_create_info = vk::CommandPoolCreateInfo::default() .queue_family_index(graphics_queue_index) - // Allows reusing the command buffer every frame by resetting it .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? }; - // Create synchronization primitives - // We use MAX_FRAMES_IN_FLIGHT (2) to allow the CPU to stay one frame ahead of the GPU. - const MAX_FRAMES_IN_FLIGHT: usize = 2; - let mut image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); - let mut render_finished_semaphores = Vec::with_capacity(swapchain_images.len()); - let mut in_flight_fences = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); - let mut command_buffers = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); - - let semaphore_info = vk::SemaphoreCreateInfo::default(); - let fence_info = vk::FenceCreateInfo::default() - .flags(vk::FenceCreateFlags::SIGNALED); - + // 9. Command Buffers let alloc_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY) .command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32); - let cbs = unsafe { device.allocate_command_buffers(&alloc_info)? }; + let command_buffers = unsafe { device.allocate_command_buffers(&alloc_info)? }; - for i in 0..MAX_FRAMES_IN_FLIGHT { - image_available_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); - in_flight_fences.push(unsafe { device.create_fence(&fence_info, None)? }); - command_buffers.push(cbs[i]); - } - - // We create one render-finished semaphore per swapchain image. - // This satisfies strict drivers (like AMD RADV) that require a semaphore - // to be tied to a specific image until it is re-acquired. - for _ in 0..swapchain_images.len() { - render_finished_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); - } - - // Initially, no image is in use by a frame. - // We use this to track which frame's fence is protecting which swapchain image. - let images_in_flight = vec![vk::Fence::null(); swapchain_images.len()]; + // 10. Synchronization Primitives + let sync = sync::create_sync_primitives(&device, MAX_FRAMES_IN_FLIGHT, swapchain_images.len())?; Ok(Self { _entry: entry, @@ -205,433 +92,8 @@ impl Renderer { swapchain_image_views: image_views, command_pool, command_buffers, - image_available_semaphores, - render_finished_semaphores, - in_flight_fences, - images_in_flight, + sync, current_frame: 0, }) } - - /// 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 { - 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 { - 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)) - } - - /// Creates a swapchain and retrieves its images. - /// - /// This function negotiates with the surface to find a suitable color format, - /// presentation mode, and image resolution. - fn create_swapchain( - instance: &Instance, - physical_device: vk::PhysicalDevice, - device: &Device, - surface_loader: &khr::surface::Instance, - surface: vk::SurfaceKHR, - width: u32, - height: u32, - ) -> Result< - (khr::swapchain::Device, vk::SwapchainKHR, Vec, vk::Format, vk::Extent2D), - RendererError, - > { - let surface_capabilities = unsafe { surface_loader.get_physical_device_surface_capabilities(physical_device, surface)? }; - - let surface_formats = unsafe { surface_loader.get_physical_device_surface_formats(physical_device, surface)? }; - - let surface_present_modes = unsafe { surface_loader.get_physical_device_surface_present_modes(physical_device, surface)?}; - - // 1. Pick the best color format (Prefer SRGB for accurate colors) - let format = surface_formats.iter() - .find(|f| f.format == vk::Format::B8G8R8A8_SRGB && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR) - .unwrap_or(&surface_formats[0]); - - // 2. Pick the best presentation mode (Mailbox = Triple Buffering, FIFO = VSync) - let present_mode = surface_present_modes.into_iter() - .find(|&m| m == vk::PresentModeKHR::MAILBOX) - .unwrap_or(vk::PresentModeKHR::FIFO); - - // 3. Match the window resolution - // If the surface says u32::MAX, it means we can pick any size within bounds. - let extent = if surface_capabilities.current_extent.width != u32::MAX { - surface_capabilities.current_extent - } else { - vk::Extent2D { - width: width.clamp(surface_capabilities.min_image_extent.width, surface_capabilities.max_image_extent.width), - height: height.clamp(surface_capabilities.min_image_extent.height, surface_capabilities.max_image_extent.height), - } - }; - - let mut image_count = surface_capabilities.min_image_count + 1; - - if surface_capabilities.max_image_count > 0 && image_count > surface_capabilities.max_image_count { - image_count = surface_capabilities.max_image_count; - } - - let swapchain_loader = khr::swapchain::Device::new(instance, device); - - let create_info = vk::SwapchainCreateInfoKHR::default() - .surface(surface) - .min_image_count(image_count) - .image_format(format.format) - .image_color_space(format.color_space) - .image_extent(extent) - .image_array_layers(1) - .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) - .image_sharing_mode(vk::SharingMode::EXCLUSIVE) - .pre_transform(surface_capabilities.current_transform) - .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(present_mode) - .clipped(true); - - let swapchain = unsafe { swapchain_loader.create_swapchain(&create_info, None)? }; - - let images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? }; - - Ok((swapchain_loader, swapchain, images, format.format, extent )) - } - - /// Creates image views for the swapchain images. - /// - /// An image view describes how to access an image (e.g. as a 2D texture). - fn create_image_views( - device: &Device, - images: &[vk::Image], - format: vk::Format, - ) -> Result, RendererError> { - let mut views = Vec::with_capacity(images.len()); - - for &image in images { - let create_info = vk::ImageViewCreateInfo::default() - .image(image) - .view_type(vk::ImageViewType::TYPE_2D) - .format(format) - .components(vk::ComponentMapping { - r: vk::ComponentSwizzle::IDENTITY, - g: vk::ComponentSwizzle::IDENTITY, - b: vk::ComponentSwizzle::IDENTITY, - a: vk::ComponentSwizzle::IDENTITY, - }) - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }); - - let view = unsafe { device.create_image_view(&create_info, None)? }; - views.push(view); - } - - Ok(views) - } - - /// Renders a single frame. - /// - /// This function handles synchronization, image acquisition, command recording - /// for clearing the screen, and presentation. - pub fn draw_frame(&mut self) -> Result<(), RendererError> { - let in_flight_fence = self.in_flight_fences[self.current_frame]; - let image_available_semaphore = self.image_available_semaphores[self.current_frame]; - let cmd = self.command_buffers[self.current_frame]; - - // 1. Wait for the current frame's GPU work to finish - unsafe { - self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?; - self.device.reset_fences(&[in_flight_fence])?; - } - - // 2. Acquire an image from the swapchain - let (image_index, _is_suboptimal) = unsafe { - self.swapchain_loader.acquire_next_image( - self.swapchain, - u64::MAX, - image_available_semaphore, - vk::Fence::null(), - )? - }; - - // If the acquired image is still being used by a previous frame, wait for it - let image_fence = self.images_in_flight[image_index as usize]; - if image_fence != vk::Fence::null() { - unsafe { self.device.wait_for_fences(&[image_fence], true, u64::MAX)? }; - } - // Mark the image as being in use by the current frame's fence - self.images_in_flight[image_index as usize] = in_flight_fence; - - // Use the semaphore tied to this specific swapchain image for rendering completion - let render_finished_semaphore = self.render_finished_semaphores[image_index as usize]; - - // 3. Reset and begin recording the command buffer - unsafe { - self.device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())?; - let begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); - self.device.begin_command_buffer(cmd, &begin_info)?; - } - - let image = self.swapchain_images[image_index as usize]; - let view = self.swapchain_image_views[image_index as usize]; - - // 4. Transition the swapchain image to a layout suitable for drawing - let range = vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }; - - let barrier_to_draw = vk::ImageMemoryBarrier2::default() - .image(image) - .subresource_range(range) - .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) - .src_access_mask(vk::AccessFlags2::empty()) - .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) - .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) - .old_layout(vk::ImageLayout::UNDEFINED) - .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); - - let dependency_info = vk::DependencyInfo::default() - .image_memory_barriers(std::slice::from_ref(&barrier_to_draw)); - - unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) }; - - // 5. Begin Dynamic Rendering with a clear color - let color_attachment = vk::RenderingAttachmentInfo::default() - .image_view(view) - .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) - .load_op(vk::AttachmentLoadOp::CLEAR) - .store_op(vk::AttachmentStoreOp::STORE) - .clear_value(vk::ClearValue { - color: vk::ClearColorValue { - float32: [0.1, 0.2, 0.4, 1.0], // Project Catalyst Blue - }, - }); - - let rendering_info = vk::RenderingInfo::default() - .render_area(vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent: self.swapchain_extent, - }) - .layer_count(1) - .color_attachments(std::slice::from_ref(&color_attachment)); - - unsafe { - self.device.cmd_begin_rendering(cmd, &rendering_info); - // Future draw calls will go here - self.device.cmd_end_rendering(cmd); - } - - // 6. Transition the image back to Present layout - let barrier_to_present = vk::ImageMemoryBarrier2::default() - .image(image) - .subresource_range(range) - .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) - .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) - .dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE) - .dst_access_mask(vk::AccessFlags2::empty()) - .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) - .new_layout(vk::ImageLayout::PRESENT_SRC_KHR); - - let dependency_info = vk::DependencyInfo::default() - .image_memory_barriers(std::slice::from_ref(&barrier_to_present)); - - unsafe { - self.device.cmd_pipeline_barrier2(cmd, &dependency_info); - self.device.end_command_buffer(cmd)?; - } - - // 7. Submit the work to the GPU - let submit_info = vk::SubmitInfo::default() - .wait_semaphores(std::slice::from_ref(&image_available_semaphore)) - .wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]) - .command_buffers(std::slice::from_ref(&cmd)) - .signal_semaphores(std::slice::from_ref(&render_finished_semaphore)); - - unsafe { - self.device.queue_submit(self.graphics_queue, &[submit_info], in_flight_fence)?; - } - - // 8. Present the result to the screen - let present_info = vk::PresentInfoKHR::default() - .wait_semaphores(std::slice::from_ref(&render_finished_semaphore)) - .swapchains(std::slice::from_ref(&self.swapchain)) - .image_indices(std::slice::from_ref(&image_index)); - - unsafe { - self.swapchain_loader.queue_present(self.graphics_queue, &present_info)?; - } - - // Advance the frame index for the next call - self.current_frame = (self.current_frame + 1) % self.in_flight_fences.len(); - - Ok(()) - } -} - -/// Ensures all Vulkan resources are destroyed in the correct order. -impl Drop for Renderer { - fn drop(&mut self) { - unsafe { - // Ensure the GPU is finished before we start destroying things - let _ = self.device.device_wait_idle(); - - self.device.destroy_command_pool(self.command_pool, None); - - // Destroy synchronization primitives for all frames - for &semaphore in &self.image_available_semaphores { - self.device.destroy_semaphore(semaphore, None); - } - for &semaphore in &self.render_finished_semaphores { - self.device.destroy_semaphore(semaphore, None); - } - for &fence in &self.in_flight_fences { - self.device.destroy_fence(fence, None); - } - - // Destroy the swapchain - self.swapchain_loader.destroy_swapchain(self.swapchain, None); - - // Destroy image views - for &view in &self.swapchain_image_views { - self.device.destroy_image_view(view, None); - } - - // Destroy the logical device - self.device.destroy_device(None); - - // Destroy the surface - self.surface_loader.destroy_surface(self.surface, None); - - // Destroy the debug messenger if we created one - if let Some(utils) = &self.debug_utils { - utils.destroy_debug_utils_messenger(self.debug_messenger, None); - } - - // 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 } \ No newline at end of file diff --git a/crates/renderer/src/renderer.rs b/crates/renderer/src/renderer.rs new file mode 100644 index 0000000..50031d4 --- /dev/null +++ b/crates/renderer/src/renderer.rs @@ -0,0 +1,207 @@ +use ash::{Device, Instance, khr, vk}; +use crate::error::RendererError; +use crate::sync::SyncPrimitives; + +pub const MAX_FRAMES_IN_FLIGHT: usize = 2; + +/// The core renderer structure holding the Vulkan resources. +pub struct Renderer { + pub(crate) _entry: ash::Entry, + pub(crate) instance: Instance, + pub(crate) debug_utils: Option, + pub(crate) debug_messenger: vk::DebugUtilsMessengerEXT, + pub(crate) physical_device: vk::PhysicalDevice, + pub(crate) device: Device, + pub(crate) graphics_queue: vk::Queue, + pub(crate) graphics_queue_index: u32, + pub(crate) surface_loader: khr::surface::Instance, + pub(crate) surface: vk::SurfaceKHR, + pub(crate) swapchain_loader: khr::swapchain::Device, + pub(crate) swapchain: vk::SwapchainKHR, + pub(crate) swapchain_images: Vec, + pub(crate) swapchain_format: vk::Format, + pub(crate) swapchain_extent: vk::Extent2D, + pub(crate) swapchain_image_views: Vec, + pub(crate) command_pool: vk::CommandPool, + pub(crate) command_buffers: Vec, + pub(crate) sync: SyncPrimitives, + pub(crate) current_frame: usize, +} + +impl Renderer { + /// Renders a single frame. + pub fn draw_frame(&mut self) -> Result<(), RendererError> { + let in_flight_fence = self.sync.in_flight[self.current_frame]; + let image_available_semaphore = self.sync.image_available[self.current_frame]; + let cmd = self.command_buffers[self.current_frame]; + + // 1. Wait for the current frame's GPU work to finish + unsafe { + self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?; + self.device.reset_fences(&[in_flight_fence])?; + } + + // 2. Acquire an image from the swapchain + let (image_index, _is_suboptimal) = unsafe { + self.swapchain_loader.acquire_next_image( + self.swapchain, + u64::MAX, + image_available_semaphore, + vk::Fence::null(), + )? + }; + + // If the acquired image is still being used by a previous frame, wait for it + let image_fence = self.sync.images_in_flight[image_index as usize]; + if image_fence != vk::Fence::null() { + unsafe { self.device.wait_for_fences(&[image_fence], true, u64::MAX)? }; + } + // Mark the image as being in use by the current frame's fence + self.sync.images_in_flight[image_index as usize] = in_flight_fence; + + // Use the semaphore tied to this specific swapchain image for rendering completion + let render_finished_semaphore = self.sync.render_finished[image_index as usize]; + + // 3. Reset and begin recording the command buffer + unsafe { + self.device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())?; + let begin_info = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + self.device.begin_command_buffer(cmd, &begin_info)?; + } + + let image = self.swapchain_images[image_index as usize]; + let view = self.swapchain_image_views[image_index as usize]; + + // 4. Transition the swapchain image to a layout suitable for drawing + let range = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }; + + let barrier_to_draw = vk::ImageMemoryBarrier2::default() + .image(image) + .subresource_range(range) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); + + let dependency_info = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&barrier_to_draw)); + + unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) }; + + // 5. Begin Dynamic Rendering with a clear color + let color_attachment = vk::RenderingAttachmentInfo::default() + .image_view(view) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .clear_value(vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.1, 0.2, 0.4, 1.0], // Project Catalyst Blue + }, + }); + + let rendering_info = vk::RenderingInfo::default() + .render_area(vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, + }) + .layer_count(1) + .color_attachments(std::slice::from_ref(&color_attachment)); + + unsafe { + self.device.cmd_begin_rendering(cmd, &rendering_info); + // Future draw calls will go here + self.device.cmd_end_rendering(cmd); + } + + // 6. Transition the image back to Present layout + let barrier_to_present = vk::ImageMemoryBarrier2::default() + .image(image) + .subresource_range(range) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE) + .dst_access_mask(vk::AccessFlags2::empty()) + .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR); + + let dependency_info = vk::DependencyInfo::default() + .image_memory_barriers(std::slice::from_ref(&barrier_to_present)); + + unsafe { + self.device.cmd_pipeline_barrier2(cmd, &dependency_info); + self.device.end_command_buffer(cmd)?; + } + + // 7. Submit the work to the GPU + let submit_info = vk::SubmitInfo::default() + .wait_semaphores(std::slice::from_ref(&image_available_semaphore)) + .wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]) + .command_buffers(std::slice::from_ref(&cmd)) + .signal_semaphores(std::slice::from_ref(&render_finished_semaphore)); + + unsafe { + self.device.queue_submit(self.graphics_queue, &[submit_info], in_flight_fence)?; + } + + // 8. Present the result to the screen + let present_info = vk::PresentInfoKHR::default() + .wait_semaphores(std::slice::from_ref(&render_finished_semaphore)) + .swapchains(std::slice::from_ref(&self.swapchain)) + .image_indices(std::slice::from_ref(&image_index)); + + unsafe { + self.swapchain_loader.queue_present(self.graphics_queue, &present_info)?; + } + + // Advance the frame index for the next call + self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; + + Ok(()) + } +} + +impl Drop for Renderer { + fn drop(&mut self) { + unsafe { + let _ = self.device.device_wait_idle(); + + self.device.destroy_command_pool(self.command_pool, None); + + // Use the safe cleanup function from sync module + let sync = std::ptr::read(&self.sync); + crate::sync::destroy_sync_primitives(&self.device, sync); + + // Destroy the swapchain + self.swapchain_loader.destroy_swapchain(self.swapchain, None); + + // Destroy image views + for &view in &self.swapchain_image_views { + self.device.destroy_image_view(view, None); + } + + // Destroy the logical device + self.device.destroy_device(None); + + // Destroy the surface + self.surface_loader.destroy_surface(self.surface, None); + + // Destroy the debug messenger if we created one + if let Some(utils) = &self.debug_utils { + utils.destroy_debug_utils_messenger(self.debug_messenger, None); + } + + // Destroy the instance + self.instance.destroy_instance(None); + } + } +} diff --git a/crates/renderer/src/surface.rs b/crates/renderer/src/surface.rs new file mode 100644 index 0000000..086c6ab --- /dev/null +++ b/crates/renderer/src/surface.rs @@ -0,0 +1,18 @@ +use ash::{Entry, Instance, khr, vk}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; +use crate::error::RendererError; + +/// Creates a Vulkan surface for the given window. +pub fn create_surface( + entry: &Entry, + instance: &Instance, + display_handle: RawDisplayHandle, + window_handle: RawWindowHandle, +) -> Result<(khr::surface::Instance, vk::SurfaceKHR), RendererError> { + let surface = unsafe { + ash_window::create_surface(entry, instance, display_handle, window_handle, None)? + }; + let surface_loader = khr::surface::Instance::new(entry, instance); + + Ok((surface_loader, surface)) +} diff --git a/crates/renderer/src/swapchain.rs b/crates/renderer/src/swapchain.rs new file mode 100644 index 0000000..68046eb --- /dev/null +++ b/crates/renderer/src/swapchain.rs @@ -0,0 +1,96 @@ +use ash::{Device, Instance, khr, vk}; +use crate::error::RendererError; + +/// Creates a swapchain and retrieves its images. +pub fn create_swapchain( + instance: &Instance, + physical_device: vk::PhysicalDevice, + device: &Device, + surface_loader: &khr::surface::Instance, + surface: vk::SurfaceKHR, + width: u32, + height: u32, +) -> Result<(khr::swapchain::Device, vk::SwapchainKHR, Vec, vk::Format, vk::Extent2D), RendererError> { + let surface_capabilities = unsafe { + surface_loader.get_physical_device_surface_capabilities(physical_device, surface)? + }; + let surface_formats = unsafe { + surface_loader.get_physical_device_surface_formats(physical_device, surface)? + }; + let surface_present_modes = unsafe { + surface_loader.get_physical_device_surface_present_modes(physical_device, surface)? + }; + + let format = surface_formats.iter() + .find(|f| f.format == vk::Format::B8G8R8A8_SRGB && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR) + .unwrap_or(&surface_formats[0]); + + let present_mode = surface_present_modes.iter() + .copied() + .find(|&m| m == vk::PresentModeKHR::MAILBOX) + .unwrap_or(vk::PresentModeKHR::FIFO); + + let extent = if surface_capabilities.current_extent.width != u32::MAX { + surface_capabilities.current_extent + } else { + vk::Extent2D { + width: width.clamp(surface_capabilities.min_image_extent.width, surface_capabilities.max_image_extent.width), + height: height.clamp(surface_capabilities.min_image_extent.height, surface_capabilities.max_image_extent.height), + } + }; + + let image_count = if surface_capabilities.max_image_count > 0 && surface_capabilities.min_image_count + 1 > surface_capabilities.max_image_count { + surface_capabilities.max_image_count + } else { + surface_capabilities.min_image_count + 1 + }; + + let swapchain_loader = khr::swapchain::Device::new(instance, device); + + let create_info = vk::SwapchainCreateInfoKHR::default() + .surface(surface) + .min_image_count(image_count) + .image_format(format.format) + .image_color_space(format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(vk::SharingMode::EXCLUSIVE) + .pre_transform(surface_capabilities.current_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(present_mode) + .clipped(true); + + let swapchain = unsafe { swapchain_loader.create_swapchain(&create_info, None)? }; + let images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? }; + + Ok((swapchain_loader, swapchain, images, format.format, extent)) +} + +/// Creates image views for the swapchain images. +pub fn create_image_views( + device: &Device, + images: &[vk::Image], + format: vk::Format, +) -> Result, RendererError> { + let mut views = Vec::with_capacity(images.len()); + + for &image in images { + let create_info = vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }); + + let view = unsafe { device.create_image_view(&create_info, None)? }; + views.push(view); + } + + Ok(views) +} diff --git a/crates/renderer/src/sync.rs b/crates/renderer/src/sync.rs new file mode 100644 index 0000000..775e98f --- /dev/null +++ b/crates/renderer/src/sync.rs @@ -0,0 +1,58 @@ +use ash::{Device, vk}; +use crate::error::RendererError; + +/// Groups all synchronization primitives for the renderer. +pub struct SyncPrimitives { + pub image_available: Vec, + pub render_finished: Vec, + pub in_flight: Vec, + pub images_in_flight: Vec, +} + +/// Creates all synchronization primitives for the given number of frames and images. +pub fn create_sync_primitives( + device: &Device, + max_frames_in_flight: usize, + image_count: usize, +) -> Result { + let semaphore_info = vk::SemaphoreCreateInfo::default(); + let fence_info = vk::FenceCreateInfo::default() + .flags(vk::FenceCreateFlags::SIGNALED); + + let mut image_available = Vec::with_capacity(max_frames_in_flight); + let mut render_finished = Vec::with_capacity(image_count); + let mut in_flight = Vec::with_capacity(max_frames_in_flight); + + for _ in 0..max_frames_in_flight { + image_available.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); + in_flight.push(unsafe { device.create_fence(&fence_info, None)? }); + } + + for _ in 0..image_count { + render_finished.push(unsafe { device.create_semaphore(&semaphore_info, None)? }); + } + + let images_in_flight = vec![vk::Fence::null(); image_count]; + + Ok(SyncPrimitives { + image_available, + render_finished, + in_flight, + images_in_flight, + }) +} + +/// Destroys all synchronization primitives. +pub unsafe fn destroy_sync_primitives(device: &Device, sync: SyncPrimitives) { + unsafe { + for semaphore in sync.image_available { + device.destroy_semaphore(semaphore, None); + } + for semaphore in sync.render_finished { + device.destroy_semaphore(semaphore, None); + } + for fence in sync.in_flight { + device.destroy_fence(fence, None); + } + } +}