สร้างแอปด้วย 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 API ไฟล์ส่วนหัว C นี้จะแสดงกระบวนการและโครงสร้างข้อมูลทั้งหมดที่พร้อมใช้งาน ของ WebGPU โดยทำหน้าที่เป็นชั้นแอบสแตรกต์ของฮาร์ดแวร์ที่เข้าใจได้โดยไม่จำเป็นต้องเข้าใจแพลตฟอร์ม ซึ่งช่วยให้ คุณสามารถสร้างแอปพลิเคชันเฉพาะแพลตฟอร์ม ด้วยอินเทอร์เฟซที่สอดคล้องกัน ในแพลตฟอร์มต่างๆ

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

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

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

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

แอปเป็นตัวอย่าง C++ ที่เรียบง่ายซึ่งแสดงวิธีใช้ WebGPU เพื่อสร้างแอปบนเดสก์ท็อปและเว็บแอปจากฐานของโค้ดเดียว ในการทำงานเบื้องหลัง มีการใช้ webgpu.h ของ WebGPU เป็นเลเยอร์ Abstraction ของฮาร์ดแวร์ที่ไม่จำเป็นต้องใช้ร่วมกับแพลตฟอร์มผ่าน Wrapper 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 ซึ่งรวมถึงไลบรารี C++ GLFW สำหรับการวาดภาพไปยังหน้าจอ วิธีหนึ่งในการดาวน์โหลด Dawn คือการเพิ่มโมดูลนี้เป็นโมดูลย่อยของ git ลงในที่เก็บ คำสั่งต่อไปนี้ดึงข้อมูลใน "dawn/" โฟลเดอร์ย่อย

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

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

  • ตัวเลือก CMake DAWN_FETCH_DEPENDENCIES จะดึงทรัพยากร Dependency ของ Dawn ทั้งหมด
  • มีโฟลเดอร์ย่อย dawn/ อยู่ในเป้าหมาย
  • แอปของคุณจะขึ้นอยู่กับเป้าหมาย dawn::webgpu_dawn, glfw และ webgpu_glfw เพื่อให้คุณใช้งานในไฟล์ main.cpp ได้ในภายหลัง

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw 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++ ให้สร้างฟังก์ชันตัวช่วย 2 รายการที่ชื่อ GetAdapter() และ GetDevice() ซึ่งจะแสดงผลฟังก์ชัน Callback ที่มี wgpu::Adapter และ wgpu::Device ตามลำดับ

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

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

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


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

วาดสามเหลี่ยม

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

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

ตอนนี้เป็นโอกาสที่ดีในการสร้างไปป์ไลน์การแสดงผลด้วยโค้ดด้านล่าง ประกาศตัวแปร 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 = 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();
}

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

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

การสร้างแอปอีกครั้งด้วย 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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()

อัปเดตโค้ด

ใน Emscripten การสร้าง wgpu::surface ต้องใช้องค์ประกอบ Canvas แบบ HTML สำหรับการดำเนินการนี้ ให้เรียกใช้ instance.CreateSurface() และระบุตัวเลือก #canvas เพื่อจับคู่องค์ประกอบ 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};
  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
}

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

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

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

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

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

ในระหว่างนี้ โปรดส่งปัญหา WebGPU สำหรับ Emscripten และปัญหา Dawn พร้อมคำแนะนำและคำถาม

แหล่งข้อมูล

คุณสามารถสำรวจซอร์สโค้ดของแอปนี้

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

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

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

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

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