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}