Skip to main content

incular_rendering/
backdrop.rs

1//! Renderer-neutral execution plan for backdrop-filter stages: whether a
2//! stage runs at all, which stage captures the backdrop and which reuse a
3//! capture (Flutter's `BackdropKey`), and where `ImageFilter.matrix`
4//! samples the backdrop in device pixels.
5
6use std::collections::HashMap;
7
8use incular_core::Transform;
9
10use crate::{BackdropKey, GaussianBlur, LayerId};
11
12/// Whether a backdrop-filter stage changes anything: it is enabled and has
13/// a matrix or a blur with a nonzero sigma. An inactive stage paints its
14/// child as if the stage were not there.
15#[must_use]
16pub fn backdrop_filter_is_active(
17    blur: GaussianBlur,
18    enabled: bool,
19    matrix: Option<Transform>,
20) -> bool {
21    enabled && (matrix.is_some() || blur.sigma_x > f32::EPSILON || blur.sigma_y > f32::EPSILON)
22}
23
24/// Where a backdrop-filter stage gets the backdrop it filters.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum BackdropSample {
27    /// The stage captures what has been painted into its target so far.
28    Capture,
29    /// The stage filters the capture another stage with the same
30    /// [`BackdropKey`] took earlier in the frame.
31    Shared(LayerId),
32}
33
34/// The backdrop captures of one frame, in paint order.
35///
36/// Flutter's `BackdropGroup`: stages that share a [`BackdropKey`] sample one
37/// backdrop, captured where the first of them paints, so a later stage does
38/// not see an earlier one's output. Sharing is per render target (`scope`):
39/// a stage painting into an isolated layer, such as an opacity group, reads
40/// that layer, and never a capture of another target.
41#[derive(Clone, Debug, Default)]
42pub struct BackdropCaptures {
43    captured: HashMap<(u64, BackdropKey), LayerId>,
44}
45
46impl BackdropCaptures {
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Forgets every capture, for the start of a frame.
53    pub fn clear(&mut self) {
54        self.captured.clear();
55    }
56
57    /// How the stage `layer` gets its backdrop when it is reached in paint
58    /// order, painting into the target `scope`. An unkeyed stage always
59    /// captures; the first stage of a key in a scope captures and the
60    /// later ones share it.
61    pub fn sample(
62        &mut self,
63        layer: LayerId,
64        key: Option<BackdropKey>,
65        scope: u64,
66    ) -> BackdropSample {
67        let Some(key) = key else {
68            return BackdropSample::Capture;
69        };
70        match self.captured.get(&(scope, key)) {
71            Some(&first) => BackdropSample::Shared(first),
72            None => {
73                self.captured.insert((scope, key), layer);
74                BackdropSample::Capture
75            }
76        }
77    }
78}
79
80/// `ImageFilter.matrix` applied to a backdrop, in device pixels: the map
81/// from an output pixel position to the backdrop position it shows.
82///
83/// The matrix moves the backdrop in the stage's coordinates: content at a
84/// point `p` of the stage moves to `matrix · p`. `stage` maps the stage's
85/// coordinates to the target's logical coordinates and `scale` logical to
86/// device pixels, so an output pixel `d` shows the backdrop at
87/// `S · W · M⁻¹ · W⁻¹ · S⁻¹ · d`. `None` when the matrix or the stage
88/// transform cannot be inverted, which leaves nothing to sample.
89#[must_use]
90pub fn backdrop_matrix_sampling(
91    stage: Transform,
92    matrix: Transform,
93    scale: f32,
94) -> Option<Transform> {
95    let device = Transform::scale(scale);
96    let stage_inverse = stage.inverse()?;
97    let matrix_inverse = matrix.inverse()?;
98    let device_inverse = device.inverse()?;
99    Some(
100        device
101            .then(stage)
102            .then(matrix_inverse)
103            .then(stage_inverse)
104            .then(device_inverse),
105    )
106}