
# WebRender Bridge

## Overview

The bridge is the boundary between azul's native types and WebRender's API
types. *WIP — `wr_translate2.rs` is mid-rework; the `compositor2` ↔
WebRender seam is stable, but the resource-update collection helpers may
consolidate.*

Two files own the bridge:

- `compositor2.rs` translates `DisplayListItem` → WebRender display-list
  `push_*` calls (covered in [Rendering](../rendering.md)).
- `wr_translate2.rs` translates everything else: pipeline IDs, image keys,
  font keys, border radii, colors, clip-chain IDs, and the per-frame
  `Transaction` assembly.

`generate_frame` is the single entry point that the platform shells
call once per frame.

## Per-frame transaction shape

```rust,ignore
pub fn generate_frame(
    txn: &mut WrTransaction,
    layout_window: &mut LayoutWindow,
    render_api: &mut WrRenderApi,
    display_list_was_rebuilt: bool,
    gl_context: &azul_core::gl::OptionGlContextPtr,
) {
    // ... bail if minimized
    if display_list_was_rebuilt {
        // 1. Resource updates (fonts + images)
        txn.update_resources(font_resources);
        txn.update_resources(image_resources);

        // 2. Display list per DOM (root + nested IFrames/VirtualViews)
        for (dom_id, layout_result) in &layout_window.layout_results {
            let (_, dl, nested) = compositor2::translate_displaylist_to_wr(...)?;
            txn.set_display_list(epoch, (pipeline_id, dl));
            for (nested_pid, nested_dl) in nested {
                txn.set_display_list(epoch, (nested_pid, nested_dl));
            }
        }
        layout_window.epoch.increment();
    } else {
        txn.skip_scene_builder();
    }

    // 3. Root pipeline + viewport
    txn.set_root_pipeline(root_pipeline_id);
    txn.set_document_view(view_rect, DevicePixelScale::new(hidpi));

    // 4. Image callbacks, virtual-view re-renders
    process_image_callback_updates(layout_window, gl_context, txn);
    process_virtual_view_updates(layout_window, txn);

    // 5. Scroll positions
    scroll_all_nodes(layout_window, txn);

    // 6. GPU dynamic properties (transforms, opacities)
    synchronize_gpu_values(layout_window, txn);

    // 7. Submit
    txn.generate_frame(0, WrRenderReasons::empty());
}
```

The order matters. Resources go in *before* `set_display_list` because the
display list references `FontInstanceKey`s and `ImageKey`s that must
already exist on the backend thread by the time the scene is built. The
root pipeline goes in *after* the display list for the same reason.
WebRender's upstream expects the dependency graph to be populated before
pipeline activation.

`display_list_was_rebuilt` is the flag from the layout layer. `false` means
"only properties changed" and the function calls `txn.skip_scene_builder()`.
WebRender reuses the previous scene and only applies the dynamic properties
from `synchronize_gpu_values` and the scroll offsets from
`scroll_all_nodes`. This is the path that makes scrollbar fade and CSS
animations cheap.

## Pipelines, DOMs, and IFrames

One `WrPipelineId` per `DomId`. `wr_translate_pipeline_id` packs the ID:

```rust,ignore
PipelineId(dom_id.inner as u32, layout_window.document_id.id)
```

The root DOM gets `PipelineId(0, document_id)`. IFrames and `VirtualView`s
get the child DOM's ID. `translate_displaylist_to_wr` returns a flat
`Vec<(PipelineId, WrBuiltDisplayList)>` of nested pipelines. The caller
adds each one to the transaction with `set_display_list`. Pipeline IDs are
pure identifiers. WebRender stitches the trees together via `push_iframe`
items inside the parent display list.

## Resource translation

