Canvas
Servo supports four types of canvas context:
CanvasRenderingContext2D(2dcontext)WebGLRenderingContext(webglcontext)WebGL2RenderingContext(webgl2context)GPUCanvasContext(webgpucontext)
Each canvas context implements the CanvasContext trait, which requires contexts to implement some common features in a unified way:
context_idresize: 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,createImageBitmapon 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 beHTMLCanvasElementorOffscreenCanvas, which can also be connected toHTMLCanvasElementwith context set toplaceholder) while also providing some good default implementations (onscreen,origin_is_clean,size,mark_as_dirty).mark_as_dirtyis called from functions that affect the painter's image and tells layout to rerender the canvas element (by markingHTMLCanvasElementas 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)