본문으로 건너뛰기

Chromium SVG Pattern Paint Server

How Chromium handles the SVG <pattern> element. The pattern element is a paint server — a resource container that produces a tiling shader. It is never a visual node in the render tree or compositor layer tree.

For how PaintRecord works as a display list see paint-recording.md. For render surfaces and compositing see render-surfaces.md.


Class Hierarchy

LayoutSVGHiddenContainer
└── LayoutSVGResourceContainer (base for clips, masks, filters, paint servers)
└── LayoutSVGResourcePaintServer (base: pattern + gradient)
├── LayoutSVGResourcePattern
└── LayoutSVGResourceGradient

On the DOM side:

SVGElement
└── SVGPatternElement (also mixes in SVGURIReference, SVGTests, SVGFitToViewBox)

On the platform graphics side:

Pattern (abstract, ref-counted, holds cached PaintShader)
├── ImagePattern (raster image patterns)
└── PaintRecordPattern (SVG <pattern> — record-based tiling)

LayoutSVGResourcePattern inherits from LayoutSVGHiddenContainer. It is explicitly hidden from the visual tree. It does not create a compositor layer or render surface. It only participates as a resource that produces a shader when referenced by fill="url(#id)" or stroke="url(#id)".

Source: third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h


Pipeline Overview

SVGPatternElement (DOM)
│ CreateLayoutObject()

LayoutSVGResourcePattern (hidden resource container, not in visual tree)
│ ApplyShader() — called when a client shape needs fill/stroke

BuildPatternData(object_bounding_box)
├── CollectPatternAttributes() → resolved PatternAttributes (with href chain)
├── ResolveRectangle() → tile_bounds (x, y, w, h in user units)
├── ViewBoxToViewTransform() → tile_transform (or patternContentUnits scale)
├── AsPaintRecord(tile_transform) → PaintRecord (display list of tile content)
│ ├── PaintRecorder::beginRecording()
│ ├── SubtreeContentTransformScope(tile_transform)
│ ├── for each child: SVGObjectPainter::PaintResourceSubtree()
│ ├── canvas->concat(tile_transform)
│ └── finishRecordingAsPicture() → PaintRecord
└── Pattern::CreatePaintRecordPattern(record, tile_size)
└── PaintRecordPattern
│ CreateShader() → PaintShader::MakePaintRecord(kRepeat, kRepeat)
│ ApplyToFlags()

cc::PaintFlags::setShader(shader) ← geometry's fill/stroke now tiles the pattern

DOM Element and Attribute Collection

When the parser encounters <pattern>, it creates an SVGPatternElement. The element creates its layout object:

// svg_pattern_element.cc:171-173
LayoutObject* SVGPatternElement::CreateLayoutObject(const ComputedStyle&) {
return MakeGarbageCollected<LayoutSVGResourcePattern>(this);
}

Pattern attributes can be inherited through an xlink:href chain. CollectPatternAttributes() walks the chain with cycle detection:

// svg_pattern_element.cc:224-268
PatternAttributes SVGPatternElement::CollectPatternAttributes() const {
HeapHashSet<Member<const SVGPatternElement>> processed_patterns;
const SVGPatternElement* current = this;

PatternAttributes attributes;
while (true) {
SetPatternAttributes(*current, attributes);
processed_patterns.insert(current);

current = current->ReferencedElement();
if (!current || !current->GetLayoutObject())
break;
if (processed_patterns.Contains(current))
break; // cycle detection
}
// fill out defaults for unset fields ...
}

Each attribute is only set if not already specified (first-wins), implementing the SVG spec's pattern attribute inheritance.

PatternAttributes Value Object

The PatternAttributes struct holds the resolved values:

  • x, y, width, heightSVGLength values for the tile rect
  • viewBox — optional gfx::RectF
  • preserveAspectRatioSVGPreserveAspectRatio
  • patternUnitsSVGUnitTypes (userSpaceOnUse or objectBoundingBox)
  • patternContentUnitsSVGUnitTypes
  • patternTransformAffineTransform
  • content element — the SVGPatternElement whose children define the tile

Source: third_party/blink/renderer/core/svg/pattern_attributes.h, third_party/blink/renderer/core/svg/svg_pattern_element.cc


Paint Resolution — How a Shape Gets Its Pattern Shader

When an SVG shape (e.g. <rect fill="url(#myPattern)">) is being painted, SVGObjectPainter::PreparePaint() resolves the URL reference:

// svg_object_painter.cc:19-42
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<LayoutSVGResourcePaintServer>(
*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;
}

This calls LayoutSVGResourcePattern::ApplyShader().

Source: third_party/blink/renderer/core/paint/svg_object_painter.cc


ApplyShader — Per-Client Caching

