WebGPU ile uygulama oluşturma

François Beaufort
François Beaufort

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

Doğru olsa da bu hikaye eksik. WebGPU, Apple, Google, Intel, Mozilla ve Microsoft gibi büyük şirketlerin dahil olduğu ortak bir çalışmanın sonucudur. Bu geliştiriciler arasında, WebGPU'nun bir JavaScript API'sinden daha fazlası olabileceğini, web dışındaki ekosistemlerdeki geliştiriciler için platformlar arası bir grafik API'si olabileceğini fark edenler de vardı.

Birincil kullanım alanını karşılamak için Chrome 113'te bir JavaScript API kullanıma sunuldu. Bununla birlikte, bununla birlikte önemli bir proje daha geliştirildi: webgpu.h C API'si. Bu C başlık dosyası, WebGPU'nun mevcut tüm prosedürlerini ve veri yapılarını listeler. Platformdan bağımsız bir donanım soyutlama katmanı olarak hizmet eder. Böylece, farklı platformlarda tutarlı bir arayüz sağlayarak platforma özel uygulamalar oluşturabilirsiniz.

Bu dokümanda, 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ı: Kod tabanınızda minimum düzeyde ayarlama yaparak tarayıcı penceresinde ve masaüstü penceresinde görünen aynı kırmızı üçgeni görürsünüz.

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

İşleyiş şekli

Tamamlanmış uygulamayı görmek için WebGPU platformlar arası uygulama deposuna göz atın.

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. Arka planda, webgpu_cpp.h adlı bir C++ sarmalayıcısı aracılığıyla platformdan bağımsız bir donanım soyutlama katmanı olarak WebGPU'nin webgpu.h dosyasını kullanır.

Web'de uygulama, JavaScript API'nin üzerine webgpu.h'yi uygulayan bağlantılara sahip Emscripten için geliştirilmiştir. macOS veya Windows gibi belirli platformlarda bu proje, Chromium'un platformlar arası WebGPU uygulaması olan Dawn ile derlenebilir. webgpu.h'nin Rust uygulaması olan wgpu-native'in de mevcut olduğunu ancak bu dokümanda kullanılmadığını belirtmek isteriz.

Başlayın

Başlamak için bir C++ derleyiciye ve platformlar arası derlemeleri standart bir şekilde yönetmek için CMake'e ihtiyacınız vardır. Özel bir klasörde bir main.cpp kaynak dosyası ve bir 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ırda, yürütülebilir dosyanın adının "uygulama" ve kaynak kodunun main.cpp olduğu belirtilmektedir.

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 komutunu, uygulamayı derleyip yürütülebilir dosyayı oluşturmak için de 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ğundan henüz çıktı yok.

Get Dawn

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

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

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

  • CMake DAWN_FETCH_DEPENDENCIES seçeneği tüm Dawn bağımlılarını getirir.
  • dawn/ alt klasörü hedefe dahil edilmiştir.
  • Uygulamanız dawn::webgpu_dawn, glfw ve webgpu_glfw hedeflerine bağlıdır. Bu sayede bunları daha sonra main.cpp dosyasında kullanabilirsiniz.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)

Pencere açma

Dawn kullanıma sunulduğundan ekranda çizim yapmak için GLFW'yi kullanın. Kolaylık sağlamak için webgpu_glfw'e dahil edilen bu kitaplık, pencere yönetimi için platformdan bağımsız kod yazmanıza olanak tanır.

512x512 çözünürlüğe sahip "WebGPU penceresi" adlı bir pencere açmak için main.cpp dosyasını aşağıdaki gibi güncelleyin. Burada, belirli bir grafik API'sinin başlatılmasını istememek için glfwWindowHint() değerinin 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 derleyip önceki gibi çalıştırdığınızda artık boş bir pencere görüyorsunuz. İlerleme kaydediyorsunuz.

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

GPU cihazı alma

JavaScript'te navigator.gpu, GPU'ya erişim için 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 sağlamak için main.cpp dosyasının en üstünde instance'ü tanımlayın ve main() içinde wgpu::CreateInstance()'yi çağırın.

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

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

JavaScript API'sinin yapısı nedeniyle GPU'ya erişim ayarsızdır. C++'ta, sırasıyla wgpu::Adapter ve wgpu::Device içeren bir geri çağırma işlevi döndüren GetAdapter() ve GetDevice() adında iki yardımcı işlev oluşturun.

#include <iostream>
…

void GetAdapter(void (*callback)(wgpu::Adapter)) {
  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);
        reinterpret_cast<void (*)(wgpu::Adapter)>(userdata)(adapter);
  }, reinterpret_cast<void*>(callback));
}

void GetDevice(void (*callback)(wgpu::Device)) {
  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);
  }, reinterpret_cast<void*>(callback));
}

