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 grafik API'sidir. WebGPU, modern donanım özelliklerini gösterir ve Direct3D 12, Metal ve Vulkan'a benzer şekilde GPU'da oluşturma ve hesaplama işlemlerine olanak tanır.

Bu doğru olsa da 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'si tanıtıldı. 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 farklı platformlarda tutarlı bir arayüz sağlayarak platforma özel uygulamalar oluşturmanıza olanak tanır.

Bu dokümanda, WebGPU'yu kullanarak hem web'de hem de belirli platformlarda çalışan 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ör içinde 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")

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

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

# Run the app.
$ ./build/app

Uygulama çalışır ancak ekranda bir şeyler çizmek için bir yönteme ihtiyacınız olduğundan henüz çıkış yoktur.

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ı "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 ş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++'da, sırasıyla wgpu::Adapter ve wgpu::Device içeren bir geri çağırma işlevi döndüren GetAdapter() ve GetDevice() adlı 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 bunu manuel olarak oluşturmanız gerekir. Kolaylık sağlamak için main.cpp dosyasının en üstünde bir wgpu::Surface değişkeni tanımlayın. Start() içinde GLFW penceresini oluşturduktan hemen sonra, kullanışlı wgpu::glfw::CreateSurfaceForWindow() işlevini çağırarak bir wgpu::Surface (HTML kanvasına benzer) oluşturun 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 yapılmadığı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ırdığınızda artık bir pencerede uzun zamandır beklenen kırmızı üçgen görünüyor. 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üncelleme

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() işlevini çağırın ve Emscripten tarafından oluşturulan HTML sayfasındaki uygun HTML kanvas öğesiyle eşleşecek #canvas seçiciyi belirtin.

Render() işlevinin tarayıcı ve monitörle düzgün şekilde uyumlu olacak şekilde uygun bir hızda ç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 tek gereken 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 sıfırdan C++'ta yerel 3D uygulamalar oluşturma hakkında daha fazla bilgi edinmek istiyorsanız C++ için WebGPU'yi öğrenme dokümanlarına ve Dawn Native WebGPU örneklerine göz atın.

Rust'u kullanmak istiyorsanız WebGPU tabanlı 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.