Compositing
How are composited visuals represented?
Scene is an opaque, immutable representation of composited UI. Each frame, the rendering pipeline produces a scene that is uploaded to the engine for rasterization via
Window.render
. A scene can also be rasterized into anImage
viaScene.toImage
. Generally, aScene
encapsulates a stack of rendering operations (e.g., clips, transforms, effects) and graphics (e.g., pictures, textures, platform views).SceneBuilder
is lowest level mechanism (within the framework) for assembling a scene.SceneBuilder
manages a stack of rendering operations (viaSceneBuilder.pushClipRect
,SceneBuilder.pushOpacity
,SceneBuilder.pop
, etc) and associated graphics (viaSceneBuilder.addPicture
,SceneBuilder.addTexture
,SceneBuilder.addRetained
, etc). As operations are applied,SceneBuilder
producesEngineLayer
instances that it both tracks and returns. These may be cached by the framework to optimize subsequent frames (e.g., updating an old layer rather than recreating it, or reusing a previously rasterized layer). Once complete, the finalScene
is obtained viaSceneBuilder.build
.Drawing operations are accumulated in a
Picture
(viaPictureRecorder
) which is added to the scene viaSceneBuilder.addPicture
.External textures (represented by a texture ID established using the platform texture repository) are added to the scene via
SceneBuilder.addTexture
. This supports a “freeze” parameter so that textures may be paused when animation is paused.Previously rasterized
EngineLayers
can be reused as an efficiency operation viaSceneBuilder.addRetained
. This is known as retained rendering.
Picture is an opaque, immutable representation of a sequence of drawing operations. These operations can be added directly to a
Scene
(viaSceneBuilder.addPicture
) or combined into another picture (viaCanvas.drawPicture
). Pictures can also be rasterized (viaPicture.toImage
) and provide an estimate of their memory footprint.PictureRecorder
is an opaque mechanism by which drawing operations are sequenced and transformed into an immutablePicture
. Picture recorders are generally managed via aCanvas
. Once recording is ended (viaPictureRecorder.endRecording
), a finalPicture
is returned and thePictureRecorder
(and associatedCanvas
) are invalidated.
How are layers represented?
Layer represents one slice of a composited scene. Layers are arranged into a hierarchy with each node potentially affecting the nodes below it (i.e., by applying rendering operations or drawing graphics). Layers are mutable and freely moved around the tree, but must only be used once. Each layer is composited into a
Scene
usingSceneBuilder
(viaLayer.addToScene
); adding the root layer is equivalent to compositing the entire tree (e.g., layers add their children). Note that the entire layer tree must be recomposited when mutated; there is no concept of dirty states.SceneBuilder
provides anEngineLayer
instance when a layer is added. This handle can be stored inLayer.engineLayer
and later used to optimize compositing. For instance, rather than repeating an operation,Layer.addToScene
might pass the engine layer instead (viaLayer._addToSceneWithRetainedRendering
), potentially reusing the rasterized texture from the previous frame. Additionally, an engine layer (i.e., “oldLayer
”) can be specified when performing certain rendering operations to allow these to be implemented as inline mutations.Layers provide support for retained rendering via
Layer.markNeedsAddToScene
. This flag tracks whether the previous engine layer can be reused during compositing (i.e., whether the layer is unchanged from the last frame). If so, retained rendering allows the entire subtree to be replaced with a previously rasterized bitmap (viaSceneBuilder.addRetained
). Otherwise, the layer must be recomposited from scratch. Once a layer has been added, this flag is cleared (unless the layer specifies that it must always be composited).All layers must be added to the scene initially. Once added, the cached engine layer may be used for retained rendering. Other layers disable retained rendering altogether (via
Layer.alwaysNeedsAddToScene
). Generally, when a layer is mutated (i.e., by modifying a property or adding a child), it must be re-added to the scene.If a layer must be added to the scene, all ancestor layers must be added, too. Retained rendering allows a subtree to be replaced with a cached bitmap. If a descendent has changed, this bitmap becomes invalid and therefore the ancestor layer must also be added to the scene (via
Layer.updateSubtreeNeedsAddToScene
).Note that this property isn’t enforced during painting; the layer tree is only made consistent when the scene is composited (via
ContainerLayer.buildScene
).
A layer’s parent must be re-added when it receives a new engine layer [?]
Generally, only container layers use retained rendering?
Metadata can be embedded within a layer for later retrieval (via
Layer.find
andLayer.findAll
).
ContainerLayer
manages a list of child layers, inserting them into the composited scene in order. The root layer is a subclass ofContainerLayer
(TransformLayer
); thus,ContainerLayer
is responsible for compositing the full scene (viaContainerLayer.buildScene
). This involves making retained rendering state consistent (viaContainerLayer.updateSubtreeNeedsAddToScene
), adding all the children to the scene (viaContainerLayer.addChildrenToScene
), and marking the container as no longer needing to be added (viaLayer._needsAddToScene
).ContainerLayer
implements a number of child operations (e.g.,ContainerLayer.append
,ContainerLayer.removeAllChildren
), and multiplexes most of its operations across these children (e.g.,ContainerLayer.find
; note that later children are “above” earlier children).ContainerLayer
will use retained rendering for its children provided that they aren’t subject to an offset (i.e., are positioned at the layer’s origin). This is the entrypoint for most retained rendering in the layer tree.ContainerLayer.applyTransform
transforms the provided matrix to reflect how a given child will be transformed during compositing. This method assumes all children are positioned at the origin; thus, any layer offsets must be removed and expressed as a transformation (viaSceneBuilder.pushTransform
orSceneBuilder.pushOffset
). Otherwise, the resulting matrix will be inaccurate.
OffsetLayer
is aContainerLayer
subclass that supports efficient repainting (i.e., repaint boundaries). Render objects defining repaint boundaries correspond toOffsetLayers
in the layer tree. If the render object doesn’t need to be repainted or is simply being translated, its existing offset layer may be reused. This allows the entire subtree to avoid repainting.OffsetLayer
applies any offset as a top-level transform so that descendant nodes are composited at the origin and therefore eligible for retained rendering.OffsetLayer.toImage
rasterizes the subtree rooted at this layer (viaScene.toImage
). The resulting image, consisting of raw pixel data, is rendered into a bounding rectangle using the specified pixel ratio (ignoring the device’s screen density).
TransformLayer
is aContainerLayer
subclass that applies an arbitrary transform; it also happens to (typically) be the root layer corresponding to theRenderView
. Any layer offset (viaLayer.addToScene
) or explicit offset (viaOffsetLayer.offset
) is removed and applied as a transformation to ensure thatTransformLayer.applyTransform
behaves consistently.PhysicalModelLayer
is the engine by which physical lighting effects (e.g., shadows) are integrated into the composited scene. This class incorporates an elevation, clip region, background color, and shadow color to cast a shadow behind its children (viaSceneBuilder.pushPhysicalShape
).AnnotatedRegionLayer
incorporates metadata into the layer tree within a given bounding rectangle. An offset (defaulting to the origin) determines the rectangle’s top left corner and a size (defaulting to the entire layer) represents its dimensions. Hit testing is performed byAnnotatedRegionLayer.find
andAnnotatedRegionLayer.findAll
. Results appearing deeper in the tree or later in the child list take precedence.FollowerLayer
andLeaderLayer
allow efficient transform linking (e.g., so that one layer appears to scroll with another layer). These layers communicate viaLayerLink
, passing the leader’s transform (includinglayerOffset
) to the follower. The follower uses this transform to appear where the follower is rendered.CompositedTransformFollower
andCompositedTransformTarget
widgets create and manage these layers.There are a variety of leaf and interior classes making up the layer tree.
Leaf nodes generally extend
Layer
directly.PictureLayer
describes a singlePicture
to be added to the scene.TextureLayer
is analogous, describing a texture (by ID) and a bounding rectangle;PlatformViewLayer
is nearly identical, applying a view instead of a texture.Interior nodes generally extend
ContainerLayer
or one of its subclasses.OffsetLayer
is key to implementing efficient repainting.ClipPathLayer
,ClipRectLayer
,ColorFilterLayer
,OpacityLayer
, etc., apply the corresponding effect by pushing a scene builder state, adding all children (viaContainerLayer.addChildrenToScene
, potentially utilizing retained rendering), then popping the state.
EngineLayer
and its various specializations (e.g.,OpacityEngineLayer
,OffsetEngineLayer
) represent opaque handles to backend layers.SceneBuilder
produces the appropriate handle for each operation it supports. These may then be used to enable retained rendering and inline updating.
What are the compositing building blocks?
Canvas provides an interface for performing drawing operations within the context of a single
PictureRecorder
. That is, all drawing performed by aCanvas
is constrained to a singlePicture
(andPictureLayer
). Rendering and drawing operations spanning multiple layers are supported byPaintingContext
, which managesCanvas
lifecycle based on the compositing requirements of the render tree.PaintingContext.repaintCompositedChild
is a static method that paints a render object into its own layer. Once nodes are marked dirty for painting, they will be processed during the painting phase of the rendering pipeline (viaPipelineOwner.flushPaint
). This performs a depth-first traversal of all dirty render objects. Note that only repaint boundaries are ever actually marked dirty; all other nodes traverse their ancestor chain to find the nearest enclosing repaint boundary, which is then marked dirty. The dirty node is processed (viaPaintingContext._repaintCompositedChild
), retrieving or creating anOffsetLayer
to serve as the subtree’s container layer. Next, a newPaintingContext
is created (associated with this layer) to facilitate the actual painting (viaPaintingContext._paintWithContext
).Layers can be attached and detached from the rendering pipeline. If a render object with a detached layer is found to be dirty (by
PipelineOwner.flushPaint
), the ancestor chain is traversed to find the nearest repaint boundary with an attached (or as yet uncreated) layer. This node is marked dirty to ensure that, when the layer is reattached, it will be repainted as expected (viaRenderObject._skippedPaintingOnLayer
).
PaintingContext
provides a layer of indirection between the paint method and the underlying canvas to allow render objects to adapt to changing compositing requirements. That is, a render object may introduce a new layer in some cases and re-use an existing layer in others. When this changes, any ancestor render objects will need to adapt to the possibility of a descendent introducing a new layer (viaRenderObject.needsCompositing
); in particular, this requires certain operations to be implemented by introducing their own layers (e.g., clipping).PaintingContext
manages this process and provides a number of methods that encapsulate this decision (e.g.,PaintingContext.pushClipPath
).PaintingContext
also ensures that children introducing repaint boundaries are composited into new layers (viaPaintingContext.paintChild
). Last,PaintingContext
manages thePictureRecorder
provided to theCanvas
, appending picture layers (i.e.,PictureLayer
) whenever a recording is completed.PaintingContext
is initialized with aContainerLayer
subclass (i.e., the container layer) to serve as the root of the layer tree produced via that context; painting never takes place within this layer, but is instead captured by newPictureLayer
instances which are appended as painting proceeds.PaintingContext
may manage multiple canvases due to compositing. EachCanvas
is associated with aPictureRecorder
and aPictureLayer
(i.e., the current layer) for the duration of its lifespan. Until a new layer must be added (e.g., to implement a compositing effect or because a repaint boundary is encountered), the same canvas (andPictureRecorder
) is used for all render objects.Recording begins the first time the canvas is accessed (via
PaintingContext.canvas
). This allocates a new picture layer, picture recorder, and canvas instance; the picture layer is appended to the container layer once created.Recording ends when a new layer is introduced directly or indirectly; at this point, the picture is stored in the current layer (via
PictureRecorder.endRecording
) and the canvas and current layer are cleared. The next time a child is painted, a new canvas, picture recorder, and picture layer will be created.Composited children are painted using a new
PaintingContext
initialized with a correspondingOffsetLayer
. If a composited child doesn’t actually need to be repainted (e.g., because it is only being translated), theOffsetLayer
is reused with a new offset.
PaintingContext
provides a compositing naive API. If compositing is necessary, new layers are automatically pushed to achieve the desired effect (viaPaintingContext.pushLayer
). Otherwise, the effect is implemented directly using theCanvas
(e.g., viaPictureRecorder
).Pushing a layer ends the current recording, appending the new layer to the container layer. A
PaintingContext
is created for the new layer (if present, the layer’s existing children are removed since they’re likely outdated). If provided, a painting function is applied using the new context; any painting is contained within the new layer. Subsequent operations with the originalPaintingContext
will result in a new canvas, layer, and picture recorder being created.
ClipContext
isPaintingContext
’s base class. Its primary utility is in providing support for clipping without introducing a new layer (e.g., methods suitable for render objects that do not need compositing).
What do the “needs compositing” bits do?
Compositing describes the process by which layers are combined by the engine during rasterization. Within the context of the framework, compositing typically refers to the allocation of render objects to layers with respect to painting. Needing compositing does not imply that a render object will be allocated its own layer; instead, it indicates that certain effects must be implemented by introducing a new layer rather than modifying the current one (e.g., applying opacity or clipping). For instance, if a parent first establishes a clip and then paints its child, a decision must be made as to how that clip is implemented: (1) as part of the current layer (i.e.,
Canvas.clipPath
), or (2) by pushing aContainerLayer
(i.e.,ClipPathLayer
). The “needs compositing” bit is how this information is managed.Conceptually, this is because this node might eventually paint a descendent that pushes a new layer (e.g., because it is a repaint boundary). Thus, any painting that might have non-local consequences must be implemented in a way that would work across layers (i.e., via compositing).
Note that this is different than from marking a render object as being a repaint boundary. Doing this causes a new layer to be introduced when painting the child (via
PaintingContext.paintChild
). This invalidates the needs compositing bits for all the ancestors of the repaint boundary. This might cause some ancestors to introduce new layers when painting, but only if they utilize any non-local operations
How are “needs compositing” bits updated?
RenderObject.markNeedsCompositingBitsUpdate
marks a render object as requiring a compositing bit update (viaPipelineOwner
._nodesNeedingCompositingBitsUpdate
). If a node is marked dirty, all of its ancestors are marked dirty, too. As an optimization, this walk may be cut off if the current node or the current node’s parent is a repaint boundary (or the parent is already marked dirty).If a node’s compositing bits need updating, it’s possible that it will now introduce a new layer (i.e., it will need compositing). If so, all ancestors will need compositing, too, since they may paint a descendent that introduces a new layer. As described, certain non-local effects will need to be implemented via compositing.
The walk may be cut off at repaint boundaries since all ancestors must already have been marked as needing compositing.
Adding or removing children (via
RenderObject.adoptChild
andRenderObject.dropChild
) might alter the compositing requirements of the render tree. Similarly, changing theRenderObject.alwaysNeedsCompositing
bits would require that the bits be updated.
During the rendering pipeline,
PipelineOwner.flushCompositingBits
updates all dirty compositing bits (viaRenderObject._updateCompositingBits
). A given node needs compositing if (1) it’s a repaint boundary (RenderObject.isRepaintBoundary
), (2) it’s marked as always needing compositing (RenderObject.alwaysNeedsCompositing
), or (3) any of its descendants need compositing.This process walks all descendants that have been marked dirty, updating their bits according to the above policy. Note that this will typically only walk the path identified when the compositing bits were marked dirty.
If a node’s compositing bit is changed, it will be marked dirty for painting (as painting may now require additional compositing).
If a render object always introduces a layer, it should toggle
alwaysNeedsCompositing
. If this changes (other than when altering children, which calls this automatically),markNeedsCompositingBitsUpdate
should be called.
What are repaint boundaries?
Certain render objects introduce repaint boundaries to the render tree (via
RenderObject.isRepaintBoundary
). These render objects are always painted into a new layer, allowing them to paint separately from their parent. This effectively decouples the subtree rooted at the repaint boundary from previously painted nodes.This is useful when part of the UI remains mostly static and part of the UI updates frequently. A repaint boundary helps to avoid repainting the static portion of the UI.
Render objects that are repaint boundaries are associated with an
OffsetLayer
, a special type of layer that can update its position without re-rendering.
Render objects that form repaint boundaries are handled differently during painting (via
PaintingContext.paintChild
).If the child doesn’t need painting, the entire subtree is skipped. The corresponding layer is updated to reflect the new offset (via
PaintingContext._compositeChild
) and appended to the current parent layer.If the child needs painting, the current offset layer is cleared or created (via
PaintingContext._repaintCompositedChild
) then used for painting (viaRenderObject._paintWithContext
).Otherwise, painting proceeds through
RenderObject._paintWithContext
.
Since repaint boundaries always push new layers, all ancestors must be marked as needing compositing. If any such node utilizes a non-local painting effect, that node will need to introduce a new layer, too.
How is painting managed given that children can push new layers?
PaintingContext.paintChild
manages this process by (1) ending any ongoing recording, (2) creating a new layer for the child, (3) painting the child in the new layer using a new painting context (unless it is a clean repaint boundary, which will instead be used as is), (4) appending that layer to the current layer tree, and (5) creating a new canvas and layer for any future painting with the original painting context.Methods in
PaintingContext
will consulting the compositing bits to determine whether to implement a behavior by inserting a layer or altering the canvas (e.g., clipping).
How can layers make painting more efficient?
All render objects may be associated with a
ContainerLayer
(viaRenderObject.layer
). If defined, this is the last layer that was used to paint the render object (if multiple layers are used, this is the root-most layer).Repaint boundaries are automatically assigned an
OffsetLayer
(viaPaintingContext._repaintCompositedChild
). All other layers must specifically set this field when painting.Render objects that do not directly push layers must set this to null.
ContainerLayer
tracks any associatedEngineLayer
(viaContainerLayer.engineLayer
). Certain operations can use this information to allow a layer to be updated rather than rebuilt.Methods accept an “
oldLayer
” parameter to enable this optimization.Stored engine layers can be reused directly (via
SceneBuilder.addRetained
); this is retained rendering.
How can the widget tree be manually rasterized?
A layer can be rasterized to an image via
OffsetLayer.toImage
. A convenient way to obtain an image of the entire UI is to use this method on theRenderView
’s own layer. To obtain a subset of the UI, an appropriate repaint boundary can be inserted and its layer used similarly.
How are clips, transforms, and other effects managed?
There are two code paths. Layers that do not require compositing handle transforms and clips within their own rendering context (thus, the effects are limited to that single layer), via
Canvas
. Those that are composited, however, introduce these effects as specialContainerLayers
, viaPaintingContext
.A variety of useful
ContainerLayers
are included:AnnotatedRegionLayer
, which is useful for storing metadata within sections of a layer,BackdropFilterLayer
, which allows the background to be blurred or filtered,OpacityLayer
, which allows opacity to be varied, andOffsetLayer
, which is the key mechanism by which the repaint boundary optimization works.
Canvas.saveLayer
/Canvas.restore
allow drawing commands to be grouped such that effects like blending, color filtering, and anti-aliasing can be applied once for the group instead of being incrementally stacked (which could lead to invalid renderings).
How are external textures composited?
Textures are entirely managed by the engine and referenced by a simple texture ID. Layers depicting textures are associated with this ID and, when composited, integrated with the backend texture. As such, these are painted out-of-band by the engine.
Texture layers are integrated with the render tree via
TextureBox
. This is a render box that tracks a texture ID and paints a correspondingTextureLayer
. During layout, this box expands to fill the incoming constraints.
Last updated