Skip to main content

frust_scene/
shader.rs

1//! [`ShaderProgram`]: a runtime fragment-shader handle (WGSL source).
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6/// Process-unique id counter backing [`ShaderProgram::new`].
7static NEXT_SHADER_PROGRAM_ID: AtomicU64 = AtomicU64::new(1);
8
9/// A runtime fragment-shader program (WGSL source) — the Frust analog of
10/// Flutter's `FragmentProgram`.
11///
12/// This is a renderer contract, not a scene-layer dependency: the WGSL
13/// source is carried here as an opaque string (precedent:
14/// `frust_theme::GlassMaterial::blur_radius_intent`, a future-backend
15/// contract that is likewise plain data rather than a compiled resource) —
16/// only the render backend compiles it into a GPU pipeline.
17///
18/// **Alpha contract**: the shader's output is taken as **premultiplied** —
19/// the convention every paint in a Frust frame travels in. A shader returning
20/// `vec4(rgb, a)` must have already multiplied `rgb` by `a`; the result is
21/// composited over what is behind it, so a translucent or fully transparent
22/// fragment shows the scene beneath rather than black. Opaque output
23/// (`a = 1.0`) is unaffected by the convention and needs nothing done to it.
24#[derive(Clone, Debug)]
25pub struct ShaderProgram {
26    /// Process-unique id, minted fresh by [`ShaderProgram::new`] and shared
27    /// across `Clone` — the compile-once cache key `frust-render` uses to
28    /// avoid recompiling the same WGSL source every frame.
29    id: u64,
30    /// WGSL fragment source. `Arc<str>` keeps a clone cheap (a handle copy,
31    /// not a string copy) and `Send` (no `Sync` requirement) — the Scene:
32    /// Send tripwire (`lib.rs`) this payload must satisfy for the
33    /// render-thread split.
34    source: Arc<str>,
35}
36
37impl ShaderProgram {
38    /// Wraps `wgsl` in a new program handle, minting a fresh process-unique
39    /// id.
40    ///
41    /// `Clone` shares the id — see the `id` field's doc comment.
42    ///
43    /// # Cache-once contract
44    ///
45    /// Call this **once** per distinct shader and retain (or `Clone`) the
46    /// result — never mint a fresh `ShaderProgram` every frame or rebuild.
47    /// The renderer's shader-effects engine compiles and caches a GPU
48    /// pipeline keyed by [`id()`](Self::id): a fresh id every frame is a
49    /// permanent cache miss, forcing a full pipeline recompile (plus
50    /// target/registration churn) every single frame instead of the
51    /// intended compile-once-then-reuse cost.
52    ///
53    /// Create it once in retained state — a `Component`'s `init`, or other
54    /// `Widget`/`View` state built once and reused — and clone the handle
55    /// (cheap: an `Arc` handle copy, sharing the id) wherever it's drawn
56    /// thereafter. A `View`'s own `build` method is a correct create-once
57    /// hook too: it constructs the retained widget exactly once, so minting
58    /// a program there is fine. Do **not** call `ShaderProgram::new` inline
59    /// inside a widget's `paint` method (runs every frame) or inside a
60    /// `Component`'s `build` (re-runs every rebuild) — both mint a new id on
61    /// every call. See `examples/shadertoy/src/shaders.rs`'s `all()` for the
62    /// reference pattern: a registry built once and reused.
63    pub fn new(wgsl: impl Into<Arc<str>>) -> Self {
64        let id = NEXT_SHADER_PROGRAM_ID.fetch_add(1, Ordering::Relaxed);
65        Self {
66            id,
67            source: wgsl.into(),
68        }
69    }
70
71    /// The process-unique id, stable across `Clone`.
72    pub fn id(&self) -> u64 {
73        self.id
74    }
75
76    /// The WGSL fragment source.
77    pub fn source(&self) -> &str {
78        &self.source
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn new_mints_unique_ids() {
88        let a = ShaderProgram::new("fn a() {}");
89        let b = ShaderProgram::new("fn b() {}");
90        assert_ne!(a.id(), b.id());
91    }
92
93    #[test]
94    fn new_mints_unique_ids_across_threads() {
95        let handles: Vec<_> = (0..8)
96            .map(|i| std::thread::spawn(move || ShaderProgram::new(format!("fn s{i}() {{}}")).id()))
97            .collect();
98        let mut ids: Vec<u64> = handles.into_iter().map(|h| h.join().unwrap()).collect();
99        ids.sort_unstable();
100        ids.dedup();
101        assert_eq!(ids.len(), 8, "all ids must be unique across threads");
102    }
103
104    #[test]
105    fn clone_shares_id() {
106        let a = ShaderProgram::new("fn a() {}");
107        let b = a.clone();
108        assert_eq!(a.id(), b.id());
109    }
110
111    #[test]
112    fn source_round_trips() {
113        let program = ShaderProgram::new("fn main() {}");
114        assert_eq!(program.source(), "fn main() {}");
115    }
116}