WebVR Coming to Servo: Part 1

We want VR to be a first-class citizen in the Web ecosystem. For that we need a fast and flexible runtime that will allow us to build all the forward-looking ideas we’re cooking. Servo is a modern high-performance browser engine designed for both application and embedded uses developed from the scratch at Mozilla. The technical goals, community, and culture around the project resonated with us, and we decided to roll up our sleeves and bring WebVR to Servo, in parallel to our efforts on Gecko (Firefox).

Imanol Fernandez first wrote rust-webvr, an implementation of the WebVR APIs in Rust that is separate from the browser. It that can be tested and used independently. You can, for example, develop a native Rust OpenGL application that talks to the same WebVR APIs that Web browsers will use to interface with a VR headset. The next milestone will be to integrate the module into Servo’s architecture, but initially we wanted to see something on screen.

Valve and Oculus' SDKs are currently unavailable on Linux and Mac OS X, making today’s desktop VR a Windows-only business. In order to push some 3D content on screen, Imanol started a Windows backend to the WebGL implementation in Servo. We decided to keep it simple and started with WGL since there are other components that use it already. In the future, we want to also provide an Angle-based backend to be able to pick the best performing option for each GPU. We must nail the latency and framerate required by VR applications. And for that we need a solid understanding of what happens since JS invokes a WebGL function until a pixel shows on screen. These are the different parts of Servo involved in a WebGL call:
Diagram of WebGL calls in Servo

JavaScript evaluation and IDL bindings

On Servo, JavaScript runs on moz-js, a fork of the SpiderMonkey engine. Like V8 or JavaScriptCore, it provides only core JS evaluation capabilities. In order to expose additional APIs such as WebGL, we need to be able to access native classes and functions from the JS context. This is done using the C/C++ API that comes with each Virtual Machine (VM) engine, which lets you define JS classes and functions that can call native code and vice versa.

Exposing APIs to JS requires writing a lot of boilerplate code, such as checking parameter types or passing data back and forth from C++ world to the JS context. On Servo, we also have to often write bindings between C/C++ and Rust rust-bindgen generates Rust binding code automatically from a language-agnostic API specification called WebIDL. This takes care of all the boilerplate code and lets us focus on the logic specific to the API we want to expose to JS.

Servo WebGL IDL and other APIs' IDLs can be found here.

WebGL DOM implementation

WebIDL bindings provide all the Rust-JavaScript bridge boilerplate code. We now have to write the implementation itself of each function/class, or we’ll have some missing function compilation errors. The main WebGLRenderingContext DOM object is implemented here. Other WebGL classes, such as WebGLProgram and WebGLShader, can be found in the same path.

These DOM objects take care of the WebGL objects’ status and all the validation rules defined by the WebGL spec. However, all the rendering commands are delegated to another component, the WebGL thread.

WebGL thread

Servo has a parallel-process architecture. As other Servo components, WebGL's render commands are executed independently of all the other DOM rendering and JS evaluation threads. This helps to improve performance because JS threads don’t get blocked when submitting GL render calls to the GPU driver. This is not always true, if a user calls a function (e.g., glReadPixels or glGetParameter) the JS thread blocks until it receives the response from the WebGL thread, but this shouldn’t be a problem for a well-optimized WebGL app.

The Servo WebGL thread implementation receives and handles WebGL messages from the DOM. It currently has two different render paths depending on how Servo is initialized:

  1. Webrender: The WebGL thread dispatches the GL function calls to the Webrenderer compositor thread. It has been recently made the default render path in Servo.
  2. rust-azure: graphics abstraction layer, which was the former default render path in Servo. It uses Skia graphics library internally and runs the GL function calls in the current thread.

Webrender has the most optimized WebGL render path by using shared OpenGL textures for composition. On the other hand, the Skia compositor has an unoptimized WebGL path because the sharing is done using readPixels.

Inter process communication in Servo and Rust

The WebGL DOM implementation dispatches GL Render messages to a different thread. Instead of using the asynchronous channels for communication between threads from the Rust standard library, Servo uses ipc-channel with a custom implementation with multi-processing improvements.

Combining channels with the powerful Rust enum type variants makes interprocess communication very elegant. All the WebGL commands used by the WebGL thread are implemented in the webrender_traits component, which is shared by both WebRender and Servo.

The real OpenGL call

The Webrender channel receives the GL commands and performs the real OpenGL calls. This may change in the future because the Webrender team wants to implement a GL command buffering mechanism. Webrender uses a single GPU process shared among all the WebGL contexts.

There is one final step before submitting the real GL call. The actual function invoked depends on the platform. It can be a real OpenGL driver call or a call to a OpenGL wrapper like Angle (for using a DirectX backend) or OSMesa. On some platforms like Windows you can even load different OpenGL implementation dlls at runtime: the ones provided by the OS or optimized dlls that came with the GPU drivers.

In order to allow any of these wrappers to work, OpenGL symbols are loaded dynamically. Servo uses gleam library to share the same OpenGL symbols among all the components. Gleam internally uses gl_generator, a common utility in the Rust community that provides OpenGL function pointer loader and bindings.

After all this path the real OpenGL call hits the driver (finally!)

WebGL Accelerated Compositing in Servo

We have talked about WebIDL bindings, DOM implementation, multithreading WebGL commands and the real GL calls. Now it’s time to show how a WebGL canvas is composited into the main window render context.

In Servo the main window is created using GLutin. This library eases the process of creating a cross platform window, main render context and receiving input and events.

WebGL contexts are implemented as headless offscreen contexts. All the GL commands render to a texture using a FBO with texture attachments. WebGL render commands are multiplatform, but the headless GL context creation is not. In order to hide the API differences between platforms, Servo uses rust-offscreen-rendering-context. This is a library that provides a headless context creation abstraction and supports a variety of APIs like EGL, CGL, WGL, OSMesa and more.

The Servo compositor is the component that controls the rendering of all the elements of a web page. It relies on Webrender to do all the GPU rendering work. A WebGLContext has its own rendering context, but it can be shared with other compositor render layers efficiently thanks to shared textures.