cranpose_render_wgpu/
pipeline_disk_cache.rs1use std::{
2 path::{Path, PathBuf},
3 sync::OnceLock,
4};
5
6use web_time::Instant;
7
8use crate::debug_toggles::DebugToggle;
9
10static DISK_CACHE: DebugToggle = DebugToggle::new("CRANPOSE_PIPELINE_DISK_CACHE");
11
12fn disk_cache_enabled() -> bool {
13 !DISK_CACHE.equals("0")
14}
15
16static HOST_FILE: OnceLock<PathBuf> = OnceLock::new();
17
18pub fn set_file_path(path: PathBuf) {
26 let _ = HOST_FILE.set(path);
27}
28
29pub(crate) fn file_path() -> Option<PathBuf> {
30 if !disk_cache_enabled() {
31 return None;
32 }
33 match crate::debug_toggles::debug_toggle_os("CRANPOSE_PIPELINE_CACHE_FILE") {
34 Some(path) if !path.is_empty() => Some(PathBuf::from(path)),
35 _ => HOST_FILE.get().cloned(),
36 }
37}
38
39pub(crate) fn load(device: &wgpu::Device) -> Option<wgpu::PipelineCache> {
40 if !device.features().contains(wgpu::Features::PIPELINE_CACHE) {
41 log::info!(
42 "[pipeline-cache] not offered by {:?}; compiled pipelines persist only as far \
43 as the driver's own cache does",
44 device.adapter_info().backend
45 );
46 return None;
47 }
48 let path = file_path();
49 let data = path.as_deref().and_then(|path| match std::fs::read(path) {
50 Ok(bytes) => Some(bytes),
51 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
52 Err(error) => {
53 log::warn!("[pipeline-cache] unreadable {path:?}: {error}");
54 None
55 }
56 });
57 let loaded = data.as_ref().map(Vec::len);
58 #[allow(unsafe_code)]
61 let cache = unsafe {
62 device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
63 label: Some("cranpose pipeline disk cache"),
64 data: data.as_deref(),
65 fallback: true,
66 })
67 };
68 match loaded {
69 Some(bytes) => log::info!("[pipeline-cache] loaded {bytes} B from disk"),
70 None => log::info!("[pipeline-cache] cold (no blob on disk)"),
71 }
72 Some(cache)
73}
74
75pub(crate) fn persist(cache: &wgpu::PipelineCache, path: &Path) {
76 let started = Instant::now();
77 let Some(data) = cache.get_data() else {
78 return;
79 };
80 if let Ok(existing) = std::fs::read(path)
81 && existing == data
82 {
83 return;
84 }
85 if let Some(parent) = path.parent()
86 && let Err(error) = std::fs::create_dir_all(parent)
87 {
88 log::warn!("[pipeline-cache] create_dir_all {parent:?}: {error}");
89 return;
90 }
91 let tmp = path.with_extension("tmp");
92 let written = std::fs::write(&tmp, &data).and_then(|()| std::fs::rename(&tmp, path));
93 match written {
94 Ok(()) => log::info!(
95 "[pipeline-cache] persisted {} B in {:.1} ms",
96 data.len(),
97 crate::render::instant_ms(started, Instant::now()),
98 ),
99 Err(error) => log::warn!("[pipeline-cache] write {path:?}: {error}"),
100 }
101}
102
103#[derive(Default)]
108struct PersistWatch {
109 persisted: u64,
110 seen: u64,
111}
112
113impl PersistWatch {
114 fn observe(&mut self, created: u64) -> bool {
115 let quiet = created == self.seen;
116 self.seen = created;
117 if quiet && created != self.persisted {
118 self.persisted = created;
119 return true;
120 }
121 false
122 }
123}
124
125const PERSIST_TICK: std::time::Duration = std::time::Duration::from_secs(2);
126
127pub(crate) fn spawn_persist_watcher(cache: wgpu::PipelineCache) {
128 let Some(path) = file_path() else {
129 return;
130 };
131 let spawned = std::thread::Builder::new()
132 .name("cranpose-pl-cache".into())
133 .spawn(move || {
134 let mut watch = PersistWatch::default();
135 loop {
136 std::thread::sleep(PERSIST_TICK);
137 if watch.observe(
138 crate::render::pipelines_created()
139 + crate::render::pipelines_created_off_frame(),
140 ) {
141 persist(&cache, &path);
142 }
143 }
144 });
145 if let Err(error) = spawned {
146 log::warn!("[pipeline-cache] persist thread failed to spawn: {error}");
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::PersistWatch;
153
154 #[test]
155 fn a_burst_of_pipelines_persists_once_after_it_goes_quiet() {
156 let mut watch = PersistWatch::default();
157 assert!(!watch.observe(0));
158 assert!(!watch.observe(3), "still growing");
159 assert!(!watch.observe(5), "still growing");
160 assert!(watch.observe(5), "quiet for a tick with new pipelines");
161 assert!(!watch.observe(5), "written already");
162 }
163
164 #[test]
165 fn a_pipeline_reached_late_in_a_session_persists_too() {
166 let mut watch = PersistWatch::default();
167 watch.observe(19);
168 assert!(watch.observe(19));
169 for _ in 0..20 {
170 assert!(!watch.observe(19));
171 }
172 assert!(!watch.observe(20));
173 assert!(watch.observe(20));
174 }
175}