// layout_svg_resource_pattern.cc:178-201
bool LayoutSVGResourcePattern::ApplyShader(
const SVGResourceClient& client,
const gfx::RectF& reference_box,
const AffineTransform* additional_transform,
const AutoDarkMode&,
cc::PaintFlags& flags) {
std::unique_ptr<PatternData>& pattern_data =
pattern_map_.insert(&client, nullptr).stored_value->value;
if (!pattern_data)
pattern_data = BuildPatternData(reference_box);

if (!pattern_data->pattern)
return false;

AffineTransform transform = pattern_data->transform;
if (additional_transform)
transform = *additional_transform * transform;
pattern_data->pattern->ApplyToFlags(flags, transform.ToSkMatrix());
flags.setFilterQuality(cc::PaintFlags::FilterQuality::kLow);
return true;
}

The pattern_map_ is per-client because patternUnits="objectBoundingBox" makes tile size relative to each client shape's bounding box, requiring a different shader. The source notes this as an optimization opportunity:

// layout_svg_resource_pattern.h:76-85
// FIXME: we can almost do away with this per-object map, but not quite: the
// tile size can be relative to the client bounding box, and it gets captured
// in the cached Pattern shader.
// Hence, we need one Pattern shader per client. The display list OTOH is the
// same => we should be able to cache a single display list per
// LayoutSVGResourcePattern + one Pattern(shader) for each client

PatternData Struct

// layout_svg_resource_pattern.cc:44-50
struct PatternData {
USING_FAST_MALLOC(PatternData);
public:
scoped_refptr<Pattern> pattern; // The tiling shader wrapper
AffineTransform transform; // Pattern-space to user-space transform
};

Source: third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc


BuildPatternData — Tile Metrics and Shader Construction

// layout_svg_resource_pattern.cc:122-176
std::unique_ptr<PatternData> LayoutSVGResourcePattern::BuildPatternData(
const gfx::RectF& object_bounding_box) {
auto pattern_data = std::make_unique<PatternData>();
const PatternAttributes& attributes = EnsureAttributes();

if (!attributes.PatternContentElement())
return pattern_data;

// 1. Compute tile bounds
gfx::RectF tile_bounds = ResolveRectangle(
attributes.PatternUnits(), object_bounding_box, *attributes.X(),
*attributes.Y(), *attributes.Width(), *attributes.Height());
if (tile_bounds.IsEmpty())
return pattern_data;

// 2. Compute tile content transform
AffineTransform tile_transform;
if (attributes.HasViewBox()) {
if (attributes.ViewBox().IsEmpty())
return pattern_data;
tile_transform = SVGFitToViewBox::ViewBoxToViewTransform(
attributes.ViewBox(), attributes.PreserveAspectRatio(),
tile_bounds.size());
} else {
if (attributes.PatternContentUnits() ==
SVGUnitTypes::kSvgUnitTypeObjectboundingbox) {
tile_transform.Scale(object_bounding_box.width(),
object_bounding_box.height());
}
}

if (!attributes.PatternTransform().IsInvertible())
return pattern_data;

// 3. Record tile content and create pattern shader
pattern_data->pattern = Pattern::CreatePaintRecordPattern(
AsPaintRecord(tile_transform), gfx::RectF(tile_bounds.size()));

// 4. Compose pattern-space-to-user-space transform
pattern_data->transform.Translate(tile_bounds.x(), tile_bounds.y());
pattern_data->transform.PostConcat(attributes.PatternTransform());

return pattern_data;
}

Key observations:

  • A viewBox overrides patternContentUnits per the SVG spec.
  • If patternContentUnits is objectBoundingBox and there is no viewBox, the tile content is scaled by the client's bounding box dimensions.
  • An uninvertible patternTransform causes the pattern to not paint (early return).
  • The final pattern-space transform composes: translate to (x, y) of the tile, then apply patternTransform.

Source: third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc


AsPaintRecord — Recording Pattern Children

This is where the tile content is "painted" — but into a recording, not a bitmap:

// layout_svg_resource_pattern.cc:203-237
PaintRecord LayoutSVGResourcePattern::AsPaintRecord(
const AffineTransform& tile_transform) const {
PaintRecorder paint_recorder;
cc::PaintCanvas* canvas = paint_recorder.beginRecording();

auto* pattern_content_element = attributes_.PatternContentElement();
const auto* pattern_layout_object = To<LayoutSVGResourceContainer>(
pattern_content_element->GetLayoutObject());

SubtreeContentTransformScope content_transform_scope(tile_transform);

PaintRecordBuilder builder;
for (LayoutObject* child = pattern_layout_object->FirstChild(); child;
child = child->NextSibling()) {
SVGObjectPainter(*child, nullptr).PaintResourceSubtree(builder.Context());
}
canvas->save();
canvas->concat(tile_transform.ToSkM44());
builder.EndRecording(*canvas);
canvas->restore();
return paint_recorder.finishRecordingAsPicture();
}

The recording mechanism:

  1. A PaintRecorder captures paint operations into a PaintRecord (display list), not a raster bitmap.
  2. SubtreeContentTransformScope sets a thread-local transform that descendant paint code reads for patternContentUnits/viewBox mapping.
  3. Each child of the pattern content element is painted via SVGObjectPainter::PaintResourceSubtree(), which invokes the regular Paint() method with PaintFlag::kPaintingResourceSubtree.
  4. The tile transform is baked into the recording via canvas->concat().

