Skip to content

Commit cf30ec7

Browse files
committed
Add bounded retained backdrop filters
1 parent bf948d8 commit cf30ec7

13 files changed

Lines changed: 490 additions & 13 deletions

‎docs/validation/css-retained-effect-values-256.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ This gate covers the first product-neutral slice of issue #256:
1212
Retained-scene mask, clip, filter, and backdrop painting is outside this slice
1313
and remains separately qualified by later issue #256 work.
1414

15+
## Backdrop command extension — 19 September 2026
16+
17+
Issue #503 extends this contract with a bounded kind-48 retained command. The
18+
resource preserves authored `blur()`/`saturate()` order, the command carries
19+
rounded output bounds and maximum blur sigma, and localized damage includes a
20+
backdrop when earlier sampled content changes. The producer forces these boxes
21+
into the foreground phase so reference presenters execute the command after
22+
retained canvas/GPU layers and before the element background.
23+
24+
The native regression now authors 4,096 backdrop commands, the WebKit alias,
25+
the Code OSS `blur(8px) saturate(1.08)` sequence, phase metadata, and resource
26+
bounds. Avalonia 12 records Skia backdrop save layers; Flutter replays bounded
27+
prior content through nested filters. AppScene #174 owns the packaged native
28+
Skia/Graphite consumer.
29+
30+
These additions have not been executed under the current fast-merge directive.
31+
Build, WPT, pixels, runtime, performance, memory, and package results remain
32+
deferred and must not be inferred from the historical results below.
33+
1534
## Direct gates
1635

1736
| Gate | Denominator | Result |

