
Managing asynchronous tasks in long-lived single page applications

Motiff's rendering engine now supports WebGPU and is currently in the phase of greyscale release.
Over several months, we refactored the core code of the rendering engine to support WebGPU. This allows the rendering engine to take full advantage of the GPU's capabilities and improve its rendering performance.
WebGPU delivers a 20% increase in average rendering performance compared to WebGL, and up to 400% on some Intel integrated graphics cards. This improvement is not only due to the efficient API but also benefits from the adjustments we've made to the rendering structure.
Whenever you see content changing on the screen, the rendering engine is at work. Its main task is to convert layer data into a sequence of rendering commands and then send these commands to the GPU to draw the final content.
At the beginning, Motiff's rendering engine was based on Skia, a graphics library that provided a simple and easy-to-use interface. Skia was very convenient for early development, but its high-level abstraction made it difficult to optimize for specific business logic, resulting in subpar performance. As Motiff’s business iterated, performance bottlenecks emerged, leading us to migrate the engine to WebGL. WebGL is a lower-level graphics API that allows for more direct interaction with the GPU, which significantly improving rendering performance.
Today, WebGPU has further empowered Motiff’s rendering engine. By offering deeper hardware access and enhanced optimization capabilities, WebGPU allows us to better leverage the GPU’s computational power, driving even greater performance improvements and delivering a smoother user experience.
Now, WebGPU provides even stronger support and optimization opportunities for Motiff's rendering engine, helping us better utilize the computational power of the hardware. It drives even greater performance improvements and delivers a smoother user experience.
WebGPU is the successor to WebGL, providing developers with more powerful and flexible graphics processing capabilities while reducing development difficulty.
The advantages of WebGPU are mainly reflected in the following four aspects:
The first challenge in supporting WebGPU was recognizing that we cannot support WebGPU alone.
Since WebGPU remains experimental in most browsers (only the Chrome browser supports WebGPU in its stable version), we cannot rely solely on WebGPU to implement the rendering engine. Therefore, simultaneously supporting both WebGL and WebGPU became a necessity. Before the refactoring, our rendering engine was entirely based on WebGL. Should we write two separate rendering engines? And how would business codes interact with them?
The answer is the rendering abstraction layer.
The rendering abstraction layer shields the underlying rendering interfaces, smooths out the differences between WebGL and WebGPU, and provides a unified, relatively low-level interface. The business doesn't need to check whether WebGL or WebGPU is in use. It only cares about what triangles to draw and where the vertex data is stored, and passes these as parameters to call the interfaces provided by the rendering abstraction layer.
Indeed, WebGL and WebGPU still have functional differences. They have different extensions on different hardware, operating systems, and browsers.Different hardware, operating systems, and browsers also have different extensions. To maximize the use of new features for performance improvement, we need to write business code for scenarios with and without those features. We designed a set of boolean values to describe the current support for these features. It is worth noting that whether a feature is supported is not related to whether the current rendering backend is WebGL or WebGPU. For example, the WebGL extension WEBGL_blend_func_extended and the WebGPU extension dual-source-blending essentially represent the same functionality and can be indicated by the same boolean value. On the other hand, compute shaders are only supported by WebGPU, so the corresponding boolean value will always be false under WebGL.
The rendering engine implements the rendering abstraction layer with both WebGL and WebGPU. We also added a version of dummy implementation to speed up testing that doesn't require rendering capabilities.
The challenages of rendering abstraction layer lies in the differences between WebGPU and WebGL. The most obvious problem is the different shader languages. WebGL uses GLSL (OpenGL Shading Language), whereas WebGPU uses WGSL (WebGPU Shading Language). The best solution to ensure consistency would be to write a single set of code that can be used by both WebGL and WebGPU. For example, designing an entirely new shader language, MTSL (Motiff Shading Language), which could be converted to both GLSL and WGSL. However, this solution is quite costly, as it requires language design, lexical and syntactic analysis, target code generation, and a series of IDE support tools.
We adopted a simpler solution by writing two sets of shader codes with only slight differences. For instance, the GLSL code to render a solid color is:
And the WGSL code is:
In both sets of shader code, we use the same vertex data structure, the same uniform data structure, and the same computation process to ensure consistent rendering results at the code level. Additionally, for each test that needs to validate rendering results, we execute it with both WebGL and WebGPU to ensure rendering consistency at the testing level.
Secondly, the coordinate system directions of WebGPU and WebGL are different. Motiff's rendering engine involves three coordinate systems in the rendering interface: NDC (Normalized Device Coordinates), Framebuffer coordinates, and Texture coordinates. These coordinate systems are concepts in the underlying rendering interface, not in the business side.
The NDC can be simply seen as the coordinate system where the output of the vertex shader resides. The Framebuffer coordinate system is used by viewport, scissor, and readPixel operations. The Texture coordinate system, commonly referred to as the UV coordinate system, is used for texture sampling in the fragment shader.
The coordinate system directions in WebGL and WebGPU differ. Since Motiff only involves 2D rendering, we do not consider the direction of the z-axis. Apart from that, the y-axis orientation varies. In WebGL, the y-axis of all coordinate systems points upward, whereas in WebGPU, the y-axis of the Framebuffer and Viewport points downward. How should we choose the y-axis direction in the business scenarios?
Regardless of the choice, performance should not be affected. The functionality most likely to be affected is the uploading of texture data. In WebGL, after uploading Texture data, the y-axis is flipped, while in WebGPU, it remains normal. In WebGL, you can use gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL)
to flip the y-axis again and turn the result to normal.
However, this feature exists only in WebGL and not in OpenGL, and Motiff also has rendering requirements on the server side, so this capability cannot be used. To avoid additional processing on the uploaded Texture data, we chose to flip the y-axis of the Texture content in WebGL and keep it normal in WebGPU. This choice also aligns with the different viewport coordinate systems of the two. Since the content of the Framebuffer will also be used as Texture for re-sampling, the rendered content must match the Texture's orientation.
This choice introduces a problem: content rendered in WebGL appears with a flipped y-axis. As previously explained, this occurs because the rendered output must align with the texture’s orientation—requiring WebGL’s y-axis inversion. While the simplest fix (rotating the user’s monitor 180 degrees) is obviously impractical, two viable solutions are listed in the following:
Motiff chose solution 2 because these special logics can be written into the underlying WebGL implementation of the rendering abstraction layer. Solution 1 requires the business side to determine whether the current rendering backend is WebGL, and also requires an extra Framebuffer, which could be a significant amount of cost on low-end mobile devices.
Although there are differences in the design models of WebGL and WebGPU, they still share a substantial amount of functionality. Designing an interface that is compatible with both is not overly complex. We based our overall interface design on WebGPU, referencing existing business logic to design the interface of rendering abstraction layer.
For example, WebGPU has a type named RenderPipeline, while WebGL uses a bunch of global variables to achieve the same functionality. In our design, we used a data structure similar to RenderPipeline to store all the states required for a single rendering operation.
Additionally, WebGL has a Framebuffer type, but WebGPU doesn't have a corresponding type. Instead, it achieves the same functionality by passing a combination of multiple Textures as parameters when constructing the RenderPass. In our design, we retained the Framebuffer concept to simplify the implementation on the WebGL side.
The most significant benefit brought by WebGPU is undoubtedly the performance improvement. However, WebGPU is not just a new API. It is an API designed for modern GPUs. It allows us to view rendering architecture design from the perspective of modern GPUs. How can we reduce the number of RenderPassEncoders? How can we convert LoadOp from Load to Clear? Are these optimizations always beneficial in every scenario? These questions provide direction for the future iterations of our rendering engine. Current experimental data has also shown that these directions are worth further exploration.
Indeed, WebGPU is still considered as new. Many features we look forward to are still in the proposal stage, such as push-constants and subgroups. Still, its greatest value lies in moving our focus away from interfaces that were born over 30 years ago to a more modern API on the web platform.