Skip to main content

frust_gpu/
shader.rs

1//! The WGSL shader module registry: [`ShaderLibrary`] and [`ShaderId`].
2//!
3//! Every shader the engine ever draws with is compiled into a
4//! `wgpu::ShaderModule` **once**, at library-build time, and referred to
5//! afterwards by an opaque [`ShaderId`]. That indirection is what lets
6//! [`crate::pipeline::RenderPipelineDesc`] stay a cheap, hashable value: a
7//! pipeline variant names its program by id instead of carrying a module
8//! handle or a source string around.
9//!
10//! The library is append-only and keyed by name, so building it is a
11//! start-up-time act (`insert_wgsl` per shader) and everything after it is a
12//! lookup. Once built it is wrapped in an `Arc` and shared — a
13//! `wgpu::ShaderModule` is `Send + Sync`, so the pipeline warm-up worker
14//! compiles against exactly the same modules the render thread does, with no
15//! second compile of the source.
16//!
17//! Every module is created with a label (`frust-gpu shader: <name>`) so a
18//! capture in RenderDoc/Xcode names the shader rather than showing an
19//! anonymous module.
20
21use std::collections::HashMap;
22
23/// An opaque handle to a WGSL module held by one [`ShaderLibrary`].
24///
25/// Only [`ShaderLibrary::insert_wgsl`] mints one, and an id is only meaningful
26/// against the library that minted it — [`ShaderLibrary::get`] on a foreign id
27/// returns `None` rather than a wrong module, but the pipeline layer treats
28/// that as a caller bug (see [`crate::pipeline::PipelineCache::get_or_create`]).
29#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct ShaderId(u32);
31
32impl ShaderId {
33    /// The library-local index this id addresses — the value the pipeline
34    /// key packs into its program field.
35    pub(crate) const fn raw(self) -> u32 {
36        self.0
37    }
38
39    /// Rebuilds an id from a raw index, for the pipeline layer's key packing
40    /// and for device-free tests that need an id without a library.
41    pub(crate) const fn from_raw(index: u32) -> Self {
42        Self(index)
43    }
44}
45
46/// One entry: the compiled module plus the name it was registered under.
47#[derive(Debug)]
48struct Entry {
49    name: String,
50    module: wgpu::ShaderModule,
51}
52
53/// A name-keyed, append-only set of compiled WGSL modules.
54///
55/// Build it at start-up, then share it (`Arc<ShaderLibrary>`) with the
56/// pipeline cache and its warm-up worker. Insertion is idempotent per name:
57/// re-inserting a name that is already present returns the existing id
58/// without recompiling, so a caller that cannot easily prove it only
59/// registers once still pays for one compile.
60#[derive(Debug, Default)]
61pub struct ShaderLibrary {
62    entries: Vec<Entry>,
63    by_name: HashMap<String, ShaderId>,
64}
65
66impl ShaderLibrary {
67    /// An empty library.
68    #[must_use]
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Compiles `src` as WGSL and registers it under `name`, returning its id.
74    ///
75    /// The module is labelled with `name`. If `name` is already registered the
76    /// existing id is returned and `src` is *not* compiled again — the library
77    /// is append-only, so a name never changes meaning once it is in.
78    ///
79    /// Compilation errors are not raised here: wgpu reports a malformed module
80    /// through the device's error scope / uncaptured-error handler, and the
81    /// resulting pipeline creation fails there. This mirrors `frust-render`'s
82    /// shader-effect path, which records the failure rather than panicking.
83    pub fn insert_wgsl(
84        &mut self,
85        device: &wgpu::Device,
86        name: impl Into<String>,
87        src: &str,
88    ) -> ShaderId {
89        let name = name.into();
90        if let Some(existing) = self.by_name.get(&name) {
91            return *existing;
92        }
93
94        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
95            label: Some(&format!("frust-gpu shader: {name}")),
96            source: wgpu::ShaderSource::Wgsl(src.into()),
97        });
98
99        let id = ShaderId::from_raw(self.entries.len() as u32);
100        self.entries.push(Entry {
101            name: name.clone(),
102            module,
103        });
104        self.by_name.insert(name, id);
105        id
106    }
107
108    /// The module `id` addresses, or `None` if it was minted by a different
109    /// library.
110    #[must_use]
111    pub fn get(&self, id: ShaderId) -> Option<&wgpu::ShaderModule> {
112        self.entries.get(id.raw() as usize).map(|e| &e.module)
113    }
114
115    /// The id registered under `name`, if any.
116    #[must_use]
117    pub fn id_of(&self, name: &str) -> Option<ShaderId> {
118        self.by_name.get(name).copied()
119    }
120
121    /// The name `id` was registered under — used for pipeline labels.
122    #[must_use]
123    pub fn name_of(&self, id: ShaderId) -> Option<&str> {
124        self.entries.get(id.raw() as usize).map(|e| e.name.as_str())
125    }
126
127    /// How many distinct modules are registered.
128    #[must_use]
129    pub fn len(&self) -> usize {
130        self.entries.len()
131    }
132
133    /// Whether no module is registered yet.
134    #[must_use]
135    pub fn is_empty(&self) -> bool {
136        self.entries.is_empty()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn an_empty_library_resolves_nothing() {
146        let lib = ShaderLibrary::new();
147        assert!(lib.is_empty());
148        assert_eq!(lib.len(), 0);
149        assert!(lib.get(ShaderId::from_raw(0)).is_none());
150        assert!(lib.name_of(ShaderId::from_raw(0)).is_none());
151        assert!(lib.id_of("missing").is_none());
152    }
153
154    #[test]
155    fn ids_round_trip_through_their_raw_index() {
156        for index in [0u32, 1, 7, u32::MAX] {
157            assert_eq!(ShaderId::from_raw(index).raw(), index);
158        }
159        assert_ne!(ShaderId::from_raw(0), ShaderId::from_raw(1));
160    }
161}