`collect_font_resource_updates` and `collect_image_resource_updates` read
from `layout_window.renderer_resources` and emit
`azul_core::resources::ResourceUpdate` values. Each is then routed through
`translate_resource_update` to a `webrender::ResourceUpdate`. Azul's
`ResourceUpdate::AddFont(AddFont { font, key })` becomes
`ResourceUpdate::AddFont(WrFontKey, WrFontTemplate, ...)`. Azul's
`ResourceUpdate::AddFontInstance(AddFontInstance { font_key, glyph_size, key })`
becomes `ResourceUpdate::AddFontInstance(WrFontInstanceKey, WrFontKey, ...)`.
Azul's `ResourceUpdate::AddImage(AddImage { key, descriptor, data })`
becomes `ResourceUpdate::AddImage(WrImageKey, WrImageDescriptor, WrImageData)`.
Removals translate symmetrically through the `Delete*` variants.

While translating, `generate_frame` also mirrors the registrations into the
azul-side maps:

- `font_hash_map: HashMap<u64, FontKey>`. `compositor2`'s `push_text` looks
  up `FontKey` by `font_hash`.
- `currently_registered_fonts: HashMap<FontKey, (FontRef, BTreeMap<(Au, DpiScaleFactor), FontInstanceKey>)>`.
  The same path resolves the `FontInstanceKey` by `(size, dpi)`.
- `image_key_map: HashMap<ImageKey, ImageRefHash>`. Reverse lookup for
  hit-testing.

If a `Text` item references a `font_hash` that hasn't made it into the
transaction's resource updates, `push_text` logs a warning and silently
drops the glyphs. The same goes for missing image keys. Both are usually a
sign that the layout pass and the resource collection are out of sync. A
glyph was shaped against a font that wasn't registered, or an image was
loaded but not inserted into `renderer_resources`.

## GPU dynamic properties

`synchronize_gpu_values` is the bridge between `core/src/gpu.rs` and
WebRender's `Transaction::append_dynamic_properties`. It walks
`layout_window.gpu_value_cache` (a `GpuValueCache`) and emits one
`PropertyValue<T>` per active key:

```rust,ignore
let scrollbar_v_props: Vec<PropertyValue<f32>> = ...;       // scrollbar opacity
let scrollbar_h_props: Vec<PropertyValue<f32>> = ...;
let v_transform_props: Vec<PropertyValue<LayoutTransform>> = ...; // scrollbar thumb transform
let h_transform_props: Vec<PropertyValue<LayoutTransform>> = ...;
let css_transform_props: Vec<PropertyValue<LayoutTransform>> = ...;  // CSS transforms
let css_opacity_props: Vec<PropertyValue<f32>> = ...;                 // CSS opacities

txn.append_dynamic_properties(DynamicProperties {
    transforms: ...,
    floats: ...,
    colors: ...,
});
```

The `PropertyBindingKey` ID is the `OpacityKey`/`TransformKey` value — the
same number that was embedded in the display list when the
`PushReferenceFrame` or stacking-context-with-opacity-filter was emitted.
WebRender resolves the binding at frame-build time by replacing the bound
property with the supplied `PropertyValue`. This is what lets a scrollbar
fade-out animation update without rebuilding the display list — the layout
pass produces a new opacity number, `synchronize_gpu_values` packs it into
the transaction, WebRender re-rasterises the affected primitives.

`txn.skip_scene_builder()` is compatible with `append_dynamic_properties`.
The dynamic properties path is *separate* from scene building, so the
cheap "property update only" path stays cheap.

The `DynamicProperties` translation has one DPI-related quirk. Translation
components of bound transforms (`m[3][0]`, `m[3][1]`, `m[3][2]`) must be
scaled by the HiDPI factor, because the display list's coordinates are
already in physical pixels. `compositor2` does this on display-list
emission, and `synchronize_gpu_values` does the equivalent on every
dynamic update.

## Coordinate conventions

Three coordinate spaces meet here. The bridge converts between them per
component, not in one place:

- **Window logical (CSS).** `DisplayList` items use logical pixels (CSS px)
  in absolute window coordinates.
- **Window physical.** The WebRender display list uses physical pixels
  (logical times DPI) in absolute window coordinates.
- **Stacking-context-relative physical.** Inside a stacking context that
  pushed an origin, items use physical pixels minus the stacking context
  origin.

