From 44ff4c4839c357ff68501ae2405fcf35a51dfaca Mon Sep 17 00:00:00 2001 From: janis Date: Sat, 4 Apr 2026 15:46:17 +0200 Subject: [PATCH] pipeline cache --- Cargo.toml | 3 + crates/renderer/Cargo.toml | 2 + crates/renderer/src/commands.rs | 2 +- crates/renderer/src/device.rs | 5 + crates/renderer/src/lib.rs | 6 +- crates/renderer/src/pipeline.rs | 534 +++++++++++++++------------ crates/renderer/src/rendering/mod.rs | 6 +- 7 files changed, 323 insertions(+), 235 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a28b274..b822d89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,9 @@ petgraph = "0.7" itertools = "0.14.0" ahash = "0.8" +# for non-cryptographic hashing of resources like pipelines, e.g. for caching +md-5 = "0.11.0" + parking_lot = "0.12.3" tokio = "1.42" diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index bba6729..cce3d7b 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -26,6 +26,8 @@ vk-mem = { workspace = true } gpu-allocator = { workspace = true } rectangle-pack = { workspace = true } +md-5 = { workspace = true } + raw-window-handle = { workspace = true } egui = { workspace = true , features = ["bytemuck"]} egui_winit_platform = { workspace = true } diff --git a/crates/renderer/src/commands.rs b/crates/renderer/src/commands.rs index 8f3e7d5..1fbc883 100644 --- a/crates/renderer/src/commands.rs +++ b/crates/renderer/src/commands.rs @@ -547,7 +547,7 @@ pub mod traits { self.device().dev().cmd_bind_pipeline( self.handle(), pipeline.bind_point(), - pipeline.handle(), + pipeline.raw(), ); } } diff --git a/crates/renderer/src/device.rs b/crates/renderer/src/device.rs index 6253588..e110677 100644 --- a/crates/renderer/src/device.rs +++ b/crates/renderer/src/device.rs @@ -17,6 +17,7 @@ use raw_window_handle::RawDisplayHandle; use crate::{ Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result, + pipeline::pipeline_cache::PipelineCache, queue::{DeviceQueueInfos, DeviceQueues, Queue}, sync::{self, BinarySemaphore, TimelineSemaphore}, }; @@ -109,6 +110,9 @@ pub struct DeviceInner { pub(crate) device_extensions: DeviceExtensions, #[allow(dead_code)] pub(crate) enabled_extensions: Vec<&'static CStr>, + + pub(crate) pipeline_cache: PipelineCache, + _drop: DeviceDrop, } @@ -397,6 +401,7 @@ impl PhysicalDeviceInfo { raw: device.clone(), alloc2: Mutex::new(alloc2), instance: instance.clone(), + pipeline_cache: PipelineCache::new(&device, &self)?, adapter: self, queues: device_queues, device_extensions, diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index da5d773..c7cddfa 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -809,9 +809,9 @@ impl EguiState { "crates/renderer/shaders/egui_vert.spv", )?; - let pipeline = pipeline::Pipeline::new( + let pipeline = pipeline::Pipeline::new_graphics( device.clone(), - pipeline::PipelineDesc::Graphics(pipeline::GraphicsPipelineDesc { + pipeline::GraphicsPipelineDesc { flags: Default::default(), name: Some("egui-pipeline".into()), shader_stages: &[ @@ -904,7 +904,7 @@ impl EguiState { dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR], ..Default::default() }), - }), + }, )?; Ok(Self { diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index d48d2df..a2043ac 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -4,7 +4,7 @@ use ash::{ext, prelude::*, vk}; use crate::{ define_device_owned_handle, - device::{Device, DeviceOwnedDebugObject}, + device::{Device, DeviceHandle, DeviceObject}, make_extension, }; @@ -40,12 +40,6 @@ pub struct PipelineLayoutDesc<'a> { pub name: Option>, } -#[derive(Debug)] -pub enum PipelineDesc<'a> { - Compute(ComputePipelineDesc<'a>), - Graphics(GraphicsPipelineDesc<'a>), -} - #[derive(Debug)] pub struct ComputePipelineDesc<'a> { pub flags: vk::PipelineCreateFlags, @@ -251,18 +245,17 @@ impl DescriptorPool { let info = &vk::DescriptorSetAllocateInfo::default() .descriptor_pool(self.handle()) .set_layouts(&layouts); - let sets = unsafe { self.device().dev().allocate_descriptor_sets(&info)? }; + let sets = unsafe { self.device().dev().allocate_descriptor_sets(info)? }; for (&set, desc) in sets.iter().zip(descs) { if let Some(name) = desc.name.as_ref() { - unsafe { self.device().debug_name_object(set, &name) }; + unsafe { self.device().debug_name_object(set, name) }; } } Ok(sets) } - // pub fn free(&self) {} #[allow(dead_code)] pub fn reset(&self) -> VkResult<()> { unsafe { @@ -474,18 +467,13 @@ impl ShaderModule { #[derive(Debug)] pub struct Pipeline { - pipeline: DeviceOwnedDebugObject, + pipeline: DeviceObject, bind_point: vk::PipelineBindPoint, } -impl Drop for Pipeline { - fn drop(&mut self) { - unsafe { - self.pipeline - .dev() - .dev() - .destroy_pipeline(self.pipeline.handle(), None); - } +impl DeviceHandle for vk::Pipeline { + unsafe fn destroy(&mut self, device: &Device) { + unsafe { device.raw.destroy_pipeline(*self, None) }; } } @@ -500,224 +488,314 @@ impl ShaderStageDesc<'_> { } impl Pipeline { - pub fn new(device: Device, desc: PipelineDesc) -> VkResult { - let name: Option>; - let bind_point: vk::PipelineBindPoint; - let result = match desc { - PipelineDesc::Compute(desc) => { - name = desc.name; - bind_point = vk::PipelineBindPoint::COMPUTE; - let info = &vk::ComputePipelineCreateInfo::default() - .flags(desc.flags) - .layout(desc.layout.handle()) - .base_pipeline_handle( - desc.base_pipeline - .map(|p| p.handle()) - .unwrap_or(vk::Pipeline::null()), - ) - .stage(desc.shader_stage.as_create_info()); + pub fn new_compute(device: Device, desc: ComputePipelineDesc) -> crate::Result { + let info = &vk::ComputePipelineCreateInfo::default() + .flags(desc.flags) + .layout(desc.layout.handle()) + .base_pipeline_handle( + desc.base_pipeline + .map(|p| p.raw()) + .unwrap_or(vk::Pipeline::null()), + ) + .stage(desc.shader_stage.as_create_info()); - unsafe { - device.dev().create_compute_pipelines( - vk::PipelineCache::null(), - core::slice::from_ref(info), - None, - ) - } - } - PipelineDesc::Graphics(desc) => { - name = desc.name; - bind_point = vk::PipelineBindPoint::GRAPHICS; - - let stages = desc - .shader_stages - .iter() - .map(|stage| stage.as_create_info()) - .collect::>(); - - let vertex_input = desc.vertex_input.map(|vertex| { - vk::PipelineVertexInputStateCreateInfo::default() - .vertex_attribute_descriptions(vertex.attributes) - .vertex_binding_descriptions(vertex.bindings) - }); - let input_assembly = desc.input_assembly.map(|state| { - vk::PipelineInputAssemblyStateCreateInfo::default() - .primitive_restart_enable(state.primitive_restart) - .topology(state.topology) - }); - let tessellation = desc.tessellation.map(|state| { - vk::PipelineTessellationStateCreateInfo::default() - .flags(state.flags) - .patch_control_points(state.patch_control_points) - }); - let viewport = desc.viewport.map(|state| { - let mut info = vk::PipelineViewportStateCreateInfo::default() - .scissor_count(state.num_scissors) - .viewport_count(state.num_viewports); - if let Some(viewports) = state.viewports { - info = info.viewports(viewports); - } - if let Some(scissors) = state.scissors { - info = info.scissors(scissors); - } - - info - }); - - let rasterization = desc.rasterization.map(|state| { - let mut info = vk::PipelineRasterizationStateCreateInfo::default() - .line_width(state.line_width) - .cull_mode(state.cull_mode) - .polygon_mode(state.polygon_mode) - .rasterizer_discard_enable(state.discard_enable) - .depth_clamp_enable(state.depth_clamp_enable); - - if let Some(depth_bias) = state.depth_bias { - info = info - .depth_bias_enable(true) - .depth_bias_clamp(depth_bias.clamp) - .depth_bias_constant_factor(depth_bias.constant_factor) - .depth_bias_slope_factor(depth_bias.slope_factor); - } - - info - }); - - let multisample = desc.multisample.map(|state| { - let info = vk::PipelineMultisampleStateCreateInfo::default() - .flags(state.flags) - .min_sample_shading(state.min_sample_shading) - .rasterization_samples(state.rasterization_samples) - .sample_mask(state.sample_mask) - .sample_shading_enable(state.sample_shading_enable) - .alpha_to_coverage_enable(state.alpha_to_coverage_enable) - .alpha_to_one_enable(state.alpha_to_one_enable); - - info - }); - - let color_blend = desc.color_blend.map(|state| { - let info = vk::PipelineColorBlendStateCreateInfo::default() - .flags(state.flags) - .attachments(state.attachments) - .blend_constants(state.blend_constants) - .logic_op(state.logic_op.unwrap_or(Default::default())) - .logic_op_enable(state.logic_op.is_some()); - - info - }); - - let depth_stencil = desc.depth_stencil.map(|state| { - let mut info = - vk::PipelineDepthStencilStateCreateInfo::default().flags(state.flags); - - if let Some(depth) = state.depth { - info = info - .depth_compare_op(depth.compare_op.unwrap_or(vk::CompareOp::default())) - .depth_test_enable(depth.compare_op.is_some()) - .depth_write_enable(depth.write_enable) - .depth_bounds_test_enable(depth.bounds.is_some()); - if let Some(bounds) = depth.bounds { - info = info - .max_depth_bounds(bounds.max) - .min_depth_bounds(bounds.min); - } - } - - if let Some(stencil) = state.stencil { - info = info - .stencil_test_enable(true) - .front(stencil.front) - .back(stencil.back); - } - - info - }); - - let dynamic = desc.dynamic.map(|state| { - let info = vk::PipelineDynamicStateCreateInfo::default() - .flags(state.flags) - .dynamic_states(state.dynamic_states); - - info - }); - - let mut rendering = desc.rendering.map(|state| { - let info = vk::PipelineRenderingCreateInfo::default() - .color_attachment_formats(state.color_formats) - .depth_attachment_format(state.depth_format.unwrap_or_default()) - .stencil_attachment_format(state.stencil_format.unwrap_or_default()); - - info - }); - - fn option_to_ptr(option: &Option) -> *const T { - option - .as_ref() - .map(|t| t as *const T) - .unwrap_or(core::ptr::null()) - } - - let mut info = vk::GraphicsPipelineCreateInfo { - flags: desc.flags, - stage_count: stages.len() as u32, - p_stages: stages.as_ptr(), - p_vertex_input_state: option_to_ptr(&vertex_input), - p_input_assembly_state: option_to_ptr(&input_assembly), - p_tessellation_state: option_to_ptr(&tessellation), - p_viewport_state: option_to_ptr(&viewport), - p_rasterization_state: option_to_ptr(&rasterization), - p_multisample_state: option_to_ptr(&multisample), - p_depth_stencil_state: option_to_ptr(&depth_stencil), - p_color_blend_state: option_to_ptr(&color_blend), - p_dynamic_state: option_to_ptr(&dynamic), - layout: desc.layout.handle(), - render_pass: desc.render_pass.unwrap_or(vk::RenderPass::null()), - subpass: desc.subpass.unwrap_or(0), - base_pipeline_handle: desc - .base_pipeline - .map(|piepline| piepline.pipeline.handle()) - .unwrap_or(vk::Pipeline::null()), - ..Default::default() - }; - - if let Some(rendering) = rendering.as_mut() { - info = info.push_next(rendering) - } - - unsafe { - device.dev().create_graphics_pipelines( - vk::PipelineCache::null(), - core::slice::from_ref(&info), - None, - ) - } - } - }; - - let pipeline = match result { - Ok(pipelines) => pipelines[0], - Err((pipelines, error)) => { - tracing::error!("failed to create pipelines with :{error}"); - for pipeline in pipelines { - unsafe { - device.dev().destroy_pipeline(pipeline, None); - } - } - return Err(error.into()); - } + let pipeline = unsafe { + device + .dev() + .create_compute_pipelines( + device.pipeline_cache.raw, + core::slice::from_ref(info), + None, + ) + // It's cool to just take the first one and ignore any + // potentially created pipelines since we know there wont be any + // others. + .map_err(|(_, err)| err)?[0] }; Ok(Self { - pipeline: DeviceOwnedDebugObject::new(device, pipeline, name)?, - bind_point, + pipeline: DeviceObject::new(pipeline, device, desc.name), + bind_point: vk::PipelineBindPoint::COMPUTE, }) } - pub fn handle(&self) -> vk::Pipeline { - self.pipeline.handle() + pub fn new_graphics(device: Device, desc: GraphicsPipelineDesc) -> crate::Result { + let stages = desc + .shader_stages + .iter() + .map(|stage| stage.as_create_info()) + .collect::>(); + + let vertex_input = desc.vertex_input.map(|vertex| { + vk::PipelineVertexInputStateCreateInfo::default() + .vertex_attribute_descriptions(vertex.attributes) + .vertex_binding_descriptions(vertex.bindings) + }); + let input_assembly = desc.input_assembly.map(|state| { + vk::PipelineInputAssemblyStateCreateInfo::default() + .primitive_restart_enable(state.primitive_restart) + .topology(state.topology) + }); + let tessellation = desc.tessellation.map(|state| { + vk::PipelineTessellationStateCreateInfo::default() + .flags(state.flags) + .patch_control_points(state.patch_control_points) + }); + let viewport = desc.viewport.map(|state| { + let mut info = vk::PipelineViewportStateCreateInfo::default() + .scissor_count(state.num_scissors) + .viewport_count(state.num_viewports); + if let Some(viewports) = state.viewports { + info = info.viewports(viewports); + } + if let Some(scissors) = state.scissors { + info = info.scissors(scissors); + } + + info + }); + + let rasterization = desc.rasterization.map(|state| { + let mut info = vk::PipelineRasterizationStateCreateInfo::default() + .line_width(state.line_width) + .cull_mode(state.cull_mode) + .polygon_mode(state.polygon_mode) + .rasterizer_discard_enable(state.discard_enable) + .depth_clamp_enable(state.depth_clamp_enable); + + if let Some(depth_bias) = state.depth_bias { + info = info + .depth_bias_enable(true) + .depth_bias_clamp(depth_bias.clamp) + .depth_bias_constant_factor(depth_bias.constant_factor) + .depth_bias_slope_factor(depth_bias.slope_factor); + } + + info + }); + + let multisample = desc.multisample.map(|state| { + let info = vk::PipelineMultisampleStateCreateInfo::default() + .flags(state.flags) + .min_sample_shading(state.min_sample_shading) + .rasterization_samples(state.rasterization_samples) + .sample_mask(state.sample_mask) + .sample_shading_enable(state.sample_shading_enable) + .alpha_to_coverage_enable(state.alpha_to_coverage_enable) + .alpha_to_one_enable(state.alpha_to_one_enable); + + info + }); + + let color_blend = desc.color_blend.map(|state| { + let info = vk::PipelineColorBlendStateCreateInfo::default() + .flags(state.flags) + .attachments(state.attachments) + .blend_constants(state.blend_constants) + .logic_op(state.logic_op.unwrap_or(Default::default())) + .logic_op_enable(state.logic_op.is_some()); + + info + }); + + let depth_stencil = desc.depth_stencil.map(|state| { + let mut info = vk::PipelineDepthStencilStateCreateInfo::default().flags(state.flags); + + if let Some(depth) = state.depth { + info = info + .depth_compare_op(depth.compare_op.unwrap_or(vk::CompareOp::default())) + .depth_test_enable(depth.compare_op.is_some()) + .depth_write_enable(depth.write_enable) + .depth_bounds_test_enable(depth.bounds.is_some()); + if let Some(bounds) = depth.bounds { + info = info + .max_depth_bounds(bounds.max) + .min_depth_bounds(bounds.min); + } + } + + if let Some(stencil) = state.stencil { + info = info + .stencil_test_enable(true) + .front(stencil.front) + .back(stencil.back); + } + + info + }); + + let dynamic = desc.dynamic.map(|state| { + let info = vk::PipelineDynamicStateCreateInfo::default() + .flags(state.flags) + .dynamic_states(state.dynamic_states); + + info + }); + + let mut rendering = desc.rendering.map(|state| { + let info = vk::PipelineRenderingCreateInfo::default() + .color_attachment_formats(state.color_formats) + .depth_attachment_format(state.depth_format.unwrap_or_default()) + .stencil_attachment_format(state.stencil_format.unwrap_or_default()); + + info + }); + + fn option_to_ptr(option: &Option) -> *const T { + option + .as_ref() + .map(|t| t as *const T) + .unwrap_or(core::ptr::null()) + } + + let mut info = vk::GraphicsPipelineCreateInfo { + flags: desc.flags, + stage_count: stages.len() as u32, + p_stages: stages.as_ptr(), + p_vertex_input_state: option_to_ptr(&vertex_input), + p_input_assembly_state: option_to_ptr(&input_assembly), + p_tessellation_state: option_to_ptr(&tessellation), + p_viewport_state: option_to_ptr(&viewport), + p_rasterization_state: option_to_ptr(&rasterization), + p_multisample_state: option_to_ptr(&multisample), + p_depth_stencil_state: option_to_ptr(&depth_stencil), + p_color_blend_state: option_to_ptr(&color_blend), + p_dynamic_state: option_to_ptr(&dynamic), + layout: desc.layout.handle(), + render_pass: desc.render_pass.unwrap_or(vk::RenderPass::null()), + subpass: desc.subpass.unwrap_or(0), + base_pipeline_handle: desc + .base_pipeline + .map(|piepline| *piepline.pipeline) + .unwrap_or(vk::Pipeline::null()), + ..Default::default() + }; + + if let Some(rendering) = rendering.as_mut() { + info = info.push_next(rendering) + } + + let pipeline = unsafe { + device + .dev() + .create_graphics_pipelines( + device.pipeline_cache.raw, + core::slice::from_ref(&info), + None, + ) + // It's cool to just take the first one and ignore any + // potentially created pipelines since we know there wont be any + // others. + .map_err(|(_, err)| err)?[0] + }; + + Ok(Self { + pipeline: DeviceObject::new(pipeline, device, desc.name), + bind_point: vk::PipelineBindPoint::GRAPHICS, + }) + } + + pub fn raw(&self) -> vk::Pipeline { + *self.pipeline } pub fn bind_point(&self) -> vk::PipelineBindPoint { self.bind_point } } + +pub(crate) mod pipeline_cache { + use ash::vk; + + use ash::Device; + + use crate::PhysicalDeviceInfo; + + pub struct PipelineCache { + key: u128, + pub(crate) raw: vk::PipelineCache, + } + + impl PipelineCache { + const MAGIC: [u8; 4] = *b"VYPC"; + const KEY_VERSION: u32 = 1; + const PATH: &'static str = "pipeline_cache.bin"; + fn calculate_key(adapter: &PhysicalDeviceInfo) -> u128 { + use md5::Digest; + let mut hasher = md5::Md5::new(); + let props = &adapter.properties; + hasher.update(bytemuck::bytes_of(&[ + props.core.vendor_id, + props.core.api_version, + props.core.device_id, + props.core.driver_version, + ])); + u128::from_le_bytes(hasher.finalize().into()) + } + + fn load_from_disk(key: u128) -> Option<(u128, Vec)> { + use std::io::Read; + + let file = std::fs::File::open(Self::PATH).ok()?; + let mut reader = std::io::BufReader::new(file); + let mut magic = [0; 4]; + reader.read_exact(&mut magic).ok()?; + if magic != Self::MAGIC { + return None; + } + + let mut version = 0; + reader + .read_exact(bytemuck::bytes_of_mut(&mut version)) + .ok()?; + if version != Self::KEY_VERSION { + return None; + } + + let mut disk_key = 0; + reader + .read_exact(bytemuck::bytes_of_mut(&mut disk_key)) + .ok()?; + if disk_key != key { + return None; + } + + let mut data = Vec::new(); + reader.read_to_end(&mut data).ok()?; + + Some((key, data)) + } + + fn write_to_disk(key: u128, data: &[u8]) -> std::io::Result<()> { + use std::io::Write; + + let file = std::fs::File::create(Self::PATH)?; + let mut writer = std::io::BufWriter::new(file); + writer.write_all(&Self::MAGIC)?; + writer.write_all(bytemuck::bytes_of(&Self::KEY_VERSION))?; + writer.write_all(bytemuck::bytes_of(&key))?; + writer.write_all(data)?; + Ok(()) + } + + pub fn new(device: &Device, adapter: &PhysicalDeviceInfo) -> crate::Result { + let key = Self::calculate_key(adapter); + let data = Self::load_from_disk(key).map(|(key, data)| { + tracing::info!("loaded pipeline cache from disk with key {key:x}"); + data + }); + + let info = vk::PipelineCacheCreateInfo::default() + .flags(vk::PipelineCacheCreateFlags::EXTERNALLY_SYNCHRONIZED) + .initial_data(data.as_deref().unwrap_or_default()); + + let cache = unsafe { device.create_pipeline_cache(&info, None)? }; + + Ok(Self { key, raw: cache }) + } + + pub fn export(&self, device: &ash::Device) -> crate::Result> { + let data = unsafe { device.get_pipeline_cache_data(self.raw)? }; + Ok(data) + } + } +} diff --git a/crates/renderer/src/rendering/mod.rs b/crates/renderer/src/rendering/mod.rs index 7743b84..f40c39c 100644 --- a/crates/renderer/src/rendering/mod.rs +++ b/crates/renderer/src/rendering/mod.rs @@ -201,9 +201,9 @@ impl Wireframe { "crates/renderer/shaders/wireframe.spv", )?; - let pipeline = pipeline::Pipeline::new( + let pipeline = pipeline::Pipeline::new_graphics( device.clone(), - pipeline::PipelineDesc::Graphics(pipeline::GraphicsPipelineDesc { + pipeline::GraphicsPipelineDesc { flags: Default::default(), name: Some("wireframe-pipeline".into()), shader_stages: &[ @@ -298,7 +298,7 @@ impl Wireframe { dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR], ..Default::default() }), - }), + }, )?; Ok((pipeline, pipeline_layout))