The display list
The display list is Paged's intermediate representation — a flat, versioned stream of DisplayCommands that the compose stage emits and a rasterizer consumes, decoupling what to paint from how it gets painted.
The display list is the renderer's intermediate representation: a flat, self-contained command stream that says what to paint, with no idea how it will be painted.
In short: Between laying out a page and turning it into pixels, the renderer
emits a DisplayList — a PathBuffer of shared path data plus a flat Vec of
DisplayCommands like FillPath, StrokePath, Image, and PushClip. The
compose stage produces it; a rasterizer (WebGPU/Vello or CPU) consumes it. Because
the list is small, versioned, and backend-agnostic, the same one drives a
print-quality CPU render and a live WebGPU preview, and it doubles as a stable
hand-off format for tooling. This page is the reference for what's in it and why it
exists.
Why an intermediate representation exists
It would be possible to walk the laid-out scene and call a graphics API directly — fill here, stroke there. The renderer deliberately doesn't. Instead, compose emits a display list, and only then does a backend turn that list into pixels. That extra hop buys three things:
- One scene, many backends. The list is the seam between what to paint and how. The WebGPU/Vello backend and the CPU (tiny-skia) backend consume the exact same list, so the live preview and the CI fidelity render agree by construction. See rendering backends.
- Cheap re-painting. A built page caches its display list. The interactive canvas can re-rasterize a page at a new zoom, or snapshot it at low resolution for a page navigator, without re-running parse, model, layout, and compose.
- A stable hand-off for tooling. The list is versioned precisely so it can be treated as a durable intermediate representation, not just an internal buffer.
The shape of a DisplayList
A DisplayList is small and flat. The commands carry indices into side tables
rather than embedding heavy data inline, which keeps each command Copy-cheap and
lets repeated shapes share storage.
| Attribute · DisplayList | Type / values | Support | Notes |
|---|---|---|---|
| paths | PathBuffer | Supported | All path geometry, deduplicated. Commands reference paths by PathId; glyph outlines are interned once per (font_id, glyph_id) and reused across the page. |
| commands | Vec<DisplayCommand> | Supported | The ordered draw stream. The rasterizer walks it front-to-back; paint order is list order. |
| gradients | Vec<LinearGradient> | Supported | Linear-gradient pool; a Paint::LinearGradient holds a GradientId into it. |
| radial_gradients | Vec<RadialGradient> | Supported | Radial-gradient pool; Paint::RadialGradient indexes here. |
| images | Vec<DecodedImage> | Supported | Decoded RGBA8 pixels, one entry per distinct source URI; a DisplayCommand::Image holds an ImageId into it. |
| spot_inks | Vec<SpotInk> | Supported | Named spot inks (e.g. a PANTONE swatch), interned by name so spot-on-same-spot overprints can be detected at raster time. |
Geometry is interned, not inlined
The PathBuffer is the reason a page full of text doesn't bloat. It hands out a
PathId per stored shape and dedupes via a cache key: the letter "e" in a given font
and size is tessellated to a PathData once, interned under its GlyphCacheKey, and
every later "e" reuses the same PathId. Coordinates live in a path-local space; a
command's Transform places that path on the page.
PathSegment = MoveTo | LineTo | QuadTo | CubicTo | Close
PathData = { segments: Vec<PathSegment> }
PathId = opaque index into a DisplayList's PathBuffer (not stable across lists)
Transform = 2×3 affine [a b c d tx ty] → x' = a·x + c·y + tx, y' = b·x + d·y + tyClips and fills both use the non-zero fill rule, matching IDML's path-geometry convention.
Colour is linear, with CMYK preserved
A Paint is Copy and one of four kinds:
| Attribute · Paint | Type / values | Support | Notes |
|---|---|---|---|
| Solid(Color) | linear-RGB rgba | Supported | The common case. All compositing happens in linear light; sRGB gamma conversion is the backend's job. |
| LinearGradient(GradientId) | index into gradients | Supported | Resolved against DisplayList::gradients. |
| RadialGradient(GradientId) | index into radial_gradients | Supported | Same id space, resolved against DisplayList::radial_gradients. |
| Cmyk { c,m,y,k, rgb, spot } | native CMYK + cached RGB | Supported | Native CMYK channels preserved end-to-end for overprint; rgb is the pre-resolved display colour so ordinary draws paint identically to a Solid. spot=Some(id) routes a named ink to its own plane. |
The commands
A DisplayCommand is one drawing instruction. Every variant carries a Transform
that maps its path (or image) from local space onto the page. The core set:
| Attribute · DisplayCommand (paint) | Type / values | Support | Notes |
|---|---|---|---|
| FillPath | { path_id, paint, transform } | Supported | Fill a path. The single most common command — every glyph and frame fill is one. |
| StrokePath | { path_id, paint, stroke, transform } | Supported | Stroke a path; stroke width is in pt in document space (not a scaled derivation of the points). |
| Image | { image_id, transform } | Supported | Place a decoded RGBA8 image; the unit pixel grid maps to the page via transform. |
| FillPathBlend | { path_id, paint, transform, blend_mode } | Supported | A fill composited with a non-Normal blend mode. Normal fills stay on the fast FillPath path. |
| FillPathOverprint / StrokePathOverprint | { path_id, paint, [stroke,] transform } | Parsed, not yet rendered | CMYK overprint composited as a per-channel darken — a visually-correct RGB approximation for the common dark-on-light cases, not yet a true per-channel CMYK composite. |
State and grouping commands bracket ranges of draws:
| Attribute · DisplayCommand (state / grouping) | Type / values | Support | Notes |
|---|---|---|---|
| PushClip / PopClip | { path_id, transform } / ( ) | Supported | Mask subsequent draws to the intersection of pushed clips (non-zero, anti-aliased). Mismatched pops drop to the base state. |
| PushLayer / PopLayer | { bounds, effect, blend_mode, opacity } / ( ) | Supported | Render contained draws into an offscreen buffer, apply the layer effect, then composite back. The structural successor to BeginBlendGroup for effect-driven layers. |
| BeginBlendGroup / EndBlendGroup | { bounds, blend_mode, opacity } / ( ) | Supported | Transparency-group semantics: blend / partial opacity applied at the group composite, not per fill. Kept for the per-frame backdrop-bypass path. |
Effect stamps render decorations relative to a path. By convention, behind-the-shape stamps (shadows, outer glow) are emitted before the fill; on-top stamps (inner shadow / glow, bevel, satin) after:
| Attribute · DisplayCommand (effects) | Type / values | Support | Notes |
|---|---|---|---|
| DropShadow / PathShadow | { path_id, transform, shadow } | Supported | Offset, blurred shadow behind a shape. PathShadow is the glyph/arbitrary-path flavour; DropShadow the rect-stamp flavour. |
| InnerShadow / InnerGlow | { path_id, transform, params } | Supported | Soft stamp painted on the inside of the path, composited on top of the fill. |
| OuterGlow | { path_id, transform, params } | Supported | Soft halo carved against the path exterior, behind the fill. |
| BevelEmboss | { path_id, transform, params } | Supported | Height-map-derived highlight/shadow tints on the path interior. Full fidelity on the CPU backend; stubbed on Vello (see backends). |
| Satin | { path_id, transform, params } | Supported | Two offset blurred stamps subtracted to a wave mask, tinted and composited on the interior. |
| Feather / DirectionalFeather / GradientFeather | { path_id, transform, params } | Supported | Soft alpha falloff replacing the path's hard edge — uniform, per-edge, or gradient-driven. Emitted in place of the fill. |
How the list is built — and read back
The compose stage appends commands as it walks the scene; helper emitters
(emit_rect, emit_paragraph, emit_image_at, …) push the right command plus any
interned path. Order matters: the rasterizer paints commands in list order, so paint
order is list order.
A few accessors make the list usable as data, not just as a draw buffer.
DisplayCommand::transform_mut returns the placement transform of any variant, which
post-emit passes (such as vertical justification) use to translate a whole range of
commands without inspecting each variant. The pool tables each have an indexed getter
(gradient, image, spot_ink) so a consumer can resolve an id back to its data.
Frequently asked questions
Is the display list the same as a PDF or an SVG? No, though it sits at a similar level. It's an in-memory, Rust-native command stream purpose-built for this renderer's compose-to-raster seam: linear-RGB colour, interned glyph paths, native-CMYK paints, and effect stamps that map onto our rasterizers. It is not a file format you'd hand to another tool, though it's versioned so it could serve as a stable hand-off.
Why are PathIds described as "not stable across lists"?
Because a PathId is just an index into one DisplayList's PathBuffer. Two
different pages intern their own glyph outlines starting from zero, so the same
PathId value means different shapes in different lists. Always resolve a PathId
against the list that produced it.
Why is colour stored in linear RGB? Because compositing — blending, blurring, gradient interpolation — is only correct in linear light. The display list keeps everything linear and leaves the final linear-to-sRGB gamma conversion to the rasterizer, so both backends converge on the same result.
Do both backends support every command?
Almost. The WebGPU/Vello backend covers the common set — fills, strokes, images,
clips, blend groups — and approximates the soft effects. A couple of commands
(BevelEmboss, and true per-channel CMYK overprint) are only fully realized on the
CPU backend today. The rendering-backends page
has the honest matrix.
The pipeline
How Paged's renderer turns an IDML package into pixels — a five-stage relay across focused Rust crates parse, model, layout, compose, and rasterize, each owning one job and handing its output forward.
Rendering backends
Paged has two rasterizers behind one trait — a WebGPU backend on Vello that powers the SDK and live preview, and a CPU backend on tiny-skia kept as the deterministic fidelity yardstick for headless CI.