สร้างแอปด้วย WebGPU

François Beaufort
François Beaufort

WebGPU คือ API กราฟิกบนเว็บที่ให้สิทธิ์เข้าถึง GPU รวมและรวดเร็วสำหรับนักพัฒนาเว็บ WebGPU แสดงความสามารถของฮาร์ดแวร์ที่ทันสมัย และอนุญาตการแสดงผลและประมวลผลบน GPU ในลักษณะเดียวกับ Direct3D 12, Metal และ Vulkan

ถึงแม้ว่าจะเป็นความจริง แต่เรื่องราวนั้นไม่สมบูรณ์ WebGPU เกิดขึ้นจากความร่วมมือกัน ซึ่งรวมถึงบริษัทใหญ่ๆ เช่น Apple, Google, Intel, Mozilla และ Microsoft หนึ่งในนั้น มีบางคนตระหนักว่า WebGPU เป็นมากกว่า JavaScript API แต่เป็น API กราฟิกข้ามแพลตฟอร์ม สำหรับนักพัฒนาซอฟต์แวร์ในระบบนิเวศต่างๆ นอกเหนือจากเว็บ

เราได้เปิดตัว JavaScript API ใน Chrome 113 เพื่ออำนวยความสะดวกในการใช้งานหลัก แต่ก็มีโปรเจ็กต์สำคัญอีกโปรเจ็กต์หนึ่งที่พัฒนาไปพร้อมๆ กัน ซึ่งก็คือ webgpu.h C API ไฟล์ส่วนหัว C นี้จะแสดงขั้นตอนและโครงสร้างข้อมูลที่มีอยู่ทั้งหมดของ WebGPU โดยทำหน้าที่เป็นชั้น Abstraction ของฮาร์ดแวร์ที่ใช้งานได้ทุกแพลตฟอร์ม ซึ่งช่วยให้คุณสร้างแอปพลิเคชันเฉพาะแพลตฟอร์มโดยให้อินเทอร์เฟซที่สอดคล้องกันในแพลตฟอร์มต่างๆ

ในเอกสารนี้ คุณจะได้เรียนรู้วิธีเขียนแอป C++ ขนาดเล็กโดยใช้ WebGPU ที่ทำงานได้ทั้งในเว็บและแพลตฟอร์มที่เฉพาะเจาะจง การแจ้งเตือนสปอยล์ คุณจะได้รับรูปสามเหลี่ยมสีแดงแบบเดียวกับที่ปรากฏในหน้าต่างเบราว์เซอร์และหน้าต่างเดสก์ท็อปที่มีการปรับฐานของโค้ดน้อยที่สุด

ภาพหน้าจอของสามเหลี่ยมสีแดงที่ขับเคลื่อนโดย WebGPU ในหน้าต่างเบราว์เซอร์และหน้าต่างเดสก์ท็อปใน macOS
รูปสามเหลี่ยมเดียวกับที่ขับเคลื่อนโดย WebGPU ในหน้าต่างเบราว์เซอร์และหน้าต่างของเดสก์ท็อป

หลักการทำงาน

หากต้องการดูแอปพลิเคชันที่สมบูรณ์ โปรดไปที่ที่เก็บแอปข้ามแพลตฟอร์มของ WebGPU

แอปนี้เป็นตัวอย่าง C++ แบบมินิมอลที่แสดงวิธีใช้ WebGPU เพื่อสร้างแอปบนเดสก์ท็อปและเว็บแอปจากฐานของโค้ดเดียว เบื้องหลังจะใช้ webgpu.h ของ WebGPU เป็นเลเยอร์ Abstraction ของฮาร์ดแวร์ที่ไม่ซับซ้อนผ่านแพลตฟอร์ม C++ ที่ชื่อ webgpu_cpp.h

สำหรับบนเว็บ แอปสร้างขึ้นจาก Emscripten ซึ่งมีการเชื่อมโยงที่ใช้ webgpu.h เพิ่มเติมจาก JavaScript API บนแพลตฟอร์มที่เฉพาะเจาะจง เช่น macOS หรือ Windows สามารถสร้างโปรเจ็กต์นี้ตาม Dawn ซึ่งเป็นการติดตั้งใช้งาน WebGPU แบบข้ามแพลตฟอร์มของ Chromium คุณควรพูดถึง wgpu-native ซึ่งเป็นการใช้งาน Rust ของ webgpu.h เช่นกัน แต่ไม่ได้ใช้ในเอกสารนี้

