| // 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!(), |
| }; |
| } |
| } |