atagen

monolith

public
Star0
Watch0

Commit 43e0e8e33c3e

tarball

atagen <boss@atagen.co>2026-10-19 07:56 UTC

2 files changed+143-4
render-vk: persist a pipeline cache and warm pipelines at startup

Pipelines::get compiled on first use with a null VkPipelineCache, so the first spawn animation paid the driver compiler mid-frame and every run paid it again. The cache is loaded from $XDG_CACHE_HOME/monolith/pipelines.bin after its header is checked against the device, since some drivers crash on a foreign cache instead of rejecting it, and the common pipelines are built in VulkanRenderer::with_node.
Modified file: crates/render-vk/src/pipeline.rs+140 additions−3 deletions
@@ -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+ shared
99+ .device
100+ .instance
101+ .get_physical_device_properties(shared.device.physical)
102+ };
103+ let loaded = cache_path
104+ .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, so
131+ /// 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.shared
157+ .device
158+ .device
159+ .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 = path
173+ .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 this
276+/// device. The spec lets a driver reject a foreign cache but some crash on
277+/// 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+ data
309+ }
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+}
Modified file: crates/render-vk/src/renderer/mod.rs+3 additions−1 deletions
@@ -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),