Source: third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc, third_party/blink/renderer/core/paint/svg_object_painter.cc


PaintRecordPattern — Creating the Tiling Shader

The recorded tile becomes a Skia tiling shader:

// paint_record_pattern.cc:36-41
sk_sp<PaintShader> PaintRecordPattern::CreateShader(
const SkMatrix& local_matrix) const {
return PaintShader::MakePaintRecord(
tile_record_, gfx::RectFToSkRect(tile_record_bounds_),
SkTileMode::kRepeat, SkTileMode::kRepeat, &local_matrix);
}

This creates a cc::PaintShader of type kPaintRecord with repeat tiling in both axes. The local_matrix encodes the composed pattern-space-to-user-space transform (tile offset + patternTransform + any additional context transform).

The shader is applied to the geometry's paint flags:

// pattern.cc:57-63
void Pattern::ApplyToFlags(cc::PaintFlags& flags,
const SkMatrix& local_matrix) const {
if (!cached_shader_ || local_matrix != cached_shader_->GetLocalMatrix())
cached_shader_ = CreateShader(local_matrix);
flags.setShader(cached_shader_);
}

The shader is cached and reused as long as the local matrix does not change.

Source: third_party/blink/renderer/platform/graphics/paint_record_pattern.cc, third_party/blink/renderer/platform/graphics/pattern.cc


patternTransform Handling

The patternTransform attribute is applied entirely as a shader matrix. It does not create a layer transform or affect the compositor tree. The composition order is:

  1. Translate to the tile origin (tile_bounds.x, tile_bounds.y)
  2. Post-concatenate patternTransform
  3. Pre-concatenate any additional context transform (from paint inheritance)
// layout_svg_resource_pattern.cc:171-174
pattern_data->transform.Translate(tile_bounds.x(), tile_bounds.y());
pattern_data->transform.PostConcat(attributes.PatternTransform());

// layout_svg_resource_pattern.cc:195-197
AffineTransform transform = pattern_data->transform;
if (additional_transform)
transform = *additional_transform * transform;
pattern_data->pattern->ApplyToFlags(flags, transform.ToSkMatrix());

This composed transform becomes the local_matrix of the PaintShader, which Skia uses to position and transform the tiling grid in the coordinate space of the filled geometry.


Key Design Decisions

1. Vector recording, not bitmap

The tile is a PaintRecord (display list of paint ops, analogous to SkPicture). Skia rasterizes it lazily at the appropriate resolution during draw. Pattern content stays resolution-independent — zooming in does not pixelate.

2. Per-client shader, shared display list (aspirational)

Because patternUnits="objectBoundingBox" makes tile size relative to the client shape's bounding box, there is one PatternData (shader) per client shape. The display list (recording) is the same across clients — the source notes this could be optimized by caching the recording separately.

3. Pattern never creates compositor layers

<pattern> inherits from LayoutSVGHiddenContainer. It does not trigger any of the RenderSurfaceReason conditions listed in render-surfaces.md. The pattern is purely a paint-time operation resolved during SVGObjectPainter::PreparePaint().

4. patternTransform is a shader matrix, not a layer transform

The transform is composed into the shader's local_matrix. This avoids creating transform nodes in the property tree or render surfaces in the compositor.

5. Pattern is a paint server, same category as gradients

LayoutSVGResourcePattern and LayoutSVGResourceGradient share the same base class (LayoutSVGResourcePaintServer) and the same resolution path (ApplyShader() → shader on PaintFlags). Patterns and gradients are treated identically from the compositor's perspective — both are shaders attached to draw calls.


Source Files

FileRole
third_party/blink/renderer/core/svg/svg_pattern_element.hDOM element class
third_party/blink/renderer/core/svg/svg_pattern_element.ccAttribute collection, href chain resolution, layout object creation
third_party/blink/renderer/core/svg/pattern_attributes.hResolved attribute value object
third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.hLayout resource declaration, per-client cache map
third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.ccCore: BuildPatternData(), AsPaintRecord(), ApplyShader()
third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.hAbstract base for paint servers (pattern + gradient)
third_party/blink/renderer/platform/graphics/pattern.hPlatform Pattern abstraction wrapping PaintShader
third_party/blink/renderer/platform/graphics/pattern.ccApplyToFlags() — sets shader on cc::PaintFlags
third_party/blink/renderer/platform/graphics/paint_record_pattern.hConcrete Pattern subclass for record-based tiling
third_party/blink/renderer/platform/graphics/paint_record_pattern.ccCreateShader()PaintShader::MakePaintRecord with kRepeat
third_party/blink/renderer/core/paint/svg_object_painter.ccPaint resolution, PaintResourceSubtree() for pattern children
cc/paint/paint_shader.hCompositor-level shader, MakePaintRecord() entry point