| // 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. |
| |
| //! A pipeline that performs general-purpose operations. |
| //! |
| //! A compute pipeline takes buffers and/or images as both inputs and outputs. It operates |
| //! "standalone", with no additional infrastructure such as render passes or vertex input. Compute |
| //! pipelines can be used by themselves for performing work on the Vulkan device, but they can also |
| //! assist graphics operations by precalculating or postprocessing the operations from another kind |
| //! of pipeline. While it theoretically possible to perform graphics operations entirely in a |
| //! compute pipeline, a graphics pipeline is better suited to that task. |
| //! |
| //! A compute pipeline is relatively simple to create, requiring only a pipeline layout and a single |
| //! shader, the *compute shader*. The compute shader is the actual program that performs the work. |
| //! Once created, you can execute a compute pipeline by *binding* it in a command buffer, binding |
| //! any descriptor sets and/or push constants that the pipeline needs, and then issuing a `dispatch` |
| //! command on the command buffer. |
| |
| use super::layout::PipelineLayoutCreateInfo; |
| use crate::{ |
| descriptor_set::layout::{ |
| DescriptorSetLayout, DescriptorSetLayoutCreateInfo, DescriptorSetLayoutCreationError, |
| }, |
| device::{Device, DeviceOwned}, |
| macros::impl_id_counter, |
| pipeline::{ |
| cache::PipelineCache, |
| layout::{PipelineLayout, PipelineLayoutCreationError, PipelineLayoutSupersetError}, |
| Pipeline, PipelineBindPoint, |
| }, |
| shader::{DescriptorBindingRequirements, EntryPoint, SpecializationConstants}, |
| DeviceSize, OomError, VulkanError, VulkanObject, |
| }; |
| use ahash::HashMap; |
| use std::{ |
| error::Error, |
| fmt::{Debug, Display, Error as FmtError, Formatter}, |
| mem, |
| mem::MaybeUninit, |
| num::NonZeroU64, |
| ptr, |
| sync::Arc, |
| }; |
| |
| /// A pipeline object that describes to the Vulkan implementation how it should perform compute |
| /// operations. |
| /// |
| /// The template parameter contains the descriptor set to use with this pipeline. |
| /// |
| /// Pass an optional `Arc` to a `PipelineCache` to enable pipeline caching. The vulkan |
| /// implementation will handle the `PipelineCache` and check if it is available. |
| /// Check the documentation of the `PipelineCache` for more information. |
| pub struct ComputePipeline { |
| handle: ash::vk::Pipeline, |
| device: Arc<Device>, |
| id: NonZeroU64, |
| layout: Arc<PipelineLayout>, |
| descriptor_binding_requirements: HashMap<(u32, u32), DescriptorBindingRequirements>, |
| num_used_descriptor_sets: u32, |
| } |
| |
| impl ComputePipeline { |
| /// Builds a new `ComputePipeline`. |
| /// |
| /// `func` is a closure that is given a mutable reference to the inferred descriptor set |
| /// definitions. This can be used to make changes to the layout before it's created, for example |
| /// to add dynamic buffers or immutable samplers. |
| pub fn new<Css, F>( |
| device: Arc<Device>, |
| shader: EntryPoint<'_>, |
| specialization_constants: &Css, |
| cache: Option<Arc<PipelineCache>>, |
| func: F, |
| ) -> Result<Arc<ComputePipeline>, ComputePipelineCreationError> |
| where |
| Css: SpecializationConstants, |
| F: FnOnce(&mut [DescriptorSetLayoutCreateInfo]), |
| { |
| let mut set_layout_create_infos = DescriptorSetLayoutCreateInfo::from_requirements( |
| shader.descriptor_binding_requirements(), |
| ); |
| func(&mut set_layout_create_infos); |
| let set_layouts = set_layout_create_infos |
| .iter() |
| .map(|desc| DescriptorSetLayout::new(device.clone(), desc.clone())) |
| .collect::<Result<Vec<_>, _>>()?; |
| |
| let layout = PipelineLayout::new( |
| device.clone(), |
| PipelineLayoutCreateInfo { |
| set_layouts, |
| push_constant_ranges: shader |
| .push_constant_requirements() |
| .cloned() |
| .into_iter() |
| .collect(), |
| ..Default::default() |
| }, |
| )?; |
| |
| unsafe { |
| ComputePipeline::with_unchecked_pipeline_layout( |
| device, |
| shader, |
| specialization_constants, |
| layout, |
| cache, |
| ) |
| } |
| } |
| |
| /// Builds a new `ComputePipeline` with a specific pipeline layout. |
| /// |
| /// An error will be returned if the pipeline layout isn't a superset of what the shader |
| /// uses. |
| pub fn with_pipeline_layout<Css>( |
| device: Arc<Device>, |
| shader: EntryPoint<'_>, |
| specialization_constants: &Css, |
| layout: Arc<PipelineLayout>, |
| cache: Option<Arc<PipelineCache>>, |
| ) -> Result<Arc<ComputePipeline>, ComputePipelineCreationError> |
| where |
| Css: SpecializationConstants, |
| { |
| let spec_descriptors = Css::descriptors(); |
| |
| for (constant_id, reqs) in shader.specialization_constant_requirements() { |
| let map_entry = spec_descriptors |
| .iter() |
| .find(|desc| desc.constant_id == constant_id) |
| .ok_or(ComputePipelineCreationError::IncompatibleSpecializationConstants)?; |
| |
| if map_entry.size as DeviceSize != reqs.size { |
| return Err(ComputePipelineCreationError::IncompatibleSpecializationConstants); |
| } |
| } |
| |
| layout.ensure_compatible_with_shader( |
| shader.descriptor_binding_requirements(), |
| shader.push_constant_requirements(), |
| )?; |
| |
| unsafe { |
| ComputePipeline::with_unchecked_pipeline_layout( |
| device, |
| shader, |
| specialization_constants, |
| layout, |
| cache, |
| ) |
| } |
| } |
| |
| /// Same as `with_pipeline_layout`, but doesn't check whether the pipeline layout is a |
| /// superset of what the shader expects. |
| pub unsafe fn with_unchecked_pipeline_layout<Css>( |
| device: Arc<Device>, |
| shader: EntryPoint<'_>, |
| specialization_constants: &Css, |
| layout: Arc<PipelineLayout>, |
| cache: Option<Arc<PipelineCache>>, |
| ) -> Result<Arc<ComputePipeline>, ComputePipelineCreationError> |
| where |
| Css: SpecializationConstants, |
| { |
| let fns = device.fns(); |
| |
| let handle = { |
| let spec_descriptors = Css::descriptors(); |
| let specialization = ash::vk::SpecializationInfo { |
| map_entry_count: spec_descriptors.len() as u32, |
| p_map_entries: spec_descriptors.as_ptr() as *const _, |
| data_size: mem::size_of_val(specialization_constants), |
| p_data: specialization_constants as *const Css as *const _, |
| }; |
| |
| let stage = ash::vk::PipelineShaderStageCreateInfo { |
| flags: ash::vk::PipelineShaderStageCreateFlags::empty(), |
| stage: ash::vk::ShaderStageFlags::COMPUTE, |
| module: shader.module().handle(), |
| p_name: shader.name().as_ptr(), |
| p_specialization_info: if specialization.data_size == 0 { |
| ptr::null() |
| } else { |
| &specialization |
| }, |
| ..Default::default() |
| }; |
| |
| let infos = ash::vk::ComputePipelineCreateInfo { |
| flags: ash::vk::PipelineCreateFlags::empty(), |
| stage, |
| layout: layout.handle(), |
| base_pipeline_handle: ash::vk::Pipeline::null(), |
| base_pipeline_index: 0, |
| ..Default::default() |
| }; |
| |
| let cache_handle = match cache { |
| Some(ref cache) => cache.handle(), |
| None => ash::vk::PipelineCache::null(), |
| }; |
| |
| let mut output = MaybeUninit::uninit(); |
| (fns.v1_0.create_compute_pipelines)( |
| device.handle(), |
| cache_handle, |
| 1, |
| &infos, |
| ptr::null(), |
| output.as_mut_ptr(), |
| ) |
| .result() |
| .map_err(VulkanError::from)?; |
| output.assume_init() |
| }; |
| |
| let descriptor_binding_requirements: HashMap<_, _> = shader |
| .descriptor_binding_requirements() |
| .map(|(loc, reqs)| (loc, reqs.clone())) |
| .collect(); |
| let num_used_descriptor_sets = descriptor_binding_requirements |
| .keys() |
| .map(|loc| loc.0) |
| .max() |
| .map(|x| x + 1) |
| .unwrap_or(0); |
| |
| Ok(Arc::new(ComputePipeline { |
| handle, |
| device: device.clone(), |
| id: Self::next_id(), |
| layout, |
| descriptor_binding_requirements, |
| num_used_descriptor_sets, |
| })) |
| } |
| |
| /// Returns the `Device` this compute pipeline was created with. |
| #[inline] |
| pub fn device(&self) -> &Arc<Device> { |
| &self.device |
| } |
| } |
| |
| impl Pipeline for ComputePipeline { |
| #[inline] |
| fn bind_point(&self) -> PipelineBindPoint { |
| PipelineBindPoint::Compute |
| } |
| |
| #[inline] |
| fn layout(&self) -> &Arc<PipelineLayout> { |
| &self.layout |
| } |
| |
| #[inline] |
| fn num_used_descriptor_sets(&self) -> u32 { |
| self.num_used_descriptor_sets |
| } |
| |
| #[inline] |
| fn descriptor_binding_requirements( |
| &self, |
| ) -> &HashMap<(u32, u32), DescriptorBindingRequirements> { |
| &self.descriptor_binding_requirements |
| } |
| } |
| |
| impl Debug for ComputePipeline { |
| fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { |
| write!(f, "<Vulkan compute pipeline {:?}>", self.handle) |
| } |
| } |
| |
| impl_id_counter!(ComputePipeline); |
| |
| unsafe impl VulkanObject for ComputePipeline { |
| type Handle = ash::vk::Pipeline; |
| |
| #[inline] |
| fn handle(&self) -> Self::Handle { |
| self.handle |
| } |
| } |
| |
| unsafe impl DeviceOwned for ComputePipeline { |
| #[inline] |
| fn device(&self) -> &Arc<Device> { |
| self.device() |
| } |
| } |
| |
| impl Drop for ComputePipeline { |
| #[inline] |
| fn drop(&mut self) { |
| unsafe { |
| let fns = self.device.fns(); |
| (fns.v1_0.destroy_pipeline)(self.device.handle(), self.handle, ptr::null()); |
| } |
| } |
| } |
| |
| /// Error that can happen when creating a compute pipeline. |
| #[derive(Clone, Debug, PartialEq, Eq)] |
| pub enum ComputePipelineCreationError { |
| /// Not enough memory. |
| OomError(OomError), |
| /// Error while creating a descriptor set layout object. |
| DescriptorSetLayoutCreationError(DescriptorSetLayoutCreationError), |
| /// Error while creating the pipeline layout object. |
| PipelineLayoutCreationError(PipelineLayoutCreationError), |
| /// The pipeline layout is not compatible with what the shader expects. |
| IncompatiblePipelineLayout(PipelineLayoutSupersetError), |
| /// The provided specialization constants are not compatible with what the shader expects. |
| IncompatibleSpecializationConstants, |
| } |
| |
| impl Error for ComputePipelineCreationError { |
| fn source(&self) -> Option<&(dyn Error + 'static)> { |
| match self { |
| Self::OomError(err) => Some(err), |
| Self::DescriptorSetLayoutCreationError(err) => Some(err), |
| Self::PipelineLayoutCreationError(err) => Some(err), |
| Self::IncompatiblePipelineLayout(err) => Some(err), |
| Self::IncompatibleSpecializationConstants => None, |
| } |
| } |
| } |
| |
| impl Display for ComputePipelineCreationError { |
| fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { |
| write!( |
| f, |
| "{}", |
| match self { |
| ComputePipelineCreationError::OomError(_) => "not enough memory available", |
| ComputePipelineCreationError::DescriptorSetLayoutCreationError(_) => { |
| "error while creating a descriptor set layout object" |
| } |
| ComputePipelineCreationError::PipelineLayoutCreationError(_) => { |
| "error while creating the pipeline layout object" |
| } |
| ComputePipelineCreationError::IncompatiblePipelineLayout(_) => { |
| "the pipeline layout is not compatible with what the shader expects" |
| } |
| ComputePipelineCreationError::IncompatibleSpecializationConstants => { |
| "the provided specialization constants are not compatible with what the shader \ |
| expects" |
| } |
| } |
| ) |
| } |
| } |
| |
| impl From<OomError> for ComputePipelineCreationError { |
| fn from(err: OomError) -> ComputePipelineCreationError { |
| Self::OomError(err) |
| } |
| } |
| |
| impl From<DescriptorSetLayoutCreationError> for ComputePipelineCreationError { |
| fn from(err: DescriptorSetLayoutCreationError) -> Self { |
| Self::DescriptorSetLayoutCreationError(err) |
| } |
| } |
| |
| impl From<PipelineLayoutCreationError> for ComputePipelineCreationError { |
| fn from(err: PipelineLayoutCreationError) -> Self { |
| Self::PipelineLayoutCreationError(err) |
| } |
| } |
| |
| impl From<PipelineLayoutSupersetError> for ComputePipelineCreationError { |
| fn from(err: PipelineLayoutSupersetError) -> Self { |
| Self::IncompatiblePipelineLayout(err) |
| } |
| } |
| |
| impl From<VulkanError> for ComputePipelineCreationError { |
| fn from(err: VulkanError) -> ComputePipelineCreationError { |
| match err { |
| err @ VulkanError::OutOfHostMemory => Self::OomError(OomError::from(err)), |
| err @ VulkanError::OutOfDeviceMemory => Self::OomError(OomError::from(err)), |
| _ => panic!("unexpected error: {:?}", err), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::{ |
| buffer::{Buffer, BufferCreateInfo, BufferUsage}, |
| command_buffer::{ |
| allocator::StandardCommandBufferAllocator, AutoCommandBufferBuilder, CommandBufferUsage, |
| }, |
| descriptor_set::{ |
| allocator::StandardDescriptorSetAllocator, PersistentDescriptorSet, WriteDescriptorSet, |
| }, |
| memory::allocator::{AllocationCreateInfo, MemoryUsage, StandardMemoryAllocator}, |
| pipeline::{ComputePipeline, Pipeline, PipelineBindPoint}, |
| shader::{ShaderModule, SpecializationConstants, SpecializationMapEntry}, |
| sync::{now, GpuFuture}, |
| }; |
| |
| // TODO: test for basic creation |
| // TODO: test for pipeline layout error |
| |
| #[test] |
| fn specialization_constants() { |
| // This test checks whether specialization constants work. |
| // It executes a single compute shader (one invocation) that writes the value of a spec. |
| // constant to a buffer. The buffer content is then checked for the right value. |
| |
| let (device, queue) = gfx_dev_and_queue!(); |
| |
| let module = unsafe { |
| /* |
| #version 450 |
| |
| layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; |
| |
| layout(constant_id = 83) const int VALUE = 0xdeadbeef; |
| |
| layout(set = 0, binding = 0) buffer Output { |
| int write; |
| } write; |
| |
| void main() { |
| write.write = VALUE; |
| } |
| */ |
| const MODULE: [u8; 480] = [ |
| 3, 2, 35, 7, 0, 0, 1, 0, 1, 0, 8, 0, 14, 0, 0, 0, 0, 0, 0, 0, 17, 0, 2, 0, 1, 0, 0, |
| 0, 11, 0, 6, 0, 1, 0, 0, 0, 71, 76, 83, 76, 46, 115, 116, 100, 46, 52, 53, 48, 0, |
| 0, 0, 0, 14, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 15, 0, 5, 0, 5, 0, 0, 0, 4, 0, 0, 0, |
| 109, 97, 105, 110, 0, 0, 0, 0, 16, 0, 6, 0, 4, 0, 0, 0, 17, 0, 0, 0, 1, 0, 0, 0, 1, |
| 0, 0, 0, 1, 0, 0, 0, 3, 0, 3, 0, 2, 0, 0, 0, 194, 1, 0, 0, 5, 0, 4, 0, 4, 0, 0, 0, |
| 109, 97, 105, 110, 0, 0, 0, 0, 5, 0, 4, 0, 7, 0, 0, 0, 79, 117, 116, 112, 117, 116, |
| 0, 0, 6, 0, 5, 0, 7, 0, 0, 0, 0, 0, 0, 0, 119, 114, 105, 116, 101, 0, 0, 0, 5, 0, |
| 4, 0, 9, 0, 0, 0, 119, 114, 105, 116, 101, 0, 0, 0, 5, 0, 4, 0, 11, 0, 0, 0, 86, |
| 65, 76, 85, 69, 0, 0, 0, 72, 0, 5, 0, 7, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, |
| 0, 71, 0, 3, 0, 7, 0, 0, 0, 3, 0, 0, 0, 71, 0, 4, 0, 9, 0, 0, 0, 34, 0, 0, 0, 0, 0, |
| 0, 0, 71, 0, 4, 0, 9, 0, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 71, 0, 4, 0, 11, 0, 0, 0, |
| 1, 0, 0, 0, 83, 0, 0, 0, 19, 0, 2, 0, 2, 0, 0, 0, 33, 0, 3, 0, 3, 0, 0, 0, 2, 0, 0, |
| 0, 21, 0, 4, 0, 6, 0, 0, 0, 32, 0, 0, 0, 1, 0, 0, 0, 30, 0, 3, 0, 7, 0, 0, 0, 6, 0, |
| 0, 0, 32, 0, 4, 0, 8, 0, 0, 0, 2, 0, 0, 0, 7, 0, 0, 0, 59, 0, 4, 0, 8, 0, 0, 0, 9, |
| 0, 0, 0, 2, 0, 0, 0, 43, 0, 4, 0, 6, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 50, 0, 4, 0, |
| 6, 0, 0, 0, 11, 0, 0, 0, 239, 190, 173, 222, 32, 0, 4, 0, 12, 0, 0, 0, 2, 0, 0, 0, |
| 6, 0, 0, 0, 54, 0, 5, 0, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 248, 0, 2, |
| 0, 5, 0, 0, 0, 65, 0, 5, 0, 12, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 62, |
| 0, 3, 0, 13, 0, 0, 0, 11, 0, 0, 0, 253, 0, 1, 0, 56, 0, 1, 0, |
| ]; |
| ShaderModule::from_bytes(device.clone(), &MODULE).unwrap() |
| }; |
| |
| #[derive(Debug, Copy, Clone)] |
| #[allow(non_snake_case)] |
| #[repr(C)] |
| struct SpecConsts { |
| VALUE: i32, |
| } |
| unsafe impl SpecializationConstants for SpecConsts { |
| fn descriptors() -> &'static [SpecializationMapEntry] { |
| static DESCRIPTORS: [SpecializationMapEntry; 1] = [SpecializationMapEntry { |
| constant_id: 83, |
| offset: 0, |
| size: 4, |
| }]; |
| &DESCRIPTORS |
| } |
| } |
| |
| let pipeline = ComputePipeline::new( |
| device.clone(), |
| module.entry_point("main").unwrap(), |
| &SpecConsts { VALUE: 0x12345678 }, |
| None, |
| |_| {}, |
| ) |
| .unwrap(); |
| |
| let memory_allocator = StandardMemoryAllocator::new_default(device.clone()); |
| let data_buffer = Buffer::from_data( |
| &memory_allocator, |
| BufferCreateInfo { |
| usage: BufferUsage::STORAGE_BUFFER, |
| ..Default::default() |
| }, |
| AllocationCreateInfo { |
| usage: MemoryUsage::Upload, |
| ..Default::default() |
| }, |
| 0, |
| ) |
| .unwrap(); |
| |
| let ds_allocator = StandardDescriptorSetAllocator::new(device.clone()); |
| let set = PersistentDescriptorSet::new( |
| &ds_allocator, |
| pipeline.layout().set_layouts().get(0).unwrap().clone(), |
| [WriteDescriptorSet::buffer(0, data_buffer.clone())], |
| ) |
| .unwrap(); |
| |
| let cb_allocator = StandardCommandBufferAllocator::new(device.clone(), Default::default()); |
| let mut cbb = AutoCommandBufferBuilder::primary( |
| &cb_allocator, |
| queue.queue_family_index(), |
| CommandBufferUsage::OneTimeSubmit, |
| ) |
| .unwrap(); |
| cbb.bind_pipeline_compute(pipeline.clone()) |
| .bind_descriptor_sets( |
| PipelineBindPoint::Compute, |
| pipeline.layout().clone(), |
| 0, |
| set, |
| ) |
| .dispatch([1, 1, 1]) |
| .unwrap(); |
| let cb = cbb.build().unwrap(); |
| |
| let future = now(device) |
| .then_execute(queue, cb) |
| .unwrap() |
| .then_signal_fence_and_flush() |
| .unwrap(); |
| future.wait(None).unwrap(); |
| |
| let data_buffer_content = data_buffer.read().unwrap(); |
| assert_eq!(*data_buffer_content, 0x12345678); |
| } |
| } |