C++ wrapper 🟒¢

Resulting code: step028

So far we have used the raw WebGPU API, which is a C API, and this prevented us from using some nice productive features of C++. This chapter first gives imaginary examples of how this could be improved, before linking to a little library that implements all these features in an alternate webgpu.hpp header that replaces webgpu.h.

The remainder of the guide always provides snippets with both this C++ wrapper (using webgpu.hpp) and the β€œvanilla” C API (using webgpu.h).

Important

All the changes presented here only affect the coding time, but our shallow C++ wrapper leads to the very same runtime binaries.

Note

Dawn and emscripten provide their own C++ wrapper. They follow a similar spirit as the one I introduce here, only I needed our wrapper to be valid for all possible implementations, including wgpu-native.

Caution

This chapter is not as up to date as the readme of WebGPU-C++. I recommend you read that instead for now.

NamespaceΒΆ

The C interface could not make use of namespaces, since they only exist in C++, so you may have noticed that every single function starts with wgpu and every single structure starts with WGPU. A more C++ idiomatic way of doing this is to enclose all these functions into a namespace.

// Using Vanilla webgpu.h
WGPUInstanceDescriptor desc = {};
WGPUInstance instance = wgpuCreateInstance(&desc);

becomes with namespaces:

// Using C++ webgpu.hpp
wgpu::InstanceDescriptor desc = {};
wgpu::Instance instance = wgpu::createInstance(&desc);

And of course you can start your source file with using namespace wgpu; to avoid spelling out wgpu:: everywhere. Coupled with default descriptor, this leads to simply:

using namespace wgpu;
Instance instance = createInstance();

ObjectsΒΆ

Beyond namespace, most functions are also prefixed by the type of their first argument, for instance:

WGPUBuffer wgpuDeviceCreateBuffer(WGPUDevice device, WGPUBufferDescriptor const * descriptor);
               ^^^^^^             ^^^^^^^^^^^^^^^^^
size_t wgpuAdapterEnumerateFeatures(WGPUAdapter adapter, WGPUFeatureName * features);
           ^^^^^^^                  ^^^^^^^^^^^^^^^^^^^
void wgpuBufferDestroy(WGPUBuffer buffer);
         ^^^^^^        ^^^^^^^^^^^^^^^^^

These functions are conceptually methods of the object constituted by their first argument. Once again, C does not have built-in support for methods but C++ does, so in the wrapper we expose these WebGPU functions as follows:

namespace wgpu {
    struct Device {
        // [...]
        createBuffer(BufferDescriptor const * descriptor = nullptr) const;
    };

    struct Adapter {
        // [...]
        enumerateFeatures(WGPUFeatureName * features) const;
    };

    struct Device {
        // [...]
        destroy();
    };
} // namespace wgpu

Note

The const qualifier is specified for some methods. This is extra information provided by the wrapper to reduce the potential programming mistakes.

This greatly reduces visual clutter when calling such methods:

// Using Vanilla webgpu.h
WGPUAdapter adapter = /* [...] */
size_t n = wgpuAdapterEnumerateFeatures(adapter, nullptr);

becomes with namespaces:

// Using C++ webgpu.hpp
Adapter adapter = /* [...] */
size_t n = adapter.enumerateFeatures(nullptr);

Scoped enumerationsΒΆ

Because enums are unscoped by default, the C API is forced to prefix all values that an enumeration can take with the name of the enum, leading to quite long names:

typedef enum WGPURequestAdapterStatus {
    WGPURequestAdapterStatus_Success = 0x00000000,
    WGPURequestAdapterStatus_Unavailable = 0x00000001,
    WGPURequestAdapterStatus_Error = 0x00000002,
    WGPURequestAdapterStatus_Unknown = 0x00000003,
    WGPURequestAdapterStatus_Force32 = 0x7FFFFFFF
} WGPURequestAdapterStatus;

It is possible in C++ to define scoped enums, which are strongly typed and can only be accessed through the name, for instance this scoped enum:

enum class RequestAdapterStatus {
    Success = 0x00000000,
    Unavailable = 0x00000001,
    Error = 0x00000002,
    Unknown = 0x00000003,
    Force32 = 0x7FFFFFFF
};

This can be used as follows:

wgpu::RequestAdapterStatus::Success;

Note

The actual implementation use a little trickery so that enum names are scoped, but implicitly converted to and from the original WebGPU enum values.

