Advanced Layers is a new method of compositing layers in Gecko. This document serves as a technical overview and provides a short walk-through of its source code.
Advanced Layers attempts to group as many GPU operations as it can into a single draw call. This is a common technique in GPU-based rendering called “batching”. It is not always trivial, as a batching algorithm can easily waste precious CPU resources trying to build optimal draw calls.
Advanced Layers reuses the existing Gecko layers system as much as possible. Huge layer trees do not currently scale well (see the future work section), so opportunities for batching are currently limited without expending unnecessary resources elsewhere. However, Advanced Layers has a few benefits:
- It submits smaller GPU workloads and buffer uploads than the existing compositor.
- It needs only a single pass over the layer tree.
- It uses occlusion information more intelligently.
- It is easier to add new specialized rendering paths and new layer types.
- It separates compositing logic from device logic, unlike the existing compositor.
- It is much faster at rendering 3d scenes or complex layer trees.
- It has experimental code to use the z-buffer for occlusion culling.
Because of these benefits we hope that it provides a significant improvement over the existing compositor.
Advanced Layers uses the acronym “MLG” and “MLGPU” in many places. This stands for “Mid-Level Graphics”, the idea being that it is optimized for Direct3D 11-style rendering systems as opposed to Direct3D 12 or Vulkan.
Advanced layers does not change client-side rendering at all. Content still uses Direct2D (when possible), and creates identical layer trees as it would with a normal Direct3D 11 compositor. In fact, Advanced Layers re-uses all of the existing texture handling and video infrastructure as well, replacing only the composite-side layer types.
Advanced Layers does not create a
LayerManagerComposite - instead,
it creates a
LayerManagerMLGPU. This layer manager does not have a
Compositor - instead, it has an
MLGDevice, which roughly
abstracts the Direct3D 11 API. (The hope is that this API is easily
interchangeable for something else when cross-platform or software
support is needed.)
LayerManagerMLGPU also dispenses with the old “composite” layers for
new layer types. For example,
ColorLayerMLGPU. Since these layer types implement
they integrate with
LayerTransactionParent as normal composite
The steps for rendering are described in more detail below, but roughly the process is:
- Sort layers front-to-back.
- Create a dependency tree of render targets (called “views”).
- Accumulate draw calls for all layers in each view.
- Upload draw call buffers to the GPU.
- Execute draw commands for each view.
Advanced Layers divides the layer tree into “views”
RenderViewMLGPU), which correspond to a render target. The root
layer is represented by a view corresponding to the screen. Layers that
require intermediate surfaces have temporary views. Layers are analyzed
front-to-back, and rendered back-to-front within a view. Views
themselves are rendered front-to-back, to minimize render target
Each view contains one or more rendering passes (
pass represents a single draw command with one or more rendering items
attached to it. For example, a
SolidColorPass item contains a
rectangle and an RGBA value, and many of these can be drawn with a
single GPU call.
When considering a layer, views will first try to find an existing rendering batch that can support it. If so, that pass will accumulate another draw item for the layer. Otherwise, a new pass will be added.
When trying to find a matching pass for a layer, there is a tradeoff in CPU time versus the GPU time saved by not issuing another draw commands. We generally care more about CPU time, so we do not try too hard in matching items to an existing batch.
After all layers have been processed, there is a “prepare” step. This copies all accumulated draw data and uploads it into vertex and constant buffers in the GPU.
Finally, we execute rendering commands. At the end of the frame, all batches and (most) constant buffers are thrown away.
Advanced Layers currently has five layer-related shader pipelines:
- Textured (PaintedLayer, ImageLayer, CanvasLayer)
- ComponentAlpha (PaintedLayer with component-alpha)
- YCbCr (ImageLayer with YCbCr video)
- Color (ColorLayers)
- Blend (ContainerLayers with mix-blend modes)
There are also three special shader pipelines:
- MaskCombiner, which is used to combine mask layers into a single texture.
- Clear, which is used for fast region-based clears when not directly supported by the GPU.
- Diagnostic, which is used to display the diagnostic overlay texture.
The layer shaders follow a unified structure. Each pipeline has a vertex and pixel shader. The vertex shader takes a layers ID, a z-buffer depth, a unit position in either a unit square or unit triangle, and either rectangular or triangular geometry. Shaders can also have ancillary data needed like texture coordinates or colors.
Most of the time, layers have simple rectangular clips with simple rectilinear transforms, and pixel shaders do not need to perform masking or clipping. For these layers we use a fast-path pipeline, using unit-quad shaders that are able to clip geometry so the pixel shader does not have to. This type of pipeline does not support complex masks.
If a layer has a complex mask, a rotation or 3d transform, or a complex operation like blending, then we use shaders capable of handling arbitrary geometry. Their input is a unit triangle, and these shaders are generally more expensive.
All of the shader-specific data is modelled in ShaderDefinitionsMLGPU.h.
CPU Occlusion Culling¶
By default, Advanced Layers performs occlusion culling on the CPU. Since layers are visited front-to-back, this is simply a matter of accumulating the visible region of opaque layers, and subtracting it from the visible region of subsequent layers. There is a major difference between this occlusion culling and PostProcessLayers of the old compositor: AL performs culling after invalidation, not before. Completely valid layers will have an empty visible region.
Most layer types (with the exception of images) will intelligently split their draw calls into a batch of individual rectangles, based on their visible region.
Z-Buffering and Occlusion¶
Advanced Layers also supports occlusion culling on the GPU, using a z-buffer. This is disabled by default currently since it is significantly costly on integrated GPUs. When using the z-buffer, we separate opaque layers into a separate list of passes. The render process then uses the following steps:
- The depth buffer is set to read-write.
- Opaque batches are executed.,
- The depth buffer is set to read-only.
- Transparent batches are executed.
The problem we have observed is that the depth buffer increases writes to the GPU, and on integrated GPUs this is expensive - we have seen draw call times increase by 20-30%, which is the wrong direction we want to take on battery life. In particular on a full screen video, the call to ClearDepthStencilView plus the actual depth buffer write of the video can double GPU time.
For now the depth-buffer is disabled until we can find a compelling case for it on non-integrated hardware.
Clipping is a bit tricky in Advanced Layers. We cannot use the hardware “scissor” feature, since the clip can change from instance to instance within a batch. And if using the depth buffer, we cannot write transparent pixels for the clipped area. As a result we always clip opaque draw rects in the vertex shader (and sometimes even on the CPU, as is needed for sane texture coordiantes). Only transparent items are clipped in the pixel shader. As a result, masked layers and layers with non-rectangular transforms are always considered transparent, and use a more flexible clipping pipeline.
Plane splitting is when a 3D transform causes a layer to be split - for example, one transparent layer may intersect another on a separate plane. When this happens, Gecko sorts layers using a BSP tree and produces a list of triangles instead of draw rects.
These layers cannot use the “unit quad” shaders that support the fast clipping pipeline. Instead they always use the full triangle-list shaders that support extended vertices and clipping.
This is the slowest path we can take when building a draw call, since we must interact with the polygon clipping and texturing code.
For each layer with a mask attached, Advanced Layers builds a
MaskOperation. These operations must resolve to a single mask
texture, as well as a rectangular area to which the mask applies. All
batched pixel shaders will automatically clip pixels to the mask if a
mask texture is bound. (Note that we must use separate batches if the
mask texture changes.)
Some layers have multiple mask textures. In this case, the MaskOperation will store the list of masks, and right before rendering, it will invoke a shader to combine these masks into a single texture.
MaskOperations are shared across layers when possible, but are not cached across frames.
ImageLayers and CanvasLayers can be tiled with many individual textures. This happens in rare cases where the underlying buffer is too big for the GPU. Early on this caused problems for Advanced Layers, since AL required one texture per layer. We implemented BigImage support by creating temporary ImageLayers for each visible tile, and throwing those layers away at the end of the frame.
Advanced Layers no longer has a 1:1 layer:texture restriction, but we
retain the temporary layer solution anyway. It is not much code and it
means we do not have to split
TexturedLayerMLGPU methods into
iterated and non-iterated versions.
Advanced Layers has a different texture locking scheme than the existing compositor. If a texture needs to be locked, then it is locked by the MLGDevice automatically when bound to the current pipeline. The MLGDevice keeps a set of the locked textures to avoid double-locking. At the end of the frame, any textures in the locked set are unlocked.
We cannot easily replicate the locking scheme in the old compositor, since the duration of using the texture is not scoped to when we visit the layer.
Advanced Layers uses constant buffers to send layer information and
extended instance data to the GPU. We do this by pre-allocating large
constant buffers and mapping them with
MAP_DISCARD at the beginning
of the frame. Batches may allocate into this up to the maximum bindable
constant buffer size of the device (currently, 64KB).
There are some downsides to this approach. Constant buffers are difficult to work with - they have specific alignment requirements, and care must be taken not too run over the maximum number of constants in a buffer. Another approach would be to store constants in a 2D texture and use vertex shader texture fetches. Advanced Layers implemented this and benchmarked it to decide which approach to use. Textures seemed to skew better on GPU performance, but worse on CPU, but this varied depending on the GPU. Overall constant buffers performed best and most consistently, so we have kept them.
Additionally, we tested different ways of performing buffer uploads. Buffer creation itself is costly, especially on integrated GPUs, and especially so for immutable, immediate-upload buffers. As a result we aggressively cache buffer objects and always allocate them as MAP_DISCARD unless they are write-once and long-lived.
Advanced Layers has a few different classes to help build and upload buffers to the GPU. They are:
MLGBuffer. This is the low-level shader resource that
MLGDeviceexposes. It is the building block for buffer helper classes, but it can also be used to make one-off, immutable, immediate-upload buffers. MLGBuffers, being a GPU resource, are reference counted.
SharedBufferMLGPU. These are large, pre-allocated buffers that are read-only on the GPU and write-only on the CPU. They usually exceed the maximum bindable buffer size. There are three shared buffers created by default and they are automatically unmapped as needed: one for vertices, one for vertex shader constants, and one for pixel shader constants. When callers allocate into a shared buffer they get back a mapped pointer, a GPU resource, and an offset. When the underlying device supports offsetable buffers (like
ID3D11DeviceContext1does), this results in better GPU utilization, as there are less resources and fewer upload commands.
VertexBufferSection. These are “views” into a
SharedBufferMLGPU. They contain the underlying
MLGBuffer, and when offsetting is supported, the offset information necessary for resource binding. Sections are not reference counted.
StagingBuffer. A dynamically sized CPU buffer where items can be appended in a free-form manner. The stride of a single “item” is computed by the first item written, and successive items must have the same stride. The buffer must be uploaded to the GPU manually. Staging buffers are appropriate for creating general constant or vertex buffer data. They can also write items in reverse, which is how we render back-to-front when layers are visited front-to-back. They can be uploaded to a
SharedBufferMLGPUor an immutabler
MLGBuffervery easily. Staging buffers are not reference counted.
Currently, these features of the old compositor are not yet implemented.
- OpenGL and software support (currently AL only works on D3D11).
- APZ displayport overlay.
- Diagnostic/developer overlays other than the FPS/timing overlay.
- DEAA. It was never ported to the D3D11 compositor, but we would like it.
- Component alpha when used inside an opaque intermediate surface.
- Effects prefs. Possibly not needed post-B2G removal.
- Widget overlays and underlays used by macOS and Android.
- DefaultClearColor. This is Android specific, but is easy to added when needed.
- Frame uniformity info in the profiler. Possibly not needed post-B2G removal.
- LayerScope. There are no plans to make this work.
- Refactor for D3D12/Vulkan support (namely, split MLGDevice into something less stateful and something else more low-level).
- Remove “MLG” moniker and namespace everything.
- Other backends (D3D12/Vulkan, OpenGL, Software)
- Delete CompositorD3D11
- Add DEAA support
- Re-enable the depth buffer by default for fast GPUs
- Re-enable right-sizing of inaccurately sized containers
- Drop constant buffers for ancillary vertex data
- Fast shader paths for simple video/painted layer cases
Advanced Layers has gone through four major design iterations. The initial version used tiling - each render view divided the screen into 128x128 tiles, and layers were assigned to tiles based on their screen-space draw area. This approach proved not to scale well to 3d transforms, and so tiling was eliminated.
We replaced it with a simple system of accumulating draw regions to each batch, thus ensuring that items could be assigned to batches while maintaining correct z-ordering. This second iteration also coincided with plane-splitting support.
On large layer trees, accumulating the affected regions of batches proved to be quite expensive. This led to a third iteration, using depth buffers and separate opaque and transparent batch lists to achieve z-ordering and occlusion culling.
Finally, depth buffers proved to be too expensive, and we introduced a simple CPU-based occlusion culling pass. This iteration coincided with using more precise draw rects and splitting pipelines into unit-quad, cpu-clipped and triangle-list, gpu-clipped variants.