WebGPU ile uygulama oluşturma

François Beaufort
François Beaufort

Web geliştiricileri için WebGPU, GPU'lara birleştirilmiş ve hızlı erişim sağlayan bir web grafiği API'sidir. WebGPU, modern donanım özelliklerini ortaya koyar ve Direct3D 12, Metal ve Vulkan'a benzer şekilde bir GPU üzerinde oluşturma ve hesaplama işlemlerine imkan tanır.

Doğru olsa da bu hikaye tam değil. WebGPU; Apple, Google, Intel, Mozilla ve Microsoft gibi büyük şirketlerin aralarında bulunduğu ortak bir çalışmanın sonucudur. Bunların bazıları, WebGPU'nun bir JavaScript API'den daha fazlası olabileceğini, ancak web dışındaki ekosistemlerdeki geliştiriciler için platformlar arası bir grafik API'si olabileceğini anladı.

Birincil kullanım alanını karşılamak için Chrome 113'te bir JavaScript API kullanıma sunulmuştur. Ancak, bununla birlikte bir başka önemli proje de geliştirilmiştir: webgpu.h C API. Bu C başlık dosyası, WebGPU'nun kullanılabilir tüm prosedürlerini ve veri yapılarını listeler. Platformdan bağımsız bir donanım soyutlama katmanı olarak işlev görür ve farklı platformlarda tutarlı bir arayüz sağlayarak platforma özgü uygulamalar oluşturmanıza olanak tanır.

Bu belgede, hem web'de hem de belirli platformlarda çalışan WebGPU'yu kullanarak küçük bir C++ uygulamasının nasıl yazılacağını öğreneceksiniz. Spoiler uyarısı, tarayıcı penceresinde ve masaüstü penceresinde kod tabanınızda küçük ayarlamalar yaparak aynı kırmızı üçgeni görürsünüz.

Tarayıcı penceresinde WebGPU tarafından desteklenen kırmızı bir üçgenin, macOS'te ise masaüstü penceresinin ekran görüntüsü.
Tarayıcı ve masaüstü penceresinde WebGPU tarafından desteklenen aynı üçgen.

İşleyiş şekli

Tamamlanan uygulamayı görmek için WebGPU platformlar arası uygulaması deposuna göz atın.

Bu uygulama, tek bir kod tabanından masaüstü ve web uygulamaları oluşturmak için WebGPU'nun nasıl kullanılacağını gösteren minimalist bir C++ örneğidir. Temel olarak, webgpu_cpp.h adlı bir C++ sarmalayıcı aracılığıyla platformdan bağımsız bir donanım soyutlama katmanı olarak WebGPU'nun webgpu.h'sini kullanır.

Web'de uygulama, JavaScript API'sına ek olarak webgpu.h'yi uygulayan bağlamalara sahip Emscripten'e dayalı olarak oluşturulmuştur. macOS veya Windows gibi belirli platformlarda, bu proje Chromium'un platformlar arası WebGPU uygulaması Dawn'a göre derlenebilir. Webgpu.h'nin Rust uygulaması olan wgpu-native'in de mevcut olduğunu ve bu dokümanda kullanılmadığını belirtmek isteriz.

Başlayın

Başlamak için, platformlar arası derlemeleri standart bir şekilde işlemek için bir C++ derleyicisine ve CMake'e ihtiyacınız vardır. Özel bir klasörde main.cpp kaynak dosyası ve CMakeLists.txt derleme dosyası oluşturun.

main.cpp dosyası şimdilik boş bir main() işlevi içermelidir.

int main() {}

CMakeLists.txt dosyası, projeyle ilgili temel bilgileri içerir. Son satır, yürütülebilir adın "app" ve kaynak kodu main.cpp olduğunu belirtiyor.

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")

Derleme dosyalarını "build/" alt klasöründe oluşturmak için cmake -B build, uygulamayı gerçekten derlemek ve yürütülebilir dosyayı oluşturmak içinse cmake --build build komutunu çalıştırın.

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

# Run the app.
$ ./build/app

Uygulama çalışıyor ancak ekranda bir şeyler çizmek için bir yönteme ihtiyacınız olduğu için henüz herhangi bir çıkış yok.

Gün Doğumu

