Skip to main content

gpui/
layer_effect.rs

1//! Bounded isolation and retained results for explicitly captured paint subtrees.
2use crate::*;
3use anyhow::{Result, ensure};
4use std::sync::Arc;
5
6/// Maximum captured groups in one frame (including nested groups).
7pub const MAX_LAYER_GROUPS: usize = 4;
8/// Maximum capture width or height in physical pixels.
9pub const MAX_LAYER_SIDE: u32 = 1024;
10/// Maximum retained command identities per group.
11pub const MAX_LAYER_COMMANDS: usize = 65_536;
12
13/// Working resolution for a captured subtree Gaussian blur.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum LayerBlurQuality {
16    /// Full resolution below 4 device pixels, half below 8, quarter otherwise.
17    #[default]
18    Adaptive,
19    /// Full-resolution reference, preserving the original Gaussian sampling.
20    FullResolution,
21}
22
23/// Blend a completed isolated subtree with its saved destination in encoded sRGB.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25#[repr(u8)]
26pub enum LayerBlendMode {
27    /// Ordinary source-over composition.
28    #[default]
29    Normal = 0,
30    /// Multiply straight source and destination RGB.
31    Multiply = 1,
32    /// Complementary multiply.
33    Screen = 2,
34    /// Multiply/screen selected by destination RGB.
35    Overlay = 3,
36    /// Brighten destination by dividing by complementary source RGB.
37    ColorDodge = 4,
38    /// Smooth source-controlled darkening or lightening.
39    SoftLight = 5,
40    /// Clamp source plus destination minus one.
41    LinearBurn = 6,
42}
43
44/// Encoded-sRGB color operations in saturation/brightness/contrast/inversion order,
45/// followed by group opacity and destination blending.
46#[derive(Clone, Copy, Debug)]
47pub struct LayerColorFilter {
48    /// Working resolution policy for subtree blur.
49    pub blur_quality: LayerBlurQuality,
50    /// Gaussian support radius in logical pixels; zero disables blur.
51    pub blur_radius: Pixels,
52    /// Destination blend, applied after filtering and group opacity.
53    pub blend_mode: LayerBlendMode,
54    /// Saturation in [0,2], with 1 unchanged.
55    pub saturation: f32,
56    /// Brightness multiplier in [0,2], with 1 unchanged.
57    pub brightness: f32,
58    /// Contrast around straight RGB 0.5, in [0,2], with 1 unchanged.
59    pub contrast: f32,
60    /// Inversion strength in [0,1], with 0 unchanged and 1 complemented RGB.
61    pub invert: f32,
62}
63impl Default for LayerColorFilter {
64    fn default() -> Self {
65        Self {
66            blur_quality: LayerBlurQuality::Adaptive,
67            blur_radius: px(0.),
68            blend_mode: LayerBlendMode::Normal,
69            saturation: 1.,
70            brightness: 1.,
71            contrast: 1.,
72            invert: 0.,
73        }
74    }
75}
76
77/// Immutable capture description shared by its paired scene boundaries.
78#[derive(Debug)]
79pub struct LayerCapture {
80    /// Working resolution policy for subtree blur.
81    pub blur_quality: LayerBlurQuality,
82    /// Gaussian support radius in device pixels, in [0,64].
83    pub blur_radius: ScaledPixels,
84    /// Destination blend for the completed group.
85    pub blend_mode: LayerBlendMode,
86    /// Source sampling rectangle, including any halo; bounded by the viewport/parent.
87    pub bounds: Bounds<ScaledPixels>,
88    /// Visible output clip, applied after filtering. None uses the source bounds.
89    pub clip: Option<Bounds<ScaledPixels>>,
90    /// Opacity of the completed group, including inherited opacity exactly once.
91    pub opacity: f32,
92    /// Encoded-sRGB saturation in [0,2], with 0 grayscale and 1 unchanged.
93    pub saturation: f32,
94    /// Encoded-sRGB brightness multiplier in [0,2].
95    pub brightness: f32,
96    /// Encoded-sRGB contrast in [0,2], centered on straight RGB 0.5.
97    pub contrast: f32,
98    /// Inversion strength in [0,1], with 0 unchanged and 1 complemented RGB.
99    pub invert: f32,
100    /// Shared encoded-sRGB capture/composition shader.
101    pub program: Arc<SceneShaderProgram>,
102}
103/// A begin/end marker. The renderer must preserve these global pass barriers.
104#[derive(Clone, Debug)]
105pub struct LayerBoundary {
106    /// Scene order.
107    pub order: DrawOrder,
108    /// Full viewport ordering fence.
109    pub bounds: Bounds<ScaledPixels>,
110    /// Full viewport mask for the ordering fence.
111    pub content_mask: ContentMask<ScaledPixels>,
112    /// Paired immutable description.
113    pub capture: Arc<LayerCapture>,
114    /// True at entry, false at exit.
115    pub begin: bool,
116}
117/// Checked capture plan, independent of the GPU backend.
118#[derive(Clone, Debug)]
119pub struct LayerPlan {
120    /// Begin marker index in Scene::layer_boundaries.
121    pub begin_index: usize,
122    /// End marker index in Scene::layer_boundaries.
123    pub end_index: usize,
124    /// Source rectangle origin.
125    pub origin: [u32; 2],
126    /// Physical texture extent.
127    pub size: [u32; 2],
128    /// Clear, composition, horizontal and vertical blur blocks (six float4 each).
129    pub parameters: [[[f32; 4]; 6]; 4],
130    /// Device-pixel Gaussian support radius; zero skips filter passes.
131    pub blur_radius: f32,
132    /// Horizontal and vertical filter target extent.
133    pub blur_size: [u32; 2],
134    /// Exact immutable command identities; preserved by Scene::replay.
135    pub revision: Arc<[u64]>,
136    /// Live external content cannot use retained filtered results.
137    pub reusable: bool,
138}
139/// Cumulative statistics, available through headless renderer test support.
140#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
141pub struct LayerCacheStats {
142    /// Group results rendered and captured.
143    pub captures: u64,
144    /// Retained group results reused without drawing their descendants.
145    pub hits: u64,
146}
147impl LayerPlan {
148    pub(crate) fn new(
149        capture: &LayerCapture,
150        viewport: Size<DevicePixels>,
151        begin_index: usize,
152    ) -> Result<Self> {
153        ensure!(
154            capture.program.output_encoding() == ShaderOutputEncoding::EncodedSrgb
155                && !capture.program.uses_glyph_mask()
156                && !capture.program.uses_path_mask(),
157            "layer capture requires an SDR composition program"
158        );
159        let b = capture.bounds;
160        ensure!(
161            [
162                b.origin.x.0,
163                b.origin.y.0,
164                b.size.width.0,
165                b.size.height.0,
166                capture.opacity,
167                capture.saturation,
168                capture.brightness,
169                capture.contrast,
170                capture.invert,
171                capture.blur_radius.0
172            ]
173            .iter()
174            .all(|v| v.is_finite()),
175            "non-finite layer geometry"
176        );
177        ensure!(
178            viewport.width.0 > 0
179                && viewport.height.0 > 0
180                && b.size.width.0 > 0.
181                && b.size.height.0 > 0.,
182            "empty layer capture"
183        );
184        ensure!(
185            (0. ..=1.).contains(&capture.opacity),
186            "invalid group opacity"
187        );
188        ensure!(
189            capture.saturation.is_finite() && (0. ..=2.).contains(&capture.saturation),
190            "invalid group saturation"
191        );
192        ensure!(
193            (0. ..=2.).contains(&capture.brightness),
194            "invalid group brightness"
195        );
196        ensure!(
197            (0. ..=2.).contains(&capture.contrast),
198            "invalid group contrast"
199        );
200        ensure!(
201            (0. ..=1.).contains(&capture.invert),
202            "invalid group inversion"
203        );
204        let clamp_x = |x: f32| x.max(0.).min(viewport.width.0 as f32) as u32;
205        let clamp_y = |y: f32| y.max(0.).min(viewport.height.0 as f32) as u32;
206        let origin = [clamp_x(b.origin.x.0.floor()), clamp_y(b.origin.y.0.floor())];
207        let end = [
208            clamp_x((b.origin.x.0 + b.size.width.0).ceil()),
209            clamp_y((b.origin.y.0 + b.size.height.0).ceil()),
210        ];
211        let size = [
212            end[0].saturating_sub(origin[0]),
213            end[1].saturating_sub(origin[1]),
214        ];
215        ensure!(
216            size.iter().all(|v| *v > 0 && *v <= MAX_LAYER_SIDE),
217            "layer capture must fit within 1024x1024 device pixels"
218        );
219        ensure!(
220            (0. ..=64.).contains(&capture.blur_radius.0),
221            "layer blur radius must be in [0,64] device pixels"
222        );
223        let factor = match capture.blur_quality {
224            LayerBlurQuality::FullResolution => 1,
225            LayerBlurQuality::Adaptive if capture.blur_radius.0 >= 8. => 4,
226            LayerBlurQuality::Adaptive if capture.blur_radius.0 >= 4. => 2,
227            LayerBlurQuality::Adaptive => 1,
228        };
229        let blur_size = size.map(|v| v.div_ceil(factor));
230        let output_clip = capture.clip.unwrap_or(b);
231        ensure!(
232            [
233                output_clip.origin.x.0,
234                output_clip.origin.y.0,
235                output_clip.size.width.0,
236                output_clip.size.height.0
237            ]
238            .iter()
239            .all(|v| v.is_finite())
240                && output_clip.size.width.0 >= 0.
241                && output_clip.size.height.0 >= 0.,
242            "layer output clip must be finite and nonnegative"
243        );
244        let clip = output_clip.intersect(&Bounds::new(
245            Point::default(),
246            viewport.map(|p| ScaledPixels(p.0 as f32)),
247        ));
248        ensure!(
249            [
250                clip.origin.x.0,
251                clip.origin.y.0,
252                clip.size.width.0,
253                clip.size.height.0
254            ]
255            .iter()
256            .all(|v| v.is_finite())
257                && clip.intersect(&b) == clip,
258            "layer output clip must fit within its source capture"
259        );
260        let mut parameters = [[[0.; 4]; 6]; 4];
261        for pass in 0..2 {
262            parameters[pass][0] = [
263                viewport.width.0 as f32,
264                viewport.height.0 as f32,
265                3. + pass as f32,
266                0.,
267            ];
268            parameters[pass][1] = [
269                origin[0] as f32,
270                origin[1] as f32,
271                size[0] as f32,
272                size[1] as f32,
273            ];
274            parameters[pass][2] = [0., 0., 1., 1.];
275            parameters[pass][3] = [
276                origin[0] as f32,
277                origin[1] as f32,
278                end[0] as f32,
279                end[1] as f32,
280            ];
281            if pass == 1 {
282                parameters[pass][3] =
283                    [clip.left().0, clip.top().0, clip.right().0, clip.bottom().0];
284            }
285            parameters[pass][4][2] = capture.opacity;
286            parameters[pass][4][3] = capture.saturation;
287            parameters[pass][5] = [
288                capture.brightness,
289                capture.contrast,
290                capture.invert,
291                capture.blend_mode as u8 as f32,
292            ];
293        }
294        for pass in 0..2 {
295            parameters[pass + 2][0] = [blur_size[0] as f32, blur_size[1] as f32, 5., 0.];
296            parameters[pass + 2][1] = [0., 0., blur_size[0] as f32, blur_size[1] as f32];
297            parameters[pass + 2][2] = [0., 0., 1., 1.];
298            parameters[pass + 2][4] = [
299                if pass == 0 { 1. } else { 0. },
300                if pass == 1 { 1. } else { 0. },
301                capture.blur_radius.0 * blur_size[pass] as f32 / size[pass] as f32,
302                0.,
303            ];
304        }
305        Ok(Self {
306            blur_radius: capture.blur_radius.0,
307            blur_size,
308            begin_index,
309            end_index: 0,
310            origin,
311            size,
312            parameters,
313            revision: Arc::from([]),
314            reusable: true,
315        })
316    }
317    /// Whether the two Gaussian passes must run for this frame. Raw child pixels
318    /// can survive radius edits, but filtered pixels must match the current radius.
319    pub fn needs_blur(&self, previous: Option<&Self>) -> bool {
320        self.blur_radius > 0.
321            && previous.is_none_or(|old| {
322                !self.matches(old)
323                    || old.blur_radius != self.blur_radius
324                    || old.blur_size != self.blur_size
325            })
326    }
327    /// Whether a previous raw capture is safe to reuse. Own opacity/color filters
328    /// are evaluated at composition and do not invalidate the raw child pixels.
329    pub fn matches(&self, previous: &Self) -> bool {
330        self.reusable
331            && previous.reusable
332            && self.origin == previous.origin
333            && self.size == previous.size
334            && self.revision == previous.revision
335    }
336}