DEV Community

David Rivera
David Rivera

Posted on

Leaving my comfort zone: Contributing to wgpu—A light overview of the Graphic API's

Over the last year, I've been widely immersed in the LLVM Project and some other projects derived from it (like IREE) that aim to deliver robust compiler infrastructure across multiple hardware and multiple domains. It has honestly been an amazing experience, especially given the technical skills it has provided me.

If you're interested in seeing my involvement, refer to my Github Profile. I have a portfolio that encompasses some projects aligned with my interests.

I've been very curious about GPUs these days—while I've been a consumer of these products, specifically on the chips oriented to gaming, I've grown curious about what the hype of AI is all revolving around. It is not a surprise that the discovery of the benefits of exploiting GPU parallelism on neural networks and AI in general has empowered the infrastructure of some of the most important companies like OpenAI or Anthropic.

This blog is not oriented towards my exploration of GPUs in that field (I plan to publish one soon after I finish my work on IREE). This time I'm heading to uncharted territory (at least until last weekend), which is the field of graphics APIs—specifically through a weekend contribution to the wgpu project.

More specifically, let's talk about wgpu—A WebGPU API implementation. one of the major ones, utilized in Firefox run by Mozilla. I'll cover things like what a basic overview of what a graphics api is and some of the core components I was able to identify in this project—WGSL and Naga. I might be missing some fundamentals here, but consider that this was merely a weekend project (my type of leisure).

A graphics API from 10000 feet

A graphics API mainly serves the purpose of serving as a bridge or interaction between an application's high-level rendering commands and the underlying graphics hardware (In most cases a GPU!).

By providing this set of functions, a graphics API simplifies the complexities of multiple hardware architectures, which allows developers to write portable code across multiple systems.

(In practice, this sounds not too different from compilers, specifically JIT compilers. at the end of the day what matters is that you're abstracting something down and making certain layers of your stack portable between targets — this is true in most cases).

Think about it this way: If we were to express rendering semantics in machine code directly from our app, it would be very tedious. Consider that we'd have to worry about the multiple architectures we'd have to target.

WebGPU and one of its implementations: wgpu

One of the major graphics API specifications is WebGPU. There's probably no better explanation than the one according to Mozilla:

The WebGPU API enables web developers to use the underlying system's GPU (Graphics Processing Unit) to carry out high-performance computations and draw complex images that can be rendered in the browser.

WebGPU is the successor to WebGL, providing better compatibility with modern GPUs, support for general-purpose GPU computations, faster operations, and access to more advanced GPU features.

While I could delve into more details on how it differs from the other major API's, For simplicity matters most of what I could find is that it provides better portability as it allows developers to run it on the browser through WebAssembly.

There are multiple implementations of the WebGPU API, most notably:

  • Dawn: Utilized in Chrome and Edge, written in C++
  • wgpu: Utilized in Firefox, written in Rust

Let's talk about wgpu, Shading languages (specifically WGSL, msl) and naga.

The Translation Layer: Where WGSL Meets Metal

When working with wgpu, you write your shaders in WGSL (WebGPU Shading Language), a modern, platform-agnostic shading language designed for the WebGPU specification. But here's the thing: your GPU doesn't understand WGSL directly. Each graphics backend—whether it's Metal on macOS/iOS, Vulkan on Linux/Windows, or DirectX on Windows—has its own native shading language.

This is where naga comes in. Naga is the shader translation library at the heart of wgpu, acting as a sophisticated compiler that bridges this gap. Think of it as a polyglot translator for GPU code.

How the Pipeline Works

The translation happens in several stages:

Your Application → WGSL Shader Code → Naga's Internal Representation (IR) → Backend-Specific Shading Language → Native GPU Compilation → GPU Execution

When you write a shader in WGSL and load it through wgpu's bindings, naga parses your shader code, validates it, converts it to an internal representation, and then translates it to the appropriate backend language. On macOS, that means translating WGSL to Metal Shading Language (MSL). On Linux with Vulkan, it's SPIR-V. On Windows with DirectX, it becomes HLSL.

The beauty of this architecture is that you, as a developer, never have to think about these backend differences. You write WGSL once, and naga handles the rest.

Inside the Translation: A Real Example

Let me show you what this translation looks like in practice, using a contribution I recently made to naga's Metal backend (PR #8432).

Consider a simple WGSL shader that computes a dot product on integer vectors:

let a: vec2<i32> = vec2<i32>(1, 2);
let b: vec2<i32> = vec2<i32>(3, 4);
let result: i32 = dot(a, b);
Enter fullscreen mode Exit fullscreen mode

In WGSL, the dot() function works uniformly across all vector types—floats, integers, you name it. But Metal Shading Language has a quirk: its built-in dot() function only works with floating-point vectors. For integer vectors, we need a different approach.

Previously, naga would inline the dot product calculation directly wherever it appeared:

int result = (+ a.x * b.x + a.y * b.y);
Enter fullscreen mode Exit fullscreen mode

This works, but if you use integer dot products multiple times in your shader, you get the same verbose expression repeated throughout your code. It's not ideal for code size or readability.

My PR changed this by introducing wrapper helper functions for integer dot products. Now, naga generates:

int naga_dot_int2(metal::int2 a, metal::int2 b) {
    return (a.x * b.x + a.y * b.y);
}

// Later in your shader:
int result = naga_dot_int2(a, b);
Enter fullscreen mode Exit fullscreen mode

The wrapper function is emitted once at the top of the generated Metal shader and reused throughout. For each concrete type combination (like int2, uint3, long4), naga generates a uniquely named helper function using a mangling scheme: naga_dot_{type}{size}.

Why This Matters

This might seem like a small optimization, but it illustrates a crucial point about how shader translation works in practice. Naga isn't just doing a mechanical find-and-replace translation. It's actively making decisions about how to best represent WGSL semantics in each target language, accounting for the quirks and capabilities of different backends.

These optimizations happen transparently. You never have to write different shader code for different platforms. You don't even have to know that Metal's dot() function has this limitation. Naga handles the complexity so you can focus on writing your graphics code.

This is the power of having a dedicated translation layer like naga. It's not just making wgpu cross-platform—it's making it intelligently cross-platform, generating efficient, idiomatic code for each backend while maintaining perfect semantic equivalence with the source WGSL.

Reflection

As I stated at first, I didn't expect to get into this rabbit hole (Yes, I'm very prone to getting into these), this project is the first time I'm exposed to writing Rust in production! And honestly, it wasn't as bad as I thought. By far the best thing Rust does in my opinion, is pattern matching, which is very important when writing compilers, since you may want to map different data structures/translations 1:1.

There may be more issues I'll definitely be looking forward to tackling in this project, as my experience was surprisingly smooth. In practical terms, build times were a lot smoother, cargo is perhaps the best in terms of developer tooling, and in general, I have to say the Rust ecosystem provides a ton of quality of Life as compared to C++.

Just to wrap up, it feels pretty good to contribute to these projects knowing that this technology has the potential to be utilized by millions of users—I believe I highlighted this on my first blog; however, I think that's certainly what I feel regarding most projects I get into. One thing is for sure, I won't stop exploring OSS projects in the short term—It's quite enriching to interact with different developers across different domains :).

Top comments (0)