WASM!
I never thought I would ever target web, but here we are: the demo scene working on web! Check it out here.
I was inspired by Michael Tuttle’s article about pixel buffer rendering, and since Diligent Engine had just added a WebGPU backend, I decided to try it out. There were a few challenges to get it to work, but there were fewer than expected.
I figured my first priority was figuring out how to compile the project to WASM. I searched around and got started with setting Emscripten on my system. It was straightforward to set up on my Mac. I needed to get the Nim compiler to use this, and a quick search took me to treeform’s emscripten tutorial. To my pleasant surprise, it also had a nice tutorial about integrating GLFW, which is exactly what I use with Vulkan. Did you know that Emscripten implements several libraries and all you have to do is link to them at build time? Well I didn’t, and I was stoked to find out.
With that out of the way, the next step was compiling Diligent to WASM. I was confused about how libraries worked in WASM, so I floundered a bit here. Do libraries (*.a
, *.dylib
) exist in WASM in the first place? How do I link to them? It turned out that WASM libraries do exist, and I simply needed to copy the produced *.a
files from Diligent’s build folder.
Eventually I got the engine to compile, so I proceeded to run it. Diligent is able to convert HLSL to WGSL internally for the WebGPU backend, so I hoped that things would work out. Unfortunately, things weren’t that simple. First of all, I was hitting WebGPU limits, like the number of dynamic buffers that can be attached to a pipeline. And the size of these dynamic buffers. And unified mapped buffers don’t exist in WebGPU…
Simple solution? Comment out everything that don’t immediately work, and compile again. And guess what…
Tint couldn’t translate some of the HLSL code I was using! The builtin shader translation pipeline goes something like this:
Nim >-[Nim shader transpiler]-> HLSL >-[SPIRV-cross]-> SPIRV >-[tint]-> WGSL
After days of debugging, I found that there is a fundamental difference between how atomics are handled in HLSL vs WGSL, and tint
was having trouble translating between them. Fortunately Diligent allows providing SPIRV and WGSL directly to its backends, so I decided to try generating the WGSL at build time and passing it to Diligent. The new shader translation pipeline then looked like:
Nim >-[Nim shader transpiler]-> HLSL >-[DirectXCompiler]-> SPIRV >-[naga]-> WGSL
With this setup, I generate SPIRV from HLSL using the dxc
tool that comes bundled with the Vulkan SDK (on all platforms!), and then pass that SPIRV to naga-cli which is part of the WebGPU repository. I packaged all this up in a nim gen shader
command which I would run before building the main project.
I had to make a few adjustments the shaders, like enabling runtime-sized resource arrays, using texture arrays instead of arrays of textures in WebGPU, and merging dynamic buffers to overcome size limits. There are still a few missing features, but the basic functionality is there.