Skip to main content

mirage_engine/
post_effect.rs

1//! Full-screen passes a game writes in WGSL, run over the frame the engine
2//! drew.
3//!
4//! A post effect runs inside the post chain, the passes that take a drawn
5//! frame to the window. It is a type: its fields are the values its WGSL
6//! reads, its
7//! [`STAGE`](PostEffect::STAGE) is where in the post chain it runs, and
8//! its [`SHADER`](PostEffect::SHADER) is the WGSL it runs. Name it in
9//! [`Game::PostEffects`](crate::Game::PostEffects) through
10//! [`post_effects!`](crate::post_effects), and run it for one frame with
11//! [`set_post_effect`](crate::FrameContext::set_post_effect).
12//!
13//! # What the engine declares
14//!
15//! An effect's WGSL is compiled into this, which it may read and must not
16//! declare again:
17//!
18//! ```wgsl
19//! struct Pixel {
20//!     color: vec4<f32>,     // what the chain holds at this pixel
21//!     uv: vec2<f32>,        // 0..1 across the frame, zero at its top left
22//!     position: vec2<f32>,  // physical pixels, the center of each at a half
23//!     size: vec2<f32>,      // the frame, in physical pixels
24//! }
25//!
26//! fn color_at(uv: vec2<f32>) -> vec4<f32>;
27//! fn depth_at(uv: vec2<f32>) -> f32;
28//! ```
29//!
30//! `color` is what the pass before this one wrote: at
31//! [`Lit`](EffectStage::Lit) that is high-dynamic-range light, whose
32//! channels run past `1.0`; at [`ToneMapped`](EffectStage::ToneMapped) and
33//! [`OverUi`](EffectStage::OverUi) it is the encoded value the target
34//! holds, in `0..1`. `color_at(pixel.uv)` reads the same texel as
35//! `pixel.color`.
36//!
37//! `color_at` and `depth_at` return the edge texel for a `uv` outside
38//! `0..1`. `depth_at` returns the view depth in meters the forward pass
39//! wrote —
40//! the first sample of the pixel where the frame is drawn over more than
41//! one — and the far clip of the frame's camera where nothing was drawn,
42//! which is `1000.0` until a game sets one with
43//! [`Projection::clip`](crate::Projection::clip).
44//!
45//! # What an effect declares
46//!
47//! One function, named and written as the engine reads it:
48//!
49//! ```wgsl
50//! fn draw(pixel: Pixel) -> vec4<f32>;
51//! ```
52//!
53//! The pass writes what it returns, every channel, over what the target
54//! held: nothing is blended. The effect after it in the same stage reads
55//! all four channels back as `pixel.color`.
56//!
57//! The bloom chain and the tone map read the color alone, so a `Lit`
58//! effect's alpha is dropped. A `ToneMapped` or `OverUi` effect's alpha is
59//! the alpha the target holds — the engine's own passes write `1.0` there
60//! — and a headless reading reads it back.
61//!
62//! An effect's values are bound as `effect`: a uniform of a WGSL struct
63//! named after the Rust type, so a function of the effect's own may take
64//! that struct by value. An effect with no fields binds nothing.
65//!
66//! ```wgsl
67//! fn tinted(values: Grain, color: vec3<f32>) -> vec3<f32> { … }
68//! ```
69//!
70//! An effect may declare whatever else it needs beside `draw`: its own
71//! code, its own constant values, its own struct types. The names the
72//! engine's own shader already holds are `Pixel`, `Frame`, `Fragment`,
73//! `FORESHORTENED`, `frame`, `source`, `source_sampler`, `resolved`,
74//! `scene`, `color_at`, `depth_at`, `fullscreen`, `effect_fragment`,
75//! `resolve` and `effect`; declaring one of them again stops startup, as
76//! any other error in the WGSL does. The error names the effect and counts
77//! its lines from the compiled shader, the engine's own and the effect's
78//! together, not from the effect's file.
79//!
80//! A game may name any number of effects. Each is one pipeline built at
81//! startup and one full-screen pass in a frame that submits it; a frame
82//! that submits none runs none.
83
84use crate::shader_values::{Sealed, ShaderValues};
85
86/// The engine's own full-screen shader, with [`SEAM`] where an effect's code
87/// goes.
88const FRAME: &str = include_str!("renderer/post_effect.wgsl");
89
90/// The line every stitch replaces.
91const SEAM: &str = "// mirage-engine:effect";
92
93/// The bind group an effect's values are read through.
94pub(crate) const GROUP: u32 = 0;
95
96/// The `draw` the depth resolve is built with, which reads no values and
97/// returns the pixel it was passed.
98const UNPAINTED: &str = "fn draw(pixel: Pixel) -> vec4<f32> {\n    return pixel.color;\n}\n";
99
100/// Where a post effect runs in the post chain — the passes that take a
101/// drawn frame to the window: the forward pass, the `Lit` effects, bloom,
102/// the tone map, the `ToneMapped` effects, the UI, then the `OverUi`
103/// effects.
104///
105/// Effects run stage by stage in that order, and within one stage in the
106/// order their set lists them.
107#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
108pub enum EffectStage {
109    /// After the forward pass, over the lit scene, before the bloom and the
110    /// tone map; the only stage a pixel past `1.0` can be read.
111    Lit,
112    /// After the tone map, over the frame as it will be shown, before the
113    /// UI.
114    ToneMapped,
115    /// After the UI, over everything the frame holds. Built without the
116    /// `ui` feature there is no UI pass and this runs after the
117    /// `ToneMapped` effects all the same.
118    OverUi,
119}
120
121impl EffectStage {
122    /// Every stage, in the order the post chain runs them.
123    pub(crate) const ALL: [Self; 3] = [Self::Lit, Self::ToneMapped, Self::OverUi];
124
125    /// Whether this stage draws after the tone map rather than in the
126    /// scene's own high-dynamic-range light.
127    pub(crate) fn after_tone_map(self) -> bool {
128        !matches!(self, Self::Lit)
129    }
130}
131
132/// A pass of a game's own over the whole frame, written in WGSL and
133/// stitched into the engine's own at startup.
134///
135/// Required if you want to change every pixel of the frame after the
136/// scene is drawn: implement it on a type whose fields are the values its
137/// WGSL reads, name that type in [`Game::PostEffects`](crate::Game::PostEffects),
138/// and run it with [`set_post_effect`](crate::FrameContext::set_post_effect).
139pub trait PostEffect: ShaderValues {
140    /// Where in the post chain this effect runs, and so what its `color`
141    /// holds and what its own alpha reaches; see the module's own
142    /// docs.
143    const STAGE: EffectStage;
144
145    /// WGSL declaring `fn draw(pixel: Pixel) -> vec4<f32>`, run once per
146    /// pixel, which returns what this effect writes there.
147    ///
148    /// `Pixel`, `color_at` and `depth_at` are the engine's, and the
149    /// module's own docs state what each of them holds.
150    const SHADER: &'static str;
151}
152
153/// The post effects one game passes its frame through, named together as
154/// [`Game::PostEffects`](crate::Game::PostEffects).
155///
156/// Written by [`post_effects!`](crate::post_effects) and never implemented
157/// by hand; `()` for a game that draws the frame as the chain leaves it.
158pub trait PostEffects: Sealed + 'static {
159    /// The effects startup compiles, in the order the set lists them.
160    #[doc(hidden)]
161    fn declared(into: &mut Declarations);
162
163    /// Seat of the effect this set value holds: `0` for the first effect
164    /// the set names, one more for each after it.
165    #[doc(hidden)]
166    fn seat(&self) -> u32;
167
168    /// Lays out the values of the effect this set value holds.
169    #[doc(hidden)]
170    fn write(&self, into: &mut Vec<u8>);
171}
172
173/// The effects a set declares, in the order it lists them; the set macro
174/// fills it and startup compiles what it holds.
175#[doc(hidden)]
176#[derive(Default)]
177pub struct Declarations(Vec<Declaration>);
178
179impl Declarations {
180    /// Declares the effect at the next seat.
181    #[doc(hidden)]
182    pub fn declare<S: PostEffect>(&mut self) {
183        self.0.push(Declaration::of::<S>());
184    }
185
186    /// Everything `S` declares, in seat order.
187    pub(crate) fn of<S: PostEffects>() -> Vec<Declaration> {
188        let mut declared = Self::default();
189        S::declared(&mut declared);
190        declared.0
191    }
192}
193
194/// One effect as startup takes it: its own name, where in the post chain it
195/// runs, and the shader that runs it.
196#[derive(Debug)]
197pub(crate) struct Declaration {
198    pub(crate) name: &'static str,
199    pub(crate) stage: EffectStage,
200    pub(crate) source: String,
201}
202
203impl Declaration {
204    fn of<S: PostEffect>() -> Self {
205        Self {
206            name: core::any::type_name::<S>(),
207            stage: S::STAGE,
208            source: stitched(&S::bound(GROUP, "effect"), S::SHADER),
209        }
210    }
211}
212
213/// Which of a game's post effects this is, counted in the order its set lists
214/// them.
215#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
216pub(crate) struct PostEffectId(pub(crate) u32);
217
218impl PostEffects for () {
219    /// Nothing; a game with no effects compiles none.
220    fn declared(_into: &mut Declarations) {}
221
222    /// Not reachable: no call that runs an effect compiles for a game
223    /// with none.
224    fn seat(&self) -> u32 {
225        0
226    }
227
228    /// Not reachable: no call that writes an effect's values compiles for
229    /// a game with none.
230    fn write(&self, _into: &mut Vec<u8>) {}
231}
232
233/// Writes the set of every post effect a game passes its frame through: an
234/// enum with one variant wrapping each named type, which
235/// [`Game::PostEffects`](crate::Game::PostEffects) names.
236///
237/// Each type is spelled by its own name and must be in scope; a set that
238/// holds a type twice does not compile. [`EffectStage`] orders the effects
239/// first and the order the set lists them second, so two effects in one
240/// stage run in that order. A `pub` before `enum` makes the enum public, and `///` lines
241/// before that are the enum's.
242///
243/// ```
244/// use mirage_engine::prelude::*;
245///
246/// #[derive(ShaderValues)]
247/// struct Grain {
248///     strength: f32,
249/// }
250///
251/// impl PostEffect for Grain {
252///     const STAGE: EffectStage = EffectStage::ToneMapped;
253///     const SHADER: &'static str = "
254///         fn draw(pixel: Pixel) -> vec4<f32> {
255///             let noise = fract(sin(pixel.position.x + pixel.position.y) * 43758.5453);
256///             return vec4<f32>(pixel.color.rgb + noise * effect.strength, pixel.color.a);
257///         }";
258/// }
259///
260/// post_effects! { enum Look { Grain } }
261/// ```
262#[macro_export]
263macro_rules! post_effects {
264    ($(#[$attribute:meta])* $vis:vis enum $set:ident { $($effect:ident),+ $(,)? }) => {
265        $(#[$attribute])*
266        $vis enum $set {
267            $($effect($effect)),+
268        }
269
270        $(
271            impl ::core::convert::From<$effect> for $set {
272                fn from(effect: $effect) -> Self {
273                    Self::$effect(effect)
274                }
275            }
276
277            impl $crate::Holds<$effect> for $set {}
278        )+
279
280        impl $crate::Sealed for $set {}
281
282        impl $crate::PostEffects for $set {
283            fn declared(into: &mut $crate::PostEffectDeclarations) {
284                $(into.declare::<$effect>();)+
285            }
286
287            fn seat(&self) -> u32 {
288                let mut at = 0;
289                $(
290                    if ::core::matches!(self, Self::$effect(_)) {
291                        return at;
292                    }
293                    at += 1;
294                )+
295                at
296            }
297
298            fn write(&self, into: &mut ::std::vec::Vec<u8>) {
299                match self {
300                    $(Self::$effect(values) => $crate::ShaderValues::write(values, into)),+
301                }
302            }
303        }
304    };
305}
306
307/// The shader the depth resolve is drawn with: the engine's own, with the
308/// seam left returning what it was passed.
309pub(crate) fn unpainted() -> String {
310    stitched("", UNPAINTED)
311}
312
313/// The shader one effect is drawn with: the engine's own, with the effect's
314/// values declared and its code called where the seam marks.
315fn stitched(values: &str, draw: &str) -> String {
316    let mut code = String::from(values);
317    code.push_str(draw);
318    FRAME.replace(SEAM, &code)
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    /// An effect with values of its own, written the way a game writes one.
326    #[derive(crate::ShaderValues)]
327    struct Vignette {
328        strength: f32,
329    }
330
331    impl PostEffect for Vignette {
332        const STAGE: EffectStage = EffectStage::ToneMapped;
333        const SHADER: &'static str =
334            "fn draw(pixel: Pixel) -> vec4<f32> { return pixel.color * effect.strength; }";
335    }
336
337    /// An effect that reads no values, and reads the scene's depth instead.
338    #[derive(crate::ShaderValues)]
339    struct Fog;
340
341    impl PostEffect for Fog {
342        const STAGE: EffectStage = EffectStage::Lit;
343        const SHADER: &'static str =
344            "fn draw(pixel: Pixel) -> vec4<f32> { return vec4<f32>(depth_at(pixel.uv)); }";
345    }
346
347    post_effects! { enum Look { Fog, Vignette } }
348
349    #[test]
350    fn the_shader_carries_one_seam_for_an_effect_to_be_stitched_into() {
351        assert_eq!(FRAME.matches(SEAM).count(), 1);
352    }
353
354    #[test]
355    fn an_effects_own_code_is_called_from_the_seam_and_its_values_are_bound() {
356        let source = Declaration::of::<Vignette>().source;
357
358        assert!(!source.contains(SEAM), "the seam itself is replaced");
359        assert!(source.contains("struct Vignette"));
360        assert!(source.contains("@group(0) @binding(0) var<uniform> effect: Vignette;"));
361        assert!(
362            source.find("struct Vignette") < source.find("fn draw(pixel: Pixel)"),
363            "and the values are declared before the code that reads them"
364        );
365    }
366
367    #[test]
368    fn an_effect_with_no_fields_reads_no_values_and_binds_none() {
369        let source = Declaration::of::<Fog>().source;
370
371        assert!(!source.contains("@group(0)"));
372        assert!(source.contains("fn draw(pixel: Pixel)"));
373    }
374
375    #[test]
376    fn a_set_declares_its_effects_in_the_order_it_names_them() {
377        let declared = Declarations::of::<Look>();
378
379        assert_eq!(
380            declared
381                .iter()
382                .map(|effect| effect.stage)
383                .collect::<Vec<_>>(),
384            vec![EffectStage::Lit, EffectStage::ToneMapped]
385        );
386        assert_eq!(
387            (
388                Look::from(Fog).seat(),
389                Look::from(Vignette { strength: 0.0 }).seat()
390            ),
391            (0, 1)
392        );
393        assert!(Declarations::of::<()>().is_empty());
394    }
395
396    #[test]
397    fn a_set_value_lays_out_the_values_of_the_effect_it_holds() {
398        let mut written = Vec::new();
399        Look::from(Vignette { strength: 0.25 }).write(&mut written);
400
401        assert_eq!(
402            f32::from_le_bytes(written[..4].try_into().expect("four bytes")),
403            0.25
404        );
405
406        let mut none = Vec::new();
407        Look::from(Fog).write(&mut none);
408        assert!(none.is_empty(), "where an effect reads nothing");
409    }
410
411    #[test]
412    fn the_places_run_the_scene_first_and_what_is_over_the_ui_last() {
413        assert_eq!(
414            EffectStage::ALL,
415            [
416                EffectStage::Lit,
417                EffectStage::ToneMapped,
418                EffectStage::OverUi
419            ]
420        );
421        assert!(
422            EffectStage::Lit < EffectStage::ToneMapped
423                && EffectStage::ToneMapped < EffectStage::OverUi
424        );
425        assert_eq!(
426            EffectStage::ALL.map(EffectStage::after_tone_map),
427            [false, true, true],
428            "and only the first draws in the scene's own light"
429        );
430    }
431}