Skip to main content

oxiphysics_gpu/shader_registry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use std::collections::HashMap;
8
9/// A capacity-bounded LRU cache for compiled shader variants.
10///
11/// When the cache exceeds `capacity` entries the least-recently-accessed
12/// variant is evicted.
13pub struct VariantCache {
14    /// Maximum number of variants to hold.
15    pub capacity: usize,
16    /// Stored variants in insertion order (we use swap-remove for eviction).
17    pub(super) entries: Vec<(ShaderKey, CompiledVariant, u64)>,
18    /// Monotonic access counter.
19    pub(super) clock: u64,
20}
21impl VariantCache {
22    /// Create a new empty cache with the given capacity.
23    pub fn new(capacity: usize) -> Self {
24        Self {
25            capacity: capacity.max(1),
26            entries: Vec::new(),
27            clock: 0,
28        }
29    }
30    /// Insert a compiled variant.  Evicts the LRU entry if at capacity.
31    pub fn insert(&mut self, variant: CompiledVariant) {
32        if let Some(pos) = self.entries.iter().position(|(k, _, _)| *k == variant.key) {
33            self.clock += 1;
34            self.entries[pos] = (variant.key.clone(), variant, self.clock);
35            return;
36        }
37        if self.entries.len() >= self.capacity {
38            let lru_pos = self
39                .entries
40                .iter()
41                .enumerate()
42                .min_by_key(|(_, (_, _, t))| *t)
43                .map(|(i, _)| i)
44                .unwrap_or(0);
45            self.entries.swap_remove(lru_pos);
46        }
47        self.clock += 1;
48        self.entries
49            .push((variant.key.clone(), variant, self.clock));
50    }
51    /// Look up a variant by key.  Bumps its access time on hit.
52    pub fn get(&mut self, key: &ShaderKey) -> Option<&CompiledVariant> {
53        if let Some(pos) = self.entries.iter().position(|(k, _, _)| k == key) {
54            self.clock += 1;
55            self.entries[pos].2 = self.clock;
56            return Some(&self.entries[pos].1);
57        }
58        None
59    }
60    /// Number of variants currently cached.
61    pub fn len(&self) -> usize {
62        self.entries.len()
63    }
64    /// True when the cache is empty.
65    pub fn is_empty(&self) -> bool {
66        self.entries.is_empty()
67    }
68    /// Remove all entries.
69    pub fn clear(&mut self) {
70        self.entries.clear();
71        self.clock = 0;
72    }
73    /// Total binary bytes cached.
74    pub fn total_binary_bytes(&self) -> usize {
75        self.entries
76            .iter()
77            .map(|(_, v, _)| v.binary_size_bytes)
78            .sum()
79    }
80}
81/// A compiled, instantiated shader variant ready for dispatch.
82#[derive(Debug, Clone)]
83pub struct CompiledVariant {
84    /// The key that identifies this variant.
85    pub key: ShaderKey,
86    /// The fully-substituted WGSL source text.
87    pub wgsl: String,
88    /// The effective workgroup size after substitution.
89    pub workgroup_size: [u32; 3],
90    /// Byte size of the compiled binary mock (always `wgsl.len()` bytes here).
91    pub binary_size_bytes: usize,
92}
93impl CompiledVariant {
94    fn new(key: ShaderKey, wgsl: String, workgroup_size: [u32; 3]) -> Self {
95        let binary_size_bytes = wgsl.len();
96        Self {
97            key,
98            wgsl,
99            workgroup_size,
100            binary_size_bytes,
101        }
102    }
103}
104/// A registry of named [`VariantProfile`]s with dependency resolution.
105#[derive(Debug, Default)]
106pub struct VariantProfileRegistry {
107    pub(super) profiles: HashMap<String, VariantProfile>,
108}
109impl VariantProfileRegistry {
110    /// Create an empty registry.
111    pub fn new() -> Self {
112        Self::default()
113    }
114    /// Register a profile.  Overwrites any existing profile with the same name.
115    pub fn register(&mut self, profile: VariantProfile) {
116        self.profiles.insert(profile.name.clone(), profile);
117    }
118    /// Resolve a profile by name, merging inherited defines.
119    ///
120    /// Returns `None` if the profile (or any base) is not found.
121    pub fn resolve(&self, name: &str) -> Option<HashMap<String, String>> {
122        let profile = self.profiles.get(name)?;
123        let mut merged = if let Some(base) = &profile.base {
124            self.resolve(base)?
125        } else {
126            HashMap::new()
127        };
128        for (k, v) in &profile.defines {
129            merged.insert(k.clone(), v.clone());
130        }
131        Some(merged)
132    }
133    /// Return a sorted list of all registered profile names.
134    pub fn profile_names(&self) -> Vec<&str> {
135        let mut names: Vec<&str> = self.profiles.keys().map(|s| s.as_str()).collect();
136        names.sort_unstable();
137        names
138    }
139}
140/// Tracks which shaders depend on (include) which other shaders.
141///
142/// When a shader is modified, all shaders that depend on it (transitively)
143/// also need to be recompiled.
144#[derive(Debug, Default)]
145pub struct ShaderDependencyGraph {
146    /// `depends_on[A]` = set of shaders A includes / depends on.
147    pub(super) depends_on: HashMap<String, Vec<String>>,
148}
149impl ShaderDependencyGraph {
150    /// Create an empty dependency graph.
151    pub fn new() -> Self {
152        Self::default()
153    }
154    /// Record that shader `dependent` depends on `dependency`.
155    pub fn add_dependency(&mut self, dependent: impl Into<String>, dependency: impl Into<String>) {
156        self.depends_on
157            .entry(dependent.into())
158            .or_default()
159            .push(dependency.into());
160    }
161    /// Return all shaders that directly depend on `dependency`.
162    pub fn direct_dependents(&self, dependency: &str) -> Vec<&str> {
163        self.depends_on
164            .iter()
165            .filter(|(_, deps)| deps.iter().any(|d| d == dependency))
166            .map(|(name, _)| name.as_str())
167            .collect()
168    }
169    /// Return all shaders that transitively depend on `changed_shader`
170    /// (BFS / DFS over the dependency graph).
171    pub fn transitive_dependents(&self, changed_shader: &str) -> Vec<String> {
172        let mut visited = std::collections::HashSet::new();
173        let mut queue = std::collections::VecDeque::new();
174        queue.push_back(changed_shader.to_string());
175        while let Some(current) = queue.pop_front() {
176            for (name, deps) in &self.depends_on {
177                if deps.contains(&current) && !visited.contains(name.as_str()) {
178                    visited.insert(name.clone());
179                    queue.push_back(name.clone());
180                }
181            }
182        }
183        let mut result: Vec<String> = visited.into_iter().collect();
184        result.sort_unstable();
185        result
186    }
187    /// Return the direct dependencies of `shader`.
188    pub fn direct_dependencies(&self, shader: &str) -> &[String] {
189        self.depends_on
190            .get(shader)
191            .map(Vec::as_slice)
192            .unwrap_or(&[])
193    }
194    /// Return all shader names that have any recorded dependencies.
195    pub fn shaders_with_deps(&self) -> Vec<&str> {
196        let mut names: Vec<&str> = self.depends_on.keys().map(|s| s.as_str()).collect();
197        names.sort_unstable();
198        names
199    }
200}
201/// Uniquely identifies a compiled shader variant.
202///
203/// Two keys are equal when the base shader name and all defines match.
204#[derive(Debug, Clone, PartialEq, Eq, Hash)]
205pub struct ShaderKey {
206    /// The base shader name (e.g. `"sph_density"`).
207    pub name: String,
208    /// Sorted define pairs `(KEY, VALUE)` baked into this variant.
209    pub defines: Vec<(String, String)>,
210}
211impl ShaderKey {
212    /// Create a key from a name and an unsorted define map.
213    pub fn new(name: impl Into<String>, defines: &HashMap<String, String>) -> Self {
214        let mut sorted: Vec<(String, String)> = defines
215            .iter()
216            .map(|(k, v)| (k.clone(), v.clone()))
217            .collect();
218        sorted.sort_by(|a, b| a.0.cmp(&b.0));
219        Self {
220            name: name.into(),
221            defines: sorted,
222        }
223    }
224    /// Create a key with no defines.
225    pub fn bare(name: impl Into<String>) -> Self {
226        Self {
227            name: name.into(),
228            defines: Vec::new(),
229        }
230    }
231    /// Return a deterministic string fingerprint suitable for file names.
232    pub fn fingerprint(&self) -> String {
233        if self.defines.is_empty() {
234            return self.name.clone();
235        }
236        let suffix: String = self
237            .defines
238            .iter()
239            .map(|(k, v)| format!("{k}_{v}"))
240            .collect::<Vec<_>>()
241            .join("__");
242        format!("{}__{}", self.name, suffix)
243    }
244}
245/// A named set of compile-time defines forming a "variant profile".
246///
247/// Variant profiles can inherit from a base profile, inheriting its defines
248/// while optionally overriding individual values.
249#[derive(Debug, Clone)]
250pub struct VariantProfile {
251    /// Unique name for this profile.
252    pub name: String,
253    /// The defines in this profile.
254    pub defines: HashMap<String, String>,
255    /// Optional base profile name (inherited defines are merged first).
256    pub base: Option<String>,
257}
258impl VariantProfile {
259    /// Create a new variant profile with no base.
260    pub fn new(name: impl Into<String>) -> Self {
261        Self {
262            name: name.into(),
263            defines: HashMap::new(),
264            base: None,
265        }
266    }
267    /// Create a variant profile that inherits from `base`.
268    pub fn with_base(name: impl Into<String>, base: impl Into<String>) -> Self {
269        Self {
270            name: name.into(),
271            defines: HashMap::new(),
272            base: Some(base.into()),
273        }
274    }
275    /// Set a define on this profile.
276    pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
277        self.defines.insert(key.into(), value.into());
278        self
279    }
280}
281/// Tracks shader source modification times to support hot reload.
282///
283/// In production this would watch file system events; here timestamps
284/// are set manually for testing.
285#[derive(Debug, Default)]
286pub struct HotReloadTracker {
287    /// `name → last_modified` (monotonic counter, not wall clock).
288    pub(super) timestamps: HashMap<String, u64>,
289    /// `name → last_compiled` counter.
290    pub(super) compiled_at: HashMap<String, u64>,
291}
292impl HotReloadTracker {
293    /// Create a new tracker.
294    pub fn new() -> Self {
295        Self::default()
296    }
297    /// Record a modification for `name` at the given logical time.
298    pub fn touch(&mut self, name: impl Into<String>, time: u64) {
299        self.timestamps.insert(name.into(), time);
300    }
301    /// Record that `name` was compiled at the given logical time.
302    pub fn record_compile(&mut self, name: impl Into<String>, time: u64) {
303        self.compiled_at.insert(name.into(), time);
304    }
305    /// Returns `true` when `name` has been modified since it was last compiled.
306    pub fn needs_recompile(&self, name: &str) -> bool {
307        let modified = self.timestamps.get(name).copied().unwrap_or(0);
308        let compiled = self.compiled_at.get(name).copied().unwrap_or(0);
309        modified > compiled
310    }
311    /// Return all shader names that need recompilation.
312    pub fn stale_shaders(&self) -> Vec<&str> {
313        self.timestamps
314            .keys()
315            .filter(|n| self.needs_recompile(n))
316            .map(|s| s.as_str())
317            .collect()
318    }
319}
320impl HotReloadTracker {
321    /// Touch multiple shaders at once with the same logical timestamp.
322    pub fn touch_batch(&mut self, names: &[&str], time: u64) {
323        for &name in names {
324            self.touch(name, time);
325        }
326    }
327    /// Record that all currently stale shaders have been recompiled at `time`.
328    pub fn flush_stale(&mut self, time: u64) {
329        let stale: Vec<String> = self
330            .timestamps
331            .keys()
332            .filter(|n| self.needs_recompile(n))
333            .cloned()
334            .collect();
335        for name in stale {
336            self.compiled_at.insert(name, time);
337        }
338    }
339    /// Return the set of shader names that have been modified but never compiled.
340    pub fn never_compiled(&self) -> Vec<&str> {
341        self.timestamps
342            .keys()
343            .filter(|n| !self.compiled_at.contains_key(n.as_str()))
344            .map(|s| s.as_str())
345            .collect()
346    }
347}
348/// Central registry for shader sources and compiled variants.
349///
350/// Call [`ShaderRegistry::register`] to add a [`ShaderSource`], then
351/// [`ShaderRegistry::get_or_compile`] to obtain a compiled variant,
352/// which is transparently cached in the internal [`VariantCache`].
353pub struct ShaderRegistry {
354    /// All registered shader sources, keyed by name.
355    pub(super) sources: HashMap<String, ShaderSource>,
356    /// Compiled variant cache.
357    pub(super) cache: VariantCache,
358    /// Number of cache hits since creation.
359    pub cache_hits: u64,
360    /// Number of compilations triggered since creation.
361    pub compilations: u64,
362}
363impl ShaderRegistry {
364    /// Create a new empty registry with the given cache capacity.
365    pub fn new(cache_capacity: usize) -> Self {
366        Self {
367            sources: HashMap::new(),
368            cache: VariantCache::new(cache_capacity),
369            cache_hits: 0,
370            compilations: 0,
371        }
372    }
373    /// Register a shader source.  Overwrites any existing source with the
374    /// same name and clears cached variants for that name.
375    pub fn register(&mut self, source: ShaderSource) {
376        let name = source.name.clone();
377        self.sources.insert(name.clone(), source);
378        self.cache.entries.retain(|(k, _, _)| k.name != name);
379    }
380    /// Get or compile a variant.
381    ///
382    /// Returns `Err` when the source name is unknown or when a required
383    /// placeholder is not supplied in `defines`.
384    pub fn get_or_compile(
385        &mut self,
386        name: &str,
387        defines: &HashMap<String, String>,
388    ) -> Result<&CompiledVariant, RegistryError> {
389        let key = ShaderKey::new(name, defines);
390        if self.cache.get(&key).is_some() {
391            self.cache_hits += 1;
392            return Ok(self
393                .cache
394                .get(&key)
395                .expect("cache entry must exist after insertion"));
396        }
397        let source = self
398            .sources
399            .get(name)
400            .ok_or_else(|| RegistryError::UnknownShader(name.to_string()))?;
401        for ph in &source.placeholders {
402            if !defines.contains_key(ph.as_str()) {
403                return Err(RegistryError::MissingDefine {
404                    shader: name.to_string(),
405                    define: ph.clone(),
406                });
407            }
408        }
409        let wgsl = source.instantiate(defines);
410        let workgroup_size = source.workgroup_size;
411        let variant = CompiledVariant::new(key.clone(), wgsl, workgroup_size);
412        self.compilations += 1;
413        self.cache.insert(variant);
414        Ok(self
415            .cache
416            .get(&key)
417            .expect("cache entry must exist after insertion"))
418    }
419    /// Return all registered shader names.
420    pub fn shader_names(&self) -> Vec<&str> {
421        self.sources.keys().map(|s| s.as_str()).collect()
422    }
423    /// Return the number of entries currently in the variant cache.
424    pub fn cached_count(&self) -> usize {
425        self.cache.len()
426    }
427    /// Invalidate a single shader by name (clears its cached variants).
428    pub fn invalidate(&mut self, name: &str) {
429        self.cache.entries.retain(|(k, _, _)| k.name != name);
430    }
431    /// Invalidate all cached variants.
432    pub fn invalidate_all(&mut self) {
433        self.cache.clear();
434    }
435}
436impl ShaderRegistry {
437    /// Compile a specific shader variant with the given options.
438    ///
439    /// Unlike [`get_or_compile`](ShaderRegistry::get_or_compile) this method
440    /// always forces a fresh compilation (not served from the cache) and
441    /// respects the `max_source_bytes` limit in `opts`.
442    pub fn compile_variant(
443        &mut self,
444        name: &str,
445        defines: &HashMap<String, String>,
446        opts: &ShaderCompileOptions,
447    ) -> Result<CompiledVariant, RegistryError> {
448        let source = self
449            .sources
450            .get(name)
451            .ok_or_else(|| RegistryError::UnknownShader(name.to_string()))?;
452        let mut merged = defines.clone();
453        for (k, v) in &opts.extra_defines {
454            merged.entry(k.clone()).or_insert_with(|| v.clone());
455        }
456        for ph in &source.placeholders {
457            if !merged.contains_key(ph.as_str()) {
458                return Err(RegistryError::MissingDefine {
459                    shader: name.to_string(),
460                    define: ph.clone(),
461                });
462            }
463        }
464        let wgsl = source.instantiate(&merged);
465        if opts.max_source_bytes > 0 && wgsl.len() > opts.max_source_bytes {
466            return Err(RegistryError::SourceTooLarge {
467                shader: name.to_string(),
468                size: wgsl.len(),
469                limit: opts.max_source_bytes,
470            });
471        }
472        let workgroup_size = opts.workgroup_size;
473        let key = ShaderKey::new(name, &merged);
474        self.compilations += 1;
475        Ok(CompiledVariant::new(key, wgsl, workgroup_size))
476    }
477}
478impl ShaderRegistry {
479    /// Check a [`HotReloadTracker`] and invalidate any stale shader variants.
480    ///
481    /// Returns the list of shader names that were invalidated.
482    pub fn apply_hot_reload(&mut self, tracker: &HotReloadTracker) -> Vec<String> {
483        let stale: Vec<String> = tracker
484            .stale_shaders()
485            .into_iter()
486            .map(|s| s.to_string())
487            .collect();
488        for name in &stale {
489            self.invalidate(name);
490        }
491        stale
492    }
493    /// Return all shader names registered in this registry.
494    pub fn registered_count(&self) -> usize {
495        self.sources.len()
496    }
497    /// Return the size (in WGSL source bytes) of the named shader, or 0.
498    pub fn source_bytes(&self, name: &str) -> usize {
499        self.sources.get(name).map(|s| s.wgsl.len()).unwrap_or(0)
500    }
501}
502/// A raw WGSL shader source with associated metadata.
503#[derive(Debug, Clone)]
504pub struct ShaderSource {
505    /// Human-readable identifier (must be unique in the registry).
506    pub name: String,
507    /// Raw WGSL text (may contain `{{PLACEHOLDER}}` tokens).
508    pub wgsl: String,
509    /// Suggested workgroup size `[x, y, z]`.
510    pub workgroup_size: [u32; 3],
511    /// List of placeholder names this source accepts.
512    pub placeholders: Vec<String>,
513}
514impl ShaderSource {
515    /// Create a new shader source.
516    pub fn new(name: impl Into<String>, wgsl: impl Into<String>, workgroup_size: [u32; 3]) -> Self {
517        let wgsl_str: String = wgsl.into();
518        let placeholders = collect_placeholders(&wgsl_str);
519        Self {
520            name: name.into(),
521            wgsl: wgsl_str,
522            workgroup_size,
523            placeholders,
524        }
525    }
526    /// Instantiate this source by substituting `{{KEY}}` tokens with values.
527    ///
528    /// Unknown keys are left untouched.  Returns the instantiated WGSL string.
529    pub fn instantiate(&self, defines: &HashMap<String, String>) -> String {
530        let mut out = self.wgsl.clone();
531        for (k, v) in defines {
532            let token = format!("{{{{{}}}}}", k);
533            out = out.replace(&token, v);
534        }
535        out
536    }
537    /// Return the total thread count per workgroup.
538    pub fn threads_per_group(&self) -> u32 {
539        self.workgroup_size[0] * self.workgroup_size[1] * self.workgroup_size[2]
540    }
541}
542/// A typed specialization constant (analogous to Vulkan spec constants).
543#[derive(Debug, Clone, PartialEq)]
544pub enum SpecConstValue {
545    /// Integer constant.
546    Int(i64),
547    /// Unsigned integer constant.
548    Uint(u64),
549    /// Floating-point constant.
550    Float(f64),
551    /// Boolean constant.
552    Bool(bool),
553}
554impl SpecConstValue {
555    /// Render the value as a WGSL literal string.
556    pub fn to_wgsl(&self) -> String {
557        match self {
558            SpecConstValue::Int(v) => v.to_string(),
559            SpecConstValue::Uint(v) => format!("{v}u"),
560            SpecConstValue::Float(v) => format!("{v}"),
561            SpecConstValue::Bool(v) => v.to_string(),
562        }
563    }
564}
565/// A named specialization constant bound to a shader.
566#[derive(Debug, Clone, PartialEq)]
567pub struct SpecializationConstant {
568    /// Name used in the WGSL source as `{{SPEC_NAME}}`.
569    pub name: String,
570    /// Default value.
571    pub default_value: SpecConstValue,
572    /// Optional override value set at pipeline creation time.
573    pub override_value: Option<SpecConstValue>,
574}
575impl SpecializationConstant {
576    /// Create a new specialization constant with a default value.
577    pub fn new(name: impl Into<String>, default: SpecConstValue) -> Self {
578        Self {
579            name: name.into(),
580            default_value: default,
581            override_value: None,
582        }
583    }
584    /// Set an override value.
585    pub fn with_override(mut self, value: SpecConstValue) -> Self {
586        self.override_value = Some(value);
587        self
588    }
589    /// Return the effective value (override if set, else default).
590    pub fn effective_value(&self) -> &SpecConstValue {
591        self.override_value.as_ref().unwrap_or(&self.default_value)
592    }
593}
594/// A simple pipeline cache mapping [`PipelineCacheKey`]s to byte blobs
595/// (mock: stores a label string for each entry).
596#[derive(Debug, Default)]
597pub struct PipelineCache {
598    pub(super) entries: HashMap<PipelineCacheKey, String>,
599    /// Number of cache hits.
600    pub hits: u64,
601    /// Number of cache misses.
602    pub misses: u64,
603}
604impl PipelineCache {
605    /// Create an empty pipeline cache.
606    pub fn new() -> Self {
607        Self::default()
608    }
609    /// Insert a pipeline entry.
610    pub fn insert(&mut self, key: PipelineCacheKey, label: impl Into<String>) {
611        self.entries.insert(key, label.into());
612    }
613    /// Look up a pipeline.  Returns `Some(&label)` on hit, `None` on miss.
614    pub fn get(&mut self, key: &PipelineCacheKey) -> Option<&str> {
615        if let Some(v) = self.entries.get(key) {
616            self.hits += 1;
617            Some(v.as_str())
618        } else {
619            self.misses += 1;
620            None
621        }
622    }
623    /// Number of entries in the cache.
624    pub fn len(&self) -> usize {
625        self.entries.len()
626    }
627    /// Returns `true` when the cache is empty.
628    pub fn is_empty(&self) -> bool {
629        self.entries.is_empty()
630    }
631    /// Clear all entries.
632    pub fn clear(&mut self) {
633        self.entries.clear();
634    }
635    /// Hit rate in `[0.0, 1.0]`.
636    pub fn hit_rate(&self) -> f64 {
637        let total = self.hits + self.misses;
638        if total == 0 {
639            return 0.0;
640        }
641        self.hits as f64 / total as f64
642    }
643}
644/// Describes a GPU compute pipeline built from a shader variant.
645#[derive(Debug, Clone)]
646pub struct PipelineDescriptor {
647    /// The shader variant key used by this pipeline.
648    pub key: ShaderKey,
649    /// Bind group layouts (number of bind groups required).
650    pub bind_group_count: u32,
651    /// Push-constant block size in bytes (0 = none).
652    pub push_constant_bytes: u32,
653    /// Human-readable label for debugging.
654    pub label: String,
655}
656#[allow(clippy::too_many_arguments)]
657impl PipelineDescriptor {
658    /// Create a new pipeline descriptor.
659    pub fn new(
660        key: ShaderKey,
661        bind_group_count: u32,
662        push_constant_bytes: u32,
663        label: impl Into<String>,
664    ) -> Self {
665        Self {
666            key,
667            bind_group_count,
668            push_constant_bytes,
669            label: label.into(),
670        }
671    }
672    /// Validate the descriptor (mock: checks workgroup product).
673    pub fn validate(&self) -> Result<(), RegistryError> {
674        if self.bind_group_count == 0 {
675            return Err(RegistryError::UnknownShader(
676                "pipeline has no bind groups".into(),
677            ));
678        }
679        Ok(())
680    }
681}
682/// Options controlling how a shader variant is compiled.
683#[derive(Debug, Clone, PartialEq)]
684pub struct ShaderCompileOptions {
685    /// Workgroup sizes to try when specialising the shader (x, y, z).
686    pub workgroup_size: [u32; 3],
687    /// Additional `#define`-style substitutions beyond the registry defaults.
688    pub extra_defines: HashMap<String, String>,
689    /// Optional byte-size budget.  Compilation fails if the instantiated WGSL
690    /// exceeds this limit (0 = unlimited).
691    pub max_source_bytes: usize,
692}
693impl ShaderCompileOptions {
694    /// Construct default options.
695    pub fn new() -> Self {
696        Self {
697            workgroup_size: [64, 1, 1],
698            extra_defines: HashMap::new(),
699            max_source_bytes: 0,
700        }
701    }
702}
703/// A set of specialization constants for a shader.
704#[derive(Debug, Clone, Default)]
705pub struct SpecConstSet {
706    /// The constants in this set, keyed by name.
707    pub constants: HashMap<String, SpecializationConstant>,
708}
709impl SpecConstSet {
710    /// Create an empty set.
711    pub fn new() -> Self {
712        Self::default()
713    }
714    /// Add a constant.
715    pub fn add(&mut self, constant: SpecializationConstant) {
716        self.constants.insert(constant.name.clone(), constant);
717    }
718    /// Build a `HashMap<String, String>` suitable for passing to
719    /// [`ShaderSource::instantiate`].
720    pub fn to_defines(&self) -> HashMap<String, String> {
721        self.constants
722            .iter()
723            .map(|(name, c)| (name.clone(), c.effective_value().to_wgsl()))
724            .collect()
725    }
726    /// Return `true` if a constant named `name` is in this set.
727    pub fn has(&self, name: &str) -> bool {
728        self.constants.contains_key(name)
729    }
730    /// Return the effective WGSL string for constant `name`, or `None`.
731    pub fn get_wgsl(&self, name: &str) -> Option<String> {
732        self.constants
733            .get(name)
734            .map(|c| c.effective_value().to_wgsl())
735    }
736}
737/// Errors returned by the shader registry.
738#[derive(Debug, Clone, PartialEq, Eq)]
739pub enum RegistryError {
740    /// No source registered under the given name.
741    UnknownShader(String),
742    /// A required `{{DEFINE}}` was not supplied.
743    MissingDefine {
744        /// Shader that requires the missing define.
745        shader: String,
746        /// The missing define name.
747        define: String,
748    },
749    /// The instantiated WGSL exceeds the permitted byte limit.
750    SourceTooLarge {
751        /// Shader that is too large.
752        shader: String,
753        /// Actual size in bytes.
754        size: usize,
755        /// Permitted byte limit.
756        limit: usize,
757    },
758}
759/// Composite key for a GPU pipeline cache entry.
760///
761/// Uniquely identifies a compiled pipeline based on the shader variant and
762/// pipeline parameters.
763#[derive(Debug, Clone, PartialEq, Eq, Hash)]
764pub struct PipelineCacheKey {
765    /// Shader variant key.
766    pub shader_key: ShaderKey,
767    /// Workgroup size.
768    pub workgroup_size: [u32; 3],
769    /// Push constant size in bytes.
770    pub push_constant_bytes: u32,
771    /// Bind group layout signature (e.g. a hash of the layout).
772    pub layout_hash: u64,
773}
774impl PipelineCacheKey {
775    /// Create a new cache key.
776    pub fn new(
777        shader_key: ShaderKey,
778        workgroup_size: [u32; 3],
779        push_constant_bytes: u32,
780        layout_hash: u64,
781    ) -> Self {
782        Self {
783            shader_key,
784            workgroup_size,
785            push_constant_bytes,
786            layout_hash,
787        }
788    }
789    /// Compute an FNV-1a hash of the entire key.
790    pub fn hash_key(&self) -> u64 {
791        let repr = format!(
792            "{}_{:?}_{}_{}",
793            self.shader_key.fingerprint(),
794            self.workgroup_size,
795            self.push_constant_bytes,
796            self.layout_hash,
797        );
798        compute_cache_key(&repr)
799    }
800}