Üçgeninizi çizmek için Chromium'un platformlar arası WebGPU uygulaması olan Dawn'dan yararlanabilirsiniz. Ekrana çizim için kullanılan GLFW C++ kitaplığı da buna dahildir. Dawn'ı indirmenin bir yolu da onu kod deponuza git alt modülü olarak eklemektir. Aşağıdaki komutlar veri öğesini "dawn/" alt klasöründe getirir.

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

Ardından, CMakeLists.txt dosyasına aşağıdaki gibi ekleyin:

  • CMake DAWN_FETCH_DEPENDENCIES seçeneği tüm Dawn bağımlılıklarını getirir.
  • dawn/ alt klasörü hedefe dahildir.
  • Uygulamanız webgpu_cpp, webgpu_dawn ve webgpu_glfw hedeflerini temel alacak. Böylece, bunları daha sonra main.cpp dosyasında kullanabilirsiniz.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)

Pencere açma

Artık Dawn kullanılabilir olduğuna göre ekranda çizim yapmak için GLFW'yu kullanın. Kolaylık olması için webgpu_glfw paketine dahil olan bu kitaplık, pencere yönetimi için platformdan bağımsız kod yazmanıza olanak tanır.

512x512 çözünürlüklü "WebGPU penceresi" adlı bir pencere açmak için main.cpp dosyasını aşağıdaki gibi güncelleyin. Belirli bir grafik API'si başlatma isteğinde bulunmamak için burada glfwWindowHint() öğesinin kullanıldığını unutmayın.

#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();
}

Uygulamayı yeniden derleme ve şimdi önceki gibi çalıştırması boş bir pencerede açılmasıyla sonuçlanır. İlerleme kaydediyorsunuz.

Boş bir macOS penceresinin ekran görüntüsü.
Boş bir pencere.

GPU cihazı al

JavaScript'te, GPU'ya erişmek için navigator.gpu giriş noktanızdır. C++'ta, aynı amaç için kullanılan bir wgpu::Instance değişkenini manuel olarak oluşturmanız gerekir. Kolaylık olması açısından, main.cpp dosyasının en üstünde instance özelliğini beyan edin ve main() içinde wgpu::CreateInstance() çağrısı yapın.

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

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

GPU'ya erişim, JavaScript API'sinin şeklinden dolayı eşzamansızdır. C++'da, bir geri çağırma işlevi bağımsız değişkenini alıp bunu elde edilen wgpu::Device koduyla çağıran yardımcı bir GetDevice() işlevi oluşturun.

#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));
}

Daha kolay erişim için main.cpp dosyasının en üst kısmında bir wgpu::Device değişkeni tanımlayın ve main() işlevini, GetDevice() yöntemini çağıracak ve Start() işlevini çağırmadan önce sonuç geri çağırmasını device işlevine atayacak şekilde güncelleyin.

wgpu::Device device;
…

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

Üçgen çizin

Değiştirme zinciri, tarayıcı ilgilendiği için JavaScript API'de açığa çıkmaz. C++'da bunu manuel olarak oluşturmanız gerekir. main.cpp dosyasının en üstünde, kolaylık sağlaması için bir kez daha wgpu::SwapChain değişkeni bildirin. Start() ürününde GLFW penceresini oluşturduktan hemen sonra, kullanışlı wgpu::glfw::CreateSurfaceForWindow() işlevini çağırarak bir wgpu::Surface (HTML tuvaline benzer) oluşturun ve bunu InitGraphics() içinde yeni yardımcı SetupSwapChain() işlevini çağırarak değiştirme zincirini oluşturmak için kullanın. Ayrıca, işlem döngüsündeki sonraki dokuyu sunmak için swapChain.Present() işlevini çağırmanız gerekir. Henüz oluşturma işlemi yapılmadığı için bunun görünür bir etkisi yoktur.

#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();
  }
}

Şimdi aşağıdaki kodu kullanarak görüntü oluşturma ardışık düzenini oluşturmanın zamanı geldi. Daha kolay erişim için main.cpp dosyasının en üstünde bir wgpu::RenderPipeline değişkeni tanımlayın ve InitGraphics() içinde CreateRenderPipeline() yardımcı işlevi çağırın.

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();
}

