diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d332903e..48082d05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -489,12 +489,12 @@ jobs: popd make -C examples/example_sdl2_opengl3 -f Makefile.emscripten - - name: Build example_emscripten_wgpu + - name: Build example_glfw_wgpu run: | pushd emsdk-master source ./emsdk_env.sh popd - make -C examples/example_emscripten_wgpu -f Makefile.emscripten + make -C examples/example_glfw_wgpu -f Makefile.emscripten Android: runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index 211d21dd..64b5e122 100644 --- a/.gitignore +++ b/.gitignore @@ -40,8 +40,9 @@ examples/*.o.tmp examples/*.out.js examples/*.out.wasm examples/example_glfw_opengl3/web/* +examples/example_glfw_wgpu/web/* +examples/example_glfw_wgpu/external/* examples/example_sdl2_opengl3/web/* -examples/example_emscripten_wgpu/web/* ## JetBrains IDE artifacts .idea diff --git a/backends/imgui_impl_sdl3.cpp b/backends/imgui_impl_sdl3.cpp index e08a6c8b..7e9a7781 100644 --- a/backends/imgui_impl_sdl3.cpp +++ b/backends/imgui_impl_sdl3.cpp @@ -13,7 +13,7 @@ // Issues: // [ ] Platform: Multi-viewport: Minimized windows seems to break mouse wheel events (at least under Windows). // [ ] Platform: Multi-viewport: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). -// [x] Platform: Basic IME support. Position somehow broken in SDL3 + app needs to call 'SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");' before SDL_CreateWindow()!. +// [ ] Platform: IME SUPPORT IS BROKEN IN SDL3 BECAUSE INPUTS GETS SENT TO BOTH APP AND IME + app needs to call 'SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");' before SDL_CreateWindow()!. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -26,6 +26,7 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) // 2024-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2024-04-15: Inputs: Re-enable calling SDL_StartTextInput()/SDL_StopTextInput() as SDL3 no longer enables it by default and should play nicer with IME. // 2024-02-13: Inputs: Fixed gamepad support. Handle gamepad disconnection. Added ImGui_ImplSDL3_SetGamepadMode(). // 2023-11-13: Updated for recent SDL3 API changes. // 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys, app back/forward keys. @@ -129,6 +130,11 @@ static void ImGui_ImplSDL3_SetPlatformImeData(ImGuiViewport* viewport, ImGuiPlat r.w = 1; r.h = (int)data->InputLineHeight; SDL_SetTextInputRect(&r); + SDL_StartTextInput(); + } + else + { + SDL_StopTextInput(); } } diff --git a/backends/imgui_impl_sdl3.h b/backends/imgui_impl_sdl3.h index c6979b9d..ef5497e8 100644 --- a/backends/imgui_impl_sdl3.h +++ b/backends/imgui_impl_sdl3.h @@ -14,6 +14,7 @@ // [ ] Platform: Multi-viewport: Minimized windows seems to break mouse wheel events (at least under Windows). // [ ] Platform: Multi-viewport: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). // [x] Platform: Basic IME support. Position somehow broken in SDL3 + app needs to call 'SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");' before SDL_CreateWindow()!. +// [ ] Platform: IME SUPPORT IS BROKEN IN SDL3 BECAUSE INPUTS GETS SENT TO BOTH APP AND IME + app needs to call 'SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");' before SDL_CreateWindow()!. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. diff --git a/backends/imgui_impl_wgpu.cpp b/backends/imgui_impl_wgpu.cpp index a8324455..6e82ebf4 100644 --- a/backends/imgui_impl_wgpu.cpp +++ b/backends/imgui_impl_wgpu.cpp @@ -753,7 +753,7 @@ bool ImGui_ImplWGPU_Init(ImGui_ImplWGPU_InitInfo* init_info) // Create buffers with a default size (they will later be grown as needed) bd->pFrameResources = new FrameResources[bd->numFramesInFlight]; - for (int i = 0; i < bd->numFramesInFlight; i++) + for (unsigned int i = 0; i < bd->numFramesInFlight; i++) { FrameResources* fr = &bd->pFrameResources[i]; fr->IndexBuffer = nullptr; diff --git a/docs/BACKENDS.md b/docs/BACKENDS.md index e5aa79be..88a96a6b 100644 --- a/docs/BACKENDS.md +++ b/docs/BACKENDS.md @@ -79,14 +79,14 @@ List of Renderer Backends: imgui_impl_sdlrenderer2.cpp ; SDL_Renderer (optional component of SDL2 available from SDL 2.0.18+) imgui_impl_sdlrenderer3.cpp ; SDL_Renderer (optional component of SDL3 available from SDL 3.0.0+) imgui_impl_vulkan.cpp ; Vulkan - imgui_impl_wgpu.cpp ; WebGPU + imgui_impl_wgpu.cpp ; WebGPU (web and desktop) List of high-level Frameworks Backends (combining Platform + Renderer): imgui_impl_allegro5.cpp Emscripten is also supported! -The SDL+GL, GLFW+GL and SDL+WebGPU examples are all ready to build and run with Emscripten. +The SDL+GL, GLFW+GL and GLFW+WebGPU examples are all ready to build and run with Emscripten. ### Backends for third-party frameworks, graphics API or other languages diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 8ef1744b..9ff5a712 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -43,6 +43,16 @@ Breaking changes: Other changes: +- Fonts: Fixed font ascent and descent calculation when a font hits exact integer values. + It is possible that some prior manual use of ImFontConfig::GlyphOffset may become + duplicate with this fix. (#7399, #7404) [@GamingMinds-DanielC] +- Text, DrawList: Improved handling of long single-line wrapped text. Faster and + mitigitate issues with reading vertex indexing limits with 16-bit indices. (#7496, #5720) +- Backends: SDL3: Fixed text inputs. Re-enable calling SDL_StartTextInput()/SDL_StopTextInput() + as SDL3 no longer enables it by default. (#7452, #6306, #6071, #1953) [@Green-Sky] +- Examples: GLFW+WebGPU: Added support for WebGPU-native/Dawn (#7435, #7132) [@eliasdaler, @Zelif] +- Examples: GLFW+WebGPU: Renamed example_emscripten_wgpu/ to example_glfw_wgpu/. (#7435, #7132) + Docking+Viewports Branch: - Docking: when io.ConfigDockingWithShift is enabled, fixed help tooltip erroneously diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index edaddb05..e8134896 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -104,8 +104,8 @@ OSX + OpenGL2 example.
(NB: imgui_impl_osx.mm is currently not as feature complete as other platforms backends. You may prefer to use the GLFW Or SDL backends, which will also support Windows and Linux.) -[example_emscripten_wgpu/](https://github.com/ocornut/imgui/blob/master/examples/example_emscripten_wgpu/)
-Emcripten + GLFW + WebGPU example.
+[example_glfw_wgpu/](https://github.com/ocornut/imgui/blob/master/examples/example_glfw_wgpu/)
+GLFW + WebGPU example. Supports Emscripten (web) or Dawn (desktop)
= main.cpp + imgui_impl_glfw.cpp + imgui_impl_wgpu.cpp Note that the 'example_glfw_opengl3' and 'example_sdl2_opengl3' examples also supports Emscripten! diff --git a/examples/example_glfw_wgpu/CMakeLists.txt b/examples/example_glfw_wgpu/CMakeLists.txt new file mode 100644 index 00000000..e682836d --- /dev/null +++ b/examples/example_glfw_wgpu/CMakeLists.txt @@ -0,0 +1,100 @@ +# Building for desktop (WebGPU-native) with Dawn: +# 1. git clone https://github.com/google/dawn dawn +# 2. cmake -B build -DIMGUI_DAWN_DIR=dawn +# 3. cmake --build build +# The resulting binary will be found at one of the following locations: +# * build/Debug/example_glfw_wgpu[.exe] +# * build/example_glfw_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_glfw_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_glfw_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_glfw_wgpu PUBLIC + ${IMGUI_DIR} + ${IMGUI_DIR}/backends +) + +target_link_libraries(example_glfw_wgpu PUBLIC ${LIBRARIES}) + +# Emscripten settings +if(EMSCRIPTEN) + target_link_options(example_glfw_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_glfw_wgpu PROPERTIES OUTPUT_NAME "index") + # copy our custom index.html to build directory + add_custom_command(TARGET example_glfw_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/Makefile.emscripten b/examples/example_glfw_wgpu/Makefile.emscripten similarity index 100% rename from examples/example_emscripten_wgpu/Makefile.emscripten rename to examples/example_glfw_wgpu/Makefile.emscripten diff --git a/examples/example_emscripten_wgpu/README.md b/examples/example_glfw_wgpu/README.md similarity index 90% rename from examples/example_emscripten_wgpu/README.md rename to examples/example_glfw_wgpu/README.md index e60025da..399d431f 100644 --- a/examples/example_emscripten_wgpu/README.md +++ b/examples/example_glfw_wgpu/README.md @@ -6,7 +6,7 @@ - You may also refer to our [Continuous Integration setup](https://github.com/ocornut/imgui/tree/master/.github/workflows) for Emscripten setup. -- Then build using `make -f Makefile.emscripten` while in the `example_emscripten_wgpu/` directory. +- Then build using `make -f Makefile.emscripten` while in the `example_glfw_wgpu/` directory. - Requires recent Emscripten as WGPU is still a work-in-progress API. @@ -18,7 +18,7 @@ To run on a local machine: - Otherwise, generally you will need a local webserver: - Quoting [https://emscripten.org/docs/getting_started](https://emscripten.org/docs/getting_started/Tutorial.html#generating-html):
_"Unfortunately several browsers (including Chrome, Safari, and Internet Explorer) do not support file:// [XHR](https://emscripten.org/docs/site/glossary.html#term-xhr) requests, and can’t load extra files needed by the HTML (like a .wasm file, or packaged file data as mentioned lower down). For these browsers you’ll need to serve the files using a [local webserver](https://emscripten.org/docs/getting_started/FAQ.html#faq-local-webserver) and then open http://localhost:8000/hello.html."_ - - Emscripten SDK has a handy `emrun` command: `emrun web/example_emscripten_opengl3.html --browser firefox` which will spawn a temporary local webserver (in Firefox). See https://emscripten.org/docs/compiling/Running-html-files-with-emrun.html for details. + - Emscripten SDK has a handy `emrun` command: `emrun web/example_glfw_wgpu.html --browser firefox` which will spawn a temporary local webserver (in Firefox). See https://emscripten.org/docs/compiling/Running-html-files-with-emrun.html for details. - You may use Python 3 builtin webserver: `python -m http.server -d web` (this is what `make serve` uses). - You may use Python 2 builtin webserver: `cd web && python -m SimpleHTTPServer`. - If you are accessing the files over a network, certain browsers, such as Firefox, will restrict Gamepad API access to secure contexts only (e.g. https only). diff --git a/examples/example_emscripten_wgpu/main.cpp b/examples/example_glfw_wgpu/main.cpp similarity index 80% rename from examples/example_emscripten_wgpu/main.cpp rename to examples/example_glfw_wgpu/main.cpp index 43e93a2a..4e47b832 100644 --- a/examples/example_emscripten_wgpu/main.cpp +++ b/examples/example_glfw_wgpu/main.cpp @@ -1,5 +1,6 @@ -// Dear ImGui: standalone example application for Emscripten, using GLFW + WebGPU -// (Emscripten is a C++-to-javascript compiler, used to publish executables for the web. See https://emscripten.org/) +// Dear ImGui: standalone example application for using GLFW + WebGPU +// - Emscripten is supported for publishing on web. See https://emscripten.org. +// - Dawn is used as a WebGPU implementation on desktop. // Learn about Dear ImGui: // - FAQ https://dearimgui.com/faq @@ -11,11 +12,15 @@ #include "imgui_impl_glfw.h" #include "imgui_impl_wgpu.h" #include + #ifdef __EMSCRIPTEN__ #include #include #include +#else +#include #endif + #include #include #include @@ -26,15 +31,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) @@ -66,18 +72,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 +122,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); @@ -148,7 +155,7 @@ int main(int, char**) // React to changes in screen size int width, height; glfwGetFramebufferSize((GLFWwindow*)window, &width, &height); - if (width != wgpu_swap_chain_width && height != wgpu_swap_chain_height) + if (width != wgpu_swap_chain_width || height != wgpu_swap_chain_height) { ImGui_ImplWGPU_InvalidateDeviceObjects(); CreateSwapChain(width, height); @@ -200,6 +207,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 +235,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 +260,72 @@ int main(int, char**) return 0; } -static bool InitWGPU() +#ifndef __EMSCRIPTEN__ +static WGPUAdapter RequestAdapter(WGPUInstance instance) +{ + auto onAdapterRequestEnded = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, const char* message, void* pUserData) + { + if (status == WGPURequestAdapterStatus_Success) + *(WGPUAdapter*)(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, const char* message, void* pUserData) + { + if (status == WGPURequestDeviceStatus_Success) + *(WGPUDevice*)(pUserData) = device; + else + printf("Could not get WebGPU device: %s\n", message); + }; + WGPUDevice device; + wgpuAdapterRequestDevice(adapter, nullptr, onDeviceRequestEnded, (void*)&device); + return device; +} +#endif + +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_glfw_wgpu/web/index.html similarity index 94% rename from examples/example_emscripten_wgpu/web/index.html rename to examples/example_glfw_wgpu/web/index.html index 82b1c422..a2a91c4a 100644 --- a/examples/example_emscripten_wgpu/web/index.html +++ b/examples/example_glfw_wgpu/web/index.html @@ -3,7 +3,7 @@ - Dear ImGui Emscripten+WebGPU example + Dear ImGui Emscripten+GLFW+WebGPU example