Introducing the layer based SVG engine

  1. Key concepts
↑ Table Of Contents

For those not familiar with WebKit this chapter introduces the DOM/Render/Layer Tree concepts and their applications in a short form. This is not aimed to be complete in any way, only the minimum is introduced to form a basis for further discussions.

2.1. The Render Tree in WebKit ↑ Table Of Contents

In order to display a HTML document it is first loaded, then parsed, tokenized and a DOM Tree is produced in accordance with the precise rules defined in the HTML5 specification.

A Document contains a tree of Node objects that represent the DOM. Together with the CSS style rules defined for the document, the DOM can be processed according to the CSS layout rules. This will generate a number of CSS boxes and impose a hierarchy. The hierarchy of CSS boxes is called Render Tree in WebKit.

Initially a RenderView is created, associated with the Document whose dimensions correspond to the viewport dimensions. A RenderView is a CSS box by itself and might contain child elements derived from RenderObject, that itself might have children.

To illustrate the Render Tree concept let’s dump the simple HTML document test.html

<!DOCTYPE html>
<html>
  <body>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
  </body>
</html>

by utilizing WebKitTestRunner test.html: (dump slightly altered for brevity)

(1) RenderView at (0,0) size 800x600
(2)   RenderBlock {HTML} at (0,0) size 800x84
(3)     RenderBlock {BODY} at (8,16) size 784x52
(4)       RenderBlock {P} at (0,0) size 784x18
(5)         RenderText {#text} at (0,0) size 77x18
              text run at (0,0) width 77: "Paragraph 1"
(6)       RenderBlock {P} at (0,34) size 784x18
(7)         RenderText {#text} at (0,0) size 77x18
              text run at (0,0) width 77: "Paragraph 2"

A line starts with the name of the renderer that’s dumped (e.g. RenderView), the name of the corresponding DOM node in curly braces (e.g. <html>) and the position and size of the box, if applicable.

The <html> / <body> / <p> elements are all CSS block-level elements, represented by RenderBlock. DOM text nodes are represented by RenderText nodes.

Thus the Render Tree consists of the aforementioned outermost RenderView (1), containing a single RenderBlock (2) child corresponding to the <html> element, itself containing a RenderBlock (3) associated with the <body> element. The latter RenderBlock contains two RenderBlock children (4) and (6) corresponding to the two <p> elements in the DOM. These RenderBlock elements each contain a single child element (5) and (7) corresponding to the text nodes in the DOM tree.

Besides the structure and hierarchy of the Render Tree, also the relevant positioning information are included in the Render Tree dump and can be confusing on first sight (where does the 8,16 come from?).

The RenderView (1) is 800px x 600px wide, the RenderBlock (2) for the <html> element is 84px tall and occupies the full width of the viewport. The RenderBlock (3) for the <body> element starts at position 8px x 16px - as the result of the interplay of two CSS rules in the WebKit user-agent style sheet html.css (qem units removed for brevity):

body {
    display: block;
    margin: 8px;
}

p {
    display: block;
    -webkit-margin-before: 1em;
    -webkit-margin-after: 1em;
    -webkit-margin-start: 0;
    -webkit-margin-end: 0;
}

It states that <body> elements are treated as CSS block-level elements, with an associated margin of 8px in all directions, whereas <p> elements - in left-to-right, top-to-bottom documents - only have margin to the top and bottom, not to the left and right. Naively you’d expect the <p> element to start at 8px (bottom margin from <body>) plus 1em = 16px, so in total 24px - but it’s only 16px due to a special case: CSS margin collapsing. The bottom margin from the <body> element is collapsed with the top margin from the <p> element to the largest of the individual margins: max(8px, 16px) = 16px. Alright, CSS is complex, the devil is in the details.

To summarize, we briefly discussed the construction of the DOM tree and the construction of the Render Tree for a simple test case – intentionally leaving out more complex topics such as inline elements, images, tables, videos, grid, flexbox, multi-column layout, etc.

The name Render Tree implies that it could be used to paint the document - however WebKit uses another tree structure - the so-called Layer Tree - for painting / hit-testing. In the next section it will become apparent that painting the Render Tree directly would be inefficient, whereas the Layer Tree allows for efficient painting and hit-testing.

2.2. The Layer Tree in WebKit ↑ Table Of Contents

The concept of layers was introduced in early CSS days and is related to the CSS ‘z-index’ property, that allows to change the painting order of positioned CSS boxes, as shown in the following picture:

Source: https://tympanus.net/codrops/css_reference/z-index/

CSS 2.1, Layered presentation introduces a key concept, the so-called stacking context:

In CSS 2.1, each box has a position in three dimensions. In addition to their horizontal and vertical positions, boxes lie along a “z-axis” and are formatted one on top of the other. Z-axis positions are particularly relevant when boxes overlap visually.

Each box belongs to one stacking context. Each positioned box in a given stacking context has an integer stack level, which is its position on the z-axis relative other stack levels within the same stacking context. Boxes with greater stack levels are always formatted in front of boxes with lower stack levels. Boxes may have negative stack levels. Boxes with the same stack level in a stacking context are stacked back-to-front according to document tree order.

As consequence, determining the painting order of CSS boxes boils down to the determination of the stacking context hierarchy. Thus the order in which the Render Tree is painted onto the screen is described in terms of stacking contexts.

Furthermore CSS defines which elements create a stacking context:

The root element forms the root stacking context.

Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of ‘z-index’ other than ‘auto’.

Applying these rules forms a hierarchy of stacking contexts, that we call Layer Tree in WebKit. CSS 2.1 also precisely defines the painting order for the stacking contexts:

Within each stacking context, the following layers are painted in back-to-front order:

– the background and borders of the element forming the stacking context.

– the child stacking contexts with negative stack levels (most negative first).

– the in-flow, non-inline-level, non-positioned descendants.

– the non-positioned floats.

– the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.

– the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.

– the child stacking contexts with positive stack levels (least positive first).

Thus painting a document in WebKit boils down to painting the root RenderLayer and propagating the paint request according to the aforementioned CSS rules down the Layer Tree.

There are many more cases that lead to the creation of a stacking context and/or a RenderLayer, such as the presence of a CSS ‘transform’ property, CSS masks, filters, etc. However for our purposes it is enough to know that the Layer Tree is basically a paint-order sorted view of the Render Tree, and layers are only selectively created if necessary.

2.3. Dumping the Render / Layer Tree ↑ Table Of Contents

To finish the introduction of the different tree concepts a concrete example on how the Render Tree / Layer Tree hierarchy looks like is helpful. Consider the following HTML document fragment:

<!DOCTYPE html>
<style>
.asection {
    position: relative;
    display: inline-block;
    width: 400px;
    color: black;
}

.ablock {
    position: absolute;
    border: dashed 4px blue;
    width: 150px;
    height: 45px;
}

.z1 {
    z-index: 1;
    left: 0;
    top: 30px;
    background-color: cyan;
}

.zauto {
    z-index: auto;
    left: 30px;
    top: 70px;
    background-color: magenta;
}

.z2 {
    z-index: 2;
    left: 60px;
    top: 100px;
    background-color: yellow;
}
</style>

<section class="asection">
    <div class="ablock z1">z-index: 1</div>
    <div class="ablock zauto">z-index: auto</div>
    <div class="ablock z2">z-index: 2</div>
</section>

Output:

z-index: 1
z-index: auto
z-index: 2

Previously we inspected the WebKitTestRunner dumps, which are helpful but interleave the layer and render tree dumps, which is confusing and does not fully reveal the tree structure. On WebKit macOS debug builds, one can invoke notifyutil -p com.apple.WebKit.showRenderTree to dump the Render Tree and likewise notifyutil -p com.apple.WebKit.showLayerTree to dump the Layer Tree as separated entities when e.g. MiniBrowser is used to display a web page. On other platforms one can simply attach a debugger and invoke ‘printRenderTreeForLiveDocuments()' / ‘printLayerTreeForLiveDocuments()' to get the same dumps.

Render Tree dump:

(B)lock/(I)nline/I(N)line-block, (A)bsolute/Fi(X)ed/(R)elative/Stic(K)y, (F)loating, (O)verflow clip, Anon(Y)mous, (G)enerated, has(L)ayer, hasLayer(S)crollableArea, (C)omposited, (+)Dirty style, (+)Dirty layout
B---YGLSC --* RenderView at (0,0) size 1440x752 
B-----LS- --    HTML RenderBlock at (0,0) size 1440x34 (layout overflow 0,0 1440x180)
B-------- --      BODY RenderBody at (8,8) size 1424x18 (layout overflow 0,0 1424x172)
-------- --        Line: (top: 0 bottom: 18) with leading (top: 0 bottom: 18)
-------- --        RootInlineBox at (0,0) size 400x18
-------- --          InlineBox at (0,14) size 400x0
NR----L-- --        SECTION RenderBlock at (0,14) size 400x0 (layout overflow 0,0 400x158)
BA----L-- --          DIV RenderBlock at (0,30) size 158x58
-------- --            line at (4.00,4.00) size (150.00x18.00) baseline (14.00) enclosing top (0.00) bottom (18.00)
-------- --              Inline level boxes:
-------- --                Root inline box at (0.00,0.00) size (64.42x18.00) baseline (14.00) ascent (14.00/14.00) descent (4.00/4.00)
-------- --              Runs:
-------- --                text run at (0.00,0.00) size 64.42x18.00 run(0, 10)
I-------- --            #text RenderText length->(10) "z-index: 1"
BA----L-- --          DIV RenderBlock at (30,70) size 158x58
-------- --            line at (4.00,4.00) size (150.00x18.00) baseline (14.00) enclosing top (0.00) bottom (18.00)
-------- --              Inline level boxes:
-------- --                Root inline box at (0.00,0.00) size (83.97x18.00) baseline (14.00) ascent (14.00/14.00) descent (4.00/4.00)
-------- --              Runs:
-------- --                text run at (0.00,0.00) size 83.97x18.00 run(0, 13)
I-------- --            #text RenderText length->(13) "z-index: auto"
BA----L-- --          DIV RenderBlock at (60,100) size 158x58
-------- --            line at (4.00,4.00) size (150.00x18.00) baseline (14.00) enclosing top (0.00) bottom (18.00)
-------- --              Inline level boxes:
-------- --                Root inline box at (0.00,0.00) size (64.42x18.00) baseline (14.00) ascent (14.00/14.00) descent (4.00/4.00)
-------- --              Runs:
-------- --                text run at (0.00,0.00) size 64.42x18.00 run(0, 10)
I-------- --            #text RenderText length->(10) "z-index: 2"
I-------- --        #text RenderText length->(1) "\n"

Layer Tree dump:

layer 0x142914810 scrollableArea 0x1429d1320 at (0,0) size 1440x752 (composited [root], bounds=at (0,0) size 1440x752, drawsContent=1, paints into ancestor=0)
  RenderView 0x1428f9330 at (0,0) size 1440x752
 positive z-order list (1)
  layer 0x142914968 scrollableArea 0x1429d0cf0 at (0,0) size 1440x34
    RenderBlock 0x1428f9ab0 {HTML} at (0,0) size 1440x34 (layout overflow 0,0 1440x180)
      RenderBody 0x1428f9be0 {BODY} at (8,8) size 1424x18 (layout overflow 0,0 1424x172)
        RenderText 0x1428fbb20 {#text} at (0,0) size 0x0
   positive z-order list (4)
    layer 0x142914ac0 at (8,22) size 400x0
      RenderBlock (relative positioned) 0x1428fb4e0 {SECTION} at (0,14) size 400x0 class="section" (layout overflow 0,0 400x158)
    layer 0x142914d70 at (38,92) size 158x58
      RenderBlock (positioned) 0x1428fb7c0 {DIV} at (30,70) size 158x58 [bgcolor=#FF0000] [border: (4px solid #0000FF)] class="block zauto"
        RenderText 0x1428fb8f0 {#text} at (4,4) size 84x18
          text run at (4,4) width 84: "z-index: auto"
    layer 0x142914c18 at (8,52) size 158x58
      RenderBlock (positioned) 0x1428fb610 zI: 1 {DIV} at (0,30) size 158x58 [bgcolor=#FF0000] [border: (4px solid #0000FF)] class="block z1"
        RenderText 0x1428fb740 zI: 1 {#text} at (4,4) size 65x18
          text run at (4,4) width 65: "z-index: 1"
    layer 0x142914ec8 at (68,122) size 158x58
      RenderBlock (positioned) 0x1428fb970 zI: 2 {DIV} at (60,100) size 158x58 [bgcolor=#FF0000] [border: (4px solid #0000FF)] class="block z2"
        RenderText 0x1428fbaa0 zI: 2 {#text} at (4,4) size 65x18
          text run at (4,4) width 65: "z-index: 2"

Both the Render Tree and Layer Tree dumps reveal a tree structure, which can be deduced from the indentation level. As explained in the previous section WebKit starts painting from the root layer, which propagates the paint request down to its descendants.

This can be traced by inspecting the Layer Tree dump: the paint request is propagated from the root layer to the layer associated with the <html> renderer, down to each of the four positive z-order children. Thus the box with “z-index: auto” is painted first, followed by the box with “z-index: 1” and the box with “z-index: 2”. There’s no magic involved here, the complexity involved with the Layer Tree is the construction of that tree itself since many different CSS properties/behaviours play a role.

Armed with an understanding of the relationship between the Render Tree and the Layer Tree it’s time to discuss how HTML/CSS and SVG painting actually differ in WebKit at present.

Navigation: Previous     Next
comments powered by Disqus