pipeline cache

This commit is contained in:
janis 2026-04-04 15:46:17 +02:00
parent cf3244197e
commit 44ff4c4839
7 changed files with 323 additions and 235 deletions

View file

@ -52,6 +52,9 @@ petgraph = "0.7"
itertools = "0.14.0" itertools = "0.14.0"
ahash = "0.8" 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" parking_lot = "0.12.3"
tokio = "1.42" tokio = "1.42"

View file

@ -26,6 +26,8 @@ vk-mem = { workspace = true }
gpu-allocator = { workspace = true } gpu-allocator = { workspace = true }
rectangle-pack = { workspace = true } rectangle-pack = { workspace = true }
md-5 = { workspace = true }
raw-window-handle = { workspace = true } raw-window-handle = { workspace = true }
egui = { workspace = true , features = ["bytemuck"]} egui = { workspace = true , features = ["bytemuck"]}
egui_winit_platform = { workspace = true } egui_winit_platform = { workspace = true }

View file

@ -547,7 +547,7 @@ pub mod traits {
self.device().dev().cmd_bind_pipeline( self.device().dev().cmd_bind_pipeline(
self.handle(), self.handle(),
pipeline.bind_point(), pipeline.bind_point(),
pipeline.handle(), pipeline.raw(),
); );
} }
} }

View file