‎experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
],
152152
"nativeStorageOnlyProperties": [
153153
"accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state",
154-
"appearance", "backdrop-filter", "background-attachment", "background-clip", "background-origin",
154+
"appearance", "backdrop-filter", "-webkit-backdrop-filter", "background-attachment", "background-clip", "background-origin",
155155
"background-position-x", "background-position-y", "border-bottom-style", "border-left-style", "border-right-style",
156156
"border-top-style", "clear", "clip-path", "clip-rule", "color-scheme",
157157
"column-count", "columns", "css-float", "direction", "empty-cells",
@@ -237,7 +237,7 @@
237237
"supportedPropertyExtras": [
238238
"animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode",
239239
"animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function",
240-
"accent-color", "appearance", "backdrop-filter", "background-attachment", "background-clip",
240+
"accent-color", "appearance", "backdrop-filter", "-webkit-backdrop-filter", "background-attachment", "background-clip",
241241
"background-origin", "background-position", "background-position-x", "background-position-y",
242242
"background-repeat", "background-size", "border-block", "border-bottom", "border-collapse", "border-inline", "border-left",
243243
"border-right", "border-spacing", "border-top", "clear", "column-count", "columns", "css-float",

‎experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,15 @@ inline constexpr std::array native_typed_property_identity_catalog{
209209
native_typed_property_identity{"contain-intrinsic-size", css_property_id::contain_intrinsic_size},
210210
};
211211

212-
inline constexpr std::array<std::string_view, 62> native_storage_only_property_catalog{
212+
inline constexpr std::array<std::string_view, 63> native_storage_only_property_catalog{
213213
"accent-color",
214214
"align-content",
215215
"animation-direction",
216216
"animation-fill-mode",
217217
"animation-play-state",
218218
"appearance",
219219
"backdrop-filter",
220+
"-webkit-backdrop-filter",
220221
"background-attachment",
221222
"background-clip",
222223
"background-origin",

‎experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct cssom_supported_property_metadata final {
66

77
inline constexpr std::array cssom_supported_property_catalog{
88
cssom_supported_property_metadata{"-moz-transform", "MozTransform"},
9+
cssom_supported_property_metadata{"-webkit-backdrop-filter", "WebkitBackdropFilter"},
910
cssom_supported_property_metadata{"-webkit-font-smoothing", "WebkitFontSmoothing"},
1011
cssom_supported_property_metadata{"-webkit-mask", "WebkitMask"},
1112
cssom_supported_property_metadata{"-webkit-mask-composite", "WebkitMaskComposite"},
@@ -246,4 +247,4 @@ inline constexpr std::array cssom_supported_property_catalog{
246247
cssom_supported_property_metadata{"zoom", "zoom"},
247248
};
248249

249-
inline constexpr auto cssom_style_template_property_accessor_count = 435U;
250+
inline constexpr auto cssom_style_template_property_accessor_count = 437U;

‎experiments/WebScene.NativeEngine.Probe/native/webscene_css_declarations.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ inline void append_declaration(
2525
const auto custom = name.starts_with("--");
2626
if (custom && !valid_custom_property_name(name)) return;
2727
if (name == "-webkit-mask") name = "mask";
28+
else if (name == "-webkit-backdrop-filter") name = "backdrop-filter";
2829
else if (name == "-webkit-mask-image") name = "mask-image";
2930
else if (name == "-webkit-mask-position") name = "mask-position";
3031
else if (name == "-webkit-mask-size") name = "mask-size";

‎experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc‎

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,132 @@ void native_document::append_scene(
467467
}
468468
return result;
469469
}();
470+
struct retained_backdrop_effect final {
471+
bool present{};
472+
std::string resource;
473+
float maximum_blur_sigma{};
474+
};
475+
const auto backdrop_effect = [&]() {
476+
retained_backdrop_effect result{};
477+
const auto& effects = node.style.textual().effect_values;
478+
const auto known = effects.find("backdrop-filter");
479+
if (known == effects.end()) return result;
480+
auto remaining = std::string_view{known->second};
481+
const auto trim = [](std::string_view value) {
482+
while (!value.empty()
483+
&& std::isspace(static_cast<unsigned char>(value.front()))) {
484+
value.remove_prefix(1U);
485+
}
486+
while (!value.empty()
487+
&& std::isspace(static_cast<unsigned char>(value.back()))) {
488+
value.remove_suffix(1U);
489+
}
490+
return value;
491+
};
492+
const auto equals_ascii_case_insensitive = [](std::string_view left,
493+
std::string_view right) {
494+
if (left.size() != right.size()) return false;
495+
for (size_t index = 0U; index < left.size(); ++index) {
496+
const auto character = static_cast<unsigned char>(left[index]);
497+
const auto lower = character >= 'A' && character <= 'Z'
498+
? static_cast<char>(character - 'A' + 'a')
499+
: static_cast<char>(character);
500+
if (lower != right[index]) return false;
501+
}
502+
return true;
503+
};
504+
remaining = trim(remaining);
505+
if (remaining.empty()
506+
|| equals_ascii_case_insensitive(remaining, "none")) {
507+
return result;
508+
}
509+
result.present = true;
510+
const auto invalid = [&]() {
511+
result.resource = "webscene-backdrop-invalid-v1";
512+
result.maximum_blur_sigma = 0.0F;
513+
return result;
514+
};
515+
std::ostringstream resource;
516+
resource << "webscene-backdrop-v1\t";
517+
constexpr size_t maximum_filter_functions = 16U;
518+
size_t function_count = 0U;
519+
while (!remaining.empty()) {
520+
if (++function_count > maximum_filter_functions) return invalid();
521+
const auto opening = remaining.find('(');
522+
auto closing = std::string_view::npos;
523+
if (opening != std::string_view::npos) {
524+
auto depth = 1U;
525+
for (auto index = opening + 1U; index < remaining.size(); ++index) {
526+
if (remaining[index] == '(') ++depth;
527+
else if (remaining[index] == ')' && --depth == 0U) {
528+
closing = index;
529+
break;
530+
}
531+
}
532+
}
533+
if (opening == 0U || opening == std::string_view::npos
534+
|| closing == std::string_view::npos) {
535+
return invalid();
536+
}
537+
const auto name = trim(remaining.substr(0U, opening));
538+
const auto argument = trim(
539+
remaining.substr(opening + 1U, closing - opening - 1U));
540+
float amount = -1.0F;
541+
if (equals_ascii_case_insensitive(name, "blur")) {
542+
const auto length = parse_length(std::string{argument});
543+
switch (length.unit) {
544+
case length_unit::pixels:
545+
case length_unit::em:
546+
case length_unit::rem:
547+
case length_unit::viewport_width:
548+
case length_unit::viewport_height:
549+
case length_unit::viewport_width_capped:
550+
case length_unit::viewport_height_capped:
551+
case length_unit::viewport_width_floored:
552+
case length_unit::viewport_height_floored:
553+
case length_unit::container_inline:
554+
case length_unit::container_block:
555+
case length_unit::container_min:
556+
case length_unit::container_max:
557+
amount = resolve_length(node, length, 0.0F, -1.0F);
558+
break;
559+
default:
560+
break;
561+
}
562+
constexpr auto maximum_backdrop_blur_sigma = 64.0F;
563+
if (!std::isfinite(amount) || amount < 0.0F
564+
|| amount > maximum_backdrop_blur_sigma) {
565+
return invalid();
566+
}
567+
result.maximum_blur_sigma = std::max(
568+
result.maximum_blur_sigma, amount);
569+
resource << (function_count == 1U ? "" : ";")
570+
<< "blur=" << amount;
571+
} else if (equals_ascii_case_insensitive(name, "saturate")) {
572+
const auto parsed = css::parse_ascii_number_prefix(argument);
573+
if (!parsed.has_value() || parsed->value < 0.0F) return invalid();
574+
const auto suffix = argument.substr(parsed->consumed);
575+
amount = suffix.empty()
576+
? parsed->value
577+
: suffix == "%" ? parsed->value / 100.0F : -1.0F;
578+
if (!std::isfinite(amount) || amount < 0.0F || amount > 10.0F) {
579+
return invalid();
580+
}
581+
resource << (function_count == 1U ? "" : ";")
582+
<< "saturate=" << amount;
583+
} else {
584+
return invalid();
585+
}
586+
remaining.remove_prefix(closing + 1U);
587+
if (!remaining.empty()
588+
&& !std::isspace(static_cast<unsigned char>(remaining.front()))) {
589+
return invalid();
590+
}
591+
remaining = trim(remaining);
592+
}
593+
result.resource = resource.str();
594+
return result;
595+
}();
470596
const auto mask_resource = [&]() -> std::optional<uint32_t> {
471597
const auto& effects = node.style.textual().effect_values;
472598
const auto known = effects.find("mask-image");
@@ -1280,7 +1406,9 @@ void native_document::append_scene(
12801406
// backgrounds must be emitted after retained chart canvases, just
12811407
// like their text and SVG foreground commands.
12821408
}
1283-
return is_in_modal_layer(node) || node.paints_after_retained_canvas || fixed_layer || outermost_z_index > 0;
1409+
return backdrop_effect.present || is_in_modal_layer(node)
1410+
|| node.paints_after_retained_canvas || fixed_layer
1411+
|| outermost_z_index > 0;
12841412
}();
12851413
struct resolved_radii final {
12861414
float top_left;
@@ -1707,6 +1835,29 @@ void native_document::append_scene(
17071835
node.style.border_bottom_left_radius_y(),
17081836
node.layout.width,
17091837
node.layout.height);
1838+
if (paint_self && backdrop_effect.present
1839+
&& node.layout.width > 0.0F && node.layout.height > 0.0F) {
1840+
append_ellipse_metadata(
1841+
radii,
1842+
node.layout.x,
1843+
node.layout.y,
1844+
node.layout.width,
1845+
node.layout.height);
1846+
commands.push_back(webscene_scene_command{
1847+
48U,
1848+
append_scene_string(backdrop_effect.resource, strings, string_bytes),
1849+
node.layout.x,
1850+
node.layout.y,
1851+
node.layout.width,
1852+
node.layout.height,
1853+
1U,
1854+
node.id,
1855+
radii.top_left,
1856+
radii.top_right,
1857+
radii.bottom_right,
1858+
radii.bottom_left,
1859+
backdrop_effect.maximum_blur_sigma});
1860+
}
17101861
if (paint_self
17111862
&& node.style.box_shadow_present && !node.style.box_shadow_inset
17121863
&& node.layout.width > 0

‎experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ typedef struct webscene_scene_header {
168168
// webscene-mask-svg-v1 tiled SVG resource to the current isolated layer using
169169
// destination-in before kind 31 restores it. webscene-mask-invalid-v1 clears
170170
// the isolated layer for failed or unsupported authored masks.
171+
// DOM kind 48 applies one bounded backdrop effect before the element's own
172+
// background and descendants. flags indexes a webscene-backdrop-v1 resource
173+
// containing the authored blur/saturate sequence; rgba is the paint phase,
174+
// the box and corner radii bound output, and stroke_width is the maximum blur
175+
// sigma used to bound sampling and damage. Consumers must sample pixels already
176+
// painted at this exact command position, including retained canvas/GPU output.
177+
// webscene-backdrop-invalid-v1 is an explicit fail-closed no-op.
171178
typedef struct webscene_scene_command {
172179
uint32_t kind;
173180
uint32_t flags;

‎experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_scene_utils.inc‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ bool command_uses_dom_string(const webscene_scene_command& command)
8181
{
8282
return (command.kind >= 3U && command.kind <= 6U)
8383
|| (command.kind == 12U && (command.flags & (1U << 31U)) != 0U)
84-
|| command.kind == 47U;
84+
|| command.kind == 47U
85+
|| command.kind == 48U;
8586
}
8687

8788
std::string_view command_dom_string(
@@ -166,7 +167,13 @@ webscene_damage_rect command_damage_bounds(
166167
bottom = center_y + radius;
167168
}
168169
constexpr auto antialias_padding = 2.0F;
169-
const auto padding = antialias_padding + std::max(0.0F, effect_padding);
170+
constexpr auto backdrop_blur_extent_multiplier = 3.0F;
171+
const auto backdrop_padding = command.kind == 48U
172+
? std::clamp(command.stroke_width, 0.0F, 64.0F)
173+
* backdrop_blur_extent_multiplier
174+
: 0.0F;
175+
const auto padding = antialias_padding
176+
+ std::max(std::max(0.0F, effect_padding), backdrop_padding);
170177
return {
171178
left - padding,
172179
top - padding,
@@ -241,6 +248,27 @@ bool append_localized_dom_damage(
241248
right = std::max(right, bounds.x + bounds.width);
242249
bottom = std::max(bottom, bounds.y + bounds.height);
243250
};
251+
const auto intersects = [](const webscene_damage_rect& left,
252+
const webscene_damage_rect& right) {
253+
return left.x < right.x + right.width
254+
&& right.x < left.x + left.width
255+
&& left.y < right.y + right.height
256+
&& right.y < left.y + left.height;
257+
};
258+
const auto include_sampled_backdrops = [&](const scene& owner,
259+
size_t changed_index,
260+
const webscene_damage_rect& changed_bounds) {
261+
for (auto index = changed_index + 1U; index < owner.commands.size(); ++index) {
262+
const auto& candidate = owner.commands[index];
263+
if (candidate.kind != 48U) continue;
264+
const auto sample_bounds = command_damage_bounds(
265+
candidate,
266+
std::clamp(candidate.stroke_width, 0.0F, 64.0F) * 3.0F);
267+
if (intersects(changed_bounds, sample_bounds)) {
268+
include(candidate, candidate.stroke_width * 3.0F);
269+
}
270+
}
271+
};
244272
for (size_t index = 0; index < next.commands.size(); ++index) {
245273
const auto& old_command = previous.commands[index];
246274
const auto& new_command = next.commands[index];
@@ -263,6 +291,14 @@ bool append_localized_dom_damage(
263291
changed = true;
264292
include(old_command, previous_blur_paddings[index]);
265293
include(new_command, next_blur_paddings[index]);
294+
include_sampled_backdrops(
295+
previous,
296+
index,
297+
command_damage_bounds(old_command, previous_blur_paddings[index]));
298+
include_sampled_backdrops(
299+
next,
300+
index,
301+
command_damage_bounds(new_command, next_blur_paddings[index]));
266302
}
267303
if (!changed) return false;
268304

0 commit comments

Comments
 (0)