CSS Paint API

This directory contains the implementation of the CSS Paint API.

See CSS Paint API for the web exposed APIs this implements.

See Explainer of this feature, as well as Samples.

Workflow

Historically the CSS Paint API (PaintWorklet) implementation ran on the main thread, but is currently being optimized to run on the compositor thread. We will use an example to show the workflow of both cases.

Here is a simple example of using PaintWorklet to draw something on the screen.

In our implementation, there is one PaintWorklet instance created from the frame. There are two PaintWorkletGlobalScope created to enforce stateless. The number of global scopes can be arbitrary, and our implementation chose two.

During PaintWorkletGlobalScope#registerPaint, the Javascript inside the paint function is turned into a V8 paint callback. We randomly choose one of the two global scopes to execute the callback. The execution of the callback produces a PaintRecord, which contains a set of skia draw commands. The V8 paint callback is executed on a shared V8 isolate.

Main thread workflow

During the main thread paint, the PaintWorklet::Paint is called, which executes the V8 paint callback synchronously. A PaintRecord is produced and passed to the compositor thread to raster.

When animation is involved, the main thread animation system updates the value of the animated properties, which are used by the PaintWorklet::Paint.

Off main thread workflow

Let's see how it works without animations.

  1. During the main thread paint, a PaintWorkletDeferredImage is created. This is an image without any color information, it is a placeholder to the Blink paint system. The creation of its actual content is deferred to CC raster time. It holds input arguments which is encapsulated in CSSPaintWorkletInput. The input arguments contain necessary information for the CC raster phase.

  2. During commit, the PaintWorkletInput is passed to CC. Specifically, the PictureLayerImpl owns PaintWorkletRecordMap, which is a map from PaintWorkletInput to std::pair<PaintImage::Id, PaintRecord>. The PaintImage::Id is used for efficient invalidation. The PaintRecord is the actual content of the PaintWorkletDeferredImage, which will be generated at CC raster time. Initially the PaintRecord is nullptr which indicates that it needs to be produced.

  3. After commit, we need to update the pending tree. This happens in LayerTreeHostImpl::UpdateSyncTreeAfterCommitOrImplSideInvalidation. There are two steps involved.

    1. The first step is to gather all dirty paint worklets that need to be updated, which happens in LayerTreeHostImpl::GatherDirtyPaintWorklets. It basically goes through each PictureLayerImpl whose PaintWorkletRecrodMap isn't empty, and if there is a PaintWorkletInput with its associated PaintRecord being nullptr, then this worklet needs to be updated.

    2. Once we have gathered all the dirty paint worklets, the next step is to produce the PaintRecord which is the actual contents. The compositor thread asynchronously dispatches the paint jobs that produce the PaintRecord to a worklet thread. Each paint job is basically a V8 paint callback, the paint callback is executed on the worklet thread and the PaintRecord is given back to the compositor thread such that it can be rastered. Given that the V8 paint callback contains user defined javascript code and can take arbitrary amount of time, the paint job doesn't block the tree activation. In other word, the pending tree can be activated even if the paint jobs are not finished, it will just use the PaintRecord that was produced in the previous frame.

Now let's see how it works with animation. Here is an example that animates a custom property ‘--foo’ with paint worklet. Traditionally custom properties cannot be animated on the compositor thread. With off main thread paint worklet design, we can animate the custom properties off the main thread and use them in paint worklet. Note that currently our implementation supports custom property animations only, not native properties. We do intend to extend to support native properties in the future.

  1. When resolving style, CompositorKeyframeValue will be created through CompositorKeyframeValueFactory::Create function. This basically tells the main thread animation system to not animate the custom properties, and instead creating a compositor animation for each custom property.

  2. After Blink paint, a compositor animation will be created through the CreateCompositorAnimation function. The compositor animation is passed to CC via commit process.

  3. CC ticks the compositor animation, which updates the value for the custom property. Currently we only support custom properties that represents number or color. This is handled by AnimatedPaintWorkletTracker::OnCustomPropertyMutated. The AnimatedPaintWorkletTracker class handles custom properties animated by paint worklet.

  4. By combining custom property name with ElementId, we create PaintWorkletInput::PropertyKey which can be used to identify a PaintWorkletInput. Then we can use the PaintWorkletInput to find its associated PaintRecord in the PictureLayerImpl's PaintWorkletRecordMap, invalidate it and update its content when we update the pending tree via LayerTreeHostImpl::UpdateSyncTreeAfterCommitOrImplSideInvalidation.

Implementation

CSSPaintDefinition

Represents a class registered by the author through PaintWorkletGlobalScope#registerPaint. Specifically this class holds onto the javascript constructor and paint functions of the class via persistent handles. This class keeps these functions alive so they don't get garbage collected.

The CSSPaintDefinition also holds onto an instance of the paint class via a persistent handle. This instance is lazily created upon first use. If the constructor throws for some reason the constructor is marked as invalid and will always produce invalid images.

The PaintWorkletGlobalScope has a map of paint name to CSSPaintDefinition.

CSSPaintImageGenerator and CSSPaintImageGeneratorImpl

CSSPaintImageGenerator represents the interface from which the CSSPaintValue can generate Images. This is done via the CSSPaintImageGenerator#paint method. Each CSSPaintValue owns a separate instance of CSSPaintImageGenerator.

CSSPaintImageGeneratorImpl is the implementation which lives in modules/csspaint. (We have this interface / implementation split as core/ cannot depend on modules/).

When created the generator will access its paint worklet and lookup it's corresponding CSSPaintDefinition via PaintWorkletGlobalScope#findDefinition.

If the paint worklet does not have a CSSPaintDefinition matching the paint name the CSSPaintImageGeneratorImpl is placed in a “pending” map. Once a paint class with name is registered the generator is notified so it can invalidate an display the correct image.

Generating a PaintGeneratedImage

PaintGeneratedImage is a Image which just paints a single PaintRecord.

A CSSPaintValue can generate an image from the method CSSPaintImageGenerator#paint. This method calls through to CSSPaintDefinition#paint which actually invokes the javascript paint method. This method returns the PaintGeneratedImage.

Style Invalidation

The CSSPaintDefinition keeps a list of both native and custom properties it will invalidate on. During style invalidation ComputedStyle checks if it has any CSSPaintValues, and if any of their properties have changed; if so it will invalidate paint for that ComputedStyle.

If the CSSPaintValue doesn‘t have a corresponding CSSPaintDefinition yet, it doesn’t invalidate paint.

Testing

Tests live here and here.