เริ่มต้นใช้งาน

ในการเริ่มต้น คุณต้องมีคอมไพเลอร์ C++ และ CMake เพื่อจัดการบิลด์ข้ามแพลตฟอร์มด้วยวิธีมาตรฐาน ภายในโฟลเดอร์เฉพาะ ให้สร้างไฟล์ต้นฉบับ main.cpp และไฟล์บิลด์ CMakeLists.txt

ไฟล์ main.cpp ควรจะมีฟังก์ชัน main() ที่ว่างเปล่าในตอนนี้

int main() {}

ไฟล์ CMakeLists.txt มีข้อมูลพื้นฐานเกี่ยวกับโปรเจ็กต์ บรรทัดสุดท้ายระบุชื่อสั่งการคือ "app" และซอร์สโค้ดคือ 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")

เรียกใช้ cmake -B build เพื่อสร้างไฟล์สำหรับบิลด์ในโฟลเดอร์ย่อย "build/" และ cmake --build build เพื่อสร้างแอปและสร้างไฟล์สั่งการ

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

# Run the app.
$ ./build/app

แอปทำงานอยู่แต่ไม่มีเอาต์พุต เนื่องจากคุณต้องการวิธีวาดสิ่งต่างๆ บนหน้าจอ

ตื่นเช้า

ในการวาดรูปสามเหลี่ยม คุณสามารถใช้ประโยชน์จาก Dawn ซึ่งเป็นการใช้งาน WebGPU แบบข้ามแพลตฟอร์มของ Chromium ซึ่งรวมถึงไลบรารี GLFW C++ สำหรับการวาดไปยังหน้าจอ วิธีหนึ่งในการดาวน์โหลด Dawn คือการเพิ่มเป็นโมดูลย่อยของ git ในที่เก็บ คำสั่งต่อไปนี้จะดึงข้อมูลในโฟลเดอร์ย่อย "dawn/"

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

จากนั้น เพิ่มต่อท้ายไฟล์ CMakeLists.txt ดังนี้

  • ตัวเลือก CMake DAWN_FETCH_DEPENDENCIES จะดึงข้อมูลทรัพยากร Dependency ของ Dawn ทั้งหมด
  • มีโฟลเดอร์ย่อย dawn/ รวมอยู่ในเป้าหมาย
  • แอปของคุณจะขึ้นอยู่กับเป้าหมาย webgpu_cpp, webgpu_dawn และ webgpu_glfw เพื่อให้คุณใช้ในไฟล์ main.cpp ในภายหลังได้
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)

เปิดหน้าต่าง

ตอนนี้ Dawn มีให้บริการแล้ว ลองใช้ GLFW เพื่อวาดสิ่งต่างๆ บนหน้าจอ ไลบรารีนี้รวมอยู่ใน webgpu_glfw เพื่อความสะดวก ทำให้คุณเขียนโค้ดได้โดยไม่จำเป็นต้องคำนึงถึงแพลตฟอร์มสำหรับการจัดการหน้าต่าง

หากต้องการเปิดหน้าต่างชื่อ "หน้าต่าง WebGPU" ที่มีความละเอียด 512x512 ให้อัปเดตไฟล์ main.cpp ตามด้านล่างนี้ โปรดทราบว่าจะมีการใช้ glfwWindowHint() ที่นี่เพื่อขอการเริ่มต้น API กราฟิกที่เฉพาะเจาะจง

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

การสร้างแอปอีกครั้งและเรียกใช้งานเหมือนก่อนหน้านี้จะทำให้หน้าต่างว่างเปล่า ถือว่าก้าวหน้าไปมากแล้ว

ภาพหน้าจอหน้าต่าง macOS ที่ไม่มีข้อมูล
หน้าต่างว่าง

รับอุปกรณ์ GPU

