Skip to main content

sequoia_view/
shader_pass.rs

1//! Typed shader passes and chains for [`Canvas`](crate::Canvas).
2//!
3//! A [`ShaderPass`] bundles a generated shader with its rectangle, blend mode,
4//! UV window, and texture bindings. [`ShaderChain`] sequences several passes,
5//! feeding each result into the next one as an offscreen target.
6
7use std::rc::Rc;
8
9use sequoia_core::{BlendMode, Rect, TexInput};
10use sequoia_shader::SequoiShader;
11
12use crate::painter::Painter;
13
14/// A configured shader draw that can be handed to a canvas or reused.
15///
16/// Inputs are optional: [`Painter`] samples transparent white for textures the
17/// pass does not bind. [`Self::primary_input`] binds the shader's first
18/// declared texture, which is what most single-input effects need.
19#[derive(Clone)]
20pub struct ShaderPass<S: SequoiShader> {
21    shader: S,
22    rect: Option<Rect>,
23    uv: Option<[f32; 4]>,
24    blend: Option<BlendMode>,
25    inputs: Vec<(&'static str, TexInput)>,
26}
27
28impl<S: SequoiShader> ShaderPass<S> {
29    pub fn new(shader: S) -> Self {
30        Self {
31            shader,
32            rect: None,
33            uv: None,
34            blend: None,
35            inputs: Vec::new(),
36        }
37    }
38
39    /// Draws into `rect`. Without it the canvas bounds are used.
40    pub fn at(mut self, rect: Rect) -> Self {
41        self.rect = Some(rect);
42        self
43    }
44
45    /// Samples the shader inputs with an explicit UV window.
46    pub fn uv(mut self, uv: [f32; 4]) -> Self {
47        self.uv = Some(uv);
48        self
49    }
50
51    /// Overrides the blend mode for this pass only.
52    pub fn blend(mut self, blend: BlendMode) -> Self {
53        self.blend = Some(blend);
54        self
55    }
56
57    /// Binds `input` to the shader texture parameter `name`.
58    pub fn input(mut self, name: &'static str, input: TexInput) -> Self {
59        if let Some(existing) = self.inputs.iter_mut().find(|(key, _)| *key == name) {
60            existing.1 = input;
61        } else {
62            self.inputs.push((name, input));
63        }
64        self
65    }
66
67    /// Binds the first texture declared by the shader, unless already bound.
68    ///
69    /// This removes the need to repeat the parameter name at every call site.
70    pub fn primary_input(mut self, input: TexInput) -> Self {
71        if let Some(name) = S::TEXTURES.first().copied() {
72            if !self.inputs.iter().any(|(key, _)| *key == name) {
73                self.inputs.push((name, input));
74            }
75        }
76        self
77    }
78
79    /// Binds the first two declared textures in order.
80    pub fn layers(self, first: TexInput, second: TexInput) -> Self {
81        let mut pass = self;
82        if let Some(name) = S::TEXTURES.first().copied() {
83            pass = pass.input(name, first);
84        }
85        if let Some(name) = S::TEXTURES.get(1).copied() {
86            pass = pass.input(name, second);
87        }
88        pass
89    }
90
91    pub fn name(&self) -> &'static str {
92        S::NAME
93    }
94
95    pub fn target_rect(&self) -> Option<Rect> {
96        self.rect
97    }
98
99    /// Records the draw. Returns `false` when the shader is not registered on
100    /// the runtime or the rectangle is empty.
101    pub fn draw(&self, g: &mut Painter) -> bool
102    where
103        S: Copy,
104    {
105        let rect = self.rect.unwrap_or_else(|| g.rect());
106        let inputs: Vec<(&str, TexInput)> = self
107            .inputs
108            .iter()
109            .map(|(name, input)| (*name, input.clone()))
110            .collect();
111        match (self.uv, self.blend) {
112            (Some(uv), Some(blend)) => {
113                g.try_shader_at_with_blend(self.shader, rect, uv, &inputs, blend)
114            }
115            (Some(uv), None) => g.try_shader_at(self.shader, rect, uv, &inputs),
116            (None, Some(blend)) => g.try_shader_with_blend(self.shader, rect, &inputs, blend),
117            (None, None) => g.try_shader(self.shader, rect, &inputs),
118        }
119    }
120}
121
122type ChainPass = Rc<dyn Fn(&mut Painter, Rect, TexInput) -> bool>;
123
124/// An ordered list of shader effects applied to captured content.
125///
126/// Each pass receives the previous pass output as a texture target. A pass that
127/// is not registered is skipped and its input is forwarded to the next pass. If
128/// the final pass is missing, the captured layer is blitted back unchanged, so
129/// a missing optional effect degrades instead of blanking the layer.
130#[derive(Clone, Default)]
131pub struct ShaderChain {
132    passes: Vec<ChainPass>,
133}
134
135impl ShaderChain {
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Appends an effect that binds the shader's first declared texture.
141    pub fn effect<S: SequoiShader + Copy + 'static>(self, shader: S) -> Self {
142        self.push(shader, None, None)
143    }
144
145    /// Appends an effect with an explicit blend mode.
146    pub fn effect_with_blend<S: SequoiShader + Copy + 'static>(
147        self,
148        shader: S,
149        blend: BlendMode,
150    ) -> Self {
151        self.push(shader, None, Some(blend))
152    }
153
154    /// Appends an effect that binds `input` to a named texture parameter.
155    pub fn effect_to<S: SequoiShader + Copy + 'static>(
156        self,
157        shader: S,
158        input: &'static str,
159        blend: Option<BlendMode>,
160    ) -> Self {
161        self.push(shader, Some(input), blend)
162    }
163
164    fn push<S: SequoiShader + Copy + 'static>(
165        mut self,
166        shader: S,
167        input: Option<&'static str>,
168        blend: Option<BlendMode>,
169    ) -> Self {
170        let input = input.unwrap_or_else(|| S::TEXTURES.first().copied().unwrap_or("content"));
171        let pass = Rc::new(
172            move |g: &mut Painter, rect: Rect, target: TexInput| -> bool {
173                let inputs = [(input, target)];
174                match blend {
175                    Some(blend) => g.try_shader_with_blend(shader, rect, &inputs, blend),
176                    None => g.try_shader(shader, rect, &inputs),
177                }
178            },
179        );
180        self.passes.push(pass);
181        self
182    }
183
184    /// Appends every pass from `other` after the current ones.
185    pub fn extend(&mut self, other: ShaderChain) {
186        self.passes.extend(other.passes);
187    }
188
189    pub fn len(&self) -> usize {
190        self.passes.len()
191    }
192
193    pub fn is_empty(&self) -> bool {
194        self.passes.is_empty()
195    }
196
197    pub(crate) fn passes(&self) -> &[ChainPass] {
198        &self.passes
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use sequoia_core::RenderTargetId;
206    use sequoia_shader::shader;
207
208    shader! {
209        fn invert(uv: FragUv, content: Sampler2D) -> Color {
210            let c = textureSample(content, content_sampler, uv);
211            return vec4<f32>(1.0 - c.rgb, c.a);
212        }
213    }
214
215    #[test]
216    fn primary_input_does_not_override_explicit_binding() {
217        let pass = ShaderPass::new(Invert)
218            .input("content", TexInput::Target(RenderTargetId::new(7)))
219            .primary_input(TexInput::Target(RenderTargetId::new(9)));
220        assert_eq!(pass.inputs.len(), 1);
221        assert_eq!(pass.inputs[0].1, TexInput::Target(RenderTargetId::new(7)));
222    }
223
224    #[test]
225    fn primary_input_uses_the_declared_parameter_name() {
226        let pass = ShaderPass::new(Invert).primary_input(TexInput::Target(RenderTargetId::new(3)));
227        assert_eq!(
228            pass.inputs,
229            vec![("content", TexInput::Target(RenderTargetId::new(3)))]
230        );
231        assert_eq!(pass.name(), Invert::NAME);
232    }
233
234    #[test]
235    fn chain_keeps_order_and_can_be_extended() {
236        let mut chain = ShaderChain::new()
237            .effect(Invert)
238            .effect_with_blend(Invert, BlendMode::Add);
239        assert_eq!(chain.len(), 2);
240        chain.extend(ShaderChain::new().effect(Invert));
241        assert_eq!(chain.len(), 3);
242        assert!(!chain.is_empty());
243    }
244}