Son olarak, her bir kareyi çağıran Render() işlevindeki GPU'ya oluşturma komutları gönderin.

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);
}

Uygulamayı CMake ile yeniden derleyip çalıştırdığınızda artık uzun zamandır beklenen kırmızı bir üçgenle karşılaşacaksınız. Biraz ara vermeyi hak ettiniz.

macOS penceresindeki kırmızı üçgenin ekran görüntüsü.
Masaüstü penceresinde kırmızı bir üçgen.

WebAssembly ile derleyin

Bu kırmızı üçgeni bir tarayıcı penceresinde çizmek amacıyla mevcut kod tabanınızı ayarlamak için gereken minimum değişikliklere göz atalım. Uygulama yine, C/C++ programlarını WebAssembly'de derleme aracı olan Emscripten'e göre derlenir. Bu araç, JavaScript API'sının üzerine webgpu.h uygulayan bağlamalara sahiptir.

CMake ayarlarını güncelleme

Emscripten yüklendikten sonra CMakeLists.txt derleme dosyasını aşağıdaki şekilde güncelleyin. Değiştirmeniz gereken tek şey, vurgulanan koddur.

  • set_target_properties, "html" dosya uzantısını hedef dosyaya otomatik olarak eklemek için kullanılır. Diğer bir deyişle, bir "app.html" dosyası oluşturursunuz.
  • Emscripten'da WebGPU desteğini etkinleştirmek için USE_WEBGPU uygulama bağlantısı seçeneği gereklidir. Bu olmadan main.cpp dosyanız, webgpu/webgpu_cpp.h dosyasına erişemez.
  • GLFW kodunuzu yeniden kullanabilmeniz için burada USE_GLFW uygulama bağlantısı seçeneği de gereklidir.
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()

Kodu güncelleyin

Emscripten'da wgpu::surface oluşturmak için HTML tuval öğesi gerekir. Bunun için instance.CreateSurface() yöntemini çağırın ve #canvas seçiciyi, Emscripten tarafından oluşturulan HTML sayfasındaki uygun HTML tuval öğesiyle eşleşecek şekilde belirtin.

Süre döngüsü kullanmak yerine, Render() işlevinin tarayıcı ve monitörle düzgün bir şekilde hizalanan uygun bir hızda çağrıldığından emin olmak için emscripten_set_main_loop(Render) yöntemini çağırın.

#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
}

Uygulamayı Emscripten ile oluşturma

Uygulamayı Emscripten ile derlemek için gereken tek değişiklik, cmake komutlarının başına sihirli emcmake kabuk komut dosyası eklemektir. Bu kez uygulamayı build-web alt klasöründe oluşturun ve HTTP sunucusu başlatın. Son olarak, tarayıcınızı açın ve build-web/app.html adresini ziyaret edin.

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

# Start a HTTP server.
$ npx http-server
Tarayıcı penceresindeki kırmızı bir üçgenin ekran görüntüsü.
Tarayıcı penceresinde kırmızı bir üçgen.

Sırada ne var?

Gelecekte şunları bekleyebilirsiniz:

  • webgpu.h ve webgpu_cpp.h API'lerinin kararlı hale getirilmesiyle ilgili iyileştirmeler.
  • Android ve iOS için Dawn ilk destek.

Bu süre zarfında lütfen öneri ve sorularınızla Emscripten için WebGPU sorunlarını ve Dawn sorunlarını bildirin.

Kaynaklar

Bu uygulamanın kaynak kodunu inceleyebilirsiniz.

WebGPU ile C++'ta sıfırdan yerel 3D uygulamalar oluşturma konusunda daha fazla bilgi edinmek istiyorsanız, C++ dokümanları için WebGPU'yu Öğrenme ve Dawn Native WebGPU Example sayfalarına göz atın.

Rust ile ilgileniyorsanız WebGPU'ya dayalı wgpu grafik kitaplığını da keşfedebilirsiniz. Geliştiricinin hello-triangle demosuna göz atın.

Tasdik

Bu makale Corentin Wallez, Kai Ninomiya ve Rachel Andrew tarafından incelendi.

Fotoğraf Marc-Olivier Jodoin tarafından Unsplash'ta yayınlandı.