Skip to main content

cranpose_render_common/
layer_composition.rs

1use cranpose_ui_graphics::{BlendMode, CompositingStrategy, GraphicsLayer, RenderEffect};
2
3#[derive(Clone)]
4pub struct LayerIsolation {
5    pub effect: Option<RenderEffect>,
6    pub blend_mode: BlendMode,
7    pub composite_alpha: f32,
8}
9
10pub fn layer_requires_isolation(layer: &GraphicsLayer) -> bool {
11    let has_effect = layer.render_effect.is_some();
12    let has_layer_blend = layer.blend_mode != BlendMode::SrcOver;
13    match layer.compositing_strategy {
14        CompositingStrategy::Offscreen => true,
15        CompositingStrategy::Auto => has_effect || has_layer_blend || layer.alpha < 1.0,
16        CompositingStrategy::ModulateAlpha => has_effect || has_layer_blend,
17    }
18}
19
20fn isolation_composite_alpha(layer: &GraphicsLayer) -> f32 {
21    if layer.compositing_strategy == CompositingStrategy::ModulateAlpha {
22        // The float-fold branch, which HWUI takes when nothing overlaps: the
23        // alpha never becomes a byte there, so nothing to snap.
24        1.0
25    } else {
26        GraphicsLayer::composite_alpha_8bit(layer.alpha)
27    }
28}
29
30/// The alpha a layer's own contents carry when the backend has no offscreen to
31/// isolate them in and folds the layer's alpha into them instead.
32///
33/// Folding is an approximation — it fades a subtree's overlapping parts
34/// separately where a real layer fades the composited result — but the alpha
35/// it folds is not a matter of taste: for a layer that would have been
36/// isolated it is the composite's byte, so the two backends land on the same
37/// pixel wherever the approximation is exact at all.
38fn folded_layer_alpha(layer: &GraphicsLayer) -> f32 {
39    if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
40        && layer_requires_isolation(layer)
41    {
42        GraphicsLayer::composite_alpha_8bit(layer.alpha)
43    } else {
44        layer.alpha
45    }
46}
47
48pub fn effective_layer_isolation(layer: &GraphicsLayer) -> Option<LayerIsolation> {
49    layer_requires_isolation(layer).then(|| LayerIsolation {
50        effect: layer.render_effect.clone(),
51        blend_mode: layer.blend_mode,
52        composite_alpha: isolation_composite_alpha(layer),
53    })
54}
55
56/// The composite alpha and blend mode an isolated layer contributes at its
57/// parent, for callers that never read the isolation's render effect and would
58/// otherwise deep-clone it once per layer per frame.
59pub fn layer_composite_params(layer: &GraphicsLayer) -> Option<(f32, BlendMode)> {
60    layer_requires_isolation(layer).then(|| (isolation_composite_alpha(layer), layer.blend_mode))
61}
62
63pub fn layer_for_content(
64    layer: &GraphicsLayer,
65    isolation: Option<&LayerIsolation>,
66) -> GraphicsLayer {
67    let mut content = layer.clone();
68    if isolation.is_some() && layer.compositing_strategy != CompositingStrategy::ModulateAlpha {
69        content.alpha = 1.0;
70    }
71    content
72}
73
74pub fn local_content_layer(layer: &GraphicsLayer) -> GraphicsLayer {
75    GraphicsLayer {
76        alpha: folded_layer_alpha(layer),
77        color_filter: layer.color_filter,
78        ..GraphicsLayer::default()
79    }
80}
81
82/// `local_content_layer(&layer_for_content(layer, isolation))` without building
83/// either intermediate. The content layer differs from `layer` only in the
84/// alpha the isolation moves to the composite step, and the local layer keeps
85/// just that alpha and the colour filter, so the two clones the composed form
86/// performs — one `GraphicsLayer`, one `RenderEffect` — are pure waste.
87pub fn local_content_layer_for(layer: &GraphicsLayer) -> GraphicsLayer {
88    let alpha = if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
89        && layer_requires_isolation(layer)
90    {
91        1.0
92    } else {
93        layer.alpha
94    };
95    GraphicsLayer {
96        alpha,
97        color_filter: layer.color_filter,
98        ..GraphicsLayer::default()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn auto_alpha_triggers_isolation_with_composite_alpha() {
108        let layer = GraphicsLayer {
109            alpha: 0.5,
110            compositing_strategy: CompositingStrategy::Auto,
111            ..Default::default()
112        };
113        let isolation = effective_layer_isolation(&layer).expect("expected isolation");
114        assert!(isolation.effect.is_none());
115        // A half composites at 127/255, not at a half: `(int)(0.5f * 255)`.
116        assert!((isolation.composite_alpha - 127.0 / 255.0).abs() < 1e-6);
117
118        let content = layer_for_content(&layer, Some(&isolation));
119        assert!((content.alpha - 1.0).abs() < 1e-6);
120    }
121
122    #[test]
123    fn a_backend_without_an_offscreen_folds_the_composites_byte_not_the_float() {
124        // The software path has no offscreen, so it folds the layer's alpha
125        // into the contents. What it folds has to be the alpha the isolating
126        // path composites at, or the two backends part company by a level.
127        let layer = GraphicsLayer {
128            alpha: 0.88,
129            compositing_strategy: CompositingStrategy::Auto,
130            ..Default::default()
131        };
132        let folded = local_content_layer(&layer);
133        let (composite_alpha, _) = layer_composite_params(&layer).expect("expected isolation");
134        assert!((folded.alpha - composite_alpha).abs() < 1e-6);
135        assert!((folded.alpha - 224.0 / 255.0).abs() < 1e-6);
136
137        // A layer that names ModulateAlpha is asking for the float fold, which
138        // is the branch HWUI takes when nothing overlaps, so it keeps it.
139        let modulated = GraphicsLayer {
140            alpha: 0.88,
141            compositing_strategy: CompositingStrategy::ModulateAlpha,
142            ..Default::default()
143        };
144        assert_eq!(local_content_layer(&modulated).alpha, 0.88);
145    }
146
147    #[test]
148    fn modulate_alpha_keeps_in_place_alpha_without_offscreen() {
149        let layer = GraphicsLayer {
150            alpha: 0.5,
151            compositing_strategy: CompositingStrategy::ModulateAlpha,
152            ..Default::default()
153        };
154        assert!(effective_layer_isolation(&layer).is_none());
155    }
156
157    #[test]
158    fn non_src_over_layer_blend_triggers_isolation() {
159        let layer = GraphicsLayer {
160            blend_mode: BlendMode::DstOut,
161            compositing_strategy: CompositingStrategy::Auto,
162            ..Default::default()
163        };
164        let isolation = effective_layer_isolation(&layer).expect("expected blend isolation");
165        assert_eq!(isolation.blend_mode, BlendMode::DstOut);
166        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
167    }
168
169    #[test]
170    fn offscreen_isolation_has_no_effect_payload() {
171        let layer = GraphicsLayer {
172            alpha: 1.0,
173            compositing_strategy: CompositingStrategy::Offscreen,
174            ..Default::default()
175        };
176        let isolation = effective_layer_isolation(&layer).expect("expected isolation");
177        assert!(isolation.effect.is_none());
178        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
179    }
180
181    #[test]
182    fn render_effect_forces_isolation_even_with_modulate_alpha() {
183        let layer = GraphicsLayer {
184            alpha: 0.4,
185            compositing_strategy: CompositingStrategy::ModulateAlpha,
186            render_effect: Some(RenderEffect::blur(4.0)),
187            ..Default::default()
188        };
189        let isolation = effective_layer_isolation(&layer).expect("expected effect isolation");
190        assert!(isolation.effect.is_some());
191        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
192
193        let content = layer_for_content(&layer, Some(&isolation));
194        assert!((content.alpha - layer.alpha).abs() < 1e-6);
195    }
196
197    #[test]
198    fn local_content_layer_keeps_only_local_alpha_and_color_filter() {
199        let layer = GraphicsLayer {
200            alpha: 0.25,
201            color_filter: Some(cranpose_ui_graphics::ColorFilter::Tint(
202                cranpose_ui_graphics::Color::RED,
203            )),
204            shadow_elevation: 6.0,
205            translation_x: 14.0,
206            clip: true,
207            ..Default::default()
208        };
209
210        let local = local_content_layer(&layer);
211        assert!((local.alpha - 63.0 / 255.0).abs() < 1e-6);
212        assert_eq!(local.color_filter, layer.color_filter);
213        assert_eq!(local.shadow_elevation, 0.0);
214        assert_eq!(local.translation_x, 0.0);
215        assert!(!local.clip);
216    }
217
218    #[test]
219    fn local_content_layer_for_matches_the_composed_form() {
220        let filter = Some(cranpose_ui_graphics::ColorFilter::Tint(
221            cranpose_ui_graphics::Color::RED,
222        ));
223        let cases = [
224            GraphicsLayer::default(),
225            GraphicsLayer {
226                alpha: 0.5,
227                color_filter: filter,
228                compositing_strategy: CompositingStrategy::Auto,
229                ..Default::default()
230            },
231            GraphicsLayer {
232                alpha: 0.5,
233                compositing_strategy: CompositingStrategy::ModulateAlpha,
234                render_effect: Some(RenderEffect::blur(4.0)),
235                ..Default::default()
236            },
237            GraphicsLayer {
238                alpha: 0.5,
239                compositing_strategy: CompositingStrategy::Offscreen,
240                ..Default::default()
241            },
242            GraphicsLayer {
243                blend_mode: BlendMode::DstOut,
244                compositing_strategy: CompositingStrategy::Auto,
245                ..Default::default()
246            },
247        ];
248
249        for layer in cases {
250            let isolation = effective_layer_isolation(&layer);
251            let composed = local_content_layer(&layer_for_content(&layer, isolation.as_ref()));
252            let direct = local_content_layer_for(&layer);
253            assert!((composed.alpha - direct.alpha).abs() < 1e-6);
254            assert_eq!(composed.color_filter, direct.color_filter);
255        }
256    }
257
258    #[test]
259    fn layer_composite_params_match_the_isolation_it_replaces() {
260        let cases = [
261            GraphicsLayer::default(),
262            GraphicsLayer {
263                alpha: 0.25,
264                compositing_strategy: CompositingStrategy::Auto,
265                ..Default::default()
266            },
267            GraphicsLayer {
268                alpha: 0.25,
269                compositing_strategy: CompositingStrategy::ModulateAlpha,
270                render_effect: Some(RenderEffect::blur(4.0)),
271                ..Default::default()
272            },
273            GraphicsLayer {
274                blend_mode: BlendMode::DstOut,
275                compositing_strategy: CompositingStrategy::Auto,
276                ..Default::default()
277            },
278        ];
279
280        for layer in cases {
281            let isolation = effective_layer_isolation(&layer);
282            let expected =
283                isolation.map(|isolation| (isolation.composite_alpha, isolation.blend_mode));
284            assert_eq!(expected, layer_composite_params(&layer));
285        }
286    }
287}