Membangun aplikasi dengan WebGPU

François Beaufort
François Beaufort

Dipublikasikan: 20 Juli 2023, Terakhir diperbarui: 27 Agustus 2025

Untuk developer web, WebGPU adalah API grafis web yang menyediakan akses terpadu dan cepat ke GPU. WebGPU mengekspos kemampuan hardware modern dan memungkinkan operasi rendering dan komputasi di GPU, mirip dengan Direct3D 12, Metal, dan Vulkan.

Meskipun benar, kisah itu tidak lengkap. WebGPU adalah hasil upaya kolaboratif, termasuk perusahaan besar, seperti Apple, Google, Intel, Mozilla, dan Microsoft. Di antaranya, beberapa orang menyadari bahwa WebGPU bisa lebih dari sekadar API Javascript, tetapi juga API grafis lintas platform untuk developer di seluruh ekosistem, selain web.

Untuk memenuhi kasus penggunaan utama, diperkenalkan JavaScript API di Chrome 113. Namun, proyek penting lainnya telah dikembangkan bersamaan dengan proyek ini: API C webgpu.h. File header C ini mencantumkan semua prosedur dan struktur data WebGPU yang tersedia. HAL ini berfungsi sebagai lapisan abstraksi hardware yang tidak bergantung pada platform, sehingga Anda dapat membangun aplikasi khusus platform dengan menyediakan antarmuka yang konsisten di berbagai platform.

Dalam dokumen ini, Anda akan mempelajari cara menulis aplikasi C++ kecil menggunakan WebGPU yang berjalan di web dan platform tertentu. Spoiler alert, Anda akan mendapatkan segitiga merah yang sama yang muncul di jendela browser dan jendela desktop dengan sedikit penyesuaian pada codebase Anda.

Screenshot segitiga merah yang didukung oleh WebGPU di jendela browser dan jendela desktop di macOS.
Segitiga yang sama yang didukung oleh WebGPU di jendela browser dan jendela desktop.

Bagaimana cara kerjanya?

Untuk melihat aplikasi yang telah selesai, lihat repositori aplikasi lintas platform WebGPU.

Aplikasi ini adalah contoh C++ minimalis yang menunjukkan cara menggunakan WebGPU untuk mem-build aplikasi desktop dan web dari satu codebase. Di balik layar, library ini menggunakan webgpu.h WebGPU sebagai lapisan abstraksi hardware yang agnostik terhadap platform melalui wrapper C++ yang disebut webgpu_cpp.h.

Di web, aplikasi ini dibangun berdasarkan emdawnwebgpu (Emscripten Dawn WebGPU), yang memiliki binding yang menerapkan webgpu.h di atas JavaScript API. Di platform tertentu seperti macOS atau Windows, project ini dapat dibangun berdasarkan Dawn, implementasi WebGPU lintas platform Chromium. Perlu disebutkan bahwa wgpu-native, implementasi webgpu.h Rust, juga ada tetapi tidak digunakan dalam dokumen ini.

Mulai

Untuk memulai, Anda memerlukan compiler C++ dan CMake untuk menangani build lintas platform dengan cara standar. Di dalam folder khusus, buat file sumber main.cpp dan file build CMakeLists.txt.

File main.cpp harus berisi fungsi main() yang kosong untuk saat ini.

int main() {}

File CMakeLists.txt berisi informasi dasar tentang project. Baris terakhir menentukan bahwa nama file yang dapat dieksekusi adalah "app" dan kode sumbernya adalah main.cpp.

cmake_minimum_required(VERSION 3.22) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

Jalankan cmake -B build untuk membuat file build di subfolder "build/" dan cmake --build build untuk benar-benar membangun aplikasi dan membuat file yang dapat dieksekusi.

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

Aplikasi berjalan, tetapi belum ada output, karena Anda memerlukan cara untuk menggambar sesuatu di layar.

Mendapatkan Dawn

Untuk menggambar segitiga, Anda dapat memanfaatkan Dawn, implementasi WebGPU lintas platform Chromium. Hal ini mencakup library C++ GLFW untuk menggambar ke layar. Salah satu cara untuk mendownload Dawn adalah dengan menambahkannya sebagai submodul git ke repositori Anda. Perintah berikut mengambilnya di subfolder "dawn/".

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

Kemudian, tambahkan ke file CMakeLists.txt sebagai berikut:

  • Opsi CMake DAWN_FETCH_DEPENDENCIES mengambil semua dependensi Dawn.
  • Opsi CMake DAWN_BUILD_MONOLITHIC_LIBRARY menggabungkan semua komponen Dawn ke dalam satu library.
  • Subfolder dawn/ disertakan dalam target.
  • Aplikasi Anda akan bergantung pada target webgpu_dawn, webgpu_glfw, dan glfw sehingga Anda dapat menggunakannya dalam file main.cpp nanti.

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

Membuka jendela

