preface

CSS, full of Cascading Style Sheets, is used to customize document styles; CSS makes the presentation of web pages richer, which is the most novel place for beginners to learn the front end;

But with the in-depth use of CSS will be found: those we are talking about “xx attributes of strange skills” such things are just floating on the surface of THE CSS phenomenon; This way of using CSS in my opinion is like “blind men and elephants”, that is, you can only look at the surface to summarize the use of methods, rather than from the essence of the solution, so you can always rely on the surface to solve the problem.

Positioning of the CSS

After a preliminary understanding of the browser on the mechanism and principle of web rendering, there is a question in the mind has become more prominent, that is – “CSS in web rendering positioning is what”; I combined the graphic rendering process of webGL, a simple graphic rendering API, with the role of CSS code in web rendering after parsing in the browser, and came to a conclusion of my own:

CSS is an auxiliary DSL for structurally describing rendered information.

The main reasons for drawing such a conclusion are as follows:

  • Structured description:CSSThe code can be parsed intoCSSOMAnd then attach toDOMOn, this benefitsCSSThe syntax itself is an object description of key-value pairs;
  • supporting: singleCSSThe code doesn’t draw anything useful, it has to be combinedHTMLThe layout, location and other node information obtained by analysis can be drawn; namelyCSSThe code does not act as a skeleton in the rendering process, but more based on the skeleton to give more variety of rendering;
  • DSL: Needless to say,CSSThe language itself is not a universal programming language, it is only for the rendering of web documents, its scope of action andGLSL/HLSLThis kind of shader programming language could not be more specialized.

CSS and draw

Since CSS only AIDS rendering, how does the style information carried by CSS translate into the underlying drawing statement?

This is where a more detailed browser rendering pipeline is needed, because from the outline pipeline shown in Figure 1, it is impossible to understand how CSS string information is parsed inside the browser and translated into specific low-level drawing commands. Fortunately, Google has published a very detailed internal speech explaining how the pixels on the web are displayed after the pipeline: Life of a Pixel (this speech is highly recommended to read); Thanks to this presentation, I don’t have to look for hints of CSS rendering in the Chromium project source code…

The figure above is a rendering pipeline summarized by me according to the PPT of the speech above and my own understanding. It looks very complete, but in fact, it is not all the rendering pipelines of the browser. This is only a preliminary process, and the subsequent optimization of the rendering process has not been involved. Since the optimized rendering pipeline and updated rendering pipeline are more complex, they will be studied separately in the future. The process summarized here is regarded as a simple full rendering pipeline.

Mock CSS rendering

If you really want to simulate all pipeline in the above flow chart, that is HTMl + CSS → pixel, it is a huge project, it is really powerless; The part I’m most interested in is actually the rasterization part, Paint Operator → pixel, so I made a simplified model of this rasterization based on webGL:

You can see that the render pipeline is very simple, IMGUI + full rendering; Because I just want to verify how the drawing information carried by the so-called Paint Operator gets passed down to the real layer, the shader, I want to know how the Paint Operator is digested and understood inside the shader; So the above model is just a low-level interaction model that I personally came up with based on Life of a Pixel;

Simulation code

import { PaintOperator } from '@/types/css-gl'
import { vec2, vec4 } from 'gl-matrix'

