diff options
Diffstat (limited to 'src/command_buffer/state_cacher.rs')
-rw-r--r-- | src/command_buffer/state_cacher.rs | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/src/command_buffer/state_cacher.rs b/src/command_buffer/state_cacher.rs new file mode 100644 index 0000000..63bfc3d --- /dev/null +++ b/src/command_buffer/state_cacher.rs @@ -0,0 +1,484 @@ +// Copyright (c) 2017 The vulkano developers +// Licensed under the Apache License, Version 2.0 +// <LICENSE-APACHE or +// https://www.apache.org/licenses/LICENSE-2.0> or the MIT +// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +use crate::buffer::BufferAccess; +use crate::command_buffer::DynamicState; +use crate::descriptor_set::DescriptorSetWithOffsets; +use crate::pipeline::input_assembly::IndexType; +use crate::pipeline::ComputePipelineAbstract; +use crate::pipeline::GraphicsPipelineAbstract; +use crate::pipeline::PipelineBindPoint; +use crate::DeviceSize; +use crate::VulkanObject; +use smallvec::SmallVec; +use std::ops::Range; + +/// Keep track of the state of a command buffer builder, so that you don't need to bind objects +/// that were already bound. +/// +/// > **Important**: Executing a secondary command buffer invalidates the state of a command buffer +/// > builder. When you do so, you need to call `invalidate()`. +pub struct StateCacher { + // The dynamic state to synchronize with `CmdSetState`. + dynamic_state: DynamicState, + // The compute pipeline currently bound. 0 if nothing bound. + compute_pipeline: ash::vk::Pipeline, + // The graphics pipeline currently bound. 0 if nothing bound. + graphics_pipeline: ash::vk::Pipeline, + // The descriptor sets for the compute pipeline. + compute_descriptor_sets: SmallVec<[(ash::vk::DescriptorSet, SmallVec<[u32; 32]>); 12]>, + // The descriptor sets for the graphics pipeline. + graphics_descriptor_sets: SmallVec<[(ash::vk::DescriptorSet, SmallVec<[u32; 32]>); 12]>, + // If the user starts comparing descriptor sets, but drops the helper struct in the middle of + // the processing then we will end up in a weird state. This bool is true when we start + // comparing sets, and is set to false when we end up comparing. If it was true when we start + // comparing, we know that something bad happened and we flush the cache. + poisoned_descriptor_sets: bool, + // The vertex buffers currently bound. + vertex_buffers: SmallVec<[(ash::vk::Buffer, DeviceSize); 12]>, + // Same as `poisoned_descriptor_sets` but for vertex buffers. + poisoned_vertex_buffers: bool, + // The index buffer, offset, and index type currently bound. `None` if nothing bound. + index_buffer: Option<(ash::vk::Buffer, DeviceSize, IndexType)>, +} + +/// Outcome of an operation. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum StateCacherOutcome { + /// The caller needs to perform the state change in the actual command buffer builder. + NeedChange, + /// The state change is not necessary. + AlreadyOk, +} + +impl StateCacher { + /// Builds a new `StateCacher`. + #[inline] + pub fn new() -> StateCacher { + StateCacher { + dynamic_state: DynamicState::none(), + compute_pipeline: ash::vk::Pipeline::null(), + graphics_pipeline: ash::vk::Pipeline::null(), + compute_descriptor_sets: SmallVec::new(), + graphics_descriptor_sets: SmallVec::new(), + poisoned_descriptor_sets: false, + vertex_buffers: SmallVec::new(), + poisoned_vertex_buffers: false, + index_buffer: None, + } + } + + /// Resets the cache to its default state. You **must** call this after executing a secondary + /// command buffer. + #[inline] + pub fn invalidate(&mut self) { + self.dynamic_state = DynamicState::none(); + self.compute_pipeline = ash::vk::Pipeline::null(); + self.graphics_pipeline = ash::vk::Pipeline::null(); + self.compute_descriptor_sets = SmallVec::new(); + self.graphics_descriptor_sets = SmallVec::new(); + self.vertex_buffers = SmallVec::new(); + self.index_buffer = None; + } + + /// Compares the current state with `incoming`, and returns a new state that contains the + /// states that differ and that need to be actually set in the command buffer builder. + /// + /// This function also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after this function returns. + pub fn dynamic_state(&mut self, incoming: &DynamicState) -> DynamicState { + let mut changed = DynamicState::none(); + + macro_rules! cmp { + ($field:ident) => { + if self.dynamic_state.$field != incoming.$field { + changed.$field = incoming.$field.clone(); + if incoming.$field.is_some() { + self.dynamic_state.$field = incoming.$field.clone(); + } + } + }; + } + + cmp!(line_width); + cmp!(viewports); + cmp!(scissors); + cmp!(compare_mask); + cmp!(reference); + cmp!(write_mask); + + changed + } + + /// Starts the process of comparing a list of descriptor sets to the descriptor sets currently + /// in cache. + /// + /// After calling this function, call `add` for each set one by one. Then call `compare` in + /// order to get the index of the first set to bind, or `None` if the sets were identical to + /// what is in cache. + /// + /// This process also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after the `compare` function returns. + #[inline] + pub fn bind_descriptor_sets( + &mut self, + pipeline_bind_point: PipelineBindPoint, + ) -> StateCacherDescriptorSets { + if self.poisoned_descriptor_sets { + self.compute_descriptor_sets = SmallVec::new(); + self.graphics_descriptor_sets = SmallVec::new(); + } + + self.poisoned_descriptor_sets = true; + + StateCacherDescriptorSets { + poisoned: &mut self.poisoned_descriptor_sets, + state: match pipeline_bind_point { + PipelineBindPoint::Compute => &mut self.compute_descriptor_sets, + PipelineBindPoint::Graphics => &mut self.graphics_descriptor_sets, + }, + offset: 0, + found_diff: None, + } + } + + /// Checks whether we need to bind a graphics pipeline. Returns `StateCacherOutcome::AlreadyOk` + /// if the pipeline was already bound earlier, and `StateCacherOutcome::NeedChange` if you need + /// to actually bind the pipeline. + /// + /// This function also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after this function returns. + pub fn bind_graphics_pipeline<P>(&mut self, pipeline: &P) -> StateCacherOutcome + where + P: GraphicsPipelineAbstract, + { + let inner = GraphicsPipelineAbstract::inner(pipeline).internal_object(); + if inner == self.graphics_pipeline { + StateCacherOutcome::AlreadyOk + } else { + self.graphics_pipeline = inner; + StateCacherOutcome::NeedChange + } + } + + /// Checks whether we need to bind a compute pipeline. Returns `StateCacherOutcome::AlreadyOk` + /// if the pipeline was already bound earlier, and `StateCacherOutcome::NeedChange` if you need + /// to actually bind the pipeline. + /// + /// This function also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after this function returns. + pub fn bind_compute_pipeline<P>(&mut self, pipeline: &P) -> StateCacherOutcome + where + P: ComputePipelineAbstract, + { + let inner = pipeline.inner().internal_object(); + if inner == self.compute_pipeline { + StateCacherOutcome::AlreadyOk + } else { + self.compute_pipeline = inner; + StateCacherOutcome::NeedChange + } + } + + /// Starts the process of comparing a list of vertex buffers to the vertex buffers currently + /// in cache. + /// + /// After calling this function, call `add` for each set one by one. Then call `compare` in + /// order to get the range of the vertex buffers to bind, or `None` if the sets were identical + /// to what is in cache. + /// + /// This process also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after the `compare` function returns. + #[inline] + pub fn bind_vertex_buffers(&mut self) -> StateCacherVertexBuffers { + if self.poisoned_vertex_buffers { + self.vertex_buffers = SmallVec::new(); + } + + self.poisoned_vertex_buffers = true; + + StateCacherVertexBuffers { + poisoned: &mut self.poisoned_vertex_buffers, + state: &mut self.vertex_buffers, + offset: 0, + first_diff: None, + last_diff: 0, + } + } + + /// Checks whether we need to bind an index buffer. Returns `StateCacherOutcome::AlreadyOk` + /// if the index buffer was already bound earlier, and `StateCacherOutcome::NeedChange` if you + /// need to actually bind the buffer. + /// + /// This function also updates the state cacher. The state cacher assumes that the state + /// changes are going to be performed after this function returns. + pub fn bind_index_buffer<B>(&mut self, index_buffer: &B, ty: IndexType) -> StateCacherOutcome + where + B: ?Sized + BufferAccess, + { + let value = { + let inner = index_buffer.inner(); + (inner.buffer.internal_object(), inner.offset, ty) + }; + + if self.index_buffer == Some(value) { + StateCacherOutcome::AlreadyOk + } else { + self.index_buffer = Some(value); + StateCacherOutcome::NeedChange + } + } +} + +/// Helper struct for comparing descriptor sets. +/// +/// > **Note**: For reliability reasons, if you drop/leak this struct before calling `compare` then +/// > the cache of the currently bound descriptor sets will be reset. +pub struct StateCacherDescriptorSets<'s> { + // Reference to the parent's `poisoned_descriptor_sets`. + poisoned: &'s mut bool, + // Reference to the descriptor sets list to compare to. + state: &'s mut SmallVec<[(ash::vk::DescriptorSet, SmallVec<[u32; 32]>); 12]>, + // Next offset within the list to compare to. + offset: usize, + // Contains the return value of `compare`. + found_diff: Option<u32>, +} + +impl<'s> StateCacherDescriptorSets<'s> { + /// Adds a descriptor set to the list to compare. + #[inline] + pub fn add(&mut self, descriptor_set: &DescriptorSetWithOffsets) { + let (descriptor_set, dynamic_offsets) = descriptor_set.as_ref(); + let raw = descriptor_set.inner().internal_object(); + let dynamic_offsets = dynamic_offsets.iter().copied().collect(); + + if let Some(state) = self.state.get_mut(self.offset) { + if (&state.0, &state.1) == (&raw, &dynamic_offsets) { + self.offset += 1; + return; + } + + *state = (raw, dynamic_offsets); + } else { + self.state.push((raw, dynamic_offsets)); + } + + if self.found_diff.is_none() { + self.found_diff = Some(self.offset as u32); + } + self.offset += 1; + } + + /// Compares your list to the list in cache, and returns the offset of the first set to bind. + /// Returns `None` if the two lists were identical. + /// + /// After this function returns, the cache will be updated to match your list. + #[inline] + pub fn compare(self) -> Option<u32> { + *self.poisoned = false; + // Removing from the cache any set that wasn't added with `add`. + self.state.truncate(self.offset); + self.found_diff + } +} + +/// Helper struct for comparing vertex buffers. +/// +/// > **Note**: For reliability reasons, if you drop/leak this struct before calling `compare` then +/// > the cache of the currently bound vertex buffers will be reset. +pub struct StateCacherVertexBuffers<'s> { + // Reference to the parent's `poisoned_vertex_buffers`. + poisoned: &'s mut bool, + // Reference to the vertex buffers list to compare to. + state: &'s mut SmallVec<[(ash::vk::Buffer, DeviceSize); 12]>, + // Next offset within the list to compare to. + offset: usize, + // Contains the offset of the first vertex buffer that differs. + first_diff: Option<u32>, + // Offset of the last vertex buffer that differs. + last_diff: u32, +} + +impl<'s> StateCacherVertexBuffers<'s> { + /// Adds a vertex buffer to the list to compare. + #[inline] + pub fn add<B>(&mut self, buffer: &B) + where + B: ?Sized + BufferAccess, + { + let raw = { + let inner = buffer.inner(); + let raw = inner.buffer.internal_object(); + let offset = inner.offset; + (raw, offset) + }; + + if self.offset < self.state.len() { + if self.state[self.offset] == raw { + self.offset += 1; + return; + } + + self.state[self.offset] = raw; + } else { + self.state.push(raw); + } + + self.last_diff = self.offset as u32; + if self.first_diff.is_none() { + self.first_diff = Some(self.offset as u32); + } + self.offset += 1; + } + + /// Compares your list to the list in cache, and returns the range of the vertex buffers to + /// bind. Returns `None` if the two lists were identical. + /// + /// After this function returns, the cache will be updated to match your list. + /// + /// > **Note**: Keep in mind that `range.end` is *after* the last element. For example the + /// > range `1 .. 2` only contains one element. + #[inline] + pub fn compare(self) -> Option<Range<u32>> { + *self.poisoned = false; + + // Removing from the cache any set that wasn't added with `add`. + self.state.truncate(self.offset); + + self.first_diff.map(|first| { + debug_assert!(first <= self.last_diff); + first..(self.last_diff + 1) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::buffer::BufferUsage; + use crate::buffer::CpuAccessibleBuffer; + use crate::command_buffer::state_cacher::StateCacher; + + #[test] + fn vb_caching_single() { + let (device, queue) = gfx_dev_and_queue!(); + + const EMPTY: [i32; 0] = []; + let buf = + CpuAccessibleBuffer::from_data(device, BufferUsage::vertex_buffer(), false, EMPTY) + .unwrap(); + + let mut cacher = StateCacher::new(); + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf); + assert_eq!(bind_vb.compare(), Some(0..1)); + } + + for _ in 0..3 { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf); + assert_eq!(bind_vb.compare(), None); + } + } + + #[test] + fn vb_caching_invalidated() { + let (device, queue) = gfx_dev_and_queue!(); + + const EMPTY: [i32; 0] = []; + let buf = + CpuAccessibleBuffer::from_data(device, BufferUsage::vertex_buffer(), false, EMPTY) + .unwrap(); + + let mut cacher = StateCacher::new(); + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf); + assert_eq!(bind_vb.compare(), Some(0..1)); + } + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf); + assert_eq!(bind_vb.compare(), None); + } + + cacher.invalidate(); + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf); + assert_eq!(bind_vb.compare(), Some(0..1)); + } + } + + #[test] + fn vb_caching_multi() { + let (device, queue) = gfx_dev_and_queue!(); + + const EMPTY: [i32; 0] = []; + let buf1 = CpuAccessibleBuffer::from_data( + device.clone(), + BufferUsage::vertex_buffer(), + false, + EMPTY, + ) + .unwrap(); + let buf2 = CpuAccessibleBuffer::from_data( + device.clone(), + BufferUsage::vertex_buffer(), + false, + EMPTY, + ) + .unwrap(); + let buf3 = + CpuAccessibleBuffer::from_data(device, BufferUsage::vertex_buffer(), false, EMPTY) + .unwrap(); + + let mut cacher = StateCacher::new(); + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf1); + bind_vb.add(&buf2); + assert_eq!(bind_vb.compare(), Some(0..2)); + } + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf1); + bind_vb.add(&buf2); + bind_vb.add(&buf3); + assert_eq!(bind_vb.compare(), Some(2..3)); + } + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf1); + assert_eq!(bind_vb.compare(), None); + } + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf1); + bind_vb.add(&buf3); + assert_eq!(bind_vb.compare(), Some(1..2)); + } + + { + let mut bind_vb = cacher.bind_vertex_buffers(); + bind_vb.add(&buf2); + bind_vb.add(&buf3); + assert_eq!(bind_vb.compare(), Some(0..1)); + } + } +} |