diff options
Diffstat (limited to 'src/query.rs')
-rw-r--r-- | src/query.rs | 688 |
1 files changed, 688 insertions, 0 deletions
diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..44caada --- /dev/null +++ b/src/query.rs @@ -0,0 +1,688 @@ +// Copyright (c) 2016 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. + +//! Gather information about rendering, held in query pools. +//! +//! In Vulkan, queries are not created individually. Instead you manipulate **query pools**, which +//! represent a collection of queries. Whenever you use a query, you have to specify both the query +//! pool and the slot id within that query pool. + +use crate::check_errors; +use crate::device::Device; +use crate::device::DeviceOwned; +use crate::DeviceSize; +use crate::Error; +use crate::OomError; +use crate::Success; +use crate::VulkanObject; +use std::error; +use std::ffi::c_void; +use std::fmt; +use std::mem::MaybeUninit; +use std::ops::Range; +use std::ptr; +use std::sync::Arc; + +/// A collection of one or more queries of a particular type. +#[derive(Debug)] +pub struct QueryPool { + pool: ash::vk::QueryPool, + device: Arc<Device>, + num_slots: u32, + ty: QueryType, +} + +impl QueryPool { + /// Builds a new query pool. + pub fn new( + device: Arc<Device>, + ty: QueryType, + num_slots: u32, + ) -> Result<QueryPool, QueryPoolCreationError> { + let statistics = match ty { + QueryType::PipelineStatistics(flags) => { + if !device.enabled_features().pipeline_statistics_query { + return Err(QueryPoolCreationError::PipelineStatisticsQueryFeatureNotEnabled); + } + + flags.into() + } + QueryType::Occlusion | QueryType::Timestamp => { + ash::vk::QueryPipelineStatisticFlags::empty() + } + }; + + let pool = unsafe { + let infos = ash::vk::QueryPoolCreateInfo { + flags: ash::vk::QueryPoolCreateFlags::empty(), + query_type: ty.into(), + query_count: num_slots, + pipeline_statistics: statistics, + ..Default::default() + }; + + let mut output = MaybeUninit::uninit(); + let fns = device.fns(); + check_errors(fns.v1_0.create_query_pool( + device.internal_object(), + &infos, + ptr::null(), + output.as_mut_ptr(), + ))?; + output.assume_init() + }; + + Ok(QueryPool { + pool, + device, + num_slots, + ty, + }) + } + + /// Returns the [`QueryType`] that this query pool was created with. + #[inline] + pub fn ty(&self) -> QueryType { + self.ty + } + + /// Returns the number of query slots of this query pool. + #[inline] + pub fn num_slots(&self) -> u32 { + self.num_slots + } + + /// Returns a reference to a single query slot, or `None` if the index is out of range. + #[inline] + pub fn query(&self, index: u32) -> Option<Query> { + if index < self.num_slots() { + Some(Query { pool: self, index }) + } else { + None + } + } + + /// Returns a reference to a range of queries, or `None` if out of range. + /// + /// # Panic + /// + /// Panics if the range is empty. + #[inline] + pub fn queries_range(&self, range: Range<u32>) -> Option<QueriesRange> { + assert!(!range.is_empty()); + + if range.end <= self.num_slots() { + Some(QueriesRange { pool: self, range }) + } else { + None + } + } +} + +unsafe impl VulkanObject for QueryPool { + type Object = ash::vk::QueryPool; + + #[inline] + fn internal_object(&self) -> ash::vk::QueryPool { + self.pool + } +} + +unsafe impl DeviceOwned for QueryPool { + #[inline] + fn device(&self) -> &Arc<Device> { + &self.device + } +} + +impl Drop for QueryPool { + #[inline] + fn drop(&mut self) { + unsafe { + let fns = self.device.fns(); + fns.v1_0 + .destroy_query_pool(self.device.internal_object(), self.pool, ptr::null()); + } + } +} + +/// Error that can happen when creating a query pool. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum QueryPoolCreationError { + /// Not enough memory. + OomError(OomError), + /// A pipeline statistics pool was requested but the corresponding feature wasn't enabled. + PipelineStatisticsQueryFeatureNotEnabled, +} + +impl error::Error for QueryPoolCreationError { + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + QueryPoolCreationError::OomError(ref err) => Some(err), + _ => None, + } + } +} + +impl fmt::Display for QueryPoolCreationError { + #[inline] + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!( + fmt, + "{}", + match *self { + QueryPoolCreationError::OomError(_) => "not enough memory available", + QueryPoolCreationError::PipelineStatisticsQueryFeatureNotEnabled => { + "a pipeline statistics pool was requested but the corresponding feature \ + wasn't enabled" + } + } + ) + } +} + +impl From<OomError> for QueryPoolCreationError { + #[inline] + fn from(err: OomError) -> QueryPoolCreationError { + QueryPoolCreationError::OomError(err) + } +} + +impl From<Error> for QueryPoolCreationError { + #[inline] + fn from(err: Error) -> QueryPoolCreationError { + match err { + err @ Error::OutOfHostMemory => QueryPoolCreationError::OomError(OomError::from(err)), + err @ Error::OutOfDeviceMemory => QueryPoolCreationError::OomError(OomError::from(err)), + _ => panic!("unexpected error: {:?}", err), + } + } +} + +/// A reference to a single query slot. +/// +/// This is created through [`QueryPool::query`]. +#[derive(Clone, Debug)] +pub struct Query<'a> { + pool: &'a QueryPool, + index: u32, +} + +impl<'a> Query<'a> { + /// Returns a reference to the query pool. + #[inline] + pub fn pool(&self) -> &'a QueryPool { + &self.pool + } + + /// Returns the index of the query represented. + #[inline] + pub fn index(&self) -> u32 { + self.index + } +} + +/// A reference to a range of queries. +/// +/// This is created through [`QueryPool::queries_range`]. +#[derive(Clone, Debug)] +pub struct QueriesRange<'a> { + pool: &'a QueryPool, + range: Range<u32>, +} + +impl<'a> QueriesRange<'a> { + /// Returns a reference to the query pool. + #[inline] + pub fn pool(&self) -> &'a QueryPool { + &self.pool + } + + /// Returns the range of queries represented. + #[inline] + pub fn range(&self) -> Range<u32> { + self.range.clone() + } + + /// Copies the results of this range of queries to a buffer on the CPU. + /// + /// [`self.pool().ty().result_size()`](QueryType::result_size) elements + /// will be written for each query in the range, plus 1 extra element per query if + /// [`QueryResultFlags::with_availability`] is enabled. + /// The provided buffer must be large enough to hold the data. + /// + /// `true` is returned if every result was available and written to the buffer. `false` + /// is returned if some results were not yet available; these will not be written to the buffer. + /// + /// See also [`copy_query_pool_results`](crate::command_buffer::AutoCommandBufferBuilder::copy_query_pool_results). + pub fn get_results<T>( + &self, + destination: &mut [T], + flags: QueryResultFlags, + ) -> Result<bool, GetResultsError> + where + T: QueryResultElement, + { + let stride = self.check_query_pool_results::<T>( + destination.as_ptr() as DeviceSize, + destination.len() as DeviceSize, + flags, + )?; + + let result = unsafe { + let fns = self.pool.device.fns(); + check_errors(fns.v1_0.get_query_pool_results( + self.pool.device.internal_object(), + self.pool.internal_object(), + self.range.start, + self.range.end - self.range.start, + std::mem::size_of_val(destination), + destination.as_mut_ptr() as *mut c_void, + stride, + ash::vk::QueryResultFlags::from(flags) | T::FLAG, + ))? + }; + + Ok(match result { + Success::Success => true, + Success::NotReady => false, + s => panic!("unexpected success value: {:?}", s), + }) + } + + pub(crate) fn check_query_pool_results<T>( + &self, + buffer_start: DeviceSize, + buffer_len: DeviceSize, + flags: QueryResultFlags, + ) -> Result<DeviceSize, GetResultsError> + where + T: QueryResultElement, + { + assert!(buffer_len > 0); + debug_assert!(buffer_start % std::mem::size_of::<T>() as DeviceSize == 0); + + let count = self.range.end - self.range.start; + let per_query_len = self.pool.ty.result_size() + flags.with_availability as DeviceSize; + let required_len = per_query_len * count as DeviceSize; + + if buffer_len < required_len { + return Err(GetResultsError::BufferTooSmall { + required_len: required_len as DeviceSize, + actual_len: buffer_len as DeviceSize, + }); + } + + match self.pool.ty { + QueryType::Occlusion => (), + QueryType::PipelineStatistics(_) => (), + QueryType::Timestamp => { + if flags.partial { + return Err(GetResultsError::InvalidFlags); + } + } + } + + Ok(per_query_len * std::mem::size_of::<T>() as DeviceSize) + } +} + +/// Error that can happen when calling [`QueriesRange::get_results`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GetResultsError { + /// The buffer is too small for the operation. + BufferTooSmall { + /// Required number of elements in the buffer. + required_len: DeviceSize, + /// Actual number of elements in the buffer. + actual_len: DeviceSize, + }, + /// The connection to the device has been lost. + DeviceLost, + /// The provided flags are not allowed for this type of query. + InvalidFlags, + /// Not enough memory. + OomError(OomError), +} + +impl From<Error> for GetResultsError { + #[inline] + fn from(err: Error) -> Self { + match err { + Error::OutOfHostMemory | Error::OutOfDeviceMemory => { + Self::OomError(OomError::from(err)) + } + Error::DeviceLost => Self::DeviceLost, + _ => panic!("unexpected error: {:?}", err), + } + } +} + +impl From<OomError> for GetResultsError { + #[inline] + fn from(err: OomError) -> Self { + Self::OomError(err) + } +} + +impl fmt::Display for GetResultsError { + #[inline] + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!( + fmt, + "{}", + match *self { + Self::BufferTooSmall { .. } => { + "the buffer is too small for the operation" + } + Self::DeviceLost => "the connection to the device has been lost", + Self::InvalidFlags => { + "the provided flags are not allowed for this type of query" + } + Self::OomError(_) => "not enough memory available", + } + ) + } +} + +impl error::Error for GetResultsError { + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + Self::OomError(ref err) => Some(err), + _ => None, + } + } +} + +/// A trait for elements of buffers that can be used as a destination for query results. +/// +/// # Safety +/// This is implemented for `u32` and `u64`. Unless you really know what you're doing, you should +/// not implement this trait for any other type. +pub unsafe trait QueryResultElement { + const FLAG: ash::vk::QueryResultFlags; +} + +unsafe impl QueryResultElement for u32 { + const FLAG: ash::vk::QueryResultFlags = ash::vk::QueryResultFlags::empty(); +} + +unsafe impl QueryResultElement for u64 { + const FLAG: ash::vk::QueryResultFlags = ash::vk::QueryResultFlags::TYPE_64; +} + +/// The type of query that a query pool should perform. +#[derive(Debug, Copy, Clone)] +pub enum QueryType { + /// Tracks the number of samples that pass per-fragment tests (e.g. the depth test). + Occlusion, + /// Tracks statistics on pipeline invocations and their input data. + PipelineStatistics(QueryPipelineStatisticFlags), + /// Writes timestamps at chosen points in a command buffer. + Timestamp, +} + +impl QueryType { + /// Returns the number of [`QueryResultElement`]s that are needed to hold the result of a + /// single query of this type. + /// + /// - For `Occlusion` and `Timestamp` queries, this returns 1. + /// - For `PipelineStatistics` queries, this returns the number of statistics flags enabled. + /// + /// If the results are retrieved with [`QueryResultFlags::with_availability`] enabled, then + /// an additional element is required per query. + #[inline] + pub const fn result_size(&self) -> DeviceSize { + match self { + Self::Occlusion | Self::Timestamp => 1, + Self::PipelineStatistics(flags) => flags.count(), + } + } +} + +impl From<QueryType> for ash::vk::QueryType { + #[inline] + fn from(value: QueryType) -> Self { + match value { + QueryType::Occlusion => ash::vk::QueryType::OCCLUSION, + QueryType::PipelineStatistics(_) => ash::vk::QueryType::PIPELINE_STATISTICS, + QueryType::Timestamp => ash::vk::QueryType::TIMESTAMP, + } + } +} + +/// Flags that control how a query is to be executed. +#[derive(Clone, Copy, Debug, Default)] +pub struct QueryControlFlags { + /// For occlusion queries, specifies that the result must reflect the exact number of + /// tests passed. If not enabled, the query may return a result of 1 even if more fragments + /// passed the test. + pub precise: bool, +} + +impl From<QueryControlFlags> for ash::vk::QueryControlFlags { + #[inline] + fn from(value: QueryControlFlags) -> Self { + let mut result = ash::vk::QueryControlFlags::empty(); + if value.precise { + result |= ash::vk::QueryControlFlags::PRECISE; + } + result + } +} + +/// For pipeline statistics queries, the statistics that should be gathered. +#[derive(Clone, Copy, Debug, Default)] +pub struct QueryPipelineStatisticFlags { + /// Count the number of vertices processed by the input assembly. + pub input_assembly_vertices: bool, + /// Count the number of primitives processed by the input assembly. + pub input_assembly_primitives: bool, + /// Count the number of times a vertex shader is invoked. + pub vertex_shader_invocations: bool, + /// Count the number of times a geometry shader is invoked. + pub geometry_shader_invocations: bool, + /// Count the number of primitives generated by geometry shaders. + pub geometry_shader_primitives: bool, + /// Count the number of times the clipping stage is invoked on a primitive. + pub clipping_invocations: bool, + /// Count the number of primitives that are output by the clipping stage. + pub clipping_primitives: bool, + /// Count the number of times a fragment shader is invoked. + pub fragment_shader_invocations: bool, + /// Count the number of patches processed by a tessellation control shader. + pub tessellation_control_shader_patches: bool, + /// Count the number of times a tessellation evaluation shader is invoked. + pub tessellation_evaluation_shader_invocations: bool, + /// Count the number of times a compute shader is invoked. + pub compute_shader_invocations: bool, +} + +impl QueryPipelineStatisticFlags { + #[inline] + pub fn none() -> QueryPipelineStatisticFlags { + QueryPipelineStatisticFlags { + input_assembly_vertices: false, + input_assembly_primitives: false, + vertex_shader_invocations: false, + geometry_shader_invocations: false, + geometry_shader_primitives: false, + clipping_invocations: false, + clipping_primitives: false, + fragment_shader_invocations: false, + tessellation_control_shader_patches: false, + tessellation_evaluation_shader_invocations: false, + compute_shader_invocations: false, + } + } + + /// Returns the number of flags that are set to `true`. + #[inline] + pub const fn count(&self) -> DeviceSize { + let &Self { + input_assembly_vertices, + input_assembly_primitives, + vertex_shader_invocations, + geometry_shader_invocations, + geometry_shader_primitives, + clipping_invocations, + clipping_primitives, + fragment_shader_invocations, + tessellation_control_shader_patches, + tessellation_evaluation_shader_invocations, + compute_shader_invocations, + } = self; + input_assembly_vertices as DeviceSize + + input_assembly_primitives as DeviceSize + + vertex_shader_invocations as DeviceSize + + geometry_shader_invocations as DeviceSize + + geometry_shader_primitives as DeviceSize + + clipping_invocations as DeviceSize + + clipping_primitives as DeviceSize + + fragment_shader_invocations as DeviceSize + + tessellation_control_shader_patches as DeviceSize + + tessellation_evaluation_shader_invocations as DeviceSize + + compute_shader_invocations as DeviceSize + } + + /// Returns `true` if any flags referring to compute operations are set to `true`. + #[inline] + pub const fn is_compute(&self) -> bool { + let &Self { + compute_shader_invocations, + .. + } = self; + compute_shader_invocations + } + + /// Returns `true` if any flags referring to graphics operations are set to `true`. + #[inline] + pub const fn is_graphics(&self) -> bool { + let &Self { + input_assembly_vertices, + input_assembly_primitives, + vertex_shader_invocations, + geometry_shader_invocations, + geometry_shader_primitives, + clipping_invocations, + clipping_primitives, + fragment_shader_invocations, + tessellation_control_shader_patches, + tessellation_evaluation_shader_invocations, + .. + } = self; + input_assembly_vertices + || input_assembly_primitives + || vertex_shader_invocations + || geometry_shader_invocations + || geometry_shader_primitives + || clipping_invocations + || clipping_primitives + || fragment_shader_invocations + || tessellation_control_shader_patches + || tessellation_evaluation_shader_invocations + } +} + +impl From<QueryPipelineStatisticFlags> for ash::vk::QueryPipelineStatisticFlags { + fn from(value: QueryPipelineStatisticFlags) -> ash::vk::QueryPipelineStatisticFlags { + let mut result = ash::vk::QueryPipelineStatisticFlags::empty(); + if value.input_assembly_vertices { + result |= ash::vk::QueryPipelineStatisticFlags::INPUT_ASSEMBLY_VERTICES; + } + if value.input_assembly_primitives { + result |= ash::vk::QueryPipelineStatisticFlags::INPUT_ASSEMBLY_PRIMITIVES; + } + if value.vertex_shader_invocations { + result |= ash::vk::QueryPipelineStatisticFlags::VERTEX_SHADER_INVOCATIONS; + } + if value.geometry_shader_invocations { + result |= ash::vk::QueryPipelineStatisticFlags::GEOMETRY_SHADER_INVOCATIONS; + } + if value.geometry_shader_primitives { + result |= ash::vk::QueryPipelineStatisticFlags::GEOMETRY_SHADER_PRIMITIVES; + } + if value.clipping_invocations { + result |= ash::vk::QueryPipelineStatisticFlags::CLIPPING_INVOCATIONS; + } + if value.clipping_primitives { + result |= ash::vk::QueryPipelineStatisticFlags::CLIPPING_PRIMITIVES; + } + if value.fragment_shader_invocations { + result |= ash::vk::QueryPipelineStatisticFlags::FRAGMENT_SHADER_INVOCATIONS; + } + if value.tessellation_control_shader_patches { + result |= ash::vk::QueryPipelineStatisticFlags::TESSELLATION_CONTROL_SHADER_PATCHES; + } + if value.tessellation_evaluation_shader_invocations { + result |= + ash::vk::QueryPipelineStatisticFlags::TESSELLATION_EVALUATION_SHADER_INVOCATIONS; + } + if value.compute_shader_invocations { + result |= ash::vk::QueryPipelineStatisticFlags::COMPUTE_SHADER_INVOCATIONS; + } + result + } +} + +/// Flags to control how the results of a query should be retrieved. +/// +/// `VK_QUERY_RESULT_64_BIT` is not included, as it is determined automatically via the +/// [`QueryResultElement`] trait. +#[derive(Clone, Copy, Debug, Default)] +pub struct QueryResultFlags { + /// Wait for the results to become available before writing the results. + pub wait: bool, + /// Write an additional element to the end of each query's results, indicating the availability + /// of the results: + /// - Nonzero: The results are available, and have been written to the element(s) preceding. + /// - Zero: The results are not yet available, and have not been written. + pub with_availability: bool, + /// Allow writing partial results to the buffer, instead of waiting until they are fully + /// available. + pub partial: bool, +} + +impl From<QueryResultFlags> for ash::vk::QueryResultFlags { + #[inline] + fn from(value: QueryResultFlags) -> Self { + let mut result = ash::vk::QueryResultFlags::empty(); + if value.wait { + result |= ash::vk::QueryResultFlags::WAIT; + } + if value.with_availability { + result |= ash::vk::QueryResultFlags::WITH_AVAILABILITY; + } + if value.partial { + result |= ash::vk::QueryResultFlags::PARTIAL; + } + result + } +} + +#[cfg(test)] +mod tests { + use crate::query::QueryPipelineStatisticFlags; + use crate::query::QueryPool; + use crate::query::QueryPoolCreationError; + use crate::query::QueryType; + + #[test] + fn pipeline_statistics_feature() { + let (device, _) = gfx_dev_and_queue!(); + + let ty = QueryType::PipelineStatistics(QueryPipelineStatisticFlags::none()); + match QueryPool::new(device, ty, 256) { + Err(QueryPoolCreationError::PipelineStatisticsQueryFeatureNotEnabled) => (), + _ => panic!(), + }; + } +} |