Membangun aplikasi dengan WebGPU

François Beaufort
François Beaufort

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

Meskipun benar, cerita itu tidak lengkap. WebGPU merupakan hasil dari upaya kolaboratif, yang mencakup sejumlah perusahaan besar, seperti Apple, Google, Intel, Mozilla, dan Microsoft. Di antaranya, beberapa orang menyadari bahwa WebGPU lebih dari sekadar JavaScript API, tetapi merupakan API grafis lintas platform untuk developer di seluruh ekosistem, selain web.

Untuk memenuhi kasus penggunaan utama, JavaScript API diperkenalkan pada Chrome 113. Namun, project signifikan lainnya telah dikembangkan bersamanya: webgpu.h C API. File header C ini mencantumkan semua prosedur dan struktur data WebGPU. Library ini berfungsi sebagai lapisan abstraksi hardware agnostik platform, sehingga Anda dapat mem-build 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 maupun platform tertentu. Peringatan {i>spoiler<i}, Anda akan melihat segitiga merah yang sama seperti yang muncul di jendela {i>browser<i} dan jendela {i>desktop<i} dengan sedikit penyesuaian pada {i>code<i} 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 membangun aplikasi desktop dan web dari satu codebase. Di balik layar, komponen ini menggunakan webgpu.h WebGPU sebagai lapisan abstraksi hardware platform-agnostic melalui wrapper C++ yang disebut webgpu_cpp.h.

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

Memulai

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.

Untuk saat ini, file main.cpp seharusnya berisi fungsi main() yang kosong.

int main() {}

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

cmake_minimum_required(VERSION 3.13) # 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 mem-build aplikasi dan menghasilkan 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.

Fajar

Untuk menggambar segitiga, Anda dapat memanfaatkan Dawn, implementasi WebGPU lintas platform Chromium. 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 akan mengambilnya di sub folder "dawn/".

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

Kemudian, tambahkan ke file CMakeLists.txt sebagai berikut:

  • Opsi DAWN_FETCH_DEPENDENCIES CMake akan mengambil semua dependensi Dawn.
  • Subfolder dawn/ disertakan dalam target.
  • Aplikasi Anda akan bergantung pada target webgpu_cpp, webgpu_dawn, dan webgpu_glfw sehingga Anda dapat menggunakannya di file main.cpp nanti.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)

Membuka jendela

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

Untuk membuka jendela bernama "WebGPU window" dengan resolusi 512x512, update file main.cpp seperti di bawah. Perhatikan bahwa glfwWindowHint() digunakan di sini untuk tidak meminta 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 menghasilkan jendela kosong. Anda membuat kemajuan!

Screenshot jendela macOS kosong.
Jendela kosong.

Dapatkan perangkat GPU

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

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

int main() {
  instance = wgpu::CreateInstance();
  Start();
}

Akses GPU bersifat asinkron karena bentuk JavaScript API. Pada C++, buat fungsi GetDevice() helper yang menggunakan argumen fungsi callback dan panggil dengan wgpu::Device yang dihasilkan.

#include <iostream>
…

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);
              device.SetUncapturedErrorCallback(
                  [](WGPUErrorType type, const char* message, void* userdata) {
                    std::cout << "Error: " << type << " - message: " << message;
                  },
                  nullptr);
              reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
            },
            userdata);
      },
      reinterpret_cast<void*>(callback));
}

Untuk memudahkan akses, deklarasikan variabel wgpu::Device di bagian atas file main.cpp dan update fungsi main() untuk memanggil GetDevice() dan menetapkan callback hasilnya ke device sebelum memanggil Start().

wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetDevice([](wgpu::Device dev) {
    device = dev;
    Start();
  });
}

Menggambar segitiga

