
# Rendering

## Overview

Rendering takes the `DisplayList` produced by [Layout](layout.md) and turns
it into pixels. *WIP — APIs in `compositor2` and `gpu` are still moving;
the shape documented here matches HEAD, but signatures may shift before 1.0.*

A frame moves through three boundaries. The layout engine emits an
`azul_layout::solver3::display_list::DisplayList` — a flat
`Vec<DisplayListItem>` in absolute window coordinates.
`compositor2::translate_displaylist_to_wr` walks that list and pushes
equivalent items into a WebRender `DisplayListBuilder`. The
[WebRender bridge](rendering/webrender-bridge.md) wraps the resulting
`BuiltDisplayList` in a `Transaction` and ships it to the WebRender backend
thread. Hardware (GPU WebRender) and software (CPU `cpurender`) paths share
the exact same display list. The split is in the sink, not the source.

This page documents the display-list-to-WebRender translation. The
sub-pages cover the rest of the rendering layer:

- [WebRender Bridge](rendering/webrender-bridge.md) — transaction shape,
  pipelines/IFrames, resource registration, hit-test bridge.
- [GL Loading](rendering/gl-loading.md) — per-platform symbol resolution,
  `GenericGlContext`, library shaders.
- [Image Pipeline](rendering/image-pipeline.md) — raster decode, the four
  texture caches, ExternalImageId, shader disk cache.
- [Text Pipeline](rendering/text-pipeline.md) — font discovery, parsing,
  fallback chain resolution, the `font_dirty_nodes` invalidation set.

## Stage map

A frame moves through these stages in order:

- **Layout → display list.** Produces a `DisplayList` (`Vec<DisplayListItem>`)
  via `LayoutWindow::layout_and_generate_display_list`.
- **GPU key sync.** Walks the `StyledDom` and emits a `GpuEventChanges`
  describing transform/opacity deltas, in `GpuValueCache::synchronize`.
- **Display-list translation.** Walks the display list and pushes WebRender
  items, producing a `WrBuiltDisplayList`, nested pipelines, and resource
  updates. Implemented by `compositor2::translate_displaylist_to_wr`.
- **Resource translation.** Produces a `Vec<WrResourceUpdate>` in
  `wr_translate2.rs`.
- **Frame submission.** Wraps everything in a `Transaction` and ships it to
  the WebRender backend thread, in `generate_frame`.
- **GL function loading.** Resolves GL symbols per platform into a
  `GenericGlContext`, in `gl_loader::load_gl_context`.
- **Library shaders.** Compiles the SVG, multicolor-SVG, and FXAA programs
  once at context creation, in `GlContextPtr::new`.

`LayoutWindow` owns the layout side; `dll/src/desktop/window.rs` owns the
WebRender side and the GL context.

## translate_displaylist_to_wr

```rust,ignore
pub fn translate_displaylist_to_wr(
    display_list: &DisplayList,
    pipeline_id: PipelineId,
    viewport_size: DeviceIntSize,
    renderer_resources: &azul_core::resources::RendererResources,
    dpi: DpiScaleFactor,
    wr_resources: Vec<WrResourceUpdate>,
    layout_results: &BTreeMap<DomId, DomLayoutResult>,
    document_id: u32,
) -> Result<
    (Vec<WrResourceUpdate>, WrBuiltDisplayList, Vec<(PipelineId, WrBuiltDisplayList)>),
    String,
>
```

The function is a single sequential walk. There is no intermediate IR; each
`DisplayListItem` translates directly into one or more `builder.push_*`
calls. Three stacks track the WebRender-side context:

- `clip_stack: Vec<WrClipChainId>` — the current clip chain (rounded-rect,
  image mask, scroll-frame viewport).
- `spatial_stack: Vec<SpatialId>` — the current spatial node (root scroll
  node, scroll frame, transform reference frame).
- `offset_stack: Vec<(f32, f32)>` — the coordinate origin offset. The
  display list uses absolute window coordinates; WebRender wants
  frame-relative coordinates for stacking contexts. Push when entering a
  `PushStackingContext`, subtract on every coordinate translation, pop on
  `PopStackingContext`.

The call sites are in `wr_translate2.rs`. Each builds a transaction
containing one or more display lists.

### resolve_rect — the coordinate gate

Every coordinate that crosses into WebRender goes through `resolve_rect`:

```rust,ignore
fn resolve_rect(
    bounds: &azul_layout::solver3::display_list::WindowLogicalRect,
    dpi: f32,
    offset: (f32, f32),
) -> LayoutRect {
    let raw = scale_bounds_to_layout_rect(bounds.inner(), dpi);
    LayoutRect::from_origin_and_size(
        LayoutPoint::new(raw.min.x - offset.0, raw.min.y - offset.1),
        LayoutSize::new(raw.width(), raw.height()),
    )
}
```

