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::holds::Sealed;
85use crate::shader_values::ShaderValues;
86
87/// The engine's own full-screen shader, with [`SEAM`] where an effect's code
88/// goes.
89const FRAME: &str = include_str!("renderer/post_effect.wgsl");
90
91/// The line every stitch replaces.
92const SEAM: &str = "// mirage-engine:effect";
93
94/// The bind group an effect's values are read through.
95pub(crate) const GROUP: u32 = 0;
96
97/// The `draw` the depth resolve is built with, which reads no values and
98/// returns the pixel it was passed.
99const UNPAINTED: &str = "fn draw(pixel: Pixel) -> vec4<f32> {\n    return pixel.color;\n}\n";
100
101/// Where a post effect runs in the post chain — the passes that take a
102/// drawn frame to the window: the forward pass, the `Lit` effects, bloom,
103/// the tone map, the `ToneMapped` effects, the UI, then the `OverUi`
104/// effects.
105///
106/// Effects run stage by stage in that order, and within one stage in the
107/// order their set lists them.
108#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
109pub enum EffectStage {
110    /// After the forward pass, over the lit scene, before the bloom and the
111    /// tone map; the only stage a pixel past `1.0` can be read.
112    Lit,
113    /// After the tone map, over the frame as it will be shown, before the
114    /// UI.
115    ToneMapped,
116    /// After the UI, over everything the frame holds. Built without the
117    /// `ui` feature there is no UI pass and this runs after the
118    /// `ToneMapped` effects all the same.
119    OverUi,
120}
121
122impl EffectStage {
123    /// Every stage, in the order the post chain runs them.
124    pub(crate) const ALL: [Self; 3] = [Self::Lit, Self::ToneMapped, Self::OverUi];
125
126    /// Whether this stage draws after the tone map rather than in the
127    /// scene's own high-dynamic-range light.
128    pub(crate) fn after_tone_map(self) -> bool {
129        !matches!(self, Self::Lit)
130    }
131}
132
133/// A pass of a game's own over the whole frame, written in WGSL and
134/// stitched into the engine's own at startup.
135///
136/// Required if you want to change every pixel of the frame after the
137/// scene is drawn: implement it on a type whose fields are the values its
138/// WGSL reads, name that type in [`Game::PostEffects`](crate::Game::PostEffects),
139/// and run it with [`set_post_effect`](crate::FrameContext::set_post_effect).
140pub trait PostEffect: ShaderValues {
141    /// Where in the post chain this effect runs, and so what its `color`
142    /// holds and what its own alpha reaches; see the module's own
143    /// docs.
144    const STAGE: EffectStage;
145
146    /// WGSL declaring `fn draw(pixel: Pixel) -> vec4<f32>`, run once per
147    /// pixel, which returns what this effect writes there.
148    ///
149    /// `Pixel`, `color_at` and `depth_at` are the engine's, and the
150    /// module's own docs state what each of them holds.
151    const SHADER: &'static str;
152}
153
154/// The post effects one game passes its frame through, named together as
155/// [`Game::PostEffects`](crate::Game::PostEffects).
156///
157/// Written by [`post_effects!`](crate::post_effects) and never implemented
158/// by hand; [`NoPostEffects`] for a game that draws the frame as the chain
159/// leaves it.
160pub trait PostEffects: Sealed + 'static {
161    /// The effects startup compiles, in the order the set lists them.
162    #[doc(hidden)]
163    fn declared(into: &mut Declarations);
164
165    /// Seat of the effect this set value holds: `0` for the first effect
166    /// the set names, one more for each after it.
167    #[doc(hidden)]
168    fn seat(&self) -> u32;
169
170    /// Lays out the values of the effect this set value holds.
171    #[doc(hidden)]
172    fn write(&self, into: &mut Vec<u8>);
173}
174
175/// The set of a game that draws the frame as the chain leaves it.
176///
177/// No value of it exists, so such a game runs no pass of its own. `()` is
178/// not an effect set:
179///
180/// ```compile_fail
181/// use mirage_engine::prelude::*;
182///
183/// fn set<E: PostEffects>() {}
184///
185/// set::<()>();
186/// ```
187#[derive(Clone, Debug, Eq, Hash, PartialEq)]
188pub enum NoPostEffects {}
189
190impl Sealed for NoPostEffects {}
191
192impl PostEffects for NoPostEffects {
193    fn declared(_into: &mut Declarations) {}
194
195    fn seat(&self) -> u32 {
196        match *self {}
197    }
198
199    fn write(&self, _into: &mut Vec<u8>) {
200        match *self {}
201    }
202}
203
204/// The effects a set declares, in the order it lists them; the set macro
205/// fills it and startup compiles what it holds.
206#[doc(hidden)]
207#[derive(Default)]
208pub struct Declarations(Vec<Declaration>);
209
210impl Declarations {
211    /// Declares the effect at the next seat.
212    #[doc(hidden)]
213    pub fn declare<S: PostEffect>(&mut self) {
214        self.0.push(Declaration::of::<S>());
215    }
216
217    /// Everything `S` declares, in seat order.
218    pub(crate) fn of<S: PostEffects>() -> Vec<Declaration> {
219        let mut declared = Self::default();
220        S::declared(&mut declared);
221        declared.0
222    }
223}
224
225/// One effect as startup takes it: its own name, where in the post chain it
226/// runs, and the shader that runs it.
227#[derive(Debug)]
228pub(crate) struct Declaration {
229    pub(crate) name: &'static str,
230    pub(crate) stage: EffectStage,
231    pub(crate) source: String,
232}
233
234impl Declaration {
235    fn of<S: PostEffect>() -> Self {
236        Self {
237            name: core::any::type_name::<S>(),
238            stage: S::STAGE,
239            source: stitched(&S::bound(GROUP, "effect"), S::SHADER),
240        }
241    }
242}
243
244/// Which of a game's post effects this is, counted in the order its set lists
245/// them.
246#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
247pub(crate) struct PostEffectId(pub(crate) u32);
248
249/// Writes the set of every post effect a game passes its frame through: an
250/// enum with one variant wrapping each named type, which
251/// [`Game::PostEffects`](crate::Game::PostEffects) names.
252///
253/// Each type is spelled by its own name and must be in scope; a set that
254/// holds a type twice does not compile. [`EffectStage`] orders the effects
255/// first and the order the set lists them second, so two effects in one
256/// stage run in that order. A `pub` before `enum` makes the enum public, and `///` lines
257/// before that are the enum's.
258///
259/// ```
260/// use mirage_engine::prelude::*;
261///
262/// #[derive(ShaderValues)]
263/// struct Grain {
264///     strength: f32,
265/// }
266///
267/// impl PostEffect for Grain {
268///     const STAGE: EffectStage = EffectStage::ToneMapped;
269///     const SHADER: &'static str = "
270///         fn draw(pixel: Pixel) -> vec4<f32> {
271///             let noise = fract(sin(pixel.position.x + pixel.position.y) * 43758.5453);
272///             return vec4<f32>(pixel.color.rgb + noise * effect.strength, pixel.color.a);
273///         }";
274/// }
275///
276/// post_effects! { enum Look { Grain } }
277/// ```
278#[macro_export]
279macro_rules! post_effects {
280    ($(#[$attribute:meta])* $vis:vis enum $set:ident { $($effect:ident),+ $(,)? }) => {
281        $(#[$attribute])*
282        $vis enum $set {
283            $($effect($effect)),+
284        }
285
286        $(
287            impl ::core::convert::From<$effect> for $set {
288                fn from(effect: $effect) -> Self {
289                    Self::$effect(effect)
290                }
291            }
292
293            impl $crate::Holds<$effect> for $set {}
294        )+
295
296        impl $crate::Sealed for $set {}
297
298        impl $crate::PostEffects for $set {
299            fn declared(into: &mut $crate::PostEffectDeclarations) {
300                $(into.declare::<$effect>();)+
301            }
302
303            fn seat(&self) -> u32 {
304                let mut at = 0;
305                $(
306                    if ::core::matches!(self, Self::$effect(_)) {
307                        return at;
308                    }
309                    at += 1;
310                )+
311                at
312            }
313
314            fn write(&self, into: &mut ::std::vec::Vec<u8>) {
315                match self {
316                    $(Self::$effect(values) => $crate::ShaderValues::write(values, into)),+
317                }
318            }
319        }
320    };
321}
322
323/// The shader the depth resolve is drawn with: the engine's own, with the
324/// seam left returning what it was passed.
325pub(crate) fn unpainted() -> String {
326    stitched("", UNPAINTED)
327}
328
329/// The shader one effect is drawn with: the engine's own, with the effect's
330/// values declared and its code called where the seam marks.
331fn stitched(values: &str, draw: &str) -> String {
332    let mut code = String::from(values);
333    code.push_str(draw);
334    FRAME.replace(SEAM, &code)
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    /// An effect with values of its own, written the way a game writes one.
342    #[derive(crate::ShaderValues)]
343    struct Vignette {
344        strength: f32,
345    }
346
347    impl PostEffect for Vignette {
348        const STAGE: EffectStage = EffectStage::ToneMapped;
349        const SHADER: &'static str =
350            "fn draw(pixel: Pixel) -> vec4<f32> { return pixel.color * effect.strength; }";
351    }
352
353    /// An effect that reads no values, and reads the scene's depth instead.
354    #[derive(crate::ShaderValues)]
355    struct Fog;
356
357    impl PostEffect for Fog {
358        const STAGE: EffectStage = EffectStage::Lit;
359        const SHADER: &'static str =
360            "fn draw(pixel: Pixel) -> vec4<f32> { return vec4<f32>(depth_at(pixel.uv)); }";
361    }
362
363    post_effects! { enum Look { Fog, Vignette } }
364
365    #[test]
366    fn the_shader_carries_one_seam_for_an_effect_to_be_stitched_into() {
367        assert_eq!(FRAME.matches(SEAM).count(), 1);
368    }
369
370    #[test]
371    fn an_effects_own_code_is_called_from_the_seam_and_its_values_are_bound() {
372        let source = Declaration::of::<Vignette>().source;
373
374        assert!(!source.contains(SEAM), "the seam itself is replaced");
375        assert!(source.contains("struct Vignette"));
376        assert!(source.contains("@group(0) @binding(0) var<uniform> effect: Vignette;"));
377        assert!(
378            source.find("struct Vignette") < source.find("fn draw(pixel: Pixel)"),
379            "and the values are declared before the code that reads them"
380        );
381    }
382
383    #[test]
384    fn an_effect_with_no_fields_reads_no_values_and_binds_none() {
385        let source = Declaration::of::<Fog>().source;
386
387        assert!(!source.contains("@group(0)"));
388        assert!(source.contains("fn draw(pixel: Pixel)"));
389    }
390
391    #[test]
392    fn a_set_declares_its_effects_in_the_order_it_names_them() {
393        let declared = Declarations::of::<Look>();
394
395        assert_eq!(
396            declared
397                .iter()
398                .map(|effect| effect.stage)
399                .collect::<Vec<_>>(),
400            vec![EffectStage::Lit, EffectStage::ToneMapped]
401        );
402        assert_eq!(
403            (
404                Look::from(Fog).seat(),
405                Look::from(Vignette { strength: 0.0 }).seat()
406            ),
407            (0, 1)
408        );
409        assert!(Declarations::of::<NoPostEffects>().is_empty());
410    }
411
412    #[test]
413    fn a_set_value_lays_out_the_values_of_the_effect_it_holds() {
414        let mut written = Vec::new();
415        Look::from(Vignette { strength: 0.25 }).write(&mut written);
416
417        assert_eq!(
418            f32::from_le_bytes(written[..4].try_into().expect("four bytes")),
419            0.25
420        );
421
422        let mut none = Vec::new();
423        Look::from(Fog).write(&mut none);
424        assert!(none.is_empty(), "where an effect reads nothing");
425    }
426
427    #[test]
428    fn the_places_run_the_scene_first_and_what_is_over_the_ui_last() {
429        assert_eq!(
430            EffectStage::ALL,
431            [
432                EffectStage::Lit,
433                EffectStage::ToneMapped,
434                EffectStage::OverUi
435            ]
436        );
437        assert!(
438            EffectStage::Lit < EffectStage::ToneMapped
439                && EffectStage::ToneMapped < EffectStage::OverUi
440        );
441        assert_eq!(
442            EffectStage::ALL.map(EffectStage::after_tone_map),
443            [false, true, true],
444            "and only the first draws in the scene's own light"
445        );
446    }
447}