I'm reading about WebAssembly, and I was curious about how to port graphics programming like Vulkan or OpenGL to a canvas element. The documentation is long, and I think I'll figure this out eventually, but I was curious and not successful in searching for the answer on the spot.
So far, I know it can export functions to JS, and JS will do the dirty job of manipulating the DOM as usual.
I could write WebGL directly, but that's not my point. I have seen games ported to WebAssembly, and I'd like to know how it works. How WebAssembly can render anything if it does not have direct access to the DOM? Typically, graphics applications use an external Window Manager like GLFW or SDL to create a Window context to draw your stuff.
If I compile a program using libraries that expect an environment with a Window object, how do these instructions map to the canvas if there's no such concept in the DOM? Do I need to adapt my C++ program somehow?
OK, it turns out the answer was on the same page I was reading, but a little bit down further.
The answer is that it's necessary to have a Javascript "glue" code to load .wasm and convert native library calls to the DOM context, but you do not need to write it by hand. People use Emscripten, which includes filesystem emulation, porting to the most popular media libraries like SDL, and it generates both .wasm code and its JS glue counterpart.
From the Mozilla WebAssembly page:
The Emscripten tool is able to take just about any C/C++ source code and compile it into a .wasm module, plus the necessary JavaScript "glue" code for loading and running the module, and an HTML document to display the results of the code.
In a nutshell, the process works as follows:
- Emscripten first feeds the C/C++ into clang+LLVM — a mature open-source C/C++ compiler toolchain, shipped as part of XCode on OSX for example.
- Emscripten transforms the compiled result of clang+LLVM into a .wasm binary.
- By itself, WebAssembly cannot currently directly access the DOM; it can only call JavaScript, passing in integer and floating point primitive data types. Thus, to access any Web API, WebAssembly needs to call out to JavaScript, which then makes the Web API call. Emscripten therefore creates the HTML and JavaScript glue code needed to achieve this.
Emscripten only supports fully portable C and C++ code, and you need to enable some optimizations according to their Portability Guidelines.
Also, you need to keep in mind the inherent constraints of the Web platform and JS runtime, so you do not have direct access to the filesystem and you cannot make synchronous (blocking) networking calls, as detailed in API Limitations page.
For OpenGL, specifically, as the code will eventually translate to a sandboxed WebGL context, you need to limit yourself to what WebGL offers (i.e OpenGL ES 2.0). According to Optimizing WebGL page, Emscripten will convert your code to WebGL 1 by default, with fewer capabilities and unfriendlier syntax, but supported by more platforms. However, you can also target WebGL 2, which offers a nicer API and a couple of hardware optimizations.
For Vulkan, there's currently no native support, but there is an ongoing discussion in W3C for publishing a WebGPU specification. This is their Github page with up-to-date information and the current browser support page.
There is also an experimental WebGPU Rust implementation by Mozilla, so we can already have a glimpse of the future.
Chrome has released WebGPU.
Mozilla's wgpu released its first major version, and I have seen it used in production in different contexts. It's not experimental anymore.
Another thing I can add to my original answer is that it's not exactly a WebGPU implementation as I originally described; it's a full-blown hardware abstraction layer. Mozilla uses it behind Firefox's WebGPU implementation, but wgpu can be deployed anywhere, for example, in image editors or game engines. You can use it to abstract and target any native GPU API: Vulkan, Metal, DirectX, WebGPU, WebGL, OpenGL. It works for Mobile, Desktop and, of course, the Web.