From bc6a28a5f5eb50f345913899bd70e8c9603f7655 Mon Sep 17 00:00:00 2001 From: NotAPenguin Date: Thu, 16 Mar 2023 18:37:13 +0100 Subject: [PATCH 1/3] Write docs for command buffer and allocator modules --- src/allocator/default_allocator.rs | 7 +++++++ src/allocator/memory_type.rs | 1 + src/allocator/mod.rs | 12 ++++++++++++ src/allocator/scratch_allocator.rs | 22 ++++++++++++++++------ src/allocator/traits.rs | 10 ++++++++++ src/command_buffer/graphics.rs | 17 +++++++++++++++-- src/command_buffer/incomplete.rs | 12 +++++++++++- src/command_buffer/mod.rs | 19 ++++++++++++++++--- src/command_buffer/traits.rs | 2 +- src/command_buffer/transfer.rs | 4 ++++ src/wsi/frame.rs | 20 ++++++++++++-------- 11 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/allocator/default_allocator.rs b/src/allocator/default_allocator.rs index 8fbee17..82bce1b 100644 --- a/src/allocator/default_allocator.rs +++ b/src/allocator/default_allocator.rs @@ -11,6 +11,9 @@ use gpu_allocator::vulkan::AllocationScheme; use crate::{Device, Error, PhysicalDevice, VkInstance}; use crate::allocator::memory_type::MemoryType; +/// The default allocator. This calls into the `gpu_allocator` crate. +/// It's important to note that this allocator is `Clone`, `Send` and `Sync`. All its internal state is safely +/// wrapped inside an `Arc>`. This is to facilitate passing it around everywhere. #[derive(Clone, Derivative)] #[derivative(Debug)] pub struct DefaultAllocator { @@ -18,6 +21,7 @@ pub struct DefaultAllocator { alloc: Arc>, } +/// Allocation returned from the default allocator. This must be freed explicitly by calling [`DefaultAllocator::free()`] #[derive(Default, Derivative)] #[derivative(Debug)] pub struct Allocation { @@ -25,6 +29,7 @@ pub struct Allocation { } impl DefaultAllocator { + /// Create a new default allocator. pub fn new(instance: &VkInstance, device: &Arc, physical_device: &PhysicalDevice) -> Result { Ok(Self { alloc: Arc::new(Mutex::new(vk_alloc::Allocator::new( @@ -44,6 +49,7 @@ impl DefaultAllocator { impl traits::Allocator for DefaultAllocator { type Allocation = Allocation; + /// Allocates raw memory of a specific memory type. The given name is used for internal tracking. fn allocate(&mut self, name: &'static str, requirements: &MemoryRequirements, ty: MemoryType) -> Result { let mut alloc = self.alloc.lock().map_err(|_| Error::PoisonError)?; let allocation = alloc.allocate(&vk_alloc::AllocationCreateDesc { @@ -59,6 +65,7 @@ impl traits::Allocator for DefaultAllocator { }) } + /// Free some memory allocated from this allocator. fn free(&mut self, allocation: Self::Allocation) -> Result<()> { let mut alloc = self.alloc.lock().map_err(|_| Error::PoisonError)?; alloc.free(allocation.allocation)?; diff --git a/src/allocator/memory_type.rs b/src/allocator/memory_type.rs index 05a7b67..44f48cb 100644 --- a/src/allocator/memory_type.rs +++ b/src/allocator/memory_type.rs @@ -1,3 +1,4 @@ +/// The memory type of an allocation indicates where it should live. #[derive(Debug)] pub enum MemoryType { /// Store the allocation in GPU only accessible memory - typically this is the faster GPU resource and this should be diff --git a/src/allocator/mod.rs b/src/allocator/mod.rs index 2cff21c..7afd3e1 100644 --- a/src/allocator/mod.rs +++ b/src/allocator/mod.rs @@ -1,3 +1,15 @@ +//! The allocator module exposes a couple interesting parts of the API +//!
+//!
+//! # Allocator traits +//! These are defined in [`traits`], and can be implemented to supply a custom allocator type to all phobos functions. +//! # Default allocator +//! A default allocator based on the `gpu_allocator` crate is implemented here. Most types that take a generic allocator +//! parameter default to this allocator. +//! # Scratch allocator +//! A linear allocator used for making temporary, short lived allocations. For more information check the [`scratch_allocator`] +//! module documentation. + pub mod traits; pub mod default_allocator; pub mod memory_type; diff --git a/src/allocator/scratch_allocator.rs b/src/allocator/scratch_allocator.rs index e625c5c..1bff880 100644 --- a/src/allocator/scratch_allocator.rs +++ b/src/allocator/scratch_allocator.rs @@ -7,14 +7,14 @@ //! # Example //! //! ``` -//! use ash::vk; -//! use phobos as ph; -//! +//! use phobos::prelude::*; +//! // Some allocator +//! let alloc = create_allocator(); //! // Create a scratch allocator with at most 1 KiB of available memory for uniform buffers -//! let mut allocator = ph::ScratchAllocator::new(device.clone(), alloc.clone(), (1 * 1024) as vk::DeviceSize, vk::BufferUsageFlags::UNIFORM_BUFFER); +//! let mut allocator = ScratchAllocator::new(device.clone(), alloc.clone(), 1 * 1024u64, vk::BufferUsageFlags::UNIFORM_BUFFER); //! //! // Allocate a 64 byte uniform buffer and use it -//! let buffer = allocator.allocate(64 as vk::DeviceSize)?; +//! let buffer = allocator.allocate(64 as u64)?; //! // For buffer usage, check the buffer module documentation. //! //! // Once we're ready for the next batch of allocations, call reset(). This must happen @@ -31,6 +31,7 @@ use crate::{Allocator, Buffer, BufferView, DefaultAllocator, Device, Error, Memo use crate::Error::AllocationError; use anyhow::Result; +/// Very simple linear allocator. For example usage, see the module level documentation. #[derive(Debug)] pub struct ScratchAllocator { buffer: Buffer, @@ -39,6 +40,8 @@ pub struct ScratchAllocator { } impl ScratchAllocator { + /// Create a new scratch allocator with a specified max capacity. All possible usages for buffers allocated from this should be + /// given in the usage flags. pub fn new(device: Arc, allocator: &mut A, max_size: impl Into, usage: vk::BufferUsageFlags) -> Result { let buffer = Buffer::new(device.clone(), allocator, max_size, usage, MemoryType::CpuToGpu)?; let alignment = if usage.intersects(vk::BufferUsageFlags::VERTEX_BUFFER | vk::BufferUsageFlags::INDEX_BUFFER) { @@ -62,6 +65,9 @@ impl ScratchAllocator { } } + /// Allocates a fixed amount of bytes from the allocator. + /// # Errors + /// - Fails if the allocator has ran out of memory. pub fn allocate(&mut self, size: impl Into) -> Result { let size = size.into(); // Part of the buffer that is over the min alignment @@ -84,7 +90,11 @@ impl ScratchAllocator { } } - pub fn reset(&mut self) { + /// Resets the linear allocator back to the beginning. Proper external synchronization needs to be + /// added to ensure old buffers are not overwritten. + /// # Safety + /// This function is only safe if the old allocations can be completely discarded by the next time [`Self::allocate()`] is called. + pub unsafe fn reset(&mut self) { self.offset = 0; } } \ No newline at end of file diff --git a/src/allocator/traits.rs b/src/allocator/traits.rs index 28c29d6..83bb458 100644 --- a/src/allocator/traits.rs +++ b/src/allocator/traits.rs @@ -4,15 +4,25 @@ use ash::vk; use anyhow::Result; use crate::allocator::memory_type::MemoryType; +/// To supply custom allocators to phobos, this trait must be implemented. +/// Note that all allocators must be `Clone`, `Send` and `Sync`. To do this, wrap internal state in +/// `Arc>` where applicable. pub trait Allocator: Clone + Send + Sync { + /// Allocation type for this allocator. Must implement [`Allocation`] type Allocation: Allocation; + /// Allocates raw memory of a specific memory type. The given name is used for internal tracking. fn allocate(&mut self, name: &'static str, requirements: &vk::MemoryRequirements, ty: MemoryType) -> Result; + /// Free some memory allocated from this allocator. fn free(&mut self, allocation: Self::Allocation) -> Result<()>; } +/// Represents an allocation. This trait exposes methods for accessing the underlying device memory, obtain a mapped pointer, etc. pub trait Allocation: Default { + /// Access the underlying [`VkDeviceMemory`]. Remember to always `Self::offset()` into this. unsafe fn memory(&self) -> vk::DeviceMemory; + /// The offset of this allocation in the underlying memory block. fn offset(&self) -> vk::DeviceSize; + /// Obtain a mapped pointer to the memory, or None if this is not possible. fn mapped_ptr(&self) -> Option>; } \ No newline at end of file diff --git a/src/command_buffer/graphics.rs b/src/command_buffer/graphics.rs index 2641f79..893725b 100644 --- a/src/command_buffer/graphics.rs +++ b/src/command_buffer/graphics.rs @@ -6,7 +6,7 @@ use anyhow::Result; use crate::command_buffer::IncompleteCommandBuffer; impl GraphicsCmdBuffer for IncompleteCommandBuffer<'_, D> { - + /// Sets the viewport and scissor regions to the entire render area. Can only be called inside a renderpass. fn full_viewport_scissor(self) -> Self { let area = self.current_render_area; self.viewport(vk::Viewport { @@ -20,29 +20,37 @@ impl GraphicsCmdBuffer for IncompleteCommandBuf .scissor(area) } - + /// Sets the viewport. Directly translates to [`vkCmdSetViewport`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdSetViewport.html). fn viewport(self, viewport: vk::Viewport) -> Self { unsafe { self.device.cmd_set_viewport(self.handle, 0, std::slice::from_ref(&viewport)); } self } + /// Sets the scissor region. Directly translates to [`vkCmdSetScissor`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdSetScissor.html). fn scissor(self, scissor: vk::Rect2D) -> Self { unsafe { self.device.cmd_set_scissor(self.handle, 0, std::slice::from_ref(&scissor)); } self } + /// Issue a drawcall. This will flush the current descriptor set state and actually bind the descriptor sets. + /// Directly translates to [`vkCmdDraw`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdDraw.html). fn draw(mut self, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) -> Result { self = self.ensure_descriptor_state()?; unsafe { self.device.cmd_draw(self.handle, vertex_count, instance_count, first_vertex, first_instance); } Ok(self) } + /// Issue an indexed drawcall. This will flush the current descriptor state and actually bind the + /// descriptor sets. Directly translates to [`vkCmdDrawIndexed`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdDrawIndexed.html). fn draw_indexed(mut self, index_count: u32, instance_count: u32, first_index: u32, vertex_offset: i32, first_instance: u32) -> Result { self = self.ensure_descriptor_state()?; unsafe { self.device.cmd_draw_indexed(self.handle, index_count, instance_count, first_index, vertex_offset, first_instance) } Ok(self) } + /// Bind a graphics pipeline by name. + /// # Errors + /// Fails if the pipeline was not previously registered in the pipeline cache. fn bind_graphics_pipeline(mut self, name: &str) -> Result { let Some(cache) = &self.pipeline_cache else { return Err(Error::NoPipelineCache.into()); }; { @@ -57,16 +65,21 @@ impl GraphicsCmdBuffer for IncompleteCommandBuf Ok(self) } + /// Binds a vertex buffer to the specified binding point. Note that currently there is no validation as to whether this + /// binding actually exists for the given pipeline. Direct translation of [`vkCmdBindVertexBuffers`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdBindVertexBuffers.html). fn bind_vertex_buffer(self, binding: u32, buffer: &BufferView) -> Self where Self: Sized { unsafe { self.device.cmd_bind_vertex_buffers(self.handle, binding, std::slice::from_ref(&buffer.handle), std::slice::from_ref(&buffer.offset)) }; self } + /// Bind the an index buffer. The index type must match. Direct translation of [`vkCmdBindIndexBuffer`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdBindIndexBuffer.html) fn bind_index_buffer(self, buffer: &BufferView, ty: vk::IndexType) -> Self where Self: Sized { unsafe { self.device.cmd_bind_index_buffer(self.handle, buffer.handle, buffer.offset, ty); } self } + /// Blit a source image to a destination image, using the specified offsets into the images and a filter. Direct and thin wrapper around + /// [`vkCmdBlitImage`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdBlitImage.html) fn blit_image(self, src: &ImageView, dst: &ImageView, src_offsets: &[vk::Offset3D; 2], dst_offsets: &[vk::Offset3D; 2], filter: vk::Filter) -> Self where Self: Sized { let blit = vk::ImageBlit { src_subresource: vk::ImageSubresourceLayers { diff --git a/src/command_buffer/incomplete.rs b/src/command_buffer/incomplete.rs index 9579f08..f9ee1d5 100644 --- a/src/command_buffer/incomplete.rs +++ b/src/command_buffer/incomplete.rs @@ -136,7 +136,7 @@ impl IncompleteCommandBuffer<'_, D> { /// - This function can error if allocating the descriptor set fails. /// # Example /// ``` - /// use phobos::{DescriptorSetBuilder, domain, ExecutionManager}; + /// use phobos::prelude::*; /// let exec = ExecutionManager::new(device.clone(), &physical_device); /// /// let cmd = exec.on_domain::()? /// .bind_graphics_pipeline("my_pipeline", pipeline_cache.clone()) @@ -162,6 +162,9 @@ impl IncompleteCommandBuffer<'_, D> { self } + /// Resolve a virtual resource from the given bindings, and bind it as a sampled image to the given slot. + /// # Errors + /// Fails if the virtual resource has no binding associated to it. pub fn resolve_and_bind_sampled_image(mut self, set: u32, binding: u32, @@ -176,6 +179,7 @@ impl IncompleteCommandBuffer<'_, D> { Ok(self) } + /// Binds a combined image + sampler to the specified slot. pub fn bind_sampled_image(mut self, set: u32, binding: u32, image: &ImageView, sampler: &Sampler) -> Result { self.modify_descriptor_set(set, |builder| { builder.bind_sampled_image(binding, image, sampler); @@ -184,6 +188,7 @@ impl IncompleteCommandBuffer<'_, D> { Ok(self) } + /// Binds a uniform buffer to teh specified slot. pub fn bind_uniform_buffer(mut self, set: u32, binding: u32, buffer: &BufferView) -> Result { self.modify_descriptor_set(set, |builder| { builder.bind_uniform_buffer(binding, buffer); @@ -239,7 +244,12 @@ impl IncompleteCommandBuffer<'_, D> { self } + /// Upload push constants. These are small packets of data stored inside the command buffer, so their state is tracked while executing. + /// Direct translation of [`vkCmdPushConstants`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdPushConstants.html). + /// Tends to crash on some drivers if the specified push constant range does not exist (possible due to unused variable optimization in the shader, + /// or incorrect stage flags specified) pub fn push_constants(self, stage: vk::ShaderStageFlags, offset: u32, data: &[T]) -> Self { + // TODO: Validate push constant ranges with current pipeline layout to prevent crashes. unsafe { let (_, data, _) = data.align_to::(); self.device.cmd_push_constants(self.handle, self.current_pipeline_layout, stage, offset, data); diff --git a/src/command_buffer/mod.rs b/src/command_buffer/mod.rs index 94d4046..22d4b34 100644 --- a/src/command_buffer/mod.rs +++ b/src/command_buffer/mod.rs @@ -16,8 +16,14 @@ //! //! Vulkan command buffers need to call `vkEndCommandBuffer` before they can be submitted. After this call, no more commands should be //! recorded to it. For this reason, we expose two command buffer types. The [`IncompleteCommandBuffer`] still accepts commands, and can only -//! be converted into a [`CommandBuffer`] by calling [`IncompleteCommandBuffer::finish`]. This turns it into a complete commad buffer, which can +//! be converted into a [`CommandBuffer`] by calling [`IncompleteCommandBuffer::finish`]. This turns it into a complete command buffer, which can //! be submitted to the execution manager. +//! +//! # Commands +//! All commands are implemented through traits for each domain. These are all defined inside the [`traits`] module, and are most easily imported +//! through the [`prelude`](crate::prelude). +//! +//! There are also a bunch of methods that do not directly translate to Vulkan commands, for example for binding descriptor sets directly. use std::collections::HashMap; use std::marker::PhantomData; @@ -40,7 +46,7 @@ pub(crate) mod state; pub(crate) mod command_pool; /// This struct represents a finished command buffer. This command buffer can't be recorded to anymore. -/// It can only be obtained by calling finish() on an incomplete command buffer; +/// It can only be obtained by calling [`IncompleteCommandBuffer::finish()`]. pub struct CommandBuffer { pub(crate) handle: vk::CommandBuffer, _domain: PhantomData, @@ -54,7 +60,7 @@ pub struct CommandBuffer { /// /// # Example /// ``` -/// use phobos::{domain, ExecutionManager}; +/// use phobos::prelude::*; /// /// let exec = ExecutionManager::new(device.clone(), &physical_device); /// let cmd = exec.on_domain::()? @@ -64,6 +70,12 @@ pub struct CommandBuffer { /// // This allows the command buffer to be submitted. /// .finish(); /// ``` +/// # Descriptor sets +/// Descriptor sets can be bound simply by calling the associated `bind_xxx` functions on the command buffer. +/// It should be noted that these are not actually bound yet on calling this function. +/// Instead, the next `draw()` or `dispatch()` call flushes these bind calls and does an actual `vkCmdBindDescriptorSets` call. +/// This also forgets the old binding state, so to update the bindings you need to re-bind all previously bound sets (this is something +/// that could change in the future, see https://github.com/NotAPenguin0/phobos-rs/issues/23) #[derive(Derivative)] #[derivative(Debug)] pub struct IncompleteCommandBuffer<'q, D: ExecutionDomain> { @@ -84,6 +96,7 @@ pub struct IncompleteCommandBuffer<'q, D: ExecutionDomain> { } impl<'q, D: ExecutionDomain> CmdBuffer for CommandBuffer { + /// Immediately delete a command buffer. Must be externally synchronized. unsafe fn delete(&mut self, exec: ExecutionManager) -> Result<()> { let queue = exec.get_queue::().ok_or(Error::NoCapableQueue)?; let handle = self.handle; diff --git a/src/command_buffer/traits.rs b/src/command_buffer/traits.rs index 5b45e1b..37c6739 100644 --- a/src/command_buffer/traits.rs +++ b/src/command_buffer/traits.rs @@ -24,7 +24,7 @@ pub trait GraphicsCmdBuffer : TransferCmdBuffer { fn draw(self, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) -> Result where Self: Sized; /// Record a single indexed drawcall. Equivalent of `vkCmdDrawIndexed` fn draw_indexed(self, index_count: u32, instance_count: u32, first_index: u32, vertex_offset: i32, first_instance: u32) -> Result where Self: Sized; - /// Bind a graphics pipeline with a given name. This is looked up from the given pipeline cache. + /// Bind a graphics pipeline with a given name. /// # Errors /// This function can report an error in case the pipeline name is not registered in the cache. fn bind_graphics_pipeline(self, name: &str) -> Result where Self: Sized; diff --git a/src/command_buffer/transfer.rs b/src/command_buffer/transfer.rs index 5781a63..6092d74 100644 --- a/src/command_buffer/transfer.rs +++ b/src/command_buffer/transfer.rs @@ -6,6 +6,9 @@ use ash::vk; use crate::command_buffer::IncompleteCommandBuffer; impl TransferCmdBuffer for IncompleteCommandBuffer<'_, D> { + /// Copy one buffer to the other. + /// # Errors + /// Fails if the buffer views do not have the same size. fn copy_buffer(self, src: &BufferView, dst: &BufferView) -> Result { if src.size != dst.size { return Err(Error::InvalidBufferCopy.into()); @@ -22,6 +25,7 @@ impl TransferCmdBuffer for IncompleteComma Ok(self) } + /// Copy a buffer to the base mip level of the specified image. fn copy_buffer_to_image(self, src: &BufferView, dst: &ImageView) -> Result where Self: Sized { let copy = vk::BufferImageCopy { buffer_offset: src.offset, diff --git a/src/wsi/frame.rs b/src/wsi/frame.rs index dfaee2e..1d37f36 100644 --- a/src/wsi/frame.rs +++ b/src/wsi/frame.rs @@ -264,10 +264,12 @@ impl FrameManager { self.current_frame = (self.current_frame + 1) % self.per_frame.len() as u32; // Advance per-frame allocator to the next frame - self.per_frame[self.current_frame as usize].vertex_allocator.reset(); - self.per_frame[self.current_frame as usize].index_allocator.reset(); - self.per_frame[self.current_frame as usize].uniform_allocator.reset(); - self.per_frame[self.current_frame as usize].storage_allocator.reset(); + unsafe { + self.per_frame[self.current_frame as usize].vertex_allocator.reset(); + self.per_frame[self.current_frame as usize].index_allocator.reset(); + self.per_frame[self.current_frame as usize].uniform_allocator.reset(); + self.per_frame[self.current_frame as usize].storage_allocator.reset(); + } let (index, resize_required) = self.acquire_image()?; self.current_image = index; @@ -329,10 +331,12 @@ impl FrameManager { self.current_frame = (self.current_frame + 1) % self.per_frame.len() as u32; // Advance per-frame allocator to the next frame - self.per_frame[self.current_frame as usize].vertex_allocator.reset(); - self.per_frame[self.current_frame as usize].index_allocator.reset(); - self.per_frame[self.current_frame as usize].uniform_allocator.reset(); - self.per_frame[self.current_frame as usize].storage_allocator.reset(); + unsafe { + self.per_frame[self.current_frame as usize].vertex_allocator.reset(); + self.per_frame[self.current_frame as usize].index_allocator.reset(); + self.per_frame[self.current_frame as usize].uniform_allocator.reset(); + self.per_frame[self.current_frame as usize].storage_allocator.reset(); + } let (index, resize_required) = self.acquire_image()?; self.current_image = index; From f782586b18172f0684c119c49aa65f8d5f34ce08 Mon Sep 17 00:00:00 2001 From: NotAPenguin Date: Thu, 16 Mar 2023 23:43:19 +0100 Subject: [PATCH 2/3] Add submit batching and type-safe semaphore sync for it. --- examples/basic/main.rs | 14 ++- src/command_buffer/mod.rs | 1 + src/core/queue.rs | 19 +++- src/sync/execution_manager.rs | 25 +++-- src/sync/mod.rs | 1 + src/sync/semaphore.rs | 1 - src/sync/submit_batch.rs | 166 ++++++++++++++++++++++++++++++++++ 7 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 src/sync/submit_batch.rs diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 39ce35c..4a55c59 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -4,7 +4,7 @@ use std::io::Read; use std::path::Path; use std::sync::{Arc, Mutex}; -use phobos::prelude as ph; +use phobos::{domain, PipelineStage, prelude as ph}; use phobos::command_buffer::traits::*; use phobos::RecordGraphToCommandBuffer; use ph::vk; @@ -36,6 +36,18 @@ fn main_loop(frame: &mut ph::FrameManager, exec: ph::ExecutionManager, surface: &ph::Surface, window: &winit::window::Window) -> Result<()> { + + // Lets try the new batch submit API + block_on({ + let cmd1 = exec.on_domain::(None, None)?.finish()?; + let cmd2 = exec.on_domain::(None, None)?.finish()?; + let mut batch = exec.start_submit_batch::()?; + batch.submit(cmd1)? + .then(PipelineStage::COLOR_ATTACHMENT_OUTPUT, cmd2, &mut batch)?; + batch.finish()? + }); + + // Define a virtual resource pointing to the swapchain let swap_resource = ph::VirtualResource::image("swapchain".to_string()); let offscreen = ph::VirtualResource::image("offscreen".to_string()); diff --git a/src/command_buffer/mod.rs b/src/command_buffer/mod.rs index 22d4b34..fd5e09c 100644 --- a/src/command_buffer/mod.rs +++ b/src/command_buffer/mod.rs @@ -47,6 +47,7 @@ pub(crate) mod command_pool; /// This struct represents a finished command buffer. This command buffer can't be recorded to anymore. /// It can only be obtained by calling [`IncompleteCommandBuffer::finish()`]. +#[derive(Debug)] pub struct CommandBuffer { pub(crate) handle: vk::CommandBuffer, _domain: PhantomData, diff --git a/src/core/queue.rs b/src/core/queue.rs index 395474e..a2d92d2 100644 --- a/src/core/queue.rs +++ b/src/core/queue.rs @@ -46,7 +46,7 @@ pub struct Queue { } impl Queue { - pub fn new(device: Arc, handle: vk::Queue, info: QueueInfo) -> Result { + pub(crate) fn new(device: Arc, handle: vk::Queue, info: QueueInfo) -> Result { // We create a transient command pool because command buffers will be allocated and deallocated // frequently. let pool = CommandPool::new(device.clone(), info.family_index, vk::CommandPoolCreateFlags::TRANSIENT)?; @@ -72,7 +72,22 @@ impl Queue { self.device.queue_submit(self.handle, submits, fence) } - pub fn handle(&self) -> vk::Queue { + /// Submits a batch of submissions to the queue, and signals the given fence when the + /// submission is done + ///
+ ///
+ /// # Thread safety + /// This function is **not yet** thread safe! This function is marked as unsafe for now to signal this. + pub unsafe fn submit2(&self, submits: &[vk::SubmitInfo2], fence: Option<&Fence>) -> Result<(), vk::Result> { + let fence = match fence { + None => { vk::Fence::null() } + Some(fence) => { fence.handle } + }; + self.device.queue_submit2(self.handle, submits, fence) + } + + /// Obtain the raw vulkan handle of a queue. + pub unsafe fn handle(&self) -> vk::Queue { self.handle } diff --git a/src/sync/execution_manager.rs b/src/sync/execution_manager.rs index 258ecc7..edc4a88 100644 --- a/src/sync/execution_manager.rs +++ b/src/sync/execution_manager.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex, MutexGuard, TryLockError, TryLockResult}; -use crate::{CmdBuffer, DescriptorCache, Device, domain, Error, Fence, PhysicalDevice, PipelineCache}; +use crate::{CmdBuffer, DescriptorCache, Device, Error, Fence, PhysicalDevice, PipelineCache}; use crate::command_buffer::*; use anyhow::Result; use ash::vk; use crate::core::queue::Queue; +use crate::domain::ExecutionDomain; +use crate::sync::submit_batch::SubmitBatch; /// The execution manager is responsible for allocating command buffers on correct /// queues. To obtain any command buffer, you must allocate it by calling @@ -67,7 +69,7 @@ impl ExecutionManager { } /// Tries to obtain a command buffer over a domain, or returns an Err state if the lock is currently being held. - pub fn try_on_domain<'q, D: domain::ExecutionDomain>(&'q self, + pub fn try_on_domain<'q, D: ExecutionDomain>(&'q self, pipelines: Option>>, descriptors: Option>>) -> Result> { let queue = self.try_get_queue::().map_err(|_| Error::QueueLocked)?; @@ -75,15 +77,20 @@ impl ExecutionManager { } /// Obtain a command buffer capable of operating on the specified domain. - pub fn on_domain<'q, D: domain::ExecutionDomain>(&'q self, + pub fn on_domain<'q, D: ExecutionDomain>(&'q self, pipelines: Option>>, descriptors: Option>>) -> Result> { let queue = self.get_queue::().ok_or(Error::NoCapableQueue)?; Queue::allocate_command_buffer::<'q, D::CmdBuf<'q>>(self.device.clone(), queue, pipelines, descriptors) } + /// Begin a submit batch. Note that all submits in a batch are over a single domain (currently). + pub fn start_submit_batch(&self) -> Result> { + SubmitBatch::new(self.device.clone(), self.clone()) + } + /// Submit a command buffer to its queue. TODO: Add semaphores - pub fn submit<'q, D: domain::ExecutionDomain + 'static>(&self, mut cmd: CommandBuffer) -> Result { + pub fn submit(&self, mut cmd: CommandBuffer) -> Result { let fence = Fence::new(self.device.clone(), false)?; let info = vk::SubmitInfo { @@ -107,12 +114,18 @@ impl ExecutionManager { })) } + pub(crate) fn submit_batch(&self, submits: &[vk::SubmitInfo2], fence: &Fence) -> Result<()> { + let queue = self.get_queue::().ok_or(Error::NoCapableQueue)?; + unsafe { queue.submit2(submits, Some(fence))?; } + Ok(()) + } + /// Obtain a reference to a queue capable of presenting. pub(crate) fn get_present_queue(&self) -> Option> { self.queues.iter().find(|&queue| queue.lock().unwrap().info.can_present.clone()).map(|q| q.lock().unwrap()) } - pub fn try_get_queue(&self) -> TryLockResult> { + pub fn try_get_queue(&self) -> TryLockResult> { let q = self.queues.iter().find(|&q| { let q = q.try_lock(); match q { @@ -127,7 +140,7 @@ impl ExecutionManager { } /// Obtain a reference to a queue matching predicate. - pub fn get_queue(&self) -> Option> { + pub fn get_queue(&self) -> Option> { self.queues.iter().find(|&q| { let q = q.lock().unwrap(); D::queue_is_compatible(&*q) diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 3ef24d8..d705dde 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -2,3 +2,4 @@ pub mod fence; pub mod semaphore; pub mod execution_manager; pub mod thread_context; +pub mod submit_batch; diff --git a/src/sync/semaphore.rs b/src/sync/semaphore.rs index 1d8a09e..548ec0c 100644 --- a/src/sync/semaphore.rs +++ b/src/sync/semaphore.rs @@ -9,7 +9,6 @@ pub struct Semaphore { pub handle: vk::Semaphore, } - impl Semaphore { /// Create a new `VkSemaphore` object. pub fn new(device: Arc) -> Result { diff --git a/src/sync/submit_batch.rs b/src/sync/submit_batch.rs new file mode 100644 index 0000000..a93a9f1 --- /dev/null +++ b/src/sync/submit_batch.rs @@ -0,0 +1,166 @@ +use std::rc::Rc; +use std::sync::Arc; +use crate::{CmdBuffer, Device, ExecutionManager, Fence, PipelineStage, Semaphore}; +use crate::command_buffer::CommandBuffer; +use crate::domain::ExecutionDomain; + +use anyhow::Result; +use ash::vk; + + +#[derive(Debug)] +struct SubmitInfo { + cmd: CommandBuffer, + signal_semaphore: Option>, + wait_semaphores: Vec>, + wait_stages: Vec, +} + +#[derive(Debug, Copy, Clone)] +pub struct SubmitHandle { + index: usize, +} + +#[derive(Debug)] +pub struct SubmitBatch { + device: Arc, + exec: ExecutionManager, + submits: Vec>, + signal_fence: Fence, +} + +impl SubmitBatch { + pub(crate) fn new(device: Arc, exec: ExecutionManager) -> Result { + Ok(Self { + submits: vec![], + signal_fence: Fence::new(device.clone(), false)?, + device, + exec, + }) + } + + fn get_submit_semaphore(&self, submit: SubmitHandle) -> Option> { + self.submits.get(submit.index).map(|submit| submit.signal_semaphore.clone()).flatten() + } + + fn submit_after(&mut self, handles: &[SubmitHandle], cmd: CommandBuffer, wait_stages: &[PipelineStage]) -> Result { + let wait_semaphores = handles + .iter() + .map(|handle| { + self.get_submit_semaphore(*handle).unwrap() + }) + .collect::>(); + + self.submits.push(SubmitInfo { + cmd, + signal_semaphore: Some(Rc::new(Semaphore::new(self.device.clone())?)), + wait_semaphores, + wait_stages: wait_stages.to_vec(), + }); + + Ok(SubmitHandle { + index: self.submits.len() - 1, + }) + } + + pub fn submit(&mut self, cmd: CommandBuffer) -> Result { + self.submits.push(SubmitInfo { + cmd, + signal_semaphore: Some(Rc::new(Semaphore::new(self.device.clone())?)), + wait_semaphores: vec![], + wait_stages: vec![], + }); + + Ok(SubmitHandle { + index: self.submits.len() - 1, + }) + } + + pub fn finish(self) -> Result { + struct PerSubmit { + wait_semaphores: Vec, + cmd_buffer: Vec, + signal_semaphores: Vec, + } + + let mut per_submit_info = Vec::new(); + for submit in &self.submits { + let info = PerSubmit { + wait_semaphores: submit.wait_semaphores + .iter() + .zip(&submit.wait_stages) + .map(|(semaphore, stage)| { + vk::SemaphoreSubmitInfo { + s_type: vk::StructureType::SEMAPHORE_SUBMIT_INFO, + p_next: std::ptr::null(), + semaphore: semaphore.handle, + value: 0, + stage_mask: *stage, + device_index: 0, + } + }) + .collect(), + cmd_buffer: vec![vk::CommandBufferSubmitInfo { + s_type: vk::StructureType::COMMAND_BUFFER_SUBMIT_INFO, + p_next: std::ptr::null(), + command_buffer: submit.cmd.handle, + device_mask: 0, + }], + signal_semaphores: match &submit.signal_semaphore { + None => { vec![] } + Some(semaphore) => { + vec![ + vk::SemaphoreSubmitInfo { + s_type: vk::StructureType::SEMAPHORE_SUBMIT_INFO, + p_next: std::ptr::null(), + semaphore: semaphore.handle, + value: 0, + stage_mask: PipelineStage::BOTTOM_OF_PIPE, + device_index: 0, + } + ] + } + }, + }; + per_submit_info.push(info); + } + let submits = per_submit_info + .iter() + .map(|submit| { + vk::SubmitInfo2 { + s_type: vk::StructureType::SUBMIT_INFO_2, + p_next: std::ptr::null(), + flags: Default::default(), + wait_semaphore_info_count: submit.wait_semaphores.len() as u32, + p_wait_semaphore_infos: submit.wait_semaphores.as_ptr(), + command_buffer_info_count: submit.cmd_buffer.len() as u32, + p_command_buffer_infos: submit.cmd_buffer.as_ptr(), + signal_semaphore_info_count: submit.signal_semaphores.len() as u32, + p_signal_semaphore_infos: submit.signal_semaphores.as_ptr(), + } + }) + .collect::>(); + + self.exec.submit_batch::(submits.as_slice(), &self.signal_fence)?; + let fence = self.signal_fence.with_cleanup(move || { + // Take ownership of every resource inside the submit batch, to delete it afterwards + for mut submit in self.submits { + unsafe { submit.cmd.delete(self.exec.clone()).unwrap(); } + } + }); + + Ok(fence) + } +} + +impl SubmitHandle { + pub fn then( + &self, + wait_stage: PipelineStage, + cmd: CommandBuffer, + batch: &mut SubmitBatch) + -> Result { + + batch.submit_after(std::slice::from_ref(&self), cmd, std::slice::from_ref(&wait_stage)) + } +} \ No newline at end of file From cc035a7d64727120d339ddbec4423bb01469725c Mon Sep 17 00:00:00 2001 From: NotAPenguin Date: Fri, 17 Mar 2023 01:31:03 +0100 Subject: [PATCH 3/3] Update docs and add examples where needed --- examples/basic/main.rs | 2 +- src/buffer.rs | 28 +++++++++-- src/core/debug.rs | 1 + src/core/mod.rs | 2 + src/descriptor/builder.rs | 21 ++++++++ src/descriptor/cache.rs | 2 - src/descriptor/descriptor_set.rs | 3 ++ src/graph/mod.rs | 33 +++++++------ src/graph/pass.rs | 41 +++++++++------- src/graph/pass_graph.rs | 10 ++-- src/graph/physical_resource.rs | 8 ++-- src/graph/resource.rs | 5 +- src/image.rs | 3 +- src/lib.rs | 1 + src/pipeline/builder.rs | 23 ++++++++- src/pipeline/cache.rs | 14 ++++-- src/pipeline/create_info.rs | 6 +-- src/pipeline/mod.rs | 19 ++++---- src/pipeline/pipeline_layout.rs | 2 +- src/pipeline/set_layout.rs | 2 +- src/pipeline/shader.rs | 2 + src/pipeline/shader_reflection.rs | 2 +- src/sync/execution_manager.rs | 35 ++++++++++---- src/sync/fence.rs | 79 +++++++++++++++++++++++++++++++ src/sync/mod.rs | 13 +++++ src/sync/submit_batch.rs | 9 ++++ src/sync/thread_context.rs | 2 + src/util/byte_size.rs | 3 ++ src/util/deferred_delete.rs | 1 + src/wsi/frame.rs | 53 +++++++++++++-------- src/wsi/mod.rs | 3 ++ src/wsi/surface.rs | 1 + src/wsi/swapchain.rs | 3 ++ 33 files changed, 331 insertions(+), 101 deletions(-) diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 4a55c59..908628c 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -41,7 +41,7 @@ fn main_loop(frame: &mut ph::FrameManager, block_on({ let cmd1 = exec.on_domain::(None, None)?.finish()?; let cmd2 = exec.on_domain::(None, None)?.finish()?; - let mut batch = exec.start_submit_batch::()?; + let mut batch = exec.start_submit_batch()?; batch.submit(cmd1)? .then(PipelineStage::COLOR_ATTACHMENT_OUTPUT, cmd2, &mut batch)?; batch.finish()? diff --git a/src/buffer.rs b/src/buffer.rs index f9ae773..a19c439 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,4 +1,4 @@ -//! Similarly to the [`image`] module, this module exposes two types: [`Buffer`] and [`BufferView`]. The difference here is that a +//! Similarly to the [`image`](crate::image) module, this module exposes two types: [`Buffer`] and [`BufferView`]. The difference here is that a //! [`BufferView`] does not own a vulkan resource, so it cane be freely copied around as long as the owning [`Buffer`] lives. //! //! It also exposes some utilities for writing to memory-mapped buffers. For this you can use [`BufferView::mapped_slice`]. This only succeeds @@ -7,9 +7,8 @@ //! # Example //! //! ``` -//! use ash::vk; -//! use gpu_allocator::MemoryLocation; -//! use phobos as ph; +//! use phobos::prelude::*; +//! //! // Allocate a new buffer //! let buf = Buffer::new(device.clone(), //! alloc.clone(), @@ -19,7 +18,7 @@ //! vk::BufferUsageFlags::UNIFORM_BUFFER, //! // CpuToGpu will always set HOST_VISIBLE and HOST_COHERENT, and try to set DEVICE_LOCAL. //! // Usually this resides on the PCIe BAR. -//! MemoryLocation::CpuToGpu); +//! MemoryType::CpuToGpu); //! // Obtain a buffer view to the entire buffer. //! let mut view = buf.view_full(); //! // Obtain a slice of floats @@ -38,6 +37,7 @@ use crate::{Allocation, Allocator, DefaultAllocator, Device, Error, MemoryType}; use anyhow::Result; +/// Wrapper around a [`VkBuffer`](vk::Buffer). #[derive(Derivative)] #[derivative(Debug)] pub struct Buffer { @@ -52,6 +52,9 @@ pub struct Buffer { pub size: vk::DeviceSize, } +/// View into a specific offset and range of a [`Buffer`]. +/// Care should be taken with the lifetime of this, as there is no checking that the buffer +/// is not dropped while using this. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct BufferView { pub(crate) handle: vk::Buffer, @@ -61,6 +64,8 @@ pub struct BufferView { } impl Buffer
{ + /// Allocate a new buffer with a specific size, at a specific memory location. + /// All usage flags must be given. pub fn new(device: Arc, allocator: &mut A, size: impl Into, usage: vk::BufferUsageFlags, location: MemoryType) -> Result { let size = size.into(); let handle = unsafe { @@ -91,10 +96,16 @@ impl Buffer { }) } + /// Allocate a new buffer with device local memory (VRAM). This is usually the correct memory location for most buffers. pub fn new_device_local(device: Arc, allocator: &mut A, size: impl Into, usage: vk::BufferUsageFlags) -> Result { Self::new(device, allocator, size, usage, MemoryType::GpuOnly) } + /// Creates a view into an offset and size of the buffer. + /// # Lifetime + /// This view is valid as long as the buffer is valid. + /// # Errors + /// Fails if `offset + size >= self.size`. pub fn view(&self, offset: impl Into, size: impl Into) -> Result { let offset = offset.into(); let size = size.into(); @@ -110,6 +121,9 @@ impl Buffer { } } + /// Creates a view of the entire buffer. + /// # Lifetime + /// This view is valid as long as the buffer is valid. pub fn view_full(&self) -> BufferView { BufferView { handle: self.handle, @@ -119,6 +133,7 @@ impl Buffer { } } + /// True if this buffer has a mapped pointer and thus can directly be written to. pub fn is_mapped(&self) -> bool { self.pointer.is_some() } @@ -133,6 +148,9 @@ impl Drop for Buffer { } impl BufferView { + /// Obtain a slice to the mapped memory of this buffer. + /// # Errors + /// Fails if this buffer is not mappable (not `HOST_VISIBLE`). pub fn mapped_slice(&mut self) -> Result<&mut [T]> { if let Some(pointer) = self.pointer { Ok(unsafe { std::slice::from_raw_parts_mut(pointer.cast::().as_ptr(), self.size as usize / std::mem::size_of::()) }) diff --git a/src/core/debug.rs b/src/core/debug.rs index 69880df..71b1c5c 100644 --- a/src/core/debug.rs +++ b/src/core/debug.rs @@ -4,6 +4,7 @@ use crate::{VkInstance}; use anyhow::Result; use crate::util::string::wrap_c_str; +/// Vulkan debug messenger, can be passed to certain functions to extend debugging functionality. #[derive(Derivative)] #[derivative(Debug)] pub struct DebugMessenger { diff --git a/src/core/mod.rs b/src/core/mod.rs index 2df9e21..17af538 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,5 @@ +//! The core module holds all functionality that is minimally required to initialize a Vulkan context. + pub mod app_info; pub mod instance; pub mod device; diff --git a/src/descriptor/builder.rs b/src/descriptor/builder.rs index 9effb9b..b1947f3 100644 --- a/src/descriptor/builder.rs +++ b/src/descriptor/builder.rs @@ -1,3 +1,6 @@ +//! The descriptor set builder is useful for building descriptor sets, but its public usage is now deprecated. +//! Instead, use the `bind_xxx` functions of [`IncompleteCommandBuffer`](crate::command_buffer::IncompleteCommandBuffer) + use crate::{BufferView, Error, ImageView, PhysicalResourceBindings, Sampler, VirtualResource}; use anyhow::Result; @@ -44,6 +47,7 @@ pub struct DescriptorSetBuilder<'a> { impl<'r> DescriptorSetBuilder<'r> { + /// Create a new empty descriptor set builder with no reflection information. pub fn new() -> Self { Self { inner: DescriptorSetBinding { @@ -58,6 +62,8 @@ impl<'r> DescriptorSetBuilder<'r> { } } + /// Create a new empty descriptor set builder with associated reflection information. + /// This enables the usage of the `bind_named_xxx` set of functions. #[cfg(feature="shader-reflection")] pub fn with_reflection(info: &'r ReflectionInfo) -> Self { Self { @@ -70,6 +76,9 @@ impl<'r> DescriptorSetBuilder<'r> { } } + /// Resolve the virtual resource through the given bindings, and bind it to a specific slot as a combined image sampler. + /// # Errors + /// Fails if the binding did not exist, or did not contain an image. pub fn resolve_and_bind_sampled_image(&mut self, binding: u32, resource: &VirtualResource, sampler: &Sampler, bindings: &PhysicalResourceBindings) -> Result<()> { if let Some(PhysicalResource::Image(image)) = bindings.resolve(resource) { self.bind_sampled_image(binding, image, sampler); @@ -92,6 +101,11 @@ impl<'r> DescriptorSetBuilder<'r> { }); } + /// Bind an image view to the given binding as a [`vk::DescriptorType::COMBINED_IMAGE_SAMPLER`]. + /// Uses the reflection information provided at construction to look up the correct binding slot by its name + /// defined in the shader. + /// # Errors + /// Fails if `self` was not constructed with [`DescriptorSetBuilder::with_reflection()`]. #[cfg(feature="shader-reflection")] pub fn bind_named_sampled_image(&mut self, name: &str, image: &ImageView, sampler: &Sampler) -> Result<()> { let Some(info) = self.reflection else { return Err(Error::NoReflectionInformation.into()); }; @@ -100,6 +114,7 @@ impl<'r> DescriptorSetBuilder<'r> { Ok(()) } + /// Bind a uniform buffer to the specified slot. pub fn bind_uniform_buffer(&mut self, binding: u32, buffer: &BufferView) -> () { self.inner.bindings.push(DescriptorBinding { binding, @@ -110,6 +125,11 @@ impl<'r> DescriptorSetBuilder<'r> { }); } + /// Bind a buffer to the given binding as a [`vk::DescriptorType::UNIFORM_BUFFER`]. + /// Uses the reflection information provided at construction to look up the correct binding slot by its name + /// defined in the shader. + /// # Errors + /// Fails if `self` was not constructed with [`DescriptorSetBuilder::with_reflection()`]. #[cfg(feature="shader-reflection")] pub fn bind_named_uniform_buffer(&mut self, name: &str, buffer: &BufferView) -> Result<()> { let Some(info) = self.reflection else { return Err(Error::NoReflectionInformation.into()); }; @@ -118,6 +138,7 @@ impl<'r> DescriptorSetBuilder<'r> { Ok(()) } + /// Build the descriptor set creation info to pass into the cache. pub fn build(self) -> DescriptorSetBinding { self.inner } diff --git a/src/descriptor/cache.rs b/src/descriptor/cache.rs index 0106045..8273046 100644 --- a/src/descriptor/cache.rs +++ b/src/descriptor/cache.rs @@ -19,8 +19,6 @@ pub struct DescriptorCache { deferred_pool_delete: DeletionQueue, } - - impl DescriptorCache { /// Create a new descriptor cache object. /// # Errors diff --git a/src/descriptor/descriptor_set.rs b/src/descriptor/descriptor_set.rs index be57c10..a06f7fa 100644 --- a/src/descriptor/descriptor_set.rs +++ b/src/descriptor/descriptor_set.rs @@ -30,6 +30,7 @@ pub(crate) struct DescriptorBinding { pub descriptors: Vec, } +/// Specifies a set of bindings in a descriptor set. Can be created by a [`DescriptorSetBuilder`](crate::DescriptorSetBuilder). #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct DescriptorSetBinding { pub(crate) pool: vk::DescriptorPool, @@ -37,6 +38,8 @@ pub struct DescriptorSetBinding { pub(crate) layout: vk::DescriptorSetLayout, } +/// Wrapper over a Vulkan `VkDescriptorSet`. You don't explicitly need to use this, as the command buffer and descriptor cache can manage these +/// fully for you. #[derive(Derivative)] #[derivative(Debug, PartialEq, Eq)] pub struct DescriptorSet { diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 41cb351..9c9665c 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -1,45 +1,48 @@ -//! The task graph system is a powerful abstraction that allows you to automatically manage synchronization **within a single queue**, +//! The pass graph system is a powerful abstraction that allows you to automatically manage synchronization **within a single queue**, //! and automatically transition image layouts based on usage. Each pass needs to declare its inputs and outputs, and then the graph -//! is built and barriers are inserted where needed. All resources are specified as [`VirtualResource`]s, referenced by a string ID. +//! is built and barriers are inserted where needed. All resources are specified as [`VirtualResource`](crate::VirtualResource)s, referenced by a string ID. //! This means that it's possible to not have to rebuild the graph every frame. //! //! Actual resources need to be bound to each virtual resource before recording the graph into a command buffer. -//! This is done using the [`PhysicalResourceBindings`] struct. +//! This is done using the [`PhysicalResourceBindings`](crate::PhysicalResourceBindings) struct. //! -//! Through the [`GraphViz`] trait, it's possible to export a graphviz-compatible dot file to display the task graph. +//! Through the [`GraphViz`](task_graph::GraphViz) trait, it's possible to export a graphviz-compatible dot file to display the task graph. //! //! # Example //! //! ``` -//! use phobos as ph; +//! use phobos::prelude::*; //! //! // Define a virtual resource for the swapchain -//! let swap_resource = ph::VirtualResource::image("swapchain".to_string()); +//! let swap_resource = VirtualResource::image("swapchain"); //! // Define a pass that will handle the layout transition to `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`. //! // This is required in your main frame graph. -//! let present_pass = ph::PassBuilder::present("present".to_string(), swap_resource); -//! let mut graph = ph::PassGraph::::new(swap_resource.clone()); +//! let present_pass = PassBuilder::present("present", &swap_resource); +//! // Create the graph. Note that we need to pass the swapchain resource to it as well. +//! let mut graph = PassGraph::::new(Some(&swap_resource)); +//! // Add our pass //! graph.add_pass(present_pass)?; //! // Build the graph and obtain a BuiltPassGraph. //! let mut graph = graph.build()?; +//! // To record, check the next example. //! ``` //! //! For more complex passes, see the [`pass`] module documentation. //! //! # Recording //! -//! Once a graph has been built it can be recorded to a compatible command buffer (one over the same [`ExecutionDomain`] as the task graph). -//! To do this, first bind physical resources to each virtual resource used, and then call [`ph::record_graph`]. +//! Once a graph has been built it can be recorded to a compatible command buffer (one over the same [`ExecutionDomain`](crate::domain::ExecutionDomain) as the task graph. The +//! type system enforces this.). +//! To do this, first bind physical resources to each virtual resource used, and then call [`BuiltPassGraph::record()`](crate::graph::pass_graph::BuiltPassGraph::record). //! //! Using the graph from the previous example: //! ``` -//! use phobos as ph; -//! use phobos::IncompleteCmdBuffer; +//! use phobos::prelude::*; //! //! // Bind swapchain virtual resource to this frame's swapchain image. -//! let mut bindings = ph::PhysicalResourceBindings::new(); -//! bindings.bind_image("swapchain".to_string(), ifc.swapchain_image.as_ref().unwrap().clone()); -//! let cmd = exec.on_domain::()?; +//! let mut bindings = PhysicalResourceBindings::new(); +//! bindings.bind_image("swapchain", ifc.swapchain_image.as_ref().unwrap()); +//! let cmd = exec.on_domain::(None, None)?; //! // Debug messenger not required, but recommended together with the `debug-markers` feature. //! let final_cmd = graph.record(cmd, &bindings, &mut ifc, Some(debug_messenger))? //! .finish(); diff --git a/src/graph/pass.rs b/src/graph/pass.rs index 6fb20a1..7816f92 100644 --- a/src/graph/pass.rs +++ b/src/graph/pass.rs @@ -1,51 +1,55 @@ //! This module mainly exposes the [`PassBuilder`] struct, used for correctly defining passes in a -//! [`GpuTaskGraph`]. For documentation on how to use the task graph, refer to the [`task_graph`] module. +//! [`PassGraph`](crate::PassGraph). For documentation on how to use the pass graph, refer to the [`graph`](crate::graph) module level documentation. //! There are a few different types of passes. Each pass must declare its inputs and outputs, and can optionally //! specify a closure to be executed when the pass is recorded to a command buffer. Additionally, a color can be given to each pass -//! which will show up in debuggers like `RenderDoc` if the `debug-markers` feature is enabled. +//! which will show up in debuggers like [*RenderDoc*](https://renderdoc.org/) if the `debug-markers` feature is enabled. //! //! # Example //! //! In this example we will define two passes: One that writes to an offscreen texture, and one that samples from this //! texture to render it to the screen. This is a very simple dependency, but a very common pattern. //! The task graph system will ensure access is properly synchronized, and the offscreen image is properly transitioned from its -//! initial layout before execution, to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`, to `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`. +//! initial layout before execution, to `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`, and then to `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` +//! before the final pass. //! //! First, we define some virtual resources. //! ``` -//! use phobos as ph; +//! use phobos::prelude::*; //! -//! let offscreen = ph::VirtualResource::new("offscreen".to_string()); -//! let swapchain = ph::VirtualResource::new("swapchain".to_string()); +//! let offscreen = ph::VirtualResource::image("offscreen"); +//! let swapchain = ph::VirtualResource::image("swapchain"); //! ``` //! Now we create the offscreen pass. Note that we use [`PassBuilder::render()`] to create a render pass. //! This is a pass that outputs to at least one color or depth attachment by using the graphics pipeline. +//! A pass that was not created through this method cannot define any attachments, this is useful for +//! e.g. compute-only passes. //! ``` -//! use ash::vk; +//! use phobos::prelude::*; //! -//! let offscreen_pass = ph::PassBuilder::render("offscreen".to_string()) +//! let offscreen_pass = PassBuilder::render("offscreen") //! // All this does is make the pass show up red in graphics debuggers, if //! // the debug-markers feature is enabled. //! .color([1.0, 0.0, 0.0, 1.0]) //! // Add a single color attachment that will be cleared to red. -//! .color_attachment(offscreen.clone(), +//! .color_attachment(&offscreen, //! vk::AttachmentLoadOp::CLEAR, //! Some(vk::ClearColorValue{ float32: [1.0, 0.0, 0.0, 1.0] }))? //! .build(); //! //! ``` //! Next we can create the pass that will sample from this pass. To do this, we have to declare that we will sample -//! using [`PassBuilder::sample_image`]. Note that this is only necessary for images that are used elsewhere in the frame. +//! the virtual resource using [`PassBuilder::sample_image`]. +//! Note that this is only necessary for images that are used elsewhere in the frame. //! Regular textures for rendering do not need to be declared like this. //! //! ``` -//! use ash::vk; +//! use phobos::prelude::*; //! //! // This is important. The virtual resource that encodes the output of the offscreen pass is not the same as the one -//! // we gave it in `color_attachment`. We have to look up the correct version using `Pass::output()`. +//! // we gave it in `color_attachment()`. We have to look up the correct version using `Pass::output()`. //! let input_resource = offscreen_pass.output(&offscreen).unwrap(); //! -//! let sample_pass = ph::PassBuilder::render("sample".to_string()) +//! let sample_pass = PassBuilder::render("sample") //! // Let's color this pass green //! .color([0.0, 1.0, 0.0, 1.0]) //! // Clear the swapchain to black. @@ -53,15 +57,20 @@ //! vk::AttachmentLoadOp::CLEAR, //! Some(vk::ClearColorValue{ float32: [0.0, 0.0, 0.0, 1.0] }))? //! // We sample the input resource in the fragment shader. -//! .sample_image(input_resource.clone(), ph::PipelineStage::FRAGMENT_SHADER) +//! .sample_image(&input_resource, PipelineStage::FRAGMENT_SHADER) //! .execute(|mut cmd, ifc, bindings| { -//! // Commands to sample from the input texture go here +//! // Draw a fullscreen quad using our sample pipeline and a descriptor set pointing to the input resource. +//! // This assumes we created a pipeline before and a sampler before, and that we bind the proper resources +//! // before recording the graph. +//! cmd = cmd.bind_graphics_pipeline("fullscreen_sample")? +//! .resolve_and_bind_sampled_image(0, 0, &input_resource, &sampler, &bindings)? +//! .draw(6, 1, 0, 0)?; //! Ok(cmd) //! }) //! .build(); //! ``` //! -//! Binding physical resources and recording is covered under the [`task_graph`] module documentation. +//! Binding physical resources and recording is covered under the [`graph`](crate::graph) module documentation. use ash::vk; use crate::{Allocator, Error, InFlightContext, PhysicalResourceBindings, VirtualResource}; diff --git a/src/graph/pass_graph.rs b/src/graph/pass_graph.rs index 73962a5..d0f12f4 100644 --- a/src/graph/pass_graph.rs +++ b/src/graph/pass_graph.rs @@ -6,7 +6,7 @@ use crate::domain::ExecutionDomain; use crate::graph::task_graph::{Barrier, Node, Resource, Task, TaskGraph}; use crate::graph::resource::{ResourceUsage}; use crate::graph::virtual_resource::VirtualResource; -use crate::{Allocator, Error, InFlightContext, PhysicalResourceBindings}; +use crate::{Allocator, DefaultAllocator, Error, InFlightContext, PhysicalResourceBindings}; use anyhow::Result; use petgraph::{Direction, Graph}; @@ -20,7 +20,7 @@ use crate::pipeline::PipelineStage; #[derive(Derivative, Default, Clone)] #[derivative(Debug)] pub struct PassResource { - pub usage: ResourceUsage, + pub(crate) usage: ResourceUsage, pub resource: VirtualResource, pub stage: PipelineStage, pub layout: vk::ImageLayout, @@ -41,7 +41,7 @@ pub struct PassResourceBarrier { /// A task in a pass graph. Either a render pass, or a compute pass, etc. -pub struct PassNode<'exec, 'q, R, D, A: Allocator> where R: Resource, D: ExecutionDomain { +pub struct PassNode<'exec, 'q, R, D, A: Allocator = DefaultAllocator> where R: Resource, D: ExecutionDomain { pub identifier: String, pub color: Option<[f32; 4]>, pub inputs: Vec, @@ -51,7 +51,7 @@ pub struct PassNode<'exec, 'q, R, D, A: Allocator> where R: Resource, D: Executi } /// Pass graph, used for synchronizing resources over a single queue. -pub struct PassGraph<'exec, 'q, D, A: Allocator> where D: ExecutionDomain { +pub struct PassGraph<'exec, 'q, D, A: Allocator = DefaultAllocator> where D: ExecutionDomain { pub(crate) graph: TaskGraph>, // Note that this is guaranteed to be stable. // This is because the only time indices are invalidated is when deleting a node, and even then only the last @@ -61,7 +61,7 @@ pub struct PassGraph<'exec, 'q, D, A: Allocator> where D: ExecutionDomain { last_usages: HashMap, } -pub struct BuiltPassGraph<'exec, 'q, D, A: Allocator> where D: ExecutionDomain { +pub struct BuiltPassGraph<'exec, 'q, D, A: Allocator = DefaultAllocator> where D: ExecutionDomain { graph: PassGraph<'exec, 'q, D, A>, } diff --git a/src/graph/physical_resource.rs b/src/graph/physical_resource.rs index a78a213..fba2f34 100644 --- a/src/graph/physical_resource.rs +++ b/src/graph/physical_resource.rs @@ -13,16 +13,14 @@ pub enum PhysicalResource { /// Stores bindings from virtual resources to physical resources. /// # Example usage /// ``` -/// use ash::vk; -/// use phobos::{Error, Image, VirtualResource}; -/// use phobos::graph::physical_resource::PhysicalResourceBindings; +/// use phobos::prelude::*; /// -/// let resource = VirtualResource::new(String::from("image")); +/// let resource = VirtualResource::new("image"); /// let image = Image::new(/*...*/); /// let view = image.view(vk::ImageAspectFlags::COLOR)?; /// let mut bindings = PhysicalResourceBindings::new(); /// // Bind the virtual resource to the image -/// bindings.bind_image(String::from("image"), view.clone()); +/// bindings.bind_image("image", &view); /// // ... Later, lookup the physical image handle from a virtual resource handle /// let view = bindings.resolve(&resource).ok_or(Error::NoResourceBound)?; /// ``` diff --git a/src/graph/resource.rs b/src/graph/resource.rs index 29d785f..239ba03 100644 --- a/src/graph/resource.rs +++ b/src/graph/resource.rs @@ -9,7 +9,7 @@ pub enum ResourceType { } #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub enum AttachmentType { +pub(crate) enum AttachmentType { #[default] Color, Depth, @@ -18,7 +18,8 @@ pub enum AttachmentType { /// Resource usage in a task graph. #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub enum ResourceUsage { +#[allow(dead_code)] +pub(crate) enum ResourceUsage { #[default] Nothing, Present, diff --git a/src/image.rs b/src/image.rs index 435be84..60d400d 100644 --- a/src/image.rs +++ b/src/image.rs @@ -10,7 +10,7 @@ //! Using [`Image::view`] you can create an [`ImageView`] that covers the entire image. Note that [`ImageView`] is in fact an //! `Arc`. The relationship between [`ImageView`] and [`ImgView`] is similar to `String` vs `str`, except that an //! [`ImgView`] also owns a full Vulkan resource. For this reason, we wrap it in a reference-counted `Arc` so we can safely treat it as if it were -//! a `str` to a `String`. Most API functions will ask for an `ImageView`. +//! a `str` to a `String`. Most API functions will ask for an [`ImageView`]. use std::sync::{Arc}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -210,6 +210,7 @@ impl ImgView { COUNTER.fetch_add(1, Ordering::Relaxed) } + /// Returns the subresource range of the original image that this image view covers. pub fn subresource_range(&self) -> vk::ImageSubresourceRange { vk::ImageSubresourceRange { aspect_mask: self.aspect, diff --git a/src/lib.rs b/src/lib.rs index 44b9e5f..0d22d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ //! ``` //!
//!
+//! //! # Example //! //! For illustrative purposes, we will use winit here. Any windowing library can be supported by implementing a few trait objects diff --git a/src/pipeline/builder.rs b/src/pipeline/builder.rs index 1283404..4c03cce 100644 --- a/src/pipeline/builder.rs +++ b/src/pipeline/builder.rs @@ -6,7 +6,8 @@ use crate::pipeline::create_info::*; use anyhow::Result; -/// Used to facilitate creating a graphics pipeline. +/// Used to facilitate creating a graphics pipeline. For an example, please check the +/// [`pipeline`](crate::pipeline) module level documentation. pub struct PipelineBuilder { inner: PipelineCreateInfo, vertex_binding_offsets: HashMap, @@ -128,6 +129,7 @@ impl PipelineBuilder { } } + /// Add a vertex input binding. These are the binding indices for `vkCmdBindVertexBuffers` pub fn vertex_input(mut self, binding: u32, rate: vk::VertexInputRate) -> Self { self.vertex_binding_offsets.insert(0, 0); self.inner.vertex_input_bindings.push(VertexInputBindingDescription{ @@ -139,6 +141,9 @@ impl PipelineBuilder { self } + /// Add a vertex attribute to the specified binding. + /// Doing this will automatically calculate offsets and sizes, so make sure to add these in order of declaration in + /// the shader. pub fn vertex_attribute(mut self, binding: u32, location: u32, format: vk::Format) -> Result { let offset = self.vertex_binding_offsets.get_mut(&binding).ok_or(Error::NoVertexBinding)?; self.inner.vertex_attributes.push(VertexInputAttributeDescription{ @@ -156,31 +161,37 @@ impl PipelineBuilder { Ok(self) } + /// Add a shader to the pipeline. pub fn attach_shader(mut self, info: ShaderCreateInfo) -> Self { self.inner.shaders.push(info); self } + /// Set depth testing mode. pub fn depth_test(mut self, enable: bool) -> Self { self.inner.depth_stencil.0.depth_test_enable = vk::Bool32::from(enable); self } + /// Set depth write mode. pub fn depth_write(mut self, enable: bool) -> Self { self.inner.depth_stencil.0.depth_write_enable = vk::Bool32::from(enable); self } + /// Set the depth compare operation. pub fn depth_op(mut self, op: vk::CompareOp) -> Self { self.inner.depth_stencil.0.depth_compare_op = op; self } + /// Toggle depth clamping. pub fn depth_clamp(mut self, enable: bool) -> Self { self.inner.rasterizer.0.depth_clamp_enable = vk::Bool32::from(enable); self } + /// Configure all depth state in one call. pub fn depth(self, test: bool, write: bool, clamp: bool, op: vk::CompareOp) -> Self { self.depth_test(test) .depth_write(write) @@ -188,6 +199,7 @@ impl PipelineBuilder { .depth_op(op) } + /// Add a dynamic state to the pipeline. pub fn dynamic_state(mut self, state: vk::DynamicState) -> Self { self.inner.dynamic_states.push(state); // When setting a viewport dynamic state, we still need a dummy viewport to make validation shut up @@ -215,6 +227,7 @@ impl PipelineBuilder { self } + /// Add dynamic states to the pipeline. pub fn dynamic_states(mut self, states: &[vk::DynamicState]) -> Self { for state in states { self = self.dynamic_state(*state); @@ -222,32 +235,38 @@ impl PipelineBuilder { self } + /// Set the polygon mode. pub fn polygon_mode(mut self, mode: vk::PolygonMode) -> Self { self.inner.rasterizer.0.polygon_mode = mode; self } + /// Set the face culling mask. pub fn cull_mask(mut self, cull: vk::CullModeFlags) -> Self { self.inner.rasterizer.0.cull_mode = cull; self } + /// Set the front face. pub fn front_face(mut self, face: vk::FrontFace) -> Self { self.inner.rasterizer.0.front_face = face; self } + /// Set the amount of MSAA samples. pub fn samples(mut self, samples: vk::SampleCountFlags) -> Self { self.inner.multisample.0.rasterization_samples = samples; self } + /// Enable sample shading and set the sample shading rate. pub fn sample_shading(mut self, value: f32) -> Self { self.inner.multisample.0.sample_shading_enable = vk::TRUE; self.inner.multisample.0.min_sample_shading = value; self } + /// Add a blend attachment, but with no blending enabled. pub fn blend_attachment_none(mut self) -> Self { self.inner.blend_attachments.push(PipelineColorBlendAttachmentState{ 0: vk::PipelineColorBlendAttachmentState { @@ -263,6 +282,7 @@ impl PipelineBuilder { self } + /// Add an additive blend attachment, writing to each color component. pub fn blend_additive_unmasked(mut self, src: vk::BlendFactor, dst: vk::BlendFactor, src_alpha: vk::BlendFactor, dst_alpha: vk::BlendFactor) -> Self { self.inner.blend_attachments.push(PipelineColorBlendAttachmentState{ 0: vk::PipelineColorBlendAttachmentState { @@ -283,6 +303,7 @@ impl PipelineBuilder { self.inner } + /// Obtain the pipeline name. pub fn get_name(&self) -> &str { &self.inner.name } diff --git a/src/pipeline/cache.rs b/src/pipeline/cache.rs index cf7bfd7..c032d05 100644 --- a/src/pipeline/cache.rs +++ b/src/pipeline/cache.rs @@ -25,12 +25,12 @@ struct PipelineEntry

where P: std::fmt::Debug { /// [`PipelineCache::create_named_pipeline`]. /// # Example usage /// ``` -/// use phobos::{PipelineBuilder, PipelineCache}; -/// let cache = PipelineCache::new(device.clone()); -/// let pci = PipelineBuilder::new(String::from("my_pipeline")) +/// use phobos::prelude::*; +/// let cache = PipelineCache::new(device.clone())?; +/// let pci = PipelineBuilder::new("my_pipeline") /// // ... options for pipeline creation /// .build(); -/// cache.or_else(|_| Err(anyhow::Error::from(Error::PoisonError)))?.create_named_pipeline(pci); +/// cache.lock().or_else(|_| Err(anyhow::Error::from(Error::PoisonError)))?.create_named_pipeline(pci)?; /// ``` #[derive(Debug)] pub struct PipelineCache { @@ -129,11 +129,17 @@ impl PipelineCache { Ok(()) } + /// Get reflection info for a previously registered pipeline. + /// # Errors + /// Fails if the pipeline was not found in the cache. #[cfg(feature="shader-reflection")] pub fn reflection_info(&self, name: &str) -> Result<&ReflectionInfo> { Ok(&self.named_pipelines.get(name).unwrap().reflection) } + /// Get the pipeline create info associated with a pipeline + /// # Errors + /// Fails if the pipeline was not found in the cache. pub fn pipeline_info(&self, name: &str) -> Option<&PipelineCreateInfo> { self.named_pipelines.get(name).map(|entry| &entry.info) } diff --git a/src/pipeline/create_info.rs b/src/pipeline/create_info.rs index 2c66181..0eb706b 100644 --- a/src/pipeline/create_info.rs +++ b/src/pipeline/create_info.rs @@ -39,7 +39,7 @@ pub struct Rect2D(pub(super) vk::Rect2D); /// Defines a full graphics pipeline. You can modify this manually, but all /// information is also exposed through the pipeline builder, -/// with additional quality of life and presets. +/// with additional quality of life and presets, so that method is recommended. #[derive(Debug, Clone, Derivative)] #[derivative(PartialEq, Eq, Hash)] pub struct PipelineCreateInfo { @@ -93,7 +93,7 @@ pub struct PipelineCreateInfo { impl PipelineCreateInfo { - pub fn build_rendering_state(&mut self) -> () { + pub(crate) fn build_rendering_state(&mut self) -> () { self.vk_rendering_state = vk::PipelineRenderingCreateInfo::builder() .view_mask(self.rendering_info.view_mask) .color_attachment_formats(self.rendering_info.color_formats.as_slice()) @@ -102,7 +102,7 @@ impl PipelineCreateInfo { .build(); } - pub fn build_inner(&mut self) -> () { + pub(crate) fn build_inner(&mut self) -> () { self.vk_attributes = self.vertex_attributes.iter().map(|v| v.0.clone()).collect(); self.vk_vertex_inputs = self.vertex_input_bindings.iter().map(|v| v.0.clone()).collect(); self.vertex_input_state = vk::PipelineVertexInputStateCreateInfo::builder() diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index b1265fb..4286c1c 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -1,15 +1,14 @@ -//! The pipeline module mainly exposes the [`PipelineCache`] struct. This is a helper that manages creating +//! The pipeline module mainly exposes the [`PipelineCache`](crate::PipelineCache) struct. This is a helper that manages creating //! pipelines, obtaining reflection information from them (if the `shader-reflection` feature is enabled). //! You probably only want one of these in the entire application. Since it's used everywhere, to ensure safe access -//! is possible, [`PipelineCache::new()`] returns itself wrapped in an `Arc>`. +//! is possible, [`PipelineCache::new()`](crate::PipelineCache::new) returns itself wrapped in an `Arc>`. //! //! # Example -//! The following example uses the [`PipelineBuilder`] utility to make a graphics pipeline and add it to the pipeline cache. +//! The following example uses the [`PipelineBuilder`](crate::PipelineBuilder) utility to make a graphics pipeline and add it to the pipeline cache. //! //! ``` //! use std::path::Path; -//! use ash::vk; -//! use phobos as ph; +//! use phobos::prelude::*; //! //! let mut cache = ph::PipelineCache::new(device.clone())?; //! @@ -20,11 +19,11 @@ //! //! // Create shaders, these can safely be discarded after building the pipeline //! // as they are just create info structs that are also stored internally -//! let vertex = ph::ShaderCreateInfo::from_spirv(vk::ShaderStageFlags::VERTEX, vtx_code); -//! let fragment = ph::ShaderCreateInfo::from_spirv(vk::ShaderStageFlags::FRAGMENT, frag_code); +//! let vertex = ShaderCreateInfo::from_spirv(vk::ShaderStageFlags::VERTEX, vtx_code); +//! let fragment = ShaderCreateInfo::from_spirv(vk::ShaderStageFlags::FRAGMENT, frag_code); //! //! // Now we can build the actual pipeline. -//! let pci = ph::PipelineBuilder::new("sample".to_string()) +//! let pci = PipelineBuilder::new("sample") //! // One vertex binding at binding 0. We have to specify this before adding attributes //! .vertex_input(0, vk::VertexInputRate::VERTEX) //! // Equivalent of `layout (location = 0) in vec2 Attr1;` @@ -37,7 +36,9 @@ //! .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]) //! // We don't want any blending, but we still need to specify what happens to our color output. //! .blend_attachment_none() +//! // Disable face culling //! .cull_mask(vk::CullModeFlags::NONE) +//! // Add our shaders //! .attach_shader(vertex) //! .attach_shader(fragment) //! .build(); @@ -50,7 +51,7 @@ //! ``` //! # Correct usage //! The pipeline cache internally frees up resources by destroying pipelines that have not been accessed in a long time. -//! To ensure this happens periodically, call [`PipelineCache::next_frame()`] at the end of each iteration of your render loop. +//! To ensure this happens periodically, call [`PipelineCache::next_frame()`](crate::PipelineCache::next_frame) at the end of each iteration of your render loop. use std::sync::Arc; use ash::vk; diff --git a/src/pipeline/pipeline_layout.rs b/src/pipeline/pipeline_layout.rs index bee9f60..ce2ddcc 100644 --- a/src/pipeline/pipeline_layout.rs +++ b/src/pipeline/pipeline_layout.rs @@ -8,7 +8,7 @@ use crate::pipeline::set_layout::{DescriptorSetLayout, DescriptorSetLayoutCreate use crate::util::cache::{Cache, Resource}; /// A fully built Vulkan pipeline layout. This is a managed resource, so it cannot be manually -/// cloned or dropped. +/// created or dropped. #[derive(Derivative)] #[derivative(Debug)] pub struct PipelineLayout { diff --git a/src/pipeline/set_layout.rs b/src/pipeline/set_layout.rs index 7c0f06f..15302ae 100644 --- a/src/pipeline/set_layout.rs +++ b/src/pipeline/set_layout.rs @@ -7,7 +7,7 @@ use anyhow::Result; use crate::util::cache::Resource; /// A fully built Vulkan descriptor set layout. This is a managed resource, so it cannot be manually -/// cloned or dropped. +/// created or dropped. #[derive(Derivative)] #[derivative(Debug)] pub struct DescriptorSetLayout { diff --git a/src/pipeline/shader.rs b/src/pipeline/shader.rs index 776b30e..73b0365 100644 --- a/src/pipeline/shader.rs +++ b/src/pipeline/shader.rs @@ -17,6 +17,8 @@ pub struct Shader { pub(crate) handle: vk::ShaderModule } +/// Info required to create a shader. Filling out the hash properly is necessary, but can easily be done automatically +/// by using the [`ShaderCreateInfo::from_spirv`] method. #[derive(Debug, Clone)] pub struct ShaderCreateInfo { pub stage: vk::ShaderStageFlags, diff --git a/src/pipeline/shader_reflection.rs b/src/pipeline/shader_reflection.rs index d9268d7..6e33e6f 100644 --- a/src/pipeline/shader_reflection.rs +++ b/src/pipeline/shader_reflection.rs @@ -178,7 +178,7 @@ pub(crate) fn reflect_shaders(info: &PipelineCreateInfo) -> Result PipelineLayoutCreateInfo { +pub(crate) fn build_pipeline_layout(info: &ReflectionInfo) -> PipelineLayoutCreateInfo { let mut layout = PipelineLayoutCreateInfo { flags: Default::default(), set_layouts: vec![], diff --git a/src/sync/execution_manager.rs b/src/sync/execution_manager.rs index edc4a88..be9b263 100644 --- a/src/sync/execution_manager.rs +++ b/src/sync/execution_manager.rs @@ -11,24 +11,24 @@ use crate::sync::submit_batch::SubmitBatch; /// The execution manager is responsible for allocating command buffers on correct /// queues. To obtain any command buffer, you must allocate it by calling /// [`ExecutionManager::on_domain()`]. An execution domain is a type that implements -/// the [`domain::ExecutionDomain`] trait. Four domains are already defined, and these should cover +/// the [`domain::ExecutionDomain`](crate::domain::ExecutionDomain) trait. Four domains are already defined, and these should cover /// virtually every available use case. /// -/// - [`domain::All`] supports all operations and is essentially a combination of the other three domains. -/// - [`domain::Graphics`] supports only graphics operations. -/// - [`domain::Transfer`] supports only transfer operations. -/// - [`domain::Compute`] supports only compute operations. +/// - [`domain::All`](crate::domain::All) supports all operations and is essentially a combination of the other three domains. +/// - [`domain::Graphics`](crate::domain::Graphics) supports only graphics operations. +/// - [`domain::Transfer`](crate::domain::Transfer) supports only transfer operations. +/// - [`domain::Compute`](crate::domain::Compute) supports only compute operations. /// /// Note that all domains also implement a couple commands that apply to all domains with no /// restrictions on queue type support, such as pipeline barriers. /// /// # Example /// ``` -/// use phobos::{domain, ExecutionManager}; +/// use phobos::prelude::*; /// // Create an execution manager first. You only want one of these. /// let exec = ExecutionManager::new(device.clone(), &physical_device); /// // Obtain a command buffer on the Transfer domain -/// let cmd = exec.on_domain::()? +/// let cmd = exec.on_domain::(None, None)? /// .copy_image(/*command parameters*/) /// .finish(); /// // Submit the command buffer, either to this frame's command list, @@ -69,6 +69,7 @@ impl ExecutionManager { } /// Tries to obtain a command buffer over a domain, or returns an Err state if the lock is currently being held. + /// If this command buffer needs access to pipelines or descriptor sets, pass in the relevant caches. pub fn try_on_domain<'q, D: ExecutionDomain>(&'q self, pipelines: Option>>, descriptors: Option>>) -> Result> { @@ -77,6 +78,7 @@ impl ExecutionManager { } /// Obtain a command buffer capable of operating on the specified domain. + /// If this command buffer needs access to pipelines or descriptor sets, pass in the relevant caches. pub fn on_domain<'q, D: ExecutionDomain>(&'q self, pipelines: Option>>, descriptors: Option>>) -> Result> { @@ -85,6 +87,21 @@ impl ExecutionManager { } /// Begin a submit batch. Note that all submits in a batch are over a single domain (currently). + /// # Example + /// ``` + /// use phobos::prelude::*; + /// let exec = ExecutionManager::new(device.clone(), &physical_device)?; + /// async { + /// let cmd1 = exec.on_domain::(None, None)?.finish()?; + /// let cmd2 = exec.on_domain::(None, None)?.finish()?; + /// let mut batch = exec.start_submit_batch()?; + /// // Submit the first command buffer first + /// batch.submit(cmd1)? + /// // The second command buffer waits at COLOR_ATTACHMENT_OUTPUT on the first command buffer's completion. + /// .then(PipelineStage::COLOR_ATTACHMENT_OUTPUT, cmd2, &mut batch)?; + /// batch.finish()?.await; + /// } + /// ``` pub fn start_submit_batch(&self) -> Result> { SubmitBatch::new(self.device.clone(), self.clone()) } @@ -125,6 +142,8 @@ impl ExecutionManager { self.queues.iter().find(|&queue| queue.lock().unwrap().info.can_present.clone()).map(|q| q.lock().unwrap()) } + /// Try to get a reference to a queue matching the domain, or return an error state if this would need to block + /// to lock the queue. pub fn try_get_queue(&self) -> TryLockResult> { let q = self.queues.iter().find(|&q| { let q = q.try_lock(); @@ -139,7 +158,7 @@ impl ExecutionManager { } } - /// Obtain a reference to a queue matching predicate. + /// Obtain a reference to a queue matching the domain. Blocks if this queue is currently locked. pub fn get_queue(&self) -> Option> { self.queues.iter().find(|&q| { let q = q.lock().unwrap(); diff --git a/src/sync/fence.rs b/src/sync/fence.rs index cf4bbc1..752bd48 100644 --- a/src/sync/fence.rs +++ b/src/sync/fence.rs @@ -19,6 +19,83 @@ trait FenceValue { } /// Wrapper around a [`VkFence`](vk::Fence) object. Fences are used for CPU-GPU sync. +/// The most powerful feature of fences is that they have [`Future`](std::future::Future) +/// implemented for them. This allows you to wait for GPU work using `.await` like any normal +/// Rust future. +/// # Example +/// ``` +/// use phobos::prelude::*; +/// +/// let exec = ExecutionManager::new(device, &physical_device)?; +/// // Obtain some command buffer +/// let cmd = exec.on_domain::(None, None)?.finish()?; +/// let fence = exec.submit(cmd)?; +/// // We can now await this fence, or attach a resulting value to it to make the future +/// // a little more useful +/// async { +/// fence.attach_value(5) // This would usually be some kind of GPU resource, like an image that was just written to +/// .await?; +/// } +/// ``` +/// # Caveats +/// Since returning a fence and awaiting it later would make objects +/// local to the function go out of scope and drop them, this is a problem when you consider the fact +/// that the GPU might still be using those resources. +/// Consider the following case +/// ``` +/// use std::mem::size_of; +/// use std::sync::Arc; +/// use anyhow::Result; +/// +/// use phobos::prelude::*; +/// +/// async fn upload_buffer(device: Arc, mut allocator: DefaultAllocator, exec: ExecutionManager, src: &[T]) -> Result { +/// // Create our result buffer +/// let size = (src.len() * size_of::()) as u64; +/// let buffer = Buffer::new_device_local(device.clone(), &allocator, size, vk::BufferUsageFlags::TRANSFER_DST)?; +/// let view = buffer.view_full(); +/// // Create a staging buffer and copy our data to it +/// let staging = Buffer::new(device.clone(), &allocator, size, vk::BufferUsageFlags::TRANSFER_SRC, MemoryType::CpuToGpu)?; +/// let mut staging_view = staging.view_full(); +/// staging_view.mapped_slice()?.copy_from_slice(src); +/// // Create a command buffer to copy the buffers +/// let cmd = +/// exec.on_domain::(None, None)? +/// .copy_buffer(&staging_view, &view)? +/// .finish()?; +/// // Submit our command buffer and obtain a fence +/// let fence = exec.submit(cmd)?; +/// // Attach our resulting buffer and await the fence. +/// fence.attach_value(Ok(buffer)).await +/// } +/// ``` +/// This has a major problem in that the staging buffer is dropped when the future is returned, +/// but the fence is still not done so the gpu is still accessing it. To fix this, we can use +/// [`Fence::with_cleanup`] as follows: +/// ``` +/// use std::mem::size_of; +/// use std::sync::Arc; +/// use anyhow::Result; +/// +/// use phobos::prelude::*; +/// +/// +/// async fn upload_buffer(device: Arc, mut allocator: DefaultAllocator, exec: ExecutionManager, src: &[T]) -> Result { +/// // ... snip +/// // Submit our command buffer and obtain a fence +/// let fence = exec.submit(cmd)?; +/// // Attach our resulting buffer and await the fence. +/// fence +/// // Add a cleanup function which will take ownership of any data that needs to be freed +/// // after the fence completes. +/// // The future will call these functions when the fence is ready. +/// .with_cleanup(move || { +/// drop(staging); +/// }) +/// .attach_value(Ok(buffer)).await +/// } +/// ``` +/// #[derive(Derivative)] #[derivative(Debug)] pub struct Fence { @@ -42,6 +119,8 @@ impl FenceValue<()> for Fence<()> { } impl Fence<()> { + /// Attach a value to the fence that is returned from the future + /// when it completes. pub fn attach_value(mut self, value: T) -> Fence { let mut handle = vk::Fence::null(); std::mem::swap(&mut self.handle, &mut handle); diff --git a/src/sync/mod.rs b/src/sync/mod.rs index d705dde..09d2725 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -1,3 +1,16 @@ +//! The sync module provides utilities dealing with Vulkan synchronization outside the scope of the +//! pass graph. +//! +//! - The [`fence`] module provides a wrapper around `VkFence` objects, used for CPU-GPU sync, +//! as well as an implementation for [`Future`](std::future::Future) for them. +//! - The [`semaphore`] module provides a simple wrapper around `VkSemaphore` objects, used for GPU-GPU sync. +//! - The [`execution_manager`] module abstracts away vulkan queues and synchronizes access to them by using the +//! [`domain`](crate::domain) system. Most of the time, submissions should go through here. +//! - [`thread_context`] provides an in-flight thread context that can act as a replacement [`InFlightContext`](crate::InFlightContext) +//! when one is expected. +//! - [`submit_batch`] provides a utility to chain [`Semaphore`](crate::Semaphore)s together and submit them all +//! as one batch. + pub mod fence; pub mod semaphore; pub mod execution_manager; diff --git a/src/sync/submit_batch.rs b/src/sync/submit_batch.rs index a93a9f1..1b9b3c2 100644 --- a/src/sync/submit_batch.rs +++ b/src/sync/submit_batch.rs @@ -16,11 +16,16 @@ struct SubmitInfo { wait_stages: Vec, } +/// A handle to a submit inside a batch. +/// Can be used to make submits wait on other submits inside a single batch #[derive(Debug, Copy, Clone)] pub struct SubmitHandle { index: usize, } +/// A batch of submits containing multiple command buffers that possibly +/// wait on each other using semaphores. An example usage is given in the documentation for +/// [`ExecutionManager::start_submit_batch`]. #[derive(Debug)] pub struct SubmitBatch { device: Arc, @@ -63,6 +68,7 @@ impl SubmitBatch { }) } + /// Submit a new command buffer in this batch with no dependencies. pub fn submit(&mut self, cmd: CommandBuffer) -> Result { self.submits.push(SubmitInfo { cmd, @@ -76,6 +82,8 @@ impl SubmitBatch { }) } + /// Finish this batch by submitting it to the execution manager. + /// This returns a [`Fence`] that can be awaited to wait for completion. pub fn finish(self) -> Result { struct PerSubmit { wait_semaphores: Vec, @@ -154,6 +162,7 @@ impl SubmitBatch { } impl SubmitHandle { + /// Add another submit to the batch that waits on this submit at the specified wait stage mask. pub fn then( &self, wait_stage: PipelineStage, diff --git a/src/sync/thread_context.rs b/src/sync/thread_context.rs index 004d10d..8c850f7 100644 --- a/src/sync/thread_context.rs +++ b/src/sync/thread_context.rs @@ -3,6 +3,8 @@ use ash::vk; use crate::{Allocator, DefaultAllocator, Device, InFlightContext, ScratchAllocator}; use anyhow::Result; +/// Thread context with linear allocators that can be used as a substitute +/// [`InFlightContext`] outside of a frame. pub struct ThreadContext { vbo_allocator: ScratchAllocator, ibo_allocator: ScratchAllocator, diff --git a/src/util/byte_size.rs b/src/util/byte_size.rs index 2acb249..b17adff 100644 --- a/src/util/byte_size.rs +++ b/src/util/byte_size.rs @@ -1,11 +1,14 @@ use std::mem::size_of; use ash::vk; +/// Simple trait to get the size of one element in bytes of a `vk::Format`. pub trait ByteSize { + /// Returns the size, in bytes, of one element of this thing. fn byte_size(&self) -> usize; } impl ByteSize for vk::Format { + /// If an image is created with this format, then the return value of this function is the size in bytes of one pixel. fn byte_size(&self) -> usize { match *self { vk::Format::R32G32_SFLOAT => 2 * size_of::(), diff --git a/src/util/deferred_delete.rs b/src/util/deferred_delete.rs index 6089b9d..f089e42 100644 --- a/src/util/deferred_delete.rs +++ b/src/util/deferred_delete.rs @@ -5,6 +5,7 @@ struct Item { ttl: u32, } +/// Deletion queue that stores resources until they are ready to be deleted. #[derive(Debug)] pub struct DeletionQueue { max_ttl: u32, diff --git a/src/wsi/frame.rs b/src/wsi/frame.rs index 1d37f36..0613e36 100644 --- a/src/wsi/frame.rs +++ b/src/wsi/frame.rs @@ -7,12 +7,14 @@ //! //! Example code for a main loop using `winit` and `futures::block_on` as the future executor. //! ``` -//! use phobos as ph; -//! use ash::vk; +//! use winit::event_loop::ControlFlow; +//! use winit::event::{Event, WindowEvent}; +//! use phobos::prelude::*; //! +//! let alloc = DefaultAllocator::new(&instance, &device, &physical_device)?; //! let mut frame = { -//! let swapchain = ph::Swapchain::new(&instance, device.clone(), &settings, &surface)?; -//! ph::FrameManager::new(device.clone(), alloc.clone(), &settings, swapchain)? +//! let swapchain = Swapchain::new(&instance, device.clone(), &settings, &surface)?; +//! FrameManager::new(device.clone(), alloc.clone(), &settings, swapchain)? //! }; //! //! event_loop.run(move |event, _, control_flow| { @@ -21,19 +23,6 @@ //! if let ControlFlow::ExitWithCode(_) = *control_flow { return; } //! *control_flow = ControlFlow::Poll; //! -//! futures::executor::block_on(frame.new_frame(exec.clone(), window, &surface, |mut ifc| { -//! // This closure is expected to return a command buffer with this frame's commands. -//! // This command buffer should at the very least transition the swapchain image to -//! // `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`. -//! // This can be done using the render graph API, or with a single command: -//! let cmd = exec.on_domain::()? -//! .transition_image(&ifc.swapchain_image, -//! vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, -//! vk::ImageLayout::UNDEFINED, vk::ImageLayout::PRESENT_SRC_KHR, -//! vk::AccessFlags::empty(), vk::AccessFlags::empty()) -//! .finish(); -//! Ok(cmd) -//! }))?; //! //! // Advance caches to next frame to ensure resources are freed up where possible. //! pipeline_cache.lock().unwrap().next_frame(); @@ -50,6 +39,25 @@ //! *control_flow = ControlFlow::Exit; //! device.wait_idle().unwrap(); //! }, +//! Event::MainEventsCleared => { +//! window.request_redraw(); +//! }, +//! Event::RedrawRequested(_) => { +//! // When a redraw is requested, we'll run our frame logic +//! futures::executor::block_on(frame.new_frame(exec.clone(), window, &surface, |mut ifc| { +//! // This closure is expected to return a command buffer with this frame's commands. +//! // This command buffer should at the very least transition the swapchain image to +//! // `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`. +//! // This can be done using the render graph API, or with a single command: +//! let cmd = exec.on_domain::()? +//! .transition_image(&ifc.swapchain_image, +//! vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, +//! vk::ImageLayout::UNDEFINED, vk::ImageLayout::PRESENT_SRC_KHR, +//! vk::AccessFlags::empty(), vk::AccessFlags::empty()) +//! .finish()?; +//! Ok(cmd) +//! }))?; +//! } //! _ => (), //! } //! }); @@ -107,7 +115,7 @@ struct PerImage { /// /// ``` /// -/// Another way to acquire an instance of this struct is through a [`ThreadContext`]. +/// Another way to acquire an instance of this struct is through a [`ThreadContext`](crate::ThreadContext). #[derive(Debug)] pub struct InFlightContext<'f, A: Allocator = DefaultAllocator> { pub swapchain_image: Option, @@ -250,6 +258,7 @@ impl FrameManager { Ok(new_swapchain) } + /// Obtain a new frame context to run commands in. pub async fn new_frame(&mut self, exec: ExecutionManager, window: &Window, surface: &Surface, f: F) -> Result<()> where @@ -459,24 +468,26 @@ impl FrameManager { Ok(self.swapchain.images[self.current_image as usize].view.clone()) } + /// Unsafe access to the underlying swapchain. pub unsafe fn get_swapchain(&self) -> &Swapchain { &self.swapchain } } impl<'f, A: Allocator> InFlightContext<'f, A> { + /// Allocate a scratch vertex buffer, which is only valid for the duration of this frame. pub fn allocate_scratch_vbo(&mut self, size: vk::DeviceSize) -> Result { self.vertex_allocator.allocate(size) } - + /// Allocate a scratch index buffer, which is only valid for the duration of this frame. pub fn allocate_scratch_ibo(&mut self, size: vk::DeviceSize) -> Result { self.index_allocator.allocate(size) } - + /// Allocate a scratch uniform buffer, which is only valid for the duration of this frame. pub fn allocate_scratch_ubo(&mut self, size: vk::DeviceSize) -> Result { self.uniform_allocator.allocate(size) } - + /// Allocate a scratch shader storage buffer, which is only valid for the duration of this frame. pub fn allocate_scratch_ssbo(&mut self, size: vk::DeviceSize) -> Result { self.storage_allocator.allocate(size) } diff --git a/src/wsi/mod.rs b/src/wsi/mod.rs index 181cfd1..03fd38b 100644 --- a/src/wsi/mod.rs +++ b/src/wsi/mod.rs @@ -1,3 +1,6 @@ +//! The wsi module provides utilities for interacting with the window and rendering frames. +//! If you are using a headless context, you can largely ignore this module. + pub mod frame; pub mod surface; pub mod swapchain; diff --git a/src/wsi/surface.rs b/src/wsi/surface.rs index dc9d6ba..5eeced3 100644 --- a/src/wsi/surface.rs +++ b/src/wsi/surface.rs @@ -20,6 +20,7 @@ pub struct Surface { } impl Surface { + /// Create a new surface. pub fn new(instance: &VkInstance, settings: &AppSettings) -> Result { if let Some(window) = settings.window { let functions = ash::extensions::khr::Surface::new(&instance.entry, &instance.instance); diff --git a/src/wsi/swapchain.rs b/src/wsi/swapchain.rs index 1b32e06..53eb80c 100644 --- a/src/wsi/swapchain.rs +++ b/src/wsi/swapchain.rs @@ -32,6 +32,7 @@ pub struct Swapchain { } impl Swapchain { + /// Create a new swapchain. pub fn new(instance: &VkInstance, device: Arc, settings: &AppSettings, surface: &Surface) -> Result { let format = choose_surface_format(settings, surface)?; let present_mode = choose_present_mode(settings, surface); @@ -100,10 +101,12 @@ impl Swapchain { }) } + /// Unsafe access to the swapchain extension functions. pub unsafe fn loader(&self) -> ash::extensions::khr::Swapchain { self.functions.clone() } + /// Unsafe access to the underlying vulkan handle. pub unsafe fn handle(&self) -> vk::SwapchainKHR { self.handle }