For web developers, WebGPU is a web graphics API that provides a unified and fast access to GPUs by exposing modern hardware capabilities and allowing rendering and computation operations on a GPU, similar to Direct3D 12, Metal, and Vulkan.
While it’s true, that story is not complete. WebGPU is first and foremost the result of a collaborative effort including major companies such as Apple, Google, Intel, Mozilla, and Microsoft. Among them, some realized that WebGPU could be more than a Javascript API but a cross-platform graphics API that could be used by developers across ecosystems other than the web. To fulfill the primary goal, a JavaScript API was introduced in Chrome 113. However, another significant project has been developed alongside it: the webgpu.h C API. This C header file lists all the available procedures and data structures of WebGPU. It serves as a platform-agnostic hardware abstraction layer, allowing you to build platform-specific applications by providing a consistent interface across different platforms.
In this article, you will learn how to write a simple C++ app using WebGPU that runs both on the web and specific platforms. Spoiler alert, you’ll get the same red triangle that appears in a browser window and a desktop window with minimal adjustments to your codebase.

How does it work?
To see the completed application check out the WebGPU cross-platform app repository.
The app is a minimalistic C++ example that shows how to use WebGPU to build desktop and web apps from a single codebase. Under the hood, it uses WebGPU’s webgpu.h as a platform-agnostic hardware abstraction layer through a C++ wrapper called webgpu_cpp.h.
The webgpu.h and webgpu_cpp.h APIs are not yet stabilized.
On the web, the app is built against Emscripten, which has bindings implementing webgpu.h on top of the JavaScript API. On specific platforms such as macOS or Windows, this project can be built against Dawn, Chromium’s cross-platform WebGPU implementation. It’s worth mentioning wgpu-native, a Rust implementation of webgpu.h, also exists but is not used in this article.
Getting started
All you need to start is a C++ compiler and CMake to handle cross-platform builds in a standard way. Then, in a dedicated folder, create a main.cpp
source file and a CMakeLists.txt
build file.
The main.cpp
file contains an empty main()
function for now.
int main() {}
The CMakeLists.txt
file contains basic information about the project. The last line specifies the executable name is “app” and its source code is main.cpp
.
cmake_minimum_required(VERSION 3.13)
project(app)
set(CMAKE_CXX_STANDARD 20) add_executable(app "main.cpp")
Run cmake -B build
to create build files in a “build/” sub folder and cmake --build build
to actually build the app and generate the executable file.
$ cmake -B build && cmake --build build
$ ./build/app
The app runs but there’s no output yet, as you need a way to draw things on the screen.
Get Dawn
To draw your triangle, you can take advantage of Dawn, Chromium’s cross-platform WebGPU implementation. This includes GLFW C++ library for drawing to the screen. One way to download Dawn is to add it as a git submodule to your repository. The commands below will fetch it in a “dawn/” sub folder.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Then, append to the CMakeLists.txt
file as follows:
- The CMake
DAWN_FETCH_DEPENDENCIES
option will fetch all Dawn dependencies. - The
dawn/
sub folder will be included in the target. - Your app will depend on
webgpu_dawn
,webgpu_cpp
, andwebgpu_glfw
targets so that you can use them in themain.cpp
file later.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_dawn webgpu_cpp webgpu_glfw)
Open a window
Now that Dawn is available, use GLFW to draw things on the screen. This library included in webgpu_glfw
for convenience, allows you to write code that is platform-agnostic for window management.
To open a window named “WebGPU window” with a resolution of 512×512, update the main.cpp
file as below. Note that glfwWindowHint()
is used here to request no particular graphics API initialization.
#include const uint32_t kWidth = 512;
const uint32_t kHeight = 512;
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
int main() {
Start();
}
Rebuilding the app and running it as before now results in an empty window. You’re making progress!

Get GPU device
In JavaScript, navigator.gpu
is your entrypoint for accessing the GPU. In C++, you need to manually create a wgpu::Instance
variable that’s used for the same purpose. For convenience, declare instance
at the top of the main.cpp
file and call wgpu::CreateInstance()
inside main()
.
…
#include wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
Accessing the GPU is asynchronous due to the shape of the JavaScript API. In C++, create a helper GetDevice()
function that takes a callback function argument and calls it with the resulting wgpu::Device
.
void GetDevice(void (*callback)(wgpu::Device)) {
instance.RequestAdapter(
nullptr,
[](WGPURequestAdapterStatus status, WGPUAdapter cAdapter,
const char* message, void* userdata) {
if (status != WGPURequestAdapterStatus_Success) {
exit(0);
}
wgpu::Adapter adapter = wgpu::Adapter::Acquire(cAdapter);
adapter.RequestDevice(
nullptr,
[](WGPURequestDeviceStatus status, WGPUDevice cDevice,
const char* message, void* userdata) {
wgpu::Device device = wgpu::Device::Acquire(cDevice);
reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
},
userdata);
},
reinterpret_cast<void*>(callback));
}
For easier access, declare a wgpu::Device
variable at the top of the main.cpp
file and update the main()
function to call GetDevice()
and assign its result callback to device
before calling Start()
.
wgpu::Device device;
…int main() {
instance = wgpu::CreateInstance();
GetDevice([](wgpu::Device dev) {
device = dev;
Start();
});
}
Draw a triangle
The swap chain is not exposed in the JavaScript API as the browser takes care of it. In C++, you need to create it manually. Once again, for convenience, declare a wgpu::SwapChain
variable at the top of the main.cpp
file. Just after creating the GLFW window in Start()
, call the handy wgpu::glfw::CreateSurfaceForWindow()
function to create a wgpu::Surface
(similar to an HTML canvas) and use it to setup the swap chain by calling the new helper SetupSwapChain()
function in InitGraphics()
. You also need to call swapChain.Present()
to present the next texture in the while loop. This has no visible effect as there is no rendering happening yet.
#include
…wgpu::SwapChain swapChain;
void SetupSwapChain(wgpu::Surface surface) {
wgpu::SwapChainDescriptor scDesc{
.usage = wgpu::TextureUsage::RenderAttachment,
.format = wgpu::TextureFormat::BGRA8Unorm,
.width = kWidth,
.height = kHeight,
.presentMode = wgpu::PresentMode::Fifo};
swapChain = device.CreateSwapChain(surface, &scDesc);
}
void InitGraphics(wgpu::Surface surface) {
SetupSwapChain(surface);
}
void Render() {
}
void Start() {
…
wgpu::Surface surface =
wgpu::glfw::CreateSurfaceForWindow(instance, window);
InitGraphics(surface);
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
Render();
swapChain.Present();
}
}
Now is a good time to create the render pipeline with the code below. For easier access, declare a wgpu::RenderPipeline
variable at the top of the main.cpp
file and call the helper function CreateRenderPipeline()
in InitGraphics()
.
wgpu::RenderPipeline pipeline;
…const char shaderCode[] = R"(
@vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
@builtin(position) vec4f {
const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
return vec4f(pos[i], 0, 1);
}