Skia CPU Backend Architecture

This document outlines the major pieces of Skia's CPU backend. The CPU backend is responsible for rendering graphics on the CPU, without the use of a GPU.

Background and Definitions

  • Bitmap: A grid of pixels representing an image, where each pixel has a color. This is the digital canvas Skia's CPU backend paints on. Common image file formats like PNG and JPEG store bitmap data.
  • Blend Mode: A rule that defines how a source color (the one being drawn) is mathematically combined with a destination color (the one already on the canvas).
  • Blitter: A component that writes the final pixel colors from the rendering pipeline into the destination bitmap. It's the “hand” that actually puts the colored pixels onto the canvas.
  • Clip: A region that restricts drawing. Anything drawn outside the clip is not visible.
  • Coverage Mask: An array of values (typically 8-bit alpha) that represents how much a geometric shape covers each pixel. A value of 255 means fully covered, 0 means not covered, and intermediate values represent partial coverage for anti-aliasing.
  • Hairline: The thinnest visual line, typically one device pixel wide. A stroke width of 0 in a paint is interpreted as a hairline. Hairlines do not scale with the CTM.
  • Matrix: A mathematical tool used to transform (translate, scale, rotate, skew) what is being drawn.
  • Paint: An object holding stylistic information for drawing, like color, stroke width, and effects.
  • Path: A sequence of lines and curves that describe a shape. Skia uses quadratic, cubic, and conic Bézier curves to create smooth, scalable curves. A quadratic curve has one control point, while a cubic curve has two for more complex shapes. A conic curve represents perfect circles etc using one control point and one weight.
  • Rasterization: Converting vector graphics (math-based shapes) into a grid of pixels (a raster image) for display.
  • Scanline: A single horizontal row of pixels. Complex shapes are often rendered one scanline at a time.
  • Shader: A program that calculates the color of a pixel. Shaders can produce solid colors, gradients, textures, or procedural patterns.
  • Tiling: An optimization strategy where a large drawing operation is broken into a series of smaller, tile-sized operations to keep memory usage (e.g., for intermediate buffers) bounded.
  • Winding Number: An algorithm used to determine if a point is inside a complex or self-intersecting path. Imagine drawing a ray from the point in any fixed direction to infinity. The winding number is the count of how many times the path crosses the ray in one direction (e.g., clockwise) minus the number of times it crosses in the other direction (counter-clockwise). For a non-zero winding rule, any point with a non-zero winding number is considered “inside” the path. This correctly handles shapes with holes and overlapping sections.

Data Flow Walkthrough

The following sections describe the journey of a single drawing command (e.g., canvas.drawPath(...)) through the CPU backend, from the public API call to modifying raw pixel memory.

See CPU.dot for a supplementary diagram.

1. The API Layer

The journey begins with the user-facing drawing API.

  • SkSurface: Represents the drawing destination. A CPU-backed surface, created via SkSurfaces::Raster(...), allocates and owns pixel memory via an SkPixelRef. The SkSurface manages an SkBitmap using this memory and provides an SkCanvas for drawing.
  • SkImage: Represents an immutable snapshot of pixel data. It can be created from an SkSurface (via surface->makeImageSnapshot()), from an SkBitmap, or from encoded data (e.g., a PNG file). On the CPU backend, this is implemented by SkImage_Raster, which holds an SkBitmap that in turn points to the underlying pixels via an SkPixelRef. Because it is immutable, it can be cached and shared safely across threads. It can be drawn to a canvas via canvas->drawImage(...).
  • SkCanvas: The primary drawing interface with methods like drawPath, drawImage, etc. The user provides geometry (e.g., SkPath), images (SkImage), styling (SkPaint), and font information (SkFont) to the canvas. The SkCanvas holds the current transformation matrix and clip. When a draw call is made, the SkCanvas forwards all the information to an internal SkDevice for rendering.
  • SkPath: A high-level object representing vector geometry (lines, curves). It is a lightweight, copy-on-write object that holds a shared pointer (sk_sp) to an SkPathRef.
  • SkPathRef: The reference-counted object that actually stores the raw path data: arrays of points, verbs (move, line, quad, conic, cubic, close), and conic weights. SkPathRef objects are always heap-allocated and managed by sk_sp. Internally, SkPathRef uses skia_private::STArray for its geometry data storage. This STArray is a small-object optimized array, meaning that for small paths, the geometry data is stored directly within the STArray's internal buffer (part of the heap-allocated SkPathRef object). For larger paths, the STArray dynamically allocates additional memory on the heap to store the geometry data.

Text Rendering

Drawing text is a specialized, but common, case. The process involves three key classes:

  • SkFontMgr: A top-level object that discovers and manages the fonts installed on the system. It is used to create an SkTypeface by matching a font family name (e.g., “Roboto”) or by loading font data directly from a file or stream. It also handles “fallback”, which allows a user to find the data to draw a glyph from a collection of fonts and ordering heuristics (e.g. use this font for Latin characters and this font for emoji).
  • SkTypeface: An immutable object representing the raw data of a single font face (e.g., “Roboto Bold”). It typically contains the vector paths for each glyph, but also supports bitmap glyphs. Vector glyphs are rasterized and stored in a glyph cache (SkStrikeCache).
  • SkFont: A lightweight object that holds a reference to an SkTypeface plus styling attributes like text size, scale, and skew.

