// Copyright (c) 2016 The vulkano developers // Licensed under the Apache License, Version 2.0 // or the MIT // license , // 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::{ buffer::BufferContents, device::{Device, DeviceOwned}, macros::{impl_id_counter, vulkan_bitflags}, DeviceSize, OomError, RequirementNotMet, RequiresOneOf, VulkanError, VulkanObject, }; use std::{ error::Error, ffi::c_void, fmt::{Display, Error as FmtError, Formatter}, mem::{size_of_val, MaybeUninit}, num::NonZeroU64, ops::Range, ptr, sync::Arc, }; /// A collection of one or more queries of a particular type. #[derive(Debug)] pub struct QueryPool { handle: ash::vk::QueryPool, device: Arc, id: NonZeroU64, query_type: QueryType, query_count: u32, } impl QueryPool { /// Creates a new `QueryPool`. /// /// # Panics /// /// - Panics if `create_info.query_count` is `0`. pub fn new( device: Arc, create_info: QueryPoolCreateInfo, ) -> Result, QueryPoolCreationError> { let QueryPoolCreateInfo { query_type, query_count, _ne: _, } = create_info; // VUID-VkQueryPoolCreateInfo-queryCount-02763 assert!(query_count != 0); let pipeline_statistics = match query_type { QueryType::PipelineStatistics(flags) => { // VUID-VkQueryPoolCreateInfo-queryType-00791 if !device.enabled_features().pipeline_statistics_query { return Err(QueryPoolCreationError::PipelineStatisticsQueryFeatureNotEnabled); } // VUID-VkQueryPoolCreateInfo-queryType-00792 flags.into() } QueryType::Occlusion | QueryType::Timestamp => { ash::vk::QueryPipelineStatisticFlags::empty() } }; let create_info = ash::vk::QueryPoolCreateInfo { flags: ash::vk::QueryPoolCreateFlags::empty(), query_type: query_type.into(), query_count, pipeline_statistics, ..Default::default() }; let handle = unsafe { let fns = device.fns(); let mut output = MaybeUninit::uninit(); (fns.v1_0.create_query_pool)( device.handle(), &create_info, ptr::null(), output.as_mut_ptr(), ) .result() .map_err(VulkanError::from)?; output.assume_init() }; Ok(Arc::new(QueryPool { handle, device, id: Self::next_id(), query_type, query_count, })) } /// Creates a new `QueryPool` from a raw object handle. /// /// # Safety /// /// - `handle` must be a valid Vulkan object handle created from `device`. /// - `create_info` must match the info used to create the object. #[inline] pub unsafe fn from_handle( device: Arc, handle: ash::vk::QueryPool, create_info: QueryPoolCreateInfo, ) -> Arc { let QueryPoolCreateInfo { query_type, query_count, _ne: _, } = create_info; Arc::new(QueryPool { handle, device, id: Self::next_id(), query_type, query_count, }) } /// Returns the query type of the pool. #[inline] pub fn query_type(&self) -> QueryType { self.query_type } /// Returns the number of query slots of this query pool. #[inline] pub fn query_count(&self) -> u32 { self.query_count } /// 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> { if index < self.query_count { Some(Query { pool: self, index }) } else { None } } /// Returns a reference to a range of queries, or `None` if out of range. /// /// # Panics /// /// - Panics if the range is empty. #[inline] pub fn queries_range(&self, range: Range) -> Option> { assert!(!range.is_empty()); if range.end <= self.query_count { Some(QueriesRange { pool: self, range }) } else { None } } } impl Drop for QueryPool { #[inline] fn drop(&mut self) { unsafe { let fns = self.device.fns(); (fns.v1_0.destroy_query_pool)(self.device.handle(), self.handle, ptr::null()); } } } unsafe impl VulkanObject for QueryPool { type Handle = ash::vk::QueryPool; #[inline] fn handle(&self) -> Self::Handle { self.handle } } unsafe impl DeviceOwned for QueryPool { #[inline] fn device(&self) -> &Arc { &self.device } } impl_id_counter!(QueryPool); /// Parameters to create a new `QueryPool`. #[derive(Clone, Debug)] pub struct QueryPoolCreateInfo { /// The type of query that the pool should be for. /// /// There is no default value. pub query_type: QueryType, /// The number of queries to create in the pool. /// /// The default value is `0`, which must be overridden. pub query_count: u32, pub _ne: crate::NonExhaustive, } impl QueryPoolCreateInfo { /// Returns a `QueryPoolCreateInfo` with the specified `query_type`. #[inline] pub fn query_type(query_type: QueryType) -> Self { Self { query_type, query_count: 0, _ne: crate::NonExhaustive(()), } } } /// 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 for QueryPoolCreationError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { QueryPoolCreationError::OomError(err) => Some(err), _ => None, } } } impl Display for QueryPoolCreationError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { write!( f, "{}", 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 for QueryPoolCreationError { fn from(err: OomError) -> QueryPoolCreationError { QueryPoolCreationError::OomError(err) } } impl From for QueryPoolCreationError { fn from(err: VulkanError) -> QueryPoolCreationError { match err { err @ VulkanError::OutOfHostMemory => { QueryPoolCreationError::OomError(OomError::from(err)) } err @ VulkanError::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, } 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 { self.range.clone() } /// Copies the results of this range of queries to a buffer on the CPU. /// /// [`self.pool().ty().result_len()`] will be written for each query in the range, plus 1 extra /// element per query if [`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`]. /// /// [`self.pool().ty().result_len()`]: QueryType::result_len /// [`WITH_AVAILABILITY`]: QueryResultFlags::WITH_AVAILABILITY /// [`copy_query_pool_results`]: crate::command_buffer::AutoCommandBufferBuilder::copy_query_pool_results #[inline] pub fn get_results( &self, destination: &mut [T], flags: QueryResultFlags, ) -> Result where T: QueryResultElement, { let stride = self.check_query_pool_results::( destination.as_ptr() as DeviceSize, destination.len() as DeviceSize, flags, )?; let result = unsafe { let fns = self.pool.device.fns(); (fns.v1_0.get_query_pool_results)( self.pool.device.handle(), self.pool.handle(), self.range.start, self.range.end - self.range.start, size_of_val(destination), destination.as_mut_ptr() as *mut c_void, stride, ash::vk::QueryResultFlags::from(flags) | T::FLAG, ) }; match result { ash::vk::Result::SUCCESS => Ok(true), ash::vk::Result::NOT_READY => Ok(false), err => Err(VulkanError::from(err).into()), } } pub(crate) fn check_query_pool_results( &self, buffer_start: DeviceSize, buffer_len: DeviceSize, flags: QueryResultFlags, ) -> Result where T: QueryResultElement, { // VUID-vkGetQueryPoolResults-flags-parameter // VUID-vkCmdCopyQueryPoolResults-flags-parameter flags.validate_device(&self.pool.device)?; assert!(buffer_len > 0); // VUID-vkGetQueryPoolResults-flags-02828 // VUID-vkGetQueryPoolResults-flags-00815 debug_assert!(buffer_start % std::mem::size_of::() as DeviceSize == 0); let count = self.range.end - self.range.start; let per_query_len = self.pool.query_type.result_len() + flags.intersects(QueryResultFlags::WITH_AVAILABILITY) as DeviceSize; let required_len = per_query_len * count as DeviceSize; // VUID-vkGetQueryPoolResults-dataSize-00817 if buffer_len < required_len { return Err(GetResultsError::BufferTooSmall { required_len: required_len as DeviceSize, actual_len: buffer_len as DeviceSize, }); } match self.pool.query_type { QueryType::Occlusion => (), QueryType::PipelineStatistics(_) => (), QueryType::Timestamp => { // VUID-vkGetQueryPoolResults-queryType-00818 if flags.intersects(QueryResultFlags::PARTIAL) { return Err(GetResultsError::InvalidFlags); } } } Ok(per_query_len * std::mem::size_of::() as DeviceSize) } } /// Error that can happen when calling [`QueriesRange::get_results`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum GetResultsError { /// The connection to the device has been lost. DeviceLost, /// Not enough memory. OomError(OomError), RequirementNotMet { required_for: &'static str, requires_one_of: RequiresOneOf, }, /// 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 provided flags are not allowed for this type of query. InvalidFlags, } impl Error for GetResultsError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::OomError(err) => Some(err), _ => None, } } } impl Display for GetResultsError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { match self { Self::OomError(_) => write!(f, "not enough memory available"), Self::DeviceLost => write!(f, "the connection to the device has been lost"), Self::RequirementNotMet { required_for, requires_one_of, } => write!( f, "a requirement was not met for: {}; requires one of: {}", required_for, requires_one_of, ), Self::BufferTooSmall { .. } => write!(f, "the buffer is too small for the operation"), Self::InvalidFlags => write!( f, "the provided flags are not allowed for this type of query" ), } } } impl From for GetResultsError { fn from(err: VulkanError) -> Self { match err { VulkanError::OutOfHostMemory | VulkanError::OutOfDeviceMemory => { Self::OomError(OomError::from(err)) } VulkanError::DeviceLost => Self::DeviceLost, _ => panic!("unexpected error: {:?}", err), } } } impl From for GetResultsError { fn from(err: OomError) -> Self { Self::OomError(err) } } impl From for GetResultsError { fn from(err: RequirementNotMet) -> Self { Self::RequirementNotMet { required_for: err.required_for, requires_one_of: err.requires_one_of, } } } /// 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: BufferContents + Sized { 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 [`WITH_AVAILABILITY`] enabled, then an additional element /// is required per query. /// /// [`Occlusion`]: QueryType::Occlusion /// [`Timestamp`]: QueryType::Timestamp /// [`PipelineStatistics`]: QueryType::PipelineStatistics /// [`WITH_AVAILABILITY`]: QueryResultFlags::WITH_AVAILABILITY #[inline] pub const fn result_len(self) -> DeviceSize { match self { Self::Occlusion | Self::Timestamp => 1, Self::PipelineStatistics(flags) => flags.count() as DeviceSize, } } } impl From 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, } } } vulkan_bitflags! { #[non_exhaustive] /// Flags that control how a query is to be executed. QueryControlFlags = QueryControlFlags(u32); /// 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. PRECISE = PRECISE, } vulkan_bitflags! { #[non_exhaustive] /// For pipeline statistics queries, the statistics that should be gathered. QueryPipelineStatisticFlags impl { /// Returns `true` if `self` contains any flags referring to compute operations. #[inline] pub const fn is_compute(self) -> bool { self.intersects(QueryPipelineStatisticFlags::COMPUTE_SHADER_INVOCATIONS) } /// Returns `true` if `self` contains any flags referring to graphics operations. #[inline] pub const fn is_graphics(self) -> bool { self.intersects( (QueryPipelineStatisticFlags::INPUT_ASSEMBLY_VERTICES) .union(QueryPipelineStatisticFlags::INPUT_ASSEMBLY_PRIMITIVES) .union(QueryPipelineStatisticFlags::VERTEX_SHADER_INVOCATIONS) .union(QueryPipelineStatisticFlags::GEOMETRY_SHADER_INVOCATIONS) .union(QueryPipelineStatisticFlags::GEOMETRY_SHADER_PRIMITIVES) .union(QueryPipelineStatisticFlags::CLIPPING_INVOCATIONS) .union(QueryPipelineStatisticFlags::CLIPPING_PRIMITIVES) .union(QueryPipelineStatisticFlags::FRAGMENT_SHADER_INVOCATIONS) .union(QueryPipelineStatisticFlags::TESSELLATION_CONTROL_SHADER_PATCHES) .union(QueryPipelineStatisticFlags::TESSELLATION_EVALUATION_SHADER_INVOCATIONS), ) } } = QueryPipelineStatisticFlags(u32); /// Count the number of vertices processed by the input assembly. INPUT_ASSEMBLY_VERTICES = INPUT_ASSEMBLY_VERTICES, /// Count the number of primitives processed by the input assembly. INPUT_ASSEMBLY_PRIMITIVES = INPUT_ASSEMBLY_PRIMITIVES, /// Count the number of times a vertex shader is invoked. VERTEX_SHADER_INVOCATIONS = VERTEX_SHADER_INVOCATIONS, /// Count the number of times a geometry shader is invoked. GEOMETRY_SHADER_INVOCATIONS = GEOMETRY_SHADER_INVOCATIONS, /// Count the number of primitives generated by geometry shaders. GEOMETRY_SHADER_PRIMITIVES = GEOMETRY_SHADER_PRIMITIVES, /// Count the number of times the clipping stage is invoked on a primitive. CLIPPING_INVOCATIONS = CLIPPING_INVOCATIONS, /// Count the number of primitives that are output by the clipping stage. CLIPPING_PRIMITIVES = CLIPPING_PRIMITIVES, /// Count the number of times a fragment shader is invoked. FRAGMENT_SHADER_INVOCATIONS = FRAGMENT_SHADER_INVOCATIONS, /// Count the number of patches processed by a tessellation control shader. TESSELLATION_CONTROL_SHADER_PATCHES = TESSELLATION_CONTROL_SHADER_PATCHES, /// Count the number of times a tessellation evaluation shader is invoked. TESSELLATION_EVALUATION_SHADER_INVOCATIONS = TESSELLATION_EVALUATION_SHADER_INVOCATIONS, /// Count the number of times a compute shader is invoked. COMPUTE_SHADER_INVOCATIONS = COMPUTE_SHADER_INVOCATIONS, /* TODO: enable // TODO: document TASK_SHADER_INVOCATIONS = TASK_SHADER_INVOCATIONS_NV { device_extensions: [nv_mesh_shader], },*/ /* TODO: enable // TODO: document MESH_SHADER_INVOCATIONS = MESH_SHADER_INVOCATIONS_NV { device_extensions: [nv_mesh_shader], },*/ } vulkan_bitflags! { #[non_exhaustive] /// 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. QueryResultFlags = QueryResultFlags(u32); /// Wait for the results to become available before writing the results. WAIT = WAIT, /// 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. WITH_AVAILABILITY = WITH_AVAILABILITY, /// Allow writing partial results to the buffer, instead of waiting until they are fully /// available. PARTIAL = PARTIAL, /* TODO: enable // TODO: document WITH_STATUS = WITH_STATUS_KHR { device_extensions: [khr_video_queue], },*/ } #[cfg(test)] mod tests { use super::QueryPoolCreateInfo; use crate::query::{QueryPipelineStatisticFlags, QueryPool, QueryPoolCreationError, QueryType}; #[test] fn pipeline_statistics_feature() { let (device, _) = gfx_dev_and_queue!(); let query_type = QueryType::PipelineStatistics(QueryPipelineStatisticFlags::empty()); match QueryPool::new( device, QueryPoolCreateInfo { query_count: 256, ..QueryPoolCreateInfo::query_type(query_type) }, ) { Err(QueryPoolCreationError::PipelineStatisticsQueryFeatureNotEnabled) => (), _ => panic!(), }; } }