Devblog: Rendering HTML/CSS on the GPU

When rendering web pages most browsers use a general purpose graphics library to do all their drawing. For example Chrome uses the Skia graphics library. This makes sense for cross platform browsers since they can use a single drawing API and leave the implementation details to the graphics library. The graphics library can try to optimise the drawing operations using some platform specific 2D hardware acceleration, or using a 3D library such as OpenGL/DirectX to take advantage of the GPU. If there is no hardware acceleration available the graphics library can do all the drawing in software using the CPU.

The main disadvantage of this approach however is that when designing a graphics API it is not possible to optimise for all the different possible hardware backends at the same time. Also these graphics libraries typically were initially designed for software rendering only, with hardware (eg. GPU) acceleration being added later. In addition, graphics libraries provide a generic drawing API which gives the ability to draw a few different types of primitives, such as rectangles, lines, and vector paths. If we have more complicated shapes we have to combine multiple graphics operations to give the desired result.

Flow uses a different approach. Firstly we tailor our drawing routines with a ‘GPU first’ policy, ie. we optimise for capabilities of a GPU. Nearly all set-top-box hardware currently produced contains a GPU within their System-on-a-chip architecture. Secondly we have optimised graphics routines for drawing different HTML/CSS elements using the GPU so that we don’t have to sacrifice any performance by going through a generic graphics API.

Let’s take drawing CSS borders as example. If we have the following border style on a div:

border: 10px dashed green;
border-colour: green red;

Then we would want the border to look something like the following:

To describe what we want to draw in words: Draw a rectangle outline with a line-width of 10 pixels which has a dash pattern along the line. The horizontal lines are green, the vertical lines are red, and the join between the 2 colours should be at 45 degrees. Also the gaps of the dashes should be centred along the line, and there should never be gaps at the corners.

Now a graphics library would never provide an API for doing exactly that, so instead the border would be drawn using a combination of APIs. A typical way to do this would be to draw the four lines separately, eg. stroking a line with a stroke-width of 10px, and an appropriate dash pattern. Then the corner joins would be drawn, perhaps by creating a vector paths of a triangles, and filling these in separately, or perhaps using a triangle path as a clipping mask.

In Flow we renderer this shape in one operation. Since the basic primitive of the GPU is a triangle we split this shape into triangles as shown below:

GPUs are also very efficient at rendering textures spread across triangles, so to produce the dash pattern we effectively stretch and repeat a 2×1 texture across the length of the lines. We use a ‘nearest’ texture filter instead of the typical ‘linear’ filter to give the dashes hard instead of soft edges.

This is an example of how we draw HTML/CSS specifically optimised for the GPU, and we do the same for all HTML/CSS elements. Nearly all of these operations can be batched together and sent to the GPU in one go, resulting in very fast rendering performance, and high frame rates.

BACK TO BLOG