Commit 43e0e8e33c3e
tarball2 files changed+143-4
@@ -1,8 +1,12 @@
11
use crate::Error;22
use crate::device::Shared;3
+use crate::format::{SUPPORTED, vk_format};34
use ash::vk;45
use std::collections::HashMap;6
+use std::path::PathBuf;57
use std::sync::Arc;8
+use std::sync::atomic::{AtomicU32, Ordering};9
+use std::time::Instant;610
711
/// Push constants shared by every shader, see `shaders/push.glsl`.812
/// `dst` is the quad in normalized device coordinates as (x, y, w, h),@@ -44,6 +48,9 @@ pub(crate) struct Pipelines {
4448
texture: vk::ShaderModule,4549
effect: vk::ShaderModule,4650
cache: HashMap<(vk::Format, Kind), vk::Pipeline>,51
+ vk_cache: vk::PipelineCache,52
+ cache_path: Option<PathBuf>,53
+ loaded_len: usize,4754
}4855
4956
const QUAD_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/quad.vert.spv"));@@ -86,6 +93,25 @@ impl Pipelines {
8693
d.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)?8794
})8895
};96
+ let cache_path = cache_path();97
+ let props = unsafe {98
+ shared99
+ .device100
+ .instance101
+ .get_physical_device_properties(shared.device.physical)102
+ };103
+ let loaded = cache_path104
+ .as_ref()105
+ .and_then(|p| std::fs::read(p).ok())106
+ .filter(|data| header_matches(data, &props))107
+ .unwrap_or_default();108
+ let create = |data: &[u8]| unsafe {109
+ d.create_pipeline_cache(110
+ &vk::PipelineCacheCreateInfo::default().initial_data(data),111
+ None,112
+ )113
+ };114
+ let vk_cache = create(&loaded).or_else(|_| create(&[])).unwrap_or_default();89115
Ok(Self {90116
shared: shared.clone(),91117
set_layout,@@ -95,9 +121,65 @@ impl Pipelines {
95121
texture: module(TEXTURE_FRAG)?,96122
effect: module(EFFECT_FRAG)?,97123
cache: HashMap::new(),124
+ vk_cache,125
+ cache_path,126
+ loaded_len: loaded.len(),98127
})99128
}100129
130
+ /// Compiles the pipelines a frame draws with for every render format, so131
+ /// the first spawn animation does not stall on the driver compiler.132
+ pub fn warm(&mut self) {133
+ let began = Instant::now();134
+ for fourcc in SUPPORTED {135
+ let Some(format) = vk_format(fourcc) else {136
+ continue;137
+ };138
+ for kind in [Kind::Solid, Kind::Texture, Kind::Effect] {139
+ if let Err(e) = self.get(format.format, kind) {140
+ tracing::warn!(?kind, "cannot warm a pipeline: {e}");141
+ return;142
+ }143
+ }144
+ }145
+ tracing::debug!(elapsed = ?began.elapsed(), "warmed pipelines");146
+ }147
+148
+ fn save_cache(&self) {149
+ let Some(path) = &self.cache_path else {150
+ return;151
+ };152
+ if self.vk_cache == vk::PipelineCache::null() {153
+ return;154
+ }155
+ let Ok(data) = (unsafe {156
+ self.shared157
+ .device158
+ .device159
+ .get_pipeline_cache_data(self.vk_cache)160
+ }) else {161
+ return;162
+ };163
+ if data.len() == self.loaded_len {164
+ return;165
+ }166
+ static WRITERS: AtomicU32 = AtomicU32::new(0);167
+ let tmp = path.with_extension(format!(168
+ "{}.{}.tmp",169
+ std::process::id(),170
+ WRITERS.fetch_add(1, Ordering::Relaxed)171
+ ));172
+ let written = path173
+ .parent()174
+ .map_or(Ok(()), std::fs::create_dir_all)175
+ .and_then(|()| std::fs::write(&tmp, &data))176
+ .and_then(|()| std::fs::rename(&tmp, path));177
+ if let Err(e) = written {178
+ tracing::debug!("cannot save the pipeline cache: {e}");179
+ let _ = std::fs::remove_file(&tmp);180
+ }181
+ }182
+101183
pub fn get(&mut self, format: vk::Format, kind: Kind) -> Result<vk::Pipeline, Error> {102184
if let Some(&p) = self.cache.get(&(format, kind)) {103185
return Ok(p);@@ -156,9 +238,8 @@ impl Pipelines {
156238
.dynamic_state(&dynamic)157239
.layout(self.layout)158240
.push_next(&mut rendering);159
- let pipeline =160
- unsafe { d.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) }161
- .map_err(|(_, e)| e)?[0];241
+ let pipeline = unsafe { d.create_graphics_pipelines(self.vk_cache, &[info], None) }242
+ .map_err(|(_, e)| e)?[0];162243
self.cache.insert((format, kind), pipeline);163244
Ok(pipeline)164245
}@@ -166,11 +247,13 @@ impl Pipelines {
166247
167248
impl Drop for Pipelines {168249
fn drop(&mut self) {250
+ self.save_cache();169251
let d = &self.shared.device.device;170252
unsafe {171253
for (_, p) in self.cache.drain() {172254
d.destroy_pipeline(p, None);173255
}256
+ d.destroy_pipeline_cache(self.vk_cache, None);174257
d.destroy_shader_module(self.vert, None);175258
d.destroy_shader_module(self.solid, None);176259
d.destroy_shader_module(self.texture, None);@@ -180,3 +263,57 @@ impl Drop for Pipelines {
180263
}181264
}182265
}266
+267
+fn cache_path() -> Option<PathBuf> {268
+ let base = std::env::var_os("XDG_CACHE_HOME")269
+ .filter(|v| !v.is_empty())270
+ .map(PathBuf::from)271
+ .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))?;272
+ Some(base.join("monolith").join("pipelines.bin"))273
+}274
+275
+/// Whether `data` opens with a cache header written by this driver on this276
+/// device. The spec lets a driver reject a foreign cache but some crash on277
+/// one instead, so the header is checked first.278
+fn header_matches(data: &[u8], props: &vk::PhysicalDeviceProperties) -> bool {279
+ let word = |at: usize| {280
+ data.get(at..at + 4)281
+ .and_then(|b| b.try_into().ok())282
+ .map(u32::from_ne_bytes)283
+ };284
+ word(0).is_some_and(|size| size >= 32 && size as usize <= data.len())285
+ && word(4) == Some(vk::PipelineCacheHeaderVersion::ONE.as_raw() as u32)286
+ && word(8) == Some(props.vendor_id)287
+ && word(12) == Some(props.device_id)288
+ && data.get(16..32) == Some(&props.pipeline_cache_uuid[..])289
+}290
+291
+#[cfg(test)]292
+mod tests {293
+ use super::*;294
+295
+ fn props() -> vk::PhysicalDeviceProperties {296
+ vk::PhysicalDeviceProperties {297
+ vendor_id: 0x10de,298
+ device_id: 0x2487,299
+ pipeline_cache_uuid: [7; 16],300
+ ..Default::default()301
+ }302
+ }303
+304
+ fn header(vendor: u32, uuid: u8) -> Vec<u8> {305
+ let words = [32u32, 1, vendor, 0x2487];306
+ let mut data: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();307
+ data.extend([uuid; 16]);308
+ data309
+ }310
+311
+ #[test]312
+ fn cache_header_must_match_the_device_and_driver() {313
+ assert!(header_matches(&header(0x10de, 7), &props()));314
+ assert!(!header_matches(&header(0x1002, 7), &props()));315
+ assert!(!header_matches(&header(0x10de, 8), &props()));316
+ assert!(!header_matches(&header(0x10de, 7)[..20], &props()));317
+ assert!(!header_matches(&[], &props()));318
+ }319
+}@@ -143,13 +143,15 @@ impl VulkanRenderer {
143143
)?144144
};145145
}146
+ let mut pipelines = Pipelines::new(&shared)?;147
+ pipelines.warm();146148
Ok(Self {147149
pending: Vec::new(),148150
uploads: Vec::new(),149151
upload_bytes: 0,150152
dmabuf_textures: HashMap::new(),151153
dmabuf_targets: HashMap::new(),152
- pipelines: Pipelines::new(&shared)?,154
+ pipelines,153155
pool,154156
spare_cmds: Vec::new(),155157
submissions: Cell::new(0),