ใน JavaScript navigator.gpu คือจุดแรกเข้าสำหรับการเข้าถึง GPU ใน C++ คุณต้องสร้างตัวแปร wgpu::Instance ที่ใช้สำหรับวัตถุประสงค์เดียวกันด้วยตนเอง เพื่อความสะดวก ให้ประกาศ instance ที่ด้านบนของไฟล์ main.cpp แล้วเรียก wgpu::CreateInstance() ใน main()

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

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

การเข้าถึง GPU ไม่พร้อมกันเนื่องจากรูปร่างของ JavaScript API ใน C++ ให้สร้างฟังก์ชันตัวช่วย GetDevice() ที่ใช้อาร์กิวเมนต์ของฟังก์ชันเรียกกลับและเรียกใช้ด้วย wgpu::Device ที่เป็นผลลัพธ์

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

ประกาศตัวแปร wgpu::Device ที่ด้านบนของไฟล์ main.cpp และอัปเดตฟังก์ชัน main() เพื่อเรียกใช้ GetDevice() และกำหนดโค้ดเรียกกลับของผลลัพธ์เป็น device ก่อนเรียกใช้ Start() เพื่อให้เข้าถึงได้ง่ายขึ้น

wgpu::Device device;
…

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

วาดรูปสามเหลี่ยม

การสลับห่วงโซ่จะไม่แสดงใน JavaScript API เนื่องจากเบราว์เซอร์จะดูแลเรื่องนี้ ใน C++ คุณจะต้องสร้างข้อมูลดังกล่าวด้วยตนเอง ประกาศตัวแปร wgpu::SwapChain ที่ด้านบนของไฟล์ main.cpp อีกครั้งเพื่อความสะดวก หลังจากที่สร้างหน้าต่าง GLFW ใน Start() แล้ว ให้เรียกใช้ฟังก์ชัน wgpu::glfw::CreateSurfaceForWindow() ที่มีประโยชน์เพื่อสร้าง wgpu::Surface (คล้ายกับแคนวาส HTML) และใช้เพื่อตั้งค่าเชนการสลับโดยเรียกใช้ฟังก์ชันตัวช่วยใหม่ SetupSwapChain() ใน InitGraphics() คุณยังต้องเรียกใช้ swapChain.Present() เพื่อนำเสนอพื้นผิวถัดไปในการวนรอบด้วย สิ่งนี้จะไม่มีเอฟเฟกต์ที่มองเห็นได้เนื่องจากยังไม่มีการแสดงผลเกิดขึ้น

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

ตอนนี้เป็นโอกาสที่ดีในการสร้างไปป์ไลน์การแสดงภาพด้วยโค้ดด้านล่าง ประกาศตัวแปร wgpu::RenderPipeline ที่ด้านบนของไฟล์ main.cpp และเรียกใช้ฟังก์ชันตัวช่วย CreateRenderPipeline() ใน 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();
}

สุดท้าย ให้ส่งคำสั่งการแสดงผลไปยัง GPU ในฟังก์ชัน Render() ที่เรียกว่าแต่ละเฟรม

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

สร้างแอปด้วย CMake อีกครั้งแล้วเรียกใช้ตอนนี้จะทำให้เกิดผลสามเหลี่ยมสีแดงที่ทุกคนรอคอยกันในหน้าต่าง คุณควรพักสักหน่อยนะ

ภาพหน้าจอของสามเหลี่ยมสีแดงในหน้าต่าง macOS
รูปสามเหลี่ยมสีแดงในหน้าต่างเดสก์ท็อป

คอมไพล์ไปยัง WebAssembly

เราลองมาดูการเปลี่ยนแปลงที่ต้องทำเพียงเล็กน้อยในการปรับฐานของโค้ดที่มีอยู่เพื่อวาดรูปสามเหลี่ยมสีแดงนี้ในหน้าต่างเบราว์เซอร์ แอปดังกล่าวสร้างขึ้นโดยเทียบกับ Emscripten ซึ่งเป็นเครื่องมือสำหรับรวบรวมโปรแกรม C/C++ ไปยัง WebAssembly ซึ่งมีการเชื่อมโยงที่ติดตั้งใช้งาน webgpu.h เพิ่มเติมจาก JavaScript API