@ -17,6 +17,7 @@ use raw_window_handle::RawDisplayHandle;
use crate::{ use crate::{
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result, Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
pipeline::pipeline_cache::PipelineCache,
queue::{DeviceQueueInfos, DeviceQueues, Queue}, queue::{DeviceQueueInfos, DeviceQueues, Queue},
sync::{self, BinarySemaphore, TimelineSemaphore}, sync::{self, BinarySemaphore, TimelineSemaphore},
}; };
@ -109,6 +110,9 @@ pub struct DeviceInner {
pub(crate) device_extensions: DeviceExtensions, pub(crate) device_extensions: DeviceExtensions,
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enabled_extensions: Vec<&'static CStr>, pub(crate) enabled_extensions: Vec<&'static CStr>,
pub(crate) pipeline_cache: PipelineCache,
_drop: DeviceDrop, _drop: DeviceDrop,
} }
@ -397,6 +401,7 @@ impl PhysicalDeviceInfo {
raw: device.clone(), raw: device.clone(),
alloc2: Mutex::new(alloc2), alloc2: Mutex::new(alloc2),
instance: instance.clone(), instance: instance.clone(),
pipeline_cache: PipelineCache::new(&device, &self)?,
adapter: self, adapter: self,
queues: device_queues, queues: device_queues,
device_extensions, device_extensions,

View file

@ -809,9 +809,9 @@ impl EguiState {
"crates/renderer/shaders/egui_vert.spv", "crates/renderer/shaders/egui_vert.spv",
)?; )?;
let pipeline = pipeline::Pipeline::new( let pipeline = pipeline::Pipeline::new_graphics(
device.clone(), device.clone(),
pipeline::PipelineDesc::Graphics(pipeline::GraphicsPipelineDesc { pipeline::GraphicsPipelineDesc {
flags: Default::default(), flags: Default::default(),
name: Some("egui-pipeline".into()), name: Some("egui-pipeline".into()),
shader_stages: &[ shader_stages: &[
@ -904,7 +904,7 @@ impl EguiState {
dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR], dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR],
..Default::default() ..Default::default()
}), }),
}), },
)?; )?;
Ok(Self { Ok(Self {

View file

@ -4,7 +4,7 @@ use ash::{ext, prelude::*, vk};
use crate::{ use crate::{
define_device_owned_handle, define_device_owned_handle,
device::{Device, DeviceOwnedDebugObject}, device::{Device, DeviceHandle, DeviceObject},
make_extension, make_extension,
}; };
@ -40,12 +40,6 @@ pub struct PipelineLayoutDesc<'a> {
pub name: Option<Cow<'static, str>>, pub name: Option<Cow<'static, str>>,
} }
#[derive(Debug)]
pub enum PipelineDesc<'a> {
Compute(ComputePipelineDesc<'a>),
Graphics(GraphicsPipelineDesc<'a>),
}
#[derive(Debug)] #[derive(Debug)]
pub struct ComputePipelineDesc<'a> { pub struct ComputePipelineDesc<'a> {
pub flags: vk::PipelineCreateFlags, pub flags: vk::PipelineCreateFlags,
@ -251,18 +245,17 @@ impl DescriptorPool {
let info = &vk::DescriptorSetAllocateInfo::default() let info = &vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(self.handle()) .descriptor_pool(self.handle())
.set_layouts(&layouts); .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) { for (&set, desc) in sets.iter().zip(descs) {
if let Some(name) = desc.name.as_ref() { 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) Ok(sets)
} }
// pub fn free(&self) {}
#[allow(dead_code)] #[allow(dead_code)]
pub fn reset(&self) -> VkResult<()> { pub fn reset(&self) -> VkResult<()> {
unsafe { unsafe {
@ -474,18 +467,13 @@ impl ShaderModule {
#[derive(Debug)] #[derive(Debug)]
pub struct Pipeline { pub struct Pipeline {
pipeline: DeviceOwnedDebugObject<vk::Pipeline>, pipeline: DeviceObject<vk::Pipeline>,
bind_point: vk::PipelineBindPoint, bind_point: vk::PipelineBindPoint,
} }
impl Drop for Pipeline { impl DeviceHandle for vk::Pipeline {
fn drop(&mut self) { unsafe fn destroy(&mut self, device: &Device) {
unsafe { unsafe { device.raw.destroy_pipeline(*self, None) };
self.pipeline
.dev()
.dev()
.destroy_pipeline(self.pipeline.handle(), None);
}
} }
} }
@ -500,35 +488,38 @@ impl ShaderStageDesc<'_> {
} }
impl Pipeline { impl Pipeline {
pub fn new(device: Device, desc: PipelineDesc) -> VkResult<Self> { pub fn new_compute(device: Device, desc: ComputePipelineDesc) -> crate::Result<Self> {
let name: Option<Cow<'static, str>>;
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() let info = &vk::ComputePipelineCreateInfo::default()
.flags(desc.flags) .flags(desc.flags)
.layout(desc.layout.handle()) .layout(desc.layout.handle())
.base_pipeline_handle( .base_pipeline_handle(
desc.base_pipeline desc.base_pipeline
.map(|p| p.handle()) .map(|p| p.raw())
.unwrap_or(vk::Pipeline::null()), .unwrap_or(vk::Pipeline::null()),
) )
.stage(desc.shader_stage.as_create_info()); .stage(desc.shader_stage.as_create_info());
unsafe { let pipeline = unsafe {
device.dev().create_compute_pipelines( device
vk::PipelineCache::null(), .dev()
.create_compute_pipelines(
device.pipeline_cache.raw,
core::slice::from_ref(info), core::slice::from_ref(info),
None, None,
) )
} // It's cool to just take the first one and ignore any
} // potentially created pipelines since we know there wont be any
PipelineDesc::Graphics(desc) => { // others.
name = desc.name; .map_err(|(_, err)| err)?[0]
bind_point = vk::PipelineBindPoint::GRAPHICS; };
Ok(Self {
pipeline: DeviceObject::new(pipeline, device, desc.name),
bind_point: vk::PipelineBindPoint::COMPUTE,
})
}
pub fn new_graphics(device: Device, desc: GraphicsPipelineDesc) -> crate::Result<Self> {
let stages = desc let stages = desc
.shader_stages .shader_stages
.iter() .iter()
@ -608,8 +599,7 @@ impl Pipeline {
}); });
let depth_stencil = desc.depth_stencil.map(|state| { let depth_stencil = desc.depth_stencil.map(|state| {
let mut info = let mut info = vk::PipelineDepthStencilStateCreateInfo::default().flags(state.flags);
vk::PipelineDepthStencilStateCreateInfo::default().flags(state.flags);
if let Some(depth) = state.depth { if let Some(depth) = state.depth {
info = info info = info
@ -676,7 +666,7 @@ impl Pipeline {
subpass: desc.subpass.unwrap_or(0), subpass: desc.subpass.unwrap_or(0),
base_pipeline_handle: desc base_pipeline_handle: desc
.base_pipeline .base_pipeline
.map(|piepline| piepline.pipeline.handle()) .map(|piepline| *piepline.pipeline)
.unwrap_or(vk::Pipeline::null()), .unwrap_or(vk::Pipeline::null()),
..Default::default() ..Default::default()
}; };
@ -685,39 +675,127 @@ impl Pipeline {
info = info.push_next(rendering) info = info.push_next(rendering)
} }
unsafe { let pipeline = unsafe {
device.dev().create_graphics_pipelines( device
vk::PipelineCache::null(), .dev()
.create_graphics_pipelines(
device.pipeline_cache.raw,
core::slice::from_ref(&info), core::slice::from_ref(&info),
None, 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]
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());
}
}; };
Ok(Self { Ok(Self {
pipeline: DeviceOwnedDebugObject::new(device, pipeline, name)?, pipeline: DeviceObject::new(pipeline, device, desc.name),
bind_point, bind_point: vk::PipelineBindPoint::GRAPHICS,
}) })
} }
pub fn handle(&self) -> vk::Pipeline { pub fn raw(&self) -> vk::Pipeline {
self.pipeline.handle() *self.pipeline
} }
pub fn bind_point(&self) -> vk::PipelineBindPoint { pub fn bind_point(&self) -> vk::PipelineBindPoint {
self.bind_point 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<u8>)> {
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<Self> {
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<Vec<u8>> {
let data = unsafe { device.get_pipeline_cache_data(self.raw)? };
Ok(data)
}
}
}

View file

@ -201,9 +201,9 @@ impl Wireframe {
"crates/renderer/shaders/wireframe.spv", "crates/renderer/shaders/wireframe.spv",
)?; )?;
let pipeline = pipeline::Pipeline::new( let pipeline = pipeline::Pipeline::new_graphics(
device.clone(), device.clone(),
pipeline::PipelineDesc::Graphics(pipeline::GraphicsPipelineDesc { pipeline::GraphicsPipelineDesc {
flags: Default::default(), flags: Default::default(),
name: Some("wireframe-pipeline".into()), name: Some("wireframe-pipeline".into()),
shader_stages: &[ shader_stages: &[
@ -298,7 +298,7 @@ impl Wireframe {
dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR], dynamic_states: &[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR],
..Default::default() ..Default::default()
}), }),
}), },
)?; )?;
Ok((pipeline, pipeline_layout)) Ok((pipeline, pipeline_layout))