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}