From c306d838782adac0d48b0048c116d76e83b56232 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 24 Apr 2026 18:13:55 +0900 Subject: [PATCH 1/4] docs(research): document Chromium SVG rendering pipeline Nine-document research subdirectory under docs/wg/research/chromium/svg/ covering Blink's SVG pipeline from DOM to composite, plus a factual cross-engine comparison with Servo and resvg. Scope: pipeline overview, coordinate systems (viewBox/CTM/non-scaling-stroke), paint servers, resources and effects (clip/mask/filter/marker), path geometry and stroking, SVG text layout algorithm, / bridging, and SVG-as-image embedding modes. --- docs/wg/research/chromium/index.md | 1 + docs/wg/research/chromium/svg/comparison.md | 158 ++++++++++ .../chromium/svg/coordinate-systems.md | 220 ++++++++++++++ docs/wg/research/chromium/svg/index.md | 63 ++++ .../wg/research/chromium/svg/paint-servers.md | 210 +++++++++++++ .../wg/research/chromium/svg/path-geometry.md | 251 ++++++++++++++++ docs/wg/research/chromium/svg/pipeline.md | 226 ++++++++++++++ .../chromium/svg/resources-and-effects.md | 280 ++++++++++++++++++ docs/wg/research/chromium/svg/svg-as-image.md | 133 +++++++++ docs/wg/research/chromium/svg/text.md | 183 ++++++++++++ .../chromium/svg/use-and-foreign-object.md | 188 ++++++++++++ 11 files changed, 1913 insertions(+) create mode 100644 docs/wg/research/chromium/svg/comparison.md create mode 100644 docs/wg/research/chromium/svg/coordinate-systems.md create mode 100644 docs/wg/research/chromium/svg/index.md create mode 100644 docs/wg/research/chromium/svg/paint-servers.md create mode 100644 docs/wg/research/chromium/svg/path-geometry.md create mode 100644 docs/wg/research/chromium/svg/pipeline.md create mode 100644 docs/wg/research/chromium/svg/resources-and-effects.md create mode 100644 docs/wg/research/chromium/svg/svg-as-image.md create mode 100644 docs/wg/research/chromium/svg/text.md create mode 100644 docs/wg/research/chromium/svg/use-and-foreign-object.md diff --git a/docs/wg/research/chromium/index.md b/docs/wg/research/chromium/index.md index 2f0728b4f..db827fcdb 100644 --- a/docs/wg/research/chromium/index.md +++ b/docs/wg/research/chromium/index.md @@ -37,6 +37,7 @@ material when designing rendering systems that face similar problems. | [effect-optimizations.md](./effect-optimizations.md) | Effect optimization: filter demotion, render pass bypass, damage tracking | | [node-data-layout.md](./node-data-layout.md) | Node data layout: DOM RareData, compositor property trees, ECS comparison | | [svg-pattern.md](./svg-pattern.md) | SVG `` paint server semantics, Chromium/resvg/Skia comparison | +| [svg/index.md](./svg/index.md) | **SVG rendering subdirectory** — full pipeline, coordinate systems, paint servers, effects, text | | [blink-rendering-pipeline.md](./blink-rendering-pipeline.md) | Blink Style → Layout → Paint pipeline, ComputedStyle groups, LayoutNG, inline layout, list markers | | [external-resource-loading.md](./external-resource-loading.md) | Resource fetch lifecycle, ImageResource observer pattern, CSS background-image/img loading pipeline | | [dirty-flag-management.md](./dirty-flag-management.md) | Dirty-flag families across Blink + cc, categorized by type and by invalidation shape/granularity | diff --git a/docs/wg/research/chromium/svg/comparison.md b/docs/wg/research/chromium/svg/comparison.md new file mode 100644 index 000000000..3681b8bda --- /dev/null +++ b/docs/wg/research/chromium/svg/comparison.md @@ -0,0 +1,158 @@ +--- +title: "SVG Rendering: Chromium vs Servo vs resvg" +tags: + - internal + - research + - chromium + - svg + - servo + - resvg +--- + +# SVG Rendering: Chromium vs Servo vs resvg + +A cross-engine comparison of how SVG rendering is factored across three +open-source engines. This is the only doc in `svg/` that steps outside +Chromium; the rest describe Chromium as-is. + +## Cross-engine factoring + +| Concern | Chromium (Blink) | Servo | resvg | +| --------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Parser | Blink HTML/XML parser; `SVGElement` subclasses | html5ever / xml5ever → `SVG*Element` DOM stubs | `roxmltree` → `usvg::Tree` (normalized) | +| Inheritance / cascade | CSS cascade on `ComputedStyle` with SVG-specific fields in `SVGComputedStyle` | Delegated to resvg (doesn't cascade SVG presentation attributes itself) | usvg resolves inheritance during parse; outputs explicit per-node values | +| `` handling | Runtime shadow-DOM instantiation; invalidation on target change | Delegated | Deep copy at parse time; independent subtree | +| Text | Two-phase: LayoutNG inline + `SvgTextLayoutAlgorithm` | Delegated | Shaped (rustybuzz) + flattened to `Path` nodes at parse time | +| Layout | `LayoutSVG*` tree; local transforms + bounding boxes | Treats `` as a replaced element; no SVG-specific layout | No layout — usvg emits pre-resolved absolute transforms and bboxes | +| Paint backend | Skia via `cc::PaintRecord` / `cc::PaintFlags` | Via resvg → tiny-skia → WebRender image | tiny-skia (CPU) `Pixmap` | +| Paint servers | `LayoutSVGResource*` with per-client shader cache | Delegated | Top-level pools in `Tree`; rendered per-use | +| Filters | `SVGFilterBuilder` → `FilterEffect` → Skia `PaintFilter`; may be compositor-accel. | Delegated | Sequential primitive pipeline on CPU | +| Clip / mask | Path-based (fast) + rasterized fallback | Delegated | Pixel buffers; apply via tiny-skia `Mask` | +| Composite / GPU | cc property trees + render surfaces; GPU raster | WebRender rasterizes the resvg-produced image | None — CPU only | +| `` | Full HTML paint bridge | Unknown / not supported in practice | Not supported (by design) | +| Animation | CSS + SMIL | Delegated | Static (by design) | + +### Short version + +- **Chromium** implements SVG as a first-class citizen of Blink's pipeline, + deeply integrated with the compositor, CSS cascade, and GPU raster. It + pays the integration cost but supports the full spec and animates. +- **Servo** treats SVG as a black box: parse to a DOM just enough for + scripting, but all rendering decisions are delegated to resvg. Inline + SVG is serialized to a `data:` URL and shipped through the image cache + as a rasterized bitmap. No SVG pipeline of its own. +- **resvg** is two crates: **usvg** (normalize everything — inheritance, + ``, units, text shaping) and **resvg** (draw a normalized tree with + tiny-skia). Static-only, CPU-only, but comprehensive for the + features it supports. + +## The usvg/resvg split as a design pattern + +resvg's README explicitly calls out the split: + +> SVG parsing and rendering are two completely separate steps… split into two +> separate libraries: `resvg` and `usvg`. Meaning you can easily write your +> own renderer on top of `usvg` using any 2D library of your liking. + +What usvg normalizes away, so the renderer doesn't have to: + +- **Inheritance**: every node in the output tree has every presentation + attribute resolved. No `currentColor` resolution at paint time; no + "inherit this from my parent." +- **`` expansion**: the target is deep-cloned into the use site. +- **Unit resolution**: em, %, mm, in — all become user units. +- **`objectBoundingBox` gradients**: converted to `userSpaceOnUse`. +- **Basic shape conversion**: ``, ``, ``, ``, + ``, `` all become `` equivalents. +- **Arcs**: arc-to-cubic decomposition. +- **``**: resolved at parse time. +- **Text**: shaped with rustybuzz, decomposed to `Path` nodes (and + `Image` nodes for color emoji). +- **Bounding boxes and absolute transforms**: pre-computed per node. +- **Paint servers**: pulled into `Tree`-level pools (gradients, + patterns, clips, masks, filters) and referenced by `Arc`. + +The pre-computed bounding boxes are especially important: `Group::abs_bbox` +lets the renderer skip subtrees that don't intersect the dirty area +without any traversal. + +Chromium doesn't factor this way. Its `LayoutSVG*` tree is a +**mid-normalized** representation: inheritance is resolved (via +`ComputedStyle`) but `` is a runtime shadow tree and coordinate +resolution happens at paint time. The difference is that Blink needs a +live DOM that JavaScript can mutate; resvg's tree is frozen. + +## Text: two strategies + +Chromium keeps text **semantic** all the way to paint (one `DrawTextBlob` +per glyph run), which preserves selectability, accessibility, and +animation. But it requires `SvgTextLayoutAlgorithm` — a per-glyph +post-processor. + +resvg/usvg **flatten text to paths** at parse time using rustybuzz. The +renderer never sees a `Text` node (it's flattened to `Group` + +`Path` + `Image` for color emoji). Trade-offs: + +- **+** Renderer has zero font-handling code. +- **+** Reproducibility: same input → same tree on every platform. +- **+** Works on GPU without a font rasterization library. +- **−** No selection, no accessibility. +- **−** Large file sizes (outlines vs glyph ids). +- **−** Animations that depend on text content (e.g., `` of a + `` text) don't work. + +For Grida's canvas use case — render SVG as-is to Skia — the resvg +approach wins for simplicity. A pure GPU renderer that still needs +selectable text can reach for Skia's `SkTextBlob` (similar to Blink), but +needs to reproduce the SVG per-character positioning algorithm. + +## Paint servers: per-client vs per-tree + +Chromium: per-client shader cache because `objectBoundingBox` makes the +shader depend on the referencing shape's bounds. The display list (the +tile's pre-rendered commands) could in theory be shared across clients, +but Chromium currently doesn't. + +resvg: gradient/pattern definitions live at `Tree` level; the renderer +computes a fresh shader for each use site, but the underlying definition +is shared (via `Arc`). Patterns are rendered to a pixmap per use +site — reasonable on CPU; on GPU, a texture atlas or render-once-reuse- +many strategy would be worth considering. + +## Filters: DAG vs sequential + +Chromium builds an explicit DAG (`FilterEffect` graph), composes to a +single `PaintFilter`, and can translate to `CompositorFilterOperations` +for GPU execution. + +resvg walks the primitives in document order, maintaining a named result +table. No graph compilation — each primitive reads named inputs from the +table and writes to it. + +Chromium's composed `PaintFilter` reuses Skia's `SkImageFilter` graph +compiler. resvg walks primitives in document order on CPU. + +## Source anchors + +- **Chromium SVG**: this research subdirectory + (`docs/wg/research/chromium/svg/`). +- **Servo SVG stance**: + `servo/components/layout/replaced.rs` — SVG treated as replaced + element, serialized to `data:` URL; + `servo/components/net/image_cache.rs` — invokes + `resvg::render()` into a tiny-skia pixmap, shipped to WebRender as an + image; + `servo/components/script/dom/svg/` — scriptable DOM stubs without a + native rendering pipeline. +- **resvg architecture**: + `resvg/crates/usvg/src/tree/` — normalized tree types; + `resvg/crates/usvg/src/parser/` — inheritance resolution, `` + expansion, unit resolution; + `resvg/crates/usvg/src/text/flatten.rs` — rustybuzz shaping + text + outlining; + `resvg/crates/resvg/src/render.rs` — tree traversal and layer + composition; + `resvg/crates/resvg/src/path.rs` — path, gradient, pattern rendering; + `resvg/crates/resvg/src/filter/mod.rs` — primitive pipeline; + `resvg/docs/unsupported.md` — documented non-goals (no animation, no + scripting, no SVG 1.2 Tiny, no ``). diff --git a/docs/wg/research/chromium/svg/coordinate-systems.md b/docs/wg/research/chromium/svg/coordinate-systems.md new file mode 100644 index 000000000..c5afb040b --- /dev/null +++ b/docs/wg/research/chromium/svg/coordinate-systems.md @@ -0,0 +1,220 @@ +--- +title: "Chromium SVG Coordinate Systems" +tags: + - internal + - research + - chromium + - rendering + - svg +--- + +# Chromium SVG Coordinate Systems + +How Blink tracks transforms and coordinate spaces from the outer `` down +to a leaf shape. This is the conceptual difference between HTML layout +(rectangular boxes in CSS pixels) and SVG layout (user units, viewBoxes, +per-element transforms). + +## The coordinate-space hierarchy + +For a deeply nested shape, the full transform chain is: + +``` +Shape local (user units) + │ LocalSVGTransform() (the shape's own `transform` attribute) + ▼ +Parent SVG coords + │ chain of ancestor LocalToSVGParentTransform() up to nearest + ▼ +Nearest viewport coords (user units inside that ) + │ viewBoxToViewTransform (viewBox → viewport) + ▼ +Nearest viewport (CSS-px inside that ) + │ if nested, repeat for outer + ▼ +Outer CSS box + │ LocalToBorderBoxTransform() on LayoutSVGRoot + ▼ +CSS layout tree (HTML border box) + │ standard HTML transforms + ▼ +Screen +``` + +Blink exposes this via `SVGElement::LocalCoordinateSpaceTransform(CTMScope)`, +which JavaScript's `getCTM()` and `getScreenCTM()` call: + +```cpp +// third_party/blink/renderer/core/svg/svg_element.h +enum CTMScope { + kNearestViewportScope, // getCTM() — up to nearest + kScreenScope, // getScreenCTM() — all the way to the screen + kAncestorScope, // getEnclosureList() +}; +virtual AffineTransform LocalCoordinateSpaceTransform(CTMScope) const; +``` + +## `LayoutSVGRoot` — the bridge + +`LayoutSVGRoot` is the single object responsible for translating between CSS +box layout and SVG's internal coordinate space. + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_root.h +class LayoutSVGRoot final : public LayoutReplaced { + public: + void LayoutRoot(const PhysicalRect& content_rect); + const AffineTransform& LocalToBorderBoxTransform() const { + return local_to_border_box_transform_; + } + gfx::RectF ViewBoxRect() const; + gfx::SizeF ViewportSize() const; +}; +``` + +- **CSS side:** `LayoutSVGRoot` extends `LayoutReplaced`, so CSS treats it + like an `` — it has `width`/`height`/`aspect-ratio`, participates in + flex/grid/block layout, and has a border box. +- **SVG side:** inside its border box, the `` establishes an SVG + viewport in user units. `viewBox` and `preserveAspectRatio` map that + viewport to the border box. + +`LocalToBorderBoxTransform()` is the pre-composed transform that encodes: + +1. Translation to the SVG viewport origin within the border box (CSS + padding + border offset). +2. Scaling + translation from `viewBox` to viewport size, accounting for + `preserveAspectRatio` alignment (`xMidYMid meet`, `xMinYMin slice`, …). +3. CSS `zoom` and device-pixel scale where applicable. + +## `viewBox` and `preserveAspectRatio` + +The implementation is in `SVGFitToViewBox`: + +```cpp +// third_party/blink/renderer/core/svg/svg_fit_to_view_box.h +static AffineTransform ViewBoxToViewTransform( + const gfx::RectF& view_box, + const SVGPreserveAspectRatio&, + const gfx::SizeF& viewport_size); +``` + +This function is called in at least three places: + +- `LayoutSVGRoot` — outer `` to border box. +- `LayoutSVGViewportContainer` — nested `` inside another. +- `LayoutSVGResourcePattern::BuildPatternData()` — `` with a + `viewBox` (see [paint-servers.md](./paint-servers.md)). +- `LayoutSVGResourceMarker` — `` with a `viewBox`. + +`preserveAspectRatio` values: `none`, `xMin/xMid/xMax × YMin/YMid/YMax`, each +paired with `meet` (fit fully, letterbox) or `slice` (fill fully, crop). + +Default: `xMidYMid meet`. + +## `LocalToSVGParentTransform()` + +Every `LayoutSVGModelObject` carries an `AffineTransform` mapping its own +coordinate space to its parent's. Sources that contribute: + +1. The element's `transform` attribute. +2. CSS `transform` (SVG 2 merged these with CSS). +3. `x` / `y` attributes on elements like ``, ``, `` + (treated as a translate). +4. `viewBox` → viewport for nested `` (handled inside the viewport + container's transform). +5. `animateMotion` offset (SMIL motion path). +6. Non-scaling-stroke correction (not the element's own transform, but a + stroke-time un-scale; see [path-geometry.md](./path-geometry.md)). + +```cpp +// LayoutSVGModelObject exposes: +virtual AffineTransform LocalSVGTransform() const; // this element's transform +virtual AffineTransform LocalToSVGParentTransform() const; // composed +``` + +The painter calls `LocalSVGTransform()` before recording child paint ops +(via `ScopedSVGTransformState`), so the composed CTM naturally accumulates +down the tree without requiring a separate property tree traversal. + +## Percentage length resolution + +SVG percentages resolve differently from CSS. A `` resolves +against the **nearest viewport-establishing ancestor** — the nearest +`` or ``, not the immediate parent. This is handled by +`SVGLengthContext`: + +```cpp +// third_party/blink/renderer/core/svg/svg_length_context.h +class SVGLengthContext { + public: + explicit SVGLengthContext(const SVGElement* context); + float ValueForLength(const Length&, SVGLengthMode) const; + // … +}; +``` + +`SVGLengthMode` is one of `kWidth`, `kHeight`, `kOther` (diagonal = +sqrt(w² + h²) / sqrt(2)) — the spec requires different resolution axes for +different attributes. + +## Pattern & gradient units + +`patternUnits`, `patternContentUnits`, `gradientUnits` use one of: + +- `userSpaceOnUse` — coordinates are in the user space of the element + **referencing** the paint server. +- `objectBoundingBox` — coordinates are fractions of the referencing + element's bounding box (so `x="0"` to `x="1"` spans the whole shape). + +This is resolved in `LayoutSVGResourcePattern::BuildPatternData()` and the +equivalent for gradients, producing an `AffineTransform` that is composed +into the shader's local matrix. + +## Non-scaling stroke + +`vector-effect: non-scaling-stroke` decouples stroke width from the element's +transform. Implementation: + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_shape.cc +if (HasNonScalingStroke()) { + root_transform.Scale(StyleRef().EffectiveZoom()) + .PreConcat(NonScalingStrokeTransform()); + path = &NonScalingStrokePath(); +} +``` + +The path is pre-transformed into a coordinate space where scale has been +factored out, then stroked at the nominal width, then projected back. The +result is a stroke whose visual width is independent of the element's +transform. + +## Hit-test coordinate mapping + +Hit tests start in CSS-pixel screen space and walk down: + +1. `LayoutSVGRoot::NodeAtPoint()` applies the inverse of + `LocalToBorderBoxTransform()` to get SVG viewport coords. +2. `LayoutSVGContainer::NodeAtPoint()` iterates children in paint order, + recursively applying each child's `LocalSVGTransform().Inverse()`. +3. `LayoutSVGShape::NodeAtPoint()` tests the transformed location against + the cached `Path` (fill via winding rule, stroke via the cached + `stroke_path_cache_`). + +`pointer-events` gates whether fill/stroke count: +`auto | none | visiblePainted | visibleFill | visibleStroke | visible | +painted | fill | stroke | all`. + +## Source files + +| File | Role | +| --------------------------------------------------------------------------------- | ------------------------------------------------ | +| `third_party/blink/renderer/core/svg/svg_element.h` | `LocalCoordinateSpaceTransform`, CTMScope | +| `third_party/blink/renderer/core/svg/svg_fit_to_view_box.h` | `ViewBoxToViewTransform()` | +| `third_party/blink/renderer/core/svg/svg_preserve_aspect_ratio.h` | Alignment / meet-or-slice enum | +| `third_party/blink/renderer/core/svg/svg_length_context.h` | Percentage and unit resolution | +| `third_party/blink/renderer/core/layout/svg/layout_svg_root.h` | Outer ``; `LocalToBorderBoxTransform` | +| `third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.h` | Nested `` | +| `third_party/blink/renderer/core/layout/svg/layout_svg_transformable_container.h` | `` | +| `third_party/blink/renderer/core/layout/svg/layout_svg_model_object.h` | `LocalSVGTransform`, `LocalToSVGParentTransform` | diff --git a/docs/wg/research/chromium/svg/index.md b/docs/wg/research/chromium/svg/index.md new file mode 100644 index 000000000..509a8bb5e --- /dev/null +++ b/docs/wg/research/chromium/svg/index.md @@ -0,0 +1,63 @@ +--- +title: "Chromium SVG Rendering Research" +tags: + - internal + - research + - chromium + - rendering + - svg +--- + +# Chromium SVG Rendering Research + +How Blink (Chromium's rendering engine) renders SVG. These documents describe +the end-to-end pipeline — DOM construction, layout, paint, and compositing — +as it applies to SVG content, both inline and as a standalone image format. + +SVG is part of HTML. An `` element can be the root of a document, embedded +inline inside an HTML page, or loaded as an image. Blink handles all three +cases by wiring SVG into the same Style → Layout → Paint → Composite pipeline +that drives HTML, with an additional "SVG local coordinate space" layer that +lives under `LayoutSVGRoot`. + +## Documents + +| Document | Scope | +| -------------------------------------------------------- | -------------------------------------------------------------------------------- | +| [pipeline.md](./pipeline.md) | End-to-end pipeline: DOM → LayoutSVG\* → paint → composite | +| [coordinate-systems.md](./coordinate-systems.md) | viewBox, preserveAspectRatio, CTM, local-to-parent transforms | +| [paint-servers.md](./paint-servers.md) | Gradients and patterns as shader-producing resources | +| [resources-and-effects.md](./resources-and-effects.md) | ``, ``, ``, `` resolution | +| [path-geometry.md](./path-geometry.md) | `d=` parsing, `SVGPath` → `SkPath`, stroke properties → Skia | +| [text.md](./text.md) | SVG text: two-phase layout, text-on-path, `SvgTextLayoutAlgorithm` | +| [use-and-foreign-object.md](./use-and-foreign-object.md) | `` shadow instance tree, `` HTML-in-SVG bridging | +| [svg-as-image.md](./svg-as-image.md) | Inline vs standalone vs ``-embedded SVG; `SVGImage`, `SVGImageForContainer` | +| [comparison.md](./comparison.md) | Cross-engine comparison: Chromium vs Servo vs resvg | + +## Pre-existing companion docs + +These sit under `docs/wg/research/chromium/` (not this subdirectory): + +- [`svg-pattern.md`](../svg-pattern.md) — deep dive on the `` paint + server (pre-dates this folder). [paint-servers.md](./paint-servers.md) + summarizes and cross-references it. +- [`render-surfaces.md`](../render-surfaces.md) — compositor render-surface + creation rules (filters, masks, blend modes). Referenced by + [resources-and-effects.md](./resources-and-effects.md). +- [`paint-recording.md`](../paint-recording.md) — `PaintRecord`, display + lists, R-tree indexing. SVG painters emit into this same machinery. +- [`blink-rendering-pipeline.md`](../blink-rendering-pipeline.md) — the + Style → Layout → Paint pipeline for HTML. SVG layers on top of it. + +## Source locations + +All findings are from the `third_party/blink/renderer/core/` subtree: + +- `svg/` — DOM element classes (`SVGElement`, `SVGPathElement`, …). +- `layout/svg/` — layout tree (`LayoutSVGRoot`, `LayoutSVGShape`, …). +- `paint/` — painters (`SVGShapePainter`, `SVGObjectPainter`, …). +- `svg/graphics/filters/` — filter graph builder. +- `svg/graphics/` — `SVGImage`, `SVGImageForContainer`. + +Platform-graphics types (`Pattern`, `Gradient`, `Path`, `GraphicsContext`) live +under `third_party/blink/renderer/platform/graphics/`. diff --git a/docs/wg/research/chromium/svg/paint-servers.md b/docs/wg/research/chromium/svg/paint-servers.md new file mode 100644 index 000000000..d323ae41c --- /dev/null +++ b/docs/wg/research/chromium/svg/paint-servers.md @@ -0,0 +1,210 @@ +--- +title: "Chromium SVG Paint Servers" +tags: + - internal + - research + - chromium + - rendering + - svg +--- + +# Chromium SVG Paint Servers + +How `fill="url(#id)"` and `stroke="url(#id)"` resolve into Skia shaders. Paint +servers are the ``, ``, and `` +elements. They are **resources** — they never create compositor layers or +render surfaces; they produce shaders applied to draw calls at paint time. + +A deep-dive on `` specifically lives at +[svg-pattern.md](../svg-pattern.md). This document covers the shared paint +server architecture and gradients. + +## Class hierarchy + +``` +LayoutSVGHiddenContainer never visual; extends LayoutSVGContainer + └── LayoutSVGResourceContainer base for all -type resources + ├── LayoutSVGResourcePaintServer ApplyShader() → cc::PaintFlags + │ ├── LayoutSVGResourcePattern + │ └── LayoutSVGResourceGradient + │ ├── LayoutSVGResourceLinearGradient + │ └── LayoutSVGResourceRadialGradient + ├── LayoutSVGResourceClipper (see resources-and-effects.md) + ├── LayoutSVGResourceMasker + ├── LayoutSVGResourceFilter + └── LayoutSVGResourceMarker +``` + +All paint servers share the same entry point: + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h +class LayoutSVGResourcePaintServer : public LayoutSVGResourceContainer { + public: + virtual bool ApplyShader(const SVGResourceClient&, + const gfx::RectF& reference_box, + const AffineTransform* additional_transform, + const AutoDarkMode&, + cc::PaintFlags&) = 0; +}; +``` + +## Resolution pipeline — from `fill="url(#id)"` to shader + +1. **Style cascade** — `fill: url(#id)` becomes an `SVGPaintType::kUriFunction` + in `SVGComputedStyle::fill.Resource()`. +2. **Client registration** — when a `LayoutSVG*` object is created for a shape + that references a resource, `SVGResources::UpdatePaints()` registers the + shape as a client of the target element via `SVGElementResourceClient`. +3. **Paint time** — `SVGShapePainter` calls `SVGObjectPainter::PreparePaint()`, + which looks up the resource and calls `ApplyShader()`: + +```cpp +// third_party/blink/renderer/core/paint/svg_object_painter.cc +bool ApplyPaintResource(const SvgContextPaints::ContextPaint& context_paint, + const AffineTransform* additional_paint_server_transform, + cc::PaintFlags& flags) { + SVGElementResourceClient* client = + SVGResources::GetClient(context_paint.object); + auto* uri_resource = GetSVGResourceAsType( + *client, context_paint.paint.Resource()); + if (!uri_resource->ApplyShader( + *client, SVGResources::ReferenceBoxForEffects(context_paint.object), + additional_paint_server_transform, auto_dark_mode, flags)) + return false; + return true; +} +``` + +4. **Shader attached** — the paint server sets `cc::PaintShader` on the + `cc::PaintFlags`, which is then used by the subsequent `DrawPath` / + `DrawRect`. + +`SVGResources::ReferenceBoxForEffects()` returns the bounding box to use +for `objectBoundingBox` resolution; this is configurable via +`geometry_box` (`fill-box` / `stroke-box` / `view-box`) from the +`geometry-box` CSS value. + +## Per-client caching + +Paint servers maintain a per-client cache because `objectBoundingBox` makes +tile/gradient geometry depend on the referencing shape's bounds: + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h +// A pattern can be referenced by many shapes; each shape may have a +// different bounding box, so each client needs its own Pattern shader. +HeapHashMap, std::unique_ptr> + pattern_map_; +``` + +Gradients follow the same pattern. + +Invalidation: when the resource element changes, `SVGResource` notifies its +clients via `SVGResourceClient::ResourceContentChanged()`, which clears the +cache and schedules paint invalidation on each shape. + +## Gradients + +### Attribute collection + +Gradients inherit attributes through an `xlink:href` chain (same as patterns). +`SVGGradientElement::CollectCommonAttributes()` walks the href chain with +cycle detection, filling in any unset attributes from referenced elements. + +Common gradient attributes: + +- `gradientUnits` — `userSpaceOnUse` or `objectBoundingBox` +- `gradientTransform` — applied as shader local matrix +- `spreadMethod` — `pad` / `reflect` / `repeat` (Skia `SkTileMode`) +- color stops (from `` children): offset, color, opacity + +Linear-specific: `x1`, `y1`, `x2`, `y2` (the gradient vector). +Radial-specific: `cx`, `cy`, `r`, `fx`, `fy`, `fr` (center, radius, focal). + +### BuildGradientData + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc +std::unique_ptr LayoutSVGResourceGradient::BuildGradientData( + const gfx::RectF& object_bounding_box) { + const GradientAttributes& attributes = EnsureAttributes(); + auto gradient_data = std::make_unique(); + + // 1. Resolve endpoints in user space: + // objectBoundingBox → scale by object bbox, then translate + // userSpaceOnUse → already in user space + + // 2. Build Gradient with resolved stops + spread mode + + // 3. Apply gradientTransform to Gradient's local matrix + gradient_data->gradient->SetGradientSpaceTransform(gradient_transform); + return gradient_data; +} +``` + +The resolved `Gradient` (platform wrapper around `SkShader`) produces a +`cc::PaintShader` via `Gradient::CreateShader()`, which is attached to +`cc::PaintFlags` identically to the pattern case. + +## Patterns + +See [svg-pattern.md](../svg-pattern.md) for the full walkthrough. In short: + +1. `CollectPatternAttributes()` — walk href chain, fill defaults. +2. `BuildPatternData()`: + - Resolve `tile_bounds` (x/y/width/height) in user space. + - Resolve `tile_transform` from `viewBox` or `patternContentUnits`. + - Call `AsPaintRecord(tile_transform)` — record tile children into a + `PaintRecord` (**not** a bitmap). + - Wrap recording in `PaintRecordPattern`, create `PaintShader` with + `SkTileMode::kRepeat` on both axes. + - Compose shader local matrix: + `Translate(tile.x, tile.y) · patternTransform`. +3. `ApplyShader()` caches per client, applies shader to `cc::PaintFlags`. + +Key insight: the tile is a display list, not a rasterized bitmap. Skia +rasterizes it lazily at the correct scale, so patterns stay +resolution-independent. + +## Shared abstractions + +``` +platform/graphics/ +├── Pattern abstract; holds cached PaintShader +│ ├── ImagePattern raster-image patterns (CSS background-image) +│ └── PaintRecordPattern SVG — record-based tiling +└── Gradient abstract; holds stops + spread + local matrix + ├── LinearGradient + ├── RadialGradient + └── ConicGradient CSS conic-gradient() only (no SVG equivalent) +``` + +Both `Pattern` and `Gradient` expose `ApplyToFlags(cc::PaintFlags&, +SkMatrix& local_matrix)`, which constructs the `cc::PaintShader` and attaches +it. The shader is cached so long as `local_matrix` doesn't change. + +## What paint servers never do + +- They never create compositor layers. +- They never create render surfaces (see [render-surfaces.md](../render-surfaces.md)). +- They never trigger compositing promotion. +- They never participate in damage tracking as visual elements — they're + pure shader sources. + +Any SVG feature that would need compositing (blend mode, filter, mask on a +**consuming** shape) is handled by that consuming shape, not by the paint +server. + +## Source files + +| File | Role | +| ----------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h` | Abstract base | +| `third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc` | Gradient base; `BuildGradientData()`, `ApplyShader()` | +| `third_party/blink/renderer/core/layout/svg/layout_svg_resource_linear_gradient.cc` | Linear-specific | +| `third_party/blink/renderer/core/layout/svg/layout_svg_resource_radial_gradient.cc` | Radial-specific | +| `third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc` | See [svg-pattern.md](../svg-pattern.md) | +| `third_party/blink/renderer/core/paint/svg_object_painter.cc` | Resource resolution at paint time | +| `third_party/blink/renderer/platform/graphics/gradient.cc` | Platform gradient wrapper | +| `third_party/blink/renderer/platform/graphics/pattern.cc` | Platform pattern wrapper | diff --git a/docs/wg/research/chromium/svg/path-geometry.md b/docs/wg/research/chromium/svg/path-geometry.md new file mode 100644 index 000000000..6e37e2e02 --- /dev/null +++ b/docs/wg/research/chromium/svg/path-geometry.md @@ -0,0 +1,251 @@ +--- +title: "Chromium SVG Path Geometry and Stroking" +tags: + - internal + - research + - chromium + - rendering + - svg +--- + +# Chromium SVG Path Geometry and Stroking + +How `` data becomes an `SkPath`, and how SVG stroke properties +(`stroke-width`, `stroke-linecap`, `stroke-dasharray`, …) are mapped to Skia. + +## Path data parsing + +### Byte stream representation + +`SVGPath` stores parsed path data in an `SVGPathByteStream` — a compact +binary format that represents each segment as a command byte plus its +float parameters. This is the canonical internal representation; the +ASCII `d="M 10 10 L 50 50"` string is parsed once and cached. + +```cpp +// third_party/blink/renderer/core/svg/svg_path.h +class SVGPath final : public SVGPropertyBase { + public: + const SVGPathByteStream& ByteStream() const; + SVGPath* Clone() const; + String ValueAsString() const override; + SVGParsingError SetValueAsString(const String&); +}; +``` + +### Parser — consumer pattern + +The parser is a template-based producer/consumer: + +```cpp +// third_party/blink/renderer/core/svg/svg_path_parser.h +namespace svg_path_parser { + template + inline bool ParsePath(SourceType& source, ConsumerType& consumer) { + while (source.HasMoreData()) { + PathSegmentData segment = source.ParseSegment(); + if (segment.command == kPathSegUnknown) return false; + consumer.EmitSegment(segment); + } + return true; + } +} +``` + +Sources: `SVGPathStringSource` (ASCII `d=`), `SVGPathByteStreamSource` (compact +binary). + +Consumers: + +- `SVGPathByteStreamBuilder` — writes into a new byte stream. +- `SVGPathNormalizer` — converts relative commands to absolute; keeps arcs. +- `SVGPathStringBuilder` — serializes back to ASCII. +- `SVGPathBuilder` — **the renderer consumer**: builds a `Path` (SkPath + wrapper) by emitting `moveTo`, `lineTo`, `cubicTo`, etc. +- `SVGMarkerDataBuilder` — walks to compute marker positions (see + [resources-and-effects.md](./resources-and-effects.md)). +- `SVGPathAbsolutizer` — variant of normalizer. + +Arcs (`A`/`a` command) are typically converted to cubic Béziers at build +time via the standard endpoint-to-center-parameterization + arc-to-cubic +decomposition. + +## `Path` — the Skia wrapper + +`Path` (`third_party/blink/renderer/platform/graphics/path.h`) is Blink's +wrapper around `SkPath`. It adds helpers that SVG needs: + +- `BoundingRect()`, `StrokeBoundingRect(const StrokeData&)` +- `Contains(const gfx::PointF&, WindRule)` — point-in-path hit testing +- `StrokeContains(const gfx::PointF&, const StrokeData&)` +- `ApplyTransform(const AffineTransform&)` + +Shapes build their `Path` lazily via `SVGGeometryElement::AsPath()`, which +dispatches to element-specific construction: + +```cpp +// third_party/blink/renderer/core/layout/svg/layout_svg_shape.cc +void LayoutSVGShape::CreatePath() { + if (!path_) + path_ = std::make_unique(); + *path_ = To(GetElement())->AsPath(); + DCHECK(!stroke_path_cache_); +} +``` + +``, ``, ``, ``, ``, `` each +build their own `Path` directly (e.g., `SVGRectElement::AsPath()` constructs +a rectangle, or a rounded rect if `rx`/`ry` are set). `` replays the +byte stream through `SVGPathBuilder`. + +## Fill rule and winding + +`fill-rule: nonzero | evenodd` (and `clip-rule` for clip paths) maps +directly to Skia's `SkPathFillType::kWinding` / `kEvenOdd`. The fill rule is +not stored on the `SkPath` itself when it's used for stroking — it's only +applied at fill time. + +## Stroke + +### Stroke properties mapping + +```cpp +// third_party/blink/renderer/core/layout/svg/svg_layout_support.h +static void ApplyStrokeStyleToStrokeData(StrokeData&, + const ComputedStyle&, + const LayoutObject&, + float dash_scale_factor); +``` + +`StrokeData` (`platform/graphics/stroke_data.h`) maps as follows: + +| SVG / CSS property | `StrokeData` field | Skia / SkPaint equivalent | +| ------------------- | ------------------------ | --------------------------------------- | +| `stroke-width` | `thickness_` | `SkPaint::setStrokeWidth` | +| `stroke-linecap` | `line_cap_` | `SkPaint::Cap` — Butt / Round / Square | +| `stroke-linejoin` | `line_join_` | `SkPaint::Join` — Miter / Round / Bevel | +| `stroke-miterlimit` | `miter_limit_` | `SkPaint::setStrokeMiter` | +| `stroke-dasharray` | `dash_` | `SkDashPathEffect::Make(intervals, …)` | +| `stroke-dashoffset` | phase arg of dash effect | same | + +### Dash scaling for transforms + +```cpp +// layout_svg_shape.cc +StrokeData stroke_data; +SVGLayoutSupport::ApplyStrokeStyleToStrokeData(stroke_data, StyleRef(), *this, + DashScaleFactor()); +``` + +`DashScaleFactor()` accounts for uniform scale components of the element's +transform so that `stroke-dasharray` intervals remain visually consistent +when the shape is scaled. For non-uniform scales, the approximation can +diverge from the spec. + +### Non-scaling stroke + +`vector-effect: non-scaling-stroke` un-scales the path before stroking. See +[coordinate-systems.md](./coordinate-systems.md#non-scaling-stroke). + +### Stroke-path cache + +```cpp +// layout_svg_shape.h +mutable std::unique_ptr stroke_path_cache_; +``` + +The actual stroked `Path` (result of applying stroke width to the geometry) +is cached for hit testing — computing a stroke outline is expensive, so +hit tests reuse it across pointer events until the geometry, transform, or +stroke properties change. + +### Stroke bounds + +```cpp +// layout_svg_shape.cc +gfx::RectF LayoutSVGShape::StrokeBoundingBox() const { + if (!StyleRef().HasStroke() || IsShapeEmpty()) + return fill_bounding_box_; + if (!HasPath()) { + DCHECK(CanUseSimpleStrokeApproximation(geometry_type_)); + return ApproximateStrokeBoundingBox(fill_bounding_box_); + } + StrokeData stroke_data; + SVGLayoutSupport::ApplyStrokeStyleToStrokeData(stroke_data, StyleRef(), + *this, DashScaleFactor()); + DashArray dashes; + stroke_data.SetLineDash(dashes, 0); // dashes don't affect bounds per spec + const gfx::RectF stroke_bounds = GetPath().StrokeBoundingRect(stroke_data); + return gfx::UnionRects(fill_bounding_box_, stroke_bounds); +} +``` + +Two fast paths: + +- Empty shape → fill bbox only (no stroke). +- Simple geometry (rect, circle, ellipse, line) where bounds can be computed + analytically → `ApproximateStrokeBoundingBox()` inflates fill bbox by + ~`stroke-width / 2 * miter_factor`. + +Otherwise, Skia computes stroke bounds from the actual stroke outline. + +Dashes are explicitly cleared before computing bounds — the SVG spec says +bounds should reflect the un-dashed stroke envelope, not the gap regions. + +## Shape rendering modes + +`shape-rendering: auto | optimizeSpeed | crispEdges | geometricPrecision` +maps to Skia anti-aliasing and hinting: + +- `crispEdges` — disable antialiasing (`SkPaint::setAntiAlias(false)`). +- `optimizeSpeed` — implementation-defined; Blink treats as `crispEdges` + when beneficial. +- `geometricPrecision`, `auto` — full anti-aliasing. + +## Hit testing fill and stroke + +```cpp +// layout_svg_shape.cc +bool LayoutSVGShape::ShapeDependentFillContains( + const HitTestLocation& location, + const WindRule fill_rule) const { + return location.Intersects(GetPath(), fill_rule); +} + +bool LayoutSVGShape::ShapeDependentStrokeContains( + const HitTestLocation& location) { + if (!stroke_path_cache_) { + const Path* path = path_.get(); + AffineTransform root_transform; + if (HasNonScalingStroke()) { + root_transform.Scale(StyleRef().EffectiveZoom()) + .PreConcat(NonScalingStrokeTransform()); + path = &NonScalingStrokePath(); + } + StrokeData stroke_data; + SVGLayoutSupport::ApplyStrokeStyleToStrokeData( + stroke_data, StyleRef(), *this, DashScaleFactor()); + stroke_path_cache_ = std::make_unique( + path->StrokePath(stroke_data, root_transform)); + } + return stroke_path_cache_->Contains(location.TransformedPoint()); +} +``` + +Hit testing follows `pointer-events` to decide whether to test fill, stroke, +or both. + +## Source files + +| File | Role | +| -------------------------------------- | --------------------------------------------- | +| `core/svg/svg_path.h` | Parsed path wrapper; byte stream | +| `core/svg/svg_path_parser.h` | Template parser; consumer pattern | +| `core/svg/svg_path_byte_stream.h` | Compact binary representation | +| `core/svg/svg_path_builder.h` | Byte stream → `Path` (SkPath) construction | +| `core/svg/svg_path_string_source.h` | ASCII `d=` tokenizer | +| `core/svg/svg_path_normalizer.h` | Relative → absolute, arc-to-cubic | +| `core/layout/svg/layout_svg_shape.h` | Shape base; owns `Path`, `stroke_path_cache_` | +| `core/layout/svg/svg_layout_support.h` | `ApplyStrokeStyleToStrokeData()` | +| `platform/graphics/path.h` | Blink `Path` wrapper around `SkPath` | +| `platform/graphics/stroke_data.h` | Stroke properties value object | diff --git a/docs/wg/research/chromium/svg/pipeline.md b/docs/wg/research/chromium/svg/pipeline.md new file mode 100644 index 000000000..1117afc90 --- /dev/null +++ b/docs/wg/research/chromium/svg/pipeline.md @@ -0,0 +1,226 @@ +--- +title: "Chromium SVG Pipeline Overview" +tags: + - internal + - research + - chromium + - rendering + - svg +--- + +# Chromium SVG Pipeline Overview + +The end-to-end pipeline for rendering SVG inside Blink. Emphasis on where the +SVG pipeline diverges from HTML. + +``` +Parsing Style Layout Paint Composite +─────── ───── ────── ───── ───────── + ──▶ CSS cascade ──▶ LayoutSVGRoot ──▶ SVGRootPainter ──▶ cc::PaintRecord + │ on SVGElements │ │ │ + │ + presentation ├── LayoutSVG ├── SVGContainer ├── property trees + │ attributes ├── Container ├── Painter │ (transform/ + │ aliased to CSS ├── LayoutSVGShape ├── SVGShape │ effect/clip) + │ ├── LayoutSVGText ├── Painter │ + │ └── … └── … └── RenderSurfaces + ▼ ▲ ▲ for filter/mask +SVGElement tree │ │ + SVG resources resolved paint servers resolved + (clipPath, mask, filter, via SVGResources::GetClient + marker, pattern, gradient) → shader on PaintFlags +``` + +## Phases + +### 1. Parsing → `SVGElement` tree + +Blink's XML/HTML parser constructs a DOM where `` and its descendants +become concrete `SVGElement` subclasses: + +- Structural: `SVGSVGElement`, `SVGGElement`, `SVGDefsElement`, `SVGSymbolElement` +- Shapes: `SVGPathElement`, `SVGRectElement`, `SVGCircleElement`, `SVGEllipseElement`, `SVGLineElement`, `SVGPolygonElement`, `SVGPolylineElement` +- Text: `SVGTextElement`, `SVGTSpanElement`, `SVGTextPathElement` +- Paint servers: `SVGLinearGradientElement`, `SVGRadialGradientElement`, `SVGPatternElement` +- Effects: `SVGClipPathElement`, `SVGMaskElement`, `SVGFilterElement`, `SVGMarkerElement` +- Filter primitives: `SVGFEGaussianBlurElement`, `SVGFEColorMatrixElement`, 20+ more +- Structural refs: `SVGUseElement`, `SVGImageElement`, `SVGForeignObjectElement` + +Presentation attributes (`fill="red"`) and typed attributes (`width="100"`) are +stored as `SVGAnimated*` wrappers around a base value and an animated value — +`SVGAnimatedLength`, `SVGAnimatedNumber`, `SVGAnimatedTransformList`, etc. + +```cpp +// third_party/blink/renderer/core/svg/svg_animated_length.h +class SVGAnimatedLength : public ScriptWrappable, + public SVGAnimatedProperty { + public: + SVGAnimatedLength(SVGElement* context_element, + const QualifiedName& attribute_name, + SVGLengthMode mode, + SVGLength::Initial initial_value, + CSSPropertyID css_property_id = CSSPropertyID::kInvalid); + const CSSValue* CssValue() const final; // bridges SVG value → CSS cascade +}; +``` + +The `css_property_id` argument is the bridge: SVG presentation attributes that +have a CSS counterpart (`fill`, `stroke`, `opacity`, `font-family`, …) are fed +into the CSS cascade as if they were inline styles. The cascade also accepts +real CSS rules (`rect { fill: red; }`). + +### 2. Style → `ComputedStyle` + +SVG reuses the HTML style system. `ComputedStyle` is the same type for HTML and +SVG; SVG-specific properties live in `SVGComputedStyle` +(`third_party/blink/renderer/core/style/svg_computed_style.h`), accessed via +`style.SvgStyle()`. Fields include: + +- `fill`, `stroke`, `stroke_dasharray`, `fill_rule`, `clip_rule` +- `marker_start`, `marker_mid`, `marker_end` +- `stop_color`, `stop_opacity`, `flood_color`, `flood_opacity` +- `paint_order` (for controlling fill/stroke/marker order) + +The style cascade runs the same way as for HTML. The only SVG-specific step is +that presentation attributes are parsed as CSS values before entering the +cascade. + +### 3. Layout → `LayoutSVG*` tree + +SVG has its own layout tree that grafts into Blink's layout tree at +`LayoutSVGRoot`. `LayoutSVGRoot` extends `LayoutReplaced` (a CSS-sized replaced +element), and everything below it is SVG-native. + +``` +LayoutObject +├── LayoutSVGRoot extends LayoutReplaced; CSS box ↔ SVG viewport +│ └── (SVG subtree below) +├── LayoutSVGModelObject abstract base for SVG content +│ ├── LayoutSVGContainer , — groups children +│ │ ├── LayoutSVGTransformableContainer +│ │ ├── LayoutSVGViewportContainer nested +│ │ ├── LayoutSVGHiddenContainer , , , , +│ │ │ └── LayoutSVGResourceContainer base for all resource containers +│ │ │ ├── LayoutSVGResourcePaintServer abstract +│ │ │ │ ├── LayoutSVGResourcePattern +│ │ │ │ └── LayoutSVGResourceGradient (linear + radial concrete) +│ │ │ ├── LayoutSVGResourceClipper +│ │ │ ├── LayoutSVGResourceMasker +│ │ │ ├── LayoutSVGResourceFilter +│ │ │ └── LayoutSVGResourceMarker +│ │ └── … +│ ├── LayoutSVGShape , , , , , , +│ │ └── LayoutSVGPath specialization where path can be complex +│ ├── LayoutSVGImage +│ └── LayoutSVGForeignObject — bridges back to HTML layout +└── LayoutSVGText — extends LayoutSVGBlock (reuses LayoutNG inline layout) + └── LayoutSVGInline , + └── LayoutSVGInlineText text runs +``` + +Unlike HTML layout (which produces rectangular fragments), SVG layout produces +**paths and bounding boxes in the element's local coordinate system**, plus a +**transform to the parent SVG coordinate system** +(`LocalToSVGParentTransform()`). The tree is then walked during paint with +these transforms composed. + +Source: `third_party/blink/renderer/core/layout/svg/layout_svg_model_object.h` +and sibling files. + +### 4. Paint → `PaintRecord` + +Each `LayoutSVG*` object has a matching painter: + +| Layout class | Painter | +| ----------------------------------- | ------------------------- | +| `LayoutSVGRoot` | `SVGRootPainter` | +| `LayoutSVGContainer` (+ subclasses) | `SVGContainerPainter` | +| `LayoutSVGShape` | `SVGShapePainter` | +| `LayoutSVGImage` | `SVGImagePainter` | +| `LayoutSVGForeignObject` | `SVGForeignObjectPainter` | +| `LayoutSVGText` | `SVGTextPainter` | + +All painters emit into the same `PaintRecord` (display list) machinery used by +HTML — see [`paint-recording.md`](../paint-recording.md). A painter: + +1. Checks paint phase (only `kForeground` for most SVG content). +2. Runs `ScopedSVGTransformState` — concats this element's + `LocalSVGTransform()` onto the `GraphicsContext` CTM. +3. Runs `ScopedSVGPaintState` — prepares fill/stroke `cc::PaintFlags`, + applying paint servers (shaders), clip paths, masks, filters. +4. Records `DrawPath` / `DrawRect` / text ops into the `PaintRecord`. + +Paint order inside a shape respects the `paint-order` CSS property; default is +`fill, stroke, markers`. + +```cpp +// third_party/blink/renderer/core/paint/svg_shape_painter.cc +void SVGShapePainter::Paint(const PaintInfo& paint_info) { + if (paint_info.phase != PaintPhase::kForeground || + layout_svg_shape_.IsShapeEmpty()) + return; + + // cull-rect skip + if (SVGModelObjectPainter::CanUseCullRect(layout_svg_shape_.StyleRef())) { + if (!paint_info.GetCullRect().IntersectsTransformed( + layout_svg_shape_.LocalSVGTransform(), + layout_svg_shape_.VisualRectInLocalSVGCoordinates())) + return; + } + + ScopedSVGTransformState transform_state(paint_info, layout_svg_shape_); + ScopedSVGPaintState paint_state(layout_svg_shape_, …); + if (!DrawingRecorder::UseCachedDrawingIfPossible(…)) { + SVGDrawingRecorder recorder(…); + PaintShape(content_paint_info); // fill → stroke → markers + } +} +``` + +### 5. Composite → cc property trees + +Once `PaintRecord`s exist, Blink hands them to the compositor (`cc/`). SVG +content participates in the same property trees (transform / effect / clip / +scroll) as HTML. SVG-specific wrinkles: + +- Most SVG transforms are **baked into the `PaintRecord`** via + `canvas->concat(local_transform)` rather than as a compositor transform + node. Compositor transforms are reserved for elements that opt into + compositing (e.g., an animated `` root, or an ancestor with + `will-change: transform`). +- ``, ``, and explicit blend modes can force a **render + surface** (offscreen buffer). See + [render-surfaces.md](../render-surfaces.md). +- Paint servers (pattern, gradient) **never** create compositor layers — + they are always resolved as shaders at paint time. + See [svg-pattern.md](../svg-pattern.md) for the pattern case. + +## How SVG diverges from HTML + +| Aspect | HTML | SVG | +| ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| Layout tree root | `LayoutView` | `LayoutSVGRoot` (attached under HTML `LayoutView` like any replaced element) | +| Layout result | Rectangular fragments (`PhysicalFragment`) | Local paths + bounding boxes + `LocalToSVGParentTransform()` | +| Coordinate system | CSS pixels, one per element box | Arbitrary user units; `` establishes a new coordinate space; nested `` nest viewports | +| Transform ownership | Compositor transform tree | Baked into `PaintRecord` by default; compositor only for promoted layers | +| Paint order | Stacking contexts (CSS §9.9) | Document order within a group; `paint-order` controls fill/stroke/marker within a shape | +| Text layout | LayoutNG inline flow | LayoutNG inline flow **followed by** `SvgTextLayoutAlgorithm` that rewrites per-glyph positions | +| Resource resolution | URL → resource loader (for `background-image`) | `url(#id)` → same-document `SVGElementResourceClient` lookup; resolved at **paint time**, cached per-client | +| Replaced elements | ``, `