Default descriptor valuesΒΆ

Sometimes we just need to build a descriptor by default. More generally, we rarely need to have all the fields of the descriptor deviate from the default, so we could benefit from the possibility to have a default constructor for descriptors.

Capturing closuresΒΆ

Many asynchronous operations use callbacks. In order to provide some context to the callback’s body, there is always a void *userdata argument passed around. This can be alleviated in C++ by using capturing closures.

Important

This only alleviates the notations, but technically mechanism very similar to the user data pointer is automatically implemented when creating a capturing lambda.

// C style
struct Context {
    WGPUBuffer buffer;
};
auto onBufferMapped = [](WGPUBufferMapAsyncStatus status, void* pUserData) {
    Context* context = reinterpret_cast<Context*>(pUserData);
    std::cout << "Buffer mapped with status " << status << std::endl;
    unsigned char* bufferData = (unsigned char*)wgpuBufferGetMappedRange(context->buffer, 0, 16);
    std::cout << "bufferData[0] = " << (int)bufferData[0] << std::endl;
    wgpuBufferUnmap(context->buffer);
};
Context context;
wgpuBufferMapAsync(buffer, WGPUMapMode_Read, 0, 16, onBufferMapped, (void*)&context);

becomes

// C++ style
buffer.mapAsync(buffer, [&context](wgpu::BufferMapAsyncStatus status) {
    std::cout << "Buffer mapped with status " << status << std::endl;
    unsigned char* bufferData = (unsigned char*)context.buffer.getMappedRange(0, 16);
    std::cout << "bufferData[0] = " << (int)bufferData[0] << std::endl;
    context.buffer.unmap();
});

LibraryΒΆ

The library providing these C++ idioms is webgpu.hpp. It is made of a single header file, which you just have to copy in your source tree. Exactly one of your source files must define WEBGPU_CPP_IMPLEMENTATION before #include "webgpu.hpp":

#define WEBGPU_CPP_IMPLEMENTATION
#include <webgpu/webgpu.hpp>

Note

This header is actually included in the WebGPU zip I provided you earlier, at the include path <webgpu/webgpu.hpp>.

More information can be found in the webgpu-cpp repository.

Resulting code: step028

#define WEBGPU_CPP_IMPLEMENTATION
#include <webgpu/webgpu.hpp>

#include <GLFW/glfw3.h>
#include <glfw3webgpu.h>

#ifdef __EMSCRIPTEN__
#  include <emscripten.h>
#endif // __EMSCRIPTEN__

#include <iostream>
#include <cassert>
#include <vector>

using namespace wgpu;
GLFWwindow *window;
Device device;
Queue queue;
Surface surface;
std::unique_ptr<ErrorCallback> uncapturedErrorCallbackHandle;
{{Open window and get adapter}}

{{Request device}}
queue = device.getQueue();

{{Add device error callback}}

{{Surface Configuration}}

// We no longer need to access the adapter
adapter.release();
// Open window
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(640, 480, "Learn WebGPU", nullptr, nullptr);

// Create instance
Instance instance = wgpuCreateInstance(nullptr);

// Get adapter
std::cout << "Requesting adapter..." << std::endl;
{{Request adapter}}
std::cout << "Got adapter: " << adapter << std::endl;

// We no longer need to access the instance
instance.release();
// Device error callback
uncapturedErrorCallbackHandle = device.setUncapturedErrorCallback([](ErrorType type, char const* message) {
    std::cout << "Uncaptured device error: type " << type;
    if (message) std::cout << " (" << message << ")";
    std::cout << std::endl;
});
// Get device
std::cout << "Requesting device..." << std::endl;
DeviceDescriptor deviceDesc = {};
{{Build device descriptor}}
device = adapter.requestDevice(deviceDesc);
std::cout << "Got device: " << device << std::endl;
deviceDesc.label = "My Device";
deviceDesc.requiredFeatureCount = 0;
deviceDesc.requiredLimits = nullptr;
deviceDesc.defaultQueue.nextInChain = nullptr;
deviceDesc.defaultQueue.label = "The default queue";
deviceDesc.deviceLostCallback = [](WGPUDeviceLostReason reason, char const* message, void* /* pUserData */) {
    std::cout << "Device lost: reason " << reason;
    if (message) std::cout << " (" << message << ")";
    std::cout << std::endl;
};
{{Get the surface}}