Daha kolay erişim için main.cpp dosyasının en üstünde wgpu::Adapter ve wgpu::Device adlı iki değişken tanımlayın. main() işlevini, GetAdapter()'u çağırıp sonucun geri çağırma işlevini adapter'ye atayacak, ardından GetDevice()'ı çağırıp sonucun geri çağırma işlevini device'e atayacak ve Start()'i çağıracak şekilde güncelleyin.

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

int main() {
  instance = wgpu::CreateInstance();
  GetAdapter([](wgpu::Adapter a) {
    adapter = a;
    GetDevice([](wgpu::Device d) {
      device = d;
      Start();
    });
  });
}

Üçgen çizme

Değişim zinciri, tarayıcı tarafından yönetildiği için JavaScript API'sinde gösterilmez. C++'ta bu dosyayı manuel olarak oluşturmanız gerekir. Bir kez daha kolaylık sağlamak için main.cpp dosyasının üst kısmında bir wgpu::Surface değişkeni tanımlayın. Start() içinde GLFW penceresini oluşturduktan hemen sonra, wgpu::Surface (HTML kanvasına benzer) oluşturmak için kullanışlı wgpu::glfw::CreateSurfaceForWindow() işlevini çağırın ve InitGraphics() içinde yeni yardımcı ConfigureSurface() işlevini çağırarak yapılandırın. while döngüsünde bir sonraki dokuyu sunmak için surface.Present() işlevini de çağırmanız gerekir. Henüz oluşturma işlemi olmadığından bu durumun görünür bir etkisi yoktur.

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

Şimdi aşağıdaki kodla oluşturma ardışık düzenini oluşturmanın tam zamanı. 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şlevini ç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 = 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();
}

Son olarak, her kare çağrısında Render() işlevinde GPU'ya oluşturma komutları gönderin.

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

Uygulamayı CMake ile yeniden derleyip çalıştırmak artık bir pencerede uzun süredir beklenen kırmızı üçgeni gösteriyor! Biraz ara verin. Bunu hak ettiniz.

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

WebAssembly olarak derleme

Şimdi, mevcut kod tabanınızı bir tarayıcı penceresinde bu kırmızı üçgeni çizecek şekilde ayarlamak için gereken minimum değişikliklere göz atalım. Uygulama, C/C++ programlarını WebAssembly'e derleyen bir araç olan Emscripten'e göre oluşturulur. Bu araç, JavaScript API'sinin üzerine webgpu.h'yi uygulayan bağlantılara sahiptir.

CMake ayarlarını güncelle

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

  • set_target_properties, hedef dosyaya "html" dosya uzantısını otomatik olarak eklemek için kullanılır. Diğer bir deyişle, "app.html" dosyası oluşturursunuz.
  • Emscripten'de WebGPU desteğini etkinleştirmek için USE_WEBGPU uygulama bağlantısı seçeneği gerekir. Aksi takdirde 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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()

Kodu güncelleme

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

Render() işlevinin tarayıcı ve monitörle düzgün şekilde uyumlu olacak şekilde uygun bir akıcılıkta çağrılmasını sağlamak için while döngüsü yerine emscripten_set_main_loop(Render) işlevini ç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};
  surface = instance.CreateSurface(&surfaceDesc);
#else
  surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics();

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

Uygulamayı Emscripten ile derleme

Uygulamayı Emscripten ile derlemek için gereken tek değişiklik, cmake komutlarının başına sihirli emcmake kabuk komut dosyasını eklemektir. Bu kez uygulamayı bir build-web alt klasöründe oluşturun ve bir HTTP sunucusu başlatın. Son olarak, tarayıcınızı açıp 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ı üçgenin ekran görüntüsü.
Tarayıcı penceresindeki kırmızı üçgen.

Sırada ne var?

Gelecekte ne gibi değişiklikler görebileceğiniz aşağıda açıklanmıştır:

  • webgpu.h ve webgpu_cpp.h API'lerinin kararlılığında iyileştirmeler yapıldı.
  • Android ve iOS için Dawn'ın ilk desteği.

Bu süreçte, Emscripten için WebGPU sorunlarını ve Dawn sorunlarını öneri ve sorularla birlikte gönderin.

Kaynaklar

Bu uygulamanın kaynak kodunu inceleyebilirsiniz.

WebGPU ile C++'ta sıfırdan yerel 3D uygulamalar oluşturma konusunda daha fazla bilgi edinmek isterseniz C++ için WebGPU hakkında bilgi edinme ve Dawn Yerel WebGPU Örnekleri sayfalarına göz atın.

Rust'a ilgi duyuyorsanız WebGPU'ye dayalı wgpu grafik kitaplığını da keşfedebilirsiniz. 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, Unsplash.