(() = > {
  const { CSSGL } = WebGLEngine // WebGLEngine is a webGL rendering library written by myself
  const ops: PaintOperator[] = new Array(50).fill(0).map((val, idx) = > {
    const randomPos: vec2 = [
      Math.random() * window.innerWidth,
      Math.random() * window.innerHeight
    ]
    const randomSize = vec2.create()
    const randomBg: vec4 = [
      Math.random(),
      Math.random(),
      Math.random(),
      1.0
    ]
    vec2.random(randomSize, 200)

    return {
      id: idx,
      shape: {
        pos: randomPos,
        size: randomSize
      },
      flags: {
        background: randomBg
      }
    }
  }) // Randomly build 50 PaintOperator objects
  const gl = new CSSGL('test', ops) // Parse the PaintOperator data
  gl.paint() // Perform the drawing}) ()Copy the code

This is the parsing code, and because the underlying code is abstracted, most of the code generates PaintOperator objects; Take a look at the default shader code:

precision highp float; / / high precision
uniform vec2 u_Screen; // Screen size
attribute vec2 a_Pos; // Vertex coordinates

vec2 widthRange = vec2(0.0, u_Screen.x);
vec2 heightRange = vec2(0.0, u_Screen.y);
vec2 outputRange = vec2(1.0.1.0); // NDC coordinates range [-1, 1]

// Map a value from the original range equally to another range
float rangeMap (float source, vec2 sourceRange, vec2 targetRange) {
  float bais = source / (sourceRange.y - sourceRange.x); // The percentage of the range length
  float target = bais * (targetRange.y - targetRange.x) + targetRange.x;
  return target;
}

void main() {
  gl_Position = vec4(
    rangeMap(a_Pos.x, widthRange, outputRange),
    rangeMap(a_Pos.y, heightRange, outputRange) * 1.0.// The y axis needs to be flipped when converting to NDC
    1.0.1.0
  );
}
Copy the code
precision highp float; / / high precision
uniform vec2 u_Screen; // Screen size
uniform vec4 u_Background; / / the background color

void main() {
  gl_FragColor = u_Background;
}
Copy the code

Since the model itself is simple, so is the corresponding shader; As you can see from the shader code, the way I understand the underlying shader to receive Paint Operator information is in the primitive form of a one-to-one mapping with built-in properties.

On Skia

The above simulation idea is quite simple, so I would like to see how it differs from the concrete Chrome/Chromium underlying drawing (pure interest); Since almost all graphics drawing in Chromium was handed over to Skia graphics library, we could only go to Skia source code to find clues;

However, after looking around the source code of Skia project, I found that Skia project is too highly abstract, layer upon layer, from the shader code can not find any clues, because the information in the shader code seems to be very abstract/general data, can not be directly related to the pixel drawing; It took a little longer to figure out what I was looking for, which was a shame, but I found some good things about Skia itself:

  • Skia had an internal design for a shader Language called SkSL (Skia Shading Language); SkSL is actually designed based on a fixed version of GLSL syntax. Its function should be to erase the differences between different GPU driver API shader syntax, so that different GPU drivers can be further output as the target shader language. Therefore, SkSL can be regarded as a shader precompiled language 2.

  • Skia’s API style is also very interesting, similar to the Canvas API. See the Demo on the official website.

    void draw(SkCanvas* canvas) {
        canvas->drawColor(SK_ColorWHITE);
    
        SkPaint paint;
        paint.setStyle(SkPaint::kFill_Style);
        paint.setAntiAlias(true);
        paint.setStrokeWidth(4);
        paint.setColor(0xff4285F4);
        
        SkRect rect = SkRect::MakeXYWH(10.10.100.160);
        canvas->drawRect(rect, paint);
    
        SkRRect oval;
        oval.setOval(rect);
        oval.offset(40.80);
        paint.setColor(0xffDB4437);
        canvas->drawRRect(oval, paint);
    
        paint.setColor(0xff0F9D58);
        canvas->drawCircle(180.50.25, paint);
    
        rect.offset(80.50);
        paint.setColor(0xffF4B400);
        paint.setStyle(SkPaint::kStroke_Style);
        canvas->drawRoundRect(rect, 10.10, paint);
    }
    Copy the code

    Familiar imperative and primitive drawing naming;

  • Skia has three base classes: SkCanvas, SkBitmap, and SkPaint3.

    • SkCanvas: Manage drawing related apis;
    • SkBitmap: Manage bit data;
    • SkPaint: Manages the states related to primitive drawing style;

The related documents

  • How Blink Works – Google Docs: A statement aboutBlinkSuper overview of rendering engines
  • Introduction of Web IDL | mio classmate’s blog
  • www.chromium.org/blink
  • Goodbye, CSS Shader (CSS Custom Filter) | flowers marry da の izutsu, inadvertently found an abandoned specification
  • W3C CSS specification home page
  • www.chromium.org/developers/… : Overview of Skia use in the Chromium project
  • Skia Vulkan Performance – Zhihu
  • Shader – LearnOpenGL-CN

  1. Docs.google.com/document/d/…↩
  2. Github.com/google/skia…↩
  3. www.chromium.org/developers/…↩