RequestAdapterOptions adapterOpts = {};
adapterOpts.compatibleSurface = surface;

Adapter adapter = instance.requestAdapter(adapterOpts);
SurfaceConfiguration config = {};

{{Describe Surface Configuration}}

surface.configure(config);
// Configuration of the textures created for the underlying swap chain
config.width = 640;
config.height = 480;
{{Describe Surface Usage}}
{{Describe Surface Format}}
config.device = device;
config.presentMode = PresentMode::Fifo;
config.alphaMode = CompositeAlphaMode::Auto;
TextureFormat surfaceFormat = surface.getPreferredFormat(adapter);
config.format = surfaceFormat;
// And we do not need any particular view format:
config.viewFormatCount = 0;
config.viewFormats = nullptr;
config.usage = TextureUsage::RenderAttachment;
config.device = device;
surface.unconfigure();
queue.release();
surface.release();
device.release();
glfwDestroyWindow(window);
glfwTerminate();
// Get the next target texture view
TextureView targetView = GetNextSurfaceTextureView();
if (!targetView) return;
TextureView Application::GetNextSurfaceTextureView() {
    {{Get the next surface texture}}
    {{Create surface texture view}}
    {{Release the texture}}
    return targetView;
}
private:
    TextureView GetNextSurfaceTextureView();
SurfaceTexture surfaceTexture;
surface.getCurrentTexture(&surfaceTexture);
Texture texture = surfaceTexture.texture;
if (surfaceTexture.status != SurfaceGetCurrentTextureStatus::Success) {
    return nullptr;
}
TextureViewDescriptor viewDescriptor;
viewDescriptor.label = "Surface texture view";
viewDescriptor.format = texture.getFormat();
viewDescriptor.dimension = TextureViewDimension::_2D;
viewDescriptor.baseMipLevel = 0;
viewDescriptor.mipLevelCount = 1;
viewDescriptor.baseArrayLayer = 0;
viewDescriptor.arrayLayerCount = 1;
viewDescriptor.aspect = TextureAspect::All;
TextureView targetView = texture.createView(viewDescriptor);
#ifndef WEBGPU_BACKEND_WGPU
    // We no longer need the texture, only its view
    // (NB: with wgpu-native, surface textures must not be manually released)
    Texture(surfaceTexture.texture).release();
#endif // WEBGPU_BACKEND_WGPU
CommandEncoderDescriptor encoderDesc = {};
encoderDesc.label = "My command encoder";
CommandEncoder encoder = device.createCommandEncoder(encoderDesc);
RenderPassDescriptor renderPassDesc = {};

{{Describe Render Pass}}

RenderPassEncoder renderPass = encoder.beginRenderPass(renderPassDesc);
{{Use Render Pass}}
renderPass.end();
renderPass.release();
RenderPassColorAttachment renderPassColorAttachment = {};

{{Describe the attachment}}

renderPassDesc.colorAttachmentCount = 1;
renderPassDesc.colorAttachments = &renderPassColorAttachment;
renderPassDesc.depthStencilAttachment = nullptr;
renderPassDesc.timestampWrites = nullptr;
renderPassColorAttachment.view = targetView;
renderPassColorAttachment.resolveTarget = nullptr;
renderPassColorAttachment.loadOp = LoadOp::Clear;
renderPassColorAttachment.storeOp = StoreOp::Store;
renderPassColorAttachment.clearValue = Color{ 0.9, 0.1, 0.2, 1.0 };
#ifndef WEBGPU_BACKEND_WGPU
renderPassColorAttachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
#endif // NOT WEBGPU_BACKEND_WGPU
// Finally encode and submit the render pass
CommandBufferDescriptor cmdBufferDescriptor = {};
cmdBufferDescriptor.label = "Command buffer";
CommandBuffer command = encoder.finish(cmdBufferDescriptor);
encoder.release();

std::cout << "Submitting command..." << std::endl;
queue.submit(1, &command);
command.release();
std::cout << "Command submitted." << std::endl;
targetView.release();
#ifndef __EMSCRIPTEN__
surface.present();
#endif
#if defined(WEBGPU_BACKEND_DAWN)
    device.tick();
#elif defined(WEBGPU_BACKEND_WGPU)
    device.poll(false);
#endif
main.cpp