Sekarang setelah Dawn tersedia, gunakan GLFW untuk menggambar sesuatu di layar. Library ini disertakan dalam webgpu_glfw untuk memudahkan, memungkinkan Anda menulis kode yang tidak bergantung pada platform untuk pengelolaan jendela.

Untuk membuka jendela bernama "WebGPU window" dengan resolusi 512x512, perbarui file main.cpp seperti di bawah. Perhatikan bahwa glfwWindowHint() digunakan di sini untuk meminta tidak ada inisialisasi API grafis tertentu.

#include <GLFW/glfw3.h>

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();
    // TODO: Render a triangle using WebGPU.
  }
}

int main() {
  Start();
}

Membangun ulang aplikasi dan menjalankannya seperti sebelumnya kini akan menghasilkan jendela kosong. Kemajuan Anda bagus!

Screenshot jendela macOS kosong.
Jendela kosong.

Mendapatkan perangkat GPU

Di JavaScript, navigator.gpu adalah titik entri Anda untuk mengakses GPU. Di C++, Anda harus membuat variabel wgpu::Instance secara manual yang digunakan untuk tujuan yang sama. Untuk mempermudah, deklarasikan instance di bagian atas file main.cpp dan panggil wgpu::CreateInstance() di dalam Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

Deklarasikan dua variabel wgpu::Adapter dan wgpu::Device di bagian atas file main.cpp. Perbarui fungsi Init() untuk memanggil instance.RequestAdapter() dan tetapkan callback hasilnya ke adapter, lalu panggil adapter.RequestDevice() dan tetapkan callback hasilnya ke device.

#include <iostream>

#include <dawn/webgpu_cpp_print.h>


wgpu::Adapter adapter;
wgpu::Device device;


void Init() {
  

  wgpu::Future f1 = instance.RequestAdapter(
      nullptr, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
         wgpu::StringView message) {
        if (status != wgpu::RequestAdapterStatus::Success) {
          std::cout << "RequestAdapter: " << message << "\n";
          exit(0);
        }
        adapter = std::move(a);
      });
  instance.WaitAny(f1, UINT64_MAX);

  wgpu::DeviceDescriptor desc{};
  desc.SetUncapturedErrorCallback([](const wgpu::Device&,
                                     wgpu::ErrorType errorType,
                                     wgpu::StringView message) {
    std::cout << "Error: " << errorType << " - message: " << message << "\n";
  });

  wgpu::Future f2 = adapter.RequestDevice(
      &desc, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestDeviceStatus status, wgpu::Device d,
         wgpu::StringView message) {
        if (status != wgpu::RequestDeviceStatus::Success) {
          std::cout << "RequestDevice: " << message << "\n";
          exit(0);
        }
        device = std::move(d);
      });
  instance.WaitAny(f2, UINT64_MAX);
}

Menggambar segitiga

Swap chain tidak diekspos di JavaScript API karena browser yang menanganinya. Di C++, Anda harus membuatnya secara manual. Sekali lagi, untuk mempermudah, deklarasikan variabel wgpu::Surface di bagian atas file main.cpp. Tepat setelah membuat jendela GLFW di Start(), panggil fungsi wgpu::glfw::CreateSurfaceForWindow() yang praktis untuk membuat wgpu::Surface (mirip dengan kanvas HTML) dan konfigurasikan dengan memanggil fungsi helper ConfigureSurface() baru di InitGraphics(). Anda juga perlu memanggil surface.Present() untuk menampilkan tekstur berikutnya dalam loop while. Hal ini tidak memiliki efek yang terlihat karena belum ada rendering yang terjadi.

#include <webgpu/webgpu_glfw.h>


wgpu::Surface surface;
wgpu::TextureFormat format;

void ConfigureSurface() {
  wgpu::SurfaceCapabilities capabilities;
  surface.GetCapabilities(adapter, &capabilities);
  format = capabilities.formats[0];

  wgpu::SurfaceConfiguration config{.device = device,
                                    .format = format,
                                    .width = kWidth,
                                    .height = kHeight,
                                    .presentMode = wgpu::PresentMode::Fifo};
  surface.Configure(&config);
}

void InitGraphics() {
  ConfigureSurface();
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  
  surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics();

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    surface.Present();
    instance.ProcessEvents();
  }
}

Sekarang adalah saat yang tepat untuk membuat pipeline render dengan kode di bawah. Untuk akses yang lebih mudah, deklarasikan variabel wgpu::RenderPipeline di bagian atas file main.cpp dan panggil fungsi bantuan CreateRenderPipeline() di 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);
    }
    @fragment fn fragmentMain() -> @location(0) vec4f {
        return vec4f(1, 0, 0, 1);
    }
)";

