From f9df6bf66220845ed25755c5aa94adf4435b853e Mon Sep 17 00:00:00 2001 From: Elias Daler Date: Sun, 24 Mar 2024 14:18:10 +0100 Subject: [PATCH] Examples: GLFW+WebGPU: added support for WebGPU-native/Dawn (#7435, #7132) --- .gitignore | 2 + .../example_emscripten_wgpu/CMakeLists.txt | 103 ++++++++++++++++++ examples/example_emscripten_wgpu/main.cpp | 95 +++++++++++++--- .../example_emscripten_wgpu/web/index.html | 4 + 4 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 examples/example_emscripten_wgpu/CMakeLists.txt diff --git a/.gitignore b/.gitignore index 211d21dda..9570dacea 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ examples/*.out.wasm examples/example_glfw_opengl3/web/* examples/example_sdl2_opengl3/web/* examples/example_emscripten_wgpu/web/* +## Dawn build dependencies +examples/example_emscripten_wgpu/external/* ## JetBrains IDE artifacts .idea diff --git a/examples/example_emscripten_wgpu/CMakeLists.txt b/examples/example_emscripten_wgpu/CMakeLists.txt new file mode 100644 index 000000000..646c5b3c2 --- /dev/null +++ b/examples/example_emscripten_wgpu/CMakeLists.txt @@ -0,0 +1,103 @@ +# Building for desktop (WebGPU-native) with Dawn: +# +# git clone https://github.com/google/dawn dawn +# cmake -B build -DIMGUI_DAWN_DIR=dawn +# cmake --build build +# +# The resulting binary will be found at one of the following locations: +# * build/Debug/example_emscripten_wgpu[.exe] +# * build/example_emscripten_wgpu[.exe] + +# Building for Emscripten: +# +# 1. Install Emscripten SDK following the instructions: https://emscripten.org/docs/getting_started/downloads.html +# 2. Install Ninja build system +# 3. emcmake cmake -G Ninja -B build +# 3. cmake --build build +# 4. emrun build/index.html + +cmake_minimum_required(VERSION 3.10.2) +project(imgui_example_emscripten_wgpu C CXX) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) +endif() + +set(CMAKE_CXX_STANDARD 17) # Dawn requires C++17 + +# Dear ImGui +set(IMGUI_DIR ../../) + +# Libraries +if(EMSCRIPTEN) + set(LIBRARIES glfw) + add_compile_options(-sDISABLE_EXCEPTION_CATCHING=1 -DIMGUI_DISABLE_FILE_FUNCTIONS=1) +else() + # Dawn wgpu desktop + set(DAWN_FETCH_DEPENDENCIES ON) + set(IMGUI_DAWN_DIR CACHE PATH "Path to Dawn repository") + if (NOT IMGUI_DAWN_DIR) + message(FATAL_ERROR "Please specify the Dawn repository by setting IMGUI_DAWN_DIR") + endif() + + option(DAWN_FETCH_DEPENDENCIES "Use fetch_dawn_dependencies.py as an alternative to using depot_tools" ON) + + # Dawn builds many things by default - disable things we don't need + option(DAWN_BUILD_SAMPLES "Enables building Dawn's samples" OFF) + option(TINT_BUILD_CMD_TOOLS "Build the Tint command line tools" OFF) + option(TINT_BUILD_DOCS "Build documentation" OFF) + option(TINT_BUILD_TESTS "Build tests" OFF) + if (NOT APPLE) + option(TINT_BUILD_MSL_WRITER "Build the MSL output writer" OFF) + endif() + if(WIN32) + option(TINT_BUILD_SPV_READER "Build the SPIR-V input reader" OFF) + option(TINT_BUILD_WGSL_READER "Build the WGSL input reader" ON) + option(TINT_BUILD_GLSL_WRITER "Build the GLSL output writer" OFF) + option(TINT_BUILD_GLSL_VALIDATOR "Build the GLSL output validator" OFF) + option(TINT_BUILD_SPV_WRITER "Build the SPIR-V output writer" OFF) + option(TINT_BUILD_WGSL_WRITER "Build the WGSL output writer" ON) + endif() + + add_subdirectory("${IMGUI_DAWN_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/dawn" EXCLUDE_FROM_ALL) + + set(LIBRARIES webgpu_dawn webgpu_cpp webgpu_glfw glfw) +endif() + +add_executable(example_emscripten_wgpu + main.cpp + # backend files + ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp + ${IMGUI_DIR}/backends/imgui_impl_wgpu.cpp + # Dear ImGui files + ${IMGUI_DIR}/imgui.cpp + ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_demo.cpp + ${IMGUI_DIR}/imgui_tables.cpp + ${IMGUI_DIR}/imgui_widgets.cpp +) +target_include_directories(example_emscripten_wgpu PUBLIC + ${IMGUI_DIR} + ${IMGUI_DIR}/backends +) + +target_link_libraries(example_emscripten_wgpu PUBLIC ${LIBRARIES}) + +# Emscripten settings +if(EMSCRIPTEN) + target_link_options(example_emscripten_wgpu PRIVATE + "-sUSE_WEBGPU=1" + "-sUSE_GLFW=3" + "-sWASM=1" + "-sALLOW_MEMORY_GROWTH=1" + "-sNO_EXIT_RUNTIME=0" + "-sASSERTIONS=1" + "-sDISABLE_EXCEPTION_CATCHING=1" + "-sNO_FILESYSTEM=1" + ) + set_target_properties(example_emscripten_wgpu PROPERTIES OUTPUT_NAME "index") + # copy our custom index.html to build directory + add_custom_command(TARGET example_emscripten_wgpu POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_LIST_DIR}/web/index.html" $ + ) +endif() diff --git a/examples/example_emscripten_wgpu/main.cpp b/examples/example_emscripten_wgpu/main.cpp index 43e93a2ad..ad6c3dd1c 100644 --- a/examples/example_emscripten_wgpu/main.cpp +++ b/examples/example_emscripten_wgpu/main.cpp @@ -1,4 +1,6 @@ -// Dear ImGui: standalone example application for Emscripten, using GLFW + WebGPU +// Dear ImGui: standalone example application for using GLFW + WebGPU +// Dawn is used as a WebGPU implementation on desktop and Emscripten is supported for +// publishing on web. // (Emscripten is a C++-to-javascript compiler, used to publish executables for the web. See https://emscripten.org/) // Learn about Dear ImGui: @@ -10,12 +12,17 @@ #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_wgpu.h" + #include + #ifdef __EMSCRIPTEN__ #include #include #include +#else +#include #endif + #include #include #include @@ -26,15 +33,16 @@ #endif // Global WebGPU required states +static WGPUInstance wgpu_instance = nullptr; static WGPUDevice wgpu_device = nullptr; static WGPUSurface wgpu_surface = nullptr; static WGPUTextureFormat wgpu_preferred_fmt = WGPUTextureFormat_RGBA8Unorm; static WGPUSwapChain wgpu_swap_chain = nullptr; -static int wgpu_swap_chain_width = 0; -static int wgpu_swap_chain_height = 0; +static int wgpu_swap_chain_width = 1280; +static int wgpu_swap_chain_height = 720; // Forward declarations -static bool InitWGPU(); +static bool InitWGPU(GLFWwindow* window); static void CreateSwapChain(int width, int height); static void glfw_error_callback(int error, const char* description) @@ -56,6 +64,33 @@ static void wgpu_error_callback(WGPUErrorType error_type, const char* message, v printf("%s error: %s\n", error_type_lbl, message); } +static WGPUAdapter requestAdapter(WGPUInstance instance) { + auto onAdapterRequestEnded = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, char const* message, void* pUserData) { + if (status == WGPURequestAdapterStatus_Success) { + *static_cast(pUserData) = adapter; + } else { + printf("Could not get WebGPU adapter: %s\n", message); + } + }; + WGPUAdapter adapter; + wgpuInstanceRequestAdapter(instance, nullptr, onAdapterRequestEnded, (void*)&adapter); + return adapter; +} + +static WGPUDevice requestDevice(WGPUAdapter& adapter) { + auto onDeviceRequestEnded = [](WGPURequestDeviceStatus status, WGPUDevice device, char const* message, void* pUserData) { + if (status == WGPURequestDeviceStatus_Success) { + *static_cast(pUserData) = device; + } else { + printf("Could not get WebGPU device: %s\n", message); + } + }; + + WGPUDevice device; + wgpuAdapterRequestDevice(adapter, nullptr, onDeviceRequestEnded, (void*)&device); + return device; +} + // Main code int main(int, char**) { @@ -66,18 +101,19 @@ int main(int, char**) // Make sure GLFW does not initialize any graphics context. // This needs to be done explicitly later. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui GLFW+WebGPU example", nullptr, nullptr); + GLFWwindow* window = glfwCreateWindow(wgpu_swap_chain_width, wgpu_swap_chain_height, "Dear ImGui GLFW+WebGPU example", nullptr, nullptr); if (window == nullptr) return 1; // Initialize the WebGPU environment - if (!InitWGPU()) + if (!InitWGPU(window)) { if (window) glfwDestroyWindow(window); glfwTerminate(); return 1; } + CreateSwapChain(wgpu_swap_chain_width, wgpu_swap_chain_height); glfwShowWindow(window); // Setup Dear ImGui context @@ -115,7 +151,7 @@ int main(int, char**) //io.Fonts->AddFontDefault(); #ifndef IMGUI_DISABLE_FILE_FUNCTIONS //io.Fonts->AddFontFromFileTTF("fonts/segoeui.ttf", 18.0f); - io.Fonts->AddFontFromFileTTF("fonts/DroidSans.ttf", 16.0f); + // io.Fonts->AddFontFromFileTTF("fonts/DroidSans.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("fonts/Cousine-Regular.ttf", 15.0f); //io.Fonts->AddFontFromFileTTF("fonts/ProggyTiny.ttf", 10.0f); @@ -200,6 +236,11 @@ int main(int, char**) // Rendering ImGui::Render(); +#ifndef __EMSCRIPTEN__ + // Tick needs to be called in Dawn to display validation errors + wgpuDeviceTick(wgpu_device); +#endif + WGPURenderPassColorAttachment color_attachments = {}; color_attachments.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; color_attachments.loadOp = WGPULoadOp_Clear; @@ -223,6 +264,15 @@ int main(int, char**) WGPUCommandBuffer cmd_buffer = wgpuCommandEncoderFinish(encoder, &cmd_buffer_desc); WGPUQueue queue = wgpuDeviceGetQueue(wgpu_device); wgpuQueueSubmit(queue, 1, &cmd_buffer); + +#ifndef __EMSCRIPTEN__ + wgpuSwapChainPresent(wgpu_swap_chain); +#endif + + wgpuTextureViewRelease(color_attachments.view); + wgpuRenderPassEncoderRelease(pass); + wgpuCommandEncoderRelease(encoder); + wgpuCommandBufferRelease(cmd_buffer); } #ifdef __EMSCRIPTEN__ EMSCRIPTEN_MAINLOOP_END; @@ -239,29 +289,42 @@ int main(int, char**) return 0; } -static bool InitWGPU() +static bool InitWGPU(GLFWwindow* window) { + wgpu::Instance instance = wgpuCreateInstance(nullptr); + +#ifdef __EMSCRIPTEN__ wgpu_device = emscripten_webgpu_get_device(); if (!wgpu_device) return false; +#else + WGPUAdapter adapter = requestAdapter(instance.Get()); + if (!adapter) + return false; + wgpu_device = requestDevice(adapter); +#endif - wgpuDeviceSetUncapturedErrorCallback(wgpu_device, wgpu_error_callback, nullptr); - - // Use C++ wrapper due to misbehavior in Emscripten. - // Some offset computation for wgpuInstanceCreateSurface in JavaScript - // seem to be inline with struct alignments in the C++ structure +#ifdef __EMSCRIPTEN__ wgpu::SurfaceDescriptorFromCanvasHTMLSelector html_surface_desc = {}; html_surface_desc.selector = "#canvas"; - wgpu::SurfaceDescriptor surface_desc = {}; surface_desc.nextInChain = &html_surface_desc; - - wgpu::Instance instance = wgpuCreateInstance(nullptr); wgpu::Surface surface = instance.CreateSurface(&surface_desc); + wgpu::Adapter adapter = {}; wgpu_preferred_fmt = (WGPUTextureFormat)surface.GetPreferredFormat(adapter); +#else + wgpu::Surface surface = wgpu::glfw::CreateSurfaceForWindow(instance, window); + if (!surface) + return false; + wgpu_preferred_fmt = WGPUTextureFormat_BGRA8Unorm; +#endif + + wgpu_instance = instance.MoveToCHandle(); wgpu_surface = surface.MoveToCHandle(); + wgpuDeviceSetUncapturedErrorCallback(wgpu_device, wgpu_error_callback, nullptr); + return true; } diff --git a/examples/example_emscripten_wgpu/web/index.html b/examples/example_emscripten_wgpu/web/index.html index 82b1c4221..6fad6b2a7 100644 --- a/examples/example_emscripten_wgpu/web/index.html +++ b/examples/example_emscripten_wgpu/web/index.html @@ -63,6 +63,10 @@ // Initialize the graphics adapter { + if (!navigator.gpu) { + throw Error("WebGPU not supported."); + } + const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); Module.preinitializedWebGPUDevice = device;