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