use ash::vk::Handle;
use ash::{Device, vk};
use super::owned::{OwnedPipeline, VkDevice};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
static CURRENT: AtomicU64 = AtomicU64::new(0);
static STATE: Mutex<Option<Persisted>> = Mutex::new(None);
struct Persisted {
file: String,
written: usize,
warm: bool,
}
fn current() -> vk::PipelineCache {
vk::PipelineCache::from_raw(CURRENT.load(Ordering::Relaxed))
}
pub(in crate::vulkan) fn create_graphics_pipelines(
device: &VkDevice,
infos: &[vk::GraphicsPipelineCreateInfo],
) -> Result<Vec<OwnedPipeline>, vk::Result> {
let started = std::time::Instant::now();
let result = unsafe { device.create_graphics_pipelines(current(), infos, None) };
crate::pipeline_cache::note_creation(started.elapsed().as_micros() as u64);
own(device, result)
}
pub(in crate::vulkan) fn create_graphics_pipeline(
device: &VkDevice,
info: &vk::GraphicsPipelineCreateInfo,
) -> Result<OwnedPipeline, vk::Result> {
Ok(one(create_graphics_pipelines(
device,
std::slice::from_ref(info),
)?))
}
pub(in crate::vulkan) fn create_compute_pipelines(
device: &VkDevice,
infos: &[vk::ComputePipelineCreateInfo],
) -> Result<Vec<OwnedPipeline>, vk::Result> {
let started = std::time::Instant::now();
let result = unsafe { device.create_compute_pipelines(current(), infos, None) };
crate::pipeline_cache::note_creation(started.elapsed().as_micros() as u64);
own(device, result)
}
pub(in crate::vulkan) fn create_compute_pipeline(
device: &VkDevice,
info: &vk::ComputePipelineCreateInfo,
) -> Result<OwnedPipeline, vk::Result> {
Ok(one(create_compute_pipelines(
device,
std::slice::from_ref(info),
)?))
}
fn own(
device: &VkDevice,
result: Result<Vec<vk::Pipeline>, (Vec<vk::Pipeline>, vk::Result)>,
) -> Result<Vec<OwnedPipeline>, vk::Result> {
let wrap = |handles: Vec<vk::Pipeline>| -> Vec<OwnedPipeline> {
handles
.into_iter()
.filter(|h| *h != vk::Pipeline::null())
.map(|h| OwnedPipeline::new(device, h))
.collect()
};
match result {
Ok(handles) => Ok(wrap(handles)),
Err((partial, e)) => {
drop(wrap(partial));
Err(e)
}
}
}
fn one(mut pipelines: Vec<OwnedPipeline>) -> OwnedPipeline {
pipelines
.pop()
.expect("a successful one-info pipeline create returns one pipeline")
}
pub(in crate::vulkan) fn handle() -> vk::PipelineCache {
current()
}
pub(in crate::vulkan) fn install(device: &Device, props: &vk::PhysicalDeviceProperties) {
let mut state = STATE.lock().expect("pipeline cache state");
if state.is_some() {
return;
}
let file = file_name(&props.pipeline_cache_uuid);
let disk = crate::pipeline_cache::load(&file).filter(|blob| {
let ok = header_matches(
blob,
props.vendor_id,
props.device_id,
&props.pipeline_cache_uuid,
);
if !ok {
tracing::warn!("pipeline cache: {file} does not match this device, rebuilding cold");
crate::pipeline_cache::delete(&file);
}
ok
});
let info = vk::PipelineCacheCreateInfo::default().initial_data(disk.as_deref().unwrap_or(&[]));
let created = unsafe { device.create_pipeline_cache(&info, None) }.or_else(|e| {
tracing::warn!("pipeline cache: driver rejected {file} ({e}), rebuilding cold");
crate::pipeline_cache::delete(&file);
let empty = vk::PipelineCacheCreateInfo::default();
unsafe { device.create_pipeline_cache(&empty, None) }
});
match created {
Ok(cache) => {
let warm = disk.is_some();
let written = disk.as_ref().map_or(0, Vec::len);
CURRENT.store(cache.as_raw(), Ordering::Relaxed);
*state = Some(Persisted {
file,
written,
warm,
});
}
Err(e) => {
tracing::warn!("pipeline cache: create failed ({e}), pipelines build uncached");
}
}
}
pub(in crate::vulkan) fn disk_state() -> &'static str {
match &*STATE.lock().expect("pipeline cache state") {
Some(p) if p.warm => "warm",
Some(_) => "cold",
None => "absent",
}
}
pub(in crate::vulkan) fn serialize(device: &Device) {
let mut state = STATE.lock().expect("pipeline cache state");
let Some(persisted) = state.as_mut() else {
return;
};
let Ok(data) = (unsafe { device.get_pipeline_cache_data(current()) }) else {
return;
};
if crate::pipeline_cache::store_if_grown(&persisted.file, &data, persisted.written) {
persisted.written = data.len();
}
}
pub(in crate::vulkan) fn shutdown(device: &Device) {
serialize(device);
let mut state = STATE.lock().expect("pipeline cache state");
if state.take().is_some() {
let cache = current();
CURRENT.store(0, Ordering::Relaxed);
if cache != vk::PipelineCache::null() {
unsafe { device.destroy_pipeline_cache(cache, None) };
}
}
}
fn file_name(uuid: &[u8; vk::UUID_SIZE]) -> String {
let hex: String = uuid.iter().map(|b| format!("{b:02x}")).collect();
format!("vk-{hex}.bin")
}
fn header_matches(blob: &[u8], vendor_id: u32, device_id: u32, uuid: &[u8; 16]) -> bool {
if blob.len() < 32 {
return false;
}
let word = |at: usize| u32::from_le_bytes(blob[at..at + 4].try_into().expect("4 bytes"));
word(0) >= 32
&& word(4) == vk::PipelineCacheHeaderVersion::ONE.as_raw() as u32
&& word(8) == vendor_id
&& word(12) == device_id
&& blob[16..32] == uuid[..]
}
#[cfg(test)]
mod tests {
use super::*;
const UUID: [u8; 16] = *b"0123456789abcdef";
fn blob(length: u32, version: u32, vendor: u32, device: u32, uuid: [u8; 16]) -> Vec<u8> {
let mut b = Vec::new();
for word in [length, version, vendor, device] {
b.extend_from_slice(&word.to_le_bytes());
}
b.extend_from_slice(&uuid);
b.extend_from_slice(&[0xEE; 8]);
b
}
#[test]
fn a_matching_header_is_accepted() {
assert!(header_matches(
&blob(32, 1, 0x106b, 0xf01, UUID),
0x106b,
0xf01,
&UUID
));
}
#[test]
fn every_header_field_is_checked() {
let good = blob(32, 1, 7, 9, UUID);
assert!(header_matches(&good, 7, 9, &UUID));
assert!(
!header_matches(&blob(16, 1, 7, 9, UUID), 7, 9, &UUID),
"length"
);
assert!(
!header_matches(&blob(32, 2, 7, 9, UUID), 7, 9, &UUID),
"version"
);
assert!(!header_matches(&good, 8, 9, &UUID), "vendor");
assert!(!header_matches(&good, 7, 10, &UUID), "device");
assert!(!header_matches(&good, 7, 9, &[0u8; 16]), "uuid");
}
#[test]
fn a_truncated_blob_is_rejected() {
assert!(!header_matches(&[0u8; 31], 0, 0, &[0u8; 16]));
assert!(!header_matches(&[], 0, 0, &[0u8; 16]));
}
#[test]
fn the_file_name_is_per_device() {
assert_eq!(file_name(&UUID), "vk-30313233343536373839616263646566.bin");
assert_ne!(file_name(&UUID), file_name(&[0u8; 16]));
}
}