void CreateRenderPipeline() {
  wgpu::ShaderSourceWGSL wgsl{{.code = shaderCode}};

  wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl};
  wgpu::ShaderModule shaderModule =
      device.CreateShaderModule(&shaderModuleDescriptor);

  wgpu::ColorTargetState colorTargetState{.format = format};

  wgpu::FragmentState fragmentState{
      .module = shaderModule, .targetCount = 1, .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{.vertex = {.module = shaderModule},
                                            .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics() {
  
  CreateRenderPipeline();
}

Terakhir, kirim perintah rendering ke GPU dalam fungsi Render() yang dipanggil setiap frame.

void Render() {
  wgpu::SurfaceTexture surfaceTexture;
  surface.GetCurrentTexture(&surfaceTexture);

  wgpu::RenderPassColorAttachment attachment{
      .view = surfaceTexture.texture.CreateView(),
      .loadOp = wgpu::LoadOp::Clear,
      .storeOp = wgpu::StoreOp::Store};

  wgpu::RenderPassDescriptor renderpass{.colorAttachmentCount = 1,
                                        .colorAttachments = &attachment};

  wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
  wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
  pass.SetPipeline(pipeline);
  pass.Draw(3);
  pass.End();
  wgpu::CommandBuffer commands = encoder.Finish();
  device.GetQueue().Submit(1, &commands);
}

Membangun ulang aplikasi dengan CMake dan menjalankannya kini akan menghasilkan segitiga merah yang telah lama ditunggu-tunggu di jendela. Istirahatlah—Anda berhak mendapatkannya.

Screenshot segitiga merah di jendela macOS.
Segitiga merah di jendela desktop.

Mengompilasi ke WebAssembly

Sekarang, mari kita lihat perubahan minimal yang diperlukan untuk menyesuaikan codebase yang ada guna menggambar segitiga merah ini di jendela browser. Sekali lagi, aplikasi ini dibangun berdasarkan emdawnwebgpu (Emscripten Dawn WebGPU), yang memiliki binding yang mengimplementasikan webgpu.h di atas JavaScript API. Google Earth menggunakan Emscripten, alat untuk mengompilasi program C/C++ ke WebAssembly.

Memperbarui setelan CMake

Setelah Emscripten diinstal, perbarui file build CMakeLists.txt sebagai berikut. Kode yang ditandai adalah satu-satunya hal yang perlu Anda ubah.

  • set_target_properties digunakan untuk otomatis menambahkan ekstensi file "html" ke file target. Dengan kata lain, Anda akan membuat file "app.html".
  • Library link target emdawnwebgpu_cpp mengaktifkan dukungan WebGPU di Emscripten. Tanpanya, file main.cpp Anda tidak dapat mengakses file webgpu/webgpu_cpp.h.
  • Opsi link aplikasi ASYNCIFY=1 memungkinkan kode C++ sinkron berinteraksi dengan JavaScript asinkron.
  • Opsi link aplikasi USE_GLFW=3 memberi tahu Emscripten untuk menggunakan implementasi JavaScript bawaan dari GLFW 3 API.
cmake_minimum_required(VERSION 3.22) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
  target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
  target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)
endif()

Mengupdate kode

Daripada menggunakan loop while, panggil emscripten_set_main_loop(Render) untuk memastikan fungsi Render() dipanggil pada kecepatan yang lancar dan tepat yang sesuai dengan browser dan monitor.

#include <iostream>

#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
  
#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    surface.Present();
    instance.ProcessEvents();
  }
#endif
}

Membangun aplikasi dengan Emscripten

Satu-satunya perubahan yang diperlukan untuk mem-build aplikasi dengan Emscripten adalah menambahkan awalan perintah cmake dengan skrip shell emcmake ajaib. Kali ini, buat aplikasi di subfolder build-web dan mulai server HTTP. Terakhir, buka browser Anda dan kunjungi build-web/app.html.

# Build the app with Emscripten.
$ emcmake cmake -B build-web && cmake --build build-web

# Start a HTTP server.
$ npx http-server
Screenshot segitiga merah di jendela browser.
Segitiga merah di jendela browser.

Langkah berikutnya

Berikut yang dapat Anda harapkan pada masa mendatang:

  • Peningkatan dalam stabilisasi API webgpu.h dan webgpu_cpp.h.
  • Dukungan awal Dawn untuk Android dan iOS.

Sementara itu, ajukan masalah WebGPU untuk Emscripten dan masalah Dawn dengan saran dan pertanyaan.

Resource

Silakan pelajari kode sumber aplikasi ini.

Jika Anda ingin mempelajari lebih lanjut cara membuat aplikasi 3D native di C++ dari awal dengan WebGPU, lihat dokumentasi Learn WebGPU for C++ dan Dawn Native WebGPU Examples.

Jika Anda tertarik dengan Rust, Anda juga dapat mempelajari library grafis wgpu berdasarkan WebGPU. Lihat demo hello-triangle mereka.

Ucapan terima kasih

Artikel ini ditinjau oleh Corentin Wallez, Kai Ninomiya, dan Rachel Andrew.

Foto oleh Marc-Olivier Jodoin di Unsplash.