Canvas
Servo supports four types of canvas context:
CanvasRenderingContext2D
(2d
context)WebGLRenderingContext
(webgl
context)WebGL2RenderingContext
(webgl2
context)GPUCanvasContext
(webgpu
context)
Each canvas context implements the CanvasContext
trait, which requires contexts to implement some common features in a unified way:
context_id
resize
: this method clears the painter's image by setting it to transparent alpha (all bytes are zero)get_image_data
: used when obtaining the canvas image, usually by callingtoDataUrl
,toBlob
,createImageBitmap
on the canvas or indirectly by drawing one canvas in anotherupdate_the_rendering
: for triggering update of image (usually by swapping screen-buffer and back-buffer)canvas
: obtain connected canvas element (this can beHTMLCanvasElement
orOffscreenCanvas
, which can also be connected toHTMLCanvasElement
with context set toplaceholder
) while also providing some good default implementations (onscreen
,origin_is_clean
,size
,mark_as_dirty
).mark_as_dirty
is called from functions that affect the painter's image and tells layout to rerender the canvas element (by markingHTMLCanvasElement
as dirty node).
HTML event loop and rendering
flowchart TB subgraph Content Process subgraph Script flow JS-->utr[Update the rendering]-->Layout end end subgraph Main Process subgraph Painters WGPU[WGPU thread] WEBGL[WebGL thread] CPT[Canvas Paint Thread] end %% actual update in painters Painters--CreateImage-->Compositor Painters--UpdateImage-->Compositor Compositor-->WR WR[WebRender]--lock,unlock-->Painters end %% init canvas JS--create context-->Painters--ImageKey, CanvasId-->JS %% update canvas rendering utr<--Update rendering-->Painters %% rendering Layout--DisplayList (contains ImageKey)-->Compositor
As part of the HTML event loop, the script thread runs a task (parsing, script evaluating, callbacks, events, ...) and after that it performs a microtask checkpoint that drains the microtasks queue.
In the window event loop we queue a global task to update the rendering if there is a rendering opportunity (usually driven by compositor based on hardware refresh rate).
In Servo we do not actually queue a task, but instead we run update the rendering
after any IPC messages in the ScriptThread and then perform a microtask checkpoint too as the event loop would have done after a task is completed.
Update the rendering does various resize, scroll and animations steps (which also includes performing a microtask checkpoint to resolve outstanding promises) and then run the animation frame callbacks (callbacks added with requestAnimationFrame
).
At this point draw commands are issued to painters to create a new frame of animation.
Finally we trigger reflow (layout), which first updates the rendering of canvases (by flushing dirty canvases) and animated images, then traverses the DOM and its styles, builds a DisplayList
, and sends that to WebRender for rendering.
When canvas context creation is requested (canvas.getContext('2d')
), the script thread blocks on the painter thread as it initializes and creates a new WebRender image (CreateImage
), finally sending the associated ImageKey
back to script.
sequenceDiagram Script->>Constellation: Create Context Constellation->>Painter: Create Context Painter->>Compositor: GenerateImageKey Compositor->>WebRender: GenerateImageKey opt Painter<<->>Compositor: ExternalImageId end WebRender->>Compositor: ImageKey Compositor->>Painter: ImageKey Painter->>Compositor: CreateImage Compositor->>WebRender: CreateImage Painter->>Script: PainterIPCSender, ImageKey, CanvasId
Each canvas context implements LayoutCanvasRenderingContextHelpers
, which returns the ImageKey
that layout will use in its DisplayList
, or None
if the canvas is cleared or otherwise not paintable due to its size.
WebRender will read the resultant image data when rendering, based on the provided ImageKey
.
In WebGL and WebGPU painters this is done by implementing a custom WebrenderExternalImageApi
; this provides lock
and unlock
methods for WebRender to obtain the actual image data.
For 2D canvases, image data is directly provided via CreateImage
and UpdateImage
IPC messages.
sequenceDiagram Script->>Painter:Update rendering (flush) Painter->>Compositor:UpdateImage Compositor->>WebRender: UpdateImage opt Painter->>Script: Done end Note over Script: Layout Script->>Compositor: DisplayList Compositor->>WebRender: DisplayList opt Compositor<<->>WebRender: Query ExternalImage Registery WebRender->>+Painter: lock ExternalImage WebRender->>Painter: unlock ExternalImage deactivate Painter end
2D canvas context
flowchart LR CanvasRenderingContext2d --- HTMLCanvasContext subgraph OffscreenCanvasRenderingContext2D subgraph CanvasRenderingContext2d CS'[CanvasState] end end OffscreenCanvasRenderingContext2D --- OffscreenCanvas PaintRenderingContext2D --- PaintWorklet subgraph PaintRenderingContext2D CS''[CanvasState] end
While most canvases use the same DOM type for their onscreen and offscreen contexts, this is not the case for 2D canvases due to their long history. Web standards define three types of 2D canvas context:
CanvasRenderingContext2D
(connected toHTMLCanvasContext
)OffscreenCanvasRenderingContext2D
(connected toOffscreenCanvas
)PaintRenderingContext2D
(only available inPaintWorklet
)
CanvasRenderingContext2D
and PaintRenderingContext2D
are implemented as wrappers around CanvasState
, while OffscreenCanvasRenderingContext2D
is implemented as a wrapper around CanvasRenderingContext2D
because of similar logic to avoid duplication.
flowchart LR HTMLCanvasElement --getContext('2d')--> CanvasRenderingContext2d CanvasRenderingContext2d --strokeRect--> CanvasState CanvasState --IPC strokeRect--> CanvasPaintThread CanvasPaintThread --Done--> CanvasState
CanvasState
implements the actual logic of 2D drawing, by setting appropriate state and sending IPC messages to the Canvas Paint Thread.
Some commands only change internal state, but don’t need to send any messages until there is an actual draw command.
All "dirty" 2d canvases are stored in Document
and are flushed during reflow, by sending IPC messages that trigger the update_the_rendering
method on each canvas.
When drawing one 2D canvas into another 2D canvas, we send DrawImageInOther
, a special IPC message that avoids copying the bitmap out of the canvas paint thread.
WebGL canvas context
flowchart LR WebGLRenderingContext --- c["HTMLCanvasElement OffscreenCanvas"] subgraph WebGL2RenderingContext WebGLRenderingContext end
WebGL(2) canvas contexts are WebGLRenderingContext
or WebGL2RenderingContext
, and in Servo WebGL2RenderingContext
wraps and extends WebGLRenderingContext
.
These contexts store state and send IPC messages to the WebGL thread, which executes actual OpenGL (or OpenGL ES) commands and returns results via IPC.
The script thread blocks on the WebGL thread, waiting for each operation to complete.
All "dirty" WebGL canvases are stored in Document
and are flushed as part of reflow, by sending one IPC message containing all dirty context ids, then blocking on the WebGL thread until all canvases are flushed.
Flushing swaps the framebuffer, where one is for presentation (that is read by WebRender) while the other is used for drawing as the target of GL commands.
WebGPU canvas context
WebGPU presentation is the most special as it is fully async (non-blocking). More info about how async is done in WebGPU can be read in the WebGPU chapter.
sequenceDiagram loop Context Creation Script->>WGPU: CreteContext WGPU->>WebRender: CreateImage WebRender->>WGPU: ImageKey WGPU->>Script: ImageKey end alt animation Callback Note over Script: getCurrentTexture Script-)WGPU:CreateTexture activate Script Note over Script: draw operations into current texture else update the rendering Note over Script: expire current texture Script-)WGPU:SwapchainPresent WGPU-)WGPU: Copy texture to one of staging buffer WGPU-)+WGPU poller: Map stagging buffer to CPU as GPUPresentationBuffer opt presentationId is newer than existing WGPU poller -)-WebRender: UpdateImage end WGPU poller -)WGPU poller: Unmap GPUPresentationBuffer Script-)WGPU:DestroyTexture deactivate Script end loop rendering WebRender<<->>+WGPU: lock ExternalImage and read GPUPresentationBuffer WebRender->>WGPU: unlock ExternalImage deactivate WGPU end
All onscreen WebGPU contexts have their update_the_rendering
executed as part of updating the rendering in the HTML event loop.
This expires (destroys) the current texture, but before that we send a SwapChainPresent request, which copies texture data into one of 10 presentation buffers on the GPU.
After copying is done, we async map the new buffer to CPU.
Because this process is async, we mark each presentation buffer with an incrementing u64 id, and only replace the active presentation buffer if our buffer’s id is newer.
The inactive presentation buffer gets unmapped.
flowchart TD S[Staging Presentation Buffer] --copy_texture_to_buffer, mapAsync--> Mapping --mapAsync done--> UpdateWR --yes--> Mapped[Mapped, Unmapped old] UpdateWR--else unmap-->S
This is also modeled in TLA+: https://gist.github.com/gterzian/aa5d96a89db280017b04917eee67f6ac
Both WebRender's lock
and get_image_data
will use content of the active presentation buffer.
Resources
- https://medium.com/@polyglot_factotum/fixing-servos-event-loop-490c0fd74f8d
- Update the rendering of canvas (#35733)
- webgpu: renovate gpucanvascontext and webgpu presentation to match the spec (#33521)
- webgpu: Fix HTML event loop integration (#34631)
- webgpu: Introduce PresentationId to ensure updates with newer presentation (#33613)
- webgpu: Make uploading data to wr with less copies (#33368)