Rantai pertukaran tidak ditampilkan di JavaScript API karena browser akan menanganinya. Pada C++, Anda perlu membuatnya secara manual. Sekali lagi, untuk memudahkan, deklarasikan variabel wgpu::SwapChain 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 gunakan untuk menyiapkan rantai swap dengan memanggil fungsi SetupSwapChain() bantuan baru di InitGraphics(). Anda juga perlu memanggil swapChain.Present() untuk menampilkan tekstur berikutnya dalam loop when. Ini tidak memiliki efek yang terlihat karena belum ada rendering yang terjadi.

#include <webgpu/webgpu_glfw.h>
…

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

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

  InitGraphics(surface);

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

Sekarang adalah saat yang tepat untuk membuat pipeline render dengan kode di bawah ini. Untuk memudahkan akses, 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::ShaderModuleWGSLDescriptor wgslDesc{};
  wgslDesc.code = shaderCode;

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

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

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

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

void InitGraphics(wgpu::Surface surface) {
  …
  CreateRenderPipeline();
}

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

void Render() {
  wgpu::RenderPassColorAttachment attachment{
      .view = swapChain.GetCurrentTextureView(),
      .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 kembali aplikasi dengan CMake dan menjalankannya sekarang akan menghasilkan segitiga merah yang telah lama dinantikan di jendela. Istirahatlah dulu—Anda layak mendapatkannya.

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

Mengompilasi ke WebAssembly

Mari kita lihat perubahan minimal yang diperlukan untuk menyesuaikan codebase yang ada untuk menggambar segitiga merah ini di jendela browser. Sekali lagi, aplikasi ini dibuat berdasarkan Emscripten, alat untuk mengompilasi program C/C++ ke WebAssembly, yang memiliki binding yang menerapkan webgpu.h di atas JavaScript API.

Memperbarui setelan CMake

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

  • set_target_properties digunakan untuk menambahkan ekstensi file "html" secara otomatis ke file target. Dengan kata lain, Anda akan menghasilkan file "app.html".
  • Opsi link aplikasi USE_WEBGPU diperlukan untuk mengaktifkan dukungan WebGPU di Emscripten. Tanpanya, file main.cpp Anda tidak akan dapat mengakses file webgpu/webgpu_cpp.h.
  • Opsi link aplikasi USE_GLFW juga diperlukan di sini sehingga Anda dapat menggunakan kembali kode GLFW Anda.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_options(app PRIVATE "-sUSE_WEBGPU=1" "-sUSE_GLFW=3")
else()
  set(DAWN_FETCH_DEPENDENCIES ON)
  add_subdirectory("dawn" EXCLUDE_FROM_ALL)
  target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)
endif()

Mengupdate kode

Di Emscripten, pembuatan wgpu::surface memerlukan elemen kanvas HTML. Untuk itu, panggil instance.CreateSurface() dan tentukan pemilih #canvas agar cocok dengan elemen kanvas HTML yang sesuai di halaman HTML yang dihasilkan oleh Emscripten.

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

#include <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <webgpu/webgpu_glfw.h>
#endif
void Start() {
  if (!glfwInit()) {
    return;
  }

  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  GLFWwindow* window =
      glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);

#if defined(__EMSCRIPTEN__)
  wgpu::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
  canvasDesc.selector = "#canvas";

  wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &canvasDesc};
  wgpu::Surface surface = instance.CreateSurface(&surfaceDesc);
#else
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics(surface);

#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.Present();
    instance.ProcessEvents();
  }
#endif
}

Membangun aplikasi dengan Emscripten

Satu-satunya perubahan yang diperlukan untuk membangun aplikasi dengan Emscripten adalah mengawali 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 selanjutnya

Berikut ini hal yang akan terjadi pada masa mendatang:

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

Sementara itu, laporkan masalah WebGPU untuk Emscripten dan masalah Fajar dengan saran dan pertanyaan.

Referensi

Jangan ragu untuk mempelajari kode sumber aplikasi ini.

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

Jika tertarik dengan Rust, Anda juga dapat menjelajahi 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.