2D Web Rendering with Rust
By: Tom Lagier
Tags: JavaScript, Games, Rust, Graphics, Webassembly

The “JS renderer” solution was extremely fast — each frame took <1ms to draw. This is the solution shown in the rust-wasm book, storing the game state in WebAssembly memory and doing all drawing via normal Canvas2DRenderingContext calls. This leverages the web platform for native APIs, and Rust for fast game state management, and should be seen as the benchmark to beat.

2D drawing libraries

First, I used the tiny-skia library to write an all-Rust renderer. tiny-skia is a small Rust port of the Skia 2D graphics library with a nice ergonomic API. It would allow me to manage a pixel buffer and share it with JS. It was also the slowest option I tried.

I took another stab with the piet-web library, which also has a very ergonomic API. Unlike tiny-skia , it renders its output directly to the web platform by generating Canvas2DRenderingContext calls via web-sys . So, we’re pretty much writing JS from Rust with this option. It ended up being quite fast and presents an attractive alternative to pure-JS work.

However, the overhead of needing to pass function arguments across the WASM boundary, plus the additional overhead of an intermediate framework on top of the canvas means that it will never be as fast as pure JS CanvasRenderingContext2D calls.

After trying a few Rust drawing libraries, I decided to see what it would be like to hand-roll a renderer. Conway’s game of life is pretty simple, just lines and squares, so manually managing a pixel buffer should be straightforward, and it would let me keep all of the rendering logic in Rust. The performance of my home-made renderer was good, but not as good as the JS renderer and on par with piet .

Could WebGL be faster?

After looking at 2D options, I decided to try my hand at the 3D ones. winit and wgpu are both WASM- and WebGL-friendly, and they allow us to render to a WebGLRenderingContext in the DOM. Rendering 2D graphics on the GPU seems sensible, and there were a handful of Rust libraries that could do the translation — I investigated lyon , the piston2d game engine, and pathfinder .

This ended up being an enormous time sink. Getting Rust 3D libraries to play nicely with the browser is not a trivial task. While winit and wgpu might work well with wasm-bindgen , the support from other libraries was just not quite there.

I tried both piston2d and lyon and didn’t have luck with either. There have been WASM proof of concepts with both, but always using emscripten rather than wasm-bindgen . Emscripten is an incredible tool, but I wanted to try and get things working with wasm-bindgen for this project.

wasm-bindgen vs emscripten

The major difference between the two tools is that emscripten provides the standard library, and does a lot of heavy lifting to port functionality between native code and the web — it will handle the bindings for TCP, file IO, multithreading, and OpenGL. So, we can use it to compile native applications for the web but the framework is doing a lot of heavy lifting to translate native APIs into web ones.

wasm-bindgen , on the other hand, is much more lightweight and requires the applications themselves to define how they bind to the web platform. This produces smaller, more purpose-built WASM at the expense of broad compatibility with native code.