WebGPU ile uygulama oluşturma

François Beaufort
François Beaufort

WebGPU, web geliştiricileri için birleştirilmiş ve hızlı bir şekilde sunan bir web grafiği API'sıdır daha fazla bilgi edineceksiniz. WebGPU, modern donanım özelliklerini sunar ve oluşturmaya olanak tanır. ve hesaplama işlemleri, Direct3D 12, Metal ve Vulkan'a benzer.

Doğru olsa da bu hikaye eksik. WebGPU, ortak bir çalışmanın sonucudur Microsoft ve Mozilla gibi Apple, Google, Intel ve Mozilla gibi Microsoft. Bunlardan bazıları farklarına WebGPU'nun bir JavaScript API'sinden daha fazlası olduğunu, ancak aynı zamanda Web dışındaki ekosistemlerdeki geliştiricilere yönelik API.

Birincil kullanım alanını karşılamak için JavaScript API, kullanıma sunuldu. Ancak önemli bir diğer unsur birlikte geliştirildiğini göreceksiniz: webgpu.h C API'ye gidin. Bu C başlığı dosyası, kullanılabilen tüm prosedürleri ve veri yapılarını listeler örneğidir. Platformdan bağımsız bir donanım soyutlama katmanı görevi görür. Böylece, her kullanıcı için tutarlı bir arayüz sağlayarak platforma özgü uygulamalar yardımcı olabilir.

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. Kodunuzda, bir tarayıcı penceresinde ve masaüstü penceresinde görünen kırmızı üçgenin aynısını, kod tabanınızda çok az ayarlamayla görürsünüz.

Tarayıcı penceresinde WebGPU ile desteklenen kırmızı bir üçgenin ve macOS'teki bir masaüstü penceresinin ekran görüntüsü.
Tarayıcı 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. Temelde, 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 öğesini kullanır.

Web'de uygulama, JavaScript API'nin üzerinde webgpu.h uygulayan bağlamalara sahip Emscripten'e dayalı olarak derlenmiştir. macOS veya Windows gibi belirli platformlarda bu proje, Chromium'un platformlar arası WebGPU uygulaması olan Dawn'a göre geliştirilebilir. Webgpu.h'nin Rust uygulaması olan wgpu-native'nin de bu dokümanda kullanılmadığını belirtmekte fayda var.

Başlayın

Öncelikle, 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ı.

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" olduğunu belirtir ve kaynak kodu 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")

"build/" komutunu kullanarak derleme dosyaları oluşturmak için cmake -B build komutunu çalıştırın alt klasör ve cmake --build build öğesini eklemeniz gerekir.

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

Şafak vakti

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

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

Ardından, CMakeLists.txt dosyasına şu şekilde ekleme yapın:

  • CMake DAWN_FETCH_DEPENDENCIES seçeneği, tüm şafak bağımlılıklarını getirir.
  • dawn/ alt klasörü hedefe dahil edilir.
  • Uygulamanız webgpu_cpp, webgpu_dawn, glfw ve webgpu_glfw hedeflerini temel alacak. Böylece bu hedefleri daha sonra main.cpp dosyasında kullanabileceksiniz.

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)

Pencere aç

Şafak artık hazır olduğuna göre ekranda bir şeyler çizmek için GLFW'yu kullanabilirsiniz. Kolaylık olması için webgpu_glfw ürününde bulunan bu kitaplık, pencere yönetimi için platformdan bağımsız bir kod yazmanıza olanak tanır.

"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 bulunmak için glfwWindowHint() öğesinin burada 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();
}

Uygulamanın yeniden derlenmesi ve eskisi gibi çalıştırılması boş bir pencereyle sonuçlanıyor. İlerleme kaydediyorsunuz.

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

GPU cihazı al

JavaScript'te navigator.gpu, GPU'ya erişmek 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ğlaması açısından, main.cpp dosyasının üst kısmında instance değerini beyan edin ve main() içinde wgpu::CreateInstance() yöntemini çağırın.


#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;


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

GPU'ya erişim, JavaScript API'nin şeklinden dolayı eşzamansız olarak yapılı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 üst kısmında wgpu::Adapter ve wgpu::Device değişkenlerini tanımlayın. main() işlevini GetAdapter() yöntemini çağıracak ve sonuç geri çağırmasını adapter öğesine atayacak şekilde güncelleyin, ardından GetDevice() yöntemini çağırın ve Start() çağrısından önce sonuç geri çağırmasını device öğesine atayın.

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 çizin

Değişim zinciri, JavaScript API'de sunulmaz. Çünkü tarayıcı bu konuyla ilgilenir. 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() ürününde GLFW penceresini oluşturduktan hemen sonra, bir wgpu::Surface (HTML tuvaline 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 bu işlevi yapılandırın. Ayrıca, süre döngüsünde bir sonraki dokuyu sunmak için surface.Present() öğesini de çağırmanız gerekir. Henüz oluşturma işlemi yapılmadığından bunun 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 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 üst kısmında bir wgpu::RenderPipeline değişkeni tanımlayın ve InitGraphics() içindeki yardımcı işlevi CreateRenderPipeline() olarak ç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 olarak adlandırılan Render() işlevindeki oluşturma komutlarını GPU'ya 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 vermeyi hak ettiniz.

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

WebAssembly ile derle

Şimdi, bu kırmızı üçgeni tarayıcı penceresinde çizmek üzere mevcut kod tabanınızı ayarlamak için gereken minimum değişikliklere bakalım. Tekrarlamak gerekirse, uygulama, C/C++ programlarını WebAssembly'de derlemeye yarayan Emscripten'e dayalı olarak geliştirilmiştir. Bu araç, JavaScript API'nin üzerinde webgpu.h'yi uygulayan bağlamalara sahiptir.

CMake ayarlarını güncelle

Emscripten yüklendikten sonra CMakeLists.txt derleme dosyasını aşağıdaki gibi güncelleyin. Vurgulanan kod, değiştirmeniz gereken tek şeydir.

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

Kodu güncelleme

Emscripten'de, wgpu::surface oluşturmak için bir HTML tuval öğ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.

süre döngüsü kullanmak yerine, Render() işlevinin tarayıcı ve monitörle düzgün bir şekilde eşleşen düzgün bir hızda çağrıldığından emin olmak için 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 geliştirme

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çı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ı üçgenin ekran görüntüsü.
Tarayıcı penceresinde kırmızı bir üçgen.

Sırada ne var?

Gelecekte sizi neler bekliyor?

  • webgpu.h ve webgpu_cpp.h API'lerinin sabitlemesiyle ilgili iyileştirmeler.
  • Android ve iOS için şafak ilk desteği.

Bu süre içinde lütfen Emscripten için WebGPU sorunlarını ve Dawn sorunları ile ilgili önerileri ve soruları 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 isterseniz C++ için WebGPU öğrenme belgelerine ve Dawn Yerel WebGPU Örneklerine göz atın.

Rust ile ilgileniyorsanız WebGPU'ya 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 incelenmiştir.

Fotoğraf: Marc-Olivier Jodoin tarafından Unsplash'te).