When a user calls a method like canvas.drawSimpleText(...), they provide the text and an SkFont to the SkCanvas. The backend then uses the SkFont to look up the individual glyphs from the SkTypeface. Each glyph is subsequently treated as a standard SkPath to be rendered.

Note on Text Layout: Core Skia handles rendering individual glyphs, but it does not perform complex text layout (e.g., line breaking, justification, or bidirectional text). For rich text layout, Skia provides the SkParagraph module, which is a higher-level library built on top of Skia's core components.

Paint Configuration

The SkPaint object holds a suite of optional components that control the styling and pixel-processing pipeline.

  • SkShader: Generates the source color for the geometry. If no shader is present, the paint's color is used. Shaders can produce gradients, bitmap patterns, or procedural colors.
  • SkColorFilter: Modifies the source color produced by the shader or paint. Common uses include tinting or applying a color matrix for color correction.
  • SkMaskFilter: Operates on the shape‘s coverage mask, not its color. Its most common use is blurring the shape’s edges, such as with a Gaussian blur from SkMaskFilter::MakeBlur.
  • SkPathEffect: Modifies the geometry of a shape before it is drawn. For example, SkPathEffect::MakeDash creates an effect that turns solid lines into dashed lines. This happens before rasterization.
  • SkBlender: Controls how the source color (from the shader) is blended with the destination color (already on the canvas). It is a more powerful, programmable version of the traditional SkBlendMode enum. The blending logic is executed as a stage in the SkRasterPipeline.
  • SkImageFilter: Applies a complex, multi-pass effect to the output of a drawing operation. Unlike other effects, an image filter can operate on the entire result of a draw as a texture. This often requires allocating a temporary, offscreen layer. For example, a drop shadow filter might draw the shape into a layer, blur it, offset it, and then draw the original shape again on top of the blurred shadow.

2. The Device Layer: SkBitmapDevice

For the CPU backend, the SkCanvas forwards draw calls to an SkBitmapDevice. This object is the concrete target for all CPU-based drawing. It manages the destination SkBitmap and initiates rendering by creating and dispatching to an SkDraw object.

A Note on Clipping: The device manages an SkRasterClipStack corresponding to the canvas's save()/restore() calls. For each draw, it resolves this stack into a single SkRasterClip and passes it to SkDraw, which is stateless regarding the clip.

A Note on Tiling: For large or complex draws, the device may use tiling. It breaks the operation into smaller, tile-sized chunks, adjusting the clip for each. This bounds the memory required for intermediate buffers by invoking the draw process once per tile.

3. The Orchestrator: SkDraw

The SkBitmapDevice creates an SkDraw object for each primitive. SkDraw orchestrates a single draw, bundling the geometry (SkPath), styling (SkPaint), transform, clip, and destination SkPixmap. It uses SkScan to convert the vector shape into raster operations.

4. The Rasterizer: SkScan

SkDraw passes the SkPath to SkScan, the core rasterizer. SkScan converts the vector path into horizontal scanlines, calculating a coverage mask (alpha values) for each. It is unaware of color or effects. SkDraw provides SkScan with a SkBlitter, which SkScan invokes for each scanline to render the coverage data.

A Note on Scan Converters: The isAntiAlias() flag on the SkPaint selects the scan converter:

  • Aliased (SkScan_Path.cpp): When anti-aliasing (AA) is off, it produces a binary (0% or 100%) coverage mask based on whether pixel centers are inside the path, resulting in hard edges. For each horizontal run of covered pixels, it calls blitter->blitH(x, y, width).
  • Analytic Anti-Aliased (SkScan_AAAPath.cpp): When AA is on, it analytically calculates the exact geometric area of intersection with each pixel, producing fractional coverage values for smooth edges. For each horizontal run of partially-covered pixels, it calls blitter->blitAntiH(x, y, alphas, runs).

5. The Pixel Writer: SkRasterPipelineBlitter

The SkRasterPipelineBlitter is the primary SkBlitter in the CPU backend. It implements the SkBlitter interface, but instead of writing pixels directly, it translates calls like blitH or blitRect from SkScan into an execution of its internal SkRasterPipeline. It configures the pipeline with the correct coverage information from the blitter call and then runs the pipeline to compute and write the final pixel colors to those lines.

6. The Workhorse: SkRasterPipeline

The SkRasterPipeline calculates final pixel colors by chaining together single-purpose stages. For example, a draw might use stages for loading coverage (load_a8), setting a source color (uniform_color), loading the destination, blending (srcover), and storing the result (store_8888). This stage-based design avoids a combinatorial explosion of functions.

The pipeline is assembled into a sequence of pre-compiled “stages”, which are executed in a loop over the covered area. These functions use SIMD (e.g., SSE, NEON) to process multiple pixels at once for high performance. The pipeline gets memory layout information from an SkPixmap to load and store pixels.

7. The Pixel Memory View: SkPixmap, SkBitmap, and SkPixelRef

This is the final stop where pixels are modified.

  • SkPixmap: A lightweight object with a pointer to pixel memory and its metadata (dimensions, color type). It provides the SkRasterPipeline with the exact memory addresses for reading and writing.
  • SkBitmap: Held by SkBitmapDevice, it pairs an SkImageInfo with the pixel storage.
  • SkPixelRef: A smart pointer that owns the heap-allocated pixel memory, originally created by the SkSurface.

The SkRasterPipeline's final stage uses the address from the SkPixmap to write the new color into the memory owned by the SkPixelRef, completing the draw.