The two adjustments (DPI scale and stacking-context offset subtraction) are
fused so callers cannot forget one. `resolve_point` is the equivalent for
single points (used for `push_simple_stacking_context`).

### Item dispatch

The matcher in `compositor2.rs` routes each `DisplayListItem`. Common
patterns:

- **`Rect`, `SelectionRect`, `CursorRect`.** `resolve_rect`,
  `CommonItemProperties`, then `builder.push_rect`. Optional rounded-corner
  `define_border_radius_clip`.
- **`Border`.** Resolve rect, call `get_webrender_border` for per-side
  widths, call `builder.push_border`.
- **`ScrollBarStyled`.** Push an optional opacity stacking context bound to
  `opacity_key`, define a rounded clip if the container has border-radius,
  render the track, optional buttons, then the thumb. The thumb may be
  wrapped in its own reference frame for GPU-driven `transform`.
- **`PushClip` / `PopClip`.** `define_clip_rect` (or
  `define_clip_rounded_rect`), then `define_clip_chain(parent)`, then push
  to `clip_stack`.
- **`PushScrollFrame` / `PopScrollFrame`.** See
  [scroll-frame clipping](#scroll-frame-clipping).
- **`Text`.** Look up `FontInstanceKey`, scale glyph positions by DPI,
  subtract scroll offset, call `builder.push_text`.
- **`Image`.** Resolve `ImageRefHash` to `WrImageKey`, apply an optional
  rounded clip, call `builder.push_image`.
- **`LinearGradient` / `RadialGradient` / `ConicGradient`.** Compute
  start/end points from the CSS direction, convert stops, call `push_stops`
  immediately followed by `push_gradient` / `push_radial_gradient` /
  `push_conic_gradient`. No clip items can sit between the two; WebRender
  requires them adjacent.
- **`BoxShadow`.** Convert blur, offset, and spread, then call
  `push_box_shadow` with the appropriate `BoxShadowClipMode`.
- **`PushStackingContext` / `PopStackingContext`.**
  `push_simple_stacking_context` at the resolved origin. The offset stack
  tracks the CSS origin for nested children.
- **`PushReferenceFrame` / `PopReferenceFrame`.** GPU transform animation.
  Translation components are scaled by DPI; the spatial node binds to
  `transform_key`. Pushes spatial only, no offset.
- **`VirtualView`.** Recursively call `translate_displaylist_to_wr` for the
  child DOM, accumulate its built list under `nested_pipelines`, then
  `builder.push_iframe` to splice it in.
- **`PushImageMaskClip` / `PopImageMaskClip`.** `define_clip_image_mask` for
  SVG mask-style clipping.
- **`HitTestArea`.** `builder.push_hit_test` with the supplied `ItemTag`.
  Under `debug_assertions` the area also renders as a 30%-opaque red rect.

`TextLayout` items are no-ops here. An earlier pass consumed them and
emitted the resolved `Text` items.

### Scroll-frame clipping

A `ScrollFrame` in WebRender is *only* a transformation node. It does not
clip. The viewport clip must be defined separately, in **parent space**, so
it stays stationary while content scrolls. The `PushScrollFrame` arm
performs four steps:

```rust,ignore
// 1. Define the spatial node (transformation only)
let scroll_spatial_id = builder.define_scroll_frame(
    parent_space,
    external_scroll_id,
    content_rect,        // total scrollable size, origin = frame_rect.origin
    adjusted_frame_rect, // visible viewport, in parent space
    LayoutVector2D::zero(),
    0,
    HasScrollLinkedEffect::No,
    SpatialTreeItemKey::new(*scroll_id, 0),
);
spatial_stack.push(scroll_spatial_id);

// 2. Define the viewport clip in PARENT space
let scroll_clip_id = builder.define_clip_rect(parent_space, adjusted_frame_rect);

// 3. Chain it onto the current clip
let scroll_clip_chain = builder.define_clip_chain(parent_clip, [scroll_clip_id]);
clip_stack.push(scroll_clip_chain);

// 4. Push a scroll-container hit-test in parent space (TAG_TYPE_SCROLL_CONTAINER)
builder.push_hit_test(adjusted_frame_rect, scroll_clip_chain, parent_space, ..., scroll_container_tag);
```

Three things will silently break clipping if you get them wrong:

- **Clip in scroll space.** `define_clip_rect(scroll_spatial_id, ...)` makes
  the clip scroll *with* the content — content disappears off-screen. The
  clip must be in `parent_space`.
- **Content origin ≠ frame origin.** If `content_rect.origin` is
  `LayoutPoint::zero()` while `frame_rect.origin` is non-zero, the content
  is shifted by exactly that delta and items render outside the viewport.
- **Forgetting to push the clip chain.**
  `clip_stack.push(scroll_clip_chain)` must run before any inner item is
  processed; otherwise items inherit the parent clip and bleed past the
  viewport.

`PopScrollFrame` pops both the spatial and clip stacks and returns
`Err("Scroll frame stack underflow")` if either underflows. The caller
(`generate_frame`) treats that as a fatal frame error.

### Coordinate offset, stacking context, scroll frame

Three coordinate concepts collide in the compositor. Read carefully:

- The display list emits **absolute window** coordinates for everything. A
  `Text` item's clip rect at `(11, 53)` means *the window position*, not
  the position inside its parent.
- A **scroll frame** in WebRender shares its parent's coordinate space. It
  does *not* create a new origin. `offset_stack` is **not** pushed on
  `PushScrollFrame`. The scroll-frame transform handles scroll movement;
  absolute coordinates stay absolute.
- A **stacking context** (`PushStackingContext`) *does* shift the origin —
  WebRender offsets every child by the stacking context's origin. The
  compositor pushes the scaled origin onto `offset_stack` so that children
  subtract it back out (yielding net zero offset, with the visible effect
  being paint order and z-index). The same applies to
  `PushReferenceFrame`, but for stacking-context-style filters (opacity,
  transform).

If a scroll-container clip suddenly stops working, the first thing to check
is that no intervening `PushStackingContext` was added that pushed onto the
offset stack but failed to pop. Run with `LogCategory::DisplayList` enabled
to see `clip_stack.len` and `spatial_stack.len` on every push/pop.

## GPU value cache

`GpuValueCache::synchronize` decouples the layout pass from per-frame GPU
updates. It walks the `StyledDom` once per frame, comparing each node's
current CSS `transform` and `opacity` against the previously-stored values,
and emits a `GpuEventChanges` describing the deltas:

```rust,ignore
pub enum GpuTransformKeyEvent {
    Added(NodeId, TransformKey, ComputedTransform3D),
    Changed(NodeId, TransformKey, ComputedTransform3D, ComputedTransform3D),
    Removed(NodeId, TransformKey),
}

pub enum GpuOpacityKeyEvent {
    Added(NodeId, OpacityKey, f32),
    Changed(NodeId, OpacityKey, f32, f32),
    Removed(NodeId, OpacityKey),
}
```

Two compact-cache fast paths short-circuit the cascade walk:

- `cc.has_transform(node_id.index())` is a single bit — if unset and the
  cache has no prior key, skip the node entirely.
- `cc.get_opacity_raw(node_id.index())` returns a `u8`. The sentinel
  `OPACITY_SENTINEL` means "unset", which decodes to `1.0` and lets the
  loop bail out without invoking `css_property_cache.get_opacity`.

The result feeds two consumers:

- `Added` events generate fresh `TransformKey` / `OpacityKey` values via
  `unique()`, which wrap WebRender `PropertyBindingKey`s.
- `Changed` events go to `Transaction::append_dynamic_properties`, which
  WebRender applies *without* rebuilding the display list. This is the path
  that makes scrollbar fade-out and CSS `transform` animation cheap — a
  scroll doesn't trigger a re-layout, just a property update.

`GpuScrollbarOpacityEvent` is the parallel for scrollbar fade timers; it is
maintained by the scroll manager rather than `synchronize`. The cache is
owned per-window and survives across frames; only the `_changes` struct is
consumed each frame.

## Library shaders

`GlContextPtr::new` compiles three shader programs at context creation and
stores them in `GlContextPtrInner`:

- **`svg_shader`.** Solid-color SVG path fills, defined in `core/src/gl.rs`
  (`SVG_VERTEX_SHADER`, `SVG_FRAGMENT_SHADER`).
- **`svg_multicolor_shader`.** Per-vertex coloured SVG.
- **`fxaa_shader`.** Post-process anti-aliasing, defined in
  `core/src/gl_fxaa.rs`.

Compilation is checked through `check_shader_compile` and
`check_program_link`. Both log to stderr under `feature = "std"`; under
`no_std` they swallow the failure. There is no recovery. A failed shader
compile leaves the program ID present but unusable, and subsequent draws
produce no output. If you hit a black SVG, run with `RUST_LOG` and look for
"shader compile error". `GlContextPtrInner::Drop` calls `delete_program` on
all three when the context is destroyed.

### FXAA pass

`FxaaConfig` carries the runtime tunables:

```rust,ignore
pub struct FxaaConfig {
    pub enabled: bool,
    pub edge_threshold: f32,      // 0.063 – 0.333, default 0.125
    pub edge_threshold_min: f32,  // 0.0312 – 0.0833, default 0.0312
}
```

Presets (`enabled`, `high_quality`, `balanced`, `performance`) are
convenience constructors. The fragment shader is the standard NVIDIA FXAA
3.11 algorithm: sample center + N/S/E/W luminance, compute `lumRange`,
early-exit below the threshold, otherwise sample along the detected edge
direction and blend.

The actual FXAA render pass lives in `layout/src/xml/svg.rs` (`apply_fxaa` /
`apply_fxaa_with_config`), not in `core/src/gl_fxaa.rs`. The `core` crate
owns the data + GLSL strings; the SVG render pipeline owns the FBO setup,
fullscreen quad, and state save/restore.

## Texture cache

`core/src/gl.rs` declares the active texture map:

```rust,ignore
static mut ACTIVE_GL_TEXTURES:
    Option<OrderedMap<DocumentId, GlTextureStorage>> = None;

pub type GlTextureStorage =
    OrderedMap<Epoch, OrderedMap<ExternalImageId, Texture>>;
```

The keying (`DocumentId → Epoch → ExternalImageId → Texture`) exists
because WebRender may still be rendering against an old frame's textures
while the application is generating the next frame. Textures are kept alive
until the backend thread acknowledges that the epoch is no longer in use,
then `gl_textures_remove_epochs_from_pipeline` drops everything strictly
older than the supplied epoch.

The map is **not** thread-safe. The doc comment argues that `Texture`
itself is not `Send`/`Sync`, so accidental concurrent access is unlikely,
but the warning still applies: do not call any of
`insert_into_active_gl_textures`, `gl_textures_remove_epochs_from_pipeline`,
`gl_textures_remove_active_pipeline`, `gl_textures_clear_opengl_cache`, or
`get_opengl_texture` from anything other than the main thread. Rust 2024
will also forbid `static mut` references; replacement with a `Mutex<...>`
or `thread_local!` is on the cleanup list.

`Texture` carries a refcount, the `GLuint` ID, and a clone of the
`GlContextPtr`. Its `Drop` decrements the refcount and, on last drop, calls
`delete_textures` on the held context. `Texture::create`,
`Texture::allocate_rgba8`, and `Texture::clear` cover the construction
surface. `GlShader::draw` is the single render entry point that reads from
the cache.

## Hardware vs software path

`RendererType` (in `core/src/window.rs`) selects between:

- **Hardware (GPU)** — WebRender renders into a real OpenGL context. The
  context is created by the platform shell (see [GL Loading](rendering/gl-loading.md)),
  the WebRender renderer is built on top of `GenericGlContext`, and frames
  go through the standard transaction pipeline.
- **Software (CPU)** — `cpurender` rasterises into a `Vec<u8>`
  framebuffer. The display list is the same; the sink is different. Used
  for headless rendering, the reftest harness, and machines without GL.

The split happens at `dll/src/desktop/window.rs` when the renderer is
constructed; everything upstream of that (layout, display list, GPU value
cache, [WebRender bridge](rendering/webrender-bridge.md)) is identical.

## Debugging clip and spatial bugs

The most common WebRender visual bug is "items render in the wrong place".
The compositor logs every push/pop to `LogCategory::DisplayList`. Useful
patterns:

- `[CLIP DEBUG] Rect: adjusted=..., clip_chain=..., spatial=...` — every
  Rect prints its resolved bounds and stack heads. Cross-reference against
  the intended clip chain.
- `[compositor2] PushScrollFrame START ... DONE` brackets the scroll
  frame. The `clip_stack.len` and `spatial_stack.len` deltas should both be
  `+1`. If one of them stays the same, an arm above failed to push.
- `[compositor2] PopClip: SKIPPED (clip_stack.len=1, would underflow)` —
  unbalanced `PushClip` / `PopClip`. Means the display list generator is
  buggy, not the compositor.

WebRender's own debug flags are also useful: enable
`DebugFlags::PRIMITIVE_DBG | DebugFlags::CLIP_DBG | DebugFlags::SPATIAL_DBG`
in the renderer config to get on-screen overlays. If clipping is wrong, the
diagnosis is almost always: clip defined in the wrong space, or clip chain
not chained to its parent.

## Coming Up Next

- [WebRender Bridge](rendering/webrender-bridge.md) — transactions, pipelines, IFrames, resource registration
- [GL Loading](rendering/gl-loading.md) — per-platform GL symbol resolution
- [Image Pipeline](rendering/image-pipeline.md) — raster decode, texture caches, ExternalImageId
- [Text Pipeline](rendering/text-pipeline.md) — font discovery, parsing, fallback chains
- [Layout](layout.md) — solver3, formatting contexts, the per-frame relayout cycle
