Skip to main content

frust_gpu/
pipeline_cache.rs

1//! Framing + validation for a persisted `wgpu::PipelineCache` blob.
2//!
3//! A `wgpu::PipelineCache` lets a driver reuse the machine code it compiled for
4//! a device's render pipelines across process launches, cutting warm-start
5//! shader/pipeline compilation to near zero. wgpu only implements it on Vulkan
6//! (any Vulkan adapter — Android, Linux, Windows-on-Vulkan); Metal/DX12 drivers
7//! manage their own caches, so this whole path is silently inert there (the
8//! feature is absent → no cache is ever created, and
9//! [`crate::pipeline::PipelineCache`] is simply built with `None`).
10//!
11//! The shell owns file I/O and hands the framework an opaque `Vec<u8>` blob (no
12//! `serde`/file dependency lives here). Before that blob is ever fed to the
13//! **unsafe** [`wgpu::Device::create_pipeline_cache`], it is wrapped in a small
14//! self-describing header — a magic tag plus the adapter fingerprint the data
15//! was produced on — checked here ([`unframe`]). A mismatch (wrong magic,
16//! different adapter/driver, truncated blob) is treated as "no cache" and the
17//! caller starts from an empty cache instead of handing the driver bytes from a
18//! foreign device. This is belt-and-braces on top of wgpu's own
19//! `create_pipeline_cache(fallback: true)`, which already falls back to an empty
20//! cache for mismatched data rather than misbehaving.
21//!
22//! This is the **only** copy of the framing: `frust-render` used to carry an
23//! identical one (kept in lockstep by a drift-guard test) and now re-exports
24//! this module instead, so the on-disk layout has exactly one definition. A
25//! blob persisted by any earlier frust build still validates here unchanged —
26//! bump this module's `MAGIC` if the layout ever does change, so an old blob
27//! is rejected
28//! rather than misparsed.
29
30/// Magic tag prefixing every framed blob: `Frust PipeLine Cache wgpu v1`.
31/// Bumped if the framing layout below ever changes so an old on-disk blob is
32/// rejected rather than misparsed.
33const MAGIC: [u8; 8] = *b"FKPLCwg1";
34
35/// A compact identity string for the adapter a cache blob was produced on.
36///
37/// A `wgpu::PipelineCache` is only ever valid for the same adapter+driver that
38/// produced it (wgpu validates this internally too, but framing the key lets us
39/// reject a foreign blob before the unsafe API is ever called). The `driver` /
40/// `driver_info` fields are included so a driver update — which can invalidate
41/// the compiled machine code — changes the key and discards the stale blob.
42#[must_use]
43pub fn adapter_cache_key(info: &wgpu::AdapterInfo) -> String {
44    format!(
45        "{:?}|{}|{}|{}|{:#x}|{:#x}",
46        info.backend, info.name, info.driver, info.driver_info, info.vendor, info.device
47    )
48}
49
50/// Wraps a raw `PipelineCache::get_data()` payload in the validated header
51/// [`unframe`] checks, tagging it with the adapter it was produced on.
52///
53/// Layout: `MAGIC (8)` · `key_len: u32 LE` · `key bytes` · `payload_len: u32 LE`
54/// · `payload bytes`.
55#[must_use]
56pub fn frame(adapter_key: &str, payload: &[u8]) -> Vec<u8> {
57    let key = adapter_key.as_bytes();
58    let mut out = Vec::with_capacity(MAGIC.len() + 8 + key.len() + payload.len());
59    out.extend_from_slice(&MAGIC);
60    out.extend_from_slice(&(key.len() as u32).to_le_bytes());
61    out.extend_from_slice(key);
62    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
63    out.extend_from_slice(payload);
64    out
65}
66
67/// Validates a framed blob against `adapter_key` and returns the inner payload,
68/// or `None` if the blob is not a well-formed frame for *this* adapter.
69///
70/// Rejects (returns `None`) on: a truncated/malformed frame, a wrong magic tag,
71/// an adapter-key mismatch (foreign device or a post-driver-update key), or a
72/// declared payload length that disagrees with the trailing bytes. A `None`
73/// here means "start from an empty cache", never a panic.
74#[must_use]
75pub fn unframe<'a>(blob: &'a [u8], adapter_key: &str) -> Option<&'a [u8]> {
76    let mut rest = blob;
77
78    let magic = take(&mut rest, MAGIC.len())?;
79    if magic != MAGIC {
80        return None;
81    }
82
83    let key_len = take_u32(&mut rest)? as usize;
84    let key = take(&mut rest, key_len)?;
85    if key != adapter_key.as_bytes() {
86        return None;
87    }
88
89    let payload_len = take_u32(&mut rest)? as usize;
90    let payload = take(&mut rest, payload_len)?;
91    // A trailing-byte surplus means the frame is malformed — reject it.
92    if !rest.is_empty() {
93        return None;
94    }
95    Some(payload)
96}
97
98/// Splits `n` bytes off the front of `*buf`, advancing it; `None` if short.
99fn take<'a>(buf: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
100    if buf.len() < n {
101        return None;
102    }
103    let (head, tail) = buf.split_at(n);
104    *buf = tail;
105    Some(head)
106}
107
108/// Reads a little-endian `u32` off the front of `*buf`, advancing it.
109fn take_u32(buf: &mut &[u8]) -> Option<u32> {
110    let bytes = take(buf, 4)?;
111    Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
112}