อัปเดตการตั้งค่า CMake

เมื่อติดตั้ง Emscripten แล้ว ให้อัปเดตไฟล์บิลด์ CMakeLists.txt ดังนี้ โค้ดที่ไฮไลต์ไว้คือสิ่งเดียวที่คุณต้องเปลี่ยน

  • ระบบจะใช้ set_target_properties เพื่อเพิ่มนามสกุลไฟล์ "html" ลงในไฟล์เป้าหมายโดยอัตโนมัติ กล่าวคือ คุณจะต้องสร้างไฟล์ "app.html"
  • ต้องมีตัวเลือกลิงก์แอป USE_WEBGPU เพื่อเปิดใช้การรองรับ WebGPU ใน Emscripten หากไม่เป็นเช่นนั้น ไฟล์ main.cpp จะไม่สามารถเข้าถึงไฟล์ webgpu/webgpu_cpp.h ได้
  • คุณต้องระบุตัวเลือกลิงก์แอป USE_GLFW ที่นี่ด้วยเพื่อให้นำโค้ด GLFW มาใช้ซ้ำได้
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()

อัปเดตโค้ด

ใน Emscripten การสร้าง wgpu::surface ต้องใช้องค์ประกอบ HTML Canvas สำหรับกรณีนี้ ให้เรียก instance.CreateSurface() และระบุตัวเลือก #canvas เพื่อจับคู่องค์ประกอบ HTML ที่เหมาะสมในหน้า HTML ที่ Emscripten สร้างขึ้น

แทนที่จะใช้การวนรอบขณะ ให้เรียก emscripten_set_main_loop(Render) เพื่อให้แน่ใจว่ามีการเรียกฟังก์ชัน Render() ในอัตราที่ราบรื่นที่เหมาะสมและตรงกับเบราว์เซอร์และจอภาพอย่างถูกต้อง

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

สร้างแอปด้วย Emscripten

การเปลี่ยนแปลงเพียงอย่างเดียวที่จำเป็นในการสร้างแอปด้วย Emscripten คือการเพิ่มcmakeคำสั่งด้วยเวทมนตร์ emcmakeสคริปต์ Shell แต่ครั้งนี้ให้สร้างแอปในโฟลเดอร์ย่อย build-web และเริ่มเซิร์ฟเวอร์ HTTP ขั้นตอนสุดท้าย ให้เปิดเบราว์เซอร์ของคุณและไปที่ 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
ภาพหน้าจอของสามเหลี่ยมสีแดงในหน้าต่างเบราว์เซอร์
รูปสามเหลี่ยมสีแดงในหน้าต่างเบราว์เซอร์

ขั้นตอนถัดไป

สิ่งที่จะเกิดขึ้นในอนาคตมีดังนี้

  • การปรับปรุงระบบกันภาพสั่นของ webgpu.h และ webgpu_cpp.h API
  • การสนับสนุนเบื้องต้นสำหรับ Android และ iOS ใน Dawn

ในระหว่างนี้ โปรดส่งคำแนะนำและคำถามจากปัญหา WebGPU สำหรับ Emscripten และปัญหารุ่งเช้า

แหล่งข้อมูล

สํารวจซอร์สโค้ดของแอปนี้

หากคุณต้องการเจาะลึกเพิ่มเติมเกี่ยวกับการสร้างแอปพลิเคชัน 3 มิติแบบเนทีฟใน C++ ตั้งแต่ต้นด้วย WebGPU โปรดดูเรียนรู้เอกสารประกอบเกี่ยวกับ WebGPU สำหรับ C++ และตัวอย่าง WebGPU ของ Dawn Native

หากสนใจเกี่ยวกับ Rust คุณก็สำรวจคลังกราฟิก wgpu ที่อิงตาม WebGPU ได้ด้วย ลองดูการสาธิต hello-triangle เหล่านี้

บริการรับรองคำให้การ

บทความนี้ได้รับการตรวจสอบโดย Corentin Wallez, Kai Ninomiya และ Rachel Andrew

รูปภาพโดย Marc-Olivier Jodoin ใน Unsplash