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