From 8517328d520410426ac9c335dec1671eefbb18e2 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 19:05:09 +0900 Subject: [PATCH 1/7] websem: admit guarded SVG group blending Carry checked isolated normal, multiply and screen scopes through rframe and n0. Preserve opacity folding, initial SVG transparency and named source-profile boundaries. Add 94 exact Chromium cells and 43 refusals without ticking the incomplete CSS rows. Record source precision findings, mutation-sensitive gates and measured CPU costs. --- crates/n0/README.md | 26 + crates/n0/src/cache.rs | 2 + crates/n0/src/drawlist.rs | 8 + crates/n0/src/glyphless.rs | 86 +- crates/n0/src/paint.rs | 77 +- crates/n0/src/trace.rs | 227 +++++ crates/n0/tests/drawlist.rs | 2 + crates/n0/tests/group_blending.rs | 927 ++++++++++++++++++ crates/n0_cli/README.md | 39 +- crates/n0_cli/tests/group_blending.rs | 76 ++ crates/rframe/README.md | 38 +- crates/rframe/src/frame.rs | 5 +- crates/rframe/src/lib.rs | 2 +- crates/rframe/src/scope.rs | 99 +- crates/rframe/tests/blend_contract.rs | 538 ++++++++++ crates/websem/src/svg.rs | 550 ++++++++++- crates/websem/tests/capability_status.rs | 36 +- crates/websem/tests/clip_path_contract.rs | 2 +- crates/websem/tests/filter_contract.rs | 10 +- crates/websem/tests/marker_contract.rs | 2 +- crates/websem/tests/mask_contract.rs | 1 + .../websem/tests/nested_viewport_contract.rs | 1 + crates/websem/tests/shapes_contract.rs | 2 +- .../tests/support/unsupported_fixture.rs | 38 + crates/websem/tests/svg_blending.rs | 385 ++++++++ crates/websem/tests/unsupported_corpus.rs | 264 ++++- docs/wg/consolidation/svg-engine-of-record.md | 259 ++++- docs/wg/consolidation/web-checklist.md | 11 + fixtures/web-first/README.md | 13 +- fixtures/web-first/STATUS.md | 141 ++- .../html-inline-svg-group-blend-isolated.png | Bin 0 -> 240 bytes .../svg-group-blend-alpha-half-child.png | Bin 0 -> 212 bytes .../svg-group-blend-alpha-half-parent.png | Bin 0 -> 212 bytes .../svg-group-blend-alpha-half-plain.png | Bin 0 -> 212 bytes .../svg-group-blend-alpha-half-shape.png | Bin 0 -> 212 bytes ...svg-group-blend-alpha-six-tenths-child.png | Bin 0 -> 211 bytes ...vg-group-blend-alpha-six-tenths-parent.png | Bin 0 -> 211 bytes ...svg-group-blend-alpha-six-tenths-plain.png | Bin 0 -> 211 bytes ...svg-group-blend-alpha-six-tenths-shape.png | Bin 0 -> 211 bytes .../chromium/svg-group-blend-anchor.png | Bin 0 -> 232 bytes .../svg-group-blend-attr-multiply.png | Bin 0 -> 205 bytes .../chromium/svg-group-blend-case-comment.png | Bin 0 -> 207 bytes .../svg-group-blend-clip-isolated.png | Bin 0 -> 244 bytes .../chromium/svg-group-blend-clip.png | Bin 0 -> 244 bytes .../chromium/svg-group-blend-css-invalid.png | Bin 0 -> 207 bytes .../chromium/svg-group-blend-css-keyword.png | Bin 0 -> 207 bytes .../chromium/svg-group-blend-css-var.png | Bin 0 -> 207 bytes .../chromium/svg-group-blend-css-winner.png | Bin 0 -> 207 bytes .../svg-group-blend-fractional-multiply.png | Bin 0 -> 246 bytes .../svg-group-blend-fractional-normal.png | Bin 0 -> 252 bytes .../svg-group-blend-fractional-screen.png | Bin 0 -> 249 bytes .../svg-group-blend-group-attr-isolate.png | Bin 0 -> 205 bytes .../svg-group-blend-instance-direct.png | Bin 0 -> 232 bytes .../chromium/svg-group-blend-instance-use.png | Bin 0 -> 232 bytes .../chromium/svg-group-blend-isolated.png | Bin 0 -> 244 bytes .../svg-group-blend-linear-multiply.png | Bin 0 -> 447 bytes .../svg-group-blend-linear-normal.png | Bin 0 -> 522 bytes .../svg-group-blend-linear-screen.png | Bin 0 -> 478 bytes .../svg-group-blend-mapped-direct.png | Bin 0 -> 225 bytes .../svg-group-blend-mapped-viewport.png | Bin 0 -> 225 bytes .../svg-group-blend-multiply-alpha.png | Bin 0 -> 267 bytes .../svg-group-blend-multiply-each.png | Bin 0 -> 241 bytes .../svg-group-blend-multiply-group.png | Bin 0 -> 242 bytes .../svg-group-blend-multiply-leaf.png | Bin 0 -> 205 bytes .../svg-group-blend-multiply-opacity.png | Bin 0 -> 242 bytes .../svg-group-blend-multiply-stroke.png | Bin 0 -> 232 bytes .../svg-group-blend-multiply-transparent.png | Bin 0 -> 258 bytes ...svg-group-blend-nested-hidden-isolated.png | Bin 0 -> 247 bytes .../svg-group-blend-nested-hidden.png | Bin 0 -> 244 bytes .../chromium/svg-group-blend-neutral.png | Bin 0 -> 240 bytes .../chromium/svg-group-blend-normal-alpha.png | Bin 0 -> 270 bytes .../svg-group-blend-normal-clip-isolate.png | Bin 0 -> 248 bytes .../svg-group-blend-normal-clip-plain.png | Bin 0 -> 248 bytes .../chromium/svg-group-blend-normal-group.png | Bin 0 -> 238 bytes .../chromium/svg-group-blend-normal-leaf.png | Bin 0 -> 205 bytes ...roup-blend-normal-many-opacity-isolate.png | Bin 0 -> 245 bytes ...-group-blend-normal-many-opacity-plain.png | Bin 0 -> 245 bytes .../svg-group-blend-normal-opacity.png | Bin 0 -> 238 bytes .../svg-group-blend-normal-stroke.png | Bin 0 -> 232 bytes ...g-group-blend-normal-transform-isolate.png | Bin 0 -> 251 bytes ...svg-group-blend-normal-transform-plain.png | Bin 0 -> 251 bytes .../svg-group-blend-normal-transparent.png | Bin 0 -> 258 bytes .../svg-group-blend-normal-unit-isolate.png | Bin 0 -> 248 bytes .../svg-group-blend-normal-unit-plain.png | Bin 0 -> 248 bytes .../chromium/svg-group-blend-opacity-0.png | Bin 0 -> 168 bytes .../chromium/svg-group-blend-opacity-1.png | Bin 0 -> 237 bytes .../svg-group-blend-opacity-isolated.png | Bin 0 -> 244 bytes ...-group-blend-opacity-opaque-fill-child.png | Bin 0 -> 202 bytes ...roup-blend-opacity-opaque-fill-default.png | Bin 0 -> 202 bytes ...group-blend-opacity-opaque-fill-parent.png | Bin 0 -> 202 bytes ...-group-blend-opacity-opaque-fill-shape.png | Bin 0 -> 202 bytes ...-blend-opacity-opaque-fillstroke-child.png | Bin 0 -> 233 bytes ...lend-opacity-opaque-fillstroke-default.png | Bin 0 -> 233 bytes ...blend-opacity-opaque-fillstroke-parent.png | Bin 0 -> 233 bytes ...-blend-opacity-opaque-fillstroke-shape.png | Bin 0 -> 233 bytes ...group-blend-opacity-p00392156862745098.png | Bin 0 -> 194 bytes ...-group-blend-opacity-p5019607843137255.png | Bin 0 -> 235 bytes .../chromium/svg-group-blend-opacity-p6.png | Bin 0 -> 236 bytes .../chromium/svg-group-blend-opacity.png | Bin 0 -> 244 bytes .../svg-group-blend-outer-opacity.png | Bin 0 -> 238 bytes .../svg-group-blend-outer-root-multiply.png | Bin 0 -> 247 bytes .../svg-group-blend-outer-root-normal.png | Bin 0 -> 247 bytes .../svg-group-blend-outer-root-screen.png | Bin 0 -> 247 bytes .../svg-group-blend-partial-isolated.png | Bin 0 -> 217 bytes .../chromium/svg-group-blend-partial.png | Bin 0 -> 217 bytes .../svg-group-blend-pattern-multiply.png | Bin 0 -> 205 bytes .../svg-group-blend-pattern-normal.png | Bin 0 -> 205 bytes .../svg-group-blend-pattern-screen.png | Bin 0 -> 207 bytes .../chromium/svg-group-blend-root-isolate.png | Bin 0 -> 215 bytes .../svg-group-blend-root-multiply.png | Bin 0 -> 215 bytes .../chromium/svg-group-blend-root-normal.png | Bin 0 -> 215 bytes .../svg-group-blend-rotate-isolated.png | Bin 0 -> 1696 bytes .../chromium/svg-group-blend-rotate.png | Bin 0 -> 1652 bytes .../svg-group-blend-scale-isolated.png | Bin 0 -> 242 bytes .../chromium/svg-group-blend-scale.png | Bin 0 -> 240 bytes .../chromium/svg-group-blend-screen-alpha.png | Bin 0 -> 267 bytes .../chromium/svg-group-blend-screen-each.png | Bin 0 -> 242 bytes .../chromium/svg-group-blend-screen-group.png | Bin 0 -> 239 bytes .../chromium/svg-group-blend-screen-leaf.png | Bin 0 -> 207 bytes .../svg-group-blend-screen-opacity.png | Bin 0 -> 240 bytes .../svg-group-blend-screen-stroke.png | Bin 0 -> 230 bytes .../svg-group-blend-screen-transparent.png | Bin 0 -> 258 bytes .../svg-group-blend-translate-isolated.png | Bin 0 -> 247 bytes .../chromium/svg-group-blend-translate.png | Bin 0 -> 245 bytes .../html-inline-svg-group-blend-isolated.html | 1 + fixtures/web-first/oracle-bake.json | 848 +++++++++++++++- fixtures/web-first/primitives.json | 752 ++++++++++++++ .../svg-group-blend-alpha-half-child.svg | 1 + .../svg-group-blend-alpha-half-parent.svg | 1 + .../svg-group-blend-alpha-half-plain.svg | 1 + .../svg-group-blend-alpha-half-shape.svg | 1 + ...svg-group-blend-alpha-six-tenths-child.svg | 1 + ...vg-group-blend-alpha-six-tenths-parent.svg | 1 + ...svg-group-blend-alpha-six-tenths-plain.svg | 1 + ...svg-group-blend-alpha-six-tenths-shape.svg | 1 + fixtures/web-first/svg-group-blend-anchor.svg | 1 + .../svg-group-blend-attr-multiply.svg | 1 + .../svg-group-blend-case-comment.svg | 1 + .../svg-group-blend-clip-isolated.svg | 1 + fixtures/web-first/svg-group-blend-clip.svg | 1 + .../web-first/svg-group-blend-css-invalid.svg | 1 + .../web-first/svg-group-blend-css-keyword.svg | 1 + .../web-first/svg-group-blend-css-var.svg | 1 + .../web-first/svg-group-blend-css-winner.svg | 1 + .../svg-group-blend-fractional-multiply.svg | 1 + .../svg-group-blend-fractional-normal.svg | 1 + .../svg-group-blend-fractional-screen.svg | 1 + .../svg-group-blend-group-attr-isolate.svg | 1 + .../svg-group-blend-instance-direct.svg | 1 + .../svg-group-blend-instance-use.svg | 1 + .../web-first/svg-group-blend-isolated.svg | 1 + .../svg-group-blend-linear-multiply.svg | 1 + .../svg-group-blend-linear-normal.svg | 1 + .../svg-group-blend-linear-screen.svg | 1 + .../svg-group-blend-mapped-direct.svg | 1 + .../svg-group-blend-mapped-viewport.svg | 1 + .../svg-group-blend-multiply-alpha.svg | 1 + .../svg-group-blend-multiply-each.svg | 1 + .../svg-group-blend-multiply-group.svg | 1 + .../svg-group-blend-multiply-leaf.svg | 1 + .../svg-group-blend-multiply-opacity.svg | 1 + .../svg-group-blend-multiply-stroke.svg | 1 + .../svg-group-blend-multiply-transparent.svg | 1 + ...svg-group-blend-nested-hidden-isolated.svg | 1 + .../svg-group-blend-nested-hidden.svg | 1 + .../web-first/svg-group-blend-neutral.svg | 1 + .../svg-group-blend-normal-alpha.svg | 1 + .../svg-group-blend-normal-clip-isolate.svg | 1 + .../svg-group-blend-normal-clip-plain.svg | 1 + .../svg-group-blend-normal-group.svg | 1 + .../web-first/svg-group-blend-normal-leaf.svg | 1 + ...roup-blend-normal-many-opacity-isolate.svg | 1 + ...-group-blend-normal-many-opacity-plain.svg | 1 + .../svg-group-blend-normal-opacity.svg | 1 + .../svg-group-blend-normal-stroke.svg | 1 + ...g-group-blend-normal-transform-isolate.svg | 1 + ...svg-group-blend-normal-transform-plain.svg | 1 + .../svg-group-blend-normal-transparent.svg | 1 + .../svg-group-blend-normal-unit-isolate.svg | 1 + .../svg-group-blend-normal-unit-plain.svg | 1 + .../web-first/svg-group-blend-opacity-0.svg | 1 + .../web-first/svg-group-blend-opacity-1.svg | 1 + .../svg-group-blend-opacity-isolated.svg | 1 + ...-group-blend-opacity-opaque-fill-child.svg | 1 + ...roup-blend-opacity-opaque-fill-default.svg | 1 + ...group-blend-opacity-opaque-fill-parent.svg | 1 + ...-group-blend-opacity-opaque-fill-shape.svg | 1 + ...-blend-opacity-opaque-fillstroke-child.svg | 1 + ...lend-opacity-opaque-fillstroke-default.svg | 1 + ...blend-opacity-opaque-fillstroke-parent.svg | 1 + ...-blend-opacity-opaque-fillstroke-shape.svg | 1 + ...group-blend-opacity-p00392156862745098.svg | 1 + ...-group-blend-opacity-p5019607843137255.svg | 1 + .../web-first/svg-group-blend-opacity-p6.svg | 1 + .../web-first/svg-group-blend-opacity.svg | 1 + .../svg-group-blend-outer-opacity.svg | 1 + .../svg-group-blend-outer-root-multiply.svg | 1 + .../svg-group-blend-outer-root-normal.svg | 1 + .../svg-group-blend-outer-root-screen.svg | 1 + .../svg-group-blend-partial-isolated.svg | 1 + .../web-first/svg-group-blend-partial.svg | 1 + .../svg-group-blend-pattern-multiply.svg | 1 + .../svg-group-blend-pattern-normal.svg | 1 + .../svg-group-blend-pattern-screen.svg | 1 + .../svg-group-blend-root-isolate.svg | 1 + .../svg-group-blend-root-multiply.svg | 1 + .../web-first/svg-group-blend-root-normal.svg | 1 + .../svg-group-blend-rotate-isolated.svg | 1 + fixtures/web-first/svg-group-blend-rotate.svg | 1 + .../svg-group-blend-scale-isolated.svg | 1 + fixtures/web-first/svg-group-blend-scale.svg | 1 + .../svg-group-blend-screen-alpha.svg | 1 + .../web-first/svg-group-blend-screen-each.svg | 1 + .../svg-group-blend-screen-group.svg | 1 + .../web-first/svg-group-blend-screen-leaf.svg | 1 + .../svg-group-blend-screen-opacity.svg | 1 + .../svg-group-blend-screen-stroke.svg | 1 + .../svg-group-blend-screen-transparent.svg | 1 + .../svg-group-blend-translate-isolated.svg | 1 + .../web-first/svg-group-blend-translate.svg | 1 + fixtures/web-first/unsupported/README.md | 14 + .../html-group-blend-ancestor-isolation.html | 1 + ...l-group-blend-ancestor-mix-blend-mode.html | 1 + .../html-group-blend-head-animation.html | 1 + .../html-group-blend-unisolated.html | 1 + .../svg-group-blend-css-animation.svg | 1 + .../svg-group-blend-css-var-animation.svg | 1 + .../svg-group-blend-elided-filter-partial.svg | 1 + .../svg-group-blend-elided-filter-unit.svg | 1 + .../svg-group-blend-elided-mask-partial.svg | 1 + .../svg-group-blend-elided-mask-unit.svg | 1 + .../unsupported/svg-group-blend-filter.svg | 1 + .../unsupported/svg-group-blend-mask.svg | 1 + .../svg-group-blend-mode-color-burn.svg | 1 + .../svg-group-blend-mode-color-dodge.svg | 1 + .../svg-group-blend-mode-color.svg | 1 + .../svg-group-blend-mode-darken.svg | 1 + .../svg-group-blend-mode-difference.svg | 1 + .../svg-group-blend-mode-exclusion.svg | 1 + .../svg-group-blend-mode-hard-light.svg | 1 + .../unsupported/svg-group-blend-mode-hue.svg | 1 + .../svg-group-blend-mode-lighten.svg | 1 + .../svg-group-blend-mode-luminosity.svg | 1 + .../svg-group-blend-mode-overlay.svg | 1 + .../svg-group-blend-mode-plus-lighter.svg | 1 + .../svg-group-blend-mode-saturation.svg | 1 + .../svg-group-blend-mode-soft-light.svg | 1 + ...svg-group-blend-pattern-root-animation.svg | 1 + ...vg-group-blend-resource-clippath-child.svg | 1 + ...svg-group-blend-resource-clippath-root.svg | 1 + .../svg-group-blend-resource-mask-child.svg | 1 + .../svg-group-blend-resource-mask-root.svg | 1 + ...svg-group-blend-resource-pattern-child.svg | 1 + .../svg-group-blend-resource-pattern-root.svg | 1 + .../svg-group-blend-root-filter-sibling.svg | 1 + .../svg-group-blend-root-opacity.svg | 1 + .../svg-group-blend-source-clip.svg | 1 + .../svg-group-blend-source-ellipse.svg | 1 + .../svg-group-blend-source-isolation.svg | 1 + .../svg-group-blend-source-path.svg | 1 + .../svg-group-blend-source-radial.svg | 1 + .../svg-group-blend-source-rotated-clip.svg | 1 + .../svg-group-blend-source-stroke.svg | 1 + .../svg-group-blend-source-subpixel-clip.svg | 1 + 264 files changed, 5489 insertions(+), 129 deletions(-) create mode 100644 crates/n0/tests/group_blending.rs create mode 100644 crates/n0_cli/tests/group_blending.rs create mode 100644 crates/rframe/tests/blend_contract.rs create mode 100644 crates/websem/tests/support/unsupported_fixture.rs create mode 100644 crates/websem/tests/svg_blending.rs create mode 100644 fixtures/web-first/chromium/html-inline-svg-group-blend-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-half-child.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-half-parent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-half-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-half-shape.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-child.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-parent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-shape.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-anchor.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-attr-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-case-comment.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-clip-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-clip.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-css-invalid.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-css-keyword.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-css-var.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-css-winner.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-fractional-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-fractional-normal.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-fractional-screen.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-group-attr-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-instance-direct.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-instance-use.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-linear-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-linear-normal.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-linear-screen.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-mapped-direct.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-mapped-viewport.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-alpha.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-each.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-group.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-leaf.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-opacity.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-stroke.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-transparent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-nested-hidden-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-nested-hidden.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-neutral.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-alpha.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-clip-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-clip-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-group.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-leaf.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-many-opacity-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-many-opacity-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-opacity.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-stroke.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-transform-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-transform-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-transparent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-unit-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-normal-unit-plain.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-0.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-1.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-child.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-default.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-parent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-shape.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fillstroke-child.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fillstroke-default.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fillstroke-parent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fillstroke-shape.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-p00392156862745098.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-p5019607843137255.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity-p6.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-opacity.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-outer-opacity.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-outer-root-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-outer-root-normal.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-outer-root-screen.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-partial-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-partial.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-pattern-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-pattern-normal.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-pattern-screen.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-root-isolate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-root-multiply.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-root-normal.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-rotate-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-rotate.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-scale-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-scale.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-alpha.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-each.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-group.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-leaf.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-opacity.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-stroke.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-transparent.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-translate-isolated.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-translate.png create mode 100644 fixtures/web-first/html-inline-svg-group-blend-isolated.html create mode 100644 fixtures/web-first/svg-group-blend-alpha-half-child.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-half-parent.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-half-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-half-shape.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-six-tenths-child.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-six-tenths-parent.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-six-tenths-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-alpha-six-tenths-shape.svg create mode 100644 fixtures/web-first/svg-group-blend-anchor.svg create mode 100644 fixtures/web-first/svg-group-blend-attr-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-case-comment.svg create mode 100644 fixtures/web-first/svg-group-blend-clip-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-clip.svg create mode 100644 fixtures/web-first/svg-group-blend-css-invalid.svg create mode 100644 fixtures/web-first/svg-group-blend-css-keyword.svg create mode 100644 fixtures/web-first/svg-group-blend-css-var.svg create mode 100644 fixtures/web-first/svg-group-blend-css-winner.svg create mode 100644 fixtures/web-first/svg-group-blend-fractional-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-fractional-normal.svg create mode 100644 fixtures/web-first/svg-group-blend-fractional-screen.svg create mode 100644 fixtures/web-first/svg-group-blend-group-attr-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-instance-direct.svg create mode 100644 fixtures/web-first/svg-group-blend-instance-use.svg create mode 100644 fixtures/web-first/svg-group-blend-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-linear-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-linear-normal.svg create mode 100644 fixtures/web-first/svg-group-blend-linear-screen.svg create mode 100644 fixtures/web-first/svg-group-blend-mapped-direct.svg create mode 100644 fixtures/web-first/svg-group-blend-mapped-viewport.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-alpha.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-each.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-group.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-leaf.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-opacity.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-stroke.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-transparent.svg create mode 100644 fixtures/web-first/svg-group-blend-nested-hidden-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-nested-hidden.svg create mode 100644 fixtures/web-first/svg-group-blend-neutral.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-alpha.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-clip-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-clip-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-group.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-leaf.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-many-opacity-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-many-opacity-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-opacity.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-stroke.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-transform-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-transform-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-transparent.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-unit-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-normal-unit-plain.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-0.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-1.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fill-child.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fill-default.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fill-parent.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fill-shape.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-child.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-default.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-parent.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-shape.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-p00392156862745098.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-p5019607843137255.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity-p6.svg create mode 100644 fixtures/web-first/svg-group-blend-opacity.svg create mode 100644 fixtures/web-first/svg-group-blend-outer-opacity.svg create mode 100644 fixtures/web-first/svg-group-blend-outer-root-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-outer-root-normal.svg create mode 100644 fixtures/web-first/svg-group-blend-outer-root-screen.svg create mode 100644 fixtures/web-first/svg-group-blend-partial-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-partial.svg create mode 100644 fixtures/web-first/svg-group-blend-pattern-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-pattern-normal.svg create mode 100644 fixtures/web-first/svg-group-blend-pattern-screen.svg create mode 100644 fixtures/web-first/svg-group-blend-root-isolate.svg create mode 100644 fixtures/web-first/svg-group-blend-root-multiply.svg create mode 100644 fixtures/web-first/svg-group-blend-root-normal.svg create mode 100644 fixtures/web-first/svg-group-blend-rotate-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-rotate.svg create mode 100644 fixtures/web-first/svg-group-blend-scale-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-scale.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-alpha.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-each.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-group.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-leaf.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-opacity.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-stroke.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-transparent.svg create mode 100644 fixtures/web-first/svg-group-blend-translate-isolated.svg create mode 100644 fixtures/web-first/svg-group-blend-translate.svg create mode 100644 fixtures/web-first/unsupported/html-group-blend-ancestor-isolation.html create mode 100644 fixtures/web-first/unsupported/html-group-blend-ancestor-mix-blend-mode.html create mode 100644 fixtures/web-first/unsupported/html-group-blend-head-animation.html create mode 100644 fixtures/web-first/unsupported/html-group-blend-unisolated.html create mode 100644 fixtures/web-first/unsupported/svg-group-blend-css-animation.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-css-var-animation.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-elided-filter-partial.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-elided-filter-unit.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-elided-mask-partial.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-elided-mask-unit.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-filter.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mask.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-color-burn.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-color-dodge.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-color.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-darken.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-difference.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-exclusion.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-hard-light.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-hue.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-lighten.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-luminosity.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-overlay.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-plus-lighter.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-saturation.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-mode-soft-light.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-pattern-root-animation.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-clippath-child.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-clippath-root.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-mask-child.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-mask-root.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-pattern-child.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-resource-pattern-root.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-root-filter-sibling.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-root-opacity.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-clip.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-ellipse.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-isolation.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-path.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-radial.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-rotated-clip.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-stroke.svg create mode 100644 fixtures/web-first/unsupported/svg-group-blend-source-subpixel-clip.svg diff --git a/crates/n0/README.md b/crates/n0/README.md index 33691e27..3d12b778 100644 --- a/crates/n0/README.md +++ b/crates/n0/README.md @@ -296,6 +296,32 @@ integration is not admitted or implemented yet. This is a source-neutral chassis seam, not a general SVG importer; source parsing, cascade, animation interpretation, I/O, and clocks remain outside n0. +A resolved `ScopeEffect::Blend` compiles to one empty-start group layer with +the checked blend and optional opacity applied in one restoration. Unit Normal +retains isolation. The native authored `BeginOpacity` operation continues to +copy its backdrop and restore arithmetically; it has a different meaning. +Blend scopes use the existing opaque owner projection, child coverage union, +exact drawlist equality, and complete-frame damage policy, including inside +repeating programs. An unchanged group must still replay against a changed +earlier backdrop; retained glyphless products reuse immutable commands, never +the previously blended pixels. `tests/group_blending.rs` pins these consumer +laws with hand-built frames and exact pixel probes. Those tests do not claim +Chromium parity or measured performance. + +With the `trace` feature, `n0::trace::sink::drain_blend_layers()` drains typed +`BlendLayerMetrics`, separate from duration samples: one aggregate per outermost +drawlist execution, including recursive resource recording. It counts blend +save calls, observed raster pixel-span bytes and area, peak live observed blend +bytes, inaccessible observations, and empty-clip saves. The latter never count +the parent surface as a new layer. Skia-internal allocations and later picture +playback are not counted; recording/GPU storage may be inaccessible. Preflight +recording can produce separate execute aggregates: one frame operation may +yield more than one record. Drain before and after the diagnostic frame. The +pinned raster accessor reads existing +storage but calls `notifyPixelsChanged`; use an untimed trace-enabled frame, +not these instrumented observations as default-build allocation or performance +claims. No layer-bounds optimization is applied. + ## Versioned `.n0.xml` ingestion There is deliberately no XML-specific engine API. Draft 0 still has the diff --git a/crates/n0/src/cache.rs b/crates/n0/src/cache.rs index 18718ba6..ab464eeb 100644 --- a/crates/n0/src/cache.rs +++ b/crates/n0/src/cache.rs @@ -535,6 +535,8 @@ fn explicit_radial_owner(list: &crate::drawlist::DrawList) -> Option } ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } + | ItemKind::BeginIsolatedBlend { .. } + | ItemKind::EndIsolatedBlend | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } | ItemKind::BeginClipPath { .. } diff --git a/crates/n0/src/drawlist.rs b/crates/n0/src/drawlist.rs index 99815e36..b698dea1 100644 --- a/crates/n0/src/drawlist.rs +++ b/crates/n0/src/drawlist.rs @@ -416,6 +416,14 @@ pub enum ItemKind { BeginIsolatedOpacity { opacity: f32, }, + /// The resolved group's children start against transparent black. Blend + /// and opacity apply together when that completed source joins the + /// enclosing backdrop. Even Normal at unit opacity keeps this boundary. + /// The checked scope vocabulary cannot admit arbitrary native leaf blends. + BeginIsolatedBlend { + blend: rframe::ScopeBlend, + }, + EndIsolatedBlend, /// Closes the innermost opacity scope, of either meaning. EndOpacity, BeginClipRect { diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index 48f1c64f..f1ea6c53 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -4,7 +4,7 @@ //! authored n0 document, HTML/CSS/SVG syntax, parser binding, backend object, //! I/O handle, or clock. This module admits its current solid-, gradient-, and //! resolved-pattern-painted rectangle, ellipse, and path slice plus checked -//! opacity, clip, mask, and image-filter effects, compiles them into n0's one +//! opacity, group blend, clip, mask, and image-filter effects, compiles them into n0's one //! private drawlist, and executes them through n0's one private painter. //! //! The resulting [`FrameProduct`] is intentionally separate from @@ -226,6 +226,7 @@ pub struct Damage { #[derive(Debug, Clone)] enum OpenScopeKind { Opacity, + Blend, Clip { bounds: Option, }, @@ -293,7 +294,7 @@ fn damage_input(product: &FrameProduct) -> FrameDamageInput<'_, VisualRef, (), G /// rectangle) and paths, the contract's admitted `cg` paints (solids, linear /// and radial gradients — every gradient preflighted against its resolved /// paint box before the product exists), checked repeating vector programs, a -/// centred stroke over the fill, isolated opacity scopes, resolved geometric +/// centred stroke over the fill, isolated opacity and blend scopes, resolved geometric /// clip scopes, and the frame-bounds clip. /// /// The contract's item stream is a checked type ([`rframe::FrameItems`]): @@ -373,6 +374,14 @@ pub fn compile(resolved: Frame) -> Result { }); (OpenScopeKind::Opacity, None) } + ScopeEffect::Blend(blend) => { + items.push(Item { + node: slot, + world: frame_world, + kind: ItemKind::BeginIsolatedBlend { blend: *blend }, + }); + (OpenScopeKind::Blend, None) + } ScopeEffect::Clip(clip) => { let compiled = Arc::new(compile_clip_path(clip)); if !crate::paint::preflight_clip_path(&compiled) { @@ -429,6 +438,9 @@ pub fn compile(resolved: Frame) -> Result { let scope = open_scopes.pop().expect("checked stream is balanced"); let (coverage, world, end) = match scope.kind { OpenScopeKind::Opacity => (scope.coverage, frame_world, ItemKind::EndOpacity), + OpenScopeKind::Blend => { + (scope.coverage, frame_world, ItemKind::EndIsolatedBlend) + } OpenScopeKind::Clip { bounds } => match (scope.coverage, bounds) { (Some(coverage), Some(bounds)) => ( bounded_intersection_rectf(coverage, bounds, resolved.bounds), @@ -1881,6 +1893,8 @@ mod tests { } => Some(*post_paint_opacity), ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } + | ItemKind::BeginIsolatedBlend { .. } + | ItemKind::EndIsolatedBlend | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } | ItemKind::BeginClipPath { .. } @@ -3693,6 +3707,74 @@ mod tests { ); } + /// One checked final operation becomes one pair in the private stream. + /// Child paints stay identical, including at unit Normal and at the + /// extreme admitted opacity values; there is no per-paint rewrite. + #[test] + fn blend_projection_retains_one_combined_operation_and_its_owner() { + let mut lists = Vec::new(); + for mode in [ + rframe::ScopeBlendMode::Normal, + rframe::ScopeBlendMode::Multiply, + rframe::ScopeBlendMode::Screen, + ] { + for opacity in [ + None, + Some(f32::from_bits(1)), + Some(0.375), + Some(1.0_f32.next_down()), + ] { + let blend = rframe::ScopeBlend::new( + mode, + opacity.map(|value| ScopeOpacity::new(value).unwrap()), + ); + let source = frame_of( + FrameItems::try_new(vec![ + FrameItem::ScopeBegin(Scope { + owner: SCOPE_OWNER, + effect: ScopeEffect::Blend(blend), + }), + FrameItem::Node(base_node(PaintStack::solid(CGColor::RED))), + FrameItem::ScopeEnd, + ]) + .unwrap(), + ); + let product = compile(source.clone()).unwrap(); + assert_eq!(product.drawlist, compile(source).unwrap().drawlist); + let items = &product.drawlist.items; + assert_eq!( + items.len(), + 5, + "frame clip, blend, child, blend end, clip end" + ); + assert_eq!(items[1].kind, ItemKind::BeginIsolatedBlend { blend }); + assert_eq!(items[3].kind, ItemKind::EndIsolatedBlend); + assert_eq!(items[1].node, items[3].node); + assert_eq!( + product.provenance.get(items[1].node), + Some(( + SCOPE_OWNER, + Some(n0_model::math::RectF { + x: 8.0, + y: 6.0, + w: 20.0, + h: 16.0 + }) + )) + ); + for previous in &lists { + let previous: &DrawList = previous; + assert_ne!( + previous, &product.drawlist, + "mode and opacity affect equality" + ); + assert_eq!(previous.items[2], items[2], "the child stays unchanged"); + } + lists.push(product.drawlist); + } + } + } + /// A checked filter scope lowers to one private graph layer. The painter /// evaluates Source and SourceAlpha distinctly and never treats a valid /// graph as an unfiltered fallback. diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 84ef6cd7..73acf163 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -890,6 +890,8 @@ pub(crate) fn preflight_gradients( | ItemKind::PatternStroke { .. } | ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } + | ItemKind::BeginIsolatedBlend { .. } + | ItemKind::EndIsolatedBlend | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } | ItemKind::BeginClipPath { .. } @@ -1129,6 +1131,8 @@ pub(crate) fn preflight_images( | ItemKind::PatternStroke { .. } | ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } + | ItemKind::BeginIsolatedBlend { .. } + | ItemKind::EndIsolatedBlend | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } | ItemKind::BeginClipPath { .. } @@ -4109,6 +4113,38 @@ fn text_path( builder.snapshot() } +#[cfg(feature = "trace")] +fn observe_blend_layer(canvas: &Canvas) -> crate::trace::blend_layers::Observation { + use crate::trace::blend_layers::Observation; + + // An empty saveLayer can retain the prior device. Do not misattribute its + // backing storage to a new layer. Failed layer mappings also empty the clip. + if canvas.is_clip_empty() { + return Observation::EmptyClip; + } + // In pinned Skia, accessTopLayerPixels -> SkBitmapDevice::onAccessPixels + // peeks existing storage and calls notifyPixelsChanged. It does not allocate, + // but generation-state perturbation makes this diagnostic instrumentation. + // Never read/write the pixel slice or keep it across another canvas call. + let Some(top) = canvas.access_top_layer_pixels() else { + return Observation::Unavailable; + }; + let (Ok(width), Ok(height)) = ( + u64::try_from(top.info.width()), + u64::try_from(top.info.height()), + ) else { + return Observation::Unavailable; + }; + let bytes = top.info.compute_byte_size(top.row_bytes); + if bytes == usize::MAX { + return Observation::Unavailable; + } + Observation::Raster { + bytes, + pixels: width * height, + } +} + /// Replay a raw [`DrawList`] without a frame-environment check. /// /// This low-level entry exists for engine-owned resource-free glyphless @@ -4124,13 +4160,22 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, // noise and can move a boundary value across N32 quantization. skia_safe::graphics::init(); + #[cfg(feature = "trace")] + let _blend_trace = crate::trace::blend_layers::Execute::begin(); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Scope { Opacity, + Blend { + #[cfg(feature = "trace")] + observed_bytes: u128, + }, Clip, MaskContent, MaskSource, - Filter { source_preflatten: bool }, + Filter { + source_preflatten: bool, + }, } let initial_save_count = canvas.save_count(); @@ -4170,6 +4215,36 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, canvas.save_layer(&layer); scopes.push(Scope::Opacity); } + ItemKind::BeginIsolatedBlend { blend } => { + // One empty-start layer, one restoration. Nesting an opacity + // layer outside a blend would change the blend's backdrop; + // nesting it inside would introduce another quantization. + let mut restore_paint = Paint::default(); + restore_paint.set_alpha_f(blend.opacity().map_or(1.0, |opacity| opacity.get())); + restore_paint.set_blend_mode(match blend.mode() { + rframe::ScopeBlendMode::Normal => skia_safe::BlendMode::SrcOver, + rframe::ScopeBlendMode::Multiply => skia_safe::BlendMode::Multiply, + rframe::ScopeBlendMode::Screen => skia_safe::BlendMode::Screen, + }); + canvas.save_layer(&SaveLayerRec::default().paint(&restore_paint)); + scopes.push(Scope::Blend { + #[cfg(feature = "trace")] + observed_bytes: crate::trace::blend_layers::Execute::begin_layer( + observe_blend_layer(canvas), + ), + }); + } + ItemKind::EndIsolatedBlend => { + let scope = scopes.pop(); + debug_assert!(matches!(scope, Some(Scope::Blend { .. }))); + if scope.is_some() { + canvas.restore(); + } + #[cfg(feature = "trace")] + if let Some(Scope::Blend { observed_bytes }) = scope { + crate::trace::blend_layers::Execute::end_layer(observed_bytes); + } + } ItemKind::EndOpacity => { let scope = scopes.pop(); debug_assert_eq!(scope, Some(Scope::Opacity)); diff --git a/crates/n0/src/trace.rs b/crates/n0/src/trace.rs index 7d978fc5..ba7dbd9d 100644 --- a/crates/n0/src/trace.rs +++ b/crates/n0/src/trace.rs @@ -8,6 +8,43 @@ //! Instrument only the three frame seams (resolve / build / execute) — //! more is noise. The on/off delta is a one-time documented measurement, //! not a per-frame check. +//! +//! Blend-layer counters are separate from duration samples. With `trace`, +//! each outermost execute publishes one `BlendLayerMetrics` through +//! `sink::drain_blend_layers`, including recursive resource executions. + +/// Diagnostic observations of blend layers during one outermost execution. +/// +/// These are not allocator-capacity, GPU-memory, or default-build measurements. +/// Raster bytes describe the accessible pixel span (`ImageInfo::compute_byte_size` +/// with the observed row stride), excluding allocator overhead. The pinned Skia +/// accessor does not allocate pixels, but marks their generation changed even +/// when n0 only reads metadata. Use a separate untimed trace-enabled frame. +/// Missing observations make byte totals and the live peak incomplete. +/// Only explicit execute-seam blend scopes are counted: allocations internal +/// to Skia (including later playback of a recorded picture) are not observable +/// here. Preflight picture recording can produce separate execute aggregates. +#[cfg(feature = "trace")] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct BlendLayerMetrics { + /// BeginIsolatedBlend save-layer calls, including empty clips. + pub save_layer_calls: u64, + /// Calls whose top-layer raster storage was observable. + pub observed_raster_layers: u64, + /// Sum of observed accessible pixel-span bytes, not retained memory. + pub observed_raster_bytes: u128, + /// Sum of observed raster width × height, in pixels. + pub observed_raster_pixels: u128, + /// Largest simultaneously live observed blend-layer byte total. + /// Other opacity, mask, filter, and root-surface storage is excluded. + pub peak_live_blend_bytes: u128, + /// Nonempty-clip calls without accessible raster storage (for example GPU, + /// recording canvases, failed allocations, or inaccessible layer mappings). + pub missing_observations: u64, + /// Empty-clip saves, which may leave the parent device on top; never counted + /// as observing another allocation of that parent surface. + pub empty_clip_saves: u64, +} #[cfg(feature = "trace")] pub mod sink { @@ -15,6 +52,7 @@ pub mod sink { thread_local! { static FRAME: RefCell> = const { RefCell::new(Vec::new()) }; + static BLEND_LAYERS: RefCell> = const { RefCell::new(Vec::new()) }; } /// Record a span sample (nanoseconds) under `name`. @@ -26,6 +64,195 @@ pub mod sink { pub fn drain() -> Vec<(&'static str, u128)> { FRAME.with(|f| f.borrow_mut().drain(..).collect()) } + + pub(super) fn record_blend_layers(metrics: super::BlendLayerMetrics) { + BLEND_LAYERS.with(|frames| frames.borrow_mut().push(metrics)); + } + + /// Take and clear this thread's completed outermost-execute blend-layer + /// observations. This does not drain duration samples or an active execute. + pub fn drain_blend_layers() -> Vec { + BLEND_LAYERS.with(|frames| frames.borrow_mut().drain(..).collect()) + } +} + +#[cfg(feature = "trace")] +pub(crate) mod blend_layers { + use std::cell::RefCell; + + use super::{sink, BlendLayerMetrics}; + + #[derive(Default)] + struct Active { + depth: usize, + live_bytes: u128, + metrics: BlendLayerMetrics, + } + + thread_local! { + static ACTIVE: RefCell = RefCell::new(Active::default()); + } + + pub(crate) enum Observation { + Raster { bytes: usize, pixels: u64 }, + EmptyClip, + Unavailable, + } + + // Recursive resource execution contributes to the same aggregate and sees + // the outer layers' live bytes. No per-layer events are retained. + pub(crate) struct Execute { + entry_live_bytes: u128, + } + + impl Execute { + pub(crate) fn begin() -> Self { + ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + active.depth += 1; + Self { + entry_live_bytes: active.live_bytes, + } + }) + } + + pub(crate) fn begin_layer(observation: Observation) -> u128 { + ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + debug_assert!(active.depth > 0); + active.metrics.save_layer_calls += 1; + match observation { + Observation::Raster { bytes, pixels } => { + let bytes = bytes as u128; + active.metrics.observed_raster_layers += 1; + active.metrics.observed_raster_bytes += bytes; + active.metrics.observed_raster_pixels += u128::from(pixels); + active.live_bytes += bytes; + active.metrics.peak_live_blend_bytes = + active.metrics.peak_live_blend_bytes.max(active.live_bytes); + bytes + } + Observation::EmptyClip => { + active.metrics.empty_clip_saves += 1; + 0 + } + Observation::Unavailable => { + active.metrics.missing_observations += 1; + 0 + } + } + }) + } + + pub(crate) fn end_layer(bytes: u128) { + ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + debug_assert!(active.live_bytes >= bytes); + active.live_bytes = active.live_bytes.saturating_sub(bytes); + }); + } + } + + impl Drop for Execute { + fn drop(&mut self) { + let completed = ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + active.depth -= 1; + active.live_bytes = self.entry_live_bytes; + if active.depth == 0 { + Some(std::mem::take(&mut active.metrics)) + } else { + None + } + }); + if let Some(metrics) = completed { + if !std::thread::panicking() { + sink::record_blend_layers(metrics); + } + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn recursive_execution_aggregates_live_bytes_and_drains_separately() { + sink::drain_blend_layers(); + sink::drain(); + sink::record("duration", 17); + { + let _outer = Execute::begin(); + let outer = Execute::begin_layer(Observation::Raster { + bytes: 20, + pixels: 5, + }); + { + let _resource = Execute::begin(); + let inner = Execute::begin_layer(Observation::Raster { + bytes: 32, + pixels: 8, + }); + Execute::end_layer(inner); + } + assert!(sink::drain_blend_layers().is_empty()); + Execute::end_layer(outer); + let sibling = Execute::begin_layer(Observation::Raster { + bytes: 12, + pixels: 3, + }); + Execute::end_layer(sibling); + } + assert_eq!( + sink::drain_blend_layers(), + [BlendLayerMetrics { + save_layer_calls: 3, + observed_raster_layers: 3, + observed_raster_bytes: 64, + observed_raster_pixels: 16, + peak_live_blend_bytes: 52, + ..BlendLayerMetrics::default() + }] + ); + assert!(sink::drain_blend_layers().is_empty()); + assert_eq!(sink::drain(), [("duration", 17)]); + drop(Execute::begin()); + assert_eq!(sink::drain_blend_layers(), [BlendLayerMetrics::default()]); + } + + #[test] + fn unavailable_and_empty_observations_do_not_invent_storage() { + sink::drain_blend_layers(); + { + let _execute = Execute::begin(); + Execute::end_layer(Execute::begin_layer(Observation::Unavailable)); + Execute::end_layer(Execute::begin_layer(Observation::EmptyClip)); + } + assert_eq!( + sink::drain_blend_layers(), + [BlendLayerMetrics { + save_layer_calls: 2, + missing_observations: 1, + empty_clip_saves: 1, + ..BlendLayerMetrics::default() + }] + ); + } + + #[test] + fn observations_are_thread_local() { + sink::drain_blend_layers(); + let metrics = std::thread::spawn(|| { + drop(Execute::begin()); + sink::drain_blend_layers() + }) + .join() + .unwrap(); + assert_eq!(metrics, [BlendLayerMetrics::default()]); + assert!(sink::drain_blend_layers().is_empty()); + } + } } /// Time `$body` under `$name`. Off: just the block. On: the block, plus a diff --git a/crates/n0/tests/drawlist.rs b/crates/n0/tests/drawlist.rs index 31a4f7e7..af135897 100644 --- a/crates/n0/tests/drawlist.rs +++ b/crates/n0/tests/drawlist.rs @@ -17,6 +17,8 @@ fn tag(k: &ItemKind) -> &'static str { match k { ItemKind::BeginOpacity { .. } => "opacity-begin", ItemKind::BeginIsolatedOpacity { .. } => "isolated-opacity-begin", + ItemKind::BeginIsolatedBlend { .. } => "isolated-blend-begin", + ItemKind::EndIsolatedBlend => "isolated-blend-end", ItemKind::EndOpacity => "opacity-end", ItemKind::BeginClipRect { .. } => "clip-begin", ItemKind::BeginClipPath { .. } => "path-clip-begin", diff --git a/crates/n0/tests/group_blending.rs b/crates/n0/tests/group_blending.rs new file mode 100644 index 00000000..e82108e2 --- /dev/null +++ b/crates/n0/tests/group_blending.rs @@ -0,0 +1,927 @@ +//! Independent hand-built rframe consumer probes, without Web lowering. +//! Interior RGBA-premultiplied pixels use exact, integer-valued source-over +//! blend equations, with explicitly named native layer quantization below. +//! Reuse checks are equivalence laws, not Chromium reftests; +//! Chromium's layer quantization is independently probed by the Web rung. + +use std::sync::Arc; + +use cg::CGColor; +use math2::{transform::AffineTransform, Rectangle}; +use n0::glyphless::{compile, diff_frame, BuildError, FrameProduct}; +use n0::paint::{read_pixels, PaintCtx}; +use rframe::{ + ClipGeometry, ClipLayer, ClipPath, Frame, FrameItem, FrameItems, FrameNode, Geometry, Identity, + Mask, MaskMode, PaintStack, PatternPaint, Provenance, Scope, ScopeBlend, ScopeBlendMode, + ScopeEffect, ScopeOpacity, Stroke, StrokeCap, StrokeJoin, VisualRef, +}; + +const SIZE: i32 = 48; +const MODES: [ScopeBlendMode; 3] = [ + ScopeBlendMode::Normal, + ScopeBlendMode::Multiply, + ScopeBlendMode::Screen, +]; +const BACKDROP: CGColor = CGColor::from_rgb(51, 102, 153); +const FIRST: CGColor = CGColor::from_rgb(85, 170, 255); +const SECOND: CGColor = CGColor::from_rgb(170, 85, 0); + +fn owner(id: u64) -> VisualRef { + VisualRef::new(Identity::new(id), Provenance::new(id + 1000)) +} + +fn rect(x: f32, y: f32, w: f32, h: f32) -> Rectangle { + Rectangle::from_xywh(x, y, w, h) +} + +fn node(id: u64, bounds: Rectangle, paints: PaintStack) -> FrameNode { + FrameNode { + owner: owner(id), + transform: AffineTransform::identity(), + geometry: Geometry::Rect(bounds), + bounds, + paints, + stroke: None, + } +} + +fn solid(id: u64, bounds: Rectangle, color: CGColor) -> FrameItem { + FrameItem::Node(node(id, bounds, PaintStack::solid(color))) +} + +fn begin(id: u64, effect: ScopeEffect) -> FrameItem { + FrameItem::ScopeBegin(Scope { + owner: owner(id), + effect, + }) +} + +fn blend(id: u64, mode: ScopeBlendMode, opacity: Option) -> FrameItem { + begin( + id, + ScopeEffect::Blend(ScopeBlend::new( + mode, + opacity.map(|opacity| ScopeOpacity::new(opacity).unwrap()), + )), + ) +} + +fn frame(items: Vec) -> Frame { + Frame { + owner: owner(900), + bounds: rect(0.0, 0.0, SIZE as f32, SIZE as f32), + items: FrameItems::try_new(items).unwrap(), + } +} + +fn raster(product: &FrameProduct, backdrop: CGColor) -> Vec { + let mut surface = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); + surface.canvas().clear(skia_safe::Color::from_argb( + backdrop.a, backdrop.r, backdrop.g, backdrop.b, + )); + let saves = surface.canvas().save_count(); + product + .execute( + surface.canvas(), + &AffineTransform::identity(), + &PaintCtx::new(None), + ) + .unwrap(); + assert_eq!( + surface.canvas().save_count(), + saves, + "all group scopes restore" + ); + read_pixels(&mut surface, SIZE, SIZE) +} + +fn at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] { + let offset = (y * SIZE as usize + x) * 4; + pixels[offset..offset + 4].try_into().unwrap() +} + +#[cfg(feature = "trace")] +mod layer_metrics { + use super::*; + use n0::trace::{sink::drain_blend_layers, BlendLayerMetrics}; + + fn single() -> FrameProduct { + compile(frame(vec![ + blend(10, ScopeBlendMode::Multiply, Some(0.5)), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), + FrameItem::ScopeEnd, + ])) + .unwrap() + } + + #[test] + fn combined_blend_and_opacity_observe_one_real_raster_layer() { + drain_blend_layers(); + let pixels = raster(&single(), BACKDROP); + let metrics = drain_blend_layers(); + assert_eq!( + metrics, + [BlendLayerMetrics { + save_layer_calls: 1, + observed_raster_layers: 1, + observed_raster_bytes: (SIZE * SIZE * 4) as u128, + observed_raster_pixels: (SIZE * SIZE) as u128, + peak_live_blend_bytes: (SIZE * SIZE * 4) as u128, + ..BlendLayerMetrics::default() + }] + ); + assert_eq!(at(&pixels, 0, 0), [51, 102, 153, 255]); + assert!(drain_blend_layers().is_empty()); + } + + #[test] + fn nested_and_sequential_layers_have_distinct_live_peaks() { + let scene = |nested| { + let first = solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST); + let second = solid(2, rect(16.0, 16.0, 24.0, 24.0), SECOND); + compile(frame(if nested { + vec![ + blend(10, ScopeBlendMode::Normal, None), + first, + blend(11, ScopeBlendMode::Multiply, None), + second, + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ] + } else { + vec![ + blend(10, ScopeBlendMode::Normal, None), + first, + FrameItem::ScopeEnd, + blend(11, ScopeBlendMode::Multiply, None), + second, + FrameItem::ScopeEnd, + ] + })) + .unwrap() + }; + for nested in [false, true] { + drain_blend_layers(); + raster(&scene(nested), BACKDROP); + let metrics = drain_blend_layers(); + assert_eq!(metrics.len(), 1); + assert_eq!(metrics[0].save_layer_calls, 2); + assert_eq!(metrics[0].observed_raster_layers, 2); + let bytes = (SIZE * SIZE * 4) as u128; + assert_eq!(metrics[0].observed_raster_bytes, 2 * bytes); + assert_eq!( + metrics[0].peak_live_blend_bytes, + if nested { 2 * bytes } else { bytes } + ); + } + } + + #[test] + fn an_empty_clip_never_counts_the_parent_surface_as_a_blend_layer() { + drain_blend_layers(); + let mut surface = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); + surface + .canvas() + .clip_rect(skia_safe::Rect::new_empty(), None, false); + single() + .execute( + surface.canvas(), + &AffineTransform::identity(), + &PaintCtx::new(None), + ) + .unwrap(); + assert_eq!( + drain_blend_layers(), + [BlendLayerMetrics { + save_layer_calls: 1, + empty_clip_saves: 1, + ..BlendLayerMetrics::default() + }] + ); + } + + #[test] + fn observed_storage_follows_the_real_clipped_layer_not_the_frame_bounds() { + drain_blend_layers(); + let mut surface = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); + surface.canvas().clip_rect( + skia_safe::Rect::from_xywh(3.0, 5.0, 12.0, 10.0), + None, + false, + ); + single() + .execute( + surface.canvas(), + &AffineTransform::identity(), + &PaintCtx::new(None), + ) + .unwrap(); + assert_eq!( + drain_blend_layers(), + [BlendLayerMetrics { + save_layer_calls: 1, + observed_raster_layers: 1, + observed_raster_bytes: 12 * 10 * 4, + observed_raster_pixels: 12 * 10, + peak_live_blend_bytes: 12 * 10 * 4, + ..BlendLayerMetrics::default() + }] + ); + } + + #[test] + fn a_recording_canvas_reports_missing_storage_without_inventing_bytes() { + drain_blend_layers(); + let mut recorder = skia_safe::PictureRecorder::new(); + let canvas = + recorder.begin_recording(skia_safe::Rect::from_wh(SIZE as f32, SIZE as f32), false); + single() + .execute(canvas, &AffineTransform::identity(), &PaintCtx::new(None)) + .unwrap(); + assert_eq!( + drain_blend_layers(), + [BlendLayerMetrics { + save_layer_calls: 1, + missing_observations: 1, + ..BlendLayerMetrics::default() + }] + ); + assert!(recorder.finish_recording_as_picture(None).is_some()); + } + + #[test] + fn nested_resource_recording_contributes_missing_observations_to_execute() { + let tile = FrameItems::try_new(vec![ + blend(11, ScopeBlendMode::Screen, None), + solid(1, rect(0.0, 0.0, 16.0, 16.0), FIRST), + FrameItem::ScopeEnd, + ]) + .unwrap(); + let pattern = + PatternPaint::new(16.0, 16.0, AffineTransform::identity(), Arc::new(tile), 1.0) + .unwrap(); + let product = compile(frame(vec![ + blend(10, ScopeBlendMode::Normal, None), + FrameItem::Node(node( + 20, + rect(0.0, 0.0, 48.0, 48.0), + PaintStack::from_pattern(pattern), + )), + FrameItem::ScopeEnd, + ])) + .unwrap(); + // Compiling/preflighting a repeating program can itself record a + // picture. Discard that completed execute observation before replay. + drain_blend_layers(); + let pixels = raster(&product, BACKDROP); + assert_eq!(at(&pixels, 8, 8), [85, 170, 255, 255]); + let mut metrics = drain_blend_layers(); + // FrameProduct::execute preflights the pattern before its outer + // drawlist replay; that recording is a separate completed execution. + assert_eq!( + metrics.remove(0), + BlendLayerMetrics { + save_layer_calls: 1, + missing_observations: 1, + ..BlendLayerMetrics::default() + } + ); + assert_eq!( + metrics, + [BlendLayerMetrics { + save_layer_calls: 2, + observed_raster_layers: 1, + observed_raster_bytes: (SIZE * SIZE * 4) as u128, + observed_raster_pixels: (SIZE * SIZE) as u128, + peak_live_blend_bytes: (SIZE * SIZE * 4) as u128, + missing_observations: 1, + ..BlendLayerMetrics::default() + }] + ); + } + + #[test] + fn instrumented_execute_matches_the_same_native_operations_without_observation() { + // Instrumentation equivalence, not an independent Chromium oracle. + // The control never calls access_top_layer_pixels. + let source = CGColor::from_rgba(205, 104, 67, 153); + let bounds = rect(8.25, 8.125, 24.5, 24.75); + for mode in MODES { + let product = compile(frame(vec![ + blend(10, mode, Some(0.5)), + solid(1, bounds, source), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let instrumented = raster(&product, BACKDROP); + let mut control = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); + let canvas = control.canvas(); + canvas.clear(skia_safe::Color::from_rgb( + BACKDROP.r, BACKDROP.g, BACKDROP.b, + )); + let mut restore = skia_safe::Paint::default(); + restore.set_alpha_f(0.5); + restore.set_blend_mode(match mode { + ScopeBlendMode::Normal => skia_safe::BlendMode::SrcOver, + ScopeBlendMode::Multiply => skia_safe::BlendMode::Multiply, + ScopeBlendMode::Screen => skia_safe::BlendMode::Screen, + }); + canvas.save_layer(&skia_safe::canvas::SaveLayerRec::default().paint(&restore)); + let mut paint = skia_safe::Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb( + source.a, source.r, source.g, source.b, + )); + canvas.draw_rect( + skia_safe::Rect::from_xywh(bounds.x, bounds.y, bounds.width, bounds.height), + &paint, + ); + canvas.restore(); + assert_eq!( + instrumented, + read_pixels(&mut control, SIZE, SIZE), + "{mode:?}" + ); + } + drain_blend_layers(); + } +} + +#[test] +fn opaque_group_blends_once_after_overlapping_children_complete() { + // b*s and b+s-b*s are integer code values for this palette. Probes: + // (8,8) first only, (20,20) overlap, (36,36) second only, (2,2) untouched. + let first = [ + [85, 170, 255, 255], + [17, 68, 153, 255], + [119, 204, 255, 255], + ]; + let second = [[170, 85, 0, 255], [34, 34, 0, 255], [187, 153, 153, 255]]; + for (index, mode) in MODES.into_iter().enumerate() { + let product = compile(frame(vec![ + blend(10, mode, None), + solid(1, rect(4.0, 4.0, 24.0, 24.0), FIRST), + solid(2, rect(16.0, 16.0, 24.0, 24.0), SECOND), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let pixels = raster(&product, BACKDROP); + assert_eq!(at(&pixels, 8, 8), first[index], "{mode:?} first"); + assert_eq!(at(&pixels, 20, 20), second[index], "{mode:?} overlap"); + assert_eq!(at(&pixels, 36, 36), second[index], "{mode:?} second"); + assert_eq!(at(&pixels, 2, 2), [51, 102, 153, 255]); + } +} + +#[test] +fn fill_and_stroke_complete_before_the_group_blends() { + // An opaque centred stroke overlaps the fill at (10,20). Applying the + // blend independently to fill and stroke would blend against the fill. + for (mode, expected) in [ + (ScopeBlendMode::Multiply, [34, 34, 0, 255]), + (ScopeBlendMode::Screen, [187, 153, 153, 255]), + ] { + let mut shape = node(1, rect(8.0, 8.0, 24.0, 24.0), PaintStack::solid(FIRST)); + shape.stroke = Stroke::new( + PaintStack::solid(SECOND), + 8.0, + StrokeCap::Butt, + StrokeJoin::Round, + 4.0, + ) + .unwrap(); + let product = compile(frame(vec![ + blend(10, mode, None), + FrameItem::Node(shape), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let pixels = raster(&product, BACKDROP); + assert_eq!( + at(&pixels, 10, 20), + expected, + "fill/stroke overlap {mode:?}" + ); + assert_eq!(at(&pixels, 6, 20), expected, "stroke-only {mode:?}"); + } +} + +#[test] +fn partially_transparent_children_finish_source_over_before_group_blending() { + // Red alpha 153 followed by green alpha 170 yields premul [51,170,0,221]. + // Over opaque blue, multiply keeps only the uncovered blue (255-221); + // screen keeps the red/green source channels and all of the blue backdrop. + for (mode, expected) in [ + (ScopeBlendMode::Multiply, [0, 0, 34, 255]), + (ScopeBlendMode::Screen, [51, 170, 255, 255]), + ] { + let product = compile(frame(vec![ + blend(10, mode, None), + solid( + 1, + rect(4.0, 4.0, 24.0, 24.0), + CGColor::from_rgba(255, 0, 0, 153), + ), + solid( + 2, + rect(16.0, 16.0, 24.0, 24.0), + CGColor::from_rgba(0, 255, 0, 170), + ), + FrameItem::ScopeEnd, + ])) + .unwrap(); + assert_eq!( + at(&raster(&product, CGColor::BLUE), 20, 20), + expected, + "{mode:?}" + ); + } +} + +#[test] +fn partial_source_and_backdrop_use_source_over_alpha_for_every_mode() { + // as=153/255, ab=170/255: as*ab=102/255, ao=221/255. + // Green backdrop, red source make each blend channel exactly calculable. + let backdrop = CGColor::from_rgba(0, 255, 0, 170); + for (mode, expected) in [ + (ScopeBlendMode::Normal, [153, 68, 0, 221]), + (ScopeBlendMode::Multiply, [51, 68, 0, 221]), + (ScopeBlendMode::Screen, [153, 170, 0, 221]), + ] { + let product = compile(frame(vec![ + blend(10, mode, None), + solid( + 1, + rect(8.0, 8.0, 24.0, 24.0), + CGColor::from_rgba(255, 0, 0, 153), + ), + FrameItem::ScopeEnd, + ])) + .unwrap(); + assert_eq!( + at(&raster(&product, backdrop), 16, 16), + expected, + "{mode:?}" + ); + assert_eq!( + at(&raster(&product, CGColor::TRANSPARENT), 16, 16), + [153, 0, 0, 153], + "transparent backdrop {mode:?}" + ); + } +} + +#[test] +fn transparent_group_leaves_a_partial_backdrop_unchanged() { + for mode in MODES { + let product = compile(frame(vec![ + blend(10, mode, Some(0.6)), + solid(1, rect(8.0, 8.0, 24.0, 24.0), CGColor::TRANSPARENT), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let pixels = raster(&product, CGColor::from_rgba(0, 255, 0, 170)); + assert_eq!(at(&pixels, 16, 16), [0, 170, 0, 170], "{mode:?}"); + } +} + +#[test] +fn group_opacity_attenuates_the_completed_source_once() { + // Two opaque overlapping red children become one opaque red source. + // Final opacity 0.6 produces as=153/255. The pinned native Normal layer + // restore gives green=67, one below the exact green=68 from an alpha-153 + // paint above. This is a native quantization regression assertion, not a + // claim of Chromium parity and not a tolerance over that difference. + for (mode, expected) in [ + (ScopeBlendMode::Normal, [153, 67, 0, 221]), + (ScopeBlendMode::Multiply, [51, 68, 0, 221]), + (ScopeBlendMode::Screen, [153, 170, 0, 221]), + ] { + let product = compile(frame(vec![ + blend(10, mode, Some(0.6)), + solid(1, rect(4.0, 4.0, 24.0, 24.0), CGColor::RED), + solid(2, rect(16.0, 16.0, 24.0, 24.0), CGColor::RED), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let pixels = raster(&product, CGColor::from_rgba(0, 255, 0, 170)); + for (x, y) in [(8, 8), (20, 20), (36, 36)] { + assert_eq!(at(&pixels, x, y), expected, "{mode:?} at ({x},{y})"); + } + } +} + +#[test] +fn neutral_outer_group_isolates_a_nested_blend_from_the_external_backdrop() { + for mode in [ScopeBlendMode::Multiply, ScopeBlendMode::Screen] { + let child = vec![ + blend(11, mode, None), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), + FrameItem::ScopeEnd, + ]; + let direct = compile(frame(child.clone())).unwrap(); + let mut isolated = vec![blend(10, ScopeBlendMode::Normal, None)]; + isolated.extend(child); + isolated.push(FrameItem::ScopeEnd); + let isolated = compile(frame(isolated)).unwrap(); + let pixels = raster(&isolated, BACKDROP); + assert_eq!( + at(&pixels, 16, 16), + [85, 170, 255, 255], + "{mode:?} sees transparent" + ); + assert_ne!( + pixels, + raster(&direct, BACKDROP), + "unit normal must not be erased" + ); + } +} + +#[test] +fn combined_opacity_and_blend_differ_from_an_outer_opacity_scope() { + for (mode, combined_expected) in [ + (ScopeBlendMode::Multiply, [51, 68, 0, 221]), + (ScopeBlendMode::Screen, [153, 170, 0, 221]), + ] { + let shape = solid(1, rect(8.0, 8.0, 24.0, 24.0), CGColor::RED); + let combined = compile(frame(vec![ + blend(10, mode, Some(0.6)), + shape.clone(), + FrameItem::ScopeEnd, + ])) + .unwrap(); + let nested = compile(frame(vec![ + begin(12, ScopeEffect::Opacity(ScopeOpacity::new(0.6).unwrap())), + blend(10, mode, None), + shape, + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ])) + .unwrap(); + let backdrop = CGColor::from_rgba(0, 255, 0, 170); + assert_eq!(at(&raster(&combined, backdrop), 16, 16), combined_expected); + assert_eq!( + at(&raster(&nested, backdrop), 16, 16), + [153, 67, 0, 221], + "nested blend sees transparent black, then normal opacity sees green" + ); + } +} + +#[test] +fn normal_blend_with_opacity_matches_existing_isolated_opacity() { + let children = vec![ + solid(1, rect(4.0, 4.0, 24.0, 24.0), FIRST), + solid(2, rect(16.0, 16.0, 24.0, 24.0), SECOND), + ]; + for opacity in [0.125, 0.375, 0.5, 0.6, 0.999] { + let scene = |effect| { + let mut items = vec![begin(10, effect)]; + items.extend(children.clone()); + items.push(FrameItem::ScopeEnd); + compile(frame(items)).unwrap() + }; + let opacity = ScopeOpacity::new(opacity).unwrap(); + let old = scene(ScopeEffect::Opacity(opacity)); + let new = scene(ScopeEffect::Blend(ScopeBlend::new( + ScopeBlendMode::Normal, + Some(opacity), + ))); + for backdrop in [ + BACKDROP, + CGColor::TRANSPARENT, + CGColor::from_rgba(0, 255, 0, 170), + ] { + assert_eq!(raster(&old, backdrop), raster(&new, backdrop)); + } + } +} + +fn clip(bounds: Rectangle) -> ClipPath { + ClipPath::new(vec![ClipLayer::new(vec![ClipGeometry::new( + AffineTransform::identity(), + Geometry::Rect(bounds), + ) + .unwrap()]) + .unwrap()]) + .unwrap() +} + +#[test] +fn changing_blend_or_opacity_damages_only_the_scope_and_its_clipped_child_union() { + let scene = |mode, opacity| { + compile(frame(vec![ + blend(10, mode, opacity), + solid(1, rect(4.0, 8.0, 12.0, 12.0), FIRST), + begin(11, ScopeEffect::Clip(clip(rect(16.0, 12.0, 8.0, 8.0)))), + solid(2, rect(0.0, 0.0, 48.0, 48.0), SECOND), + FrameItem::ScopeEnd, + solid(3, rect(60.0, 60.0, 12.0, 12.0), FIRST), + FrameItem::ScopeEnd, + ])) + .unwrap() + }; + let before = scene(ScopeBlendMode::Multiply, None); + assert!(diff_frame(&before, &scene(ScopeBlendMode::Multiply, None)).is_empty()); + for after in [ + scene(ScopeBlendMode::Screen, None), + scene(ScopeBlendMode::Multiply, Some(0.6)), + ] { + let damage = diff_frame(&before, &after); + assert_eq!(damage.changed, [owner(10)]); + assert_eq!(damage.union_frame, Some(rect(4.0, 8.0, 20.0, 12.0))); + } +} + +#[test] +fn fully_clipped_blend_edit_keeps_owner_but_has_no_coverage() { + let scene = |mode| { + compile(frame(vec![ + blend(10, mode, None), + solid(1, rect(60.0, 60.0, 12.0, 12.0), FIRST), + FrameItem::ScopeEnd, + ])) + .unwrap() + }; + let damage = diff_frame( + &scene(ScopeBlendMode::Multiply), + &scene(ScopeBlendMode::Screen), + ); + assert_eq!(damage.changed, [owner(10)]); + assert_eq!(damage.union_frame, None); +} + +#[test] +fn blend_scope_coverage_includes_its_childs_stroke_outset() { + let scene = |mode| { + let mut shape = node(1, rect(8.0, 8.0, 24.0, 24.0), PaintStack::solid(FIRST)); + shape.stroke = Stroke::new( + PaintStack::solid(SECOND), + 8.0, + StrokeCap::Butt, + StrokeJoin::Round, + 4.0, + ) + .unwrap(); + compile(frame(vec![ + blend(10, mode, None), + FrameItem::Node(shape), + FrameItem::ScopeEnd, + ])) + .unwrap() + }; + let damage = diff_frame( + &scene(ScopeBlendMode::Multiply), + &scene(ScopeBlendMode::Screen), + ); + assert_eq!(damage.changed, [owner(10)]); + // Stroke coverage deliberately rounds outwards. Its law is containment, + // not equality to the unrounded mathematical stroke box (4,4)-(36,36). + let coverage = damage.union_frame.unwrap(); + assert!(coverage.x <= 4.0 && coverage.y <= 4.0); + assert!(coverage.x + coverage.width >= 36.0); + assert!(coverage.y + coverage.height >= 36.0); + assert!(coverage.x >= 0.0 && coverage.y >= 0.0); + assert!(coverage.x + coverage.width <= SIZE as f32); + assert!(coverage.y + coverage.height <= SIZE as f32); +} + +#[test] +fn a_blend_owner_cannot_alias_a_child_owner() { + assert!( + matches!(compile(frame(vec![blend(1, ScopeBlendMode::Normal, None), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), FrameItem::ScopeEnd])), + Err(BuildError::DuplicateOwner(value)) if value == owner(1)) + ); +} + +#[test] +fn changed_earlier_backdrop_with_retained_group_matches_fresh() { + // Glyphless reuse is immutable compiled replay; there is no raster cache. + // Reuse the group under a changed host backdrop, then compare a complete + // fresh frame. An unchanged group's previous output is NOT reusable. + for mode in [ScopeBlendMode::Multiply, ScopeBlendMode::Screen] { + let children = vec![ + blend(10, mode, Some(0.6)), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), + FrameItem::ScopeEnd, + ]; + let retained = compile(frame(children.clone())).unwrap(); + let scene = |color| { + let mut items = vec![solid(3, rect(0.0, 0.0, 48.0, 48.0), color)]; + items.extend(children.clone()); + compile(frame(items)).unwrap() + }; + let before = scene(BACKDROP); + let after = scene(SECOND); + let damage = diff_frame(&before, &after); + assert_eq!( + damage.changed, + [owner(3)], + "damage attributes changed facts" + ); + assert_eq!(damage.union_frame, Some(rect(0.0, 0.0, 48.0, 48.0))); + let old_pixels = raster(&retained, BACKDROP); + let reused = raster(&retained, SECOND); + assert_ne!( + old_pixels, reused, + "unchanged group still depends on backdrop" + ); + assert_eq!(reused, raster(&after, CGColor::TRANSPARENT)); + assert_eq!(raster(&retained.clone(), SECOND), reused); + assert_eq!(raster(&before, CGColor::TRANSPARENT), old_pixels); + } +} + +#[test] +fn rect_group_edit_matrix_retained_matches_fresh_and_damage_contains_changed_pixels() { + #[derive(Clone, Copy, Debug)] + enum Edit { + Base, + SourceColor, + SourcePosition, + Reorder, + Removal, + IsolationToggle, + } + + let scene = |edit| { + let isolated = !matches!(edit, Edit::IsolationToggle); + let mut items = vec![solid(101, rect(0.0, 0.0, 48.0, 48.0), BACKDROP)]; + if isolated { + items.push(blend(10, ScopeBlendMode::Normal, None)); + } + items.push(solid( + 102, + if matches!(edit, Edit::SourcePosition) { + rect(8.0, 4.0, 24.0, 24.0) + } else { + rect(4.0, 8.0, 24.0, 24.0) + }, + FIRST, + )); + items.push(blend(11, ScopeBlendMode::Multiply, Some(0.6))); + let lower = solid( + 103, + rect(16.0, 12.0, 24.0, 24.0), + if matches!(edit, Edit::SourceColor) { + CGColor::from_rgb(255, 0, 0) + } else { + SECOND + }, + ); + let upper = solid( + 104, + rect(12.0, 20.0, 24.0, 24.0), + CGColor::from_rgba(0, 255, 0, 153), + ); + match edit { + Edit::Reorder => items.extend([upper, lower]), + Edit::Removal => items.push(lower), + _ => items.extend([lower, upper]), + } + items.push(FrameItem::ScopeEnd); + if isolated { + items.push(FrameItem::ScopeEnd); + } + frame(items) + }; + + let before = compile(scene(Edit::Base)).unwrap(); + let before_pixels = raster(&before, CGColor::TRANSPARENT); + for edit in [ + Edit::SourceColor, + Edit::SourcePosition, + Edit::Reorder, + Edit::Removal, + Edit::IsolationToggle, + ] { + let retained = compile(scene(edit)).unwrap().clone(); + // Interleave another replay; no earlier raster result may be reused + // as the completed source of this retained command product. + assert_eq!(raster(&before, CGColor::TRANSPARENT), before_pixels); + let reused = raster(&retained, CGColor::TRANSPARENT); + let fresh = compile(scene(edit)).unwrap(); + assert_eq!(reused, raster(&fresh, CGColor::TRANSPARENT), "{edit:?}"); + let union = diff_frame(&before, &retained) + .union_frame + .expect("every edit has a damage union"); + let mut changed_pixels = 0; + for y in 0..SIZE as usize { + for x in 0..SIZE as usize { + if at(&before_pixels, x, y) == at(&reused, x, y) { + continue; + } + changed_pixels += 1; + // Integer Rect-only sources: require the whole changed pixel, + // not merely its centre, to fit the declared damage union. + assert!( + x as f32 >= union.x + && y as f32 >= union.y + && (x + 1) as f32 <= union.x + union.width + && (y + 1) as f32 <= union.y + union.height, + "{edit:?}: changed pixel ({x},{y}) escaped {union:?}" + ); + } + } + assert!(changed_pixels > 0, "{edit:?} must not be a vacuous probe"); + } +} + +#[test] +fn blend_scopes_compile_inside_repeating_programs_and_damage_the_outer_client() { + let scene = |mode| { + let tile = FrameItems::try_new(vec![ + solid(1, rect(0.0, 0.0, 16.0, 16.0), BACKDROP), + blend(10, mode, None), + solid(2, rect(4.0, 4.0, 8.0, 8.0), FIRST), + FrameItem::ScopeEnd, + ]) + .unwrap(); + let pattern = + PatternPaint::new(16.0, 16.0, AffineTransform::identity(), Arc::new(tile), 1.0) + .unwrap(); + compile(frame(vec![FrameItem::Node(node( + 20, + rect(0.0, 0.0, 48.0, 48.0), + PaintStack::from_pattern(pattern), + ))])) + .unwrap() + }; + let before = scene(ScopeBlendMode::Multiply); + let after = scene(ScopeBlendMode::Screen); + for (product, expected) in [ + (&before, [17, 68, 153, 255]), + (&after, [119, 204, 255, 255]), + ] { + let pixels = raster(product, CGColor::TRANSPARENT); + for (x, y) in [(8, 8), (24, 24), (40, 40)] { + assert_eq!(at(&pixels, x, y), expected); + } + } + assert_eq!(diff_frame(&before, &after).changed, [owner(20)]); + assert_eq!( + raster(&after.clone(), BACKDROP), + raster(&scene(ScopeBlendMode::Screen), BACKDROP) + ); +} + +#[test] +fn blend_scopes_enclose_masks_and_replay_in_both_mask_phases() { + let product = compile(frame(vec![ + blend(10, ScopeBlendMode::Multiply, None), + FrameItem::MaskBegin(Mask::new( + owner(11), + MaskMode::Alpha, + clip(rect(0.0, 0.0, 48.0, 48.0)), + )), + blend(12, ScopeBlendMode::Screen, None), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), + FrameItem::ScopeEnd, + FrameItem::MaskSource, + blend(13, ScopeBlendMode::Normal, None), + solid(2, rect(0.0, 0.0, 24.0, 48.0), CGColor::WHITE), + FrameItem::ScopeEnd, + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + ])) + .unwrap(); + let pixels = raster(&product, BACKDROP); + assert_eq!(at(&pixels, 16, 16), [17, 68, 153, 255]); + assert_eq!(at(&pixels, 28, 16), [51, 102, 153, 255]); +} + +#[test] +fn a_source_generating_filter_supplies_blend_pixels_and_scope_coverage() { + use rframe::{Filter, FilterColorSpace, FilterNode, FilterPrimitive, FilterProgram}; + let region = rect(8.0, 8.0, 24.0, 24.0); + let scene = |mode| { + let program = FilterProgram::new(Arc::from([FilterNode::new( + Arc::from([]), + region, + FilterColorSpace::Srgb, + FilterPrimitive::SolidColor { + color: FIRST.into(), + }, + )])) + .unwrap(); + let filter = Filter::new(AffineTransform::identity(), region, program) + .unwrap() + .with_transparent_source(); + compile(frame(vec![ + blend(10, mode, None), + begin(11, ScopeEffect::Filter(filter)), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ])) + .unwrap() + }; + let before = scene(ScopeBlendMode::Multiply); + let after = scene(ScopeBlendMode::Screen); + assert_eq!(at(&raster(&before, BACKDROP), 16, 16), [17, 68, 153, 255]); + assert_eq!(at(&raster(&after, BACKDROP), 16, 16), [119, 204, 255, 255]); + let damage = diff_frame(&before, &after); + assert_eq!(damage.changed, [owner(10)]); + assert_eq!(damage.union_frame, Some(region)); +} diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 761f7c2c..97fc4646 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,10 +607,10 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,284 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,378 Chromium-baked cells plus 16 sampled frames; the text estate contains sixteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two - Bungee), and the named refusal register has 260 rows. `feFlood`, `feComposite`, + Bungee), and the named refusal register has 303 rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, `feDiffuseLighting`, `feDistantLight`, `fePointLight`, `feSpotLight`, @@ -843,6 +843,41 @@ cargo run -p n0_cli --bin n0 -- \ entries. Every non-identity HTML ancestor opacity is a distinct outer scope around the selected inline SVG; explicit `inherit` on the SVG compounds with those host scopes rather than flattening them. + CSS `mix-blend-mode: normal | multiply | screen` and `isolation: auto | + isolate` have a bounded static SVG group profile. One Stylo computed value + decides the operation; raw attribute lookalikes are inert. Neutral/default + groups add no layer. Normal isolation with no escaping child blend is + redundant; the compiler elides it and preserves the established opacity + fold/layer route, including over translucent backdrops. A required isolated + group begins transparent and + composites its completed source once with the same element's opacity; + blending each child separately or adding an outer opacity group is not the + same operation. Ordinary 2D transforms and nested viewport overflow clips + do not isolate descendants. Authored `clip-path` and existing partial-opacity + groups do. A standalone SVG supplies its transparent initial backdrop in + the resolved stream, independently of the caller's canvas clear color. + The current group-source profile is sharp-cornered rectangles, including + solid, linear-gradient and admitted repeating-pattern paints, fractional + placement, 2D transforms and simple local butt/miter strokes. Non-rectangular + source geometry, radial source paints, wider strokes and curved, subpixel or + rotated clip coverage retain the named `group-source precision` refusal. + These guards deliberately over-refuse unproved combinations. Eliding a + redundant Normal boundary does not admit its contribution beneath an + unproved ancestor filter or mask. A root blend with partial opacity has its + own `root-layer precision` refusal. Filter/mask + composition, blending inside resource source programs or on their roots, + text-local blending, and the fourteen other represented blend values remain + named refusals. CSS keyframes combined with either property retain the + animated-group patrol, including custom-property indirection and HTML-head + styles. Existing SMIL source guards are not relaxed. + HTML-inline blending is admitted only when the resolved SVG-local isolation + or opacity boundary contains its backdrop dependency. An escaping blend, + non-normal blend on the selected root, or blend/isolation on an HTML ancestor + refuses in both admissions: the command still extracts an SVG contribution, + not the exterior HTML page. Attributable unsupported groups are skipped and + named at their structural path in best-effort mode, without leaking a partial + source into surviving siblings. The CSS blending rows remain unchecked; + this does not broaden `feBlend`, per-paint blending, or native-model opacity. `` and `` paint servers are consumed (the gradient rung): `fill`/`stroke` `url(#…)` references resolve through a whole-document, first-id-wins gradient table (shadow-content clones diff --git a/crates/n0_cli/tests/group_blending.rs b/crates/n0_cli/tests/group_blending.rs new file mode 100644 index 00000000..fd0f5296 --- /dev/null +++ b/crates/n0_cli/tests/group_blending.rs @@ -0,0 +1,76 @@ +//! Cross-seam execution laws. The external pixel oracle remains the separate +//! Chromium corpus; these tests make the compiler's backdrop boundary explicit. +use math2::transform::AffineTransform; +use n0::paint::{PaintCtx, read_pixels}; +use rframe::{Frame, FrameItems}; +use skia_safe::{Color, surfaces}; +use websem::{InitialViewport, compile_standalone_svg}; + +fn compile(body: &str, style: &str) -> Frame { + let source = format!( + r#"{body}"# + ); + compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)).unwrap() +} + +fn paint(frame: Frame, backdrop: Color) -> Vec { + let product = n0::glyphless::compile(frame).unwrap(); + let mut surface = surfaces::raster_n32_premul((64, 64)).unwrap(); + surface.canvas().clear(backdrop); + product + .execute( + surface.canvas(), + &AffineTransform::identity(), + &PaintCtx::new(None), + ) + .unwrap(); + read_pixels(&mut surface, 64, 64) +} + +#[test] +fn standalone_root_blend_does_not_consume_the_callers_backdrop() { + let body = r##""##; + for color in [ + Color::from_argb(255, 51, 102, 153), + Color::from_argb(170, 0, 255, 0), + ] { + let reference = paint(compile(body, "mix-blend-mode:normal"), color); + for mode in ["multiply", "screen"] { + let frame = compile(body, &format!("mix-blend-mode:{mode}")); + assert_eq!(paint(frame.clone(), color), reference, "{mode}"); + // The colored destination makes removal of the initial transparent + // boundary observable; a transparent-only oracle cannot do that. + let mut without_initial_boundary = frame; + let items: Vec<_> = without_initial_boundary.items.iter().cloned().collect(); + without_initial_boundary.items = + FrameItems::try_new(items[1..items.len() - 1].to_vec()).unwrap(); + assert_ne!(paint(without_initial_boundary, color), reference, "{mode}"); + } + } +} + +#[test] +fn redundant_isolation_preserves_partial_backdrop_opacity_pixels() { + let backdrop = r##""##; + let plain = ""; + let reference = paint( + compile(&format!("{backdrop}{plain}"), ""), + Color::TRANSPARENT, + ); + let offset = (16 * 64 + 16) * 4; + assert_eq!(&reference[offset..offset + 4], &[153, 68, 0, 221]); + for body in [ + "", + "", + "", + ] { + assert_eq!( + paint( + compile(&format!("{backdrop}{body}"), ""), + Color::TRANSPARENT + ), + reference, + "{body}" + ); + } +} diff --git a/crates/rframe/README.md b/crates/rframe/README.md index 92ce127a..e9429569 100644 --- a/crates/rframe/README.md +++ b/crates/rframe/README.md @@ -24,7 +24,7 @@ producer (e.g. websem, from SVG) | `frame` | `Frame`, `FrameNode`, `Geometry`, leaf paint stacks or checked repeating vector programs, their post-paint alpha factor, and product identity | | `path` | `PathData` — checked absolute commands, fill rule, tight bounds solved once | | `stroke` | `Stroke` — centred width, cap, join, miter limit, optional checked dash pattern, and finite `f64` `outset` | -| `scope` | A checked painter-order scope stream: isolated opacity or source-neutral geometric clipping | +| `scope` | Resolved effect scopes: isolated opacity, combined group blend/opacity, geometric clipping, or image filtering | | `clip` | `ClipPath` — bounded path unions intersected in layers, with resolved transforms, conservative bounds, and an explicit anti-aliased or hard edge policy | Two details are load-bearing enough to state here. A node's `bounds` is the @@ -45,6 +45,42 @@ resolves the complete stack to no paint. Because a `Stroke` owns the same `PaintStack`, fill and stroke cross the contract with one meaning and no source-specific duplicate field. +`ScopeEffect::Blend(ScopeBlend)` states one isolated group's final blend and +opacity. Its children paint in order against transparent black. The completed +group's premultiplied color and alpha receive the opacity once, then blend and +composite source-over into the enclosing backdrop in one final operation. +Fill/stroke overlap is already part of that completed group. A separate outer +opacity scope around a blend scope states a different nesting and backdrop; +it is not an equivalent spelling of combined blend and opacity. + +`ScopeBlend::new(mode, opacity)` takes `ScopeBlendMode::{Normal, Multiply, +Screen}` and `Option`. `mode()` and `opacity()` return those +facts unchanged. `None` means opacity 1; `Some` reuses the finite, strictly +between-zero-and-one `ScopeOpacity` check. Zero opacity resolves to no emitted +group. In particular, `Normal` with `None` **retains isolation**; a group whose +children should paint directly into the enclosing backdrop has no scope. +The existing `ScopeEffect::Opacity` still means isolated normal composition +at its checked opacity, and its constructor still rejects both 0 and 1. + +The scope-specific blend enum admits only these three functions. Reusing all +of `cg::BlendMode` would admit modes beyond this contract; reusing +`FilterBlend` would confuse a group joining its enclosing backdrop with two +explicit filter inputs. Leaf paints remain normal-only. Additive composition +(including plus-lighter), backdrop-preserving group opacity, arbitrary +compositing operators, and configurable backdrop initialization are not part +of this scope. Source producers must guard unsupported operations at ingress. +No layer allocation, backdrop copy, cache policy, or authored tree is implied. +An unchanged group can produce a different blended result when its enclosing +backdrop changes; equality of group facts does not remove that dependency. + +The existing `FrameItems` validator checks blend scopes using the same balance, +non-empty-content, and combined scope/mask depth rules as other effects +(`MAX_SCOPE_DEPTH = 64`). Repeating programs retain checked immutable item +streams and their separate `MAX_PATTERN_DEPTH = 8` bound. Validation preserves +the supplied painter order and opaque owners. Independent diagram construction +and mixed-scope, mask, and repeating-program laws live in +[`tests/blend_contract.rs`](tests/blend_contract.rs). + `Stroke::outset()` widens only the arithmetic for that derived, direction-free bound. The resolved width and miter limit remain exact `f32` facts, while every stroke admitted from finite members has a finite `f64` diff --git a/crates/rframe/src/frame.rs b/crates/rframe/src/frame.rs index 09847e26..df5909c5 100644 --- a/crates/rframe/src/frame.rs +++ b/crates/rframe/src/frame.rs @@ -10,7 +10,7 @@ //! objects, and no serialization. //! //! It is deliberately minimal (solid- and gradient-filled rectangles, -//! ellipses, and paths, composited flat or through checked opacity, clip, +//! ellipses, and paths, composited flat or through checked opacity, group blend, clip, //! mask, and image-filter effects) and //! **breakable**: the enums grow as real producers force new visual facts, and //! the sharing boundary moves *down* (toward the engine's private drawlist) @@ -526,7 +526,8 @@ pub enum FrameItem { /// One resolved painted node. Node(FrameNode), /// The following items, up to the matching [`FrameItem::ScopeEnd`], - /// composite as one isolated group under this scope's effect. + /// share this resolved effect. Opacity, blend, and filter isolate the + /// group; geometric clipping alone does not. ScopeBegin(Scope), /// Closes the innermost open scope. ScopeEnd, diff --git a/crates/rframe/src/lib.rs b/crates/rframe/src/lib.rs index a932d35a..b3cb46ca 100644 --- a/crates/rframe/src/lib.rs +++ b/crates/rframe/src/lib.rs @@ -37,7 +37,7 @@ pub use frame::{ }; pub use mask::{Mask, MaskMode}; pub use path::{FillRule, PathCommand, PathData, PathDataError}; -pub use scope::{Scope, ScopeEffect, ScopeOpacity, ScopeOpacityError}; +pub use scope::{Scope, ScopeBlend, ScopeBlendMode, ScopeEffect, ScopeOpacity, ScopeOpacityError}; pub use stroke::{ Stroke, StrokeCap, StrokeDash, StrokeDashError, StrokeDashIntervals, StrokeDashIntervalsError, StrokeError, StrokeJoin, StrokeSpace, diff --git a/crates/rframe/src/scope.rs b/crates/rframe/src/scope.rs index 480c5c6e..df8813d2 100644 --- a/crates/rframe/src/scope.rs +++ b/crates/rframe/src/scope.rs @@ -1,9 +1,10 @@ //! The compositing scope: the group fact of the resolved contract. //! -//! A scope states that the items it encloses composite as **one isolated -//! group** — its effect applies to the group's composite, never per item. -//! That is the only thing a scope is for: a fact a producer *could* state on -//! one paint pass is stated there instead. [`crate::PaintAlphaFactor`], for +//! Opacity, blend, and filter scopes state that the items they enclose +//! composite as **one isolated group** — their effect applies to the group's +//! composite, never per item. A clip scope instead constrains paint coverage +//! without isolation. A fact a producer *could* state on one paint pass is +//! stated there instead. [`crate::PaintAlphaFactor`], for //! example, modulates each paint entry without isolation after its own alpha //! materializes. A scope is the byte-distinct fact that no such per-paint //! statement can express — a translucent group whose contents overlap, or a @@ -22,9 +23,10 @@ use crate::frame::VisualRef; /// Why an opacity cannot be a scope fact. /// -/// A scope opacity lives in the **open** unit interval: `1` is identity -/// (the producer omits the scope) and `0` composites nothing (the -/// producer emits nothing), so neither is a fact a scope can carry. +/// A scope opacity lives in the **open** unit interval: for an opacity-only +/// effect, `1` is identity (the producer omits the scope) and `0` composites +/// nothing (the producer emits nothing). A [`ScopeBlend`] expresses unit +/// opacity with `None` while retaining its isolation boundary. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScopeOpacityError { pub value: f32, @@ -60,11 +62,94 @@ impl ScopeOpacity { } } +/// The admitted blend functions for an isolated group's final composition. +/// +/// These operate on unpremultiplied group and backdrop color channels; alpha +/// uses source-over for all three modes. This vocabulary is deliberately +/// narrower than [`cg::BlendMode`] and separate from [`crate::FilterBlend`]: +/// it admits neither arbitrary leaf blends nor two-image filter operations. +/// Additive composition and backdrop-preserving groups are inexpressible. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScopeBlendMode { + /// Use the completed group's color unchanged: `B(b, s) = s`. + Normal, + /// Multiply backdrop and completed-group color: `B(b, s) = b * s`. + Multiply, + /// Complement the product of complements: `B(b, s) = b + s - b * s`. + Screen, +} + +/// One isolated group's combined final blend and optional opacity. +/// +/// Children paint in order against transparent black. Their completed +/// composite is the source; the enclosing composite at this position in +/// painter order is the backdrop. Apply the optional opacity to the completed +/// source's premultiplied color and alpha, then blend and composite it over +/// that backdrop using [`ScopeBlendMode`]. Opacity and blend belong to this +/// **one** final operation: splitting them into nested scopes changes the +/// backdrop seen by the blend and can add an intermediate quantization step. +/// Neither operation is distributed over child paints, fills, or strokes. +/// +/// `None` means unit opacity, including for [`ScopeBlendMode::Normal`]. Such a +/// scope still isolates descendants and must not be erased just because its +/// final blend and opacity are neutral. Painting children directly into the +/// enclosing backdrop is represented by **no scope**. Zero opacity resolves +/// to no emitted group before construction, as with [`ScopeEffect::Opacity`]. +/// +/// Non-normal blending depends on the enclosing backdrop even when all +/// enclosed items are unchanged. Equality of this fact and its children does +/// not prove equality of the final blended result across different backdrops. +/// This names visual meaning, never a layer allocation, backdrop copy, cache +/// policy, or authored group. +/// +/// ``` +/// use rframe::{ScopeBlend, ScopeBlendMode, ScopeOpacity}; +/// +/// let isolated = ScopeBlend::new(ScopeBlendMode::Normal, None); +/// assert_eq!(isolated.opacity(), None); // unit opacity, still isolated +/// let translucent = ScopeBlend::new( +/// ScopeBlendMode::Multiply, +/// Some(ScopeOpacity::new(0.5)?), +/// ); +/// assert_eq!(translucent.mode(), ScopeBlendMode::Multiply); +/// # Ok::<(), rframe::ScopeOpacityError>(()) +/// ``` +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ScopeBlend { + mode: ScopeBlendMode, + opacity: Option, +} + +impl ScopeBlend { + /// Combine an admitted blend function and already-checked opacity. + /// `None` means opacity 1; it never means absence of isolation. + #[must_use] + pub const fn new(mode: ScopeBlendMode, opacity: Option) -> Self { + Self { mode, opacity } + } + + /// The blend function used only when the completed group joins its backdrop. + #[must_use] + pub const fn mode(self) -> ScopeBlendMode { + self.mode + } + + /// The final group opacity, or `None` for unit opacity. + #[must_use] + pub const fn opacity(self) -> Option { + self.opacity + } +} + /// The compositing effect a scope applies to its group's composite. #[derive(Clone, Debug, PartialEq)] pub enum ScopeEffect { /// The group composites at this opacity through one isolated layer. Opacity(ScopeOpacity), + /// Isolate the group, then blend its completed composite with the enclosing + /// backdrop at the optional opacity in one final operation. Normal blend + /// at unit opacity still retains this scope's isolation boundary. + Blend(ScopeBlend), /// Intersect every enclosed paint with resolved geometric coverage. /// Unlike opacity this creates no isolated layer: the clip is paint state, /// and its path facts reference no source or external resource. diff --git a/crates/rframe/tests/blend_contract.rs b/crates/rframe/tests/blend_contract.rs new file mode 100644 index 00000000..e23baccb --- /dev/null +++ b/crates/rframe/tests/blend_contract.rs @@ -0,0 +1,538 @@ +//! Producer-only laws for isolated group blending. +//! +//! A small diagram producer originates these frames from geometry and explicit +//! composition choices. No document parser, authored model, or renderer is +//! involved: the contract must preserve those choices on its own. + +use std::sync::Arc; + +use cg::{BlendMode, CGColor, LinearGradientPaint, Paint, Paints, RadialGradientPaint, SolidPaint}; +use math2::Rectangle; +use math2::transform::AffineTransform; +use rframe::{ + ClipGeometry, ClipLayer, ClipPath, Filter, FilterColorSpace, FilterInput, FilterNode, + FilterPrimitive, FilterProgram, Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, + Geometry, Identity, MAX_PATTERN_DEPTH, MAX_SCOPE_DEPTH, Mask, MaskMode, PaintAlphaFactor, + PaintStack, PaintStackError, PatternPaint, PatternPaintError, Provenance, Scope, ScopeBlend, + ScopeBlendMode, ScopeEffect, ScopeOpacity, VisualRef, +}; + +const MODES: [ScopeBlendMode; 3] = [ + ScopeBlendMode::Normal, + ScopeBlendMode::Multiply, + ScopeBlendMode::Screen, +]; + +fn owner(id: u64) -> VisualRef { + VisualRef::new(Identity::new(id), Provenance::new(id + 1000)) +} + +fn rect() -> Rectangle { + Rectangle::from_xywh(0.0, 0.0, 16.0, 16.0) +} + +fn node(id: u64, paints: PaintStack) -> FrameItem { + FrameItem::Node(FrameNode { + owner: owner(id), + transform: AffineTransform::identity(), + geometry: Geometry::Rect(rect()), + bounds: rect(), + paints, + stroke: None, + }) +} + +fn begin(id: u64, effect: ScopeEffect) -> FrameItem { + FrameItem::ScopeBegin(Scope { + owner: owner(id), + effect, + }) +} + +fn blend(id: u64, mode: ScopeBlendMode) -> FrameItem { + begin(id, ScopeEffect::Blend(ScopeBlend::new(mode, None))) +} + +fn clip() -> ClipPath { + ClipPath::new(vec![ + ClipLayer::new(vec![ + ClipGeometry::new(AffineTransform::identity(), Geometry::Rect(rect())).unwrap(), + ]) + .unwrap(), + ]) + .unwrap() +} + +fn mask(id: u64, mode: MaskMode) -> FrameItem { + FrameItem::MaskBegin(Mask::new(owner(id), mode, clip())) +} + +fn wrap(effect: ScopeEffect, children: Vec) -> Vec { + let mut items = vec![begin(100, effect)]; + items.extend(children); + items.push(FrameItem::ScopeEnd); + items +} + +fn checked(items: Vec) -> FrameItems { + let expected = items.clone(); + let checked = FrameItems::try_new(items).expect("resolved, balanced program"); + assert_eq!( + checked.iter().cloned().collect::>(), + expected, + "validation must preserve every fact, owner, and boundary in order" + ); + checked +} + +fn diagram(composition: Option) -> Frame { + // Two overlapping translucent shapes; the first also has a stroke and a + // separate paint alpha factor. Group composition cannot rewrite any of them. + let mut first = node( + 1, + PaintStack::solid(CGColor::from_rgba(220, 40, 60, 192)) + .with_alpha_factor(PaintAlphaFactor::new(0.75).unwrap()), + ); + let FrameItem::Node(first_node) = &mut first else { + unreachable!() + }; + first_node.stroke = rframe::Stroke::new( + PaintStack::solid(CGColor::BLUE), + 2.0, + rframe::StrokeCap::Round, + rframe::StrokeJoin::Round, + 4.0, + ) + .unwrap(); + let children = vec![ + first, + node(2, PaintStack::solid(CGColor::from_rgba(40, 200, 80, 128))), + ]; + Frame { + owner: owner(200), + bounds: rect(), + items: checked(match composition { + Some(composition) => wrap(ScopeEffect::Blend(composition), children), + None => children, + }), + } +} + +/// The exhaustive match is also an admission lock: growing the vocabulary +/// requires revisiting this producer's contract, not inheriting a shared enum. +#[test] +fn group_blending_admits_exactly_three_named_functions() { + let names = MODES.map(|mode| match mode { + ScopeBlendMode::Normal => "normal", + ScopeBlendMode::Multiply => "multiply", + ScopeBlendMode::Screen => "screen", + }); + assert_eq!(names, ["normal", "multiply", "screen"]); +} + +/// An explicit isolation boundary remains significant even if its own final +/// operation is neutral; a descendant may blend against that boundary later. +#[test] +fn an_independent_diagram_distinguishes_absence_isolation_and_blend() { + let direct = diagram(None); + let grouped = MODES.map(|mode| diagram(Some(ScopeBlend::new(mode, None)))); + assert_eq!(direct.items.len(), 2); + for (index, frame) in grouped.iter().enumerate() { + assert_eq!(frame.items.len(), 4); + assert_eq!(frame.nodes(), direct.nodes()); + assert_ne!(frame, &direct); + for other in &grouped[..index] { + assert_ne!(frame, other); + } + let FrameItem::ScopeBegin(scope) = frame.items.iter().next().unwrap() else { + panic!("unit opacity never erases the isolation boundary"); + }; + assert_eq!(scope.owner, owner(100)); + assert_eq!( + scope.effect, + ScopeEffect::Blend(ScopeBlend::new(MODES[index], None)) + ); + } +} + +/// Optional checked opacity adds the missing unit case without relaxing the +/// existing open interval or smuggling a raw scalar into a checked stream. +#[test] +fn unit_opacity_is_absence_of_attenuation_and_fractional_opacity_stays_exact() { + for mode in MODES { + let unit = ScopeBlend::new(mode, None); + assert_eq!(unit.mode(), mode); + assert_eq!(unit.opacity(), None); + for value in [ + f32::from_bits(1), + f32::MIN_POSITIVE, + 0.375, + 1.0_f32.next_down(), + ] { + let opacity = ScopeOpacity::new(value).unwrap(); + let group = ScopeBlend::new(mode, Some(opacity)); + assert_eq!(group.mode(), mode); + assert_eq!(group.opacity().unwrap().get().to_bits(), value.to_bits()); + assert_ne!(group, unit); + } + } + for value in [ + 0.0, + -0.0, + 1.0, + -0.5, + 1.5, + f32::NAN, + f32::INFINITY, + f32::NEG_INFINITY, + ] { + let error = ScopeOpacity::new(value).expect_err("opacity domain stays unchanged"); + assert_eq!(error.value.to_bits(), value.to_bits()); + } +} + +/// A combined operation must survive as one scope, because a nested opacity +/// scope makes a different backdrop for its enclosed blend. +#[test] +fn combined_blend_and_opacity_is_distinct_from_two_nested_operations() { + let opacity = ScopeOpacity::new(0.375).unwrap(); + let combined = diagram(Some(ScopeBlend::new( + ScopeBlendMode::Multiply, + Some(opacity), + ))); + let blended = diagram(Some(ScopeBlend::new(ScopeBlendMode::Multiply, None))); + let mut nested = blended.clone(); + let mut items = vec![begin(101, ScopeEffect::Opacity(opacity))]; + items.extend(blended.items.iter().cloned()); + items.push(FrameItem::ScopeEnd); + nested.items = checked(items); + + assert_eq!(combined.items.len(), 4); + assert_eq!(nested.items.len(), 6); + assert_eq!(combined.nodes(), nested.nodes()); + assert_ne!(combined, nested); + assert_ne!(combined, blended); +} + +/// Isolation must remain present around a blending descendant, including when +/// the outer span contains only another scope rather than a direct node. +#[test] +fn unit_normal_isolation_retains_a_nested_blending_program() { + let children = diagram(Some(ScopeBlend::new(ScopeBlendMode::Screen, None))); + let mut items = vec![blend(101, ScopeBlendMode::Normal)]; + items.extend(children.items.iter().cloned()); + items.push(FrameItem::ScopeEnd); + let isolated = checked(items); + assert_eq!(isolated.len(), children.items.len() + 2); + assert_eq!(isolated.nodes().collect::>(), children.nodes()); + assert_ne!(isolated, children.items); +} + +/// No new effect gets a separate or weaker path through stream validation. +#[test] +fn blend_scopes_reject_empty_unclosed_and_unopened_boundaries() { + for mode in MODES { + assert_eq!( + FrameItems::try_new(vec![blend(1, mode), FrameItem::ScopeEnd]), + Err(FrameItemsError::EmptyScope { index: 0 }) + ); + assert_eq!( + FrameItems::try_new(vec![ + blend(1, mode), + node(2, PaintStack::solid(CGColor::RED)) + ]), + Err(FrameItemsError::UnclosedScope { index: 0 }) + ); + assert_eq!( + FrameItems::try_new(vec![ + blend(1, mode), + node(2, PaintStack::solid(CGColor::RED)), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]), + Err(FrameItemsError::UnopenedScopeEnd { index: 3 }) + ); + } +} + +/// Both mask phases are independent composites that can contain groups, and a +/// whole masked result can itself belong to an enclosing blend group. +#[test] +fn blending_can_enclose_masks_and_nest_in_either_mask_phase() { + for mode in [MaskMode::Alpha, MaskMode::Luminance] { + checked(vec![ + blend(1, ScopeBlendMode::Multiply), + mask(2, mode), + blend(3, ScopeBlendMode::Screen), + node(4, PaintStack::solid(CGColor::RED)), + FrameItem::ScopeEnd, + FrameItem::MaskSource, + blend(5, ScopeBlendMode::Normal), + node(6, PaintStack::solid(CGColor::WHITE)), + FrameItem::ScopeEnd, + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + ]); + checked(vec![ + mask(1, mode), + blend(2, ScopeBlendMode::Normal), + node(3, PaintStack::solid(CGColor::RED)), + FrameItem::ScopeEnd, + FrameItem::MaskSource, + FrameItem::MaskEnd, + ]); + } +} + +/// Mask phase markers cannot close or bypass a blend boundary in either +/// direction, even though all effects use the same bounded stack. +#[test] +fn blend_boundaries_cannot_be_crossed_by_mask_phase_markers() { + for mode in MODES { + assert_eq!( + FrameItems::try_new(vec![ + mask(1, MaskMode::Alpha), + blend(2, mode), + node(3, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + ]), + Err(FrameItemsError::UnexpectedMaskSource { index: 3 }) + ); + assert_eq!( + FrameItems::try_new(vec![ + mask(1, MaskMode::Alpha), + node(2, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + blend(3, mode), + node(4, PaintStack::solid(CGColor::WHITE)), + FrameItem::MaskEnd, + ]), + Err(FrameItemsError::UnexpectedMaskEnd { index: 5 }) + ); + assert_eq!( + FrameItems::try_new(vec![ + blend(1, mode), + mask(2, MaskMode::Alpha), + node(3, PaintStack::solid(CGColor::RED)), + FrameItem::ScopeEnd, + ]), + Err(FrameItemsError::UnopenedScopeEnd { index: 3 }) + ); + } +} + +/// The bound counts all simultaneously open scopes and masks, with the same +/// inclusive limit for old and new effects. +#[test] +fn blends_and_masks_share_the_existing_depth_bound() { + let begins = || { + (0..MAX_SCOPE_DEPTH) + .map(|id| blend(id as u64, MODES[id % 3])) + .collect::>() + }; + let mut maximum = begins(); + maximum.push(node(100, PaintStack::solid(CGColor::RED))); + maximum.extend(std::iter::repeat_n(FrameItem::ScopeEnd, MAX_SCOPE_DEPTH)); + checked(maximum); + + let mut overflow = begins(); + overflow.push(blend(100, ScopeBlendMode::Normal)); + assert_eq!( + FrameItems::try_new(overflow), + Err(FrameItemsError::ScopeTooDeep { + index: MAX_SCOPE_DEPTH + }) + ); + let mut overflow = begins(); + overflow.push(mask(100, MaskMode::Alpha)); + assert_eq!( + FrameItems::try_new(overflow), + Err(FrameItemsError::MaskTooDeep { + index: MAX_SCOPE_DEPTH + }) + ); + + let mut masked = vec![mask(100, MaskMode::Alpha)]; + masked.extend(begins()); + assert_eq!( + FrameItems::try_new(masked), + Err(FrameItemsError::ScopeTooDeep { + index: MAX_SCOPE_DEPTH + }) + ); + + let mut maximum = begins(); + maximum.pop(); + maximum.extend([ + mask(100, MaskMode::Alpha), + node(101, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + FrameItem::MaskEnd, + ]); + maximum.extend(std::iter::repeat_n( + FrameItem::ScopeEnd, + MAX_SCOPE_DEPTH - 1, + )); + checked(maximum); +} + +fn repeating_masked_group(paints: PaintStack) -> Result { + // Put recursion in a mask-source stroke to prove neither mask markers nor + // group scopes hide nested programs from the existing depth calculation. + let mut source = node(4, PaintStack::empty()); + let FrameItem::Node(source_node) = &mut source else { + unreachable!() + }; + source_node.stroke = rframe::Stroke::new( + paints, + 1.0, + rframe::StrokeCap::Butt, + rframe::StrokeJoin::Round, + 4.0, + ) + .unwrap(); + let items = checked(vec![ + blend(1, ScopeBlendMode::Normal), + mask(2, MaskMode::Alpha), + node(3, PaintStack::solid(CGColor::WHITE)), + FrameItem::MaskSource, + blend(5, ScopeBlendMode::Multiply), + source, + FrameItem::ScopeEnd, + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + ]); + let shared = Arc::new(items); + let pattern = PatternPaint::new( + 16.0, + 16.0, + AffineTransform::identity(), + Arc::clone(&shared), + 1.0, + )?; + assert!( + Arc::ptr_eq(pattern.items(), &shared), + "the checked program stays shared and immutable" + ); + Ok(pattern) +} + +/// Repeating programs admit exactly the same group and mask facts as a frame; +/// wrapping a recursive paint in them cannot reset its depth. +#[test] +fn immutable_repeating_programs_preserve_blends_and_the_recursion_bound() { + let mut pattern = repeating_masked_group(PaintStack::solid(CGColor::RED)).unwrap(); + assert_eq!(pattern.depth(), 1); + for depth in 2..=MAX_PATTERN_DEPTH { + pattern = repeating_masked_group(PaintStack::from_pattern(pattern)).unwrap(); + assert_eq!(pattern.depth(), depth); + } + assert_eq!( + repeating_masked_group(PaintStack::from_pattern(pattern)), + Err(PatternPaintError::TooDeep) + ); +} + +/// Legacy programs remain exact children of a new group. The only admitted +/// empty effect is still a filter that explicitly generates its own source. +#[test] +fn blending_preserves_opacity_clip_filter_and_mask_programs() { + let program = FilterProgram::new(Arc::from([FilterNode::new( + Arc::from([FilterInput::Source]), + rect(), + FilterColorSpace::Srgb, + FilterPrimitive::GaussianBlur { + sigma_x: 1.0, + sigma_y: 2.0, + }, + )])) + .unwrap(); + let filter = Filter::new(AffineTransform::identity(), rect(), program).unwrap(); + let legacy = checked(vec![ + begin(1, ScopeEffect::Opacity(ScopeOpacity::new(0.5).unwrap())), + begin(2, ScopeEffect::Clip(clip())), + begin(3, ScopeEffect::Filter(filter)), + mask(4, MaskMode::Luminance), + node(5, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + node(6, PaintStack::solid(CGColor::WHITE)), + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]); + for mode in MODES { + let enclosed = checked(wrap( + ScopeEffect::Blend(ScopeBlend::new(mode, None)), + legacy.iter().cloned().collect(), + )); + assert_eq!( + enclosed + .iter() + .skip(1) + .take(legacy.len()) + .cloned() + .collect::>(), + legacy.iter().cloned().collect::>() + ); + } + + let generated = FilterProgram::new(Arc::from([FilterNode::new( + Arc::from([]), + rect(), + FilterColorSpace::Srgb, + FilterPrimitive::SolidColor { + color: CGColor::RED.into(), + }, + )])) + .unwrap(); + let generated = Filter::new(AffineTransform::identity(), rect(), generated) + .unwrap() + .with_transparent_source(); + checked(vec![ + blend(1, ScopeBlendMode::Normal), + begin(2, ScopeEffect::Filter(generated)), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]); +} + +/// Admitting group functions cannot silently grant the same functions to any +/// visible leaf kind, including gradients used by fill or stroke stacks. +#[test] +fn group_multiply_and_screen_remain_forbidden_on_every_leaf_kind() { + let stops = || { + vec![ + cg::GradientStop { + offset: 0.0, + color: CGColor::RED.into(), + }, + cg::GradientStop { + offset: 1.0, + color: CGColor::BLUE.into(), + }, + ] + }; + for mode in [BlendMode::Multiply, BlendMode::Screen] { + let mut solid = SolidPaint::new_color(CGColor::RED); + solid.blend_mode = mode; + for paint in [ + Paint::Solid(solid), + Paint::LinearGradient(LinearGradientPaint { + blend_mode: mode, + stops: stops(), + ..Default::default() + }), + Paint::RadialGradient(RadialGradientPaint { + blend_mode: mode, + stops: stops(), + ..Default::default() + }), + ] { + assert_eq!( + PaintStack::try_from_paints(Paints::new([paint])), + Err(PaintStackError { index: 0 }) + ); + } + } +} diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index 66a26563..0b95784e 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -121,6 +121,8 @@ use csscascade::cascade::CascadeDriver; use csscascade::dom::{DemoDom, DemoNodeData, NodeId}; use style::color::{AbsoluteColor, ColorSpace}; +use style::computed_values::isolation::T as StyloIsolation; +use style::computed_values::mix_blend_mode::T as StyloBlend; use style::computed_values::stroke_linecap::T as StyloLinecap; use style::computed_values::stroke_linejoin::T as StyloLinejoin; use style::computed_values::visibility::T as Visibility; @@ -151,8 +153,9 @@ use rframe::{ FilterInput, FilterLightSource, FilterMorphology, FilterNode, FilterPrimitive, FilterProgram, FilterTurbulenceKind, Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, MAX_FILTER_CONVOLVE_KERNEL_VALUES, Mask, MaskMode, PaintAlphaFactor, PaintStack, - PathData, PatternPaint, Provenance, Scope, ScopeEffect, ScopeOpacity, Stroke, StrokeCap, - StrokeDash, StrokeDashIntervals, StrokeDashIntervalsError, StrokeJoin, StrokeSpace, VisualRef, + PathData, PatternPaint, Provenance, Scope, ScopeBlend, ScopeBlendMode, ScopeEffect, + ScopeOpacity, Stroke, StrokeCap, StrokeDash, StrokeDashIntervals, StrokeDashIntervalsError, + StrokeJoin, StrokeSpace, VisualRef, }; use std::sync::Arc; @@ -1055,6 +1058,13 @@ fn host_ancestor_opacities(svg: HtmlElement<'_>) -> Result, CompileErro let data = element .borrow_data() .ok_or(CompileError::MissingComputedStyle)?; + if data.styles.primary().clone_mix_blend_mode() != StyloBlend::Normal + || data.styles.primary().clone_isolation() != StyloIsolation::Auto + { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode/isolation on an HTML ancestor needs the host backdrop and layer graph".to_string() + )); + } let opacity = data.styles.primary().clone_opacity().clamp(0.0, 1.0); if opacity < 1.0 { inner_to_outer.push(opacity); @@ -1346,8 +1356,8 @@ const CASCADE_PROPERTIES_NOT_REPRESENTED: &[&str] = &[ "mask-border-width", "mask-border-outset", "mask-border-repeat", - "mix-blend-mode", - "isolation", + // mix-blend-mode and isolation are read from the one computed cascade. + // Their unadmitted modes and composition contexts have value patrols. "paint-order", // The gradient rung's sheet-level patrol: Chromium consumes these from // the cascade (measured: `stop { stop-color: red }` beats the @@ -2009,6 +2019,8 @@ fn patrol_style_attribute( /// listed property (measured). Whatever is in force is what must be patrolled. fn stylesheet_findings(root: HtmlElement<'_>) -> Vec<(String, String)> { let mut found = Vec::new(); + let mut blend_declaration = false; + let mut keyframe_sheets = Vec::new(); // A sheet can declare a stroke-width in `em`/`rem` — an admitted unit whose // font-size basis may be poisoned anywhere: in the same sheet, another // sheet, or an element's own attributes. The walk visits all of them, so @@ -2026,6 +2038,11 @@ fn stylesheet_findings(root: HtmlElement<'_>) -> Vec<(String, String)> { sheet.push_str(text); } } + blend_declaration |= css_declares_property(&sheet, "mix-blend-mode") + || css_declares_property(&sheet, "isolation"); + if sheet.to_ascii_lowercase().contains("keyframes") { + keyframe_sheets.push(path.clone()); + } if sheet.contains('\\') { // An escape can hide a property name or a unit from every scan // below (`1\76 w` is `1vw` to the tokenizer) — measured painting @@ -2121,6 +2138,10 @@ fn stylesheet_findings(root: HtmlElement<'_>) -> Vec<(String, String)> { font_poison = stylesheet_font_size_poison(&sheet); } } + if let Some(style) = get_attr(element, "style") { + blend_declaration |= css_declares_property(&style, "mix-blend-mode") + || css_declares_property(&style, "isolation"); + } if font_poison.is_none() { for text in [ get_attr(element, "font-size"), @@ -2149,6 +2170,14 @@ fn stylesheet_findings(root: HtmlElement<'_>) -> Vec<(String, String)> { // Depth-first in document order: the stack pops in reverse. stack.extend(children.into_iter().rev()); } + // Static cascade does not apply CSS animation values. Do not match a + // second selector tree: keyframes and blend declarations anywhere can + // meet through custom properties, including an HTML-head stylesheet. + if blend_declaration { + for path in keyframe_sheets { + found.push(("CSS keyframes with mix-blend-mode/isolation need the animated group-composition profile".to_string(), path)); + } + } if let (Some((unit, path)), Some(poison)) = (sheet_width_font_relative, font_poison) { found.push(( format!( @@ -2259,6 +2288,56 @@ struct ComputedPatrol { opacity: f32, } +/// The computed group fact, not a second declaration parser. CSS attribute +/// lookalikes are deliberately absent from the presentation-hint inventory. +fn computed_blend_scope(element: HtmlElement<'_>) -> Result, CompileError> { + let data = element + .borrow_data() + .ok_or(CompileError::MissingComputedStyle)?; + let style = data.styles.primary(); + let mode = match style.clone_mix_blend_mode() { + StyloBlend::Normal => ScopeBlendMode::Normal, + StyloBlend::Multiply => ScopeBlendMode::Multiply, + StyloBlend::Screen => ScopeBlendMode::Screen, + other => { + return Err(CompileError::UnsupportedStyle(format!( + "mix-blend-mode {other:?} is outside the admitted normal/multiply/screen group profile" + ))); + } + }; + if mode == ScopeBlendMode::Normal && style.clone_isolation() == StyloIsolation::Auto { + return Ok(None); + } + let tag = element.local_name_string(); + if !matches!( + tag.as_str(), + "svg" + | "g" + | "a" + | "use" + | "rect" + | "circle" + | "ellipse" + | "line" + | "path" + | "polyline" + | "polygon" + ) { + return Err(CompileError::UnsupportedStyle(format!( + "mix-blend-mode/isolation on <{tag}> needs its own source composition profile" + ))); + } + let opacity = style.clone_opacity().clamp(0.0, 1.0); + Ok(Some(ScopeBlend::new( + mode, + if opacity > 0.0 && opacity < 1.0 { + Some(ScopeOpacity::new(opacity).expect("checked computed opacity")) + } else { + None + }, + ))) +} + #[derive(Debug, Clone, Copy)] enum MeasuredGeometry { /// An admitted subtree whose geometry is known to be empty. @@ -2870,6 +2949,9 @@ fn patrol_computed_style( opacity, }); } + // Also patrol resource-root and text consumers which do not enter the + // ordinary child walk. They must not silently discard a group fact. + computed_blend_scope(element)?; // SVG2 makes width/height geometry properties where they apply: a // cascaded (stylesheet or style-attribute) value beats both the // authored attribute and the auto default in Chromium, while this @@ -3144,6 +3226,23 @@ fn compile_svg_element( // both entries: the root paints nothing itself, and each descendant's // own computed (inherited) visibility decides its node. let root_patrol = patrol_computed_style(svg, true)?; + let root_composite = computed_blend_scope(svg)?; + if root_patrol.opacity > 0.0 + && root_patrol.opacity < 1.0 + && root_composite.is_some_and(|scope| scope.mode() != ScopeBlendMode::Normal) + { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode with partial opacity on the root crosses the root-layer precision boundary".to_string() + )); + } + if initial_viewport.is_none() + && root_composite.is_some_and(|scope| scope.mode() != ScopeBlendMode::Normal) + { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode on an inline root needs the host backdrop and layer graph" + .to_string(), + )); + } let root_disposition = root_patrol.disposition; reject_host_or_root_clip_path(svg)?; // The root's opacity composites the complete SVG-local raster, @@ -3289,29 +3388,75 @@ fn compile_svg_element( context_paint_transform: viewport, fonts, items: Vec::new(), + elided_blends: Vec::new(), top_level_shapes: Vec::new(), active_masks: Vec::new(), active_patterns: Vec::new(), active_markers: Vec::new(), next_id: 0, }; + let mut root_facts = SpanFacts::default(); if root_disposition != RenderDisposition::PrunedSubtree || initial_viewport.is_some() { let depth = host_opacities.len() + usize::from(root_patrol.opacity < 1.0); if depth > MAX_CONTAINER_DEPTH { return Err(CompileError::ContainerTooDeep(MAX_CONTAINER_DEPTH)); } - walk.compile_children(svg, viewport, bases, "svg", depth, 1.0)?; + root_facts = walk.compile_children(svg, viewport, bases, "svg", depth, 1.0)?; + } + let adds_root_blend_boundary = + root_composite.is_some() || (root_facts.escaping_blend && root_patrol.opacity == 1.0); + if adds_root_blend_boundary && root_facts.has_image_effect { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile".to_string() + )); + } + if initial_viewport.is_none() + && root_facts.escaping_blend + && root_composite.is_none() + && root_patrol.opacity == 1.0 + { + return Err(CompileError::UnsupportedStyle( + "unisolated mix-blend-mode in inline SVG needs the host backdrop and layer graph" + .to_string(), + )); + } + if adds_root_blend_boundary && let Some(reason) = root_facts.blend_precision_boundary { + return Err(blend_precision_refusal(reason)); } + walk.compact_elided_blends(); let ChildWalk { mut items, top_level_shapes, mut next_id, .. } = walk; - if root_patrol.opacity < 1.0 && !items.is_empty() { + if let Some(composite) = root_composite.filter(|scope| { + scope.mode() != ScopeBlendMode::Normal + || (scope.opacity().is_none() && root_facts.escaping_blend) + }) && !items.is_empty() + { + items.insert(0, blend_scope_item(&mut next_id, composite)); + items.push(FrameItem::ScopeEnd); + } else if root_patrol.opacity < 1.0 && !items.is_empty() { items.insert(0, scope_item(&mut next_id, root_patrol.opacity)); items.push(FrameItem::ScopeEnd); } + // A standalone SVG's initial backdrop is transparent, not the arbitrary + // destination the eventual Frame consumer supplies. Resolve that source + // boundary here; never turn every Frame into an implicitly isolated tree. + if initial_viewport.is_some() + && !items.is_empty() + && (root_composite.is_some_and(|scope| scope.mode() != ScopeBlendMode::Normal) + || (root_facts.escaping_blend + && root_composite.is_none() + && root_patrol.opacity == 1.0)) + { + items.insert( + 0, + blend_scope_item(&mut next_id, ScopeBlend::new(ScopeBlendMode::Normal, None)), + ); + items.push(FrameItem::ScopeEnd); + } for opacity in host_opacities.iter().rev() { if !items.is_empty() { items.insert(0, scope_item(&mut next_id, *opacity)); @@ -3377,6 +3522,16 @@ struct SpanFacts { /// transformed container, or a transformed draw, forces the layer; the /// scope element's own transform does not). transformed: bool, + /// Explicit blend/isolation participation, even if its boundary is elided. + has_blend: bool, + /// A descendant still reads this span's enclosing backdrop. A real + /// isolated group consumes this fact; geometric viewport clipping does not. + escaping_blend: bool, + /// B1 keeps image-effect composition outside its admitted group profile. + has_image_effect: bool, + /// Classify source material once; adding an isolation/blend later must not + /// silently switch its raster origin or its coverage/alpha materialization. + blend_precision_boundary: Option<&'static str>, } impl SpanFacts { @@ -3387,7 +3542,63 @@ impl SpanFacts { self.has_opacity |= other.has_opacity; self.has_geometry |= other.has_geometry; self.transformed |= other.transformed; + self.has_blend |= other.has_blend; + self.escaping_blend |= other.escaping_blend; + self.has_image_effect |= other.has_image_effect; + self.blend_precision_boundary = self + .blend_precision_boundary + .or(other.blend_precision_boundary); + } +} + +fn blend_precision_refusal(reason: &str) -> CompileError { + CompileError::UnsupportedStyle(format!( + "mix-blend-mode/isolation {reason} crosses the group-source precision boundary" + )) +} + +fn blend_node_precision_boundary(node: &FrameNode) -> Option<&'static str> { + if node.paints.is_empty() && node.stroke.is_none() { + return None; + } + if !matches!(node.geometry, Geometry::Rect(_)) { + return Some("with non-rectangular source geometry"); + } + if node + .paints + .iter() + .any(|paint| matches!(paint, cg::Paint::RadialGradient(_))) + { + return Some("with a radial source paint"); + } + if let Some(stroke) = &node.stroke + && (stroke.cap() != StrokeCap::Butt + || stroke.join() != StrokeJoin::Miter + || stroke.dash().is_some() + || stroke.dash_intervals().is_some() + || stroke.space() != StrokeSpace::Local + || stroke + .paints() + .iter() + .any(|paint| matches!(paint, cg::Paint::RadialGradient(_)))) + { + return Some("with a complex source stroke"); } + None +} + +fn blend_clip_precision_boundary(clip: &ClipPath) -> bool { + clip.layers().iter().any(|layer| { + layer.geometries().iter().any(|geometry| { + let [[a, c, _], [b, d, _]] = geometry.transform().matrix; + let bounds = geometry.bounds(); + !matches!(geometry.geometry(), Geometry::Rect(_)) + || !((b == 0.0 && c == 0.0) || (a == 0.0 && d == 0.0)) + || [bounds.x, bounds.y, bounds.width, bounds.height] + .into_iter() + .any(|value| value.fract() != 0.0) + }) + }) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -3458,9 +3669,15 @@ impl<'a> PatternCompiler<'a> { let (chain, external_tail) = self.template_chain(first)?; for element in &chain { + if let Some(reason) = self.override_skips.get(&element.node_id()) { + return Err(format!( + "pattern #{fragment} authored state is overridden at document load: {reason}" + )); + } patrol_rendering_attributes(*element, "pattern", &[]) .map_err(|error| error.to_string())?; patrol_style_attribute(*element, "pattern").map_err(|error| error.to_string())?; + computed_blend_scope(*element).map_err(|error| error.to_string())?; } let content_owner = chain @@ -3752,6 +3969,7 @@ impl<'a> PatternCompiler<'a> { context_paint_transform: content_to_tile, fonts: self.fonts, items: Vec::new(), + elided_blends: Vec::new(), top_level_shapes: Vec::new(), active_masks: Vec::new(), active_patterns: source_active_patterns, @@ -3769,6 +3987,7 @@ impl<'a> PatternCompiler<'a> { .map_err(|error| { format!("pattern #{fragment} source cannot compile completely: {error}") })?; + walk.compact_elided_blends(); let items = std::mem::take(&mut walk.items); drop(walk); if let Some(degradation) = degradations.first() { @@ -4766,7 +4985,7 @@ mod marker_resource { /// Geometric `clip-path` wraps a completed span in resolved path coverage; /// same-document image masks wrap it in a checked target/source composite, /// and resolved filter programs wrap it in an isolated image operation. -/// `mix-blend-mode` and `isolation` remain patrol refusals. +/// Computed blending adds only the source boundaries its descendants need. struct ChildWalk<'a> { values: &'a EffectiveValues, mode: CompileMode, @@ -4821,6 +5040,11 @@ struct ChildWalk<'a> { /// name instead of reaching for an ambient face. fonts: &'a textlayout::Environment, items: Vec, + /// Lazily allocated, identity-indexed tombstones for redundant normal + /// isolation. Keeping balanced placeholders during the walk avoids moving + /// an entire descendant span for each enclosing isolation. Program exit + /// compacts once; identity rewind also truncates this ledger. + elided_blends: Vec, /// The materialized nodes that are direct children of the root `` /// — the animation inventory's candidate targets, which it narrows /// further to ``. @@ -4851,6 +5075,45 @@ struct PaintContext<'d> { } impl<'a> ChildWalk<'a> { + fn reset_next_id(&mut self, next_id: u64) { + self.next_id = next_id; + self.elided_blends.truncate(next_id as usize); + } + + fn elide_blend(&mut self, identity: u64) { + if self.elided_blends.len() < identity as usize { + self.elided_blends.resize(identity as usize, false); + } + self.elided_blends[identity as usize - 1] = true; + } + + fn compact_elided_blends(&mut self) { + if self.elided_blends.is_empty() { + return; + } + let mut scopes = Vec::new(); + self.items.retain(|item| match item { + FrameItem::ScopeBegin(scope) => { + let elided = self + .elided_blends + .get(scope.owner.identity().get() as usize - 1) + .copied() + .unwrap_or(false); + debug_assert!( + !elided + || matches!(scope.effect, ScopeEffect::Blend(blend) + if blend.mode() == ScopeBlendMode::Normal && blend.opacity().is_none()) + ); + scopes.push(elided); + !elided + } + FrameItem::ScopeEnd => !scopes.pop().expect("balanced staged scope"), + _ => true, + }); + debug_assert!(scopes.is_empty()); + self.elided_blends.clear(); + } + fn resolve_clip( &self, element: HtmlElement<'a>, @@ -4929,6 +5192,8 @@ impl<'a> ChildWalk<'a> { draws: 0, opacity_passes: 0, has_scope: true, + has_image_effect: true, + escaping_blend: false, ..facts }; facts @@ -5009,7 +5274,7 @@ impl<'a> ChildWalk<'a> { self.degradations.truncate(degradation_checkpoint); if let Err(error) = source_result { self.items.truncate(checkpoint); - self.next_id = source_next_id; + self.reset_next_id(source_next_id); return Err(error); } @@ -5018,6 +5283,8 @@ impl<'a> ChildWalk<'a> { draws: 0, opacity_passes: 0, has_scope: true, + has_image_effect: true, + escaping_blend: false, ..facts }; Ok(facts) @@ -5054,6 +5321,7 @@ impl<'a> ChildWalk<'a> { opacity_passes: 0, has_scope: materialized, has_opacity: true, + escaping_blend: false, ..facts }; } @@ -5064,16 +5332,47 @@ impl<'a> ChildWalk<'a> { /// precedes the target's own opacity work, making clip outer and opacity /// inner — the exact same-element order Chromium's byte probe selects. fn wrap_span_with_clip( + &mut self, + checkpoint: usize, + facts: SpanFacts, + clip: Option, + ) -> SpanFacts { + self.wrap_span_with_clip_boundary(checkpoint, facts, clip, false) + } + + fn wrap_span_with_clip_boundary( &mut self, checkpoint: usize, mut facts: SpanFacts, clip: Option, + isolate_blending: bool, ) -> SpanFacts { if let Some(clip) = clip && (facts.draws > 0 || facts.has_scope) { - self.items - .insert(checkpoint, clip_scope_item(&mut self.next_id, clip)); + if blend_clip_precision_boundary(&clip) { + facts.blend_precision_boundary = facts + .blend_precision_boundary + .or(Some("with curved, subpixel, or rotated clip coverage")); + } + if isolate_blending && facts.escaping_blend { + let blend = blend_scope_item( + &mut self.next_id, + ScopeBlend::new(ScopeBlendMode::Normal, None), + ); + let clip = clip_scope_item(&mut self.next_id, clip); + // One exact-size gap shifts the existing clip's suffix once, + // not a second time for the added isolation boundary. The + // legacy clip walk is still depth-dependent; this is not a + // claim that the whole compiler has become linear. + self.items.splice(checkpoint..checkpoint, [clip, blend]); + self.items.push(FrameItem::ScopeEnd); + facts.has_blend = true; + facts.escaping_blend = false; + } else { + self.items + .insert(checkpoint, clip_scope_item(&mut self.next_id, clip)); + } self.items.push(FrameItem::ScopeEnd); facts = SpanFacts { draws: 0, @@ -5085,6 +5384,19 @@ impl<'a> ChildWalk<'a> { facts } + /// Authored SVG clip-path groups blend descendants, unlike the plain + /// viewport clip above. Carry the existing escaping-backdrop fact upward + /// rather than rescanning descendants or isolating every neutral group. + fn wrap_span_with_svg_clip( + &mut self, + checkpoint: usize, + facts: SpanFacts, + clip: Option, + already_isolated: bool, + ) -> SpanFacts { + self.wrap_span_with_clip_boundary(checkpoint, facts, clip, !already_isolated) + } + /// Compile a parent's children in painter order, accumulating the span /// facts the parent's own opacity decision reads. `replay_opacity` is an /// enclosing container's one-pass factor mid-replay (see @@ -5197,23 +5509,8 @@ impl<'a> ChildWalk<'a> { // its `href` is interaction, not paint), so the two share the // one container compiler and its patrols. `` is a // container whose children are its expanded shadow content. - let result = if tag == "g" || tag == "a" { - self.compile_container(c, transform, bases, &path, depth, &tag, replay_opacity) - } else if tag == "svg" { - self.compile_nested_viewport(c, transform, bases, &path, depth, replay_opacity) - } else if tag == "use" { - self.compile_use(c, transform, bases, &path, depth, replay_opacity) - } else { - self.compile_leaf( - c, - transform, - bases, - &path, - depth, - depth == 0, - replay_opacity, - ) - }; + let result = + self.compile_child(c, transform, bases, &path, depth, &tag, replay_opacity); match result { Ok(child_facts) => facts.absorb(child_facts), Err(error) => match self.mode { @@ -5230,6 +5527,134 @@ impl<'a> ChildWalk<'a> { Ok(facts) } + /// Append explicit blend boundaries before descending. Default containers + /// add no command or subtree rescan. Non-normal blending combines own + /// opacity in its final restore. Normal isolation instead preserves the + /// established opacity fold/layer route and elides redundant unit scopes. + #[allow(clippy::too_many_arguments)] + fn compile_child( + &mut self, + el: HtmlElement<'a>, + transform: AffineTransform, + bases: PercentBases, + path: &str, + depth: usize, + tag: &str, + replay_opacity: f32, + ) -> Result { + let composite = computed_blend_scope(el)?; + if composite.is_some() + && (!self.active_masks.is_empty() + || !self.active_patterns.is_empty() + || !self.active_markers.is_empty()) + { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode/isolation in a mask, pattern, or marker source needs its own source composition profile".to_string() + )); + } + if composite.is_some() && patrol_computed_style(el, false)?.opacity == 0.0 { + return Ok(SpanFacts { + has_opacity: true, + ..SpanFacts::default() + }); + } + let checkpoint = ( + self.items.len(), + self.next_id, + self.top_level_shapes.len(), + self.degradations.len(), + ); + // Own partial opacity already provides isolation through the existing + // measured fold/layer route. Normal isolation must not replace that + // route with a blend restore (different alpha rounding). + let emitted_composite = composite + .filter(|scope| scope.mode() != ScopeBlendMode::Normal || scope.opacity().is_none()); + if let Some(composite) = emitted_composite { + self.items + .push(blend_scope_item(&mut self.next_id, composite)); + } + let content_start = self.items.len(); + let blend_id = self.next_id; + let defer = emitted_composite.is_some(); + let result = if tag == "g" || tag == "a" { + self.compile_container( + el, + transform, + bases, + path, + depth, + tag, + replay_opacity, + defer, + ) + } else if tag == "svg" { + self.compile_nested_viewport(el, transform, bases, path, depth, replay_opacity, defer) + } else if tag == "use" { + self.compile_use(el, transform, bases, path, depth, replay_opacity, defer) + } else { + self.compile_leaf( + el, + transform, + bases, + path, + depth, + depth == 0, + replay_opacity, + defer, + ) + }; + let result = result.and_then(|mut facts| { + // Authored participation remains a patrol fact even when normal + // isolation needs no materialized boundary. Otherwise an enclosing + // image effect could bypass its conservative composition profile. + // This does not set has_scope or block the one-pass opacity fold. + facts.has_blend |= composite.is_some() && self.items.len() != content_start; + if facts.has_blend && facts.has_image_effect { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile".to_string() + )); + } + if (composite.is_some() || facts.has_blend) && let Some(reason) = facts.blend_precision_boundary { + return Err(blend_precision_refusal(reason)); + } + if let Some(composite) = composite { + if facts.has_image_effect { + return Err(CompileError::UnsupportedStyle( + "mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile".to_string() + )); + } + if emitted_composite.is_none() { + // The established opacity route already consumed it. + } else if self.items.len() == content_start { + self.items.truncate(checkpoint.0); + self.reset_next_id(checkpoint.1); + } else { + self.items.push(FrameItem::ScopeEnd); + if composite.mode() == ScopeBlendMode::Normal && !facts.escaping_blend { + self.elide_blend(blend_id); + return Ok(facts); + } + facts.draws = 0; + facts.opacity_passes = 0; + facts.has_scope = true; + facts.has_blend = true; + facts.has_opacity |= composite.opacity().is_some(); + facts.escaping_blend = composite.mode() != ScopeBlendMode::Normal; + } + } + Ok(facts) + }); + if result.is_err() { + // A named skipped element cannot leave a partially built group or + // any of its earlier source draws in the accepted sibling stream. + self.items.truncate(checkpoint.0); + self.reset_next_id(checkpoint.1); + self.top_level_shapes.truncate(checkpoint.2); + self.degradations.truncate(checkpoint.3); + } + result + } + /// A container element: patrolled like any admitted element, then /// descended with its own transform composed onto the inherited one. /// @@ -5250,13 +5675,17 @@ impl<'a> ChildWalk<'a> { depth: usize, element: &str, replay_opacity: f32, + defer_own_opacity: bool, ) -> Result { if depth >= MAX_CONTAINER_DEPTH { return Err(CompileError::ContainerTooDeep(MAX_CONTAINER_DEPTH)); } patrol_rendering_attributes(el, element, &[])?; patrol_style_attribute(el, element)?; - let patrol = patrol_computed_style(el, false)?; + let mut patrol = patrol_computed_style(el, false)?; + if defer_own_opacity { + patrol.opacity = 1.0; + } match patrol.disposition { // `display: none` generates no box: the subtree is pruned — // Chromium's correct nothing, not a hole to declare. A *hidden* @@ -5311,7 +5740,7 @@ impl<'a> ChildWalk<'a> { // Chromium's same-element effect order is byte-discriminating here: // filter is inside mask, mask is inside opacity, and all three are // inside the geometric clip. - let facts = self.wrap_span_with_clip(checkpoint, facts?, clip); + let facts = self.wrap_span_with_svg_clip(checkpoint, facts?, clip, defer_own_opacity); Ok(SpanFacts { transformed: facts.transformed || own_transformed, ..facts @@ -5336,6 +5765,7 @@ impl<'a> ChildWalk<'a> { path: &str, depth: usize, replay_opacity: f32, + defer_own_opacity: bool, ) -> Result { if depth >= MAX_CONTAINER_DEPTH { return Err(CompileError::ContainerTooDeep(MAX_CONTAINER_DEPTH)); @@ -5346,7 +5776,10 @@ impl<'a> ChildWalk<'a> { // current Blink intentionally excludes them from the viewport's used // geometry. Keep that source-provenance split named until the shared // sizing row owns it; direct attributes are resolved below. - let patrol = patrol_computed_style(el, true)?; + let mut patrol = patrol_computed_style(el, true)?; + if defer_own_opacity { + patrol.opacity = 1.0; + } if patrol.disposition == RenderDisposition::PrunedSubtree { return Ok(SpanFacts::default()); } @@ -5419,7 +5852,8 @@ impl<'a> ChildWalk<'a> { }; self.context_paint_transform = previous_context_paint_transform; - let facts = self.wrap_span_with_clip(checkpoint, facts?, authored_clip); + let facts = + self.wrap_span_with_svg_clip(checkpoint, facts?, authored_clip, defer_own_opacity); let viewport_mapping_is_identity = viewport.x == 0.0 && viewport.y == 0.0 && viewport.content_mapping == AffineTransform::identity(); @@ -5461,7 +5895,7 @@ impl<'a> ChildWalk<'a> { // geometry in Chromium, and therefore blocks an enclosing // fold, but its completed visual contribution is nothing. self.items.truncate(checkpoint.0); - self.next_id = checkpoint.1; + self.reset_next_id(checkpoint.1); self.degradations.truncate(checkpoint.2); facts = SpanFacts { has_opacity: true, @@ -5477,7 +5911,7 @@ impl<'a> ChildWalk<'a> { // Replay the span with the accumulated factor so the sole // draw can choose its solid-fold or post-paint-alpha route. self.items.truncate(checkpoint.0); - self.next_id = checkpoint.1; + self.reset_next_id(checkpoint.1); self.degradations.truncate(checkpoint.2); facts = self.compile_children( el, @@ -5500,6 +5934,7 @@ impl<'a> ChildWalk<'a> { opacity_passes: 0, has_scope: materialized, has_opacity: true, + escaping_blend: false, ..facts }; } @@ -5536,6 +5971,7 @@ impl<'a> ChildWalk<'a> { path: &str, depth: usize, replay_opacity: f32, + defer_own_opacity: bool, ) -> Result { if depth >= MAX_CONTAINER_DEPTH { return Err(CompileError::ContainerTooDeep(MAX_CONTAINER_DEPTH)); @@ -5580,7 +6016,10 @@ impl<'a> ChildWalk<'a> { "its referenced root needs the instance-sized viewport contract".to_string(), )); } - let patrol = patrol_computed_style(el, false)?; + let mut patrol = patrol_computed_style(el, false)?; + if defer_own_opacity { + patrol.opacity = 1.0; + } match patrol.disposition { RenderDisposition::PrunedSubtree => return Ok(SpanFacts::default()), RenderDisposition::Renders | RenderDisposition::HiddenPaint => {} @@ -5641,7 +6080,7 @@ impl<'a> ChildWalk<'a> { }; self.context_paint_transform = previous_context_paint_transform; self.paint_contexts.pop(); - let facts = self.wrap_span_with_clip(checkpoint, facts?, clip); + let facts = self.wrap_span_with_svg_clip(checkpoint, facts?, clip, defer_own_opacity); // The `x`/`y` translate is part of the use's own transform (SVG2 // §5.6.2), so like the transform property it stays *on* this // element — an enclosing one-pass route is broken only by a transform @@ -5680,7 +6119,7 @@ impl<'a> ChildWalk<'a> { ); if result.is_err() { self.items.truncate(checkpoint); - self.next_id = next_id; + self.reset_next_id(next_id); } result } @@ -5898,6 +6337,7 @@ impl<'a> ChildWalk<'a> { context_paint_transform: content_to_frame, fonts: self.fonts, items: Vec::new(), + elided_blends: Vec::new(), top_level_shapes: Vec::new(), active_masks: self.active_masks.clone(), active_patterns: self.active_patterns.clone(), @@ -5917,6 +6357,7 @@ impl<'a> ChildWalk<'a> { "marker #{fragment} source cannot compile completely: {error}" )) })?; + walk.compact_elided_blends(); let items = std::mem::take(&mut walk.items); let next_id = walk.next_id; drop(walk); @@ -5932,7 +6373,7 @@ impl<'a> ChildWalk<'a> { "marker #{fragment} source item stream is invalid: {error}" )) })?; - self.next_id = next_id; + self.reset_next_id(next_id); Ok(items) } @@ -5945,6 +6386,7 @@ impl<'a> ChildWalk<'a> { depth: usize, top_level: bool, replay_opacity: f32, + defer_own_opacity: bool, ) -> Result { // An admitted shape may resolve to no visual fact at all — a `` // whose `d` draws nothing. That is not a hole: the element is @@ -5989,11 +6431,12 @@ impl<'a> ChildWalk<'a> { } else { None }; - let deferred_opacity = if mask.is_some() || filter.is_some() || marker_selected { - patrol_computed_style(el, tag == "rect")?.opacity - } else { - 1.0 - }; + let deferred_opacity = + if !defer_own_opacity && (mask.is_some() || filter.is_some() || marker_selected) { + patrol_computed_style(el, tag == "rect")?.opacity + } else { + 1.0 + }; let compilation = compile_shape( el, transform, @@ -6005,7 +6448,7 @@ impl<'a> ChildWalk<'a> { &self.active_patterns, &self.paint_contexts, bases, - mask.is_some() || filter.is_some() || marker_selected, + defer_own_opacity || mask.is_some() || filter.is_some() || marker_selected, replay_opacity, self.fonts, marker_projection, @@ -6049,6 +6492,8 @@ impl<'a> ChildWalk<'a> { facts.has_opacity = outcome.has_opacity; facts.has_geometry = outcome.has_geometry; facts.transformed = outcome.transformed; + facts.blend_precision_boundary = + outcome.nodes.iter().find_map(blend_node_precision_boundary); } let marker_facts = match self.compile_marker_instances( el, @@ -6067,7 +6512,7 @@ impl<'a> ChildWalk<'a> { // best effort the parent will declare and skip this element; // no already-emitted fill or stroke may survive that skip. self.items.truncate(checkpoint); - self.next_id = next_id_checkpoint; + self.reset_next_id(next_id_checkpoint); return Err(error); } }; @@ -6091,7 +6536,7 @@ impl<'a> ChildWalk<'a> { Ok(order) => order, Err(error) => { self.items.truncate(checkpoint); - self.next_id = next_id_checkpoint; + self.reset_next_id(next_id_checkpoint); return Err(error); } } @@ -6178,6 +6623,14 @@ fn scope_item(next_id: &mut u64, opacity: f32) -> FrameItem { }) } +fn blend_scope_item(next_id: &mut u64, composite: ScopeBlend) -> FrameItem { + *next_id += 1; + FrameItem::ScopeBegin(Scope { + owner: VisualRef::new(Identity::new(*next_id), Provenance::new(*next_id)), + effect: ScopeEffect::Blend(composite), + }) +} + fn clip_scope_item(next_id: &mut u64, clip: ClipPath) -> FrameItem { let scope_id = *next_id + 1; *next_id += 1; @@ -6694,6 +7147,9 @@ mod clip_path { if patrol.disposition != RenderDisposition::Renders { return Ok(Contribution::None); } + if computed_blend_scope(element)?.is_some() { + return Err(CompileError::UnsupportedClipPath("mix-blend-mode/isolation on a geometric clip contributor needs its own source profile".to_string())); + } if element_has_computed_clip_path(element)? { return Ok(Contribution::Mask(format!( "a <{tag}> contributor with its own clip-path uses Chromium's raster-mask strategy" @@ -6726,6 +7182,9 @@ mod clip_path { if patrol.disposition != RenderDisposition::Renders { return Ok(Contribution::None); } + if computed_blend_scope(element)?.is_some() { + return Err(CompileError::UnsupportedClipPath("mix-blend-mode/isolation on a geometric clip contributor needs its own source profile".to_string())); + } if element_has_computed_clip_path(element)? { return Ok(Contribution::Mask( "a contributor with its own clip-path uses Chromium's raster-mask strategy" @@ -10090,6 +10549,7 @@ mod mask_resource { /// its dedicated decoder below. Everything else the pinned cascade drops /// must refuse before source paint can escape. fn patrol_resource_style(element: HtmlElement<'_>) -> Result<(), CompileError> { + computed_blend_scope(element)?; if let Some(style) = get_attr(element, "style") && let Some(property) = unrepresented_property_except(&style, &["filter", "mask", "mask-type"]) diff --git a/crates/websem/tests/capability_status.rs b/crates/websem/tests/capability_status.rs index 9ecdc91a..eb1d2182 100644 --- a/crates/websem/tests/capability_status.rs +++ b/crates/websem/tests/capability_status.rs @@ -16,13 +16,15 @@ #[path = "support/fixture_fonts.rs"] mod fixture_fonts; +#[path = "support/unsupported_fixture.rs"] +mod unsupported_fixture; use std::fmt::Write as _; use std::fs; use std::path::{Path, PathBuf}; use serde::Deserialize; -use websem::{DegradationAction, InitialViewport, SvgFrameSource}; +use websem::DegradationAction; #[derive(Debug, Deserialize)] struct PrimitiveSuite { @@ -54,10 +56,6 @@ fn refusal_text_is_literal_inside_the_generated_table() { assert_eq!(cell("a | b\nc * d"), r"a \| b c \* d"); } -fn viewport() -> InitialViewport { - InitialViewport::new(64.0, 64.0) -} - #[test] fn the_committed_status_view_is_fresh() { let generated = generate(); @@ -85,8 +83,14 @@ fn generate() -> String { .expect("read the unsupported corpus") .filter_map(Result::ok) .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .filter(|name| name.ends_with(".svg")) - .map(|name| name.trim_end_matches(".svg").to_string()) + .filter(|name| name.ends_with(".svg") || name.ends_with(".html")) + .map(|name| { + Path::new(&name) + .file_stem() + .unwrap() + .to_string_lossy() + .into_owned() + }) .collect(); refusals.sort(); @@ -147,20 +151,10 @@ fn generate() -> String { | --- | --- | --- |\n", ); for id in &refusals { - let source = fs::read_to_string(root.join("unsupported").join(format!("{id}.svg"))) - .unwrap_or_else(|error| panic!("{id}: read: {error}")); - let strict = SvgFrameSource::from_standalone_svg_with_fonts( - source.as_str(), - viewport(), - fixture_fonts::unsupported_environment(id), - ) - .err() - .unwrap_or_else(|| panic!("{id}: an unsupported fixture must refuse under strict")); - match SvgFrameSource::from_standalone_svg_best_effort_with_fonts( - source.as_str(), - viewport(), - fixture_fonts::unsupported_environment(id), - ) { + let strict = unsupported_fixture::compile(&root.join("unsupported"), id, false) + .err() + .unwrap_or_else(|| panic!("{id}: an unsupported fixture must refuse under strict")); + match unsupported_fixture::compile(&root.join("unsupported"), id, true) { Err(_) => { writeln!( out, diff --git a/crates/websem/tests/clip_path_contract.rs b/crates/websem/tests/clip_path_contract.rs index 4a1799f3..eb5117e7 100644 --- a/crates/websem/tests/clip_path_contract.rs +++ b/crates/websem/tests/clip_path_contract.rs @@ -46,7 +46,7 @@ fn clips(frame: &Frame) -> Vec<&rframe::ClipPath> { .filter_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Clip(clip) => Some(clip), - ScopeEffect::Opacity(_) | ScopeEffect::Filter(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Filter(_) => None, }, FrameItem::Node(_) | FrameItem::ScopeEnd diff --git a/crates/websem/tests/filter_contract.rs b/crates/websem/tests/filter_contract.rs index 0e04047a..573310fb 100644 --- a/crates/websem/tests/filter_contract.rs +++ b/crates/websem/tests/filter_contract.rs @@ -93,7 +93,7 @@ fn resolved_filter(frame: &Frame) -> &Filter { .find_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Filter(filter) => Some(filter), - ScopeEffect::Opacity(_) | ScopeEffect::Clip(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Clip(_) => None, }, _ => None, }) @@ -117,6 +117,7 @@ fn gaussian_blur_resolves_to_one_source_neutral_checked_graph() { FrameItem::ScopeBegin(scope) => match scope.effect { ScopeEffect::Filter(_) => "filter-begin", ScopeEffect::Opacity(_) => "opacity-begin", + ScopeEffect::Blend(_) => "blend-begin", ScopeEffect::Clip(_) => "clip-begin", }, FrameItem::ScopeEnd => "scope-end", @@ -133,7 +134,7 @@ fn gaussian_blur_resolves_to_one_source_neutral_checked_graph() { .find_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Filter(filter) => Some(filter), - ScopeEffect::Opacity(_) | ScopeEffect::Clip(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Clip(_) => None, }, _ => None, }) @@ -178,7 +179,7 @@ fn hard_shadow_graph_resolves_zero_one_two_and_n_input_operations() { .find_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Filter(filter) => Some(filter), - ScopeEffect::Opacity(_) | ScopeEffect::Clip(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Clip(_) => None, }, _ => None, }) @@ -238,7 +239,7 @@ fn drop_shadow_resolves_to_one_native_checked_operation() { .find_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Filter(filter) => Some(filter), - ScopeEffect::Opacity(_) | ScopeEffect::Clip(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Clip(_) => None, }, _ => None, }) @@ -1649,6 +1650,7 @@ fn admitted_effect_order_is_clip_then_opacity_then_mask_then_filter() { ScopeEffect::Clip(_) => "clip-begin", ScopeEffect::Opacity(_) => "opacity-begin", ScopeEffect::Filter(_) => "filter-begin", + ScopeEffect::Blend(_) => "blend-begin", }, FrameItem::MaskBegin(_) => "mask-begin", FrameItem::Node(_) => "node", diff --git a/crates/websem/tests/marker_contract.rs b/crates/websem/tests/marker_contract.rs index 67a6c2ef..19aaafd7 100644 --- a/crates/websem/tests/marker_contract.rs +++ b/crates/websem/tests/marker_contract.rs @@ -95,7 +95,7 @@ fn instances_flatten_to_ordinary_nodes_inside_hard_viewport_clips() { .filter_map(|item| match item { FrameItem::ScopeBegin(scope) => match &scope.effect { ScopeEffect::Clip(clip) => Some(clip), - ScopeEffect::Opacity(_) | ScopeEffect::Filter(_) => None, + ScopeEffect::Opacity(_) | ScopeEffect::Blend(_) | ScopeEffect::Filter(_) => None, }, FrameItem::Node(_) | FrameItem::ScopeEnd diff --git a/crates/websem/tests/mask_contract.rs b/crates/websem/tests/mask_contract.rs index 7031b6c9..649d95a3 100644 --- a/crates/websem/tests/mask_contract.rs +++ b/crates/websem/tests/mask_contract.rs @@ -170,6 +170,7 @@ fn same_element_order_is_clip_then_opacity_then_mask() { ScopeEffect::Clip(_) => "clip-begin", ScopeEffect::Opacity(_) => "opacity-begin", ScopeEffect::Filter(_) => "filter-begin", + ScopeEffect::Blend(_) => "blend-begin", }, FrameItem::MaskBegin(_) => "mask-begin", FrameItem::Node(_) => "node", diff --git a/crates/websem/tests/nested_viewport_contract.rs b/crates/websem/tests/nested_viewport_contract.rs index 1113dd57..4e633f02 100644 --- a/crates/websem/tests/nested_viewport_contract.rs +++ b/crates/websem/tests/nested_viewport_contract.rs @@ -60,6 +60,7 @@ fn item_tags(frame: &Frame) -> Vec<&'static str> { ScopeEffect::Clip(_) => "clip-begin", ScopeEffect::Filter(_) => "filter-begin", ScopeEffect::Opacity(_) => "opacity-begin", + ScopeEffect::Blend(_) => "blend-begin", }, FrameItem::ScopeEnd => "scope-end", FrameItem::MaskBegin(_) => "mask-begin", diff --git a/crates/websem/tests/shapes_contract.rs b/crates/websem/tests/shapes_contract.rs index 777afabc..cb04e31a 100644 --- a/crates/websem/tests/shapes_contract.rs +++ b/crates/websem/tests/shapes_contract.rs @@ -802,7 +802,7 @@ fn cascade_properties_the_build_cannot_represent_refuse_by_name() { ), ( "mix-blend-mode", - r##""##, + r##""##, "mix-blend-mode", ), ] { diff --git a/crates/websem/tests/support/unsupported_fixture.rs b/crates/websem/tests/support/unsupported_fixture.rs new file mode 100644 index 00000000..3894f103 --- /dev/null +++ b/crates/websem/tests/support/unsupported_fixture.rs @@ -0,0 +1,38 @@ +//! One ingress dispatch for the SVG and HTML named-refusal corpus. +use std::path::Path; +use websem::{CompileError, InitialViewport, SvgFrameSource}; + +pub(crate) fn compile( + root: &Path, + id: &str, + best_effort: bool, +) -> Result { + let svg = root.join(format!("{id}.svg")); + let html = root.join(format!("{id}.html")); + assert_ne!( + svg.exists(), + html.exists(), + "{id}: exactly one declared source ingress" + ); + let source = std::fs::read_to_string(if svg.exists() { &svg } else { &html }) + .unwrap_or_else(|error| panic!("{id}: read: {error}")); + if html.exists() { + if best_effort { + SvgFrameSource::from_html_inline_svg_best_effort(source.as_str()) + } else { + SvgFrameSource::from_html_inline_svg(source.as_str()) + } + } else if best_effort { + SvgFrameSource::from_standalone_svg_best_effort_with_fonts( + source.as_str(), + InitialViewport::new(64.0, 64.0), + crate::fixture_fonts::unsupported_environment(id), + ) + } else { + SvgFrameSource::from_standalone_svg_with_fonts( + source.as_str(), + InitialViewport::new(64.0, 64.0), + crate::fixture_fonts::unsupported_environment(id), + ) + } +} diff --git a/crates/websem/tests/svg_blending.rs b/crates/websem/tests/svg_blending.rs new file mode 100644 index 00000000..99619b4e --- /dev/null +++ b/crates/websem/tests/svg_blending.rs @@ -0,0 +1,385 @@ +//! Computed group ownership and refusal transactions. Pixel meaning is +//! independently guarded by the Chromium cells, not by these frame assertions. +use rframe::{Frame, FrameItem, ScopeBlend, ScopeBlendMode, ScopeEffect}; +use websem::{InitialViewport, SvgFrameSource, compile_standalone_svg}; + +fn svg(body: &str) -> String { + format!(r#"{body}"#) +} +fn frame(body: &str) -> Frame { + compile_standalone_svg(&svg(body), InitialViewport::new(64.0, 64.0)).unwrap() +} +fn blends(frame: &Frame) -> Vec { + frame + .items + .iter() + .filter_map(|item| match item { + FrameItem::ScopeBegin(scope) => match scope.effect { + ScopeEffect::Blend(blend) => Some(blend), + _ => None, + }, + _ => None, + }) + .collect() +} +const RECT: &str = r#""#; + +#[test] +fn neutral_groups_have_no_scope_and_raw_attribute_lookalikes_are_inert() { + for attrs in [ + "", + "style='mix-blend-mode:normal;isolation:auto'", + "mix-blend-mode='multiply' isolation='isolate'", + ] { + let result = frame(&format!("{RECT}")); + assert_eq!(result.items.len(), 1); + } +} + +#[test] +fn combined_opacity_is_one_blend_operation_not_a_nested_opacity_scope() { + let result = frame(&format!( + "{RECT}{RECT}" + )); + let ops = blends(&result); + assert_eq!(ops.len(), 2); // standalone initial backdrop + completed group + assert_eq!(ops[0].mode(), ScopeBlendMode::Normal); + assert_eq!(ops[1].mode(), ScopeBlendMode::Multiply); + assert_eq!(ops[1].opacity().unwrap().get(), 0.5); + assert!(!result.items.iter().any(|item|matches!(item,FrameItem::ScopeBegin(scope) if matches!(scope.effect,ScopeEffect::Opacity(_))))); +} + +#[test] +fn neutral_isolation_survives_and_consumes_descendant_backdrop_dependency() { + let result = frame(&format!( + "{RECT}" + )); + let ops = blends(&result); + assert_eq!(ops.len(), 2); // no extra standalone root boundary needed + assert_eq!(ops[0].mode(), ScopeBlendMode::Normal); + assert_eq!(ops[1].mode(), ScopeBlendMode::Screen); +} + +#[test] +fn redundant_normal_isolation_preserves_the_one_pass_opacity_fold() { + for body in [ + "", + "", + "", + ] { + let result = frame(body); + assert_eq!(result.items.len(), 1, "{body}"); + assert!(matches!( + result.items.iter().next(), + Some(FrameItem::Node(_)) + )); + } + let deep = format!( + "{}{}{}", + "".repeat(48), + RECT, + "".repeat(48) + ); + assert_eq!(frame(&deep).items.len(), 1); +} + +#[test] +fn elision_rollback_cannot_erase_a_reused_scope_identity() { + let source = svg(&format!( + "{RECT}{RECT}" + )); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!( + best.degradations() + .iter() + .filter(|d| d.action() == websem::DegradationAction::Skipped) + .count(), + 1 + ); + let modes: Vec<_> = blends(&best.base_frame()) + .iter() + .map(|b| b.mode()) + .collect(); + assert_eq!( + modes, + [ + ScopeBlendMode::Normal, + ScopeBlendMode::Screen, + ScopeBlendMode::Normal, + ScopeBlendMode::Multiply + ] + ); +} + +#[test] +fn outer_root_blending_keeps_its_own_transparent_initial_backdrop() { + for (value, mode) in [ + ("multiply", ScopeBlendMode::Multiply), + ("screen", ScopeBlendMode::Screen), + ] { + let source = svg(RECT).replace( + "width=\"64\"", + &format!("style='mix-blend-mode:{value}' width=\"64\""), + ); + let result = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)).unwrap(); + let modes: Vec<_> = blends(&result).iter().map(|b| b.mode()).collect(); + assert_eq!(modes, [ScopeBlendMode::Normal, mode]); + assert!( + matches!(result.items.iter().next(), Some(FrameItem::ScopeBegin(scope)) + if matches!(scope.effect, ScopeEffect::Blend(b) if b.mode() == ScopeBlendMode::Normal)) + ); + } +} + +#[test] +fn computed_winners_are_consumed_without_a_second_matcher() { + for attrs in [ + "style='mix-blend-mode:screen;mix-blend-mode:invalid'", + "style='--blend:screen;mix-blend-mode:var(--blend)'", + "class='subject' style='mix-blend-mode:multiply'", + ] { + let result = frame(&format!( + "" + )); + assert_eq!( + blends(&result).last().unwrap().mode(), + ScopeBlendMode::Screen + ); + } +} + +#[test] +fn unsupported_mode_skips_one_named_element_and_keeps_its_sibling() { + let source = svg(&format!( + "{RECT}{RECT}" + )); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(strict.contains("mix-blend-mode Overlay"), "{strict}"); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + let result = best.base_frame(); + assert_eq!(result.items.len(), 1); + let skipped: Vec<_> = best + .degradations() + .iter() + .filter(|d| d.action() == websem::DegradationAction::Skipped) + .collect(); + assert_eq!(skipped.len(), 1); + assert_eq!(skipped[0].path(), "svg/g[1]"); + assert!(skipped[0].reason().contains("mix-blend-mode Overlay")); +} + +#[test] +fn authored_clip_isolates_but_nested_viewport_clip_does_not() { + let child = format!("{RECT}"); + let clipped = frame(&format!( + "{child}" + )); + // Normal clip boundary + multiply child; no standalone-root layer needed. + assert_eq!(blends(&clipped).len(), 2); + let nested = frame(&format!("{child}")); + // The normal boundary belongs to the standalone root, outside viewport clip. + assert!( + matches!(nested.items.iter().next(),Some(FrameItem::ScopeBegin(scope)) if matches!(scope.effect,ScopeEffect::Blend(_))) + ); + assert!( + matches!(clipped.items.iter().next(),Some(FrameItem::ScopeBegin(scope)) if matches!(scope.effect,ScopeEffect::Clip(_))) + ); +} + +#[test] +fn image_effect_refusal_is_transactional() { + let body = format!( + "{RECT}{RECT}{RECT}" + ); + let source = svg(&body); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(strict.contains("image-effect composition"), "{strict}"); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!( + best.base_frame().items.len(), + 1, + "no partial group source survives" + ); + assert!( + best.degradations() + .iter() + .any(|d| d.path() == "svg/g[1]" && d.reason().contains("image-effect composition")) + ); +} + +#[test] +fn elided_normal_isolation_keeps_ancestor_image_effect_patrols() { + for (effect, resource) in [ + ( + "filter", + "", + ), + ( + "mask", + "", + ), + ] { + for opacity in ["1", ".6"] { + let source = svg(&format!( + "{resource}{RECT}{RECT}" + )); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(strict.contains("image-effect composition"), "{strict}"); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!(best.base_frame().items.len(), 1); + assert!( + best.degradations() + .iter() + .any(|d| d.path() == "svg/g[1]" + && d.reason().contains("image-effect composition")) + ); + } + } +} + +#[test] +fn empty_or_pruned_normal_isolation_does_not_poison_an_ancestor_profile() { + for opacity in ["1", ".6"] { + for child in [ + "", + "", + "", + ] { + let child = child.replacen("style=", &format!("opacity='{opacity}' style="), 1); + let result = frame(&format!( + "{child}{RECT}" + )); + assert!(blends(&result).is_empty()); + } + } +} + +#[test] +fn inline_backdrop_boundary_is_explicit() { + let body = svg(&format!("{RECT}")); + let html = format!("{body}"); + for result in [ + SvgFrameSource::from_html_inline_svg(html.as_str()), + SvgFrameSource::from_html_inline_svg_best_effort(html.as_str()), + ] { + assert!(result.unwrap_err().to_string().contains("host backdrop")); + } + let html = html.replace( + "width=\"64\" height=\"64\"", + "width=\"64\" height=\"64\" style='isolation:isolate'", + ); + assert!(SvgFrameSource::from_html_inline_svg(html.as_str()).is_ok()); +} + +#[test] +fn keyframes_cannot_bypass_the_static_computed_blend_patrol() { + let source = svg(&format!( + "{RECT}" + )); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(strict.contains("animated group-composition"), "{strict}"); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert!( + best.degradations().iter().any( + |d| d.path() == "svg/style[1]" && d.reason().contains("animated group-composition") + ) + ); +} + +#[test] +fn group_source_precision_refuses_transactionally_and_keeps_named_siblings() { + for source in [ + "", + "", + ] { + for style in ["mix-blend-mode:multiply", "isolation:isolate"] { + let source = svg(&format!("{RECT}{source}{RECT}")); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(strict.contains("group-source precision"), "{strict}"); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!(best.base_frame().items.len(), 1); + assert!( + best.degradations().iter().any( + |d| d.path() == "svg/g[1]" && d.reason().contains("group-source precision") + ) + ); + } + } +} + +#[test] +fn root_blend_opacity_precision_has_no_best_effort_fallback() { + let source = svg(RECT).replace( + "width=\"64\"", + "style='mix-blend-mode:screen' opacity='.5' width=\"64\"", + ); + for result in [ + SvgFrameSource::from_standalone_svg(source.as_str(), InitialViewport::new(64.0, 64.0)), + SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ), + ] { + assert!( + result + .unwrap_err() + .to_string() + .contains("root-layer precision") + ); + } +} + +#[test] +fn implicit_standalone_root_isolation_cannot_smuggle_a_sibling_image_effect() { + let source = svg(&format!( + "{RECT}" + )); + for result in [ + SvgFrameSource::from_standalone_svg(source.as_str(), InitialViewport::new(64.0, 64.0)), + SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ), + ] { + assert!( + result + .unwrap_err() + .to_string() + .contains("image-effect composition") + ); + } +} diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index 4af7b251..8a51d0a1 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -18,12 +18,14 @@ #[path = "support/fixture_fonts.rs"] mod fixture_fonts; +#[path = "support/unsupported_fixture.rs"] +mod unsupported_fixture; use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; -use websem::{DegradationAction, InitialViewport, SvgFrameSource}; +use websem::DegradationAction; /// What the two admissions must do with a fixture. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -44,6 +46,217 @@ use Departure::{BothRefuse, DeclaredByBestEffort}; /// construct itself — a refusal that stopped naming what it refused would pass /// a bare "does it error" check and fail this one. const CORPUS: &[(&str, Departure, &str)] = &[ + ( + "svg-group-blend-root-filter-sibling", + BothRefuse, + "image-effect composition", + ), + ( + "svg-group-blend-source-path", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-ellipse", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-clip", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-isolation", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-root-opacity", + BothRefuse, + "root-layer precision", + ), + ( + "svg-group-blend-source-radial", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-stroke", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-subpixel-clip", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-source-rotated-clip", + DeclaredByBestEffort, + "group-source precision", + ), + ( + "svg-group-blend-mode-overlay", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-darken", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-lighten", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-color-dodge", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-color-burn", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-hard-light", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-soft-light", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-difference", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-exclusion", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-hue", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-saturation", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-color", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-luminosity", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-mode-plus-lighter", + DeclaredByBestEffort, + "mix-blend-mode", + ), + ( + "svg-group-blend-filter", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-elided-filter-unit", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-elided-filter-partial", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-elided-mask-unit", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-elided-mask-partial", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-mask", + DeclaredByBestEffort, + "image-effect composition", + ), + ( + "svg-group-blend-resource-pattern-root", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-resource-pattern-child", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-resource-mask-root", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-resource-mask-child", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-resource-clippath-root", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-resource-clippath-child", + DeclaredByBestEffort, + "mix-blend-mode/isolation", + ), + ( + "svg-group-blend-css-animation", + DeclaredByBestEffort, + "animated group-composition", + ), + ( + "svg-group-blend-css-var-animation", + DeclaredByBestEffort, + "animated group-composition", + ), + ( + "html-group-blend-head-animation", + DeclaredByBestEffort, + "animated group-composition", + ), + ("html-group-blend-unisolated", BothRefuse, "host backdrop"), + ( + "html-group-blend-ancestor-mix-blend-mode", + BothRefuse, + "host backdrop", + ), + ( + "html-group-blend-ancestor-isolation", + BothRefuse, + "host backdrop", + ), + ( + "svg-group-blend-pattern-root-animation", + DeclaredByBestEffort, + "document load", + ), ("svg-clip-path-animation", DeclaredByBestEffort, "animation"), ( "svg-clip-path-basic-shape", @@ -1246,18 +1459,20 @@ fn corpus_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/web-first/unsupported") } -fn viewport() -> InitialViewport { - InitialViewport::new(64.0, 64.0) -} - #[test] fn the_corpus_on_disk_is_exactly_the_declared_one() { let disk: BTreeSet = fs::read_dir(corpus_root()) .expect("read the unsupported corpus") .filter_map(Result::ok) .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .filter(|name| name.ends_with(".svg")) - .map(|name| name.trim_end_matches(".svg").to_string()) + .filter(|name| name.ends_with(".svg") || name.ends_with(".html")) + .map(|name| { + Path::new(&name) + .file_stem() + .unwrap() + .to_string_lossy() + .into_owned() + }) .collect(); let declared: BTreeSet = CORPUS.iter().map(|(id, _, _)| (*id).to_string()).collect(); @@ -1270,16 +1485,9 @@ fn the_corpus_on_disk_is_exactly_the_declared_one() { #[test] fn every_unsupported_fixture_departs_by_name_in_both_admissions() { for (id, departure, named) in CORPUS { - let source = fs::read_to_string(corpus_root().join(format!("{id}.svg"))) - .unwrap_or_else(|error| panic!("{id}: read: {error}")); - - let strict = SvgFrameSource::from_standalone_svg_with_fonts( - source.as_str(), - viewport(), - fixture_fonts::unsupported_environment(id), - ) - .err() - .unwrap_or_else(|| panic!("{id}: strict must refuse an unsupported fixture")); + let strict = unsupported_fixture::compile(&corpus_root(), id, false) + .err() + .unwrap_or_else(|| panic!("{id}: strict must refuse an unsupported fixture")); assert!( strict.to_string().contains(named), "{id}: the strict refusal must name {named:?}; got {strict}" @@ -1287,27 +1495,19 @@ fn every_unsupported_fixture_departs_by_name_in_both_admissions() { match departure { BothRefuse => { - let best = SvgFrameSource::from_standalone_svg_best_effort_with_fonts( - source.as_str(), - viewport(), - fixture_fonts::unsupported_environment(id), - ) - .err() - .unwrap_or_else(|| { - panic!("{id}: a document-level contract refuses in both admissions") - }); + let best = unsupported_fixture::compile(&corpus_root(), id, true) + .err() + .unwrap_or_else(|| { + panic!("{id}: a document-level contract refuses in both admissions") + }); assert!( best.to_string().contains(named), "{id}: the best-effort refusal must name {named:?}; got {best}" ); } DeclaredByBestEffort => { - let best = SvgFrameSource::from_standalone_svg_best_effort_with_fonts( - source.as_str(), - viewport(), - fixture_fonts::unsupported_environment(id), - ) - .unwrap_or_else(|error| panic!("{id}: best-effort compiles: {error}")); + let best = unsupported_fixture::compile(&corpus_root(), id, true) + .unwrap_or_else(|error| panic!("{id}: best-effort compiles: {error}")); let declared: Vec<&websem::Degradation> = best .degradations() .iter() diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 61ae7653..291c2da2 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -58,7 +58,9 @@ from the dated addenda below: geometry, instance, resource-source, paint, and effect routes, normalized to one source-neutral fill/stroke/marker item order before the frame closes; `` and `` containers, visibility, isolated element/group/root opacity, - and HTML-ancestor opacity around the selected inline SVG; the whole + and HTML-ancestor opacity around the selected inline SVG; bounded static SVG + group blending/isolation through a combined resolved blend/opacity scope + ([B1](#b1-svg-group-blending)); the whole `transform` grammar in both spellings (the attribute is a presentation hint of the CSS `transform` property, and `gradientTransform` is that attribute on gradient elements); @@ -114,7 +116,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,284 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,378 Chromium-baked primitive cells plus 16 sampled frames, with a separate sixteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -125,7 +127,7 @@ from the dated addenda below: carrying declared one-code-value ramp-quantization bounds. The measured per-cell counts and causes are listed in the [corpus record](../../../fixtures/web-first/README.md). - The named refusal register has 260 rows. + The named refusal register has 303 rows. - **Not claimed:** no conformance score exists or may be computed — FLIP is unratified. The FLIP record and identity-changing review are prepared, but only the owner act on gridaco/nothing#49 may authorize them and the first @@ -5676,3 +5678,254 @@ frames, sixteen exact text cells, and eight real-font geometry witnesses are unchanged. External I/O, text, marker-source expansion, animation, and the separately tracked degenerate-gradient family remain separate work. No conformance score or FLIP record, rule, or baseline is touched. + +## B1: SVG group blending + +B1 introduces a bounded static CSS `mix-blend-mode`/`isolation` slice, not a +completed blending family. Normal, multiply and screen are consumed from the +existing Stylo computed values; the pin represents both longhands. Raw SVG +attribute lookalikes are inert, as their Chromium controls demonstrate. No +second matcher, presentation hint or cascade-pin change is involved. +`background-blend-mode` is untouched. All three CSS checklist rows remain open +under gridaco/nothing#81/#89/#90: the remaining honored operations and source +profiles do not acquire a tick merely because this slice is useful. The +[command README](../../../crates/n0_cli/README.md) remains the admitted-slice +statement of record. + +### Resolved meaning and the existing tree + +The source tree, resolved composition boundaries and backend allocations are +different structures. The existing balanced `FrameItems` stream carries the +new boundary without adding an authored tree or a Web-specific painter. +`ScopeEffect::Blend(ScopeBlend)` holds a checked, private-field normal/multiply/ +screen operation and optional `ScopeOpacity`; absent opacity means one and +does not erase isolation. Its source starts transparent and its completed +pixels composite once against the enclosing destination with the same +element's opacity. Producer-only tests originate this fact independently of +the SVG compiler. The contract remains backend-free and carries no resource +reference, backdrop image, layer bound, cache hint or budget. + +The renderer lowers that fact to one empty-start layer and one combined +restore. It does not change native `BeginOpacity`, whose backdrop-preserving +initialization and interpolation have a different meaning. Per-paint blending +and `feBlend` remain separate operations. The distinction follows the +[compositing model](https://www.w3.org/TR/compositing-1/) and is tested with +overlapping children: distributing the group operation changes the result. + +The compiler appends a known explicit boundary before descending and folds +private source/backdrop facts into its parent once. Neutral/default groups +emit no new command or layer. Normal isolation with no escaping descendant +blend is also redundant: balanced temporary scope markers are elided in one +linear program-exit pass, not by moving every nested descendant span. A lazy +identity-indexed ledger is truncated with every identity rewind; neutral +documents allocate no such ledger or compaction stack. Existing opacity +replay sees the unscoped facts and keeps its measured fold. Own partial +opacity already isolates, so normal isolation delegates directly to that +established opacity path. The resolved contract still retains every required +unit-normal isolation boundary. No new subtree replay discovers blending. +An implicit clip boundary inserts its opening pair in one suffix move; the +older clip insertion and opacity-fold machinery are not claimed to have become +linear in depth. No optimization policy or `DirtyClass` consumer is introduced. + +Immutable command replay remains distinct from caching completed blended +pixels. Consumer tests cover fresh-versus-reused execution over changed caller +backdrops, source edits, reorder/removal, isolation/opacity changes, scope +balance and conservative damage. No destination-dependent raster cache is +added. Standalone SVG initial transparency is resolved by the producer, not +hardcoded into the source-neutral `FrameProduct` painter. + +### Measured boundaries + +All probes and bakes use the unchanged hash-pinned `chromium_capture.ts` +through `probe_harness.ts`, Chromium 149.0.7827.55, with two deterministic +captures per source. Candidate sources also run through the actual `n0` +command in strict and best-effort admission. Exact equality here means decoded +RGBA equality, not equality of PNG encodings. No tolerance is added. + +| Chromium pair | Exact verdict | Evidence | +| --- | --- | --- | +| Whole-group multiply / per-child multiply | 576 pixels differ, maximum channel delta 89 | `svg-group-blend-multiply-{group,each}` | +| Whole-group screen / per-child screen | 576 pixels differ, maximum delta 98 | `svg-group-blend-screen-{group,each}` | +| Same-group blend plus opacity / outer opacity around blend | 2,048 pixels differ, maximum delta 76 | `svg-group-blend-multiply-opacity` / `svg-group-blend-outer-opacity` | +| Authored full/partial clip / same clip plus explicit isolate | Identical | `svg-group-blend-{clip,partial}` and their `-isolated` controls | +| Partial-opacity group / same group plus explicit isolate | Identical | `svg-group-blend-opacity{,-isolated}` | +| Translated/scaled/rotated group / explicit isolate | 2,048 / 1,536 / 2,160 pixels differ, maximum delta 152 | `svg-group-blend-{translate,scale,rotate}` and their `-isolated` controls | +| Nested viewport overflow clip / explicit isolate | 2,048 pixels differ, maximum delta 152 | `svg-group-blend-nested-hidden{,-isolated}` | +| Transparent source: normal / multiply | Identical | `svg-group-blend-{normal,multiply}-transparent` | +| Child normal / child multiply / child multiply with root isolation | Identical | `svg-group-blend-root-{normal,multiply,isolate}` | +| Actual outer-root normal / multiply / screen | Identical | `svg-group-blend-outer-root-{normal,multiply,screen}` | +| Opacity .5 or .6 over translucent green: plain / isolated shape, parent or child | Identical | `svg-group-blend-alpha-{half,six-tenths}-{plain,shape,parent,child}` | +| Multiple translucent draws, own opacity, transform or clip: plain / isolated | Identical | `svg-group-blend-normal-{unit,many-opacity,transform,clip}-{plain,isolate}` | + +In particular, authored `clip-path` isolates blending descendants, but a +nested SVG overflow clip does not. Neither a neutral container nor an ordinary +2D transform may acquire a layer merely for implementation convenience. +The rectangle matrix also covers both blend modes against mode-sensitive +colors, translucent sources, stroke overlap, gradients, repeating paints, +fractional placement, computed winners, invalid declarations and custom +properties. The self-contained HTML cell has an SVG-owned background and +explicit root isolation; it does not pretend the command paints the HTML page. +Five additional direct/instance/anchor/mapped-viewport cells and eight +explicit-Normal-isolation opacity controls cover opaque-backdrop interactions. +Nineteen further cells cover the actual outer root and redundant isolation +over translucent backdrops. The rung adds 94 exact primitive/HTML cells, +taking the corpus from 1,284 to 1,378, and 43 named refusals, taking that +register from 260 to 303. The sixteen +sampled frames and separate sixteen pixel/eight geometry text witnesses are +unchanged. + +HTML exterior-backdrop controls differ when isolation is added +**(measured, not celled)**. That exterior paint is absent from this command's +established SVG-local extraction contract. An escaping inline blend, a +non-normal inline-root blend, or blend/isolation on an HTML ancestor therefore +refuses in both admissions. Resource roots/contributors and CSS keyframes are +patrolled at their actual ingresses, including HTML-head styles and animated +custom-property indirection. A referenced pattern root's existing document-load +animation finding also survives best-effort compilation. These are named +refusals, not fallbacks to normal. Attributable failures roll back the complete +affected child transaction and retain valid siblings. +An implicit standalone-root boundary is subject to the same image-effect +profile as an explicit group: a filtered sibling cannot sneak into that new +source layer merely by residing outside the blending element. That root-level +combination refuses in both admissions and has its own registered witness. +Likewise, elision preserves authored blend/isolation participation separately +from physical scope and escaping-backdrop facts. Otherwise an ancestor filter +or mask could lose its conservative patrol when a Normal boundary disappears. +Four unit/partial-opacity ancestor witnesses guard that distinction; their +Chromium images equal their non-isolated controls **(measured, not celled)**, +not evidence of a new pixel defect. Empty/pruned contributions do not poison +an otherwise admitted ancestor. + +The final alpha patrol found a real defect in the initial B1 lowering: +redundant Normal isolation forced a different opacity restore over a +translucent green backdrop, changing 1,600 pixels by one code value. The +compiler correction above preserves the existing opacity fold instead of +introducing tolerance. Both admissions now match every new alpha cell. +Additional .375 and .5019607843137255 shape/parent/child controls also match +**(measured, not celled)**. A twelve-source cross-check of normal/multiply/screen +with opacity .5/.6, one opaque or two translucent source draws, and the same +partial backdrop is exact in both admissions **(measured, not celled)**. +Cross-seam tests separately execute actual +outer-root multiply/screen frames onto opaque and translucent colored caller +canvases; removing their initial Normal boundary changes those results. + +### The precision stop + +Clean rendering did not establish correctness. Before the new guards, an +ordinary curved path matched Chromium exactly, but multiply changed 93 pixels +at maximum delta 19 and screen changed 100 at maximum delta 28. A curved clip +changed 28 pixels at delta 12 for multiply and 28 at delta 18 for screen. A +blended root with opacity one-half changed all 4,096 pixels at delta 2. These +pixel comparisons are **(measured, not celled)**; their exact sources become +named refusal witnesses, not accepted pixel cells. + +A causal scratch control adds corner draws to force full-viewport source +bounds. That removes the large multiply/screen path discrepancies, while +ordinary isolated-normal still differs at 92 pixels by one code value +**(measured, not celled)**. This supports a source-extent/raster-materialization +dependency; it does not prove a complete Skia/Blink precision model. Tightening +layer bounds or adding tolerance would not resolve the remaining question. + +B1 therefore quarantines non-rectangular group sources and wider coverage +before admitting the rectangular slice. Radial rectangle paints, complex +strokes and subpixel/rotated clips are conservative extension guards, not +claims that every such input has a measured mismatch. Root blend/partial +opacity has a separate document-level guard. Wider image-effect composition, +resource-program blending and the fourteen other represented modes remain +registered refusals. The next widening crux is source-bound and intermediate +precision, not simply another enum value. + +### Gate sensitivity and cost discipline + +Replacing only the new painter's multiply restore with normal source-over +makes `just gate` fail. The whole-group witness changes 2,048 pixels at maximum +delta 152, the leaf witness 1,600 at delta 152, and the combined-opacity witness +2,048 at delta 76. The mutation is removed, the painter's source hash returns +exactly to its pre-mutation value, and the complete fixture gate passes again. +Neither the oracle nor a tolerance changes to accommodate that failure. + +A second mutation disables only redundant unit-Normal elision. The gate fails +five new witnesses: both child-opacity cells at 1,600 pixels/delta 1 and the +unit, transformed and clipped isolation controls at 1,024 pixels/delta 1. +Restoring the compiler's exact pre-mutation bytes returns the full gate to +green. The alpha correction therefore has its own sensitive external witness. + +On aarch64, baseline and B1 have identical `size_of` results: `FrameItem` and +`FrameNode` 200 bytes, `ScopeEffect` 64, drawlist `ItemKind` 136 and `Item` 168. +This is a data-layout observation, not proof of unchanged execution cost. +No layer-bounds optimization ships in B1. Trace-only execute aggregates record +observed raster pixel spans, area and peak live blend bytes separately from +duration samples. Empty clips and inaccessible storage are explicit outcomes. +The pinned accessor observes existing raster storage but changes its generation +state, so allocation observation belongs to a separate untimed frame. It does +not measure allocator capacity, Skia-internal allocations or GPU memory. + +The matched CPU-raster measurement used the clean pre-rung revision +`fd4097f2` and B1 on the same Apple M2 Ultra, 128 GiB host, macOS 26.5.1, +Rust 1.92.0 aarch64, skia-safe 0.99.0, release builds with tracing off. +Each process warms 20 replays, then takes 20 source-compile, 80 frame-compile +and 160 paint samples; three repetitions alternate baseline/current order. +Cargo startup, file/PNG work, canvas clear and the input clone are outside +their respective timed loops. Source compilation includes document and cascade +construction. Other task builds, captures and tests are stopped during the +final post-correction measurement. No GPU timing is claimed. + +The reproducible workload is a 256×256 SVG with an opaque `#426589` background. +For each group index `i`, set `x=(i%32)*8`, `y=((i/32)%32)*8`; paint a 6×6 +`#cd6843` rectangle at `(x,y)`, then a 3×3 `#5bace1` rectangle at `(x+2,y+2)`. +Use 100 or 1,000 sibling groups, optionally inside 16 or 48 nested groups. +The selected neutral, opacity .5, multiply or screen style applies to each +group, including the nesting wrappers. The single-large-group control instead +puts all those pairs inside one multiply group with neutral inner wrappers. +That is a different semantic workload, **not** a legal group-flattening rewrite. + +Numbers below are the median of three within-run p50s, in microseconds. + +| Workload | Source compile, B1 | Frame compile, baseline → B1 | Paint, baseline → B1 | +| --- | ---: | ---: | ---: | +| 100 neutral groups | 1,463.4 | 17.46 → 17.50 | 47.17 → 47.42 | +| 1,000 neutral groups | 15,606.1 | 186.33 → 185.54 | 429.21 → 430.67 | +| 100 neutral groups, depth 48 | 8,414.2 | 17.46 → 17.50 | 47.13 → 47.00 | +| 100 opacity groups | 1,515.4 | 25.29 → 25.04 | 3,333.29 → 3,336.54 | +| 100 multiply groups | 1,622.2 | not admitted → 25.92 | not admitted → 4,026.75 | +| 1,000 multiply groups | 17,120.5 | not admitted → 295.25 | not admitted → 40,038.04 | +| 100 screen groups | 1,617.0 | not admitted → 25.92 | not admitted → 4,027.50 | +| One multiply group containing 100 pairs | 1,614.4 | not admitted → 20.17 | not admitted → 104.75 | + +The unaffected controls show no repeatable increase above the plan's 5% +investigation threshold; this is not a universal regression guarantee. One +100-neutral current repetition is slower than the other two: source, +frame-compile and paint p50 ranges are 1,443.4–2,152.2, 17.42–21.50 and +47.04–56.29 µs respectively. The 1,000-neutral current paint p50 range is +430.21–432.08 µs; multiply-100 is 4,022.04–4,236.71 µs, with the largest +within-run p95 4,552.21 µs. The raw per-stage p50/p95/p99/min/max distributions +are retained in the ignored local execution record; no portable FPS claim is +drawn from this machine. + +The redundant-isolation workload uses the same source pairs with explicit +`isolation:isolate` and no blending descendants. At 100 / 1,000 groups it +emits 201 / 2,001 frame items and zero blend saves; paint p50 is 47.17 / 431.13 +µs. Source compilation is 1,622.6 / 17,124.9 µs, including cascade and the +elision ledger. Adding 48 isolated ancestors to the 1,000-group workload +still emits 2,001 items and zero blend saves, with 430.50 µs paint p50. Its +90,735.1 µs source compilation includes existing depth-dependent walks; +the linear final compaction is not a claim that the entire compiler is linear. + +Untimed trace observation makes the cost concrete. Multiply-100 emits 403 +frame items and 101 observable 256×256 raster layers including standalone-root +isolation: 26,476,544 cumulative accessible bytes and 524,288 peak live blend +bytes. Multiply-1,000 emits 4,003 items and 1,001 layers, 262,406,144 cumulative +bytes but the same live peak. Depth 16 increases that live peak to 4,718,592 +bytes. The one-large-group workload uses two layers and 524,288 cumulative/peak +bytes. There are no missing observations in these raster-only workloads; root +surface and non-blend allocations are excluded. Neutral groups report zero +blend saves, not an estimate of zero total renderer allocation. + +With the 100-group source positions held fixed and the canvas changed to +128×128 or 512×512, paint p50 becomes 1,196.54 or 15,295.17 µs; observed +cumulative blend storage becomes 6,619,136 or 105,906,176 bytes. The smaller +canvas also clips some source draws, so this is an allocation/viewport +experiment, not pure equal-coverage area scaling. The practical cost is repeated +active-clip-sized source materialization, not a larger semantic tree. Source +extent/precision must be proved before tightening bounds or pooling layers; +B1 records that cost and leaves the optimization unshipped. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 51532c6d..0b27ae41 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -678,6 +678,17 @@ excluded. - [ ] `background-blend-mode` - [ ] `isolation` +> B1 admits a bounded static SVG group-composition slice through the existing +> cascade: normal/multiply/screen and explicit isolation, with one combined +> group-opacity operation. These are CSS properties, not SVG presentation +> attributes. All three rows remain open: the other live blend values, +> source/effect precision profiles, resource programs, animation and HTML +> exterior-backdrop composition are not complete; `background-blend-mode` is +> untouched. The [admitted slice](../../../crates/n0_cli/README.md) names the +> boundaries, and the [B1 evidence](./svg-engine-of-record.md#b1-svg-group-blending) +> records the measured split. Partial coverage is not a tick under +> gridaco/nothing#81/#89/#90. + ### CSS fonts diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 6a059c27..44812c3b 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,8 +19,8 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): sixteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,284 primitive cells plus 16 sampled frames, those twenty-four text -witnesses, and 260 named refusal rows. Pixel cells use byte equality: what each +estate is 1,378 primitive cells plus 16 sampled frames, those twenty-four text +witnesses, and 303 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The real-font witness grades geometry before rasterization and makes no Chromium @@ -28,6 +28,15 @@ pixel claim. | File | Role | | --- | --- | +| `svg-group-blend-{normal,multiply,screen}-{leaf,group,opacity,alpha,transparent,stroke}.svg` · `svg-group-blend-{multiply,screen}-each.svg` | B1's exact group-operation controls: mode-sensitive backdrops, overlapping children, combined opacity, translucency, transparent initial source and fill/stroke composition. Whole-group versus per-child blending changes 576 pixels at maximum deltas 89 (multiply) and 98 (screen). No new tolerance. | +| `svg-group-blend-{neutral,isolated,clip,partial,opacity,outer-opacity}.svg` and their `-isolated` controls | Authored clip-path and partial opacity isolate descendants; a neutral group does not. Same-element blend plus opacity differs from an outer opacity group by 2,048 pixels at delta 76. Unit-opacity isolation is a real boundary, not an absent effect. | +| `svg-group-blend-{translate,scale,rotate,nested-hidden}.svg` and their `-isolated` controls | Ordinary affine transforms and a nested SVG overflow clip do not isolate. Adding isolation changes 2,048 / 1,536 / 2,160 pixels for the transform trio, and 2,048 for the viewport case, all at maximum delta 152. | +| `svg-group-blend-{linear,pattern,fractional}-{normal,multiply,screen}.svg` · `svg-group-blend-opacity-{0,1,p00392156862745098,p5019607843137255,p6}.svg` | Rectangular source paint/materialization and opacity boundaries, including a linear ramp, an existing repeating-vector paint, fractional rectangle edges, zero/unit opacity and fractional group alpha. This does not admit blending inside the pattern source. | +| `svg-group-blend-{css-winner,css-var,css-invalid,css-keyword,case-comment,attr-multiply,group-attr-isolate,root-normal,root-multiply,root-isolate}.svg` · `html-inline-svg-group-blend-isolated.html` | One-cascade syntax and neutral/raw-attribute controls; standalone initial transparency and a self-contained isolated inline SVG. The HTML cell has its own SVG background and makes no exterior-page rendering claim. | +| `svg-group-blend-{instance-direct,instance-use,anchor,mapped-direct,mapped-viewport}.svg` · `svg-group-blend-opacity-opaque-{fill,fillstroke}-{default,shape,parent,child}.svg` | Existing local `` and `` group composition match their direct control; mapped inner viewport composition matches the transformed control. The eight opacity controls pin fill-opacity .3 and element opacity .5 with explicit Normal isolation on the shape, parent or child, including fill/stroke overlap; they match their non-isolated counterparts exactly. | +| `svg-group-blend-outer-root-{normal,multiply,screen}.svg` | The computed mode is on the outermost SVG itself; all three blend the completed source against transparent initial black. The older `root-{normal,multiply,isolate}` trio instead compares a child blend and root isolation. A separate execution test uses a colored caller canvas and fails if the initial root boundary is removed. | +| `svg-group-blend-alpha-{half,six-tenths}-{plain,shape,parent,child}.svg` · `svg-group-blend-normal-{unit,many-opacity,transform,clip}-{plain,isolate}.svg` | Explicit Normal isolation preserves the established opacity route over a translucent backdrop. Before correction, shape/parent/child isolation changed 1,600 pixels by one code value; all now match exactly. Multiple translucent draws, transforms and clips also match their non-isolated controls when no child blend escapes. | +| *(measured, not celled — B1 precision remainder)* | An otherwise exact curved path acquires 93 differing pixels at delta 19 under multiply and 100 at delta 28 under screen; curved clip sources acquire 28 at deltas 12/18. Forcing full-viewport source bounds removes the large path-blend discrepancies, but isolated-normal still differs at 92 pixels by one code value. A root blend with opacity one-half differs at all 4,096 pixels by up to two. These sources are registered refusals, not tolerated positive cells; wider radial/stroke/clip profiles are conservatively guarded. Exterior HTML, image-effect and animated-group probes likewise retain named refusals. All CSS blending rows remain open. | | `svg-radial-start-{default,explicit-default,fx-*,fy-*,fxy-*,fr-*,outer-*,one-stop-*,zero-stops-focal,same-circle-*}.svg` | Ordered radial-circle evidence: independent focus coordinates, signed/zero/positive start radius, equal/reversed/zero end radii, exterior focus, tile behavior, and one-stop transparent domains. The older focal refusal source graduates unchanged as `svg-radial-start-graduated-focal.svg`; the one-stop witness preserves the previously silent constant-fold escape as positive evidence. | | `svg-radial-start-{grammar-*,box-percent,user-*,mapped-viewbox,template-*,transform-*,relation-*,spread-*,stops-*}.svg` | Resource-length grammar/defaults and placement: accepted signed/exponent forms versus invalid whitespace-only, trailing-dot, comma, and non-ASCII-whitespace fallbacks; axis and diagonal percentage bases; mapped user units; per-field template inheritance and local overrides; every spread mode across distinct circle relationships; transforms, stop order, duplicate stops, and alpha. Valid values surrounded by SVG whitespace match their unpadded controls in Chromium and both CLI admissions (measured, not celled). The decimal/percentage adjacent numeric controls and radius midpoint pair are byte-exact. | | `svg-radial-start-client-*.svg` · `svg-radial-start-transparent-cone.svg` · `html-inline-svg-radial-start.html` | Paint clients through shapes, path/stroke/dashes, paint order, channel/group opacity, existing `` instances, pattern source, clip, mask, offset filter, nested viewport, transparent exterior, and HTML ingress. This does not expand the marker-source paint profile or external-resource boundary. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 877ec689..66aaf293 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1284) +## Chromium-baked cells (1378) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -29,6 +29,7 @@ to its fixture source. No new image is committed for this view. html-inline-svg-ancestor-opacity html-inline-svg-currentcolor-rect +html-inline-svg-group-blend-isolated html-inline-svg-nested-viewport html-inline-svg-paint-order html-inline-svg-pattern @@ -595,6 +596,99 @@ to its fixture source. No new image is committed for this view. svg-gradient-zero-area-unit-split svg-gradient-zero-bbox svg-gradient-zero-stops-fallback +svg-group-blend-alpha-half-child +svg-group-blend-alpha-half-parent +svg-group-blend-alpha-half-plain +svg-group-blend-alpha-half-shape +svg-group-blend-alpha-six-tenths-child +svg-group-blend-alpha-six-tenths-parent +svg-group-blend-alpha-six-tenths-plain +svg-group-blend-alpha-six-tenths-shape +svg-group-blend-anchor +svg-group-blend-attr-multiply +svg-group-blend-case-comment +svg-group-blend-clip +svg-group-blend-clip-isolated +svg-group-blend-css-invalid +svg-group-blend-css-keyword +svg-group-blend-css-var +svg-group-blend-css-winner +svg-group-blend-fractional-multiply +svg-group-blend-fractional-normal +svg-group-blend-fractional-screen +svg-group-blend-group-attr-isolate +svg-group-blend-instance-direct +svg-group-blend-instance-use +svg-group-blend-isolated +svg-group-blend-linear-multiply +svg-group-blend-linear-normal +svg-group-blend-linear-screen +svg-group-blend-mapped-direct +svg-group-blend-mapped-viewport +svg-group-blend-multiply-alpha +svg-group-blend-multiply-each +svg-group-blend-multiply-group +svg-group-blend-multiply-leaf +svg-group-blend-multiply-opacity +svg-group-blend-multiply-stroke +svg-group-blend-multiply-transparent +svg-group-blend-nested-hidden +svg-group-blend-nested-hidden-isolated +svg-group-blend-neutral +svg-group-blend-normal-alpha +svg-group-blend-normal-clip-isolate +svg-group-blend-normal-clip-plain +svg-group-blend-normal-group +svg-group-blend-normal-leaf +svg-group-blend-normal-many-opacity-isolate +svg-group-blend-normal-many-opacity-plain +svg-group-blend-normal-opacity +svg-group-blend-normal-stroke +svg-group-blend-normal-transform-isolate +svg-group-blend-normal-transform-plain +svg-group-blend-normal-transparent +svg-group-blend-normal-unit-isolate +svg-group-blend-normal-unit-plain +svg-group-blend-opacity +svg-group-blend-opacity-0 +svg-group-blend-opacity-1 +svg-group-blend-opacity-isolated +svg-group-blend-opacity-opaque-fill-child +svg-group-blend-opacity-opaque-fill-default +svg-group-blend-opacity-opaque-fill-parent +svg-group-blend-opacity-opaque-fill-shape +svg-group-blend-opacity-opaque-fillstroke-child +svg-group-blend-opacity-opaque-fillstroke-default +svg-group-blend-opacity-opaque-fillstroke-parent +svg-group-blend-opacity-opaque-fillstroke-shape +svg-group-blend-opacity-p00392156862745098 +svg-group-blend-opacity-p5019607843137255 +svg-group-blend-opacity-p6 +svg-group-blend-outer-opacity +svg-group-blend-outer-root-multiply +svg-group-blend-outer-root-normal +svg-group-blend-outer-root-screen +svg-group-blend-partial +svg-group-blend-partial-isolated +svg-group-blend-pattern-multiply +svg-group-blend-pattern-normal +svg-group-blend-pattern-screen +svg-group-blend-root-isolate +svg-group-blend-root-multiply +svg-group-blend-root-normal +svg-group-blend-rotate +svg-group-blend-rotate-isolated +svg-group-blend-scale +svg-group-blend-scale-isolated +svg-group-blend-screen-alpha +svg-group-blend-screen-each +svg-group-blend-screen-group +svg-group-blend-screen-leaf +svg-group-blend-screen-opacity +svg-group-blend-screen-stroke +svg-group-blend-screen-transparent +svg-group-blend-translate +svg-group-blend-translate-isolated svg-group-inherited-fill svg-group-nested-transforms svg-group-paint-order @@ -1312,7 +1406,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (260) +## The refusal register (303) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1321,6 +1415,10 @@ its row into the cells above. | Fixture | Admission | The compiler's departure | | --- | --- | --- | +| `html-group-blend-ancestor-isolation` | **both refuse** | unsupported computed style: mix-blend-mode/isolation on an HTML ancestor needs the host backdrop and layer graph | +| `html-group-blend-ancestor-mix-blend-mode` | **both refuse** | unsupported computed style: mix-blend-mode/isolation on an HTML ancestor needs the host backdrop and layer graph | +| `html-group-blend-head-animation` | declared | declaration ignored at html/head[1]/style[1]: CSS keyframes with mix-blend-mode/isolation need the animated group-composition profile | +| `html-group-blend-unisolated` | **both refuse** | unsupported computed style: unisolated mix-blend-mode in inline SVG needs the host backdrop and layer graph | | `svg-clip-path-animation` | declared | skipped svg/rect[2]: unsupported SVG clip-path: a contributor's authored geometry is overridden at document load: its authored state is overridden at document load by the unsupported animation at svg/clipPath[1]/rect[1]/animate[1]: must be a direct child of a materialized top-level | | `svg-clip-path-basic-shape` | declared | skipped svg/rect[2]: unsupported SVG clip-path: a CSS basic-shape clip-path uses the independently listed basic-shape route | | `svg-clip-path-cycle` | declared | skipped svg/rect[2]: unsupported SVG clip-path: url(#a) forms a cyclic clip-path chain, whose raster/path strategy is not admitted | @@ -1419,6 +1517,45 @@ its row into the cells above. | `svg-gradient-stop-var` | declared | skipped svg/rect[1]: unsupported fill value "url(#g): a stop-opacity resolves through var(), an indirection this patrol cannot follow" | | `svg-gradient-unit-basis` | declared | skipped svg/rect[1]: unsupported fill value "url(#g): gradient geometry x2=\"4em\" uses a unit whose basis this slice does not consume (numbers, px, and percentages only)" | | `svg-gradient-userspace-zero-area` | declared | skipped svg/line[1]: unsupported stroke value "url(#g): a live user-space gradient on zero-area geometry cannot be mapped into the resolved unit-box paint contract" | +| `svg-group-blend-css-animation` | declared | declaration ignored at svg/style[1]: CSS keyframes with mix-blend-mode/isolation need the animated group-composition profile | +| `svg-group-blend-css-var-animation` | declared | declaration ignored at svg/style[1]: CSS keyframes with mix-blend-mode/isolation need the animated group-composition profile | +| `svg-group-blend-elided-filter-partial` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-elided-filter-unit` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-elided-mask-partial` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-elided-mask-unit` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-filter` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-mask` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-mode-color` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Color is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-color-burn` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode ColorBurn is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-color-dodge` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode ColorDodge is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-darken` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Darken is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-difference` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Difference is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-exclusion` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Exclusion is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-hard-light` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode HardLight is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-hue` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Hue is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-lighten` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Lighten is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-luminosity` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Luminosity is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-overlay` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Overlay is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-plus-lighter` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode PlusLighter is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-saturation` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Saturation is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-mode-soft-light` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode SoftLight is outside the admitted normal/multiply/screen group profile | +| `svg-group-blend-pattern-root-animation` | declared | skipped svg/rect[2]: unsupported fill value "url(#q): pattern #q authored state is overridden at document load: its authored state is overridden at document load by the unsupported animation at svg/defs[1]/pattern[1]/set[1]: animation element is outside the rect-x proving slice" | +| `svg-group-blend-resource-clippath-child` | declared | skipped svg/rect[2]: unsupported SVG clip-path: mix-blend-mode/isolation on a geometric clip contributor needs its own source profile | +| `svg-group-blend-resource-clippath-root` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode/isolation on needs its own source composition profile | +| `svg-group-blend-resource-mask-child` | declared | skipped svg/rect[2]: unsupported SVG mask: mask source cannot be compiled completely: unsupported computed style: mix-blend-mode/isolation in a mask, pattern, or marker source needs its own source composition profile | +| `svg-group-blend-resource-mask-root` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode/isolation on needs its own source composition profile | +| `svg-group-blend-resource-pattern-child` | declared | skipped svg/rect[2]: unsupported fill value "url(#q): pattern #q source cannot compile completely: unsupported computed style: mix-blend-mode/isolation in a mask, pattern, or marker source needs its own source composition profile" | +| `svg-group-blend-resource-pattern-root` | declared | skipped svg/rect[2]: unsupported fill value "url(#q): unsupported computed style: mix-blend-mode/isolation on needs its own source composition profile" | +| `svg-group-blend-root-filter-sibling` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-root-opacity` | **both refuse** | unsupported computed style: mix-blend-mode with partial opacity on the root crosses the root-layer precision boundary | +| `svg-group-blend-source-clip` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with curved, subpixel, or rotated clip coverage crosses the group-source precision boundary | +| `svg-group-blend-source-ellipse` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | +| `svg-group-blend-source-isolation` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | +| `svg-group-blend-source-path` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | +| `svg-group-blend-source-radial` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode/isolation with a radial source paint crosses the group-source precision boundary | +| `svg-group-blend-source-rotated-clip` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with curved, subpixel, or rotated clip coverage crosses the group-source precision boundary | +| `svg-group-blend-source-stroke` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode/isolation with a complex source stroke crosses the group-source precision boundary | +| `svg-group-blend-source-subpixel-clip` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with curved, subpixel, or rotated clip coverage crosses the group-source precision boundary | | `svg-image` | declared | skipped svg/image[1]: unsupported element | | `svg-line-gradient-coordinates-calc-values` | declared | skipped svg/line[1]: attribute x1="calc(16px + 16px)" is not a number; skipped svg/line[2]: attribute x2="calc(50% + 8px)" is not a number; skipped svg/line[3]: attribute y1="min(24px, 50%)" is not a number; skipped svg/line[4]: attribute y2="max(24px, 25%)" is not a number; skipped svg/rect[2]: unsupported fill value "url(#gx1): gradient geometry x1 uses calc(), whose computed length is not represented by the direct resource decoder"; skipped svg/rect[3]: unsupported fill value "url(#gx2): gradient geometry x2 uses calc(), whose computed length is not represented by the direct resource decoder"; skipped svg/rect[4]: unsupported fill value "url(#gy1): gradient geometry y1 uses min(), whose computed length is not represented by the direct resource decoder"; skipped svg/rect[5]: unsupported fill value "url(#gy2): gradient geometry y2 uses max(), whose computed length is not represented by the direct resource decoder" | | `svg-line-gradient-coordinates-css-comments` | declared | skipped svg/line[1]: attribute x1="/\*\*/8/\*\*/" is not a number; skipped svg/line[2]: attribute x2="/\*\*/56/\*\*/" is not a number; skipped svg/line[3]: attribute y1="/\*\*/8/\*\*/" is not a number; skipped svg/line[4]: attribute y2="/\*\*/56/\*\*/" is not a number; skipped svg/rect[2]: unsupported fill value "url(#gx1): gradient geometry x1 contains a CSS comment this direct length parser cannot tokenize"; skipped svg/rect[3]: unsupported fill value "url(#gx2): gradient geometry x2 contains a CSS comment this direct length parser cannot tokenize"; skipped svg/rect[4]: unsupported fill value "url(#gy1): gradient geometry y1 contains a CSS comment this direct length parser cannot tokenize"; skipped svg/rect[5]: unsupported fill value "url(#gy2): gradient geometry y2 contains a CSS comment this direct length parser cannot tokenize" | diff --git a/fixtures/web-first/chromium/html-inline-svg-group-blend-isolated.png b/fixtures/web-first/chromium/html-inline-svg-group-blend-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..b02eafefa206d420322d90de744b8817044dcae2 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMrJgR1Ar*7p+*IUhFpzL~sL0lA z)}L%tDafB}%r)<*LWQz^!ls=D0dkYSAI+@%F#WIg??Nt1ZKr2E3nU$k8^Vs+%{eZ` z;cKq;_dUl&h*%oKS%z7RTmIi;590kScQ0V;*T+A%>GVJE699>uFlVq`KvnpzHRyhJ z^6xq8EuFx!HS#--e)}F@+yYjaDqq0J!0`XSq|c!XKnJsd9L&rhqR6e&xXUOKD8k_B L>gTe~DWM4fC?i%1 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-alpha-half-child.png b/fixtures/web-first/chromium/svg-group-blend-alpha-half-child.png new file mode 100644 index 0000000000000000000000000000000000000000..2de0b221250652a325aceb7a4978d48c41d89b9c GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7p+}_CBU?6Zbpm`H- zbLz4+2kg!;D+gRqxVe7csY_Gq3yPJtDTgyW;cAFsoFJXgo?Y}Ixw!D7qdthQ%}~VZ zu#Vw`D8o)B1(+gSg1;D|s~NGVvjw{4|Nn}>L-Tz$_f-=@O1Ta JS?83{1OWC4L#Y4& literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-alpha-half-parent.png b/fixtures/web-first/chromium/svg-group-blend-alpha-half-parent.png new file mode 100644 index 0000000000000000000000000000000000000000..2de0b221250652a325aceb7a4978d48c41d89b9c GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7p+}_CBU?6Zbpm`H- zbLz4+2kg!;D+gRqxVe7csY_Gq3yPJtDTgyW;cAFsoFJXgo?Y}Ixw!D7qdthQ%}~VZ zu#Vw`D8o)B1(+gSg1;D|s~NGVvjw{4|Nn}>L-Tz$_f-=@O1Ta JS?83{1OWC4L#Y4& literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-alpha-half-plain.png b/fixtures/web-first/chromium/svg-group-blend-alpha-half-plain.png new file mode 100644 index 0000000000000000000000000000000000000000..2de0b221250652a325aceb7a4978d48c41d89b9c GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7p+}_CBU?6Zbpm`H- zbLz4+2kg!;D+gRqxVe7csY_Gq3yPJtDTgyW;cAFsoFJXgo?Y}Ixw!D7qdthQ%}~VZ zu#Vw`D8o)B1(+gSg1;D|s~NGVvjw{4|Nn}>L-Tz$_f-=@O1Ta JS?83{1OWC4L#Y4& literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-alpha-half-shape.png b/fixtures/web-first/chromium/svg-group-blend-alpha-half-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..2de0b221250652a325aceb7a4978d48c41d89b9c GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7p+}_CBU?6Zbpm`H- zbLz4+2kg!;D+gRqxVe7csY_Gq3yPJtDTgyW;cAFsoFJXgo?Y}Ixw!D7qdthQ%}~VZ zu#Vw`D8o)B1(+gSg1;D|s~NGVvjw{4|Nn}>L-Tz$_f-=@O1Ta JS?83{1OWC4L#Y4& literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-child.png b/fixtures/web-first/chromium/svg-group-blend-alpha-six-tenths-child.png new file mode 100644 index 0000000000000000000000000000000000000000..7bb50e54eae7df1f1fb3cdc2948c3abc92a49ebc GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=9iA?ZAr*7p+}_CBU?6Zbpm~#E zbBg~io{^Tn@k zV_W#@_>%8^0%sX!F>d+)k9lQZT8ICe=Z_1oU+aGk5jKG+mvk_0NMiu0gA4qAU#CzS zf1CBN;35>+o9uUh?)(4$S!3jVAcGC$P-ccF+8oKpS=YS+iZFP(`njxgN@xNA*Sl6{ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-attr-multiply.png b/fixtures/web-first/chromium/svg-group-blend-attr-multiply.png new file mode 100644 index 0000000000000000000000000000000000000000..2942b193ef60f208c543f32e50b74fae73410da7 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf@jlb`W4VINdju z?@p(8fnZ193*JsHjt;l}vP-*DmY&Z0`RAJ9=Ib+J7^8sjR*~tAd%p$#9shoaStyMN91_|+v&Anh=C>kFeg&=LRt$FzjZ12Wh^u3=_a7r>VP$M(Z=pa_GftDnm{ Hr-UW|A#_Nr literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-case-comment.png b/fixtures/web-first/chromium/svg-group-blend-case-comment.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3246856e51cd471e33e586c687fa24b48f7551 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM7Ec$)kcv5PZf@i}>>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-clip-isolated.png b/fixtures/web-first/chromium/svg-group-blend-clip-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb0ff1e6747a785876baa0f5956d61fa2dbd5e1 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMm7Xq+Ar*7p+}z07Vj$u0u;4k% z^}AMLd5feiT6VPWXyQwL&1Kd!b*?7UDdkW5jBeTdnEgld)Gltz-z{e(9gG{&7}j>w zzSv)^u%&wHm;K5vAd#~Svlva7GuSTNtCsipn%!2={(n>Ky!DqWD&KKjw zzSv)^u%&wHm;K5vAd#~Svlva7GuSTNtCsipn%!2={(n>Ky!DqWD&KKj>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-css-keyword.png b/fixtures/web-first/chromium/svg-group-blend-css-keyword.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3246856e51cd471e33e586c687fa24b48f7551 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM7Ec$)kcv5PZf@i}>>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-css-var.png b/fixtures/web-first/chromium/svg-group-blend-css-var.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3246856e51cd471e33e586c687fa24b48f7551 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM7Ec$)kcv5PZf@i}>>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-css-winner.png b/fixtures/web-first/chromium/svg-group-blend-css-winner.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3246856e51cd471e33e586c687fa24b48f7551 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM7Ec$)kcv5PZf@i}>>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-fractional-multiply.png b/fixtures/web-first/chromium/svg-group-blend-fractional-multiply.png new file mode 100644 index 0000000000000000000000000000000000000000..2749b6cd3b52a2e44e32e00e8cb3b120a4d422a6 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM)t)YnAr*7p+&s&9$U(s2;Yp#3 z4LhTnOEYxEk^{db^2Uiav3$CG!0Fqj{sX_)S#Q{Lc+RrDw(~ZoYnGj3n8j$qoRRx3 zv10$S_v)*>|1RBqZ~OUl=4XX7{_S6}&ho44=XVTeA*$Fe@GOvYFm6c0F0kdz%&nFs zFDv?9U90bYwwclOdfEhQROMno*Z%*1regMdAcGC$Y-WaCxA;P~nVl5{iZFP(`njxg HN@xNAlUHDR literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-fractional-normal.png b/fixtures/web-first/chromium/svg-group-blend-fractional-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..ec72e87a85c524e52ee8180066b990b2025e41be GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMjh-%!Ar*7p+}y}}$Uwm1qHz}s zuaM6HHlBl0S9lI9=p13wVerVDbjp)$gNXURt}pSgC$EW*DZTslV~_6bqsJs2j2qGz z)@s|{x%_+5zl(R&=g0lrRku%~z~`|g@5MXj$4-cTe#dZ@VHTqaa|YW5o&^vMSOvB` znz{A-?|7L-JAd9^^)u-}QSQ2(*MsgbqUbeaWMKIJ-|TJaexT#oK#pf-2wW(VQ-1i| Q4WI~vr>mdKI;Vst09p588~^|S literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-fractional-screen.png b/fixtures/web-first/chromium/svg-group-blend-fractional-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..acd523499b7977b27a943c9fefabf48b2361203e GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMb)GJcAr*7p+}z07>>$$cP}y=) zh}lUd&CPCUo^2=jZl1oxx@BWf;00Db8-eCMmXGR|T&Va{N?z_UQo0iprBz)9yU zYoDFs=Y4hlr=3-2-n8zlLOIxcxbm|=2mk-S)O*`0AcGC$a%KibYtbj$jE?aGMHoC? L{an^LB{Ts5NAO%k literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-group-attr-isolate.png b/fixtures/web-first/chromium/svg-group-blend-group-attr-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8cc8abf109d4f2241071cbd538cb0952b214c8 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf@jlb`W4VINkMC zpUh72u7zy37Ai|EU}9Qyaq-+QdXXFVdQ I&MBb@0O`y^wEzGB literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-instance-direct.png b/fixtures/web-first/chromium/svg-group-blend-instance-direct.png new file mode 100644 index 0000000000000000000000000000000000000000..3277a8546cbcdbb991868702f933a7debbb6fc52 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMxt=bLAr*7p+}z07U?AXd(b*+O zM<*xtz$M;0M-yevsm3_GIdY113kS#i6ZNT+o*j1V(q3NKIY)G&C36Pb1)c>{^Tn@k zV_W#@_>%8^0%sX!F>d+)k9lQZT8ICe=Z_1oU+aGk5jKG+mvk_0NMiu0gA4qAU#CzS zf1CBN;35>+o9uUh?)(4$S!3jVAcGC$P-ccF+8oKpS=YS+iZFP(`njxgN@xNA*Sl6{ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-instance-use.png b/fixtures/web-first/chromium/svg-group-blend-instance-use.png new file mode 100644 index 0000000000000000000000000000000000000000..3277a8546cbcdbb991868702f933a7debbb6fc52 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMxt=bLAr*7p+}z07U?AXd(b*+O zM<*xtz$M;0M-yevsm3_GIdY113kS#i6ZNT+o*j1V(q3NKIY)G&C36Pb1)c>{^Tn@k zV_W#@_>%8^0%sX!F>d+)k9lQZT8ICe=Z_1oU+aGk5jKG+mvk_0NMiu0gA4qAU#CzS zf1CBN;35>+o9uUh?)(4$S!3jVAcGC$P-ccF+8oKpS=YS+iZFP(`njxgN@xNA*Sl6{ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-isolated.png b/fixtures/web-first/chromium/svg-group-blend-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb0ff1e6747a785876baa0f5956d61fa2dbd5e1 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMm7Xq+Ar*7p+}z07Vj$u0u;4k% z^}AMLd5feiT6VPWXyQwL&1Kd!b*?7UDdkW5jBeTdnEgld)Gltz-z{e(9gG{&7}j>w zzSv)^u%&wHm;K5vAd#~Svlva7GuSTNtCsipn%!2={(n>Ky!DqWD&KKjt~tLx-)M7_j~-J+ zH^V)phV9p~ndi&D=UDxsX+y_4OFf~m43Fi8%PxutmMiu~^=!|$AfVvg_0b|u@a*xx z!oFphlai%EE$ctcIexkM&Z7C+4MLl)B%Mmxzv%ePOtw`TFS#wB=E{_<4%5DU(tx2l z?(}4NJ_o5oPD#dFx-64(!!|Vh?JF|kVJM0S^j`P&+|!*`|E)}IoAiC!y@h#Stt8?b z5AQp*t#?A&^EFI2>K!y@&YRdPxs<6Yl2J0gQM%fJ|D~y|LZ59wctZF6Zw&vk_^*im z5VuVbov@4Dz>)s~`wQmji-6QFh8N6V8vPi4G1M-QZm@HZf5Eh4Gf?s{SUe9b&i&w= z+yZ_c<^>@2j3D*#rC;kGUB29XMqyvp16^47a6p-}gTe~DWM4fXE40R literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-linear-normal.png b/fixtures/web-first/chromium/svg-group-blend-linear-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..f7de162f8b056c30e08ebff94dc9d97002611293 GIT binary patch literal 522 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLH@YFz)wsaSW-L^JZ4G-w^{I*SA{^ zA4>Zct!9>$LgT)M@>`B~NhYWXZfqTJ~uVHw^)D0A2@O1TaS?83{1OSs{*E|3K literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-linear-screen.png b/fixtures/web-first/chromium/svg-group-blend-linear-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..547487693290321d3c56ab15bf2aa239f601a70e GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLH@YFi!S#aSW-L^Cs$c?qLHS8@8K^ z7mK{m`}xA~ZBAXhmRalNjT$n5I$Zpc#)32?!^~jp> zac1y>QSaZS{C4x^_uVMnu+F9a;AiDRFVX&Mm3h~`KD+qZ_RSPK*Xf^Sir*cbw0XI` z^Lc;UnXgRQf8U$`0vwimh;ITSdsf6 zt5tGw`%aiKLrQ74XY$U z%xByj?8Q>|5Uy zES$E~|82DPHn!RKa`xvv%S7S}TQb-#@GOvYFm6a=ILi?Ajj^>_aR#FaGm7vm90Ghm mhyDLAb@@^bkiiCWAv43-b(}Fld%8V=A`G6celF{r5}E)4$WAf< literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-mapped-viewport.png b/fixtures/web-first/chromium/svg-group-blend-mapped-viewport.png new file mode 100644 index 0000000000000000000000000000000000000000..9e8b6808f1db6edd940942e285689f48301d0b51 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMX`U{QAr*7p+}Oz3U?AXdG5F5m zTbe9uWn2nG%}?;1OL(Y}m$b5%L#$5ee<8Q>|5Uy zES$E~|82DPHn!RKa`xvv%S7S}TQb-#@GOvYFm6a=ILi?Ajj^>_aR#FaGm7vm90Ghm mhyDLAb@@^bkiiCWAv43-b(}Fld%8V=A`G6celF{r5}E)4$WAf< literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-alpha.png b/fixtures/web-first/chromium/svg-group-blend-multiply-alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..c5eb4bf802e1fe9b8c94e16de1268f773df2c0f6 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=2RvOILn`LHxwWy=$x(vg;m*T< zJ}7w`Y+}7=#d9=i&0#B+=nY)GN4T~y$1Cu+&eXr#%T^arR=(-hyYh~|O5qGoxEf*@ zC(P!_jkwn*z3)cmF1rtHZVWq_6kt-?3`MLC>wYu1mCZf(U-IJP{B0Fm{}u0_6TjA+ z>4YezN(PAF_1y<9w3YvV$b0v8eUviD4xmAEnI&#s`2ACP&ONX?w@{m7n^;)cG7b&*CvlRwe07uO7fCFiuvsbP9K$R|6XuN6H-|T# zX$<(DeCdCmz*&eG+XbElk`BM$*D74Cn)8tN>Nfj%<+-0L-f>(6i5oYhF`y}o)A+jA zY}@&tvy@%Hvfmq{bL#(_x_5w7Mtx%x2Riis|4Z8=jDQR_kc*iauBr1(x!iG79w@@# M>FVdQ&MBb@0F@S6s{jB1 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-group.png b/fixtures/web-first/chromium/svg-group-blend-multiply-group.png new file mode 100644 index 0000000000000000000000000000000000000000..b63b68a342b1cdc23f1a6c937748ee28b5e52bc4 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM<(@8%Ar*7p+}z07U?9S9QDIxt z-l=PiBX%|`Tl5(3=&O#{(eQvNtZW&FhWnE^$y~m=$@7b5Je#mtDdil)EJhRNjMO)W zcj`3;d=0+zzfa&SM2zhM&jLw@-|uS`uHNf;D0=&2#htBRAMaFlF>Xj>Kv4q{c)P!P z;g#>VZrDA~hMI7nJ#XXRzt?3H!8Y7vj{&;$|Nm`sZ%+j>*g#HZW_aDlEwJcIP!Ujs N!PC{xWt~$(695K-UHJe2 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-leaf.png b/fixtures/web-first/chromium/svg-group-blend-multiply-leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8cc8abf109d4f2241071cbd538cb0952b214c8 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf@jlb`W4VINkMC zpUh72u7zy37Ai|EU}9Qyaq-+QdXXFVdQ I&MBb@0O`y^wEzGB literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-opacity.png b/fixtures/web-first/chromium/svg-group-blend-multiply-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..f034fce7ac699ed5613caa7502565e141eb061fc GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM<(@8%Ar*7p++4`pY9Qip@mc$y z1OW~mZiSl-u@Y?$Bbrz_`W6IOXw7oS^LjUtwRm#+uPC0!B1PZ0XBs!8F`Q+H68v4D zQ>^gkw%yBm_YRQAEJhRN47LkA3u?dHO~_ik>qAHBe%riPpT6A@TqNlL(FIon68Ls^ zZ@{bS+?@HJ&7dZ{XP$oJ-@n{G0g!21-Z1+CUHbpOuh7w3Kn5Gg$;=F=e{iK{pNowF PiZFP(`njxgN@xNAVa!`m literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-stroke.png b/fixtures/web-first/chromium/svg-group-blend-multiply-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..7aaefd0f609900761af833df9f6938259459dbc4 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMxt=bLAr*7p+}z07U?AXd(OKp| zoKBAHVOg1jiF1zD=I9=|CphPTlE4p@f7jK{wl^PGI`_J+*EN@iJPRZpj2o`i$;vzz zZpnNz>7BKc33CS9g;%xg`*^;~S;qVovb2B*L6oL3oMo8B2v+lNyZEWC=T9CLTm&*B z?7j7bU-90BEkNA}rBNRl^?~mD|9`G(rWugI268Af!_k?XnGfbqnE(`F@O1TaS?83{ F1OT7pQ>*|0 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-transparent.png b/fixtures/web-first/chromium/svg-group-blend-multiply-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..95d2ecd2be1ce83c6724222cb0b6838355b7ed4d GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=+dW+zLn`LHxw(;($x(#iVqlK5 z?jf!tH_ja4T66Nql_O_v2(d(O5VronWN9;_^5y-E2ZuLVyXW=AFiy~BC}MTUGc0*f zd28;>4QGEAK4M=B5?aS_LX=@AlR`Md6Rw7s_l!Mnw%%DMaq(l>&HCB#{I7p+uhedU zX-8EHHuD#Qv0K@mz2E-?{%2wa8UPkte4lyQI#GzxmbDH1K$rjje?m0p8IZvS@&Yr% Y3kKevFPxkI14S4-UHx3vIVCg!01lL3$p8QV literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-nested-hidden-isolated.png b/fixtures/web-first/chromium/svg-group-blend-nested-hidden-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..3611869a8a32b42d7054b98694ead969dc3eab57 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMHJ&bxAr*7p+}z07;vm9s@##4( zz1?McJN&jVcMJR$h`x~IlY7v`K$zK;H8o<6cxR5$hsl3u|GA`eT9pS&Syk z8Eh9ODYu_0m*)8SaqEZR@R_z*>+M zfd%Gm6u0^mw|mRh^E;cBUBLRkH=3UQ`}eMlV$>H#VW4yW|9=oC)(d2?f!xi^@RpDN UzfrNyVW0?ur>mdKI;Vst0NGGmRR910 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-nested-hidden.png b/fixtures/web-first/chromium/svg-group-blend-nested-hidden.png new file mode 100644 index 0000000000000000000000000000000000000000..67865bce94ef324f8cd854f4a4a65553506b7375 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMm7Xq+Ar*7p+*IUhbr5m5_%Qj0 zG1nl+>2AyZ?9dnd(PV@05;)1`?s9$-}8%GGA^>~0NwikzfucN7?8mRax^o8fE~Av TGVJE699>uFlVq`KvnpzHRyhJ z^6xq8EuFx!HS#--e)}F@+yYjaDqq0J!0`XSq|c!XKnJsd9L&rhqR6e&xXUOKD8k_B L>gTe~DWM4fC?i%1 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-alpha.png b/fixtures/web-first/chromium/svg-group-blend-normal-alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2f89ebb908c406564817db6c3299f362526240 GIT binary patch literal 270 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=hdo^!Ln`LHxwWv@$x(vg;Yry? zXJ!_Mvskc9vYWu-spyflK-?yR#dd+`iw5-*+$zTN?}kqPb26=5{q)zo<{RQgtPblK zPKYw>Jb9t*?f1f??6>cWv&UWFPzYyu!qpJNI6<2MqNG&*z=?JLKij)~`E5McHhO>T zx#j7vd!dp*{Y)4-zUmold3d$H!uk8R_t#(sK4)+<`(pP$52o`avklM}|Nmz&37iBn d*g)Q4W?(oWIHfoB#%-VogQu&X%Q~loCII$IWzhft literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-clip-isolate.png b/fixtures/web-first/chromium/svg-group-blend-normal-clip-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..01e89078a53fd1ee72b447e9c7e05b9cf8696f93 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydu{YLn`LHx$Vf+7%1ZK(0LPM zx50#(`o1uu+A@9@M zgAaE^d^%os`}S1%Elejw8Fn%$T(@s%v|Tn^k~u1V-e>>MpR?-!o8+@XbgW~5s0n9y z!qosX21)P>quX6>EK1(-GBEuA|KYvuUZBI-Kn`bSIOfK?iE&c9Hc*7Y)78&qol`;+ E0M&I^kN^Mx literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-clip-plain.png b/fixtures/web-first/chromium/svg-group-blend-normal-clip-plain.png new file mode 100644 index 0000000000000000000000000000000000000000..01e89078a53fd1ee72b447e9c7e05b9cf8696f93 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydu{YLn`LHx$Vf+7%1ZK(0LPM zx50#(`o1uu+A@9@M zgAaE^d^%os`}S1%Elejw8Fn%$T(@s%v|Tn^k~u1V-e>>MpR?-!o8+@XbgW~5s0n9y z!qosX21)P>quX6>EK1(-GBEuA|KYvuUZBI-Kn`bSIOfK?iE&c9Hc*7Y)78&qol`;+ E0M&I^kN^Mx literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-group.png b/fixtures/web-first/chromium/svg-group-blend-normal-group.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b30ff25c3c5511bce7ef1c6796af8acc8973ed GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM#hxyXAr*7p+}z07Vj$u0aK82Q zg1L_igxw1`1Z5815-m|&F0kf}k6?h@#P3r*XR+5$Uh->e?UXmdt;P*$3}+dlrrwg+ zmo2bs?@6oo92a>ONIF2oXEAR1eviGV_rK=50QGCzQ}@M`w`8zgKvrPF3>Mh?JyzrU z{%w41$}V87{~Lo}{r#8OCvcVlXviaGKAw-JOBUy literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-leaf.png b/fixtures/web-first/chromium/svg-group-blend-normal-leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..2942b193ef60f208c543f32e50b74fae73410da7 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf@jlb`W4VINdju z?@p(8fnZ193*JsHjt;l}vP-*DmY&Z0`RAJ9=Ib+J7^8sjR*~tAd%p$#9shoaStyMN91_|+v&Anh=C>kFeg&=LRt$FzjZ12Wh^u3=_a7r>VP$M(Z=pa_GftDnm{ Hr-UW|A#_Nr literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-many-opacity-isolate.png b/fixtures/web-first/chromium/svg-group-blend-normal-many-opacity-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..62ed87a2bfdf95c54af8596ceb4de98834b79646 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyCZhl<4 z`CZhl<4 z`Cs@zwTh=c{uW!AFBpr+!(iqlyesfH?u#jJ$zZ#HtiXgBEU@=`tj72K z+xXg)T|fq&{ofe;>hHhIJ^`>HkC^#@uKfQ$S47GP$Y29GmzhDgk=Lkli_>?Y2!p4q KpUXO@geCy*1yln7 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-stroke.png b/fixtures/web-first/chromium/svg-group-blend-normal-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2563860368f524e6e7e27f72180bd521a4823d GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMxt=bLAr*7p+}z07U?AXd(K+T& zsY?NC_CeM=hmHxZiFGWHzh1!9?eJjokNDXp@_bCwx5aPMh!k!$Zb)M|%MkUu^g?g2 zz`WN!yWVqLs$Tb~;37!}B&A2$_W<4Z|9?`r>P{eo4dhT}hUX&O#b;z6@Bu{_JYD@<);T3K F0RVn}Qm6m` literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-transform-isolate.png b/fixtures/web-first/chromium/svg-group-blend-normal-transform-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..feb9fb72740aeb3525fd33ca217dd3a7f0efa85e GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8$4YcLn`LHx$U@$F+s%P;pezb zjNJwkg0>|msNd+^xK~4!xq1Vma+pKVl5;#P5}25IXf|0ZfP6tOz2V>t1W znJ2Dt%T2#!e>PnGRPw}kEmZs~{{wEl!$&*#XUVU7oc{T5PHmk@HtPhKHc^J1ObX!) zPq-Rl7%>FDF#fZefkpHk(8>S*cc_WP0vT)|w=*;FZ06mxI@h8ID8k_B>gTe~DWM4f D4=h_S literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-transform-plain.png b/fixtures/web-first/chromium/svg-group-blend-normal-transform-plain.png new file mode 100644 index 0000000000000000000000000000000000000000..feb9fb72740aeb3525fd33ca217dd3a7f0efa85e GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8$4YcLn`LHx$U@$F+s%P;pezb zjNJwkg0>|msNd+^xK~4!xq1Vma+pKVl5;#P5}25IXf|0ZfP6tOz2V>t1W znJ2Dt%T2#!e>PnGRPw}kEmZs~{{wEl!$&*#XUVU7oc{T5PHmk@HtPhKHc^J1ObX!) zPq-Rl7%>FDF#fZefkpHk(8>S*cc_WP0vT)|w=*;FZ06mxI@h8ID8k_B>gTe~DWM4f D4=h_S literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-transparent.png b/fixtures/web-first/chromium/svg-group-blend-normal-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..95d2ecd2be1ce83c6724222cb0b6838355b7ed4d GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=+dW+zLn`LHxw(;($x(#iVqlK5 z?jf!tH_ja4T66Nql_O_v2(d(O5VronWN9;_^5y-E2ZuLVyXW=AFiy~BC}MTUGc0*f zd28;>4QGEAK4M=B5?aS_LX=@AlR`Md6Rw7s_l!Mnw%%DMaq(l>&HCB#{I7p+uhedU zX-8EHHuD#Qv0K@mz2E-?{%2wa8UPkte4lyQI#GzxmbDH1K$rjje?m0p8IZvS@&Yr% Y3kKevFPxkI14S4-UHx3vIVCg!01lL3$p8QV literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-unit-isolate.png b/fixtures/web-first/chromium/svg-group-blend-normal-unit-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..01e89078a53fd1ee72b447e9c7e05b9cf8696f93 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydu{YLn`LHx$Vf+7%1ZK(0LPM zx50#(`o1uu+A@9@M zgAaE^d^%os`}S1%Elejw8Fn%$T(@s%v|Tn^k~u1V-e>>MpR?-!o8+@XbgW~5s0n9y z!qosX21)P>quX6>EK1(-GBEuA|KYvuUZBI-Kn`bSIOfK?iE&c9Hc*7Y)78&qol`;+ E0M&I^kN^Mx literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-normal-unit-plain.png b/fixtures/web-first/chromium/svg-group-blend-normal-unit-plain.png new file mode 100644 index 0000000000000000000000000000000000000000..01e89078a53fd1ee72b447e9c7e05b9cf8696f93 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydu{YLn`LHx$Vf+7%1ZK(0LPM zx50#(`o1uu+A@9@M zgAaE^d^%os`}S1%Elejw8Fn%$T(@s%v|Tn^k~u1V-e>>MpR?-!o8+@XbgW~5s0n9y z!qosX21)P>quX6>EK1(-GBEuA|KYvuUZBI-Kn`bSIOfK?iE&c9Hc*7Y)78&qol`;+ E0M&I^kN^Mx literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-opacity-0.png b/fixtures/web-first/chromium/svg-group-blend-opacity-0.png new file mode 100644 index 0000000000000000000000000000000000000000..1a5a6126b8e09a5d185e1260a4378d2b07368688 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMSWg$nkcv5P&NFf{C@>r_5S)DT zsKGX~V}je*qJQ7{xY^Q9JwSK`=M|PLCey}Q4!#T2i08g#WMKIJ|GJq_IM5b0Br4#)207V!)UHx3vIVCg!0Fg;C_5c6? literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-opacity-1.png b/fixtures/web-first/chromium/svg-group-blend-opacity-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2be208d77aea81b7c16e4dd58a8d00ae1247ad GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMMV>B>Ar*7p+}z07U?9S9QGrXz zqG5N$&hEn&&98S%T+rG(b*?7Usfh*ad(7DPpY*pq>wO|p)F_?dEW<3uEmq$W?_YCx zaoqA{y?aL*Sj>bugY5#(f~nQ=9;Mrye;g^?EB8J2bJaVJi#!V?9gG`LR7pB$m+MdY z6(@cB`Jc0bi$LW-)I0evduq?<%u95BKLxfBxDgZl1N133CQS8%z;c z;NEU=uiEv!Z;R(WgBbC-_JPRl`}SuA7lDkMR(pVvf#LuEQm$pjKu5EI9L>z|f>9u0 Tv9xqMP=vwL)z4*}Q$iB})^u9U literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-child.png b/fixtures/web-first/chromium/svg-group-blend-opacity-opaque-fill-child.png new file mode 100644 index 0000000000000000000000000000000000000000..af3a85adefef9fefff3b3731d5930a63af182611 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMdQTU}kcv5PZZ70K9Kgd6xSC7* zozvZ=hfA(K_~0WT@a4MLsMGb?}p`?q=1b)9t#Yk@F#w^YqDiNyKe*Z%eM&~_jU ztKZH4@osMLsMGb?}p`?q=1b)9t#Yk@F#w^YqDiNyKe*Z%eM&~_jU ztKZH4@osMLsMGb?}p`?q=1b)9t#Yk@F#w^YqDiNyKe*Z%eM&~_jU ztKZH4@osMLsMGb?}p`?q=1b)9t#Yk@F#w^YqDiNyKe*Z%eM&~_jU ztKZH4@oskcv5PZf@jlFyLW0*f66f zR-$cXB2!n$@&)^^<;4{}f4t}2#!a<4>loGo;oBc_wuf&l+h(l@;t_&x&(}ZNS-L!klM9B8l)K*{{KI5CfW<=RyL4ZnHdi1a%p7-i9G^}FnGH9 KxvXPkpW?`kM=d8fVj?7{3$LA{q{io#mXNIDodq%o}Rs=aYdRp3^g z$FKW>iy&fW8D=q>FlYSV%fH0xmEN8td-?v`%W7=OTQb-#@GL-40}}XKu0Q2hob>JI zHD@6vJo|k>ciaB|vy@#xHt7Ct;AUX>|NrINWsiW4Wdk{unL$gK%eiR#!M#8c22WQ% Jmvv4FO#m!_S)u>{ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-opacity.png b/fixtures/web-first/chromium/svg-group-blend-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd05ac77469c0a53e37197cb8521707f200f950 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMm7Xq+Ar*7p+}z07Vj$u0@J?cJ zLGq49=?IHPZHpdl%l4(`5|TADQq#K{K6UI_-($xAPp!Ud*K6l<`xJ}WF7PanbkI(c zpLJe@<8YkX-}f9BK_bQtX$)r>W-)I0evduq?<%u95BKLxfBxDgZl1N133CQS8%z;c z;NEU=uiEv!Z;R(WgBbC-_JPRl`}SuA7lDkMR(pVvf#LuEQm$pjKu5EI9L>z|f>9u0 Tv9xqMP=vwL)z4*}Q$iB})^u9U literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-outer-opacity.png b/fixtures/web-first/chromium/svg-group-blend-outer-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..f6dd73bd8b49a2e52fa29af605a4d828438cb64e GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM#hxyXAr*7p++4`pVj$u0@V}nN zvK@C+I83|^c(e@}gcTCGZ8T&=eHHXo-%rjx>s@zwTh=c{uW!AFBpr+!(iqlyesfH?u#jJ$zZ#HtiXgBEU@=`tj72K z+xXg)T|fq&{ofe;>hHhIJ^`>HkC^#@uKfQ$S47GP$Y29GmzhDgk=Lkli_>?Y2!p4q KpUXO@geCy*1yln7 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-outer-root-multiply.png b/fixtures/web-first/chromium/svg-group-blend-outer-root-multiply.png new file mode 100644 index 0000000000000000000000000000000000000000..454bd9f9aafbb842b37bc2733452be5f084e1bc1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydl>XLn`LHxwes$DNuy8&ffl#x literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-outer-root-normal.png b/fixtures/web-first/chromium/svg-group-blend-outer-root-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..454bd9f9aafbb842b37bc2733452be5f084e1bc1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydl>XLn`LHxwes$DNuy8&ffl#x literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-outer-root-screen.png b/fixtures/web-first/chromium/svg-group-blend-outer-root-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..454bd9f9aafbb842b37bc2733452be5f084e1bc1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ydl>XLn`LHxwes$DNuy8&ffl#x literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-partial-isolated.png b/fixtures/web-first/chromium/svg-group-blend-partial-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..4917bf1130a99b75477e502d8c247cd6604e292a GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMK2I0Nkcv5PZf@jkaNu#cxWc?q zIdVs5-UES04BNu^9_BS^%gBAQ{IlNgv!bg)=8S7ABBiGqH>5F~Wthci!knR*p7DC! zk7GssfyYD_NjgAeZrSH8l=)k8jQh7>YK$US9oq$-1rX`AzZ#DD+n#}FM+k$=+rrMk m@c%#eqh~LHE@K0^jG4hbg2Q_C9r1HO5e83JKbLh*2~7a1k4oDB literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-partial.png b/fixtures/web-first/chromium/svg-group-blend-partial.png new file mode 100644 index 0000000000000000000000000000000000000000..4917bf1130a99b75477e502d8c247cd6604e292a GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMK2I0Nkcv5PZf@jkaNu#cxWc?q zIdVs5-UES04BNu^9_BS^%gBAQ{IlNgv!bg)=8S7ABBiGqH>5F~Wthci!knR*p7DC! zk7GssfyYD_NjgAeZrSH8l=)k8jQh7>YK$US9oq$-1rX`AzZ#DD+n#}FM+k$=+rrMk m@c%#eqh~LHE@K0^jG4hbg2Q_C9r1HO5e83JKbLh*2~7a1k4oDB literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-pattern-multiply.png b/fixtures/web-first/chromium/svg-group-blend-pattern-multiply.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf2326f8045172f19b8422cc941e9d5e9e8f1f1 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf)dkb`W52%$c}L z+5KH>QVjFk3xd28CJIj2P?f1x_19H9w&HXCPk-fV|7PI;5S+c9?f15M@6VT4zWJ}G z9Rd-=PfFW8cp9gnwg4HH0Uh!G|D}6+1waNH$TiFi%LE0Vm|Hw-1d1?ty85}Sb4q9e E09MCAMgRZ+ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-pattern-normal.png b/fixtures/web-first/chromium/svg-group-blend-pattern-normal.png new file mode 100644 index 0000000000000000000000000000000000000000..924a3fe9621b320921f1509381cb91cefdb2f71b GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMCQlc~kcv5PZf)dkb`W52%n6>! zT>eNXrp49$OPh!ofMOU-GR1%$V1%p&4mfBcs_|MtK2 zo19D;3B#=qndRQKCNX6pLu;TT{{O#L+ie46uz_5|%#glQ@QL1{6f2+zgQu&X%Q~lo FCICf2K+pgH literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-pattern-screen.png b/fixtures/web-first/chromium/svg-group-blend-pattern-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..58250ce16ad97dc52b267c73fe668110f95a7122 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM7Ec$)kcv5PZf)c|>>$A4cyk5+ z^rQlPw(XDjuJm~Ys-K#o8L;SSqFU9zMXlWV|7!kUJ~8j|1C}cw_-ymRPj#~I@9(<% zt$wQLD$W&zA+P+00(}wT0AzTZk%8g=f6EUGOn~lT1G$HpA*508$upPsSfB`lr>mdK II;Vst0IN4jPyhe` literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-root-isolate.png b/fixtures/web-first/chromium/svg-group-blend-root-isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..507ceeb41aad269f9f8a29c7a1cab11f92d5a2d6 GIT binary patch literal 215 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 U7oGT?3>0DTboFyt=akR{0QB@(=>Px# literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-scale.png b/fixtures/web-first/chromium/svg-group-blend-scale.png new file mode 100644 index 0000000000000000000000000000000000000000..6897e5ea16d9f7a604c3ac0cba99b00faa83f7e4 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMrJgR1Ar*7p++4`p;vnE~@g!^E zqjQ0LT&~I@&CZhoxSLw~G8p*Tn4Y9RwBPwoJ-JLx`?Xrox6UJy4#o{>47#`N&IZeW z{~=x)b5?K>MC>fXEJhRN4F5m;ZvwX_%X~juzk2$@*NlP7QhtW68`Q~ zy>91iUjCyH6TV&hecj2&P|9@A6brz7p268Yn!}V}(ovn81!9WoP MPgg&ebxsLQ0M>|9wEzGB literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-alpha.png b/fixtures/web-first/chromium/svg-group-blend-screen-alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..e10a3ff5c7406bd946105660dea528f976ddb529 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=2RvOILn`LHxwUbnlcNa3#fQnP z@-DtP4jb5i8;Edk;9*|9LE)mD$p%jA53HX8W>~(=Gbu>CdG};m`5onN?JA$6 z9rBE}JT$1y`I)%<`J*%JQjAZy8emfE7*2>X?0m;8akF;YuXLBoKa2k6{jJ;`bAPQl z6G(TSAtH5FJGMu6k+gm^>bP0l+XkKYzAdX literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-each.png b/fixtures/web-first/chromium/svg-group-blend-screen-each.png new file mode 100644 index 0000000000000000000000000000000000000000..c45fb774a754aa57329e9b39197ca3100992b8a7 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM<(@8%Ar*7p+}z07Y9Qip@x#Wy z#u3&n98uP7547?or0U2Vwbfbk#z!zfZ({YSjN*r;l`DT`a;pArJtXO1+>pkw)~mK7 zR+i)Qw)DU6IWF=nfQX)Dn8mo|{XO=gzqbu-5;OX4e>OdKyV$)WjR7QM!koc&0ipC( z`FE$C@3*e3J$GA15iGlpKPKq+@9maOV3l3`7K{uG|NnnZ7F`N-G8@Rr%nZJ5d~Cih REbKrL22WQ%mvv4FO#tszSQG#N literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-group.png b/fixtures/web-first/chromium/svg-group-blend-screen-group.png new file mode 100644 index 0000000000000000000000000000000000000000..cccee6ac6e04468ce85d0a25ac9e423e5c2b5dd2 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMC7v#hAr*7p+}z07U?9S9QQ=+U zTal(PKBXO9(kble8wy0sH*PX^b+8nAH}&GP2j)LBkEe30W;;FPSs>|P+;DA~-I0ad zEnoL+`d8kP!FB;6n#ORJA!_gU#+YjRc730}zkdooe!WxK#kc_^F^kcJ8A;{ZdwT=k zRp(~S|7`Bw0hWKy+<)cYzx+M{klHPenDc>$8!@c0V; z=}86pY}*&{UFq`*RA1%Lxai?gzpCjvF=eN}Z||GA_t+Dz3m`ac^}%nI{15lbZ~tbk zxR!x1e7m#$(eD3IONB2%41kc!_$?S282K7MHoC? L{an^LB{Ts5%H>Ob literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-opacity.png b/fixtures/web-first/chromium/svg-group-blend-screen-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..a211877c41b7bb97a9b66d46e0e81a161a0c4a0e GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMrJgR1Ar*7p+}z07U?9S9QGqWZ zS+`?y#O}7kJNlC&qPg-OaD>7gg)Fq$xDuw8g{qkUyI zQ=;wqCI4j=XFbRk(VO?;&^X@4C92+pl*jyBIg5F`Q*UQI*E9_Rijb zSJk;W^FN!rcYx*JGxy*4_b~~>WUzr8%*>Fvi+81x;#mct O2!p4qpUXO@geCx-q*lHF literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-stroke.png b/fixtures/web-first/chromium/svg-group-blend-screen-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..eb51db8a93cf3f08863d7d90bc7de2135d81c22a GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJM*`6+rAr*7p+}z07U?AXd(K+Ur z>6Rh}cY&CryqlVtBP`orTR3((Jed3=ezu7`AJg>0d*?H53iKK`q%oXjxHZE%qEDH_ z-^=y8tl}(26XuM+`OrCL73^%nVWwxsDpJ@7N9$VeoYIb6Mw< G&;$S~w@>r{ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-transparent.png b/fixtures/web-first/chromium/svg-group-blend-screen-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..95d2ecd2be1ce83c6724222cb0b6838355b7ed4d GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=+dW+zLn`LHxw(;($x(#iVqlK5 z?jf!tH_ja4T66Nql_O_v2(d(O5VronWN9;_^5y-E2ZuLVyXW=AFiy~BC}MTUGc0*f zd28;>4QGEAK4M=B5?aS_LX=@AlR`Md6Rw7s_l!Mnw%%DMaq(l>&HCB#{I7p+uhedU zX-8EHHuD#Qv0K@mz2E-?{%2wa8UPkte4lyQI#GzxmbDH1K$rjje?m0p8IZvS@&Yr% Y3kKevFPxkI14S4-UHx3vIVCg!01lL3$p8QV literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-translate-isolated.png b/fixtures/web-first/chromium/svg-group-blend-translate-isolated.png new file mode 100644 index 0000000000000000000000000000000000000000..5d295077209c3ef8cb36faa0f25b601c1a9189f1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMHJ&bxAr*7p+}zLE>>%OrQMs_G zBvReg%+u_Iz(Lz1q9qd1;Tr^X*8J&i`eaeL|I?Pr52oK^KmGdSd?rt^i0uN;0!ats z2C-U)P5&(SPH+5>eP8;2NlOMyB8}lJ!>z0DnX7I0MOqx%oRxO|wUYc?3nvq16eY75 z!2;{{iQoDccRTvl^Et-_7l90Z_WQtvlKOfx_l`7%wag3*|NnEz)!YHPn+@b{W(KZh W{QuW&h@1r!VeoYIb6Mw<&;$SpYF~r^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-translate.png b/fixtures/web-first/chromium/svg-group-blend-translate.png new file mode 100644 index 0000000000000000000000000000000000000000..3930d1752a11e3b04cd0f49f9e4aeea66f93c696 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGJMRh}-6Ar*7p+}z9A;vmBC@TtL$ z2YS1U^o%wMb{iUttU2niL0Q_s;FCCOYQ#+a7+9 z>#*tn&iAhze*F8~TyN!M!kht diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index 377c2b73..a2f94332 100644 --- a/fixtures/web-first/oracle-bake.json +++ b/fixtures/web-first/oracle-bake.json @@ -5,7 +5,7 @@ "bake_script_sha256": "2bdb5f933d072a1e87c9a675c3342fcf506c0955c0f5c26e2988f4e8fa37c4f2", "capture_module_sha256": "069296201718c43d29efe356fbea893781b73250dca51de3b6d47468d74027b0", "suite": "primitives.json", - "suite_sha256": "9cc3c4ad2a2e4e31483b2895e4458618189361e767c55a0b788605d8090ec468", + "suite_sha256": "30c19fdf4727f3ba6042b795fe897ba7477a113ad632e1dc1fa252e0fbaa1697", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -37,6 +37,15 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-group-blend-isolated", + "source": "html-inline-svg-group-blend-isolated.html", + "source_sha256": "f47e08650d407423c388a452c92cfc6ed36afeb8f7153d2070684605afa8239a", + "oracle": "chromium/html-inline-svg-group-blend-isolated.png", + "oracle_sha256": "5e59532df1bb80913c4ebb6998b6c62fb295bfe34eda57b23c84d94676dfc2e4", + "width": 64, + "height": 64 + }, { "id": "html-inline-svg-nested-viewport", "source": "html-inline-svg-nested-viewport.html", @@ -5131,6 +5140,843 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-alpha-half-child", + "source": "svg-group-blend-alpha-half-child.svg", + "source_sha256": "0e8f90a7ab8356d28ba53bca7e78c0f06ffb31f097b28d51e1f03f13b9624910", + "oracle": "chromium/svg-group-blend-alpha-half-child.png", + "oracle_sha256": "0064e97502d2b608b5fcea684d89267368c0e4828a3494a7492c7bf39897a622", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-parent", + "source": "svg-group-blend-alpha-half-parent.svg", + "source_sha256": "09be35c3962cb3f575c6a29ed2f55a53587220476ac851c4722eecb96781f36e", + "oracle": "chromium/svg-group-blend-alpha-half-parent.png", + "oracle_sha256": "0064e97502d2b608b5fcea684d89267368c0e4828a3494a7492c7bf39897a622", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-plain", + "source": "svg-group-blend-alpha-half-plain.svg", + "source_sha256": "69c551f1248853186c1280aa4d11845de9c5daefad5e13a558421af0b0ae4e90", + "oracle": "chromium/svg-group-blend-alpha-half-plain.png", + "oracle_sha256": "0064e97502d2b608b5fcea684d89267368c0e4828a3494a7492c7bf39897a622", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-shape", + "source": "svg-group-blend-alpha-half-shape.svg", + "source_sha256": "8f05b1af0c570a54262ff99afebb15215e536ac083c590fcb2079c61de75923a", + "oracle": "chromium/svg-group-blend-alpha-half-shape.png", + "oracle_sha256": "0064e97502d2b608b5fcea684d89267368c0e4828a3494a7492c7bf39897a622", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-child", + "source": "svg-group-blend-alpha-six-tenths-child.svg", + "source_sha256": "889b8ae10075620af293bb5b2e03967419d25256b59a0fbbdaefacc796c69fde", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-child.png", + "oracle_sha256": "37201098af6a9e71f3a0bacc9720d4f39788c1803636d8a3c645bd6381e5cb57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-parent", + "source": "svg-group-blend-alpha-six-tenths-parent.svg", + "source_sha256": "d31953a8817ab6c0110527a14e0f28bfd77516f9f3500c643f5f017648622342", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-parent.png", + "oracle_sha256": "37201098af6a9e71f3a0bacc9720d4f39788c1803636d8a3c645bd6381e5cb57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-plain", + "source": "svg-group-blend-alpha-six-tenths-plain.svg", + "source_sha256": "8920bad73fe29d6d498cb4b1ebdd3d5bb393e37d2048c5a6c8d19d5ba4b491b9", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-plain.png", + "oracle_sha256": "37201098af6a9e71f3a0bacc9720d4f39788c1803636d8a3c645bd6381e5cb57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-shape", + "source": "svg-group-blend-alpha-six-tenths-shape.svg", + "source_sha256": "a2079c2260c00bb13fdd612a15f4448c6f20089e45a3c2b01b9382672e444ae3", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-shape.png", + "oracle_sha256": "37201098af6a9e71f3a0bacc9720d4f39788c1803636d8a3c645bd6381e5cb57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-anchor", + "source": "svg-group-blend-anchor.svg", + "source_sha256": "a3634e6c9c7093f65ea75e5494271549256c36fb98254cdc167c4c02f0308827", + "oracle": "chromium/svg-group-blend-anchor.png", + "oracle_sha256": "d360e0365deba1a2a7cde5c3964d8ce4fa4d39df3c364cebd7cb94faddcf95b7", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-attr-multiply", + "source": "svg-group-blend-attr-multiply.svg", + "source_sha256": "4cf595420040a12691a90b3d792b2e21989437f9c73b697581da8e38030ae9a5", + "oracle": "chromium/svg-group-blend-attr-multiply.png", + "oracle_sha256": "0cdb8029e01ea5cf2371a107cef5b04e9d03fcffb474e612fcba898a50ffc6fc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-case-comment", + "source": "svg-group-blend-case-comment.svg", + "source_sha256": "982334ffeb273176d63f0bc81bf2437693f6348d9847d1c8785eefbd57e5dcb2", + "oracle": "chromium/svg-group-blend-case-comment.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-clip", + "source": "svg-group-blend-clip.svg", + "source_sha256": "a92b54c5019df4ba3d90abe5090c59071ccd0401bf03060822a0092c7807f83f", + "oracle": "chromium/svg-group-blend-clip.png", + "oracle_sha256": "7b072b98ed4a236b280394a73d336e2878fa854073c2e41eb276041018a4388c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-clip-isolated", + "source": "svg-group-blend-clip-isolated.svg", + "source_sha256": "f70e95a86d8c2372a4a8c52d35ab049345b4289cc54184a5a7bea302c1320240", + "oracle": "chromium/svg-group-blend-clip-isolated.png", + "oracle_sha256": "7b072b98ed4a236b280394a73d336e2878fa854073c2e41eb276041018a4388c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-invalid", + "source": "svg-group-blend-css-invalid.svg", + "source_sha256": "aa54cb79251a96c0182f41e790305e67db30722ee601f26f5f70971af35251fd", + "oracle": "chromium/svg-group-blend-css-invalid.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-keyword", + "source": "svg-group-blend-css-keyword.svg", + "source_sha256": "0a95fd018a04d15db3347a2652e550120ab037effe6674b4c960989289fd760b", + "oracle": "chromium/svg-group-blend-css-keyword.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-var", + "source": "svg-group-blend-css-var.svg", + "source_sha256": "fc6168cd6290e2cdaaa0103a20052b7ed6179ea62b96d1a5bd0b24b032e79ed6", + "oracle": "chromium/svg-group-blend-css-var.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-winner", + "source": "svg-group-blend-css-winner.svg", + "source_sha256": "d9152fbfc7f7399d29cff07b8079f388c9bb48e692eb673d4abc65ed04e07894", + "oracle": "chromium/svg-group-blend-css-winner.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-multiply", + "source": "svg-group-blend-fractional-multiply.svg", + "source_sha256": "57ba444f4eb9e7005d0b546b8d7b768b6f27c1f7b04cdcedf28a7ed71c0969d2", + "oracle": "chromium/svg-group-blend-fractional-multiply.png", + "oracle_sha256": "be743b053e34cd2284c6887e75e67817066caeff563d5171ae7074b500b081fc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-normal", + "source": "svg-group-blend-fractional-normal.svg", + "source_sha256": "9f6063e2d695659d4850a5792cd6de3515189bbfa4ef211b4a4f51d5cbdb3f27", + "oracle": "chromium/svg-group-blend-fractional-normal.png", + "oracle_sha256": "03a09feb9fa2902e7153f5463d627641dd829a2aedf81b65c1b5ca6d4092f95b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-screen", + "source": "svg-group-blend-fractional-screen.svg", + "source_sha256": "c728b7f97dce6d88191fc6dbac20121f804ec5ce6a32e48a5eab452609be96b0", + "oracle": "chromium/svg-group-blend-fractional-screen.png", + "oracle_sha256": "ac5313b7eae83edf12b3df20037f234c94fc9e5134be19269d897c1d98d09ac5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-group-attr-isolate", + "source": "svg-group-blend-group-attr-isolate.svg", + "source_sha256": "f9a5a5d18b6f4340d94c9f9e98359615b93e74c6eba11f99de29f2fa3000981c", + "oracle": "chromium/svg-group-blend-group-attr-isolate.png", + "oracle_sha256": "5515347c495f699e7c072602f4f8ab0eaa34178a0f04dc22c0dbfd64b5f7643b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-instance-direct", + "source": "svg-group-blend-instance-direct.svg", + "source_sha256": "4b03df0d5684d87a36678fe46a06bf3bb9f2576c275208687766cdee528b6dab", + "oracle": "chromium/svg-group-blend-instance-direct.png", + "oracle_sha256": "d360e0365deba1a2a7cde5c3964d8ce4fa4d39df3c364cebd7cb94faddcf95b7", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-instance-use", + "source": "svg-group-blend-instance-use.svg", + "source_sha256": "ceb7691d506a7310d4d0cba79a1b923b173f8d475c0c04132f13f75fdec9c624", + "oracle": "chromium/svg-group-blend-instance-use.png", + "oracle_sha256": "d360e0365deba1a2a7cde5c3964d8ce4fa4d39df3c364cebd7cb94faddcf95b7", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-isolated", + "source": "svg-group-blend-isolated.svg", + "source_sha256": "ed60268e2e40a26deb4f3e932cfb258133f86aa1b0bfc1ed821c2b2f6b50a90a", + "oracle": "chromium/svg-group-blend-isolated.png", + "oracle_sha256": "7b072b98ed4a236b280394a73d336e2878fa854073c2e41eb276041018a4388c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-multiply", + "source": "svg-group-blend-linear-multiply.svg", + "source_sha256": "ed1a1a58d6d92ce69a4b11b0700ff299582307f6447b0914f3c58a9da5e3db82", + "oracle": "chromium/svg-group-blend-linear-multiply.png", + "oracle_sha256": "79ea041f166e4e7324546ad75453502367ec13683dea02035eea076df8058f2e", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-normal", + "source": "svg-group-blend-linear-normal.svg", + "source_sha256": "90e3b1c2b8d22df501c61c81f8f2371cfdffa8462adb70b799c69869e1cf484c", + "oracle": "chromium/svg-group-blend-linear-normal.png", + "oracle_sha256": "9ea6cff9f65c7a4ed971234df6189ac73460b7264f5136ac89ca7f92b4d9255c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-screen", + "source": "svg-group-blend-linear-screen.svg", + "source_sha256": "522918e5fe3ab13d77cc83d572d748f09719cbb64a78457be86b37e9b78aeb17", + "oracle": "chromium/svg-group-blend-linear-screen.png", + "oracle_sha256": "0e86ae3e23fb31de9a2a80873fe005ad836f2aee9af1457a91573208520991c1", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-mapped-direct", + "source": "svg-group-blend-mapped-direct.svg", + "source_sha256": "2c0cbf57abd230d1110e3abc5713d534aca80a5e5c6a296499293ab2078ab22b", + "oracle": "chromium/svg-group-blend-mapped-direct.png", + "oracle_sha256": "6093674da14c781fe3dbd289d1bcf5d4137ef37469865f9402d68701c0b6e41b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-mapped-viewport", + "source": "svg-group-blend-mapped-viewport.svg", + "source_sha256": "7822fc089285d29b8945fb204618b6bc6967595cb9eddd5a6b86bd74a53aff5a", + "oracle": "chromium/svg-group-blend-mapped-viewport.png", + "oracle_sha256": "6093674da14c781fe3dbd289d1bcf5d4137ef37469865f9402d68701c0b6e41b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-alpha", + "source": "svg-group-blend-multiply-alpha.svg", + "source_sha256": "9d75dde4184e07571ead4e94089a49e6552ec1a72e0cf60b282ba8b15d33f2bc", + "oracle": "chromium/svg-group-blend-multiply-alpha.png", + "oracle_sha256": "b07fc2a0ff22d5588497c6c4d1ccc430942f66e8481a5047dc94b91943d0aa7e", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-each", + "source": "svg-group-blend-multiply-each.svg", + "source_sha256": "6be5c50d5649774107f282801fa48677c653a15776829c673198b9b1e25641e4", + "oracle": "chromium/svg-group-blend-multiply-each.png", + "oracle_sha256": "5a8e40e4bda3e1ef3b8632f01f952a9be85034276238b6b6b5a3db8c794a7e04", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-group", + "source": "svg-group-blend-multiply-group.svg", + "source_sha256": "74257582caae30bc1cec0838de3864c201e6f0b30076b074292e78740fee4db6", + "oracle": "chromium/svg-group-blend-multiply-group.png", + "oracle_sha256": "6ab8e06dcd76adcc2da46b4040318ec71217e1221e76e3079143648db44dbc07", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-leaf", + "source": "svg-group-blend-multiply-leaf.svg", + "source_sha256": "05c55e0f778e06ba79e1ccfad6a311a49c7d1e552f24fac8284781732412e1fa", + "oracle": "chromium/svg-group-blend-multiply-leaf.png", + "oracle_sha256": "5515347c495f699e7c072602f4f8ab0eaa34178a0f04dc22c0dbfd64b5f7643b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-opacity", + "source": "svg-group-blend-multiply-opacity.svg", + "source_sha256": "0a256736b20f89d6aba819cfcf27357e74caceb0fefa604e12c7a30871e1cb55", + "oracle": "chromium/svg-group-blend-multiply-opacity.png", + "oracle_sha256": "1bd2218479722f15e2b264e314fb0a9fbb429dff1915d2f39ea3dde0d548b4bf", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-stroke", + "source": "svg-group-blend-multiply-stroke.svg", + "source_sha256": "146ef0867681a9d2ca22d5851a245350892cde01875a3cbdfb779be919d4a9c1", + "oracle": "chromium/svg-group-blend-multiply-stroke.png", + "oracle_sha256": "992fc0575d9111d74494a4c357b87927694fa5325379a5178a32563e3feeeb0e", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-transparent", + "source": "svg-group-blend-multiply-transparent.svg", + "source_sha256": "ab3419aa0ecf48d1d0ea1404677997de46bd5903f3b1194db55253d682edfde1", + "oracle": "chromium/svg-group-blend-multiply-transparent.png", + "oracle_sha256": "a36c2a26bfa284913592c9f2903cf39c76496e09e2f5eb3c0b06e31b9ba70211", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-nested-hidden", + "source": "svg-group-blend-nested-hidden.svg", + "source_sha256": "0d55f821d90d7cbe01e5a00a5b5bf93c9825c228e222a7bbecfb56977a569558", + "oracle": "chromium/svg-group-blend-nested-hidden.png", + "oracle_sha256": "20e0cb039f0c4d4328b4f88a18b7ee429408467de692cf34a56f89637ec26dd8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-nested-hidden-isolated", + "source": "svg-group-blend-nested-hidden-isolated.svg", + "source_sha256": "f833ba059f2966df55ad1f16cb35771d2d68e5411afba1497b41f65393140430", + "oracle": "chromium/svg-group-blend-nested-hidden-isolated.png", + "oracle_sha256": "4f750c3565ac06374cf220c35156d762714905ed11c0ec517b4e588dbe8593c0", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-neutral", + "source": "svg-group-blend-neutral.svg", + "source_sha256": "e2eba72cd684eb9c0f201936567f59cca4d536cba3d1d78fee66604281c7e626", + "oracle": "chromium/svg-group-blend-neutral.png", + "oracle_sha256": "5e59532df1bb80913c4ebb6998b6c62fb295bfe34eda57b23c84d94676dfc2e4", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-alpha", + "source": "svg-group-blend-normal-alpha.svg", + "source_sha256": "3959bfaf133815abe55120ab88c5ed62df25a808a5ebdf791c7c7660695de04a", + "oracle": "chromium/svg-group-blend-normal-alpha.png", + "oracle_sha256": "63fe84b7011ea86b0377cb85366cd43f23cc92b51d29095c5c560c6ef0c430d3", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-clip-isolate", + "source": "svg-group-blend-normal-clip-isolate.svg", + "source_sha256": "108f4e55c7753c91a95170d888e4d96454b762f250b25849384a604add9c241d", + "oracle": "chromium/svg-group-blend-normal-clip-isolate.png", + "oracle_sha256": "bd8d68b0feb68dd13088acb2367a283242511014b8816fd8f8f5a7c3a6f76fd2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-clip-plain", + "source": "svg-group-blend-normal-clip-plain.svg", + "source_sha256": "65d66f34a2d8f9cd6c7e325f37bedad66bbd07c0069010565c9f2775b6be6b6b", + "oracle": "chromium/svg-group-blend-normal-clip-plain.png", + "oracle_sha256": "bd8d68b0feb68dd13088acb2367a283242511014b8816fd8f8f5a7c3a6f76fd2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-group", + "source": "svg-group-blend-normal-group.svg", + "source_sha256": "4ec40dc4812396d32256966fb594c41810c660d26bcaa2632bd15893906e40c9", + "oracle": "chromium/svg-group-blend-normal-group.png", + "oracle_sha256": "20a7bc4aa44feb90ea8e7059053a78677652aad48525b4fdf3c398a1c86ef3b4", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-leaf", + "source": "svg-group-blend-normal-leaf.svg", + "source_sha256": "1cff30b2b7f6f310219b8863589ece6b03cadab5389cf30e89911783b3a442a5", + "oracle": "chromium/svg-group-blend-normal-leaf.png", + "oracle_sha256": "0cdb8029e01ea5cf2371a107cef5b04e9d03fcffb474e612fcba898a50ffc6fc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-many-opacity-isolate", + "source": "svg-group-blend-normal-many-opacity-isolate.svg", + "source_sha256": "a90d4170e8367e7656a1a366aff86146379af54cbe80a8e170906be995cbc3e4", + "oracle": "chromium/svg-group-blend-normal-many-opacity-isolate.png", + "oracle_sha256": "1c1368472dd4321eb803805002b4b6db3373764b51521176feb4d92db08f0b39", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-many-opacity-plain", + "source": "svg-group-blend-normal-many-opacity-plain.svg", + "source_sha256": "83b24a4b8fae9c7cccb0b4231993ff5235d197ce26711e8e31594185f4cc1a6f", + "oracle": "chromium/svg-group-blend-normal-many-opacity-plain.png", + "oracle_sha256": "1c1368472dd4321eb803805002b4b6db3373764b51521176feb4d92db08f0b39", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-opacity", + "source": "svg-group-blend-normal-opacity.svg", + "source_sha256": "b31206adf82203bd18ffebdd892dfd3e2d240ef8884fe6fdf3ba2d2ad6943c83", + "oracle": "chromium/svg-group-blend-normal-opacity.png", + "oracle_sha256": "11af750a0de4f532dfafe4deeb4b80f006833e5daa20e166de0268c01b8e84d3", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-stroke", + "source": "svg-group-blend-normal-stroke.svg", + "source_sha256": "416fd0f382e942f31aae69a69de686803791d7c642a9882a5624bff3195b8d38", + "oracle": "chromium/svg-group-blend-normal-stroke.png", + "oracle_sha256": "41429090b99a4d95de7c5bbce830f3af1e9689d931ce27d86ddc723a28af3792", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transform-isolate", + "source": "svg-group-blend-normal-transform-isolate.svg", + "source_sha256": "204c3a27b6b6f8b9b27b14ea98f0a688b556b909f3d4caaddf253173a6a6714f", + "oracle": "chromium/svg-group-blend-normal-transform-isolate.png", + "oracle_sha256": "ed7685e33753a7768cf160fe64229b77d69f6f52efd1c2b6a68c6487cad4fed5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transform-plain", + "source": "svg-group-blend-normal-transform-plain.svg", + "source_sha256": "7b4babce789762f422d3ced13872cf883c17a5d97936d6e1a8df3eeb67e6ebc9", + "oracle": "chromium/svg-group-blend-normal-transform-plain.png", + "oracle_sha256": "ed7685e33753a7768cf160fe64229b77d69f6f52efd1c2b6a68c6487cad4fed5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transparent", + "source": "svg-group-blend-normal-transparent.svg", + "source_sha256": "466ad4ae8a07fad36e4d114571e9157fd2de0e4f988ac33e5ec3d45a17f0f738", + "oracle": "chromium/svg-group-blend-normal-transparent.png", + "oracle_sha256": "a36c2a26bfa284913592c9f2903cf39c76496e09e2f5eb3c0b06e31b9ba70211", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-unit-isolate", + "source": "svg-group-blend-normal-unit-isolate.svg", + "source_sha256": "3508f57220153b5c60cb6eb5525924740e347cb148a70ac0407c7cddc1707986", + "oracle": "chromium/svg-group-blend-normal-unit-isolate.png", + "oracle_sha256": "bd8d68b0feb68dd13088acb2367a283242511014b8816fd8f8f5a7c3a6f76fd2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-unit-plain", + "source": "svg-group-blend-normal-unit-plain.svg", + "source_sha256": "275110c584c5ad6626e4b819f5adfb9b87ced63649908a8441d8302b90516ff0", + "oracle": "chromium/svg-group-blend-normal-unit-plain.png", + "oracle_sha256": "bd8d68b0feb68dd13088acb2367a283242511014b8816fd8f8f5a7c3a6f76fd2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity", + "source": "svg-group-blend-opacity.svg", + "source_sha256": "76a92d6eb7f5932a616a985db5fde8556972eafc7615c6bc3d5be85abfbfa8ee", + "oracle": "chromium/svg-group-blend-opacity.png", + "oracle_sha256": "d97d8066a6ce75e701c9f20ae3e6577068487f99863ee58f7657b5ad8f42e11f", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-0", + "source": "svg-group-blend-opacity-0.svg", + "source_sha256": "33b94c4481a4d9229bb32ddb3c44ea00bc7e2c78c6edaa6d3438a51262a6010f", + "oracle": "chromium/svg-group-blend-opacity-0.png", + "oracle_sha256": "4f35a6bbd50a8b9d04eca38be21c4a52f2a0bcc0e3040377f8ed954fc8295877", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-1", + "source": "svg-group-blend-opacity-1.svg", + "source_sha256": "1f1217a3b8df3694d702bf257a23b99a0613e351deb52bae8d4758a20824b1a4", + "oracle": "chromium/svg-group-blend-opacity-1.png", + "oracle_sha256": "2b7ba824b3f52347ea948f83db1d092da6aecc83514f00e6cff07ab7bc35dec2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-isolated", + "source": "svg-group-blend-opacity-isolated.svg", + "source_sha256": "f997e08c6b219f82e0f6413a811bb44aae03ebc0b42c8e29bc64879a5cc38a1c", + "oracle": "chromium/svg-group-blend-opacity-isolated.png", + "oracle_sha256": "d97d8066a6ce75e701c9f20ae3e6577068487f99863ee58f7657b5ad8f42e11f", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-child", + "source": "svg-group-blend-opacity-opaque-fill-child.svg", + "source_sha256": "10a404506ea4223e29b5cb6b83e83c09d36c2cfd6a58490369f6c81180e9606e", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-child.png", + "oracle_sha256": "f94ffe454f89d39893ef02f15fc7be985c303246a70914ba9602898c120ac064", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-default", + "source": "svg-group-blend-opacity-opaque-fill-default.svg", + "source_sha256": "fbfc9ad964bc10d059d8daacf7d0ffdf481745ba819f0f040f691c1242caa9e2", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-default.png", + "oracle_sha256": "f94ffe454f89d39893ef02f15fc7be985c303246a70914ba9602898c120ac064", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-parent", + "source": "svg-group-blend-opacity-opaque-fill-parent.svg", + "source_sha256": "01d058727ca869a66c94f628d691bc04d83aee1233fc2f54edf5e293190b959f", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-parent.png", + "oracle_sha256": "f94ffe454f89d39893ef02f15fc7be985c303246a70914ba9602898c120ac064", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-shape", + "source": "svg-group-blend-opacity-opaque-fill-shape.svg", + "source_sha256": "259ef2201743943de1509ef090f293b2cdda6cd3844adf4bb6360e012149e5cf", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-shape.png", + "oracle_sha256": "f94ffe454f89d39893ef02f15fc7be985c303246a70914ba9602898c120ac064", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-child", + "source": "svg-group-blend-opacity-opaque-fillstroke-child.svg", + "source_sha256": "6b7a8e6a9b299c0f5e6ce82cdf8c57fa457fda60829f2e20e2231a30a6a9c5d5", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-child.png", + "oracle_sha256": "e800cb9c82642caaa9528bc04a5613e8c4c04f9bacf10256c1bc1aa2b5ba6895", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-default", + "source": "svg-group-blend-opacity-opaque-fillstroke-default.svg", + "source_sha256": "61a5415c1a4e59b9cf104ac10f8c1edf749af5d65898f287176a6a5446e9d848", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-default.png", + "oracle_sha256": "e800cb9c82642caaa9528bc04a5613e8c4c04f9bacf10256c1bc1aa2b5ba6895", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-parent", + "source": "svg-group-blend-opacity-opaque-fillstroke-parent.svg", + "source_sha256": "a14d4144348c6a91d4fdc20797d90eda60e3c3b256bb6e896877ddac1fa7a327", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-parent.png", + "oracle_sha256": "e800cb9c82642caaa9528bc04a5613e8c4c04f9bacf10256c1bc1aa2b5ba6895", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-shape", + "source": "svg-group-blend-opacity-opaque-fillstroke-shape.svg", + "source_sha256": "f7069d11198e484d28a6dbaccf000d1212739b69de9173042a8aa6b5bd73a065", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-shape.png", + "oracle_sha256": "e800cb9c82642caaa9528bc04a5613e8c4c04f9bacf10256c1bc1aa2b5ba6895", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p00392156862745098", + "source": "svg-group-blend-opacity-p00392156862745098.svg", + "source_sha256": "8fb6592cccc9d008a410b3ecb7758293fa9dae1fb2776eccd31115c9b165e92f", + "oracle": "chromium/svg-group-blend-opacity-p00392156862745098.png", + "oracle_sha256": "4468feba489c7759231411e1fb42512257802c5563147287260585a65d156435", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p5019607843137255", + "source": "svg-group-blend-opacity-p5019607843137255.svg", + "source_sha256": "8bf8caa841b2e51eac82397eb799a0dd3085437f75525eb5ebdd192b25b89e00", + "oracle": "chromium/svg-group-blend-opacity-p5019607843137255.png", + "oracle_sha256": "f92e9e97fd66f78f6fed11fe3759d6c9a1b494751ed8c387f72398469d051365", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p6", + "source": "svg-group-blend-opacity-p6.svg", + "source_sha256": "4e0d599de32639e440c26ad4802e42cb461fbc2f2418acaafa56d4d9b05fee93", + "oracle": "chromium/svg-group-blend-opacity-p6.png", + "oracle_sha256": "03f99134ccec0cd67f3b1c66b10b46a4cedc48e9d211ac45fe2d35c8af3a9ccc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-opacity", + "source": "svg-group-blend-outer-opacity.svg", + "source_sha256": "20e5b3b6d20c1940c991f530c59bae8f5f5e944576f35dda80fcddf1c178e493", + "oracle": "chromium/svg-group-blend-outer-opacity.png", + "oracle_sha256": "11af750a0de4f532dfafe4deeb4b80f006833e5daa20e166de0268c01b8e84d3", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-multiply", + "source": "svg-group-blend-outer-root-multiply.svg", + "source_sha256": "2d509dcbea6dd61cf7942a8b05a92527d6084e6f9ad4d36bcd61a31b5881950c", + "oracle": "chromium/svg-group-blend-outer-root-multiply.png", + "oracle_sha256": "1e386dabcff2c0a27c5708ecde62dc0f04444603aeb1ad4ab0de6b68433e0d4c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-normal", + "source": "svg-group-blend-outer-root-normal.svg", + "source_sha256": "0cb537461ea5da4c2cd15ecb5f60e761b6046edeeb7f73931c16b387bfada1f7", + "oracle": "chromium/svg-group-blend-outer-root-normal.png", + "oracle_sha256": "1e386dabcff2c0a27c5708ecde62dc0f04444603aeb1ad4ab0de6b68433e0d4c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-screen", + "source": "svg-group-blend-outer-root-screen.svg", + "source_sha256": "0b080be182218b906f72c846cff2fed43c5e7251476d7bf7c635cc594d3def18", + "oracle": "chromium/svg-group-blend-outer-root-screen.png", + "oracle_sha256": "1e386dabcff2c0a27c5708ecde62dc0f04444603aeb1ad4ab0de6b68433e0d4c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-partial", + "source": "svg-group-blend-partial.svg", + "source_sha256": "e9148260bb6b8f02b596fbbd6f3785888c67d180f5fa201adb4064d8faad0fc6", + "oracle": "chromium/svg-group-blend-partial.png", + "oracle_sha256": "e39e713aeb4f42d9e9333bd1459f8d5c7ce21dc2f95803c906c76c2a5ee3ffc8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-partial-isolated", + "source": "svg-group-blend-partial-isolated.svg", + "source_sha256": "e5c2585c4b31dbdd0491c559a518a9062891fd09ebaeda850dee73c05514eefc", + "oracle": "chromium/svg-group-blend-partial-isolated.png", + "oracle_sha256": "e39e713aeb4f42d9e9333bd1459f8d5c7ce21dc2f95803c906c76c2a5ee3ffc8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-multiply", + "source": "svg-group-blend-pattern-multiply.svg", + "source_sha256": "8b21ab4bace343600ae7af70cc530cb68bf188088c86b939f81642ddcea81649", + "oracle": "chromium/svg-group-blend-pattern-multiply.png", + "oracle_sha256": "f223311ef466b9c99fc3bf77e474dc2550ff3c8e2cc2c0a4777539c622e92500", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-normal", + "source": "svg-group-blend-pattern-normal.svg", + "source_sha256": "56c16a7733dd9e550ad23bf1ec09bd0cff0980cfc06b1714417b1b7335fe4ebe", + "oracle": "chromium/svg-group-blend-pattern-normal.png", + "oracle_sha256": "28b95ececee897051226883a3bd0a06d345467396feb71fcdb94be52b389bfbc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-screen", + "source": "svg-group-blend-pattern-screen.svg", + "source_sha256": "69bd7c6d84e20518f02397c579e4834ebfd54d9fbe86fc257a3c2d8f954511dc", + "oracle": "chromium/svg-group-blend-pattern-screen.png", + "oracle_sha256": "7769a69eddea993ea93db61f19e0d43a96c9d529a733fc265ab616da1563a925", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-isolate", + "source": "svg-group-blend-root-isolate.svg", + "source_sha256": "2a8b2d3e0ffdfeec060bb44f98e406eb6b0218607735a1b201688b4b76403598", + "oracle": "chromium/svg-group-blend-root-isolate.png", + "oracle_sha256": "5769b4dfb16d44706feb9549d18affc9c6f68a36c2f69051fd746c59a1ebebdc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-multiply", + "source": "svg-group-blend-root-multiply.svg", + "source_sha256": "05d35a3cab2c6fa39b50f73f1a821a51ec6559f31ad6d721dd24f22a302c1305", + "oracle": "chromium/svg-group-blend-root-multiply.png", + "oracle_sha256": "5769b4dfb16d44706feb9549d18affc9c6f68a36c2f69051fd746c59a1ebebdc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-normal", + "source": "svg-group-blend-root-normal.svg", + "source_sha256": "c197cb2cb42d8da6bd725b5ba57172908fb8d45b539a4429cdf05d619ff2ead0", + "oracle": "chromium/svg-group-blend-root-normal.png", + "oracle_sha256": "5769b4dfb16d44706feb9549d18affc9c6f68a36c2f69051fd746c59a1ebebdc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-rotate", + "source": "svg-group-blend-rotate.svg", + "source_sha256": "497ca6e7a4ecf440c3c5575156ea08d2ac603f99f90b7c9f770a0bd41087e9a6", + "oracle": "chromium/svg-group-blend-rotate.png", + "oracle_sha256": "a6993fc6207de91739ad7e010d0d89ea2b69de222ce6470a9660ef278ac907ac", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-rotate-isolated", + "source": "svg-group-blend-rotate-isolated.svg", + "source_sha256": "67a21534d38949bd943841a6688238327a69f34d5675c66bde7bc6c5dadb9fef", + "oracle": "chromium/svg-group-blend-rotate-isolated.png", + "oracle_sha256": "ae1f25fdb182714d59ff23ce6d69bfdcfcc45edd3d1cc92e1a00bc1240404474", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-scale", + "source": "svg-group-blend-scale.svg", + "source_sha256": "5a3bab5a04278adaf2863771bae1113e3a4d17ff7f4569e56fae1b5e24b3e9ba", + "oracle": "chromium/svg-group-blend-scale.png", + "oracle_sha256": "dd10710aaa7f119f098c13a8303950a3597d7a25bd5abf0f3d89c91e1e450f6c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-scale-isolated", + "source": "svg-group-blend-scale-isolated.svg", + "source_sha256": "017d1b9dc9a413e918a1966c33bde49352c41b71aa5074ed63de518d5339914a", + "oracle": "chromium/svg-group-blend-scale-isolated.png", + "oracle_sha256": "13f144ee3e857d5e9d0b77163fdd06e1b56bc95e3029bb873a1bb79c3ebeff43", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-alpha", + "source": "svg-group-blend-screen-alpha.svg", + "source_sha256": "eddf0b9d5cc3e2826a6817c8300197e2c0a34771c27e41dad182cda97b32a264", + "oracle": "chromium/svg-group-blend-screen-alpha.png", + "oracle_sha256": "76400432c0273673006e009168d054983f206eb7b9ab0f85d5b54f3304a61d8f", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-each", + "source": "svg-group-blend-screen-each.svg", + "source_sha256": "e9f768648b18fb32cc4f18999b05bf61d6c5d906ec13b7a28683674fcc420a28", + "oracle": "chromium/svg-group-blend-screen-each.png", + "oracle_sha256": "ecd5b6f35b517aff2d85c710785a97740ded51a232b731153dfe29da47a7d6c2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-group", + "source": "svg-group-blend-screen-group.svg", + "source_sha256": "077d1da59e0b3a0678e8d9846c489fd108a8292eaea635af456c6c32e6142c49", + "oracle": "chromium/svg-group-blend-screen-group.png", + "oracle_sha256": "3bf966d011f1cf36689ee490858582095d6f0d32e4c9f62d44a37fcdb144c5e8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-leaf", + "source": "svg-group-blend-screen-leaf.svg", + "source_sha256": "cf6b40bf46ab6c1368c99cf3ba7e44dd67ac96bb6d61133f2e2a8cc9ee2693e4", + "oracle": "chromium/svg-group-blend-screen-leaf.png", + "oracle_sha256": "f9e03bf27cb8cc786c1315a7950f5300db5be8c3f79bc7dd2ca49b97d6bc2f57", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-opacity", + "source": "svg-group-blend-screen-opacity.svg", + "source_sha256": "aab37c63521e0da12f63ca7bba9d51b031a060e877309336ebb31c8894db7981", + "oracle": "chromium/svg-group-blend-screen-opacity.png", + "oracle_sha256": "2a2d24bee3fdbc3f44f7869a39c04e59285bc23e4504ad90a7debb5d468f4b07", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-stroke", + "source": "svg-group-blend-screen-stroke.svg", + "source_sha256": "da7489d2796318f75f7bcac7e6df122a785e6a4c652e1cae445920fb090c53f4", + "oracle": "chromium/svg-group-blend-screen-stroke.png", + "oracle_sha256": "904fba724e03a7f2b9435977df3ce500882558ae9b3b525906f3594a3dcf0597", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-transparent", + "source": "svg-group-blend-screen-transparent.svg", + "source_sha256": "4cb0365fcfc6b345f287438637e089e363898674b474ec4bb59dc6fe8095c848", + "oracle": "chromium/svg-group-blend-screen-transparent.png", + "oracle_sha256": "a36c2a26bfa284913592c9f2903cf39c76496e09e2f5eb3c0b06e31b9ba70211", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-translate", + "source": "svg-group-blend-translate.svg", + "source_sha256": "1ac9490c448030963d0c524969c4e7a16bcc11055ecc6cbdca789b183a28615d", + "oracle": "chromium/svg-group-blend-translate.png", + "oracle_sha256": "06dd11d37f1b4c7a1e634071415260d993ec02669fdc6515e8d9f0370ede4933", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-translate-isolated", + "source": "svg-group-blend-translate-isolated.svg", + "source_sha256": "2c458e9ceac1a709af097645f566d759f411f9c7b4707b1a28f9de9d0527f869", + "oracle": "chromium/svg-group-blend-translate-isolated.png", + "oracle_sha256": "3cc3047cc1fb116dd4c42c92124fc9efa1dabb26b74c6fe56fd3de2444308e3b", + "width": 64, + "height": 64 + }, { "id": "svg-group-inherited-fill", "source": "svg-group-inherited-fill.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index d89a20ef..a450b64a 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -17,6 +17,14 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-group-blend-isolated", + "source": "html-inline-svg-group-blend-isolated.html", + "entry": "html-inline-svg", + "oracle": "chromium/html-inline-svg-group-blend-isolated.png", + "width": 64, + "height": 64 + }, { "id": "html-inline-svg-nested-viewport", "source": "html-inline-svg-nested-viewport.html", @@ -4633,6 +4641,750 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-alpha-half-child", + "source": "svg-group-blend-alpha-half-child.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-half-child.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-parent", + "source": "svg-group-blend-alpha-half-parent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-half-parent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-plain", + "source": "svg-group-blend-alpha-half-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-half-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-half-shape", + "source": "svg-group-blend-alpha-half-shape.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-half-shape.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-child", + "source": "svg-group-blend-alpha-six-tenths-child.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-child.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-parent", + "source": "svg-group-blend-alpha-six-tenths-parent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-parent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-plain", + "source": "svg-group-blend-alpha-six-tenths-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-alpha-six-tenths-shape", + "source": "svg-group-blend-alpha-six-tenths-shape.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-alpha-six-tenths-shape.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-anchor", + "source": "svg-group-blend-anchor.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-anchor.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-attr-multiply", + "source": "svg-group-blend-attr-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-attr-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-case-comment", + "source": "svg-group-blend-case-comment.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-case-comment.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-clip", + "source": "svg-group-blend-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-clip-isolated", + "source": "svg-group-blend-clip-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-clip-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-invalid", + "source": "svg-group-blend-css-invalid.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-css-invalid.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-keyword", + "source": "svg-group-blend-css-keyword.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-css-keyword.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-var", + "source": "svg-group-blend-css-var.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-css-var.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-css-winner", + "source": "svg-group-blend-css-winner.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-css-winner.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-multiply", + "source": "svg-group-blend-fractional-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-fractional-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-normal", + "source": "svg-group-blend-fractional-normal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-fractional-normal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-fractional-screen", + "source": "svg-group-blend-fractional-screen.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-fractional-screen.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-group-attr-isolate", + "source": "svg-group-blend-group-attr-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-group-attr-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-instance-direct", + "source": "svg-group-blend-instance-direct.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-instance-direct.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-instance-use", + "source": "svg-group-blend-instance-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-instance-use.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-isolated", + "source": "svg-group-blend-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-multiply", + "source": "svg-group-blend-linear-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-linear-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-normal", + "source": "svg-group-blend-linear-normal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-linear-normal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-linear-screen", + "source": "svg-group-blend-linear-screen.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-linear-screen.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-mapped-direct", + "source": "svg-group-blend-mapped-direct.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-mapped-direct.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-mapped-viewport", + "source": "svg-group-blend-mapped-viewport.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-mapped-viewport.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-alpha", + "source": "svg-group-blend-multiply-alpha.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-alpha.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-each", + "source": "svg-group-blend-multiply-each.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-each.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-group", + "source": "svg-group-blend-multiply-group.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-group.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-leaf", + "source": "svg-group-blend-multiply-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-opacity", + "source": "svg-group-blend-multiply-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-stroke", + "source": "svg-group-blend-multiply-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-multiply-transparent", + "source": "svg-group-blend-multiply-transparent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-multiply-transparent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-nested-hidden", + "source": "svg-group-blend-nested-hidden.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-nested-hidden.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-nested-hidden-isolated", + "source": "svg-group-blend-nested-hidden-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-nested-hidden-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-neutral", + "source": "svg-group-blend-neutral.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-neutral.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-alpha", + "source": "svg-group-blend-normal-alpha.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-alpha.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-clip-isolate", + "source": "svg-group-blend-normal-clip-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-clip-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-clip-plain", + "source": "svg-group-blend-normal-clip-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-clip-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-group", + "source": "svg-group-blend-normal-group.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-group.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-leaf", + "source": "svg-group-blend-normal-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-many-opacity-isolate", + "source": "svg-group-blend-normal-many-opacity-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-many-opacity-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-many-opacity-plain", + "source": "svg-group-blend-normal-many-opacity-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-many-opacity-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-opacity", + "source": "svg-group-blend-normal-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-stroke", + "source": "svg-group-blend-normal-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transform-isolate", + "source": "svg-group-blend-normal-transform-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-transform-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transform-plain", + "source": "svg-group-blend-normal-transform-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-transform-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-transparent", + "source": "svg-group-blend-normal-transparent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-transparent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-unit-isolate", + "source": "svg-group-blend-normal-unit-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-unit-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-normal-unit-plain", + "source": "svg-group-blend-normal-unit-plain.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-normal-unit-plain.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity", + "source": "svg-group-blend-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-0", + "source": "svg-group-blend-opacity-0.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-0.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-1", + "source": "svg-group-blend-opacity-1.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-1.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-isolated", + "source": "svg-group-blend-opacity-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-child", + "source": "svg-group-blend-opacity-opaque-fill-child.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-child.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-default", + "source": "svg-group-blend-opacity-opaque-fill-default.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-default.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-parent", + "source": "svg-group-blend-opacity-opaque-fill-parent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-parent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fill-shape", + "source": "svg-group-blend-opacity-opaque-fill-shape.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fill-shape.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-child", + "source": "svg-group-blend-opacity-opaque-fillstroke-child.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-child.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-default", + "source": "svg-group-blend-opacity-opaque-fillstroke-default.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-default.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-parent", + "source": "svg-group-blend-opacity-opaque-fillstroke-parent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-parent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-opaque-fillstroke-shape", + "source": "svg-group-blend-opacity-opaque-fillstroke-shape.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-opaque-fillstroke-shape.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p00392156862745098", + "source": "svg-group-blend-opacity-p00392156862745098.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-p00392156862745098.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p5019607843137255", + "source": "svg-group-blend-opacity-p5019607843137255.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-p5019607843137255.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-opacity-p6", + "source": "svg-group-blend-opacity-p6.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-opacity-p6.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-opacity", + "source": "svg-group-blend-outer-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-outer-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-multiply", + "source": "svg-group-blend-outer-root-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-outer-root-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-normal", + "source": "svg-group-blend-outer-root-normal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-outer-root-normal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-outer-root-screen", + "source": "svg-group-blend-outer-root-screen.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-outer-root-screen.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-partial", + "source": "svg-group-blend-partial.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-partial.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-partial-isolated", + "source": "svg-group-blend-partial-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-partial-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-multiply", + "source": "svg-group-blend-pattern-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-pattern-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-normal", + "source": "svg-group-blend-pattern-normal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-pattern-normal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-pattern-screen", + "source": "svg-group-blend-pattern-screen.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-pattern-screen.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-isolate", + "source": "svg-group-blend-root-isolate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-root-isolate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-multiply", + "source": "svg-group-blend-root-multiply.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-root-multiply.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-root-normal", + "source": "svg-group-blend-root-normal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-root-normal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-rotate", + "source": "svg-group-blend-rotate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-rotate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-rotate-isolated", + "source": "svg-group-blend-rotate-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-rotate-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-scale", + "source": "svg-group-blend-scale.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-scale.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-scale-isolated", + "source": "svg-group-blend-scale-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-scale-isolated.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-alpha", + "source": "svg-group-blend-screen-alpha.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-alpha.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-each", + "source": "svg-group-blend-screen-each.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-each.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-group", + "source": "svg-group-blend-screen-group.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-group.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-leaf", + "source": "svg-group-blend-screen-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-opacity", + "source": "svg-group-blend-screen-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-stroke", + "source": "svg-group-blend-screen-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-screen-transparent", + "source": "svg-group-blend-screen-transparent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-screen-transparent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-translate", + "source": "svg-group-blend-translate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-translate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-translate-isolated", + "source": "svg-group-blend-translate-isolated.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-translate-isolated.png", + "width": 64, + "height": 64 + }, { "id": "svg-group-inherited-fill", "source": "svg-group-inherited-fill.svg", diff --git a/fixtures/web-first/svg-group-blend-alpha-half-child.svg b/fixtures/web-first/svg-group-blend-alpha-half-child.svg new file mode 100644 index 00000000..8e32ebd6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-half-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-half-parent.svg b/fixtures/web-first/svg-group-blend-alpha-half-parent.svg new file mode 100644 index 00000000..6db821ac --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-half-parent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-half-plain.svg b/fixtures/web-first/svg-group-blend-alpha-half-plain.svg new file mode 100644 index 00000000..7d7ef9ea --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-half-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-half-shape.svg b/fixtures/web-first/svg-group-blend-alpha-half-shape.svg new file mode 100644 index 00000000..22c4d2b4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-half-shape.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-six-tenths-child.svg b/fixtures/web-first/svg-group-blend-alpha-six-tenths-child.svg new file mode 100644 index 00000000..6627b4c2 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-six-tenths-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-six-tenths-parent.svg b/fixtures/web-first/svg-group-blend-alpha-six-tenths-parent.svg new file mode 100644 index 00000000..f9cf84f9 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-six-tenths-parent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-six-tenths-plain.svg b/fixtures/web-first/svg-group-blend-alpha-six-tenths-plain.svg new file mode 100644 index 00000000..ba679b60 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-six-tenths-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-alpha-six-tenths-shape.svg b/fixtures/web-first/svg-group-blend-alpha-six-tenths-shape.svg new file mode 100644 index 00000000..35bd1765 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-alpha-six-tenths-shape.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-anchor.svg b/fixtures/web-first/svg-group-blend-anchor.svg new file mode 100644 index 00000000..bd34d442 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-anchor.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-attr-multiply.svg b/fixtures/web-first/svg-group-blend-attr-multiply.svg new file mode 100644 index 00000000..6c050ba6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-attr-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-case-comment.svg b/fixtures/web-first/svg-group-blend-case-comment.svg new file mode 100644 index 00000000..57e55a19 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-case-comment.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-clip-isolated.svg b/fixtures/web-first/svg-group-blend-clip-isolated.svg new file mode 100644 index 00000000..b0b8e60e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-clip-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-clip.svg b/fixtures/web-first/svg-group-blend-clip.svg new file mode 100644 index 00000000..c57a32c4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-css-invalid.svg b/fixtures/web-first/svg-group-blend-css-invalid.svg new file mode 100644 index 00000000..bff1997f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-css-invalid.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-css-keyword.svg b/fixtures/web-first/svg-group-blend-css-keyword.svg new file mode 100644 index 00000000..d6a3bc07 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-css-keyword.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-css-var.svg b/fixtures/web-first/svg-group-blend-css-var.svg new file mode 100644 index 00000000..678e371f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-css-var.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-css-winner.svg b/fixtures/web-first/svg-group-blend-css-winner.svg new file mode 100644 index 00000000..d6060874 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-css-winner.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-fractional-multiply.svg b/fixtures/web-first/svg-group-blend-fractional-multiply.svg new file mode 100644 index 00000000..25e56114 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-fractional-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-fractional-normal.svg b/fixtures/web-first/svg-group-blend-fractional-normal.svg new file mode 100644 index 00000000..f1170af9 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-fractional-normal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-fractional-screen.svg b/fixtures/web-first/svg-group-blend-fractional-screen.svg new file mode 100644 index 00000000..d1cee58f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-fractional-screen.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-group-attr-isolate.svg b/fixtures/web-first/svg-group-blend-group-attr-isolate.svg new file mode 100644 index 00000000..b9e80d36 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-group-attr-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-instance-direct.svg b/fixtures/web-first/svg-group-blend-instance-direct.svg new file mode 100644 index 00000000..16a8218d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-instance-direct.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-instance-use.svg b/fixtures/web-first/svg-group-blend-instance-use.svg new file mode 100644 index 00000000..cca8a1ba --- /dev/null +++ b/fixtures/web-first/svg-group-blend-instance-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-isolated.svg b/fixtures/web-first/svg-group-blend-isolated.svg new file mode 100644 index 00000000..2664527b --- /dev/null +++ b/fixtures/web-first/svg-group-blend-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-linear-multiply.svg b/fixtures/web-first/svg-group-blend-linear-multiply.svg new file mode 100644 index 00000000..e9b9c18d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-linear-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-linear-normal.svg b/fixtures/web-first/svg-group-blend-linear-normal.svg new file mode 100644 index 00000000..3a55ca82 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-linear-normal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-linear-screen.svg b/fixtures/web-first/svg-group-blend-linear-screen.svg new file mode 100644 index 00000000..22d720fb --- /dev/null +++ b/fixtures/web-first/svg-group-blend-linear-screen.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-mapped-direct.svg b/fixtures/web-first/svg-group-blend-mapped-direct.svg new file mode 100644 index 00000000..a18c20ad --- /dev/null +++ b/fixtures/web-first/svg-group-blend-mapped-direct.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-mapped-viewport.svg b/fixtures/web-first/svg-group-blend-mapped-viewport.svg new file mode 100644 index 00000000..1c340b93 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-mapped-viewport.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-alpha.svg b/fixtures/web-first/svg-group-blend-multiply-alpha.svg new file mode 100644 index 00000000..5d31cea7 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-alpha.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-each.svg b/fixtures/web-first/svg-group-blend-multiply-each.svg new file mode 100644 index 00000000..210c2c78 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-each.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-group.svg b/fixtures/web-first/svg-group-blend-multiply-group.svg new file mode 100644 index 00000000..ef9a7d09 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-group.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-leaf.svg b/fixtures/web-first/svg-group-blend-multiply-leaf.svg new file mode 100644 index 00000000..83d4dac1 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-opacity.svg b/fixtures/web-first/svg-group-blend-multiply-opacity.svg new file mode 100644 index 00000000..3589aa2f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-stroke.svg b/fixtures/web-first/svg-group-blend-multiply-stroke.svg new file mode 100644 index 00000000..d2edc387 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-multiply-transparent.svg b/fixtures/web-first/svg-group-blend-multiply-transparent.svg new file mode 100644 index 00000000..f2df812d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-transparent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-nested-hidden-isolated.svg b/fixtures/web-first/svg-group-blend-nested-hidden-isolated.svg new file mode 100644 index 00000000..53eb21f8 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-nested-hidden-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-nested-hidden.svg b/fixtures/web-first/svg-group-blend-nested-hidden.svg new file mode 100644 index 00000000..36d1ffb6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-nested-hidden.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-neutral.svg b/fixtures/web-first/svg-group-blend-neutral.svg new file mode 100644 index 00000000..783e7f5d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-neutral.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-alpha.svg b/fixtures/web-first/svg-group-blend-normal-alpha.svg new file mode 100644 index 00000000..f435c294 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-alpha.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-clip-isolate.svg b/fixtures/web-first/svg-group-blend-normal-clip-isolate.svg new file mode 100644 index 00000000..429c8174 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-clip-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-clip-plain.svg b/fixtures/web-first/svg-group-blend-normal-clip-plain.svg new file mode 100644 index 00000000..b14ca445 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-clip-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-group.svg b/fixtures/web-first/svg-group-blend-normal-group.svg new file mode 100644 index 00000000..9c7cebeb --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-group.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-leaf.svg b/fixtures/web-first/svg-group-blend-normal-leaf.svg new file mode 100644 index 00000000..e7e391ed --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-many-opacity-isolate.svg b/fixtures/web-first/svg-group-blend-normal-many-opacity-isolate.svg new file mode 100644 index 00000000..deb1b4be --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-many-opacity-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-many-opacity-plain.svg b/fixtures/web-first/svg-group-blend-normal-many-opacity-plain.svg new file mode 100644 index 00000000..451bd549 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-many-opacity-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-opacity.svg b/fixtures/web-first/svg-group-blend-normal-opacity.svg new file mode 100644 index 00000000..33ad8fc4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-stroke.svg b/fixtures/web-first/svg-group-blend-normal-stroke.svg new file mode 100644 index 00000000..dcc4c0da --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-transform-isolate.svg b/fixtures/web-first/svg-group-blend-normal-transform-isolate.svg new file mode 100644 index 00000000..cbaa5df5 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-transform-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-transform-plain.svg b/fixtures/web-first/svg-group-blend-normal-transform-plain.svg new file mode 100644 index 00000000..e24705c3 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-transform-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-transparent.svg b/fixtures/web-first/svg-group-blend-normal-transparent.svg new file mode 100644 index 00000000..f1583276 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-transparent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-unit-isolate.svg b/fixtures/web-first/svg-group-blend-normal-unit-isolate.svg new file mode 100644 index 00000000..426187b2 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-unit-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-normal-unit-plain.svg b/fixtures/web-first/svg-group-blend-normal-unit-plain.svg new file mode 100644 index 00000000..31d24219 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-normal-unit-plain.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-0.svg b/fixtures/web-first/svg-group-blend-opacity-0.svg new file mode 100644 index 00000000..21474ab6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-0.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-1.svg b/fixtures/web-first/svg-group-blend-opacity-1.svg new file mode 100644 index 00000000..8f41fb34 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-1.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-isolated.svg b/fixtures/web-first/svg-group-blend-opacity-isolated.svg new file mode 100644 index 00000000..28add75d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fill-child.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-child.svg new file mode 100644 index 00000000..0bafaae0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fill-default.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-default.svg new file mode 100644 index 00000000..9aac6a38 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-default.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fill-parent.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-parent.svg new file mode 100644 index 00000000..9e21be3e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-parent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fill-shape.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-shape.svg new file mode 100644 index 00000000..b1923fd8 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fill-shape.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-child.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-child.svg new file mode 100644 index 00000000..d6d1d923 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-default.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-default.svg new file mode 100644 index 00000000..f904091d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-default.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-parent.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-parent.svg new file mode 100644 index 00000000..4a3607cd --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-parent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-shape.svg b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-shape.svg new file mode 100644 index 00000000..60fa5144 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-opaque-fillstroke-shape.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-p00392156862745098.svg b/fixtures/web-first/svg-group-blend-opacity-p00392156862745098.svg new file mode 100644 index 00000000..adacacbb --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-p00392156862745098.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-p5019607843137255.svg b/fixtures/web-first/svg-group-blend-opacity-p5019607843137255.svg new file mode 100644 index 00000000..b9128328 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-p5019607843137255.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity-p6.svg b/fixtures/web-first/svg-group-blend-opacity-p6.svg new file mode 100644 index 00000000..05e4637e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity-p6.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-opacity.svg b/fixtures/web-first/svg-group-blend-opacity.svg new file mode 100644 index 00000000..7e21c337 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-outer-opacity.svg b/fixtures/web-first/svg-group-blend-outer-opacity.svg new file mode 100644 index 00000000..917a30ba --- /dev/null +++ b/fixtures/web-first/svg-group-blend-outer-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-outer-root-multiply.svg b/fixtures/web-first/svg-group-blend-outer-root-multiply.svg new file mode 100644 index 00000000..7441b044 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-outer-root-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-outer-root-normal.svg b/fixtures/web-first/svg-group-blend-outer-root-normal.svg new file mode 100644 index 00000000..372d77f2 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-outer-root-normal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-outer-root-screen.svg b/fixtures/web-first/svg-group-blend-outer-root-screen.svg new file mode 100644 index 00000000..cc36fdfe --- /dev/null +++ b/fixtures/web-first/svg-group-blend-outer-root-screen.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-partial-isolated.svg b/fixtures/web-first/svg-group-blend-partial-isolated.svg new file mode 100644 index 00000000..15a7df54 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-partial-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-partial.svg b/fixtures/web-first/svg-group-blend-partial.svg new file mode 100644 index 00000000..0194e338 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-partial.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-pattern-multiply.svg b/fixtures/web-first/svg-group-blend-pattern-multiply.svg new file mode 100644 index 00000000..68ddf6a5 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-pattern-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-pattern-normal.svg b/fixtures/web-first/svg-group-blend-pattern-normal.svg new file mode 100644 index 00000000..33796317 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-pattern-normal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-pattern-screen.svg b/fixtures/web-first/svg-group-blend-pattern-screen.svg new file mode 100644 index 00000000..89093969 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-pattern-screen.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-root-isolate.svg b/fixtures/web-first/svg-group-blend-root-isolate.svg new file mode 100644 index 00000000..0ba0be3b --- /dev/null +++ b/fixtures/web-first/svg-group-blend-root-isolate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-root-multiply.svg b/fixtures/web-first/svg-group-blend-root-multiply.svg new file mode 100644 index 00000000..bfd0b62e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-root-multiply.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-root-normal.svg b/fixtures/web-first/svg-group-blend-root-normal.svg new file mode 100644 index 00000000..bb579fb4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-root-normal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-rotate-isolated.svg b/fixtures/web-first/svg-group-blend-rotate-isolated.svg new file mode 100644 index 00000000..5d6709df --- /dev/null +++ b/fixtures/web-first/svg-group-blend-rotate-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-rotate.svg b/fixtures/web-first/svg-group-blend-rotate.svg new file mode 100644 index 00000000..b9fd0ce2 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-rotate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-scale-isolated.svg b/fixtures/web-first/svg-group-blend-scale-isolated.svg new file mode 100644 index 00000000..28259665 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-scale-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-scale.svg b/fixtures/web-first/svg-group-blend-scale.svg new file mode 100644 index 00000000..92cd6c2c --- /dev/null +++ b/fixtures/web-first/svg-group-blend-scale.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-alpha.svg b/fixtures/web-first/svg-group-blend-screen-alpha.svg new file mode 100644 index 00000000..d8d29e06 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-alpha.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-each.svg b/fixtures/web-first/svg-group-blend-screen-each.svg new file mode 100644 index 00000000..915dd61d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-each.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-group.svg b/fixtures/web-first/svg-group-blend-screen-group.svg new file mode 100644 index 00000000..630c4229 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-group.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-leaf.svg b/fixtures/web-first/svg-group-blend-screen-leaf.svg new file mode 100644 index 00000000..81d9b88d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-opacity.svg b/fixtures/web-first/svg-group-blend-screen-opacity.svg new file mode 100644 index 00000000..d483a832 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-stroke.svg b/fixtures/web-first/svg-group-blend-screen-stroke.svg new file mode 100644 index 00000000..6ee5db05 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-transparent.svg b/fixtures/web-first/svg-group-blend-screen-transparent.svg new file mode 100644 index 00000000..bc61987f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-transparent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-translate-isolated.svg b/fixtures/web-first/svg-group-blend-translate-isolated.svg new file mode 100644 index 00000000..0022c805 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-translate-isolated.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-translate.svg b/fixtures/web-first/svg-group-blend-translate.svg new file mode 100644 index 00000000..000a9ff4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-translate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index 220ac448..45b846cd 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -13,6 +13,9 @@ best-effort as well, and the attributable ones are declared by name at a structural path. What that gate defends is the invariant, stated over a whole directory — *nothing here renders silently*. Individual constructs are pinned a second time, from inline sources, by the contract law that owns each rung. +The source extension selects the actual ingress: `.svg` uses the standalone +entry and `.html` the inline-SVG entry. Exactly one source must exist per id; +the refusal gate and generated status share that dispatch. The scannable, generated view of this register (beside the baked cells) is [../STATUS.md](../STATUS.md), freshness-gated by @@ -20,6 +23,17 @@ The scannable, generated view of this register (beside the baked cells) is | File | Required result | | --- | --- | +| `svg-group-blend-mode-{overlay,darken,lighten,color-dodge,color-burn,hard-light,soft-light,difference,exclusion,hue,saturation,color,luminosity,plus-lighter}.svg` | Fourteen live computed values outside B1's normal/multiply/screen profile must name `mix-blend-mode`; best-effort skips the attributed group, never substitutes normal. | +| `svg-group-blend-{filter,mask}.svg` | Name the unadmitted image-effect composition profile and skip the complete affected group transaction. | +| `svg-group-blend-elided-{filter,mask}-{unit,partial}.svg` | Eliding redundant Normal isolation must retain its authored participation for an ancestor filter/mask patrol, without inventing a physical layer or blocking the established opacity fold. These four inputs keep the conservative image-effect refusal; they are not claims of measured pixel defects. | +| `svg-group-blend-resource-{pattern,mask,clippath}-{root,child}.svg` | Six resource-source ingress guards; a generic walk must not accidentally admit resource roots or geometric clip contributors. The failure propagates to the consuming client by name. | +| `svg-group-blend-{css-animation,css-var-animation}.svg` · `html-group-blend-head-animation.html` | Name the animated group-composition profile in both admissions, including indirect custom-property animation and an HTML-head stylesheet. | +| `svg-group-blend-pattern-root-animation.svg` | Preserve the existing document-load animation finding when a referenced pattern root would otherwise disappear from the best-effort source walk. | +| `html-group-blend-{unisolated,ancestor-mix-blend-mode,ancestor-isolation}.html` | Refuse in both admissions: the compiled SVG contribution has no exterior HTML backdrop/layer graph to honor these cases. | +| `svg-group-blend-source-{path,ellipse,clip,isolation}.svg` | Name the group-source precision boundary exposed by non-rectangular source or coverage materialization; best-effort skips the affected group, not its first offending draw alone. | +| `svg-group-blend-source-{radial,stroke,subpixel-clip,rotated-clip}.svg` | Conservative extension guards for radial paints, wider source strokes and unproved clip coverage. These are refusal witnesses, not claims that every member has a measured pixel mismatch. | +| `svg-group-blend-root-opacity.svg` | Refuse the partial-opacity root-blend precision class in both admissions; no root-level fallback can preserve its promised meaning. | +| `svg-group-blend-root-filter-sibling.svg` | Refuse in both admissions when an escaping blend requires a new standalone-root isolation layer around a separate filtered sibling. An implicit root boundary must not bypass the image-effect composition profile. | | `svg-context-paint-fallback-extension.svg` | Refuse Stylo's non-standard context-paint fallback extension by name. SVG2 permits a fallback only after a URL, and Chromium drops `context-fill red` as an invalid paint (measured); the pinned parser accepts it. The standard-track grammar remains the bar under gridaco/nothing#77, so this registered over-refusal cannot hold the four `fill`/`stroke` rows open. Attribute, inline-style, and stylesheet ingresses are guarded. | | `svg-viewbox-invalid-token.svg` | Reject the malformed `viewBox`; do not discard the bad token. | | `svg-viewbox-repeated-comma.svg` | Reject a repeated comma in the `viewBox` number list; do not filter empty separators. | diff --git a/fixtures/web-first/unsupported/html-group-blend-ancestor-isolation.html b/fixtures/web-first/unsupported/html-group-blend-ancestor-isolation.html new file mode 100644 index 00000000..2ba09a75 --- /dev/null +++ b/fixtures/web-first/unsupported/html-group-blend-ancestor-isolation.html @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/html-group-blend-ancestor-mix-blend-mode.html b/fixtures/web-first/unsupported/html-group-blend-ancestor-mix-blend-mode.html new file mode 100644 index 00000000..f59dedb5 --- /dev/null +++ b/fixtures/web-first/unsupported/html-group-blend-ancestor-mix-blend-mode.html @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/html-group-blend-head-animation.html b/fixtures/web-first/unsupported/html-group-blend-head-animation.html new file mode 100644 index 00000000..c9b34c5a --- /dev/null +++ b/fixtures/web-first/unsupported/html-group-blend-head-animation.html @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/html-group-blend-unisolated.html b/fixtures/web-first/unsupported/html-group-blend-unisolated.html new file mode 100644 index 00000000..9ecb30ab --- /dev/null +++ b/fixtures/web-first/unsupported/html-group-blend-unisolated.html @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-css-animation.svg b/fixtures/web-first/unsupported/svg-group-blend-css-animation.svg new file mode 100644 index 00000000..27ea899e --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-css-animation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-css-var-animation.svg b/fixtures/web-first/unsupported/svg-group-blend-css-var-animation.svg new file mode 100644 index 00000000..16e49ce9 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-css-var-animation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-elided-filter-partial.svg b/fixtures/web-first/unsupported/svg-group-blend-elided-filter-partial.svg new file mode 100644 index 00000000..61615b4b --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-elided-filter-partial.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-elided-filter-unit.svg b/fixtures/web-first/unsupported/svg-group-blend-elided-filter-unit.svg new file mode 100644 index 00000000..7a2bcbe0 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-elided-filter-unit.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-elided-mask-partial.svg b/fixtures/web-first/unsupported/svg-group-blend-elided-mask-partial.svg new file mode 100644 index 00000000..c4973654 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-elided-mask-partial.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-elided-mask-unit.svg b/fixtures/web-first/unsupported/svg-group-blend-elided-mask-unit.svg new file mode 100644 index 00000000..a0d45d6c --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-elided-mask-unit.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-filter.svg b/fixtures/web-first/unsupported/svg-group-blend-filter.svg new file mode 100644 index 00000000..6d589f57 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-filter.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mask.svg b/fixtures/web-first/unsupported/svg-group-blend-mask.svg new file mode 100644 index 00000000..b0e8983a --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mask.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-color-burn.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-color-burn.svg new file mode 100644 index 00000000..be70a8be --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-color-burn.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-color-dodge.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-color-dodge.svg new file mode 100644 index 00000000..c30089d7 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-color-dodge.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-color.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-color.svg new file mode 100644 index 00000000..ccaf6b9e --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-color.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-darken.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-darken.svg new file mode 100644 index 00000000..1a29e0bb --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-darken.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-difference.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-difference.svg new file mode 100644 index 00000000..9442bfa1 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-difference.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-exclusion.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-exclusion.svg new file mode 100644 index 00000000..40ce0b15 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-exclusion.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-hard-light.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-hard-light.svg new file mode 100644 index 00000000..f6538dc2 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-hard-light.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-hue.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-hue.svg new file mode 100644 index 00000000..2547a154 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-hue.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-lighten.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-lighten.svg new file mode 100644 index 00000000..97436658 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-lighten.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-luminosity.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-luminosity.svg new file mode 100644 index 00000000..58621a3c --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-luminosity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-overlay.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-overlay.svg new file mode 100644 index 00000000..35c7e526 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-overlay.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-plus-lighter.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-plus-lighter.svg new file mode 100644 index 00000000..9c9ae9ad --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-plus-lighter.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-saturation.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-saturation.svg new file mode 100644 index 00000000..c682ccb4 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-saturation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-mode-soft-light.svg b/fixtures/web-first/unsupported/svg-group-blend-mode-soft-light.svg new file mode 100644 index 00000000..b8da8d3f --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-mode-soft-light.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-pattern-root-animation.svg b/fixtures/web-first/unsupported/svg-group-blend-pattern-root-animation.svg new file mode 100644 index 00000000..5be3792c --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-pattern-root-animation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-child.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-child.svg new file mode 100644 index 00000000..24027f5c --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-root.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-root.svg new file mode 100644 index 00000000..94c20e99 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-clippath-root.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-mask-child.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-mask-child.svg new file mode 100644 index 00000000..a7b24b87 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-mask-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-mask-root.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-mask-root.svg new file mode 100644 index 00000000..7cf8a8d6 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-mask-root.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-child.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-child.svg new file mode 100644 index 00000000..90ce4133 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-child.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-root.svg b/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-root.svg new file mode 100644 index 00000000..193bb438 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-resource-pattern-root.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-root-filter-sibling.svg b/fixtures/web-first/unsupported/svg-group-blend-root-filter-sibling.svg new file mode 100644 index 00000000..500f9371 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-root-filter-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-root-opacity.svg b/fixtures/web-first/unsupported/svg-group-blend-root-opacity.svg new file mode 100644 index 00000000..36b3a7d1 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-root-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-clip.svg b/fixtures/web-first/unsupported/svg-group-blend-source-clip.svg new file mode 100644 index 00000000..52a3df34 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-ellipse.svg b/fixtures/web-first/unsupported/svg-group-blend-source-ellipse.svg new file mode 100644 index 00000000..b87fe68f --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-ellipse.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-isolation.svg b/fixtures/web-first/unsupported/svg-group-blend-source-isolation.svg new file mode 100644 index 00000000..d31c01ec --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-isolation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-path.svg b/fixtures/web-first/unsupported/svg-group-blend-source-path.svg new file mode 100644 index 00000000..77137799 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-path.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-radial.svg b/fixtures/web-first/unsupported/svg-group-blend-source-radial.svg new file mode 100644 index 00000000..a63391f4 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-radial.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-rotated-clip.svg b/fixtures/web-first/unsupported/svg-group-blend-source-rotated-clip.svg new file mode 100644 index 00000000..9b5697a6 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-rotated-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-stroke.svg b/fixtures/web-first/unsupported/svg-group-blend-source-stroke.svg new file mode 100644 index 00000000..c0174124 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-subpixel-clip.svg b/fixtures/web-first/unsupported/svg-group-blend-source-subpixel-clip.svg new file mode 100644 index 00000000..9b3edbc1 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-subpixel-clip.svg @@ -0,0 +1 @@ + From 7bb311c1e1e588c274cd273709fac5c2ecfb4779 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 19:08:13 +0900 Subject: [PATCH 2/7] docs: format the admitted blending slice --- crates/n0_cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 97fc4646..8c36c918 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -844,7 +844,7 @@ cargo run -p n0_cli --bin n0 -- \ around the selected inline SVG; explicit `inherit` on the SVG compounds with those host scopes rather than flattening them. CSS `mix-blend-mode: normal | multiply | screen` and `isolation: auto | - isolate` have a bounded static SVG group profile. One Stylo computed value +isolate` have a bounded static SVG group profile. One Stylo computed value decides the operation; raw attribute lookalikes are inert. Neutral/default groups add no layer. Normal isolation with no escaping child blend is redundant; the compiler elides it and preserves the established opacity From ed2f6f6884559079433b5698eda4f0e4b14754ff Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 20:10:13 +0900 Subject: [PATCH 3/7] n0: make group blend restoration byte-portable --- crates/n0/src/glyphless.rs | 18 ++ crates/n0/src/paint.rs | 241 +++++++++++++++++- fixtures/web-first/STATUS.md | 6 +- ...up-blend-multiply-opacity-small-opaque.png | Bin 0 -> 225 bytes ...p-blend-multiply-opacity-small-partial.png | Bin 0 -> 219 bytes ...roup-blend-screen-opacity-small-opaque.png | Bin 0 -> 221 bytes ...oup-blend-screen-opacity-small-partial.png | Bin 0 -> 220 bytes fixtures/web-first/oracle-bake.json | 38 ++- fixtures/web-first/primitives.json | 32 +++ ...up-blend-multiply-opacity-small-opaque.svg | 1 + ...p-blend-multiply-opacity-small-partial.svg | 1 + ...roup-blend-screen-opacity-small-opaque.svg | 1 + ...oup-blend-screen-opacity-small-partial.svg | 1 + 13 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-opaque.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-partial.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-opacity-small-opaque.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-screen-opacity-small-partial.png create mode 100644 fixtures/web-first/svg-group-blend-multiply-opacity-small-opaque.svg create mode 100644 fixtures/web-first/svg-group-blend-multiply-opacity-small-partial.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-opacity-small-opaque.svg create mode 100644 fixtures/web-first/svg-group-blend-screen-opacity-small-partial.svg diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index f1ea6c53..7744faad 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -93,6 +93,12 @@ pub enum BuildError { owner: VisualRef, reason: String, }, + /// An isolated group's deterministic backend blender could not be built. + /// No product is returned that might silently substitute a native operation. + Blend { + owner: VisualRef, + reason: String, + }, } impl std::fmt::Display for BuildError { @@ -145,6 +151,12 @@ impl std::fmt::Display for BuildError { "glyphless visual {owner:?} filter preflight failed: {reason}" ) } + BuildError::Blend { owner, reason } => { + write!( + f, + "glyphless visual {owner:?} blend preflight failed: {reason}" + ) + } } } } @@ -375,6 +387,12 @@ pub fn compile(resolved: Frame) -> Result { (OpenScopeKind::Opacity, None) } ScopeEffect::Blend(blend) => { + crate::paint::preflight_isolated_blend(*blend).map_err(|reason| { + BuildError::Blend { + owner: scope.owner, + reason, + } + })?; items.push(Item { node: slot, world: frame_world, diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 73acf163..962f343a 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -2512,9 +2512,22 @@ fn deterministic_porter_duff_blender( // that use that pipeline over explicit unorm8 values so both CPU families // reproduce the committed Chromium bytes. The remaining seven modes use Skia's // high-precision path and stay native unless measurement proves otherwise. -fn exact_unorm8_filter_blend_source(expression: &str) -> String { +const UNORM8_MULTIPLY_EXPRESSION: &str = "div255(s * (255.0 - d.a) + d * (255.0 - s.a) + s * d)"; + +fn exact_unorm8_blend_source(expression: &str, source_opacity_uniform: bool) -> String { + let declaration = if source_opacity_uniform { + "uniform float opacity_byte;" + } else { + "" + }; + let source_scale = if source_opacity_uniform { + "s = div255(s * opacity_byte);" + } else { + "" + }; format!( r#" +{declaration} float div255(float value) {{ return floor((value + 127.0) / 255.0); }} @@ -2544,6 +2557,7 @@ float hard_light_channel(float s, float d, float sa, float da) {{ half4 main(half4 src, half4 dst) {{ float4 s = floor(float4(src) * 255.0 + 0.5); float4 d = floor(float4(dst) * 255.0 + 0.5); + {source_scale} float4 result = {expression}; return half4(clamp(result, 0.0, 255.0) / 255.0); }} @@ -2585,10 +2599,7 @@ half4 main(half4 src, half4 dst) {{ fn deterministic_filter_blender(mode: ResolvedFilterBlend) -> Result { let (slot, expression) = match mode { ResolvedFilterBlend::Normal => (12, "s + div255(d * (255.0 - s.a))"), - ResolvedFilterBlend::Multiply => ( - 13, - "div255(s * (255.0 - d.a) + d * (255.0 - s.a) + s * d)", - ), + ResolvedFilterBlend::Multiply => (13, UNORM8_MULTIPLY_EXPRESSION), ResolvedFilterBlend::Screen => (14, "s + d - div255(s * d)"), ResolvedFilterBlend::Overlay => ( 15, @@ -2616,7 +2627,206 @@ fn deterministic_filter_blender(mode: ResolvedFilterBlend) -> Result return Ok(sk_filter_blend_mode(mode).into()), }; - cached_filter_blender(slot, || exact_unorm8_filter_blend_source(expression)) + cached_filter_blender(slot, || exact_unorm8_blend_source(expression, false)) +} + +thread_local! { + // Cache shader code and immutable uniform bindings, never destination + // pixels. All checked opacity values map to only 256 byte factors. Compile + // one effect per mode per painting thread, not one shader per opacity or + // replay. Unit-opacity Screen remains native. + static ISOLATED_BYTE_BLENDERS: RefCell<[Option; 2]> = + const { RefCell::new([None, None]) }; +} + +struct IsolatedByteBlenders { + effect: skia_safe::RuntimeEffect, + by_alpha: Vec>, +} + +fn isolated_byte_blender(blend: rframe::ScopeBlend) -> Result { + // Pinned Skia's N32 lowp sprite restore quantizes the paint opacity first, + // then rounds source-byte * opacity-byte / 255. A runtime blender promotes + // the surrounding pipeline to highp; leaving paint alpha active would + // instead scale by the original float before source quantization. + let alpha = (blend.opacity().map_or(1.0, |opacity| opacity.get()) * 255.0 + 0.5) as u8; + let (slot, expression) = match blend.mode() { + rframe::ScopeBlendMode::Multiply => (0, UNORM8_MULTIPLY_EXPRESSION), + rframe::ScopeBlendMode::Screen => (1, "s + d - div255(s * d)"), + rframe::ScopeBlendMode::Normal => { + return Err("Normal isolation does not use a byte-domain blender".to_string()); + } + }; + ISOLATED_BYTE_BLENDERS.with(|caches| { + let mut caches = caches.borrow_mut(); + let cache = &mut caches[slot]; + if cache.is_none() { + let options = skia_safe::runtime_effect::Options { + force_unoptimized: false, + name: "n0_isolated_byte_blender", + }; + let effect = skia_safe::RuntimeEffect::make_for_blender( + exact_unorm8_blend_source(expression, true), + Some(&options), + ) + .map_err(|error| { + format!("the backend could not compile an isolated byte-domain blender: {error}") + })?; + *cache = Some(IsolatedByteBlenders { + effect, + by_alpha: Vec::new(), + }); + } + let cache = cache.as_mut().expect("effect initialized above"); + if let Some(blender) = cache + .by_alpha + .get(usize::from(alpha)) + .and_then(Option::as_ref) + { + return Ok(blender.clone()); + } + let blender = cache + .effect + .make_blender(Data::new_copy(&f32::from(alpha).to_ne_bytes()), None) + .ok_or_else(|| "the backend could not bind isolated blend opacity".to_string())?; + if cache.by_alpha.len() <= usize::from(alpha) { + cache.by_alpha.resize_with(usize::from(alpha) + 1, || None); + } + cache.by_alpha[usize::from(alpha)] = Some(blender.clone()); + Ok(blender) + }) +} + +/// Validate fallible backend construction before an immutable frame product +/// exists. The static shader's raster lowering is guarded by execution tests; +/// preflight itself issues no raster commands. +pub(crate) fn preflight_isolated_blend(blend: rframe::ScopeBlend) -> Result<(), String> { + if uses_isolated_byte_blender(blend) { + isolated_byte_blender(blend).map(|_| ()) + } else { + Ok(()) + } +} + +fn uses_isolated_byte_blender(blend: rframe::ScopeBlend) -> bool { + blend.mode() == rframe::ScopeBlendMode::Multiply + || (blend.mode() == rframe::ScopeBlendMode::Screen && blend.opacity().is_some()) +} + +#[cfg(test)] +mod isolated_blend_policy_tests { + use super::*; + + fn scope(opacity: f32) -> rframe::ScopeBlend { + rframe::ScopeBlend::new( + rframe::ScopeBlendMode::Multiply, + if opacity == 1.0 { + None + } else { + Some(rframe::ScopeOpacity::new(opacity).unwrap()) + }, + ) + } + + fn pixel(blend: rframe::ScopeBlend, float_first: bool) -> Vec { + let mut surface = skia_safe::surfaces::raster_n32_premul((1, 1)).unwrap(); + surface.canvas().clear(Color::from_argb(170, 66, 101, 137)); + let mut restore = Paint::default(); + if float_first { + restore.set_alpha_f(blend.opacity().unwrap().get()); + restore + .set_blender(deterministic_filter_blender(ResolvedFilterBlend::Multiply).unwrap()); + } else { + restore.set_blender(isolated_byte_blender(blend).unwrap()); + } + surface + .canvas() + .save_layer(&SaveLayerRec::default().paint(&restore)); + surface.canvas().clear(Color::from_argb(149, 215, 104, 67)); + surface.canvas().restore(); + read_pixels(&mut surface, 1, 1) + } + + #[test] + fn isolated_multiply_raster_matches_ordered_integer_math_for_every_opacity_byte() { + let q = |v: u32| (v + 127) / 255; + let source = [q(215 * 149), q(104 * 149), q(67 * 149), 149]; + let destination = [q(66 * 170), q(101 * 170), q(137 * 170), 170]; + for alpha in 0..=255_u32 { + let blend = scope(if alpha == 0 { + 0.001 + } else { + alpha as f32 / 255.0 + }); + preflight_isolated_blend(blend).unwrap(); + let s = source.map(|v| q(v * alpha)); + let expected: Vec<_> = s + .iter() + .zip(destination) + .map(|(s_channel, d_channel)| { + q(s_channel * (255 - destination[3]) + + d_channel * (255 - s[3]) + + s_channel * d_channel) as u8 + }) + .collect(); + assert_eq!(pixel(blend, false), expected, "opacity byte {alpha}"); + } + ISOLATED_BYTE_BLENDERS.with(|cache| { + let cache = cache.borrow(); + let bindings = &cache[0].as_ref().unwrap().by_alpha; + assert_eq!(bindings.len(), 256); + assert!(bindings.iter().all(Option::is_some)); + }); + } + + #[test] + fn isolated_multiply_quantizes_opacity_before_scaling_source() { + let arbitrary = scope(0.123456); + assert_eq!(pixel(arbitrary, false), pixel(scope(31.0 / 255.0), false)); + assert_ne!(pixel(arbitrary, false), pixel(arbitrary, true)); + } + + #[test] + fn isolated_screen_raster_matches_ordered_integer_math_for_every_opacity_byte() { + let q = |v: u32| (v + 127) / 255; + let source = [q(215 * 149), q(104 * 149), q(67 * 149), 149]; + let destination = [q(66 * 170), q(101 * 170), q(137 * 170), 170]; + for alpha in 0..=255_u32 { + let opacity = scope(if alpha == 0 { + 0.001 + } else { + alpha as f32 / 255.0 + }) + .opacity(); + let blend = rframe::ScopeBlend::new(rframe::ScopeBlendMode::Screen, opacity); + preflight_isolated_blend(blend).unwrap(); + assert_eq!(uses_isolated_byte_blender(blend), opacity.is_some()); + let source = source.map(|v| q(v * alpha)); + let expected: Vec<_> = source + .iter() + .zip(destination) + .map(|(s, d)| (s + d - q(s * d)) as u8) + .collect(); + let warm = pixel(blend, false); + assert_eq!(warm, expected, "opacity byte {alpha}"); + ISOLATED_BYTE_BLENDERS.with(|cache| cache.borrow_mut()[1] = None); + assert_eq!( + pixel(blend, false), + warm, + "fresh binding at opacity byte {alpha}" + ); + } + } + + #[test] + fn isolated_multiply_blender_cache_matches_fresh() { + let blend = scope(0.123456); + let warm = pixel(blend, false); + let _ = pixel(scope(0.6), false); + assert_eq!(pixel(blend, false), warm); + ISOLATED_BYTE_BLENDERS.with(|cache| *cache.borrow_mut() = [None, None]); + assert_eq!(pixel(blend, false), warm); + } } fn procedural_filter_blender( @@ -4220,12 +4430,19 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, // layer outside a blend would change the blend's backdrop; // nesting it inside would introduce another quantization. let mut restore_paint = Paint::default(); - restore_paint.set_alpha_f(blend.opacity().map_or(1.0, |opacity| opacity.get())); - restore_paint.set_blend_mode(match blend.mode() { - rframe::ScopeBlendMode::Normal => skia_safe::BlendMode::SrcOver, - rframe::ScopeBlendMode::Multiply => skia_safe::BlendMode::Multiply, - rframe::ScopeBlendMode::Screen => skia_safe::BlendMode::Screen, - }); + if uses_isolated_byte_blender(*blend) { + restore_paint + .set_blender(isolated_byte_blender(*blend).expect( + "isolated blend construction was preflighted at product build", + )); + } else { + restore_paint.set_alpha_f(blend.opacity().map_or(1.0, |opacity| opacity.get())); + restore_paint.set_blend_mode(match blend.mode() { + rframe::ScopeBlendMode::Normal => skia_safe::BlendMode::SrcOver, + rframe::ScopeBlendMode::Screen => skia_safe::BlendMode::Screen, + rframe::ScopeBlendMode::Multiply => unreachable!(), + }); + } canvas.save_layer(&SaveLayerRec::default().paint(&restore_paint)); scopes.push(Scope::Blend { #[cfg(feature = "trace")] diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 66aaf293..523c7a8b 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1378) +## Chromium-baked cells (1382) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -630,6 +630,8 @@ to its fixture source. No new image is committed for this view. svg-group-blend-multiply-group svg-group-blend-multiply-leaf svg-group-blend-multiply-opacity +svg-group-blend-multiply-opacity-small-opaque +svg-group-blend-multiply-opacity-small-partial svg-group-blend-multiply-stroke svg-group-blend-multiply-transparent svg-group-blend-nested-hidden @@ -685,6 +687,8 @@ to its fixture source. No new image is committed for this view. svg-group-blend-screen-group svg-group-blend-screen-leaf svg-group-blend-screen-opacity +svg-group-blend-screen-opacity-small-opaque +svg-group-blend-screen-opacity-small-partial svg-group-blend-screen-stroke svg-group-blend-screen-transparent svg-group-blend-translate diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-opaque.png b/fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-opaque.png new file mode 100644 index 0000000000000000000000000000000000000000..1914cb8dfb5144685032b3215f8e6d51f4a4ab81 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=(>z@qLn`LHxwVnAg+YMz;=>OQ zcAS{C=4jHIqpx~g3^uYb3nxB1D$Seotvxd??1SP)B`%~|NjOp-g+Q|4dg;* Y1`kn=q?ukHqJbg|p00i_>zopr0CIv&O#lD@ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-partial.png b/fixtures/web-first/chromium/svg-group-blend-multiply-opacity-small-partial.png new file mode 100644 index 0000000000000000000000000000000000000000..210c4d765fc6377c50fd5d6370fee033b0ca046b GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=6FglULn`LHxwVnAAwcBl1qJy+ zz4Stf^g<^=Zubov1SbXW*ZQ!_`dY5N!0ZXy3`MLC>ljY-UT^+A=jX<19dY?DOeaJc zb}}i1Gd$sHh+%{)!7X^*@59>%ule4;uVO^kVp-e3&cN{hKdV`A0MKo0Ah$6ySe)ia Ue3LEd4isVVboFyt=akR{0M{x<4FCWD literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-screen-opacity-small-opaque.png b/fixtures/web-first/chromium/svg-group-blend-screen-opacity-small-opaque.png new file mode 100644 index 0000000000000000000000000000000000000000..6a2e6e9dd19c8a78459c9deeffe67fdc5b2427d0 GIT binary patch literal 221 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=lRRAhDIVVt1NQ1tlrK_kP@$v=Vy diff --git a/fixtures/web-first/svg-group-blend-multiply-opacity-small-partial.svg b/fixtures/web-first/svg-group-blend-multiply-opacity-small-partial.svg new file mode 100644 index 00000000..a2a207dd --- /dev/null +++ b/fixtures/web-first/svg-group-blend-multiply-opacity-small-partial.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-opacity-small-opaque.svg b/fixtures/web-first/svg-group-blend-screen-opacity-small-opaque.svg new file mode 100644 index 00000000..9a89a2d5 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-opacity-small-opaque.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-screen-opacity-small-partial.svg b/fixtures/web-first/svg-group-blend-screen-opacity-small-partial.svg new file mode 100644 index 00000000..6bbf84b3 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-screen-opacity-small-partial.svg @@ -0,0 +1 @@ + From 3bfed7dbf584c69e45a9a3f6b0ebe41c5e169012 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 20:36:43 +0900 Subject: [PATCH 4/7] n0: preserve exact unit-isolation restoration across CPUs --- crates/n0/src/paint.rs | 60 +++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 962f343a..5ecf3149 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -2635,8 +2635,8 @@ thread_local! { // pixels. All checked opacity values map to only 256 byte factors. Compile // one effect per mode per painting thread, not one shader per opacity or // replay. Unit-opacity Screen remains native. - static ISOLATED_BYTE_BLENDERS: RefCell<[Option; 2]> = - const { RefCell::new([None, None]) }; + static ISOLATED_BYTE_BLENDERS: RefCell<[Option; 3]> = + const { RefCell::new([None, None, None]) }; } struct IsolatedByteBlenders { @@ -2653,9 +2653,10 @@ fn isolated_byte_blender(blend: rframe::ScopeBlend) -> Result { let (slot, expression) = match blend.mode() { rframe::ScopeBlendMode::Multiply => (0, UNORM8_MULTIPLY_EXPRESSION), rframe::ScopeBlendMode::Screen => (1, "s + d - div255(s * d)"), - rframe::ScopeBlendMode::Normal => { - return Err("Normal isolation does not use a byte-domain blender".to_string()); - } + // N32 source-over sprite restoration has a separate x86 fast path: + // s + ((d * (256 - sa)) >> 8), unlike NEON's accurate /255. Rotated + // isolated sources expose it at partial-alpha edge pixels. + rframe::ScopeBlendMode::Normal => (2, "s + div255(d * (255.0 - s.a))"), }; ISOLATED_BYTE_BLENDERS.with(|caches| { let mut caches = caches.borrow_mut(); @@ -2709,8 +2710,11 @@ pub(crate) fn preflight_isolated_blend(blend: rframe::ScopeBlend) -> Result<(), } fn uses_isolated_byte_blender(blend: rframe::ScopeBlend) -> bool { - blend.mode() == rframe::ScopeBlendMode::Multiply - || (blend.mode() == rframe::ScopeBlendMode::Screen && blend.opacity().is_some()) + match blend.mode() { + rframe::ScopeBlendMode::Multiply => true, + rframe::ScopeBlendMode::Normal => blend.opacity().is_none(), + rframe::ScopeBlendMode::Screen => blend.opacity().is_some(), + } } #[cfg(test)] @@ -2729,6 +2733,10 @@ mod isolated_blend_policy_tests { } fn pixel(blend: rframe::ScopeBlend, float_first: bool) -> Vec { + pixel_with_source_alpha(blend, float_first, 149) + } + + fn pixel_with_source_alpha(blend: rframe::ScopeBlend, float_first: bool, alpha: u8) -> Vec { let mut surface = skia_safe::surfaces::raster_n32_premul((1, 1)).unwrap(); surface.canvas().clear(Color::from_argb(170, 66, 101, 137)); let mut restore = Paint::default(); @@ -2742,7 +2750,9 @@ mod isolated_blend_policy_tests { surface .canvas() .save_layer(&SaveLayerRec::default().paint(&restore)); - surface.canvas().clear(Color::from_argb(149, 215, 104, 67)); + surface + .canvas() + .clear(Color::from_argb(alpha, 215, 104, 67)); surface.canvas().restore(); read_pixels(&mut surface, 1, 1) } @@ -2824,9 +2834,41 @@ mod isolated_blend_policy_tests { let warm = pixel(blend, false); let _ = pixel(scope(0.6), false); assert_eq!(pixel(blend, false), warm); - ISOLATED_BYTE_BLENDERS.with(|cache| *cache.borrow_mut() = [None, None]); + ISOLATED_BYTE_BLENDERS.with(|cache| *cache.borrow_mut() = [None, None, None]); assert_eq!(pixel(blend, false), warm); } + + #[test] + fn isolated_normal_raster_matches_integer_math_for_every_source_alpha() { + let q = |v: u32| (v + 127) / 255; + let destination = [q(66 * 170), q(101 * 170), q(137 * 170), 170]; + let blend = rframe::ScopeBlend::new(rframe::ScopeBlendMode::Normal, None); + assert!( + !uses_isolated_byte_blender(rframe::ScopeBlend::new( + rframe::ScopeBlendMode::Normal, + scope(0.6).opacity(), + )), + "partial Normal must retain the existing isolated-opacity path" + ); + for alpha in 0..=255_u32 { + preflight_isolated_blend(blend).unwrap(); + assert!(uses_isolated_byte_blender(blend)); + let source = [q(215 * alpha), q(104 * alpha), q(67 * alpha), alpha]; + let expected: Vec<_> = source + .iter() + .zip(destination) + .map(|(s, d)| (s + q(d * (255 - source[3]))) as u8) + .collect(); + let warm = pixel_with_source_alpha(blend, false, alpha as u8); + assert_eq!(warm, expected, "source alpha byte {alpha}"); + ISOLATED_BYTE_BLENDERS.with(|cache| cache.borrow_mut()[2] = None); + assert_eq!( + pixel_with_source_alpha(blend, false, alpha as u8), + warm, + "fresh binding at source alpha byte {alpha}" + ); + } + } } fn procedural_filter_blender( From 077dbd0466c6c2181f1092e09c6e2fc26246f072 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 21:07:49 +0900 Subject: [PATCH 5/7] websem: record portable blending proof and measured costs --- crates/n0/README.md | 12 ++ crates/n0/tests/group_blending.rs | 59 ++++++-- crates/n0_cli/README.md | 2 +- docs/wg/consolidation/svg-engine-of-record.md | 142 ++++++++++++++---- fixtures/web-first/README.md | 3 +- 5 files changed, 179 insertions(+), 39 deletions(-) diff --git a/crates/n0/README.md b/crates/n0/README.md index 3d12b778..a1a1c439 100644 --- a/crates/n0/README.md +++ b/crates/n0/README.md @@ -308,6 +308,18 @@ the previously blended pixels. `tests/group_blending.rs` pins these consumer laws with hand-built frames and exact pixel probes. Those tests do not claim Chromium parity or measured performance. +Multiply and partial-opacity Screen restoration explicitly round byte opacity before scaling source +bytes and applying the byte-domain blend. This avoids pinned Skia's differing +ARM/x86 low-precision arithmetic and runtime-blender opacity ordering. +Construction is preflighted without drawing; failure returns an owner-bearing +`glyphless::BuildError::Blend`. A thread-local cache holds one compiled effect +per mode and at most 256 immutable opacity bindings per mode, never destination pixels. Tests +execute every opacity byte against integer arithmetic and prove binding reuse +equals fresh construction. Unit-Normal restoration also uses exact byte +source-over to avoid the x86 sprite blitter's separate approximation at +partial-alpha edges. Partial-opacity Normal retains the existing isolated +opacity path, and unit-opacity Screen remains native. + With the `trace` feature, `n0::trace::sink::drain_blend_layers()` drains typed `BlendLayerMetrics`, separate from duration samples: one aggregate per outermost drawlist execution, including recursive resource recording. It counts blend diff --git a/crates/n0/tests/group_blending.rs b/crates/n0/tests/group_blending.rs index e82108e2..b4994d2c 100644 --- a/crates/n0/tests/group_blending.rs +++ b/crates/n0/tests/group_blending.rs @@ -301,9 +301,13 @@ mod layer_metrics { } #[test] - fn instrumented_execute_matches_the_same_native_operations_without_observation() { + fn instrumented_execute_matches_equivalent_operations_without_observation() { // Instrumentation equivalence, not an independent Chromium oracle. - // The control never calls access_top_layer_pixels. + // The control independently spells the ordered byte operations for + // Multiply/partial Screen; native lowp restoration is not portable. + // Partial Normal retains native opacity. No control operation calls + // access_top_layer_pixels or a production blend helper. + drain_blend_layers(); let source = CGColor::from_rgba(205, 104, 67, 153); let bounds = rect(8.25, 8.125, 24.5, 24.75); for mode in MODES { @@ -314,18 +318,52 @@ mod layer_metrics { ])) .unwrap(); let instrumented = raster(&product, BACKDROP); + assert_eq!(drain_blend_layers().len(), 1, "{mode:?} execute observed"); let mut control = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); let canvas = control.canvas(); canvas.clear(skia_safe::Color::from_rgb( BACKDROP.r, BACKDROP.g, BACKDROP.b, )); let mut restore = skia_safe::Paint::default(); - restore.set_alpha_f(0.5); - restore.set_blend_mode(match mode { - ScopeBlendMode::Normal => skia_safe::BlendMode::SrcOver, - ScopeBlendMode::Multiply => skia_safe::BlendMode::Multiply, - ScopeBlendMode::Screen => skia_safe::BlendMode::Screen, - }); + match mode { + ScopeBlendMode::Normal => { + restore.set_alpha_f(0.5); + restore.set_blend_mode(skia_safe::BlendMode::SrcOver); + } + ScopeBlendMode::Multiply | ScopeBlendMode::Screen => { + let expression = match mode { + ScopeBlendMode::Multiply => { + "q(s * (255.0 - d.a) + d * (255.0 - s.a) + s * d)" + } + ScopeBlendMode::Screen => "s + d - q(s * d)", + ScopeBlendMode::Normal => unreachable!(), + }; + let shader = format!( + r#" +uniform float opacity_byte; +float4 q(float4 value) {{ + return floor((value + 127.0) / 255.0); +}} +half4 main(half4 src, half4 dst) {{ + float4 s = floor(float4(src) * 255.0 + 0.5); + float4 d = floor(float4(dst) * 255.0 + 0.5); + s = q(s * opacity_byte); + float4 result = {expression}; + return half4(clamp(result, 0.0, 255.0) / 255.0); +}} +"# + ); + let effect = skia_safe::RuntimeEffect::make_for_blender(shader, None) + .expect("test control byte blender compiles"); + // round(0.5 * 255) = 128. Scale inside the blender once; + // restore paint alpha remains one, avoiding float-first scaling. + restore.set_blender( + effect + .make_blender(skia_safe::Data::new_copy(&128.0_f32.to_ne_bytes()), None) + .expect("test control opacity binding is valid"), + ); + } + } canvas.save_layer(&skia_safe::canvas::SaveLayerRec::default().paint(&restore)); let mut paint = skia_safe::Paint::default(); paint.set_anti_alias(true); @@ -342,8 +380,11 @@ mod layer_metrics { read_pixels(&mut control, SIZE, SIZE), "{mode:?}" ); + assert!( + drain_blend_layers().is_empty(), + "{mode:?} control unobserved" + ); } - drain_blend_layers(); } } diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 8c36c918..d11236e8 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,7 +607,7 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,378 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,382 Chromium-baked cells plus 16 sampled frames; the text estate contains sixteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two Bungee), and the named refusal register has 303 rows. `feFlood`, `feComposite`, diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 291c2da2..2e5484ed 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -116,7 +116,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,378 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,382 Chromium-baked primitive cells plus 16 sampled frames, with a separate sixteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -5768,8 +5768,10 @@ explicit root isolation; it does not pretend the command paints the HTML page. Five additional direct/instance/anchor/mapped-viewport cells and eight explicit-Normal-isolation opacity controls cover opaque-backdrop interactions. Nineteen further cells cover the actual outer root and redundant isolation -over translucent backdrops. The rung adds 94 exact primitive/HTML cells, -taking the corpus from 1,284 to 1,378, and 43 named refusals, taking that +over translucent backdrops. Four more cross multiply/screen at opacity .123456 +with opaque/translucent source colors over a translucent destination. The rung +adds 98 exact primitive/HTML cells, taking the corpus from 1,284 to 1,382, +and 43 named refusals, taking that register from 260 to 303. The sixteen sampled frames and separate sixteen pixel/eight geometry text witnesses are unchanged. @@ -5809,6 +5811,59 @@ Cross-seam tests separately execute actual outer-root multiply/screen frames onto opaque and translucent colored caller canvases; removing their initial Normal boundary changes those results. +### Portable byte arithmetic + +Hosted x86 testing caught 24 new multiply cells departing by one or two code +values while the same cells were exact on ARM. In pinned Skia 0.99.0, +`SkRasterPipeline_opts.h` uses accurate divide-by-255 on NEON but approximate +`(value + 255) / 256` on x86 for this low-precision operation. For example, +source green 104 times destination green 101 produces 41 with accurate +rounding, but 42 with that approximation. This is the same backend arithmetic +class already established by the filter-blend rung, not a new tolerance. + +Multiply restoration now uses explicit byte arithmetic. Sharing only the +filter blender was insufficient: a runtime blender promotes its surrounding +pipeline to high precision, so leaving the original float paint opacity in +place changes normalization order. At opacity .123456 with partial source and +destination alpha, that prototype changed 1,600 pixels by one code value. +The four small-opacity cells guard the corrected order: quantize opacity to a +byte, round each premultiplied source-byte product, then blend. Restore-paint +alpha stays one, so opacity is not applied twice. Sixteen scratch candidates +cross multiply/screen, .123456/.499/.501/.999 opacity and opaque/translucent +sources; all match Chromium through both actual CLI admissions on ARM +**(measured, not celled)** apart from the four committed .123456 witnesses. + +Screen's final blend uses accurate arithmetic, but pinned source inspection +shows its preceding partial-opacity scale shares the same x86 approximation. +The ordered helper therefore also carries screen with non-unit opacity. +The next hosted x86 run passes multiply and all four small-opacity cells, but +leaves one isolated-rotation cell: 141 pixels at delta 1. Its first differing +RGBA pixel is `[70,100,134,255]` instead of `[71,101,135,255]`. + +This is a second source-over backend path. Pinned `SkBlitRow_opts.h` implements +the AVX2/SSE2 sprite restore as `s + floor(d * (256 - sa) / 256)`, while NEON +uses accurate divide-by-255 rounding. A mutation of only the new unit-Normal +restore to that formula reproduces 141 differing pixels and the same first +pixel. The raster-pipeline approximation alone produces a different 135-pixel +signature, so the two backend paths are not conflated. Exact byte source-over +now carries unit-Normal blend boundaries as well. Partial-opacity Normal keeps +the established native isolated-opacity operation and its independently +guarded byte behavior; unit-opacity screen also stays native. No old opacity +operation is changed. The complete 1,382-cell gate then passes on ARM and +hosted x86 with the scoped correction, without an oracle or tolerance change. + +One compiled effect per mode per thread and at most 256 immutable opacity +bindings per mode amortize shader construction; this is a code/uniform cache, never a pixel or +backdrop cache. Frame compilation preflights fallible construction and returns +an owner-bearing `BuildError::Blend` on failure. It issues no raster commands. +Execution tests guard the static shader's raster lowering against independent +integer arithmetic for every opacity byte in multiply/screen and every source +alpha byte in unit-Normal, distinguish float-first ordering, and prove warm +binding reuse equals fresh construction. Three effect slots are bounded; the +Normal slot is used only with unit opacity. +The group shader enables Skia's optimizer; its exactness is independently +gated, and the existing filter-blender configuration is unchanged. + ### The precision stop Clean rendering did not establish correctness. Before the new guards, an @@ -5850,6 +5905,12 @@ unit, transformed and clipped isolation controls at 1,024 pixels/delta 1. Restoring the compiler's exact pre-mutation bytes returns the full gate to green. The alpha correction therefore has its own sensitive external witness. +A third mutation replaces accurate division with the x86 approximation in +only the new ordered group shader. The gate fails 28 blend cells, including +both small-opacity source profiles in both modes at 1,600 pixels/delta 1 or 2. +Restoring the exact shader-source bytes returns all 1,382 positive cells and +303 named refusals to green. This separately guards the portable correction. + On aarch64, baseline and B1 have identical `size_of` results: `FrameItem` and `FrameNode` 200 bytes, `ScopeEffect` 64, drawlist `ItemKind` 136 and `Item` 168. This is a data-layout observation, not proof of unchanged execution cost. @@ -5863,8 +5924,12 @@ not measure allocator capacity, Skia-internal allocations or GPU memory. The matched CPU-raster measurement used the clean pre-rung revision `fd4097f2` and B1 on the same Apple M2 Ultra, 128 GiB host, macOS 26.5.1, Rust 1.92.0 aarch64, skia-safe 0.99.0, release builds with tracing off. -Each process warms 20 replays, then takes 20 source-compile, 80 frame-compile -and 160 paint samples; three repetitions alternate baseline/current order. +Each workload records its first frame compile and paint separately, warms five +replays, then takes 20 source-compile, 80 frame-compile and 40 paint samples; +three repetitions alternate baseline/current order. This bounded final +protocol applies equally to baseline and current. Earlier exploratory runs +used 20 warmups and 160 paint samples; their numbers are not substituted into +the matched table. Cargo startup, file/PNG work, canvas clear and the input clone are outside their respective timed loops. Source compilation includes document and cascade construction. Other task builds, captures and tests are stopped during the @@ -5883,32 +5948,52 @@ Numbers below are the median of three within-run p50s, in microseconds. | Workload | Source compile, B1 | Frame compile, baseline → B1 | Paint, baseline → B1 | | --- | ---: | ---: | ---: | -| 100 neutral groups | 1,463.4 | 17.46 → 17.50 | 47.17 → 47.42 | -| 1,000 neutral groups | 15,606.1 | 186.33 → 185.54 | 429.21 → 430.67 | -| 100 neutral groups, depth 48 | 8,414.2 | 17.46 → 17.50 | 47.13 → 47.00 | -| 100 opacity groups | 1,515.4 | 25.29 → 25.04 | 3,333.29 → 3,336.54 | -| 100 multiply groups | 1,622.2 | not admitted → 25.92 | not admitted → 4,026.75 | -| 1,000 multiply groups | 17,120.5 | not admitted → 295.25 | not admitted → 40,038.04 | -| 100 screen groups | 1,617.0 | not admitted → 25.92 | not admitted → 4,027.50 | -| One multiply group containing 100 pairs | 1,614.4 | not admitted → 20.17 | not admitted → 104.75 | - -The unaffected controls show no repeatable increase above the plan's 5% -investigation threshold; this is not a universal regression guarantee. One -100-neutral current repetition is slower than the other two: source, -frame-compile and paint p50 ranges are 1,443.4–2,152.2, 17.42–21.50 and -47.04–56.29 µs respectively. The 1,000-neutral current paint p50 range is -430.21–432.08 µs; multiply-100 is 4,022.04–4,236.71 µs, with the largest -within-run p95 4,552.21 µs. The raw per-stage p50/p95/p99/min/max distributions +| 100 neutral groups | 1,434.8 | 17.71 → 17.58 | 46.96 → 47.50 | +| 1,000 neutral groups | 15,509.0 | 185.67 → 186.50 | 428.25 → 432.21 | +| 100 neutral groups, depth 48 | 8,373.3 | 17.46 → 17.92 | 47.54 → 47.08 | +| 100 opacity groups | 1,528.0 | 25.46 → 26.79 | 3,329.75 → 3,348.25 | +| 100 multiply groups | 1,578.6 | not admitted → 27.21 | not admitted → 131,461.92 | +| 1,000 multiply groups | 16,717.9 | not admitted → 302.71 | not admitted → 1,304,405.25 | +| 100 screen groups, unit opacity | 1,567.3 | not admitted → 26.46 | not admitted → 5,205.21 | +| One multiply group containing 100 pairs | 1,555.9 | not admitted → 20.58 | not admitted → 2,531.71 | + +The old-opacity frame-compile median crosses the plan's 5% investigation +threshold (25.46 → 26.79 µs). A focused quiet rerun of that unchanged workload, +with five alternating baseline/current process pairs and the same per-stage +sampling, gives 25.21 → 25.83 µs; ranges overlap at 25.00–27.50 and +25.04–27.83 µs. Paint in that rerun is 3,344.71 → 3,351.38 µs. The alert does +not repeat above the threshold in that follow-up median; the original alert +is retained, not replaced. Other unaffected stage medians remain below the +threshold. This is not a universal regression guarantee. + +One 100-neutral current repetition is slower: source, frame-compile and paint +p50 ranges are 1,421.92–2,156.58, 17.42–21.83 and 46.92–58.08 µs; baseline +paint is 46.75–47.25 µs. The 1,000-neutral current paint range is +427.79–432.54 µs, versus baseline 427.96–434.79 µs. Multiply-100 is +130,987.79–131,516.63 µs, with the largest within-run p95 136,350.08 µs. +The raw per-stage p50/p95/p99/min/max distributions are retained in the ignored local execution record; no portable FPS claim is drawn from this machine. +The first multiply-100 frame compilation, including the thread's first +blend-effect construction, takes 671.96–1,021.50 µs, versus a steady median 27.21 µs. +Its first paint is 131,080.88–133,097.67 µs. These are whole-stage first-use +samples, not an isolated shader-compilation timer. The portable path is +substantially slower than the pre-correction native-only experiment (about +4.03 ms for 100 groups), which was pixel-wrong on x86. An unoptimized portable +shader took 137.50 ms for that workload in an exploratory repetition; enabling +the optimizer took 130.52 ms. A transparent-source shortcut instead took +158.00 ms and was removed. Those exploratory timings are not additional +matched repetitions. Partial-opacity screen uses the portable helper but was +not separately timed. The bounded slice is not a claim of realtime throughput. + The redundant-isolation workload uses the same source pairs with explicit `isolation:isolate` and no blending descendants. At 100 / 1,000 groups it -emits 201 / 2,001 frame items and zero blend saves; paint p50 is 47.17 / 431.13 -µs. Source compilation is 1,622.6 / 17,124.9 µs, including cascade and the +emits 201 / 2,001 frame items and zero blend saves; paint p50 is 47.38 / 432.96 +µs. Source compilation is 1,578.9 / 16,807.0 µs, including cascade and the elision ledger. Adding 48 isolated ancestors to the 1,000-group workload -still emits 2,001 items and zero blend saves, with 430.50 µs paint p50. Its -90,735.1 µs source compilation includes existing depth-dependent walks; +still emits 2,001 items and zero blend saves, with 432.04 µs paint p50. Its +90,182.0 µs source compilation includes existing depth-dependent walks; the linear final compaction is not a claim that the entire compiler is linear. Untimed trace observation makes the cost concrete. Multiply-100 emits 403 @@ -5922,10 +6007,11 @@ surface and non-blend allocations are excluded. Neutral groups report zero blend saves, not an estimate of zero total renderer allocation. With the 100-group source positions held fixed and the canvas changed to -128×128 or 512×512, paint p50 becomes 1,196.54 or 15,295.17 µs; observed +128×128 or 512×512, paint p50 becomes 33,014.71 or 524,706.00 µs; observed cumulative blend storage becomes 6,619,136 or 105,906,176 bytes. The smaller canvas also clips some source draws, so this is an allocation/viewport -experiment, not pure equal-coverage area scaling. The practical cost is repeated -active-clip-sized source materialization, not a larger semantic tree. Source +experiment, not pure equal-coverage area scaling. The practical cost combines +repeated active-clip-sized source materialization with explicit byte arithmetic +over those layers, not a larger semantic tree. Source extent/precision must be proved before tightening bounds or pooling layers; B1 records that cost and leaves the optimization unshipped. diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 44812c3b..065a9a3f 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,7 +19,7 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): sixteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,378 primitive cells plus 16 sampled frames, those twenty-four text +estate is 1,382 primitive cells plus 16 sampled frames, those twenty-four text witnesses, and 303 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The @@ -29,6 +29,7 @@ pixel claim. | File | Role | | --- | --- | | `svg-group-blend-{normal,multiply,screen}-{leaf,group,opacity,alpha,transparent,stroke}.svg` · `svg-group-blend-{multiply,screen}-each.svg` | B1's exact group-operation controls: mode-sensitive backdrops, overlapping children, combined opacity, translucency, transparent initial source and fill/stroke composition. Whole-group versus per-child blending changes 576 pixels at maximum deltas 89 (multiply) and 98 (screen). No new tolerance. | +| `svg-group-blend-{multiply,screen}-opacity-small-{opaque,partial}.svg` | Opacity .123456 with opaque/translucent source colors over a translucent destination. The multiply partial-source cell rejects a float-first opacity prototype at 1,600 pixels/delta 1: byte opacity must be applied before the byte-domain blend. Hosted x86 also guards the low-precision backend's approximate division; no oracle or tolerance is relaxed. | | `svg-group-blend-{neutral,isolated,clip,partial,opacity,outer-opacity}.svg` and their `-isolated` controls | Authored clip-path and partial opacity isolate descendants; a neutral group does not. Same-element blend plus opacity differs from an outer opacity group by 2,048 pixels at delta 76. Unit-opacity isolation is a real boundary, not an absent effect. | | `svg-group-blend-{translate,scale,rotate,nested-hidden}.svg` and their `-isolated` controls | Ordinary affine transforms and a nested SVG overflow clip do not isolate. Adding isolation changes 2,048 / 1,536 / 2,160 pixels for the transform trio, and 2,048 for the viewport case, all at maximum delta 152. | | `svg-group-blend-{linear,pattern,fractional}-{normal,multiply,screen}.svg` · `svg-group-blend-opacity-{0,1,p00392156862745098,p5019607843137255,p6}.svg` | Rectangular source paint/materialization and opacity boundaries, including a linear ramp, an existing repeating-vector paint, fractional rectangle edges, zero/unit opacity and fractional group alpha. This does not admit blending inside the pattern source. | From 7d42ab20faedc8ed92b197fcb026403f305bc6d6 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 21:25:44 +0900 Subject: [PATCH 6/7] test: pin near-unit SVG group opacity boundaries --- crates/n0_cli/README.md | 2 +- docs/wg/consolidation/svg-engine-of-record.md | 18 ++- fixtures/web-first/README.md | 3 +- fixtures/web-first/STATUS.md | 14 ++- ...vg-group-blend-near-unit-isolated-near.png | Bin 0 -> 1696 bytes ...vg-group-blend-near-unit-isolated-p998.png | Bin 0 -> 1725 bytes ...vg-group-blend-near-unit-isolated-p999.png | Bin 0 -> 1696 bytes ...vg-group-blend-near-unit-isolated-unit.png | Bin 0 -> 1696 bytes .../svg-group-blend-near-unit-plain-near.png | Bin 0 -> 1488 bytes .../svg-group-blend-near-unit-plain-p998.png | Bin 0 -> 1509 bytes .../svg-group-blend-near-unit-plain-p999.png | Bin 0 -> 1488 bytes .../svg-group-blend-near-unit-plain-unit.png | Bin 0 -> 1504 bytes .../svg-group-blend-near-unit-screen-near.png | Bin 0 -> 1510 bytes .../svg-group-blend-near-unit-screen-p998.png | Bin 0 -> 1520 bytes .../svg-group-blend-near-unit-screen-p999.png | Bin 0 -> 1510 bytes .../svg-group-blend-near-unit-screen-unit.png | Bin 0 -> 1510 bytes fixtures/web-first/oracle-bake.json | 110 +++++++++++++++++- fixtures/web-first/primitives.json | 96 +++++++++++++++ ...vg-group-blend-near-unit-isolated-near.svg | 1 + ...vg-group-blend-near-unit-isolated-p998.svg | 1 + ...vg-group-blend-near-unit-isolated-p999.svg | 1 + ...vg-group-blend-near-unit-isolated-unit.svg | 1 + .../svg-group-blend-near-unit-plain-near.svg | 1 + .../svg-group-blend-near-unit-plain-p998.svg | 1 + .../svg-group-blend-near-unit-plain-p999.svg | 1 + .../svg-group-blend-near-unit-plain-unit.svg | 1 + .../svg-group-blend-near-unit-screen-near.svg | 1 + .../svg-group-blend-near-unit-screen-p998.svg | 1 + .../svg-group-blend-near-unit-screen-p999.svg | 1 + .../svg-group-blend-near-unit-screen-unit.svg | 1 + 30 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-near.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-p998.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-p999.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-unit.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-plain-near.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-plain-p998.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-plain-p999.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-plain-unit.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-screen-near.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-screen-p998.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-screen-p999.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-screen-unit.png create mode 100644 fixtures/web-first/svg-group-blend-near-unit-isolated-near.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-isolated-p998.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-isolated-p999.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-isolated-unit.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-plain-near.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-plain-p998.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-plain-p999.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-plain-unit.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-screen-near.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-screen-p998.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-screen-p999.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-screen-unit.svg diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index d11236e8..37f89394 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,7 +607,7 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,382 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,394 Chromium-baked cells plus 16 sampled frames; the text estate contains sixteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two Bungee), and the named refusal register has 303 rows. `feFlood`, `feComposite`, diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 2e5484ed..2dd44ee1 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -116,7 +116,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,382 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,394 Chromium-baked primitive cells plus 16 sampled frames, with a separate sixteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -5770,12 +5770,26 @@ explicit-Normal-isolation opacity controls cover opaque-backdrop interactions. Nineteen further cells cover the actual outer root and redundant isolation over translucent backdrops. Four more cross multiply/screen at opacity .123456 with opaque/translucent source colors over a translucent destination. The rung -adds 98 exact primitive/HTML cells, taking the corpus from 1,284 to 1,382, +adds 110 exact primitive/HTML cells, taking the corpus from 1,284 to 1,394, and 43 named refusals, taking that register from 260 to 303. The sixteen sampled frames and separate sixteen pixel/eight geometry text witnesses are unchanged. +Twelve review-boundary witnesses cross a rotated Normal-isolated group with +blending children, a plain Normal-isolated group, and a screen group at opacity +`1`, `0.9999999403953552` (the next smaller `f32`), `.999` and `.998`: +`svg-group-blend-near-unit-{isolated,plain,screen}-{unit,near,p999,p998}`. +Chromium gives identical pixels for the first three opacity values in the +blending-child and screen groups. Against unit, `.998` differs at 2,151 pixels +(maximum delta 2) and 2,003 pixels (maximum delta 1), respectively. The plain +group instead differs between unit and either near-unit value at 143 pixels +(maximum delta 1): even when the final opacity quantizes to byte 255, an +authored partial-opacity group must keep its source layer. All twelve sources +match Chromium in both command admissions on ARM. The corresponding four +multiply probes also match, with unit/near/`.999` identical and `.998` +differing at 1,013 pixels (maximum delta 1) **(measured, not celled)**. + HTML exterior-backdrop controls differ when isolation is added **(measured, not celled)**. That exterior paint is absent from this command's established SVG-local extraction contract. An escaping inline blend, a diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 065a9a3f..1f7ca894 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,7 +19,7 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): sixteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,382 primitive cells plus 16 sampled frames, those twenty-four text +estate is 1,394 primitive cells plus 16 sampled frames, those twenty-four text witnesses, and 303 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The @@ -30,6 +30,7 @@ pixel claim. | --- | --- | | `svg-group-blend-{normal,multiply,screen}-{leaf,group,opacity,alpha,transparent,stroke}.svg` · `svg-group-blend-{multiply,screen}-each.svg` | B1's exact group-operation controls: mode-sensitive backdrops, overlapping children, combined opacity, translucency, transparent initial source and fill/stroke composition. Whole-group versus per-child blending changes 576 pixels at maximum deltas 89 (multiply) and 98 (screen). No new tolerance. | | `svg-group-blend-{multiply,screen}-opacity-small-{opaque,partial}.svg` | Opacity .123456 with opaque/translucent source colors over a translucent destination. The multiply partial-source cell rejects a float-first opacity prototype at 1,600 pixels/delta 1: byte opacity must be applied before the byte-domain blend. Hosted x86 also guards the low-precision backend's approximate division; no oracle or tolerance is relaxed. | +| `svg-group-blend-near-unit-{isolated,plain,screen}-{unit,near,p999,p998}.svg` | Rotated groups at opacity 1, the next smaller f32, .999 and .998. The first three match for isolated blending children and screen; plain isolation instead changes 143 pixels/delta 1 at the unit-to-partial boundary. A byte-255 restore must not erase the authored partial-opacity source layer. The .998 controls discriminate the next opacity byte. | | `svg-group-blend-{neutral,isolated,clip,partial,opacity,outer-opacity}.svg` and their `-isolated` controls | Authored clip-path and partial opacity isolate descendants; a neutral group does not. Same-element blend plus opacity differs from an outer opacity group by 2,048 pixels at delta 76. Unit-opacity isolation is a real boundary, not an absent effect. | | `svg-group-blend-{translate,scale,rotate,nested-hidden}.svg` and their `-isolated` controls | Ordinary affine transforms and a nested SVG overflow clip do not isolate. Adding isolation changes 2,048 / 1,536 / 2,160 pixels for the transform trio, and 2,048 for the viewport case, all at maximum delta 152. | | `svg-group-blend-{linear,pattern,fractional}-{normal,multiply,screen}.svg` · `svg-group-blend-opacity-{0,1,p00392156862745098,p5019607843137255,p6}.svg` | Rectangular source paint/materialization and opacity boundaries, including a linear ramp, an existing repeating-vector paint, fractional rectangle edges, zero/unit opacity and fractional group alpha. This does not admit blending inside the pattern source. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 523c7a8b..59e357a4 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1382) +## Chromium-baked cells (1394) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -634,6 +634,18 @@ to its fixture source. No new image is committed for this view. svg-group-blend-multiply-opacity-small-partial svg-group-blend-multiply-stroke svg-group-blend-multiply-transparent +svg-group-blend-near-unit-isolated-near +svg-group-blend-near-unit-isolated-p998 +svg-group-blend-near-unit-isolated-p999 +svg-group-blend-near-unit-isolated-unit +svg-group-blend-near-unit-plain-near +svg-group-blend-near-unit-plain-p998 +svg-group-blend-near-unit-plain-p999 +svg-group-blend-near-unit-plain-unit +svg-group-blend-near-unit-screen-near +svg-group-blend-near-unit-screen-p998 +svg-group-blend-near-unit-screen-p999 +svg-group-blend-near-unit-screen-unit svg-group-blend-nested-hidden svg-group-blend-nested-hidden-isolated svg-group-blend-neutral diff --git a/fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-near.png b/fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-near.png new file mode 100644 index 0000000000000000000000000000000000000000..5f68ec8843b134f066692cf11eb689c7d6b6bc2e GIT binary patch literal 1696 zcmV;R24DG!P)000J6Nkl({I0nH(3Z8UY=`2F$6S8YC@&e*Wf;<%# zV?bcy;sDqr^$SM?!t&6}ljfID>VQ;Y$pTA1W9eg1U{dl65P`uOv|T#Pg8q;kV!&i` z%r8I=8pL;kOftv(0^~^p_+^`81(s^zh=wG;0Gs4Bj3;Z3)!0lCQV3IK$^Ljb<_jPR z-36jTc*0Lx0QDoXX8`=o;y5N4`!H|v=a?^mTY>V&kMQEJlNQG3Q0_8um}4-^u?9qa zeu6K69BIPUD@q3UN$oz=uw7?S#2BTSdye_C5_!0)5pYt9bU&o~-_SokBW3rV-XpLA z$8pkwDd{<6+&g#!+?q)K*lGauC7XBe;|*|o>~Ki~ooz(UK{AG&uSR1g<8F{1B&&GfGR>gk}eeYs%J zL6l)^fIFOb03y!_$u*AtQdGY24hCE*m|ZsC^SxAb?8m!?l&-%GB;cC;LctssUMhPX zt#QN*-Sb>~0JPA!yZ|0a4h6s~td`1ZCYdoAGxb)Z->7Q!s@7@%vv)N100Attxbzl; zp{~jH`orH!2$d6Q49MLx2fEfgYX9|%+3Q~XUfWOMwa`L*MTlh}4882yTxlvwN-Aq= z+U0-Q00gkm(rP&QgL=`kPnCY7W;^ZL00gkm;&M3ogDR@_U#-2a+8(z7xD7|ZB^=<8 z3ZOM$M>@0pm)5A+9=8J^{0>5yg)p?!wZHsESJhAKv-%1s;W%KP3kMt$FBl z%0++x7Ft}o3tp92VBp{0=W2b?F+e!c2)*12sX4TSH_jUr3x%x@ipWr4u`7UryT zBXA1xk8B+S9|^CM<_4+fp}s;+oM>n)BgB^>3^`qX#vJbeBiVZ)9q8zx1GZ;CLvObM z4dlt3f6NhP{&el-s=VxVX0e9OsXyY1aVdjrR1^15* zT+icqPK?5bb5Gdn6Idk%!~5L_v-dVt^G1Zq}dAh4#;+zib{=!!xq|FW#)PynW`X)TXwJsQ)ob5RUo zLH3K{bO&&<&{hQpkkgBo58_PtOf{bkujn8qko=H@-ip#W(GA9_0Q9D}FAfSo1v#E_ z?{Z?W-%bq-Y-eSH82PAhGeVT}NrwKpIpDLL?sX)Z-*I4{(=TWG6- zPow{eF}+>KI4y4zQC8AfCAEklM8{esyIW|h#s;tmU01KVhkZR!C3iJ4lnh5^MbBFRc>SS`aG zi#R?((pnxC+N!Guu!!lHW4jo`FOJPC$;IJRmPixf+|TfT`C-BURt-+t7@ghcL=fb} z0+x!5H4_`Y+MzW$wg#sLa3*FL{nrN=0KZc?qGXo!HpkA$^Z~3gwO+Tf!*$9V|MvPn q00030|4xN000JZNklmz z1Z>C!6AUqn1KzdwHa&|~V$9xVy_oRR5nAlowf~-TX8tp~!k0h)0#_Lht~d+=1Zv-d z>L|3~Lqdq&bdEHB*2izA?$?i|~Lp$+PGO9iq< ze>oQEna9DYqU zb>Pb;~1tHD8V@vX;>(cehYvV1@!x5 z`zP+7ugGR_%kUm3(s$#R=YOy^f*XYQz^Q{6JQi>-=vZ)@@E%y<2Aw~rR3Kv$KDbSY z0G`^5Fo+8FAtC1DC`m&4#uK>48fufJ2aX$hFXBAp*`@=z)QgT9jr% zDfGXEz8oqv6Qx;D4=`Ei4(U&SpnAisuGwl8`ePIN7O+AX^M?~scfKRMh=)#7HFws` z?KNxrv7y?IK_R+LMvJCOaDVpdPY>{Y&lD_AYn>3k;dQ0iEXgLZwl zU9TC9I-%6Fy|;iE4nmmv2$Dc|=0(VyS;k*Dg3oR6Hom%KV+3+iX<~v7k`ki$~Knx3=)c6Db18B=<0PS$5hz-jB zN=q9wYep^uapn#rgzpsQpFhyk15F}7pbHORL2zu`^u2&TcIIN#kLzP+S5b16>h z@9ra7=r@M9gD?a02UrEXhmie|xrdtgP&0S}vog0h#4V<{>oUi&FU!h41wbvsI1HJW z;@=^Z^M%fVP2B+m<2Q679Nb88i)jpgLW(NIZw6@N1WZGGaUTMd!uc0*Bz&eh&)i?s z8s$mJRl+T#xobj@m@VCZ4G>1`6vFjh$kxq~P5(Z&G4Oa&A}FJj=5M5kd^X9@KdTm- zh3+P%0BJmjn>^9m*9=-S>^~Yd7EDp<i7rhrsj0o~x*YdvTJj$|ui3OM-dcR4rZhB83}2`+%P8eYK=TY%Hn z_39>uyGJ;dm0Ts0;x&bEF*oOW2)5AO^#3#xMZ1F0tgI11L>aO&B`K49y02AosDn5q z$;YYCb+u~S7|xdeT?HW=9nbs3X)R5zr5`G(3W`^VxhaaZ~@(*Icx2{!oXvG zWGsbBl!BPa5&q2AXsZnC@_`EI#y)N|F|X;P_t(P%Ji?R)?Nx}ZkuWNu;L%0IpS9{>OV|NliXD$)P|00v1!K~w_(m?X-- Tn;G4}00000NkvXXu0mjftnn_Y literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-p999.png b/fixtures/web-first/chromium/svg-group-blend-near-unit-isolated-p999.png new file mode 100644 index 0000000000000000000000000000000000000000..5f68ec8843b134f066692cf11eb689c7d6b6bc2e GIT binary patch literal 1696 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 M07*qoM6N<$g6TEMy#N3J literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-near-unit-screen-p998.png b/fixtures/web-first/chromium/svg-group-blend-near-unit-screen-p998.png new file mode 100644 index 0000000000000000000000000000000000000000..af49b69364dcdf89d62bb73d78f04d8283723e80 GIT binary patch literal 1520 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 M07*qoM6N<$g6TEMy#N3J literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-group-blend-near-unit-screen-unit.png b/fixtures/web-first/chromium/svg-group-blend-near-unit-screen-unit.png new file mode 100644 index 0000000000000000000000000000000000000000..b2a2ad6497172709a659c9243f67ea93de6889ad GIT binary patch literal 1510 zcmV000G>NklS8S2ofU1*fR!0^VRP#!DGUQ`Tb_TGlu!<=U>rN7LA^`2mrh^_4!&T z>hqxglNkK2A4b@pib-hPh{_S)Kj!_pj7Vm$}F7XoZ zl)H+t3Kjx@gG6BZW|}I-{188EqwYXI5u~G5|KFC>Ry4FM;Q|C0dMnIYbO?FrXXzjW zT5gWv0_;Zp6ouJ$sX2xVP>ivNT&6j8#7n%qupGk$*nd<2q?*jJYHv{D`!F&mLSNSA z8W${pq@P*iyl8QPcZ2vc7uF2pyaNRzm0Hq8*Gau#0qn;7$Q^ko>fh;r|Kv7 z!*a;?n|&4Y9q<66P6~ij97BJW<{+SbE2%&NX$FI1onu&5q8N8;1ZF86V7}N0|8kK2 zZZm4#(|!aBL0^I;Snn1qn18K1*nhI!*fR3RR0CK>+H(UO%sqiR)|t>(?dByL0ER+4 zv1J#)ScfgiwvP?KM^Qg)1JH_eD#*5dYyftmD=UDOtd>Q^wwn#W3Jc)GZvGj`3c%Jv zyQ3joNiVCr1_&E`YgGvQNz_eFpx97MpkTO zx{ZKlpUP7LyNYRttO3X-&?XljeR`1OWiO&R2xv#;Zbk%{W~$fp>R3C0Ba8`jq?rI< zp&<~7YN3?r>uoj!ahBMk4I?25!(kDd(4f)GRq8~*Dga~w;J`vxgDL9{Iz=_OIK*Lk z$E&HSUMqV)GE)Tu4&tAZl z^Ct2+MKY;u695Mmx@I^3%r7Vj!)y&P0dQcU>%j%*&ttVN%cifJ0PGV^px_`Hoa+EE z0;aE<0FV{{I0(hF$YZa^>UX<$&4%fvX#lX$xu6e`=ynNT|Mm5(OVa=jEc9(~W%+|1 z1CZtqIEYpthFlgA09gg!a>=oUc1b-!=-=JVy7~aXfrVZ|{xr*Fi>nX5JVn!gJLloi z&;a|)z7|}J1+tJu{-CK5xEtudUv?m1A%g>zy?9_R=Ff+Gvz9f{8|bh>@uUR3qJ8$AeU$63$*{jdN` z*ypKouIEqVvTWUftYqQg++Wo$1mqN~cf}vSMlMUREC56bFc#E!+aEz*vNfAjsw@lD znsp!NPcD+11>~~$n~Gs~4C5RiQE(l=Hh+*~0E+fRvsvV_c=rHeYgyXk?hl77EcB!t z0wgy#k;}4Pzs7P3P*_3*+vg9+v;FK_M6SlgWl!LBU1vFtW?Xp)d-~#q^%^{q!~ZRd z-Rn{bf}*HMCId54GGBlOwV(%T))snF^L}}TVYF*Ci1XL48J{na&qH=4@&$i5gyxX7 zg`U(RM;}JK{{B4#v;&dIh$NBA!Lbw~6lMXS#R5!9>U6FS4(G1>r?X>e*T~(158S6wID9vyrCISe0^h7{Xm8AEYMgh0~kwJHJEcC&R)Lsaa?Ni zgLb`!fBWhICZ+UxH^(KSTb}%{*Z%_m0RR7`?cs<3000I_L_t&o01dl-I{LFOhyVZp M07*qoM6N<$g6TEMy#N3J literal 0 HcmV?d00001 diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index 0343bfee..dbd99bca 100644 --- a/fixtures/web-first/oracle-bake.json +++ b/fixtures/web-first/oracle-bake.json @@ -5,7 +5,7 @@ "bake_script_sha256": "2bdb5f933d072a1e87c9a675c3342fcf506c0955c0f5c26e2988f4e8fa37c4f2", "capture_module_sha256": "069296201718c43d29efe356fbea893781b73250dca51de3b6d47468d74027b0", "suite": "primitives.json", - "suite_sha256": "762a6ec537c0c20615b80626e06c9ae2f11168f7a98029dd86f6759d8a7eec60", + "suite_sha256": "1179a0d86cadb59c0b10825941ae2a7ee433031cd6a3f8e9441f54d2752a396c", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -5482,6 +5482,114 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-near-unit-isolated-near", + "source": "svg-group-blend-near-unit-isolated-near.svg", + "source_sha256": "aaa4ef2682772a050e539adaabe4abd114854fdd458313da3f9644e10998e2a6", + "oracle": "chromium/svg-group-blend-near-unit-isolated-near.png", + "oracle_sha256": "ae1f25fdb182714d59ff23ce6d69bfdcfcc45edd3d1cc92e1a00bc1240404474", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-p998", + "source": "svg-group-blend-near-unit-isolated-p998.svg", + "source_sha256": "f18bebb39666caf06659cc5eb1bbdbd6a86244e72f782aceba3a0b993715b48a", + "oracle": "chromium/svg-group-blend-near-unit-isolated-p998.png", + "oracle_sha256": "3878501db74383b3dc1f63b239ba560a1cba01912e02e71ed814f5cbca51f939", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-p999", + "source": "svg-group-blend-near-unit-isolated-p999.svg", + "source_sha256": "c385a32f0f02c8cad38e7fab96851195ac670c0459430f684d16216b10acc41f", + "oracle": "chromium/svg-group-blend-near-unit-isolated-p999.png", + "oracle_sha256": "ae1f25fdb182714d59ff23ce6d69bfdcfcc45edd3d1cc92e1a00bc1240404474", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-unit", + "source": "svg-group-blend-near-unit-isolated-unit.svg", + "source_sha256": "52b6663a08132782cfffb2982f7cec5d2aff6d568b7b43a1ed816fd4f424a541", + "oracle": "chromium/svg-group-blend-near-unit-isolated-unit.png", + "oracle_sha256": "ae1f25fdb182714d59ff23ce6d69bfdcfcc45edd3d1cc92e1a00bc1240404474", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-near", + "source": "svg-group-blend-near-unit-plain-near.svg", + "source_sha256": "f849bae7a86202d6fd19b4f0315b995c9ea4f964c9a03f0b7f5fda111a22ec21", + "oracle": "chromium/svg-group-blend-near-unit-plain-near.png", + "oracle_sha256": "d1c94cd09b9e33a1a77e79063e0767163f1bfb027db78a8fa35f7bb02e8df024", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-p998", + "source": "svg-group-blend-near-unit-plain-p998.svg", + "source_sha256": "0a61a011847d2a483277590f9e2c3c9a8dbb7fb1a2269afd2e12d3db6974168f", + "oracle": "chromium/svg-group-blend-near-unit-plain-p998.png", + "oracle_sha256": "33846d98c6e04bed4fafb819f2815e6111ddb8362a6cce0803f56cd3753029e0", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-p999", + "source": "svg-group-blend-near-unit-plain-p999.svg", + "source_sha256": "63f88419106c1908cf331e63e603293e31d7fdac5a4780285742c461dcaff8e7", + "oracle": "chromium/svg-group-blend-near-unit-plain-p999.png", + "oracle_sha256": "d1c94cd09b9e33a1a77e79063e0767163f1bfb027db78a8fa35f7bb02e8df024", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-unit", + "source": "svg-group-blend-near-unit-plain-unit.svg", + "source_sha256": "061fcfbf58d9147cbef003c181721eb11f9765463ec0c48b832c9d26765fef04", + "oracle": "chromium/svg-group-blend-near-unit-plain-unit.png", + "oracle_sha256": "9e44a243459ac6f1c2168bbe52c63f045967614006873a3e13f5e964aa52960a", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-near", + "source": "svg-group-blend-near-unit-screen-near.svg", + "source_sha256": "005171685453d59c82568bf3af9b85525805284d0f881dd654989ba7c6292e43", + "oracle": "chromium/svg-group-blend-near-unit-screen-near.png", + "oracle_sha256": "bd4c0f568c7a7506da3cd6b87815dae06024266d2faa221e9333369630fc4e8b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-p998", + "source": "svg-group-blend-near-unit-screen-p998.svg", + "source_sha256": "328b41be82215dfb674b380015812cdfd74ff23540eab7e9f65aa6c5b7b9890b", + "oracle": "chromium/svg-group-blend-near-unit-screen-p998.png", + "oracle_sha256": "f14a992189272a005e92ceff7805f39f00b9aa5561e73f444c8a7b163d6f8674", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-p999", + "source": "svg-group-blend-near-unit-screen-p999.svg", + "source_sha256": "0d44f780c00630f1c5bac65ac695662f193b78916f2cd9ab9f45c2f1ea75e82e", + "oracle": "chromium/svg-group-blend-near-unit-screen-p999.png", + "oracle_sha256": "bd4c0f568c7a7506da3cd6b87815dae06024266d2faa221e9333369630fc4e8b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-unit", + "source": "svg-group-blend-near-unit-screen-unit.svg", + "source_sha256": "0fdcff87a62fee97684f01b87fa295c848b23861b682c71fb3ce486ed741603d", + "oracle": "chromium/svg-group-blend-near-unit-screen-unit.png", + "oracle_sha256": "bd4c0f568c7a7506da3cd6b87815dae06024266d2faa221e9333369630fc4e8b", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-nested-hidden", "source": "svg-group-blend-nested-hidden.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index af856a74..36f77af0 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -4945,6 +4945,102 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-near-unit-isolated-near", + "source": "svg-group-blend-near-unit-isolated-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-isolated-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-p998", + "source": "svg-group-blend-near-unit-isolated-p998.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-isolated-p998.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-p999", + "source": "svg-group-blend-near-unit-isolated-p999.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-isolated-p999.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-isolated-unit", + "source": "svg-group-blend-near-unit-isolated-unit.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-isolated-unit.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-near", + "source": "svg-group-blend-near-unit-plain-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-plain-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-p998", + "source": "svg-group-blend-near-unit-plain-p998.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-plain-p998.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-p999", + "source": "svg-group-blend-near-unit-plain-p999.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-plain-p999.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-plain-unit", + "source": "svg-group-blend-near-unit-plain-unit.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-plain-unit.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-near", + "source": "svg-group-blend-near-unit-screen-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-screen-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-p998", + "source": "svg-group-blend-near-unit-screen-p998.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-screen-p998.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-p999", + "source": "svg-group-blend-near-unit-screen-p999.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-screen-p999.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-near-unit-screen-unit", + "source": "svg-group-blend-near-unit-screen-unit.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-near-unit-screen-unit.png", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-nested-hidden", "source": "svg-group-blend-nested-hidden.svg", diff --git a/fixtures/web-first/svg-group-blend-near-unit-isolated-near.svg b/fixtures/web-first/svg-group-blend-near-unit-isolated-near.svg new file mode 100644 index 00000000..f95b20ed --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-isolated-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-isolated-p998.svg b/fixtures/web-first/svg-group-blend-near-unit-isolated-p998.svg new file mode 100644 index 00000000..5e60b97b --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-isolated-p998.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-isolated-p999.svg b/fixtures/web-first/svg-group-blend-near-unit-isolated-p999.svg new file mode 100644 index 00000000..9a089017 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-isolated-p999.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-isolated-unit.svg b/fixtures/web-first/svg-group-blend-near-unit-isolated-unit.svg new file mode 100644 index 00000000..87099886 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-isolated-unit.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-plain-near.svg b/fixtures/web-first/svg-group-blend-near-unit-plain-near.svg new file mode 100644 index 00000000..41d9daa0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-plain-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-plain-p998.svg b/fixtures/web-first/svg-group-blend-near-unit-plain-p998.svg new file mode 100644 index 00000000..39b7b0bb --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-plain-p998.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-plain-p999.svg b/fixtures/web-first/svg-group-blend-near-unit-plain-p999.svg new file mode 100644 index 00000000..ff7736e1 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-plain-p999.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-plain-unit.svg b/fixtures/web-first/svg-group-blend-near-unit-plain-unit.svg new file mode 100644 index 00000000..3a2c917f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-plain-unit.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-screen-near.svg b/fixtures/web-first/svg-group-blend-near-unit-screen-near.svg new file mode 100644 index 00000000..6e6ab103 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-screen-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-screen-p998.svg b/fixtures/web-first/svg-group-blend-near-unit-screen-p998.svg new file mode 100644 index 00000000..2cca0943 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-screen-p998.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-screen-p999.svg b/fixtures/web-first/svg-group-blend-near-unit-screen-p999.svg new file mode 100644 index 00000000..f9924099 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-screen-p999.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-screen-unit.svg b/fixtures/web-first/svg-group-blend-near-unit-screen-unit.svg new file mode 100644 index 00000000..dc8d0f9f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-screen-unit.svg @@ -0,0 +1 @@ + From b2e6a4b1acd6b49e00752b430c6edcd1050646f9 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 7 Sep 2026 22:16:55 +0900 Subject: [PATCH 7/7] n0: restore byte-255 group opacity exactly --- crates/n0/README.md | 11 +- crates/n0/src/glyphless.rs | 123 +++++++++++- crates/n0/src/paint.rs | 70 ++++++- crates/n0/tests/group_blending.rs | 181 +++++++++++++++++- crates/n0_cli/README.md | 2 +- docs/wg/consolidation/svg-engine-of-record.md | 102 +++++++++- fixtures/web-first/README.md | 3 +- fixtures/web-first/STATUS.md | 6 +- .../svg-group-blend-near-unit-bare-near.png | Bin 0 -> 1488 bytes .../svg-group-blend-near-unit-bare-p998.png | Bin 0 -> 1509 bytes .../svg-group-blend-near-unit-bare-p999.png | Bin 0 -> 1488 bytes .../svg-group-blend-near-unit-bare-unit.png | Bin 0 -> 1504 bytes fixtures/web-first/oracle-bake.json | 38 +++- fixtures/web-first/primitives.json | 32 ++++ .../svg-group-blend-near-unit-bare-near.svg | 1 + .../svg-group-blend-near-unit-bare-p998.svg | 1 + .../svg-group-blend-near-unit-bare-p999.svg | 1 + .../svg-group-blend-near-unit-bare-unit.svg | 1 + 18 files changed, 550 insertions(+), 22 deletions(-) create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-bare-near.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-bare-p998.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-bare-p999.png create mode 100644 fixtures/web-first/chromium/svg-group-blend-near-unit-bare-unit.png create mode 100644 fixtures/web-first/svg-group-blend-near-unit-bare-near.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-bare-p998.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-bare-p999.svg create mode 100644 fixtures/web-first/svg-group-blend-near-unit-bare-unit.svg diff --git a/crates/n0/README.md b/crates/n0/README.md index a1a1c439..91ac7bdd 100644 --- a/crates/n0/README.md +++ b/crates/n0/README.md @@ -315,10 +315,15 @@ Construction is preflighted without drawing; failure returns an owner-bearing `glyphless::BuildError::Blend`. A thread-local cache holds one compiled effect per mode and at most 256 immutable opacity bindings per mode, never destination pixels. Tests execute every opacity byte against integer arithmetic and prove binding reuse -equals fresh construction. Unit-Normal restoration also uses exact byte +equals fresh construction. Byte-255 Normal restoration also uses exact byte source-over to avoid the x86 sprite blitter's separate approximation at -partial-alpha edges. Partial-opacity Normal retains the existing isolated -opacity path, and unit-opacity Screen remains native. +partial-alpha edges. This includes accepted near-unit opacity values whose +backend byte is 255. An existing `ScopeEffect::Opacity` in that bucket lowers +to the same checked Normal-blend command, retaining its original opacity, +owner and one source layer. Lower-byte Normal retains the native isolated +opacity path, and unit-opacity Screen remains native. The trace counters +include these promoted opacity layers; promotion changes their restore +operation, not how many source layers exist. With the `trace` feature, `n0::trace::sink::drain_blend_layers()` drains typed `BlendLayerMetrics`, separate from duration samples: one aggregate per outermost diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index 7744faad..faaeb31d 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -375,7 +375,22 @@ pub fn compile(resolved: Frame) -> Result { provenance.owners.push(scope.owner); // Placeholder until the scope closes and its union is known. provenance.coverage.push(None); - let (kind, initial_coverage) = match &scope.effect { + // Backend byte-255 opacity takes the same exact Normal + // restore as a Blend scope. Retain the original resolved + // opacity and the layer itself: byte quantization is not + // permission to erase this isolation boundary. Both spellings + // share preflight, owner coverage, and the matching close. + let promoted_opacity = match &scope.effect { + ScopeEffect::Opacity(opacity) => { + let blend = + rframe::ScopeBlend::new(rframe::ScopeBlendMode::Normal, Some(*opacity)); + crate::paint::uses_isolated_byte_blender(blend) + .then_some(ScopeEffect::Blend(blend)) + } + _ => None, + }; + let effect = promoted_opacity.as_ref().unwrap_or(&scope.effect); + let (kind, initial_coverage) = match effect { ScopeEffect::Opacity(opacity) => { items.push(Item { node: slot, @@ -3725,6 +3740,112 @@ mod tests { ); } + #[test] + fn isolated_opacity_projection_preserves_facts_and_owner_across_byte_routes() { + let mut child = None; + let values = (0..=255_u32) + .map(|alpha| match alpha { + 0 => (0.001, false), + 255 => (1.0_f32.next_down(), true), + _ => (alpha as f32 / 255.0, false), + }) + .chain([(0.998, false), (0.999, true)]); + for (value, promoted) in values { + let source = frame_of( + FrameItems::try_new(vec![ + scope_begin(SCOPE_OWNER, value), + FrameItem::Node(base_node(PaintStack::solid(CGColor::RED))), + FrameItem::ScopeEnd, + ]) + .unwrap(), + ); + #[cfg(feature = "trace")] + crate::trace::sink::drain_blend_layers(); + let product = compile(source.clone()).unwrap(); + assert_eq!(product.resolved(), &source, "resolved facts at {value}"); + assert_eq!(product.drawlist, compile(source).unwrap().drawlist); + #[cfg(feature = "trace")] + assert!(crate::trace::sink::drain_blend_layers().is_empty()); + let items = &product.drawlist.items; + assert_eq!(items.len(), 5, "one scope remains at {value}"); + let (begin, end) = if promoted { + ( + ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new( + rframe::ScopeBlendMode::Normal, + Some(ScopeOpacity::new(value).unwrap()), + ), + }, + ItemKind::EndIsolatedBlend, + ) + } else { + ( + ItemKind::BeginIsolatedOpacity { opacity: value }, + ItemKind::EndOpacity, + ) + }; + assert_eq!(items[1].kind, begin, "begin at {value}"); + assert_eq!(items[3].kind, end, "end at {value}"); + assert_eq!(items[1].node, items[3].node); + assert_eq!( + product.provenance.get(items[1].node), + Some(( + SCOPE_OWNER, + Some(n0_model::math::RectF { + x: 8.0, + y: 6.0, + w: 20.0, + h: 16.0, + }), + )) + ); + if let Some(child) = &child { + assert_eq!(&items[2], child, "the child is not rewritten"); + } else { + child = Some(items[2].clone()); + } + } + } + + #[test] + fn isolated_opacity_byte_promotion_also_preflights_nested_programs() { + for value in [0.999, 1.0_f32.next_down()] { + let pattern = rframe::PatternPaint::new( + 64.0, + 48.0, + AffineTransform::identity(), + Arc::new( + FrameItems::try_new(vec![ + scope_begin(SCOPE_OWNER, value), + FrameItem::Node(base_node(PaintStack::solid(CGColor::RED))), + FrameItem::ScopeEnd, + ]) + .unwrap(), + ), + 1.0, + ) + .unwrap(); + #[cfg(feature = "trace")] + crate::trace::sink::drain_blend_layers(); + let compiled = compile_pattern(&pattern, RECT_OWNER).unwrap(); + #[cfg(feature = "trace")] + assert!(crate::trace::sink::drain_blend_layers().is_empty()); + let items = &compiled.program.items; + assert_eq!(items.len(), 5); + assert_eq!( + items[1].kind, + ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new( + rframe::ScopeBlendMode::Normal, + Some(ScopeOpacity::new(value).unwrap()), + ), + } + ); + assert_eq!(items[3].kind, ItemKind::EndIsolatedBlend); + assert_eq!(items[1].node, items[3].node); + } + } + /// One checked final operation becomes one pair in the private stream. /// Child paints stay identical, including at unit Normal and at the /// extreme admitted opacity values; there is no per-paint rewrite. diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 5ecf3149..7a9fcf42 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -2644,12 +2644,18 @@ struct IsolatedByteBlenders { by_alpha: Vec>, } +/// The pinned N32 restore's paint-alpha conversion. Keep route selection and +/// shader uniforms in the same byte domain without changing the resolved fact. +fn isolated_opacity_byte(opacity: Option) -> u8 { + (opacity.map_or(1.0, |opacity| opacity.get()) * 255.0 + 0.5) as u8 +} + fn isolated_byte_blender(blend: rframe::ScopeBlend) -> Result { // Pinned Skia's N32 lowp sprite restore quantizes the paint opacity first, // then rounds source-byte * opacity-byte / 255. A runtime blender promotes // the surrounding pipeline to highp; leaving paint alpha active would // instead scale by the original float before source quantization. - let alpha = (blend.opacity().map_or(1.0, |opacity| opacity.get()) * 255.0 + 0.5) as u8; + let alpha = isolated_opacity_byte(blend.opacity()); let (slot, expression) = match blend.mode() { rframe::ScopeBlendMode::Multiply => (0, UNORM8_MULTIPLY_EXPRESSION), rframe::ScopeBlendMode::Screen => (1, "s + d - div255(s * d)"), @@ -2709,10 +2715,13 @@ pub(crate) fn preflight_isolated_blend(blend: rframe::ScopeBlend) -> Result<(), } } -fn uses_isolated_byte_blender(blend: rframe::ScopeBlend) -> bool { +pub(crate) fn uses_isolated_byte_blender(blend: rframe::ScopeBlend) -> bool { match blend.mode() { rframe::ScopeBlendMode::Multiply => true, - rframe::ScopeBlendMode::Normal => blend.opacity().is_none(), + // SkPaint::getAlpha() also makes near-unit Some values 255. Those + // select the same x86 SrcOver sprite approximation as None. Lower + // bytes retain the distinct native global-alpha restore arithmetic. + rframe::ScopeBlendMode::Normal => isolated_opacity_byte(blend.opacity()) == 255, rframe::ScopeBlendMode::Screen => blend.opacity().is_some(), } } @@ -2757,6 +2766,48 @@ mod isolated_blend_policy_tests { read_pixels(&mut surface, 1, 1) } + #[test] + fn isolated_blend_routing_uses_the_native_opacity_byte() { + let boundary = 254.5_f32 / 255.0; + let values = (0..=255_u32) + .map(|alpha| { + if alpha == 0 { + 0.001 + } else { + alpha as f32 / 255.0 + } + }) + .chain([ + 0.123456, + 0.6, + 0.998, + 0.999, + 1.0_f32.next_down(), + boundary.next_down(), + boundary, + boundary.next_up(), + ]); + for value in values { + let opacity = scope(value).opacity(); + let mut native = Paint::default(); + native.set_alpha_f(value); + assert_eq!(isolated_opacity_byte(opacity), native.alpha(), "{value}"); + for (mode, expected) in [ + (rframe::ScopeBlendMode::Normal, native.alpha() == 255), + (rframe::ScopeBlendMode::Multiply, true), + (rframe::ScopeBlendMode::Screen, opacity.is_some()), + ] { + assert_eq!( + uses_isolated_byte_blender(rframe::ScopeBlend::new(mode, opacity)), + expected, + "{mode:?} at {value}" + ); + } + } + assert_eq!(isolated_opacity_byte(scope(0.998).opacity()), 254); + assert_eq!(isolated_opacity_byte(scope(0.999).opacity()), 255); + } + #[test] fn isolated_multiply_raster_matches_ordered_integer_math_for_every_opacity_byte() { let q = |v: u32| (v + 127) / 255; @@ -2867,6 +2918,19 @@ mod isolated_blend_policy_tests { warm, "fresh binding at source alpha byte {alpha}" ); + for value in [0.999, 1.0_f32.next_down()] { + let alias = rframe::ScopeBlend::new( + rframe::ScopeBlendMode::Normal, + Some(rframe::ScopeOpacity::new(value).unwrap()), + ); + preflight_isolated_blend(alias).unwrap(); + assert!(uses_isolated_byte_blender(alias)); + assert_eq!( + pixel_with_source_alpha(alias, false, alpha as u8), + expected, + "opacity {value}, source alpha byte {alpha}" + ); + } } } } diff --git a/crates/n0/tests/group_blending.rs b/crates/n0/tests/group_blending.rs index b4994d2c..f3d4f14a 100644 --- a/crates/n0/tests/group_blending.rs +++ b/crates/n0/tests/group_blending.rs @@ -134,6 +134,35 @@ mod layer_metrics { assert!(drain_blend_layers().is_empty()); } + #[test] + fn byte_255_opacity_is_observed_as_one_blend_operation_without_erasing_its_layer() { + for (value, count) in [(0.998, 0), (0.999, 1), (1.0_f32.next_down(), 1)] { + drain_blend_layers(); + let product = compile(frame(vec![ + begin(10, ScopeEffect::Opacity(ScopeOpacity::new(value).unwrap())), + solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST), + FrameItem::ScopeEnd, + ])) + .unwrap(); + assert!( + drain_blend_layers().is_empty(), + "build issues no raster commands" + ); + raster(&product, BACKDROP); + let metrics = drain_blend_layers(); + assert_eq!(metrics.len(), 1); + assert_eq!(metrics[0].save_layer_calls, count, "opacity {value}"); + assert_eq!(metrics[0].observed_raster_layers, count); + assert_eq!( + metrics[0].observed_raster_bytes, + u128::from(count) * (SIZE * SIZE * 4) as u128 + ); + assert_eq!(metrics[0].missing_observations, 0); + // Byte 254 still has a native opacity layer; it is deliberately + // outside these execute-seam blend-operation counters. + } + } + #[test] fn nested_and_sequential_layers_have_distinct_live_peaks() { let scene = |nested| { @@ -616,7 +645,14 @@ fn normal_blend_with_opacity_matches_existing_isolated_opacity() { solid(1, rect(4.0, 4.0, 24.0, 24.0), FIRST), solid(2, rect(16.0, 16.0, 24.0, 24.0), SECOND), ]; - for opacity in [0.125, 0.375, 0.5, 0.6, 0.999] { + let values = (0..=255_u32) + .map(|alpha| match alpha { + 0 => 0.001, + 255 => 1.0_f32.next_down(), + _ => alpha as f32 / 255.0, + }) + .chain([0.125, 0.375, 0.5, 0.6, 0.998, 0.999]); + for opacity in values { let scene = |effect| { let mut items = vec![begin(10, effect)]; items.extend(children.clone()); @@ -634,7 +670,89 @@ fn normal_blend_with_opacity_matches_existing_isolated_opacity() { CGColor::TRANSPARENT, CGColor::from_rgba(0, 255, 0, 170), ] { - assert_eq!(raster(&old, backdrop), raster(&new, backdrop)); + let pixels = raster(&old, backdrop); + assert_eq!(pixels, raster(&new, backdrop), "opacity {opacity:?}"); + let mut restore = skia_safe::Paint::default(); + restore.set_alpha_f(opacity.get()); + if restore.alpha() < 255 { + // Lower byte factors must remain exactly the old native + // saveLayer operation, not the ordered byte-blender formula. + let mut surface = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); + surface.canvas().clear(skia_safe::Color::from_argb( + backdrop.a, backdrop.r, backdrop.g, backdrop.b, + )); + surface + .canvas() + .save_layer(&skia_safe::canvas::SaveLayerRec::default().paint(&restore)); + for (x, y, color) in [(4.0, 4.0, FIRST), (16.0, 16.0, SECOND)] { + let mut paint = skia_safe::Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb( + color.a, color.r, color.g, color.b, + )); + surface + .canvas() + .draw_rect(skia_safe::Rect::from_xywh(x, y, 24.0, 24.0), &paint); + } + surface.canvas().restore(); + assert_eq!( + pixels, + read_pixels(&mut surface, SIZE, SIZE), + "native opacity {opacity:?}" + ); + } + } + } +} + +#[test] +fn near_unit_normal_spellings_share_exact_partial_alpha_restore_and_retain_the_frame() { + // Both alphas are partial. Unlike an opaque axis-aligned child, this + // palette distinguishes exact /255 from x86's native unit sprite /256. + let source_color = CGColor::from_rgba(215, 104, 67, 8); + let backdrop = CGColor::from_rgba(66, 101, 137, 170); + let source = [7_u32, 3, 2, 8]; + let destination = [44_u32, 67, 91, 170]; + let expected: [u8; 4] = std::array::from_fn(|i| { + (source[i] + (destination[i] * (255 - source[3]) + 127) / 255) as u8 + }); + let native_x86: [u8; 4] = + std::array::from_fn(|i| (source[i] + destination[i] * (256 - source[3]) / 256) as u8); + assert_ne!( + expected, native_x86, + "the witness must discriminate the rounding routes" + ); + let scene = |effect| { + frame(vec![ + begin(10, effect), + solid(1, rect(8.0, 8.0, 24.0, 24.0), source_color), + FrameItem::ScopeEnd, + ]) + }; + let unit = compile(scene(ScopeEffect::Blend(ScopeBlend::new( + ScopeBlendMode::Normal, + None, + )))) + .unwrap(); + let unit_pixels = raster(&unit, backdrop); + assert_eq!(at(&unit_pixels, 16, 16), expected); + for value in [0.999, 1.0_f32.next_down()] { + let opacity = ScopeOpacity::new(value).unwrap(); + for effect in [ + ScopeEffect::Opacity(opacity), + ScopeEffect::Blend(ScopeBlend::new(ScopeBlendMode::Normal, Some(opacity))), + ] { + let resolved = scene(effect); + let product = compile(resolved.clone()).unwrap(); + assert_eq!(product.resolved(), &resolved); + let retained = product.clone(); + let pixels = raster(&retained, backdrop); + assert_eq!(at(&pixels, 16, 16), expected, "opacity {value}"); + assert_eq!( + pixels, unit_pixels, + "same retained isolation topology at {value}" + ); + assert_eq!(pixels, raster(&compile(resolved).unwrap(), backdrop)); } } } @@ -675,6 +793,65 @@ fn changing_blend_or_opacity_damages_only_the_scope_and_its_clipped_child_union( } } +#[test] +fn opacity_byte_254_to_255_keeps_scope_damage_coverage_and_retained_matches_fresh() { + for blend_spelling in [false, true] { + let scene = |value| { + let opacity = ScopeOpacity::new(value).unwrap(); + let effect = if blend_spelling { + ScopeEffect::Blend(ScopeBlend::new(ScopeBlendMode::Normal, Some(opacity))) + } else { + ScopeEffect::Opacity(opacity) + }; + frame(vec![ + begin(10, effect), + solid(1, rect(4.0, 8.0, 12.0, 12.0), FIRST), + begin(11, ScopeEffect::Clip(clip(rect(16.0, 12.0, 8.0, 8.0)))), + solid(2, rect(0.0, 0.0, 48.0, 48.0), SECOND), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]) + }; + for (old, new) in [(0.998, 0.999), (0.999, 0.998)] { + let before = compile(scene(old)).unwrap(); + let resolved = scene(new); + let retained = compile(resolved.clone()).unwrap().clone(); + assert_eq!(retained.resolved(), &resolved); + let fresh = compile(scene(new)).unwrap(); + assert!(diff_frame(&retained, &fresh).is_empty()); + let damage = diff_frame(&before, &retained); + assert_eq!(damage.changed, [owner(10)]); + assert_eq!(damage.union_frame, Some(rect(4.0, 8.0, 20.0, 12.0))); + let before_pixels = raster(&before, BACKDROP); + let after_pixels = raster(&retained, BACKDROP); + assert_eq!(after_pixels, raster(&fresh, BACKDROP)); + let union = damage.union_frame.unwrap(); + let mut changed_pixels = 0; + for y in 0..SIZE as usize { + for x in 0..SIZE as usize { + if at(&before_pixels, x, y) != at(&after_pixels, x, y) { + changed_pixels += 1; + assert!( + x as f32 >= union.x + && y as f32 >= union.y + && (x + 1) as f32 <= union.x + union.width + && (y + 1) as f32 <= union.y + union.height, + "{old} -> {new}: changed pixel ({x},{y}) escaped {union:?}" + ); + } + } + } + assert!(changed_pixels > 0, "byte-route crossing must change pixels"); + } + // Distinct resolved floats in byte 255 remain distinct damage facts, + // even though their restoration pixels alias on this backend. + let near = compile(scene(1.0_f32.next_down())).unwrap(); + let alias = compile(scene(0.999)).unwrap(); + assert_eq!(diff_frame(&near, &alias).changed, [owner(10)]); + assert_eq!(raster(&near, BACKDROP), raster(&alias, BACKDROP)); + } +} + #[test] fn fully_clipped_blend_edit_keeps_owner_but_has_no_coverage() { let scene = |mode| { diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 37f89394..958c183b 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,7 +607,7 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,394 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,398 Chromium-baked cells plus 16 sampled frames; the text estate contains sixteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two Bungee), and the named refusal register has 303 rows. `feFlood`, `feComposite`, diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 2dd44ee1..dd6ef469 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -116,7 +116,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,394 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,398 Chromium-baked primitive cells plus 16 sampled frames, with a separate sixteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -5770,7 +5770,7 @@ explicit-Normal-isolation opacity controls cover opaque-backdrop interactions. Nineteen further cells cover the actual outer root and redundant isolation over translucent backdrops. Four more cross multiply/screen at opacity .123456 with opaque/translucent source colors over a translucent destination. The rung -adds 110 exact primitive/HTML cells, taking the corpus from 1,284 to 1,394, +adds 114 exact primitive/HTML cells, taking the corpus from 1,284 to 1,398, and 43 named refusals, taking that register from 260 to 303. The sixteen sampled frames and separate sixteen pixel/eight geometry text witnesses are @@ -5789,6 +5789,11 @@ authored partial-opacity group must keep its source layer. All twelve sources match Chromium in both command admissions on ARM. The corresponding four multiply probes also match, with unit/near/`.999` identical and `.998` differing at 1,013 pixels (maximum delta 1) **(measured, not celled)**. +Four `svg-group-blend-near-unit-bare-{unit,near,p999,p998}` controls remove +authored isolation from the plain group. Each is pixel-identical to its +explicitly isolated counterpart in Chromium and both command admissions on +ARM. The near-unit restore boundary therefore also belongs to ordinary +group opacity; the open blending rows cannot shelter a gap in that operation. HTML exterior-backdrop controls differ when isolation is added **(measured, not celled)**. That exterior paint is absent from this command's @@ -5860,12 +5865,58 @@ uses accurate divide-by-255 rounding. A mutation of only the new unit-Normal restore to that formula reproduces 141 differing pixels and the same first pixel. The raster-pipeline approximation alone produces a different 135-pixel signature, so the two backend paths are not conflated. Exact byte source-over -now carries unit-Normal blend boundaries as well. Partial-opacity Normal keeps -the established native isolated-opacity operation and its independently -guarded byte behavior; unit-opacity screen also stays native. No old opacity -operation is changed. The complete 1,382-cell gate then passes on ARM and +first carried unit-Normal blend boundaries as well. At that point, +partial-opacity Normal retained the established native isolated-opacity +operation; unit-opacity screen also stayed native. The 1,382-cell gate +passed on ARM and hosted x86 with the scoped correction, without an oracle or tolerance change. +Review then exposed the near-unit boundary documented above. Pinned +`SkPaint::getAlpha()` rounds accepted partial opacities such as `.999` to byte +255. `ChooseL32` passes that byte into `Sprite_D32_S32`, which omits its global +alpha flag and selects the same source-over route as unit opacity. Route +selection and shader bindings now share one n0-owned byte conversion: Normal +uses the exact restore for byte 255, including a checked partial opacity. +Bytes below 255 retain the distinct native global-alpha operation; screen's +separately measured routing is unchanged. + +The Web compiler sends own partial Normal through `ScopeEffect::Opacity`, +not just the new Blend variant. The n0 consumer therefore lowers that existing +opacity fact into the same Normal-blend command when its backend byte is 255. +It preserves the original resolved opacity, source layer, owner, child coverage +union and balanced close. Both spellings share owner-bearing preflight and +reuse/damage guards. Backend quantization never enters websem or rframe, and +native backdrop-preserving `BeginOpacity` remains untouched. These promoted +opacity layers now appear in the trace blend-layer counters and incur the +exact helper's cost; they are not newly allocated layers. + +The pre-correction hosted x86 run fails exactly four of the twelve new +boundary cells: the isolated blending-child and plain isolated groups at the +next-smaller `f32` and `.999`. Each has 141 changed pixels at delta 1 and the +same first-pixel signature above; the unit, `.998` and screen controls pass. +The four ordinary-opacity controls were added after that run. A deliberate +AVX2-formula mutation of the corrected Normal restore makes `just gate` fail +eight cells with that same 141-pixel/delta-1 signature, including both ordinary +near-unit controls. No oracle or tolerance changes to absorb the failure. + +An additional ordinary-opacity regression patrol crosses eighteen rotated +source controls and six unrotated effect controls at `.999`/`.998`. +Circles, ellipses, curved paths, round strokes, curved clips and the admitted +alpha mask remain exact on ARM. Rotated pattern/mask and small-kernel blur +controls retain their existing named refusals. The radial ramp is silently +admitted with 1,312/1,307 one-code-value differences, the unrotated pattern +with one, and the circle-plus-blur control with 25 pixels at maximum delta +3/2. Every successful output is encoded-byte identical between the retained +pre-rung `fd4097f2` binary, the pre-follow-up build and the correction. +These are **(measured, not celled)** controls, not a new tolerance or an exact +parity claim for those departures. Their causes and disposition are tracked +separately in [gridaco/nothing#136](https://github.com/gridaco/nothing/issues/136). +Pre-existence establishes no regression from this correction, not that a +closed row is unaffected: both opacity rows and `` are ticked, +and causal attribution and tick ownership remain unresolved. A proven +closed-row defect requires repair/refusal and tick reassessment. These +controls do not widen the authored-blending source profile. + One compiled effect per mode per thread and at most 256 immutable opacity bindings per mode amortize shader construction; this is a code/uniform cache, never a pixel or backdrop cache. Frame compilation preflights fallible construction and returns @@ -5874,7 +5925,7 @@ Execution tests guard the static shader's raster lowering against independent integer arithmetic for every opacity byte in multiply/screen and every source alpha byte in unit-Normal, distinguish float-first ordering, and prove warm binding reuse equals fresh construction. Three effect slots are bounded; the -Normal slot is used only with unit opacity. +Normal slot is used only with byte-255 opacity. The group shader enables Skia's optimizer; its exactness is independently gated, and the existing filter-blender configuration is unchanged. @@ -5935,7 +5986,7 @@ The pinned accessor observes existing raster storage but changes its generation state, so allocation observation belongs to a separate untimed frame. It does not measure allocator capacity, Skia-internal allocations or GPU memory. -The matched CPU-raster measurement used the clean pre-rung revision +The first matched CPU-raster measurement, before the near-unit follow-up, used the clean pre-rung revision `fd4097f2` and B1 on the same Apple M2 Ultra, 128 GiB host, macOS 26.5.1, Rust 1.92.0 aarch64, skia-safe 0.99.0, release builds with tracing off. Each workload records its first frame compile and paint separately, warms five @@ -5947,7 +5998,7 @@ the matched table. Cargo startup, file/PNG work, canvas clear and the input clone are outside their respective timed loops. Source compilation includes document and cascade construction. Other task builds, captures and tests are stopped during the -final post-correction measurement. No GPU timing is claimed. +post-correction measurement. No GPU timing is claimed. The reproducible workload is a 256×256 SVG with an opaque `#426589` background. For each group index `i`, set `x=(i%32)*8`, `y=((i/32)%32)*8`; paint a 6×6 @@ -5980,6 +6031,39 @@ not repeat above the threshold in that follow-up median; the original alert is retained, not replaced. Other unaffected stage medians remain below the threshold. This is not a universal regression guarantee. +The near-unit follow-up uses the same 100-pair, 256×256 workload with ordinary +group opacity `.999`, `.998`, `.5`, and a neutral control. Before/after builds +share one dependency lock; the baseline is the pre-follow-up `7d42ab20` and +the corrected build retains the same public type sizes. The same hardware, +release/trace-off posture, first-use sample, five warmups, 20/80/40 stage +samples and three alternating repetitions apply, with other task builds, +captures and tests stopped. Medians of the three p50s are: + +| 100 groups | Frame compile, before → after (µs) | Paint, before → after (µs) | +| --- | ---: | ---: | +| Opacity .999, byte 255 | 25.29 → 26.25 | 1,959.50 → 119,690.54 | +| Opacity .998, byte 254 | 25.08 → 24.83 | 3,348.54 → 3,342.83 | +| Opacity .5 | 25.13 → 24.83 | 3,350.67 → 3,350.08 | +| Neutral | 17.42 → 17.21 | 47.54 → 47.08 | + +The byte-255 correction has a material CPU cost: near-unit opacity now pays +for the same explicit exact restore as unit Normal. It is not a performance +improvement. Its paint p50 spans 119,656.17–119,693.96 µs; maximum within-run +p95 is 120,200.83 µs. First frame compilation spans 654.58–2,151.42 µs and +first paint 119,923.29–140,139.42 µs; the high first repetition is retained. +No control-stage median regresses beyond the 5% investigation threshold in +this bounded run. Source-compile medians range from 1,405.54 to 1,486.33 µs +after correction, versus 1,443.88–1,509.63 µs before it; these small changes +are not a new optimization claim. + +Separate untimed trace observation records 100 promoted-opacity restores, +26,214,400 cumulative accessible raster bytes and 262,144 peak live bytes. +The `.998` and `.5` opacity layers remain outside these blend-operation +counters, not absent. Promotion retains the existing source layers rather +than creating 100 new ones. Reducing active-clip-sized exact-restore work +requires the same source-extent and intermediate-precision investigation as +the broader blending profile; no unchecked bounds or native fallback ships. + One 100-neutral current repetition is slower: source, frame-compile and paint p50 ranges are 1,421.92–2,156.58, 17.42–21.83 and 46.92–58.08 µs; baseline paint is 46.75–47.25 µs. The 1,000-neutral current paint range is diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 1f7ca894..60a055eb 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,7 +19,7 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): sixteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,394 primitive cells plus 16 sampled frames, those twenty-four text +estate is 1,398 primitive cells plus 16 sampled frames, those twenty-four text witnesses, and 303 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The @@ -31,6 +31,7 @@ pixel claim. | `svg-group-blend-{normal,multiply,screen}-{leaf,group,opacity,alpha,transparent,stroke}.svg` · `svg-group-blend-{multiply,screen}-each.svg` | B1's exact group-operation controls: mode-sensitive backdrops, overlapping children, combined opacity, translucency, transparent initial source and fill/stroke composition. Whole-group versus per-child blending changes 576 pixels at maximum deltas 89 (multiply) and 98 (screen). No new tolerance. | | `svg-group-blend-{multiply,screen}-opacity-small-{opaque,partial}.svg` | Opacity .123456 with opaque/translucent source colors over a translucent destination. The multiply partial-source cell rejects a float-first opacity prototype at 1,600 pixels/delta 1: byte opacity must be applied before the byte-domain blend. Hosted x86 also guards the low-precision backend's approximate division; no oracle or tolerance is relaxed. | | `svg-group-blend-near-unit-{isolated,plain,screen}-{unit,near,p999,p998}.svg` | Rotated groups at opacity 1, the next smaller f32, .999 and .998. The first three match for isolated blending children and screen; plain isolation instead changes 143 pixels/delta 1 at the unit-to-partial boundary. A byte-255 restore must not erase the authored partial-opacity source layer. The .998 controls discriminate the next opacity byte. | +| `svg-group-blend-near-unit-bare-{unit,near,p999,p998}.svg` | Ordinary group opacity without an isolation or blending style. Each matches its explicit-isolation plain-group control exactly, guarding the shared opacity lowering rather than only the newly authored blending ingress. | | `svg-group-blend-{neutral,isolated,clip,partial,opacity,outer-opacity}.svg` and their `-isolated` controls | Authored clip-path and partial opacity isolate descendants; a neutral group does not. Same-element blend plus opacity differs from an outer opacity group by 2,048 pixels at delta 76. Unit-opacity isolation is a real boundary, not an absent effect. | | `svg-group-blend-{translate,scale,rotate,nested-hidden}.svg` and their `-isolated` controls | Ordinary affine transforms and a nested SVG overflow clip do not isolate. Adding isolation changes 2,048 / 1,536 / 2,160 pixels for the transform trio, and 2,048 for the viewport case, all at maximum delta 152. | | `svg-group-blend-{linear,pattern,fractional}-{normal,multiply,screen}.svg` · `svg-group-blend-opacity-{0,1,p00392156862745098,p5019607843137255,p6}.svg` | Rectangular source paint/materialization and opacity boundaries, including a linear ramp, an existing repeating-vector paint, fractional rectangle edges, zero/unit opacity and fractional group alpha. This does not admit blending inside the pattern source. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 59e357a4..d29600ba 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1394) +## Chromium-baked cells (1398) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -634,6 +634,10 @@ to its fixture source. No new image is committed for this view. svg-group-blend-multiply-opacity-small-partial svg-group-blend-multiply-stroke svg-group-blend-multiply-transparent +svg-group-blend-near-unit-bare-near +svg-group-blend-near-unit-bare-p998 +svg-group-blend-near-unit-bare-p999 +svg-group-blend-near-unit-bare-unit svg-group-blend-near-unit-isolated-near svg-group-blend-near-unit-isolated-p998 svg-group-blend-near-unit-isolated-p999 diff --git a/fixtures/web-first/chromium/svg-group-blend-near-unit-bare-near.png b/fixtures/web-first/chromium/svg-group-blend-near-unit-bare-near.png new file mode 100644 index 0000000000000000000000000000000000000000..b00aee948a48cb6ad56e3c80e0f2f6538d4e854e GIT binary patch literal 1488 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 diff --git a/fixtures/web-first/svg-group-blend-near-unit-bare-p998.svg b/fixtures/web-first/svg-group-blend-near-unit-bare-p998.svg new file mode 100644 index 00000000..c9a884db --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-bare-p998.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-bare-p999.svg b/fixtures/web-first/svg-group-blend-near-unit-bare-p999.svg new file mode 100644 index 00000000..7e55c9d2 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-bare-p999.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-near-unit-bare-unit.svg b/fixtures/web-first/svg-group-blend-near-unit-bare-unit.svg new file mode 100644 index 00000000..27b1997e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-near-unit-bare-unit.svg @@ -0,0 +1 @@ +