diff --git a/crates/game/src/main.rs b/crates/game/src/main.rs index c7cd133..e8c0534 100644 --- a/crates/game/src/main.rs +++ b/crates/game/src/main.rs @@ -28,7 +28,7 @@ impl WindowState { .with_resizable(true) .with_inner_size(LogicalSize::new(800, 600)), // TODO: pass down this error and add some kind of error handling UI or dump - renderer: Renderer::new(display).expect("renderer"), + renderer: Renderer::new(display.as_raw()).expect("renderer"), } } @@ -40,7 +40,7 @@ impl WindowState { _ = (window_id, new_size); info!("TODO: implement resize events"); if let Some(ctx) = self.renderer.window_contexts.get_mut(&window_id) { - ctx.recreate_swapchain(renderer::Extent2D { + ctx.recreate_with(renderer::Extent2D { width: new_size.width, height: new_size.height, }) diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index 2bb7573..390553a 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -21,3 +21,4 @@ winit = "0.30.5" crossbeam = "0.8.4" parking_lot = "0.12.3" smol.workspace = true +tracing-test = "0.2.5" diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 5f86c26..9373d96 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -1,6 +1,7 @@ #![feature(c_str_module, closure_lifetime_binder, let_chains, negative_impls)] #![allow(unused)] use std::{ + borrow::Borrow, collections::{BTreeMap, BTreeSet, HashMap}, ffi::{CStr, CString}, fmt::Debug, @@ -8,10 +9,12 @@ use std::{ ops::Deref, sync::{ atomic::{AtomicU32, AtomicU64}, - Arc, Mutex, + Arc, }, }; +use parking_lot::{Mutex, MutexGuard, RwLock}; + use ash::{ khr, prelude::VkResult, @@ -22,7 +25,7 @@ use dyn_clone::DynClone; use rand::{Rng, SeedableRng}; use tinyvec::{array_vec, ArrayVec}; use tracing::info; -use winit::raw_window_handle::DisplayHandle; +use winit::raw_window_handle::{DisplayHandle, RawDisplayHandle}; mod commands; mod images; @@ -111,12 +114,12 @@ impl Queue { } pub fn with_locked T>(&self, map: F) -> T { - let lock = self.0.lock().expect("mutex lock poison"); + let lock = self.0.lock(); map(*lock) } - pub fn lock(&self) -> std::sync::MutexGuard<'_, vk::Queue> { - self.0.lock().unwrap() + pub fn lock(&self) -> MutexGuard<'_, vk::Queue> { + self.0.lock() } } @@ -454,6 +457,7 @@ struct DeviceInner { physical: PhysicalDevice, device: ash::Device, swapchain: khr::swapchain::Device, + debug_utils: ash::ext::debug_utils::Device, main_queue: Queue, compute_queue: Queue, transfer_queue: Queue, @@ -481,6 +485,9 @@ impl Device { fn swapchain(&self) -> &khr::swapchain::Device { &self.0.swapchain } + fn debug_utils(&self) -> &ash::ext::debug_utils::Device { + &self.0.debug_utils + } fn queue_families(&self) -> &DeviceQueueFamilies { &self.0.physical.queue_families } @@ -540,20 +547,14 @@ struct RawSwapchain(vk::SwapchainKHR); impl !Sync for RawSwapchain {} #[derive(Debug)] -struct SwapchainHandle(parking_lot::Mutex); +struct SwapchainHandle(Mutex); impl SwapchainHandle { unsafe fn from_handle(swapchain: vk::SwapchainKHR) -> SwapchainHandle { - Self(parking_lot::Mutex::new(swapchain)) + Self(Mutex::new(swapchain)) } - fn lock( - &self, - ) -> parking_lot::lock_api::MutexGuard< - '_, - parking_lot::RawMutex, - vk::SwapchainKHR, - > { + fn lock(&self) -> MutexGuard<'_, vk::SwapchainKHR> { self.0.lock() } @@ -587,6 +588,7 @@ pub struct Swapchain { fences: Vec>, current_frame: AtomicU32, + present_id: AtomicU64, } impl Drop for Swapchain { @@ -645,6 +647,8 @@ struct SwapchainParams { extent: vk::Extent2D, } +#[must_use = "This struct represents an acquired image from the swapchain and + must be presented in order to free resources on the device."] pub struct SwapchainFrame { pub swapchain: Arc, pub index: u32, @@ -654,6 +658,12 @@ pub struct SwapchainFrame { pub release: vk::Semaphore, } +impl SwapchainFrame { + fn present(self) { + self.swapchain.clone().present(self); + } +} + impl Swapchain { const PREFERRED_IMAGES_IN_FLIGHT: u32 = 3; @@ -794,28 +804,79 @@ impl Swapchain { let acquire_semaphores = { (0..inflight_frames) - .map(|_| unsafe { + .map(|i| unsafe { device.dev().create_semaphore( &vk::SemaphoreCreateInfo::default(), None, - ) + ).inspect(|r| { + #[cfg(debug_assertions)] + + { + let name = CString::new(format!( + "semaphore-{:x}_{i}-acquire", + swapchain.0.lock().as_raw() + )) + .unwrap(); + unsafe { + device.debug_utils().set_debug_utils_object_name( + &vk::DebugUtilsObjectNameInfoEXT::default() + .object_handle(*r) + .object_name(&name), + );} + } + }) }) .collect::>>()? }; + let release_semaphores = { (0..inflight_frames) - .map(|_| unsafe { + .map(|i| unsafe { device.dev().create_semaphore( &vk::SemaphoreCreateInfo::default(), None, - ) + ).inspect(|r| { + #[cfg(debug_assertions)] + + { + let name = CString::new(format!( + "semaphore-{:x}_{i}-release", + swapchain.0.lock().as_raw() + )) + .unwrap(); + unsafe { + device.debug_utils().set_debug_utils_object_name( + &vk::DebugUtilsObjectNameInfoEXT::default() + .object_handle(*r) + .object_name(&name), + );} + } + }) }) .collect::>>()? }; let fences = { (0..inflight_frames) - .map(|_| Ok(Arc::new(sync::Fence::create(device.clone())?))) + .map(|i| Ok(Arc::new(sync::Fence::create(device.clone()) + .inspect(|r| { + #[cfg(debug_assertions)] + + { + let name = CString::new(format!( + "fence-{:x}_{i}", + swapchain.0.lock().as_raw() + )) + .unwrap(); + unsafe { + device.debug_utils().set_debug_utils_object_name( + &vk::DebugUtilsObjectNameInfoEXT::default() + .object_handle(r.fence()) + .object_name(&name), + );} + } + }) +?))) .collect::>>()? }; @@ -837,6 +898,7 @@ impl Swapchain { release_semaphores, fences, current_frame: AtomicU32::new(0), + present_id: AtomicU64::new(1), }) } @@ -919,6 +981,35 @@ impl Swapchain { } } + fn present(&self, frame: SwapchainFrame) -> Result<()> { + let swpchain = self.swapchain.lock(); + let queue = self.device.present_queue().lock(); + + let wait_semaphores = [frame.release]; + + // TODO: make this optional for devices with no support for present_wait/present_id + let present_id = self + .present_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut present_id = vk::PresentIdKHR::default() + .present_ids(core::slice::from_ref(&present_id)); + + let present_info = vk::PresentInfoKHR::default() + .image_indices(core::slice::from_ref(&frame.index)) + .swapchains(core::slice::from_ref(&swpchain)) + .wait_semaphores(&wait_semaphores) + .push_next(&mut present_id); + + // call winits pre_present_notify here + + unsafe { + self.device + .swapchain() + .queue_present(*queue, &present_info)?; + } + Ok(()) + } + fn create_vkswapchainkhr( device: &Device, surface: vk::SurfaceKHR, @@ -956,6 +1047,23 @@ impl Swapchain { let (swapchain, images) = unsafe { let swapchain = device.swapchain().create_swapchain(&create_info, None)?; + + #[cfg(debug_assertions)] + { + let name = CString::new(format!( + "swapchain-{}_{}", + surface.as_raw(), + SWAPCHAIN_COUNT + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )) + .unwrap(); + device.debug_utils().set_debug_utils_object_name( + &vk::DebugUtilsObjectNameInfoEXT::default() + .object_handle(swapchain) + .object_name(&name), + ); + } + let images = device.swapchain().get_swapchain_images(swapchain)?; (SwapchainHandle::from_handle(swapchain), images) @@ -964,6 +1072,7 @@ impl Swapchain { Ok((swapchain, images)) } } +static SWAPCHAIN_COUNT: AtomicU64 = AtomicU64::new(0); struct Surface { instance: Arc, @@ -971,9 +1080,25 @@ struct Surface { } impl Surface { + fn headless(instance: Arc) -> Result { + unsafe { + let headless_instance = ash::ext::headless_surface::Instance::new( + &instance.entry, + &instance.instance, + ); + + let surface = headless_instance.create_headless_surface( + &vk::HeadlessSurfaceCreateInfoEXT::default(), + None, + )?; + + Ok(Self { instance, surface }) + } + } + fn create( instance: Arc, - display_handle: winit::raw_window_handle::RawDisplayHandle, + display_handle: RawDisplayHandle, window_handle: winit::raw_window_handle::RawWindowHandle, ) -> Result { let surface = unsafe { @@ -1004,6 +1129,14 @@ pub struct Vulkan { alloc: VkAllocator, } +impl Drop for Vulkan { + fn drop(&mut self) { + unsafe { + self.device.dev().device_wait_idle(); + } + } +} + impl Vulkan { const VALIDATION_LAYER_NAME: &'static core::ffi::CStr = c"VK_LAYER_KHRONOS_validation"; @@ -1021,7 +1154,7 @@ impl Vulkan { app_name: &str, instance_layers: &[&CStr], instance_extensions: &[&CStr], - display_handle: winit::raw_window_handle::DisplayHandle, + display_handle: Option, ) -> Result { let entry = unsafe { ash::Entry::load()? }; @@ -1059,6 +1192,8 @@ impl Vulkan { let layers = Self::get_layers(&entry, &[Self::VALIDATION_LAYER_NAME]).unwrap(); + + // optional display handle let extensions = Self::get_extensions( &entry, &layers, @@ -1209,33 +1344,29 @@ impl Vulkan { instance: &Instance, pdev: vk::PhysicalDevice, queue_family: u32, - display_handle: winit::raw_window_handle::DisplayHandle, + display_handle: RawDisplayHandle, ) -> bool { unsafe { - match display_handle.as_raw() { - winit::raw_window_handle::RawDisplayHandle::Xlib( - _xlib_display_handle, - ) => { + match display_handle { + RawDisplayHandle::Xlib(_xlib_display_handle) => { todo!() } - winit::raw_window_handle::RawDisplayHandle::Xcb( - _xcb_display_handle, - ) => todo!(), - winit::raw_window_handle::RawDisplayHandle::Wayland( - wayland_display_handle, - ) => ash::khr::wayland_surface::Instance::new( - &instance.entry, - &instance.instance, - ) - .get_physical_device_wayland_presentation_support( - pdev, - queue_family, - wayland_display_handle.display.cast().as_mut(), - ), - winit::raw_window_handle::RawDisplayHandle::Drm(_) => { + RawDisplayHandle::Xcb(_xcb_display_handle) => todo!(), + RawDisplayHandle::Wayland(wayland_display_handle) => { + ash::khr::wayland_surface::Instance::new( + &instance.entry, + &instance.instance, + ) + .get_physical_device_wayland_presentation_support( + pdev, + queue_family, + wayland_display_handle.display.cast().as_mut(), + ) + } + RawDisplayHandle::Drm(_) => { todo!() } - winit::raw_window_handle::RawDisplayHandle::Windows(_) => { + RawDisplayHandle::Windows(_) => { ash::khr::win32_surface::Instance::new( &instance.entry, &instance.instance, @@ -1252,7 +1383,7 @@ impl Vulkan { fn select_pdev_queue_families( instance: &Instance, - display_handle: winit::raw_window_handle::DisplayHandle, + display_handle: Option, pdev: vk::PhysicalDevice, ) -> DeviceQueueFamilies { let queue_families = unsafe { @@ -1329,12 +1460,16 @@ impl Vulkan { family.queue_flags.contains(vk::QueueFlags::TRANSFER) || is_compute || is_graphics; - let is_present = Self::queue_family_supports_presentation( - instance, - pdev, - q, - display_handle, - ); + let is_present = display_handle + .map(|display_handle| { + Self::queue_family_supports_presentation( + instance, + pdev, + q, + display_handle, + ) + }) + .unwrap_or(false); QueueFamily { num_queues: family.queue_count, is_compute, @@ -1359,12 +1494,7 @@ impl Vulkan { // find present queue first because it is rather more important than a secondary compute queue let present = if !queue_families.0.get(graphics as usize).unwrap().is_present { - // unwrap because we do need a present queue - Some( - queue_families - .find_first(|family| family.is_present) - .unwrap(), - ) + queue_families.find_first(|family| family.is_present) } else { None }; @@ -1379,12 +1509,13 @@ impl Vulkan { async_compute, transfer, present: present.or({ - if !queue_families.0.get(graphics as usize).unwrap().is_present - { - panic!("no present queue available"); + if display_handle.is_none() { + // in this case the graphics queue will be used by default + tracing::info!("no present queue available, using graphics queue as fallback for headless_surface"); + Some(graphics) } else { - None - } + tracing::warn!("no present queue available, this is unexpected!"); + None} }), }; @@ -1483,6 +1614,10 @@ impl Vulkan { &instance.instance, &device, ), + debug_utils: ash::ext::debug_utils::Device::new( + &instance.instance, + &device, + ), instance, main_queue, present_queue, @@ -1497,7 +1632,7 @@ impl Vulkan { fn choose_physical_device( instance: &Instance, - display_handle: winit::raw_window_handle::DisplayHandle, + display_handle: Option, requirements: &PhysicalDeviceFeatures, extra_properties: Vec>, ) -> Result { @@ -1580,7 +1715,7 @@ impl Vulkan { entry: &ash::Entry, layers: &[&'a CStr], extensions: &[&'a CStr], - display_handle: winit::raw_window_handle::DisplayHandle, + display_handle: Option, ) -> core::result::Result, (Vec<&'a CStr>, Vec<&'a CStr>)> { unsafe { @@ -1606,10 +1741,11 @@ impl Vulkan { } } - let Ok(required_extension_names) = - ash_window::enumerate_required_extensions( - display_handle.as_raw(), - ) + let Ok(required_extension_names) = display_handle + .map(|display_handle| { + ash_window::enumerate_required_extensions(display_handle) + }) + .unwrap_or(Ok(&[])) else { return Err((out_extensions, unsupported_extensions)); }; @@ -1665,9 +1801,47 @@ impl Vulkan { } } +use winit::raw_window_handle::RawWindowHandle; + pub struct WindowContext { + window_handle: RawWindowHandle, surface: Arc, - current_swapchain: Arc, + // this mutex is for guarding the swapchain against being replaced + // underneath WindowContext's functions + current_swapchain: RwLock>, +} + +impl Drop for WindowContext { + fn drop(&mut self) { + unsafe { + self.current_swapchain + .read() + .device + .dev() + .device_wait_idle(); + } + } +} + +unsafe impl Send for WindowContext {} +unsafe impl Sync for WindowContext {} + +impl Borrow for WindowContext { + fn borrow(&self) -> &RawWindowHandle { + &self.window_handle + } +} +impl PartialEq for WindowContext { + fn eq(&self, other: &Self) -> bool { + self.window_handle == other.window_handle + } +} +impl Eq for WindowContext {} + +impl core::hash::Hash for WindowContext { + fn hash(&self, state: &mut H) { + self.window_handle.hash(state); + } } impl WindowContext { @@ -1675,11 +1849,14 @@ impl WindowContext { instance: Arc, device: Device, extent: vk::Extent2D, - window: winit::raw_window_handle::RawWindowHandle, - display: winit::raw_window_handle::RawDisplayHandle, + window_handle: winit::raw_window_handle::RawWindowHandle, + display: RawDisplayHandle, ) -> Result { - let surface = - Arc::new(Surface::create(instance.clone(), display, window)?); + let surface = Arc::new(Surface::create( + instance.clone(), + display, + window_handle, + )?); let swapchain = Arc::new(Swapchain::new( instance, @@ -1690,14 +1867,53 @@ impl WindowContext { )?); Ok(Self { + window_handle, surface, - current_swapchain: swapchain, + current_swapchain: RwLock::new(swapchain), }) } - pub fn recreate_swapchain(&mut self, extent: vk::Extent2D) -> Result<()> { - self.current_swapchain = - Arc::new(self.current_swapchain.recreate(extent)?); + /// spawns a task that continuously requests images from the current + /// swapchain, sending them to a channel. returns the receiver of the + /// channel, and a handle to the task, allowing for cancellation. + fn images( + self: Arc, + ) -> ( + smol::channel::Receiver, + smol::Task>, + ) { + let (tx, rx) = smol::channel::bounded(8); + let task = smol::spawn(async move { + loop { + let frame = self.acquire_image().await?; + tx.send(frame) + .await + .expect("channel closed on swapchain acquiring frame"); + } + Result::Ok(()) + }); + + (rx, task) + } + + async fn acquire_image(&self) -> Result { + // clone swapchain to keep it alive + let swapchain = self.current_swapchain.read().clone(); + let (frame, suboptimal) = swapchain.clone().acquire_image().await?; + if suboptimal { + let mut lock = self.current_swapchain.write(); + // only recreate our swapchain if it is still same, or else it might have already been recreated. + if Arc::ptr_eq(&swapchain, &lock) { + *lock = Arc::new(lock.recreate(lock.extent)?); + } + } + + Ok(frame) + } + + pub fn recreate_with(&self, extent: vk::Extent2D) -> Result<()> { + let mut swapchain = self.current_swapchain.write(); + *swapchain = Arc::new(swapchain.recreate(extent)?); Ok(()) } @@ -1705,18 +1921,18 @@ impl WindowContext { pub struct Renderer { vulkan: Vulkan, - display: winit::raw_window_handle::RawDisplayHandle, + display: RawDisplayHandle, pub window_contexts: HashMap, } pub use vk::Extent2D; impl Renderer { - pub fn new(display: DisplayHandle) -> Result { - let vulkan = Vulkan::new("Vidya", &[], &[], display)?; + pub fn new(display: RawDisplayHandle) -> Result { + let vulkan = Vulkan::new("Vidya", &[], &[], Some(display))?; Ok(Self { vulkan, - display: display.as_raw(), + display, window_contexts: HashMap::new(), }) } @@ -1736,15 +1952,14 @@ impl Renderer { commands::SingleUseCommand::new(dev.clone(), pool.pool())?; let buffer = cmd.command_buffer(); - let extent = ctx.current_swapchain.extent; - - let (frame, suboptimal) = - smol::block_on(ctx.current_swapchain.clone().acquire_image())?; + let (frame, suboptimal) = smol::block_on( + ctx.current_swapchain.read().clone().acquire_image(), + )?; if suboptimal { tracing::warn!( "swapchain ({:?}) is suboptimal!", - ctx.current_swapchain.swapchain + ctx.current_swapchain.read().swapchain ); } @@ -1826,42 +2041,11 @@ impl Renderer { Arc::new(sync::Fence::create(dev.clone())?), )?; - let present_id = { - let swapchain = ctx.current_swapchain.swapchain.lock(); - let queue = dev.present_queue().lock(); - - let wait_semaphores = [frame.release]; - let swapchains = [*swapchain]; - let indices = [frame.index]; - static PRESENT_ID: AtomicU64 = AtomicU64::new(1); - let mut present_ids = [PRESENT_ID - .fetch_add(1, std::sync::atomic::Ordering::Release)]; - let mut present_id = - vk::PresentIdKHR::default().present_ids(&present_ids); - let present_info = vk::PresentInfoKHR::default() - .image_indices(core::slice::from_ref(&frame.index)) - .swapchains(core::slice::from_ref(&swapchain)) - .wait_semaphores(&wait_semaphores) - .push_next(&mut present_id); - dev.swapchain().queue_present(*queue, &present_info)?; - - present_ids[0] - }; - + frame.present(); future.block()?; - ctx.current_swapchain.swapchain.with_locked(|swapchain| { - khr::present_wait::Device::new( - &self.vulkan.instance.instance, - self.vulkan.device.dev(), - ) - .wait_for_present( - swapchain, - present_id, - u64::MAX, - ) - })?; - dev.dev().device_wait_idle(); + // wait for idle here is unnecessary. + // dev.dev().device_wait_idle(); } } @@ -2534,3 +2718,58 @@ pub mod utils { } } } + +#[cfg(test)] +mod test_swapchain { + use super::*; + + fn create_headless_vk() -> Result<(Vulkan, WindowContext)> { + let vk = Vulkan::new( + "testing", + &[], + &[ash::ext::headless_surface::NAME, khr::surface::NAME], + None, + )?; + + let surface = Arc::new(Surface::headless(vk.instance.clone())?); + + let swapchain = Arc::new(Swapchain::new( + vk.instance.clone(), + vk.device.clone(), + surface.clone(), + vk.device.phy(), + vk::Extent2D::default().width(1).height(1), + )?); + + let window_ctx = WindowContext { + window_handle: RawWindowHandle::Web( + winit::raw_window_handle::WebWindowHandle::new(0), + ), + surface, + current_swapchain: RwLock::new(swapchain), + }; + + Ok((vk, window_ctx)) + } + + #[tracing_test::traced_test] + #[test] + fn async_swapchain_acquiring() { + let (vk, ctx) = create_headless_vk().expect("init"); + let ctx = Arc::new(ctx); + let (rx, handle) = ctx.images(); + + let mut count = 0; + loop { + let mut now = std::time::Instant::now(); + let frame = rx.recv_blocking().expect("recv"); + frame.present(); + tracing::info!("mspf: {}ms", now.elapsed().as_secs_f64() / 1000.0); + count += 1; + if count > 1000 { + handle.cancel(); + break; + } + } + } +}