`compositor2`'s `resolve_rect` and `resolve_point` fuse the DPI multiply
and the offset subtract. The *scroll* frame does **not** add to the offset
stack, because WebRender scroll frames share their parent's coordinate
space. Only stacking contexts and reference frames push offsets. This is
documented in detail at [Rendering](../rendering.md).

Scroll positions: `scroll_all_nodes` reads the scroll manager's state
(logical CSS pixels), multiplies by `hidpi_factor`, and submits via
`txn.set_scroll_offsets`. The scroll offset and the display list both end
up in physical pixels, so the addition in WebRender's spatial tree
resolves correctly.

## Hit-test bridge

The forward path is straightforward: `compositor2` emits a `HitTestArea`
item that calls `builder.push_hit_test(rect, clip_chain, spatial_id, flags, tag)`
with an `ItemTag = (u64, u16)`. The `u16` namespace marker distinguishes:

- `0x0100`. DOM node hit.
- `0x0200`. Scrollbar component hit, decoded by
  `wr_translate_scrollbar_hit_id`.
- `0x0500`. `TAG_TYPE_SCROLL_CONTAINER`, the scroll container itself, used
  for wheel/trackpad scroll target lookup.

The reverse path (cursor to tag) is owned by the platform shell. It
queries WebRender's hit tester each frame and routes results through the
event dispatch system. See [Hit Testing](../events/hit-testing.md) for the
full namespace layout and result-type mapping.

## Scene-builder skip

Two flags govern WebRender's scene-build cycle:

- **Display list rebuilt** (`display_list_was_rebuilt = true`). Full scene
  build. Resources, display lists, and root pipeline are all submitted.
- **Property-only update** (`display_list_was_rebuilt = false`).
  `txn.skip_scene_builder()`. WebRender reuses the existing scene; only
  `set_scroll_offsets`, `append_dynamic_properties`, and
  `set_document_view` apply.

Layout decides which path to take based on the `Update` value returned from
callbacks. `Update::RefreshDom` means full rebuild. `Update::DoNothing`
with only GPU or scroll changes means property-only.

## Software path (cpurender)

The bridge does *not* know about `cpurender`. The split happens one layer
above, in `dll/src/desktop/window.rs`, where the renderer is constructed.
With the GPU path, `WrRenderApi` is a real WebRender API. With the
software path, it's a `cpurender` API that exposes the same `Transaction`
interface but rasterises into a `Vec<u8>` framebuffer instead of
dispatching GL. Everything in `wr_translate2.rs` runs identically on both
paths. The only difference is who is on the receiving end of
`txn.generate_frame`.

That's the point of the abstraction. If you add a new bridge function,
make sure it works against the `webrender::api::Transaction` interface and
not against any GL-specific assumption. Otherwise the headless reftest
harness breaks.

## Common bridge bugs

- **Resource not registered before display list.** `txn.update_resources(...)`
  must run before `txn.set_display_list(...)` *in the same transaction*.
  If the call sites are reordered, the scene builder fails to resolve the
  key and the entire DOM renders blank.
- **Epoch not incremented after a rebuild.** WebRender uses `Epoch` to
  decide whether to drop old textures. If two consecutive frames share an
  epoch, GL textures from the old frame leak. `layout_window.epoch.increment()`
  must run on every rebuild path.
- **Forgetting `set_root_pipeline`.** Required even if the display list
  was reused — WebRender needs to know which pipeline is the root for
  hit-testing and viewport calculations.
- **Dynamic property without a corresponding binding in the display list.**
  WebRender silently ignores updates whose key doesn't match a
  `PropertyBinding::Binding(key, ...)` in the live display list. Symptom:
  scrollbar opacity changes but nothing visible. Diagnosis: check that
  `compositor2` actually pushed a stacking context with the binding
  (search for the `OpacityKey::id` in the compositor logs).

## Coming Up Next

- [Rendering](../rendering.md) — display list to pixels
- [GL Loading](gl-loading.md) — per-platform GL symbol resolution
- [Image Pipeline](image-pipeline.md) — texture caches, ExternalImageId
- [Hit Testing](../events/hit-testing.md) — tag namespaces and reverse routing
