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