diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 47c991cb6..9d8a4cd05 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -86,8 +86,8 @@ jobs: CXX: ${{ matrix.CXX }} CMAKE_PREFIX_PATH: ${{env.QT_ROOT_DIR}}/lib/cmake run: > - cmake -G Ninja - -DCMAKE_BUILD_TYPE=${{matrix.BUILD_TYPE}} + cmake -G Ninja + -DCMAKE_BUILD_TYPE=${{matrix.BUILD_TYPE}} -DALP_ENABLE_ASSERTS=ON -DALP_ENABLE_ADDRESS_SANITIZER=ON -DALP_ENABLE_APP_SHUTDOWN_AFTER_60S=ON @@ -115,7 +115,7 @@ jobs: QT_QPA_PLATFORM: offscreen DISPLAY: :1 LD_PRELOAD: ./libdlclose.so - LSAN_OPTIONS: suppressions=./sanitizer_supressions/linux_leak.supp + LSAN_OPTIONS: suppressions=./misc/sanitizer_suppressions/linux_leak.supp ASAN_OPTIONS: verify_asan_link_order=0 # QSG_RENDER_LOOP: basic run: | diff --git a/.gitignore b/.gitignore index 9db9daf94..5d210db86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ /CMakeLists.txt.user /extern/* -/doc/* -/doc +/docs/binary /build/* +/install/* +.vs +.vscode +.qtcreator +__pycache__ /.qtcreator/CMakeLists.txt.user **/.qmlls.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index d3dcef8b9..c47f98f4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ ############################################################################# # Alpine Terrain Renderer # Copyright (C) 2023 Adam Celarek +# Copyright (C) 2024 Gerald Kimmersdorfer +# Copyright (C) 2024 Patrick Komon # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,9 +19,21 @@ ############################################################################# cmake_minimum_required(VERSION 3.25) -project(alpine-renderer LANGUAGES CXX) +project(alpine-renderer LANGUAGES C CXX) option(ALP_UNITTESTS "include unit test targets in the buildsystem" ON) +option(ALP_GL_ENGINE "include the gl engine in the buildsystem" OFF) +set(ALP_WEBGPU_DEFAULT OFF) +if (EMSCRIPTEN OR (UNIX AND NOT APPLE AND NOT ANDROID)) + set(ALP_WEBGPU_DEFAULT ON) +endif() +option(ALP_WEBGPU_ENGINE "include the webgpu engine in the buildsystem" ${ALP_WEBGPU_DEFAULT}) +option(ALP_WEBGPU_APP "include the webgpu app in the buildsystem" ${ALP_WEBGPU_DEFAULT}) +option(ALP_PLAIN_RENDERER "include the plain renderer in the buildsystem" OFF) +option(ALP_QML_APP "include the qml app in the buildsystem" OFF) + +option(ALP_WEBGPU_APP_ENABLE_COMPUTE "Build the webgpu_compute graph into the app" ON) + option(ALP_ENABLE_ADDRESS_SANITIZER "compiles atb with address sanitizer enabled (only debug, works only on g++ and clang)" OFF) option(ALP_ENABLE_THREAD_SANITIZER "compiles atb with thread sanitizer enabled (only debug, works only on g++ and clang)" OFF) option(ALP_ENABLE_ASSERTS "enable asserts (do not define NDEBUG)" ON) @@ -41,14 +55,14 @@ if(ALP_ENABLE_TRACK_OBJECT_LIFECYCLE) add_definitions(-DALP_ENABLE_TRACK_OBJECT_LIFECYCLE) endif() -if (EMSCRIPTEN) - set(ALP_WWW_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "path to the install directory (for webassembly files, i.e., www directory)") - option(ALP_ENABLE_THREADING "Puts the scheduler into an extra thread." OFF) - option(ALP_ENABLE_DEV_TOOLS "HotReload, Renderstats, .. (increases binary size)" OFF) - option(ALP_ENABLE_POSITIONING "enable qt positioning (gnss / gps)" ON) -elseif(ANDROID) - option(ALP_ENABLE_THREADING "Puts the scheduler into an extra thread." ON) - option(ALP_ENABLE_DEV_TOOLS "HotReload, Renderstats, .. (increases binary size)" OFF) +if (EMSCRIPTEN) + set(ALP_WWW_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "path to the install directory (for webassembly files, i.e., www directory)") + option(ALP_ENABLE_THREADING "Puts the scheduler into an extra thread." OFF) + option(ALP_ENABLE_DEV_TOOLS "HotReload, Renderstats, .. (increases binary size)" OFF) + option(ALP_ENABLE_POSITIONING "enable qt positioning (gnss / gps)" ON) +elseif(ANDROID) + option(ALP_ENABLE_THREADING "Puts the scheduler into an extra thread." ON) + option(ALP_ENABLE_DEV_TOOLS "HotReload, Renderstats, .. (increases binary size)" OFF) option(ALP_ENABLE_POSITIONING "enable qt positioning (gnss / gps)" ON) else() option(ALP_ENABLE_THREADING "Puts the scheduler into an extra thread." ON) @@ -88,13 +102,18 @@ if (ALP_USE_LLVM_LINKER) string(APPEND CMAKE_EXE_LINKER_FLAGS " -fuse-ld=lld") endif() +# Disable Qt debug output in release builds +if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_compile_definitions(QT_NO_DEBUG_OUTPUT) +endif() + ########################################### dependencies ################################################# find_package(Qt6 REQUIRED COMPONENTS Core Gui OpenGL Network Quick QuickControls2 LinguistTools) -qt_standard_project_setup(REQUIRES 6.8) +qt_standard_project_setup(REQUIRES 6.7) alp_add_git_repository(renderer_static_data URL https://github.com/AlpineMapsOrg/renderer_static_data.git COMMITISH v23.11 DO_NOT_ADD_SUBPROJECT) alp_add_git_repository(alpineapp_fonts URL https://github.com/AlpineMapsOrg/fonts.git COMMITISH v24.02 DO_NOT_ADD_SUBPROJECT) -alp_add_git_repository(doc URL https://github.com/AlpineMapsOrg/documentation.git COMMITISH origin/main DO_NOT_ADD_SUBPROJECT DESTINATION_PATH doc) +alp_add_git_repository(doc URL https://github.com/AlpineMapsOrg/documentation.git COMMITISH origin/main DO_NOT_ADD_SUBPROJECT DESTINATION_PATH docs/binary) if (ANDROID) @@ -115,6 +134,29 @@ if (ALP_ENABLE_GL_ENGINE) add_subdirectory(app) endif() -if (ALP_UNITTESTS) - add_subdirectory(unittests) +if (ALP_GL_ENGINE) + add_subdirectory(gl_engine) endif() +if (ALP_PLAIN_RENDERER) + add_subdirectory(plain_renderer) +endif() +if (ALP_QML_APP) + add_subdirectory(app) +endif() +if (ALP_WEBGPU_APP OR ALP_WEBGPU_ENGINE) + add_subdirectory(webgpu/base) + if (ALP_WEBGPU_ENGINE) + add_subdirectory(webgpu/engine) + endif() + if (ALP_WEBGPU_APP) + if (ALP_WEBGPU_APP_ENABLE_COMPUTE) + add_subdirectory(webgpu/compute) + endif() + add_subdirectory(apps/webgpu_app) + endif() + +endif() + +if (ALP_UNITTESTS) + add_subdirectory(unittests) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..c78427ea2 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,162 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "configurePresets": [ + { + "name": "alp-base", + "hidden": true, + "cacheVariables": { + "ALP_ENABLE_GL_ENGINE": "OFF", + "ALP_QML_APP": "OFF", + "ALP_ENABLE_LABELS": "OFF", + "ALP_UNITTESTS": "OFF", + "ALP_WEBGPU_ENGINE": "ON", + "ALP_WEBGPU_APP": "ON", + "ALP_ENABLE_THREADING": "ON", + "ALP_ENABLE_DEV_TOOLS": "ON" + } + }, + { + "name": "msvc-base", + "hidden": true, + "inherits": "alp-base", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "Qt6_DIR": "C:/Qt/6.10.1/msvc2022_64/lib/cmake/Qt6" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-debug", + "displayName": "MSVC Debug (Windows)", + "description": "Debug build using MSVC compiler", + "inherits": "msvc-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "/DQT_DEBUG" + } + }, + { + "name": "msvc-release", + "displayName": "MSVC Release (Windows)", + "description": "Release build using MSVC compiler", + "inherits": "msvc-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "msvc-debug-test", + "displayName": "MSVC Debug Tests (Windows)", + "description": "Debug build for unit tests using MSVC compiler", + "inherits": "msvc-debug", + "cacheVariables": { + "ALP_UNITTESTS": "ON", + "ALP_WEBGPU_APP": "OFF" + } + }, + { + "name": "msvc-release-test", + "displayName": "MSVC Release Tests (Windows)", + "description": "Release build for unit tests using MSVC compiler", + "inherits": "msvc-release", + "cacheVariables": { + "ALP_UNITTESTS": "ON", + "ALP_WEBGPU_APP": "OFF" + } + }, + { + "name": "emscripten-base", + "hidden": true, + "inherits": "alp-base", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "toolchainFile": "C:/Qt/6.10.1/wasm_multithread/lib/cmake/Qt6/qt.toolchain.cmake", + "environment": { + "PATH": "C:/Qt/Tools/Ninja;$penv{PATH}", + "EMSDK": "C:/tmp/alpinemaps/emsdk" + } + }, + { + "name": "wasm-debug", + "displayName": "WebAssembly Debug", + "description": "Debug build for WebAssembly using Emscripten", + "inherits": "emscripten-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-DQT_DEBUG" + } + }, + { + "name": "wasm-release", + "displayName": "WebAssembly Release", + "description": "Release build for WebAssembly using Emscripten", + "inherits": "emscripten-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "wasm-publish", + "displayName": "WebAssembly Publish", + "description": "Production build for WebAssembly using Emscripten", + "inherits": "emscripten-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_FLAGS": "-DQT_NO_DEBUG_OUTPUT -DQT_NO_WARNING_OUTPUT -DQT_NO_INFO_OUTPUT", + "ALP_ENABLE_WGSL_MINIFICATION": "ON" + } + } + ], + "buildPresets": [ + { + "name": "msvc-debug", + "configurePreset": "msvc-debug" + }, + { + "name": "msvc-release", + "configurePreset": "msvc-release" + }, + { + "name": "msvc-debug-test", + "configurePreset": "msvc-debug-test" + }, + { + "name": "msvc-release-test", + "configurePreset": "msvc-release-test" + }, + { + "name": "wasm-debug", + "configurePreset": "wasm-debug" + }, + { + "name": "wasm-release", + "configurePreset": "wasm-release" + }, + { + "name": "wasm-publish", + "configurePreset": "wasm-publish" + } + ] +} diff --git a/README.md b/README.md index ad0ab309a..9ac1b4011 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,67 @@ -# AlpineMaps.org Renderer -This is the software behind [alpinemaps.org](https://alpinemaps.org). +# AlpineMaps.org -A developer version (trunk) is released [here](https://alpinemapsorg.github.io/renderer/), including APKs for android. Be aware that it can break at any time! +![License](https://img.shields.io/github/license/AlpineMapsOrg/renderer) [![AlpineMaps.org | live](https://img.shields.io/badge/AlpineMaps.org-live-brightgreen)](https://alpinemaps.org) [![weBIGeo | live](https://img.shields.io/badge/weBIGeo-live-brightgreen)](https://webigeo.alpinemaps.org/) [![Discord](https://img.shields.io/badge/discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/p8T9XzVwRa) + +This is a mono-repository containing the [AlpineMaps.org](https://alpinemaps.org) and [weBIGeo](https://webigeo.alpinemaps.org/) projects alongside their shared dependencies. Both are under active development and aim to provide state-of-the-art real-time rendering and processing for large-scale, tile-based geodata. + +### [AlpineMaps.org (`app`)](docs/app.md) +Qt Quick / OpenGL frontend, the original alpinemaps.org client. + +### [weBIGeo (`webgpu_app`)](docs/webgpu_app.md) +WebGPU rendering engine with ImGui UI and GPU compute graph. [If looking at the issues, best to filter out projects!](https://github.com/AlpineMapsOrg/renderer/issues?q=is%3Aissue%20state%3Aopen%20no%3Aproject) We are in discord, talk to us! https://discord.gg/p8T9XzVwRa -# Cloning and building -`git clone git@github.com:AlpineMapsOrg/renderer.git` - -After that it should be a normal cmake project. That is, you run cmake to generate a project or build file and then run your favourite tool. All dependencies should be pulled automatically while you run CMake. -We use Qt Creator (with mingw on Windows), which is the only tested setup atm and makes setup of Android and WebAssembly builds reasonably easy. If you have questions, please go to Discord. - -## Dependencies -* Qt 6.11.1, or greater -* g++ 12+, clang or msvc -* OpenGL -* Qt Positioning and Charts modules -* Some other dependencies will be pulled automatically during building. - -## Building the native version -* just run cmake and build - -## Building the android version -* We are usually building with Qt Creator, because it works relatively out of the box. However, it should also work on the command line or other IDEs if you set it up correctly. -* You need a Java JDK before you can do anything else. Not all Java versions work, and the error messages might be surprising (or non-existant). I'm running with Java 19, and I can compile for old devices. Iirc a newer version of Java caused issues. [Android documents the required Java version](https://developer.android.com/build/jdks), but as said, for me Java 19 works as well. It might change in the future. -* Once you have Java, go to Qt Creator Preferences -> Devices -> Android. There click "Set Up SDK" to automatically download and install an Android SDK. -* Finally, you might need to click on SDK Manager to install a fitting SDK Platform (take the newest, it also works for older devices), and ndk (newest as well). -* Then Google the internet to find out how to enable the developer mode on Android. -* On linux, you'll have to setup some udev rules. Run `Android/SDK/platform-tools/adb devices` and you should get instructions. -* If there are problems, check out the [documentation from Qt](https://doc.qt.io/qt-6/android-getting-started.html) -* Finally, you are welcome to ask in discord if something is not working! - -## Building the WebAssembly version: -* [The Qt documentation is quite good on how to get it to run](https://doc-snapshots.qt.io/qt6-dev/wasm.html#installing-emscripten). -* Be aware that only specific versions of emscripten work for specific versions of Qt, and the error messages are not helpfull. -* [More info on building and getting Hotreload to work](https://github.com/AlpineMapsOrg/documentation/blob/main/WebAssembly_local_build.md) - -# Code style + +## Architecture + +```mermaid +graph TD + app["AlpineMaps.org [app]"] + webgpu_app["weBIGeo [webgpu_app]"] + gl_engine(["gl_engine"]) + webgpu_compute(["webgpu_compute"]) + webgpu(["webgpu"]) + nucleus(["nucleus"]) + webgpu_engine(["webgpu_engine"]) + + app ---> gl_engine + webgpu_app --> webgpu_engine + webgpu_app --> webgpu_compute + gl_engine --> nucleus + webgpu_engine --> webgpu + webgpu_compute --> webgpu + webgpu --> nucleus + + +``` + +*...only top-level dependencies are shown.* + +| Module | Description | +|--------|-------------| +| `nucleus` | Shared core: tile management, camera, data structures | +| [`webgpu`](docs/webgpu_base.md) | RAII WebGPU wrappers (device, pipelines, buffers), WGSL preprocessor and GPU resource registry | +| `gl_engine` | OpenGL rendering engine (used by the QML app) | +| [`webgpu_engine`](docs/webgpu_engine.md) | WebGPU rendering engine (used by the webgpu_app) | +| `webgpu_compute` | CPU/GPU compute node graph library | + +## Code style * class names are CamelCase, method, function and variable names are snake_case. * class attributes have an m_ prefix and are usually private, struct attributes don't and are usually public. -* use `void set_attribute(int value)` and `int attribute() const` for setters and getters (that is, avoid the get_). Use [the Qt recommendations](https://wiki.qt.io/API_Design_Principles#Naming_Boolean_Getters,_Setters,_and_Properties) for naming boolean getters. +* "use `void set_attribute(int value)` and `int attribute() const` for setters and getters (that is, avoid the get_)." Use Qt recommendations for naming boolean getters. * structs are usually small, simple, and have no or only few methods. they never have inheritance. * files are CamelCase if the content is a CamelCase class. otherwise they are snake_case, and have a snake_case namespace with stuff. * the folder/structure.h is reflected in namespace folder::structure{ .. } * indent with space only, indent 4 spaces * ideally, use the clang-format file provided with the project (in case you use Qt Creator, go to Preferences -> C++ -> Code Style: Formatting mode: Full, Format while typing, Format edited code on file save, don't override formatting) -* follow the [Qt recommendations](https://wiki.qt.io/API_Design_Principles) and the [c++ core guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) everywhere else. +* follow the Qt recommendations and the C++ core guidelines everywhere else. -# Developer workflow +## Developer workflow * Fork this repository. * Enable github pages from actions (Repository Settings -> Pages -> Source -> GitHub Actions) * Work in branches or your main. @@ -59,4 +69,4 @@ We use Qt Creator (with mingw on Windows), which is the only tested setup atm an * Github Actions will run the unit tests and create packages for the browser and Android and deploy them to your_username.github.io/your_clone_name/. * Make sure that the unit tests run through. * We will also look at the browser version during the pull request. -* Ideally you'll also setup the signing keys for Android packages ([instructions](https://github.com/AlpineMapsOrg/renderer/blob/main/creating_apk_keys.md)). +* Ideally you'll also setup the signing keys for Android packages. diff --git a/app/TerrainRendererItem.cpp b/app/TerrainRendererItem.cpp index fa369b991..68c861ceb 100644 --- a/app/TerrainRendererItem.cpp +++ b/app/TerrainRendererItem.cpp @@ -164,6 +164,7 @@ QQuickFramebufferObject::Renderer* TerrainRendererItem::createRenderer() const return r; } + void TerrainRendererItem::touchEvent(QTouchEvent* e) { this->setFocus(true); diff --git a/apps/webgpu_app/App.cpp b/apps/webgpu_app/App.cpp new file mode 100644 index 000000000..7c40c0809 --- /dev/null +++ b/apps/webgpu_app/App.cpp @@ -0,0 +1,655 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "App.h" + +#include "webgpu/engine/Window.h" +#include +#include +#include +#include //TODO maybe only for threading enabled? +#include + +#ifdef __EMSCRIPTEN__ +#include "util/WebInterop.h" +#include +#else +#include "nucleus/utils/image_loader.h" +#pragma comment(lib, "dwmapi.lib") +#endif + +#include "util/dark_mode.h" + +#include "imgui.h" +#include "util/error_logging.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_app { + +App::App() +{ +#ifdef __EMSCRIPTEN__ + // execute on window resize when canvas size changes + QObject::connect(&WebInterop::instance(), &WebInterop::body_size_changed, this, &App::set_window_size); + + m_surface_presentmode = WGPUPresentMode_Fifo; // chrome does not want other present modes +#endif +} + +void App::init_window() +{ + // Initializes SDL2 video subsystem + SDL_SetMainReady(); + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + qFatal("Could not initialize SDL2 video subsystem! SDL_Error: %s", SDL_GetError()); + } + +#ifdef __EMSCRIPTEN__ + // Fetch size of the webpage + m_viewport_size = WebInterop::instance().get_body_size(); +#endif + m_sdl_window = SDL_CreateWindow("weBIGeo - Geospatial Visualization Tool", // Window title + SDL_WINDOWPOS_CENTERED, // Window position x + SDL_WINDOWPOS_CENTERED, // Window position y + m_viewport_size.x, // Window width + m_viewport_size.y, // Window height +#ifdef __EMSCRIPTEN__ + SDL_WINDOW_RESIZABLE); +#else + SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED); +#endif + + if (!m_sdl_window) { + SDL_Quit(); + qFatal("Could not create SDL window! SDL_Error: %s", SDL_GetError()); + } + + util::enable_darkmode_on_windows(m_sdl_window); + +#ifndef __EMSCRIPTEN__ + // Load icon using the existing image loader + auto icon = nucleus::utils::image_loader::rgba8(":/icons/logo32.png").value(); + // Create SDL_Surface from the raw image data + SDL_Surface* iconSurface = SDL_CreateRGBSurfaceFrom((void*)icon.bytes(), // Pixel data + icon.width(), // Image width + icon.height(), // Image height + 32, // Bits per pixel (RGBA = 32 bits) + icon.width() * 4, // Pitch (width * 4 bytes per pixel) + 0x000000ff, // Red mask + 0x0000ff00, // Green mask + 0x00ff0000, // Blue mask + 0xff000000 // Alpha mask + ); + + if (iconSurface) { + SDL_SetWindowIcon(m_sdl_window, iconSurface); // Set the window icon + SDL_FreeSurface(iconSurface); // Free the surface after setting the icon + } else { + qWarning("Could not create SDL surface for window icon. SDL_Error: %s", SDL_GetError()); + } +#endif +} + +void App::render_gui() +{ + static bool vsync_enabled = (m_surface_presentmode == WGPUPresentMode::WGPUPresentMode_Fifo); + if (ImGui::Checkbox("VSync", &vsync_enabled)) { + m_surface_presentmode = vsync_enabled ? WGPUPresentMode::WGPUPresentMode_Fifo : WGPUPresentMode::WGPUPresentMode_Immediate; + // Reconfigure surface + m_force_repaint_once = true; + this->on_window_resize(m_viewport_size.x, m_viewport_size.y); + } + ImGui::Checkbox("Repaint each frame", &m_force_repaint); + ImGui::Text("Repaint-Counter: %d", m_repaint_count); + + if (ImGui::Button("Reload shaders [F5]", ImVec2(350, 0))) { + m_webgpu_window->reload_shaders(); + } +} + +void App::poll_events() +{ + // TODO hack, makes animations work + m_camera_controller->advance_camera(); + + // NOTE: The following line is not strictly necessary, we discovered that SDL somehow + // triggers the processing of qt events. On the web we assume that Qt attaches itself to + // the emscripten event loop. + QCoreApplication::processEvents(); + // Poll SDL events and handle them. + // (contrary to GLFW, close event is not automatically managed, and there + // is no callback mechanism by default.) + static SDL_Event events[15]; // Only allocate memory once (11 is the max events at once i witnessed) + bool events_contain_touch = false; + int event_count = 0; + static SDL_Event event; + while (SDL_PollEvent(&event)) { + m_gui_manager->on_sdl_event(event); + if (event.type == SDL_QUIT) { + m_window_open = false; + } else if (event.type == SDL_WINDOWEVENT) { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + on_window_resize(event.window.data1, event.window.data2); + } + } else { + events[event_count++] = event; + if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION) { + events_contain_touch = true; + } + } + } + + // IMPORTANT: SDL seems to emulate touch events as mouse events aswell. In order to avoid this + // we need to filter out the mouse events if there are touch events. Meaning we priortize touch over mouse. + for (int i = 0; i < event_count; i++) { + if (events_contain_touch && (events[i].type == SDL_MOUSEMOTION || events[i].type == SDL_MOUSEBUTTONDOWN || events[i].type == SDL_MOUSEBUTTONUP)) { + continue; + } + m_input_mapper->on_sdl_event(events[i]); + } + + wgpuInstanceProcessEvents(m_instance); +} + +void App::render() +{ + // Do nothing, this checks for ongoing asynchronous operations and call their callbacks + + WGPUSurfaceTexture surface_texture; + wgpuSurfaceGetCurrentTexture(m_surface, &surface_texture); + + m_cputimer->start(); + + if (surface_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal + && surface_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) { + // skip frame (?) + qDebug() << "Could not get current surface texture: surface_texture.status=" << surface_texture.status; + return; + } + + WGPUTextureViewDescriptor viewDescriptor {}; + viewDescriptor.nextInChain = nullptr; + viewDescriptor.label = WGPUStringView { .data = "Surface texture view", .length = WGPU_STRLEN }; + viewDescriptor.format = wgpuTextureGetFormat(surface_texture.texture); + viewDescriptor.dimension = WGPUTextureViewDimension_2D; + viewDescriptor.baseMipLevel = 0; + viewDescriptor.mipLevelCount = 1; + viewDescriptor.baseArrayLayer = 0; + viewDescriptor.arrayLayerCount = 1; + viewDescriptor.aspect = WGPUTextureAspect_All; + WGPUTextureView surface_texture_view = wgpuTextureCreateView(surface_texture.texture, &viewDescriptor); + + if (!surface_texture_view) { + qFatal("Cannot acquire next surface texture"); + } + + WGPUCommandEncoderDescriptor command_encoder_desc {}; + command_encoder_desc.label = WGPUStringView { .data = "Command Encoder", .length = WGPU_STRLEN }; + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(m_device, &command_encoder_desc); + + if (webgpu::isTimingSupported()) + m_gputimer->start(encoder); + + m_frame_count++; + if (m_webgpu_window->needs_redraw() || m_force_repaint || m_force_repaint_once) { + m_webgpu_window->paint(m_framebuffer.get(), encoder); + m_repaint_count++; + m_force_repaint_once = false; + } + + { + webgpu::raii::RenderPassEncoder render_pass(encoder, surface_texture_view, nullptr); + wgpuRenderPassEncoderSetPipeline(render_pass.handle(), m_gui_pipeline.get()->pipeline().handle()); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 0, m_gui_bind_group->handle(), 0, nullptr); + wgpuRenderPassEncoderDraw(render_pass.handle(), 3, 1, 0, 0); + + // We add the GUI drawing commands to the render pass + m_gui_manager->render(render_pass.handle()); + } + + if (webgpu::isTimingSupported()) + m_gputimer->stop(encoder); + + wgpuTextureViewRelease(surface_texture_view); + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "Command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder, &cmd_buffer_descriptor); + wgpuCommandEncoderRelease(encoder); + wgpuQueueSubmit(m_queue, 1, &command); + wgpuCommandBufferRelease(command); + + if (webgpu::isTimingSupported()) + m_gputimer->resolve(); + + m_cputimer->stop(); + +#ifndef __EMSCRIPTEN__ + // Surface present in the WEB is handled by the browser! + wgpuSurfacePresent(m_surface); + wgpuDeviceTick(m_device); +#endif +} + +void App::start() +{ + init_window(); + + webgpu_create_context(); + + m_context = std::make_unique(); + configure_surface(m_viewport_size.x, m_viewport_size.y); + m_webgpu_ctx.set_surface_texture_format(m_surface_texture_format); + m_context->initialize(m_webgpu_ctx); + + m_camera_controller = std::make_unique( + nucleus::camera::PositionStorage::instance()->get("grossglockner"), m_webgpu_window.get(), m_context->data_querier()); + + // clang-format off + // NOTICE ME!!!! READ THIS, IF YOU HAVE TROUBLES WITH SIGNALS NOT REACHING THE QML RENDERING THREAD!!!!111elevenone + // In Qt/QML the rendering thread goes to sleep (at least until Qt 6.5, See RenderThreadNotifier). + // At the time of writing, an additional connection from tile_ready and tile_expired to the notifier is made. + // this only works if ALP_ENABLE_THREADING is on, i.e., the tile scheduler is on an extra thread. -> potential issue on webassembly + connect(m_camera_controller.get(), &nucleus::camera::Controller::definition_changed, m_context->geometry_scheduler(), &nucleus::tile::Scheduler::update_camera); + connect(m_camera_controller.get(), &nucleus::camera::Controller::definition_changed, m_context->ortho_scheduler(), &nucleus::tile::Scheduler::update_camera); + connect(m_camera_controller.get(), &nucleus::camera::Controller::definition_changed, m_context->cloud_scheduler(), &nucleus::tile::Scheduler::update_camera); + connect(m_camera_controller.get(), &nucleus::camera::Controller::definition_changed, m_webgpu_window.get(), &webgpu_engine::Window::update_camera); + + connect(m_context->geometry_scheduler(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, m_webgpu_window.get(), &webgpu_engine::Window::update_requested); + connect(m_context->ortho_scheduler(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, m_webgpu_window.get(), &webgpu_engine::Window::update_requested); + connect(m_context->cloud_scheduler(), &nucleus::tile::Texture3DScheduler::gpu_tiles_updated, m_webgpu_window.get(), &webgpu_engine::Window::update_requested); + connect(m_context->clouds_manager(), &clouds::Manager::shadow_texture_ready, m_webgpu_window.get(), &webgpu_engine::Window::on_shadow_texture_updated); + // clang-format on + + m_gui_manager = std::make_unique(this); + + m_input_mapper = std::make_unique(this, m_camera_controller.get(), m_gui_manager.get(), [this]() { return m_viewport_size; }); + + // TODO connect this (is used from ImGuiManager to update camera when settings are changed) + // connect(this, &App::update_camera_requested, camera_controller, &nucleus::camera::Controller::update_camera_request); + connect(m_webgpu_window.get(), + &webgpu_engine::Window::set_camera_definition_requested, + m_camera_controller.get(), + &nucleus::camera::Controller::set_model_matrix); + + connect(m_webgpu_window.get(), &nucleus::AbstractRenderWindow::update_requested, this, &App::schedule_update); + connect(m_input_mapper.get(), &InputMapper::key_pressed, this, &App::handle_shortcuts); + + m_webgpu_window->set_context(m_context->engine_context()); + m_webgpu_window->initialise_gpu(); + + // Configures surface + this->on_window_resize(m_viewport_size.x, m_viewport_size.y); + + { // load first camera definition without changing preset in nucleus + auto new_definition = nucleus::camera::stored_positions::grossglockner(); + new_definition.set_viewport_size(m_viewport_size); + m_camera_controller->set_model_matrix(new_definition); + } + + qDebug() << "Create GUI Pipeline..."; + m_gui_ubo = std::make_unique>(m_device, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, 1, "gui ubo"); + m_gui_ubo->write(m_queue, &m_gui_ubo_data); + + webgpu::FramebufferFormat format {}; + format.color_formats.emplace_back(m_surface_texture_format); + + WGPUBindGroupLayoutEntry backbuffer_texture_entry {}; + backbuffer_texture_entry.binding = 0; + backbuffer_texture_entry.visibility = WGPUShaderStage_Fragment; + backbuffer_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + backbuffer_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry gui_ubo_entry = {}; + gui_ubo_entry.binding = 1; + gui_ubo_entry.visibility = WGPUShaderStage_Fragment; + gui_ubo_entry.buffer.type = WGPUBufferBindingType_Uniform; + gui_ubo_entry.buffer.minBindingSize = sizeof(App::GuiPipelineUBO); + + m_gui_bind_group_layout = std::make_unique( + m_device, std::vector { backbuffer_texture_entry, gui_ubo_entry }, "gui bind group layout"); + + const char preprocessed_code[] = R"( + @group(0) @binding(0) var backbuffer_texture : texture_2d; + @group(0) @binding(1) var gui_ubo : vec2f; + + struct VertexOut { + @builtin(position) position : vec4f, + @location(0) texcoords : vec2f + } + + @vertex + fn vertexMain(@builtin(vertex_index) vertex_index : u32) -> VertexOut { + const VERTICES = array(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); + var vertex_out : VertexOut; + vertex_out.position = vec4(VERTICES[vertex_index], 0.0, 1.0); + vertex_out.texcoords = vec2(0.5, -0.5) * vertex_out.position.xy + vec2(0.5); + return vertex_out; + } + + @fragment + fn fragmentMain(vertex_out : VertexOut) -> @location(0) vec4f { + let tci : vec2 = vec2u(vertex_out.texcoords * gui_ubo); + var backbuffer_color = textureLoad(backbuffer_texture, tci, 0); + return backbuffer_color; + } + )"; + + WGPUShaderSourceWGSL wgsl_desc {}; + wgsl_desc.chain.next = nullptr; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_desc.code = WGPUStringView { + .data = preprocessed_code, + .length = WGPU_STRLEN, + }; + WGPUShaderModuleDescriptor shader_module_desc {}; + shader_module_desc.label = WGPUStringView { .data = "Gui Shader Module", .length = WGPU_STRLEN }; + shader_module_desc.nextInChain = &wgsl_desc.chain; + auto shader_module = std::make_unique(m_device, shader_module_desc); + + m_gui_pipeline = std::make_unique(m_device, + *shader_module, + *shader_module, + std::vector {}, + format, + std::vector { m_gui_bind_group_layout.get() }); + + m_gui_bind_group = std::make_unique(m_device, + *m_gui_bind_group_layout.get(), + std::initializer_list { m_framebuffer->color_texture_view(0).create_bind_group_entry(0), m_gui_ubo->create_bind_group_entry(1) }); + + m_timer_manager = std::make_unique(); + m_gui_manager->init(m_sdl_window, m_device, m_surface_texture_format, WGPUTextureFormat_Undefined); + + m_cputimer = std::make_shared(120); + m_timer_manager->add_timer(m_cputimer, "CPU Timer", "Renderer"); + if (webgpu::isTimingSupported()) { + m_gputimer = std::make_shared(m_device, 3, 120); + m_timer_manager->add_timer(m_gputimer, "GPU Timer", "Renderer"); + } + + this->on_window_resize(m_viewport_size.x, m_viewport_size.y); + m_initialized = true; + + qInfo() << "App ready"; + m_webgpu_window->ready(); + + m_gui_manager->ready(); + +#if defined(__EMSCRIPTEN__) + emscripten_set_main_loop_arg( + [](void* userData) { + App& renderer = *reinterpret_cast(userData); + renderer.poll_events(); + renderer.render(); + }, + (void*)this, + 0, + true); +#else + while (m_window_open) { + poll_events(); + render(); + } +#endif + + // NOTE: Ressources are freed by the browser when the page is closed. Also keep in mind + // that this part of code will be executed immediately since the main loop is not blocking. +#ifndef __EMSCRIPTEN__ + m_gui_manager->shutdown(); + webgpu_release_context(); + m_webgpu_window->destroy(); + m_context->destroy(); + + SDL_DestroyWindow(m_sdl_window); + SDL_Quit(); + m_initialized = false; +#endif +} + +void App::set_window_size(glm::uvec2 size) +{ + if (m_viewport_size == size) + return; + m_viewport_size = size; + if (m_initialized) { + SDL_SetWindowSize(m_sdl_window, size.x, size.y); + on_window_resize(size.x, size.y); + } +} + +void App::handle_shortcuts(QKeyCombination key) +{ + if (key.key() == Qt::Key_F5) { + m_webgpu_window->reload_shaders(); + } else if (key.key() == Qt::Key_H) { + m_gui_manager->set_gui_visibility(!m_gui_manager->get_gui_visibility()); + } +} + +void App::schedule_update() { m_force_repaint_once = true; } + +void App::create_framebuffer(uint32_t width, uint32_t height) +{ + qDebug() << "creating framebuffer textures for size " << width << "x" << height; + + webgpu::FramebufferFormat format { .size = { width, height }, .depth_format = m_depth_texture_format, .color_formats = { m_surface_texture_format } }; + m_framebuffer = std::make_unique(m_device, format); + + if (m_gui_bind_group) { + m_gui_bind_group = std::make_unique(m_device, + *m_gui_bind_group_layout.get(), + std::initializer_list { + m_framebuffer->color_texture_view(0).create_bind_group_entry(0), m_gui_ubo->create_bind_group_entry(1) }); + } + + if (m_gui_ubo) { + m_gui_ubo_data.resolution = glm::vec2(m_viewport_size); + m_gui_ubo->write(m_queue, &m_gui_ubo_data); + } +} + +void App::configure_surface(uint32_t width, uint32_t height) +{ + qDebug() << "configuring surface..."; + + // from Learn WebGPU C++ tutorial + + WGPUSurfaceCapabilities surface_capabilities {}; + wgpuSurfaceGetCapabilities(m_surface, m_adapter, &surface_capabilities); + if (surface_capabilities.formatCount < 1) { + qFatal() << "WebGPU surface formatCount is 0 - must support at least one format"; + } + + m_surface_texture_format = surface_capabilities.formats[0]; + WGPUSurfaceConfiguration config = {}; + config.nextInChain = nullptr; + config.width = width; + config.height = height; + config.format = m_surface_texture_format; + config.viewFormatCount = 0; + config.viewFormats = nullptr; + config.usage = WGPUTextureUsage_RenderAttachment; + config.device = m_device; + config.presentMode = m_surface_presentmode; + config.alphaMode = WGPUCompositeAlphaMode_Auto; + + qInfo() << "trying to configure surface with size " << width << "x" << height << "alpha mode=" << config.alphaMode + << ", present mode=" << m_surface_presentmode; + wgpuSurfaceConfigure(m_surface, &config); + qInfo() << "configured surface with size " << width << "x" << height << ", present mode=" << m_surface_presentmode; +} + +void App::update_camera() { emit update_camera_requested(); } + +void App::on_window_resize(int width, int height) +{ + m_viewport_size = { width, height }; + + configure_surface(width, height); + create_framebuffer(width, height); + + m_webgpu_window->resize_framebuffer(m_viewport_size.x, m_viewport_size.y); + m_camera_controller->set_viewport(m_viewport_size); +} + +void App::webgpu_create_context() +{ + qDebug() << "Creating WebGPU instance..."; + m_instance_desc = {}; + m_instance_desc.nextInChain = nullptr; + +#ifndef __EMSCRIPTEN__ + WGPUDawnTogglesDescriptor dawnToggles; + dawnToggles.chain.next = nullptr; + dawnToggles.chain.sType = WGPUSType_DawnTogglesDescriptor; + + std::vector enabledToggles = { "allow_unsafe_apis" }; +#if defined(QT_DEBUG) + // TODO: Figure out why this doesnt work + enabledToggles.push_back("use_user_defined_labels_in_backend"); + enabledToggles.push_back("enable_vulkan_validation"); + enabledToggles.push_back("disable_symbol_renaming"); +#endif + + QStringList toggleList; + for (const auto& toggle : enabledToggles) + toggleList << QString::fromStdString(toggle); + qDebug() << "Dawn toggles:" << toggleList.join(", "); + + dawnToggles.enabledToggles = enabledToggles.data(); + dawnToggles.enabledToggleCount = enabledToggles.size(); + dawnToggles.disabledToggleCount = 0; +#endif + + const auto timed_wait_feature = WGPUInstanceFeatureName_TimedWaitAny; + m_instance_desc.requiredFeatureCount = 1; + m_instance_desc.requiredFeatures = &timed_wait_feature; + + m_instance = wgpuCreateInstance(&m_instance_desc); + + if (!m_instance) { + qFatal("Could not initialize WebGPU Instance!"); + } + qInfo() << "Got instance: " << m_instance; + + qDebug() << "Requesting surface..."; + m_surface = SDL_GetWGPUSurface(m_instance, m_sdl_window); + if (!m_surface) { + qFatal("Could not create surface!"); + } + qInfo() << "Got surface: " << m_surface; + + qDebug() << "Requesting adapter..."; + WGPURequestAdapterOptions adapter_opts {}; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + adapter_opts.compatibleSurface = m_surface; + m_adapter = webgpu::requestAdapterSync(m_instance, adapter_opts); + if (!m_adapter) { + qFatal("Could not get adapter!"); + } + qInfo() << "Got adapter: " << m_adapter; + + m_webgpu_window = std::make_unique(); + + qDebug() << "Requesting device..."; + WGPULimits required_limits {}; + WGPULimits supported_limits {}; + wgpuAdapterGetLimits(m_adapter, &supported_limits); + + // irrelevant for us, but needs to be set + required_limits.minStorageBufferOffsetAlignment = supported_limits.minStorageBufferOffsetAlignment; + required_limits.minUniformBufferOffsetAlignment = supported_limits.minUniformBufferOffsetAlignment; + required_limits.maxInterStageShaderVariables = WGPU_LIMIT_U32_UNDEFINED; // required for current version of Chrome Canary (2025-04-03) + constexpr uint64_t desired_max_buffer_size = 2 * 1073741824ull; // 2 GiB + if (supported_limits.maxBufferSize < desired_max_buffer_size) + qWarning() << "Adapter maxBufferSize" << supported_limits.maxBufferSize << "is below the desired" << desired_max_buffer_size + << ". cloud rendering might fail."; + required_limits.maxBufferSize = std::min(supported_limits.maxBufferSize, desired_max_buffer_size); + + // Let the engine change the required limits + m_webgpu_window->update_required_gpu_limits(required_limits, supported_limits); + + std::vector requiredFeatures; + requiredFeatures.push_back(WGPUFeatureName_TimestampQuery); + requiredFeatures.push_back(WGPUFeatureName_TextureCompressionBC); + requiredFeatures.push_back(WGPUFeatureName_TextureCompressionBCSliced3D); + + WGPUDeviceDescriptor device_desc {}; + device_desc.label = WGPUStringView { .data = "webigeo device", .length = WGPU_STRLEN }; + device_desc.requiredFeatures = requiredFeatures.data(); + device_desc.requiredFeatureCount = (uint32_t)requiredFeatures.size(); + device_desc.requiredLimits = &required_limits; + device_desc.defaultQueue.label = WGPUStringView { .data = "webigeo queue", .length = WGPU_STRLEN }; + device_desc.uncapturedErrorCallbackInfo = WGPUUncapturedErrorCallbackInfo { + .nextInChain = nullptr, + .callback = webgpu_device_error_callback, + .userdata1 = nullptr, + .userdata2 = nullptr, + }; + device_desc.deviceLostCallbackInfo = WGPUDeviceLostCallbackInfo { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = webgpu_device_lost_callback, + .userdata1 = nullptr, + .userdata2 = nullptr, + }; + +#ifndef __EMSCRIPTEN__ + device_desc.nextInChain = &dawnToggles.chain; +#endif + + m_device = webgpu::requestDeviceSync(m_instance, m_adapter, device_desc); + if (!m_device) { + qFatal("Could not get device!"); + } + qInfo() << "Got device: " << m_device; + + webgpu::checkForTimingSupport(m_adapter, m_device); + + qDebug() << "Requesting queue..."; + m_queue = wgpuDeviceGetQueue(m_device); + if (!m_queue) { + qFatal("Could not get queue!"); + } + qInfo() << "Got queue: " << m_queue; + + m_webgpu_ctx.init(m_instance, m_device, m_adapter, m_surface, m_queue); +} + +void App::webgpu_release_context() +{ + qDebug() << "Releasing WebGPU context..."; + wgpuSurfaceUnconfigure(m_surface); + wgpuQueueRelease(m_queue); + wgpuSurfaceRelease(m_surface); + wgpuDeviceRelease(m_device); + wgpuAdapterRelease(m_adapter); + wgpuInstanceRelease(m_instance); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/App.h b/apps/webgpu_app/App.h new file mode 100644 index 000000000..07b80de5e --- /dev/null +++ b/apps/webgpu_app/App.h @@ -0,0 +1,135 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiManager.h" +#include "util/InputMapper.h" +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "webgpu/engine/Context.h" +#include "webgpu/engine/Window.h" + +#include "RenderingContext.h" + +namespace webgpu_app { + +class App : public QObject { + Q_OBJECT + +public: + App(); + ~App() = default; + + struct GuiPipelineUBO { + glm::vec2 resolution; + }; + + void init_window(); + void start(); + void poll_events(); + void render(); + void render_gui(); + void on_window_resize(int width, int height); + void update_camera(); + + [[nodiscard]] InputMapper* get_input_mapper() { return m_input_mapper.get(); } + [[nodiscard]] ImGuiManager* get_gui_manager() { return m_gui_manager.get(); } + [[nodiscard]] webgpu::timing::GuiTimerManager* get_timer_manager() { return m_timer_manager.get(); } + [[nodiscard]] webgpu_engine::Window* get_webgpu_window() { return m_webgpu_window.get(); } + [[nodiscard]] RenderingContext* get_rendering_context() { return m_context.get(); } + [[nodiscard]] nucleus::camera::Controller* get_camera_controller() { return m_camera_controller.get(); } + +signals: + void update_camera_requested(); + +private slots: + void set_window_size(glm::uvec2 size); + void handle_shortcuts(QKeyCombination key); + void schedule_update(); + +private: + SDL_Window* m_sdl_window; + std::unique_ptr m_webgpu_window; + std::unique_ptr m_camera_controller; + std::unique_ptr m_context; + + std::unique_ptr m_input_mapper; + std::unique_ptr m_gui_manager; + std::unique_ptr m_timer_manager; + + WGPUInstanceDescriptor m_instance_desc; + + webgpu::Context m_webgpu_ctx; + + WGPUInstance m_instance = nullptr; + WGPUSurface m_surface = nullptr; + WGPUAdapter m_adapter = nullptr; + WGPUDevice m_device = nullptr; + WGPUQueue m_queue = nullptr; + WGPUTextureFormat m_surface_texture_format = WGPUTextureFormat::WGPUTextureFormat_Undefined; // Will be replaced at swapchain creation + WGPUTextureFormat m_depth_texture_format = WGPUTextureFormat::WGPUTextureFormat_Depth24Plus; + + glm::uvec2 m_viewport_size = glm::uvec2(1280u, 1024u); + bool m_initialized = false; + GuiPipelineUBO m_gui_ubo_data = { glm::vec2(1280.0, 1024.0) }; + + std::unique_ptr m_framebuffer; + void create_framebuffer(uint32_t width, uint32_t height); + void configure_surface(uint32_t width, uint32_t height); + + std::unique_ptr m_gui_pipeline; + std::unique_ptr m_gui_bind_group_layout; + std::unique_ptr m_gui_bind_group; + std::unique_ptr> m_gui_ubo; + + WGPUQuerySetDescriptor m_timestamp_query_desc; + WGPUQuerySet m_timestamp_queries; + WGPUPassTimestampWrites m_timestamp_writes; + std::unique_ptr> m_timestamp_resolve; + std::unique_ptr> m_timestamp_result; + + std::shared_ptr m_gputimer; + std::shared_ptr m_cputimer; + + bool m_force_repaint = false; + bool m_force_repaint_once = false; + uint32_t m_repaint_count = 0; + uint32_t m_frame_count = 0; + WGPUPresentMode m_surface_presentmode = WGPUPresentMode_Fifo; // WGPUPresentMode_Immediate; + + // Flag to exit the rendering loop + bool m_window_open = true; + + void webgpu_create_context(); + void webgpu_release_context(); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/CMakeLists.txt b/apps/webgpu_app/CMakeLists.txt new file mode 100644 index 000000000..df202f65c --- /dev/null +++ b/apps/webgpu_app/CMakeLists.txt @@ -0,0 +1,233 @@ +############################################################################# +# weBIGeo +# Copyright (C) 2024 Adam Celarek +# Copyright (C) 2024 Gerald Kimmersdorfer +# Copyright (C) 2024 Patrick Komon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +project(alpine-renderer-webgpu_app LANGUAGES C CXX) + + +set(SOURCES + main.cpp + App.h App.cpp + RenderingContext.h RenderingContext.cpp + ImGuiManager.h ImGuiManager.cpp + + util/error_logging.h util/error_logging.cpp + util/dark_mode.h util/dark_mode.cpp + util/InputMapper.h util/InputMapper.cpp + util/SearchService.h util/SearchService.cpp + + cloud/CloudsManager.h cloud/CloudsManager.cpp + + ui/ImGuiPanel.h + ui/TimingPanel.h ui/TimingPanel.cpp + ui/CameraPanel.h ui/CameraPanel.cpp + ui/AppPanel.h ui/AppPanel.cpp + ui/ShadingPanel.h ui/ShadingPanel.cpp + ui/AboutPanel.h ui/AboutPanel.cpp + ui/CompassPanel.h ui/CompassPanel.cpp + ui/LogoPanel.h ui/LogoPanel.cpp + ui/SearchPanel.h ui/SearchPanel.cpp + + atmosphere/AtmospherePanel.h atmosphere/AtmospherePanel.cpp + cloud/CloudPanel.h cloud/CloudPanel.cpp + track/TrackPanel.h track/TrackPanel.cpp + + overlay/OverlaysPanel.h overlay/OverlaysPanel.cpp + overlay/OverlayImGuiRenderer.h overlay/OverlayImGuiRenderer.cpp + overlay/OverlayImGuiRendererFactory.h overlay/OverlayImGuiRendererFactory.cpp + overlay/HeightLinesOverlayImGuiRenderer.h overlay/HeightLinesOverlayImGuiRenderer.cpp + overlay/ScreenSpaceSnowOverlayImGuiRenderer.h overlay/ScreenSpaceSnowOverlayImGuiRenderer.cpp + overlay/TextureOverlayImGuiRenderer.h overlay/TextureOverlayImGuiRenderer.cpp + overlay/TileDebugOverlayImGuiRenderer.h overlay/TileDebugOverlayImGuiRenderer.cpp +) + +if (ALP_WEBGPU_APP_ENABLE_COMPUTE) + list(APPEND SOURCES + compute/OverlayRenderNode.h compute/OverlayRenderNode.cpp + compute/NodeGraphPanel.h compute/NodeGraphPanel.cpp + compute/nodes/NodeRenderer.h compute/nodes/NodeRenderer.cpp + compute/nodes/NodeRendererFactory.h compute/nodes/NodeRendererFactory.cpp + compute/nodes/ExportNodeRenderer.h compute/nodes/ExportNodeRenderer.cpp + compute/nodes/OverlayNodeRenderer.h compute/nodes/OverlayNodeRenderer.cpp + compute/nodes/BufferToTextureNodeRenderer.h compute/nodes/BufferToTextureNodeRenderer.cpp + compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.h compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.cpp + compute/nodes/ComputeReleasePointsNodeRenderer.h compute/nodes/ComputeReleasePointsNodeRenderer.cpp + compute/nodes/ComputeSnowNodeRenderer.h compute/nodes/ComputeSnowNodeRenderer.cpp + compute/nodes/RequestTilesNodeRenderer.h compute/nodes/RequestTilesNodeRenderer.cpp + compute/nodes/SelectTilesNodeRenderer.h compute/nodes/SelectTilesNodeRenderer.cpp + compute/nodes/GPXTrackNodeRenderer.h compute/nodes/GPXTrackNodeRenderer.cpp + ) +endif() + +if (EMSCRIPTEN) + list(APPEND SOURCES util/WebInterop.h util/WebInterop.cpp) +endif() + +qt_add_executable(webgpu_app + resources.qrc + ${SOURCES} +) + + +set_target_properties(webgpu_app PROPERTIES + WIN32_EXECUTABLE FALSE # Set to FALSE to keep console attached on Windows + MACOSX_BUNDLE TRUE +) + +target_link_libraries(webgpu_app PUBLIC webgpu webgpu_engine Qt::Core Qt::Network) +target_include_directories(webgpu_app PRIVATE .) + +if (ALP_WEBGPU_APP_ENABLE_COMPUTE) + target_link_libraries(webgpu_app PUBLIC webgpu_compute) + target_compile_definitions(webgpu_app PRIVATE ALP_WEBGPU_APP_ENABLE_COMPUTE) +endif() + +# For Font Awesome Icon Headers: + alp_add_git_repository(iconfontcppheaders URL https://github.com/juliettef/IconFontCppHeaders.git COMMITISH f30b1e73b2d71eb331d77619c3f1de34199afc38 DO_NOT_ADD_SUBPROJECT) + target_include_directories(webgpu_app PRIVATE ${CMAKE_SOURCE_DIR}/extern/iconfontcppheaders) + + alp_add_git_repository(imgui URL https://github.com/AlpineMapsOrgDependencies/imgui_slim.git COMMITISH 4d8ecbf58ebf32e757ff56a60f1fb0446033edb8) + + set(IMNODES_IMGUI_TARGET_NAME imgui) + set(BUILD_SHARED_LIBS_SAVED ${BUILD_SHARED_LIBS}) + set(BUILD_SHARED_LIBS OFF) + alp_add_git_repository(imnodes URL https://github.com/AlpineMapsOrgDependencies/imnodes_slim.git COMMITISH 324355e64ed8b5bea02fe1439cdcb5c7773b4436) + set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_SAVED}) + + alp_add_git_repository(ImGuiFileDialog URL https://github.com/AlpineMapsOrgDependencies/ImGuiFileDialog_slim.git COMMITISH 39f98eb131ee00d476f1002336fc7e9e1795ccc5) + target_link_libraries(ImGuiFileDialog PRIVATE imgui) + + target_link_libraries(webgpu_app PRIVATE imgui imnodes ImGuiFileDialog) + +# Copy necessary DLLs to the output directory on Windows +if (WIN32 AND NOT EMSCRIPTEN) + include("${CMAKE_SOURCE_DIR}/cmake/alp_find_dawn_dxc.cmake") + + # Copy SDL2 DLL + if (EXISTS "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll") + add_custom_command(TARGET webgpu_app POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll" + "$" + COMMENT "Copying SDL2.dll" + ) + endif() + + alp_find_dawn_dxc_dlls(ALP_DAWN_DXC_DLLS) + foreach(ALP_DAWN_DXC_DLL IN LISTS ALP_DAWN_DXC_DLLS) + get_filename_component(ALP_DAWN_DXC_DLL_NAME "${ALP_DAWN_DXC_DLL}" NAME) + add_custom_command(TARGET webgpu_app POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${ALP_DAWN_DXC_DLL}" + "$" + COMMENT "Copying ${ALP_DAWN_DXC_DLL_NAME}" + ) + endforeach() + + # Use Qt's windeployqt to copy all necessary Qt dependencies + get_target_property(QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION) + get_filename_component(QT_BIN_DIR "${QMAKE_EXECUTABLE}" DIRECTORY) + find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${QT_BIN_DIR}") + + if(WINDEPLOYQT_EXECUTABLE) + add_custom_command(TARGET webgpu_app POST_BUILD + COMMAND "${WINDEPLOYQT_EXECUTABLE}" --no-translations "$" + COMMENT "Running windeployqt for webgpu_app" + ) + endif() + + + # === INSTALL CONFIGURATION FOR NATIVE WINDOWS BUILD === + install(TARGETS webgpu_app RUNTIME DESTINATION . BUNDLE DESTINATION .) + if (EXISTS "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll") + install(FILES "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll" DESTINATION .) + endif() + install(FILES ${ALP_DAWN_DXC_DLLS} DESTINATION .) + if(WINDEPLOYQT_EXECUTABLE) + install(CODE " + execute_process( + COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" --no-translations \"\${CMAKE_INSTALL_PREFIX}/webgpu_app.exe\" + ) + ") + endif() + # ================================================ +endif() + +if (EMSCRIPTEN) + + target_link_options(webgpu_app PRIVATE + --use-port=sdl2 # Use emscripten SDL2 implementation + #--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/resources # DONT USE RESOURCES PLS VIA QT RESOURCE SYSTEM + #--shell-file "${CMAKE_CURRENT_SOURCE_DIR}/shell/${SHELL_FILE}" # DOESNT WORK AS QT WILL OVERWRITE IT + -msimd128 #enables auto-vectorization and simd support + -lembind #enable embind + #-gsource-map #generate source maps + + # FOLLOWING IS SET BY Qt6.10 + #-s EXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS,callMain + #-s EXPORTED_FUNCTIONS=_main,__embind_initialize_bindings + #-s PTHREAD_POOL_SIZE=4 + #-s INITIAL_MEMORY=50MB + #-s MAXIMUM_MEMORY=4GB + #-s MAX_WEBGL_VERSION=2 + #-s WASM_BIGINT=1 + #-s STACK_SIZE=5MB + #-pthread + #-s ALLOW_MEMORY_GROWTH + #--profiling-funcs + #-sERROR_ON_UNDEFINED_SYMBOLS=1 + #-sFETCH + #-sASYNCIFY=1 + ) + + # enables to append to EXPORTED_RUNTIME_METHODS which is overridden by Qt + # see issue (https://bugreports.qt.io/browse/QTBUG-104882) and fix (https://codereview.qt-project.org/c/qt/qtbase/+/421733) + set_target_properties(webgpu_app PROPERTIES QT_WASM_EXTRA_EXPORTED_METHODS "ccall,cwrap") + + # Copy custom shell (name of file == name of target) + # IMPORTANT: THIS WILL ONLY BE EXECUTED WHEN THE TARGET IS ACTUALLY REBUILT + # SO THERE HAVE TO BE CHANGES IN THE C++ CODE. + file(GLOB_RECURSE SHELL_FILES "${CMAKE_CURRENT_SOURCE_DIR}/shell/*") + message(STATUS "SHELL_FILES: ${SHELL_FILES}") + # now create a custom command to move all those files to the build directory + foreach(SHELL_FILE ${SHELL_FILES}) + get_filename_component(SHELL_FILE_NAME ${SHELL_FILE} NAME) + add_custom_command(TARGET webgpu_app POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove -f + ${CMAKE_CURRENT_BINARY_DIR}/${SHELL_FILE_NAME} + COMMAND ${CMAKE_COMMAND} -E copy + ${SHELL_FILE} + ${CMAKE_CURRENT_BINARY_DIR}/${SHELL_FILE_NAME} + ) + endforeach() + + # === INSTALL CONFIGURATION FOR WASM BUILD === + set(WASM_OUTPUT_FILES + ${CMAKE_CURRENT_BINARY_DIR}/webgpu_app.html + ${CMAKE_CURRENT_BINARY_DIR}/webgpu_app.js + ${CMAKE_CURRENT_BINARY_DIR}/webgpu_app.wasm + ${CMAKE_CURRENT_BINARY_DIR}/qtloader.js + ) + foreach(SHELL_FILE ${SHELL_FILES}) + get_filename_component(SHELL_FILE_NAME ${SHELL_FILE} NAME) + list(APPEND WASM_OUTPUT_FILES ${CMAKE_CURRENT_BINARY_DIR}/${SHELL_FILE_NAME}) + endforeach() + install(FILES ${WASM_OUTPUT_FILES} DESTINATION ${ALP_WWW_INSTALL_DIR}) + # ================================================ +endif() diff --git a/apps/webgpu_app/ImGuiManager.cpp b/apps/webgpu_app/ImGuiManager.cpp new file mode 100644 index 000000000..d4fd67ad0 --- /dev/null +++ b/apps/webgpu_app/ImGuiManager.cpp @@ -0,0 +1,364 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ImGuiManager.h" +#include "App.h" +#include "RenderingContext.h" + +#include "ui/ImGuiPanel.h" + +#ifdef __EMSCRIPTEN__ +#include "util/WebInterop.h" +#else +#include +#include +#endif + +#include "atmosphere/AtmospherePanel.h" +#include "backends/imgui_impl_sdl2.h" +#include "backends/imgui_impl_wgpu.h" +#include "cloud/CloudPanel.h" +#include "overlay/OverlaysPanel.h" +#include "track/TrackPanel.h" +#include "ui/AboutPanel.h" +#include "ui/AppPanel.h" +#include "ui/CameraPanel.h" +#include "ui/CompassPanel.h" +#include "ui/LogoPanel.h" +#include "ui/SearchPanel.h" +#include "ui/ShadingPanel.h" +#include "ui/TimingPanel.h" +#ifdef ALP_WEBGPU_APP_ENABLE_COMPUTE +#include "compute/NodeGraphPanel.h" +#endif +#include +#include +#include + +#include "util/dark_mode.h" +#include +#include + +namespace webgpu_app { + +ImGuiManager::ImGuiManager(App* terrain_renderer) + : m_terrain_renderer(terrain_renderer) +{ +} + +void ImGuiManager::init( + SDL_Window* window, WGPUDevice device, [[maybe_unused]] WGPUTextureFormat swapchainFormat, [[maybe_unused]] WGPUTextureFormat depthTextureFormat) +{ + qDebug() << "Setup ImGuiManager..."; + m_window = window; + m_device = device; + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + // Setup ImNodes + ImNodes::CreateContext(); + + // Setup Platform/Renderer backends + ImGui_ImplSDL2_InitForOther(m_window); + ImGui_ImplWGPU_InitInfo init_info = {}; + init_info.Device = m_device; + init_info.RenderTargetFormat = swapchainFormat; + init_info.DepthStencilFormat = depthTextureFormat; + init_info.NumFramesInFlight = 3; + ImGui_ImplWGPU_Init(&init_info); + + webgpu_app::util::setup_darkmode_imgui_style(); + install_fonts(); + + auto* rc = m_terrain_renderer->get_rendering_context(); + auto* engine_ctx = rc->engine_context(); + + m_panels.push_back(std::make_unique(m_device)); + m_panels.push_back(std::make_unique(m_terrain_renderer)); + m_panels.push_back(std::make_unique()); + m_panels.push_back(std::make_unique(m_terrain_renderer)); + SearchPanel& search_panel = static_cast(**(m_panels.end() - 1)); + m_panels.push_back(std::make_unique(m_terrain_renderer)); + m_panels.push_back(std::make_unique(m_terrain_renderer)); + m_panels.push_back(std::make_unique(m_terrain_renderer)); + m_panels.push_back(std::make_unique(engine_ctx, rc->clouds_manager(), engine_ctx->cloud_renderer())); + m_panels.push_back(std::make_unique(engine_ctx)); + m_panels.push_back(std::make_unique(engine_ctx)); + m_panels.push_back(std::make_unique(engine_ctx, m_terrain_renderer)); +#ifdef ALP_WEBGPU_APP_ENABLE_COMPUTE + m_panels.push_back(std::make_unique(engine_ctx)); +#endif + m_panels.push_back(std::make_unique(engine_ctx)); + + connect(&search_panel, &SearchPanel::search_requested, rc->search_service(), &SearchService::search); + connect(&search_panel, + &SearchPanel::search_result_selected, + m_terrain_renderer->get_camera_controller(), + &nucleus::camera::Controller::fly_to_latitude_longitude); + connect(rc->search_service(), &SearchService::search_results_arrived, &search_panel, &SearchPanel::display_search_results); + +#ifdef __EMSCRIPTEN__ + connect(&WebInterop::instance(), &WebInterop::file_uploaded, this, &ImGuiManager::on_file_uploaded); +#endif +} + +void ImGuiManager::ready() +{ + for (auto& panel : m_panels) + panel->ready(); +} + +void ImGuiManager::install_fonts() +{ + ImGuiIO& io = ImGui::GetIO(); + + float baseFontSize = 16.0f; + float iconFontSize = 14.0f; + float smallFontSize = 14.0f; + float smallIconFontSize = 12.0f; + + QByteArray robotoData; + { + QFile file(":/fonts/Roboto-Regular.ttf"); + if (!file.open(QIODevice::ReadOnly)) { + throw std::runtime_error("Failed to open Main Font."); + } + robotoData = file.readAll(); + file.close(); + } + + QByteArray faData; + { + QFile file(":/fonts/fa5-solid-900.ttf"); + if (!file.open(QIODevice::ReadOnly)) { + throw std::runtime_error("Failed to open glyph font."); + } + faData = file.readAll(); + file.close(); + } + + static const ImWchar icons_ranges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; + + // Default UI font (16 px Roboto + FA icons) + { + ImFontConfig font_cfg; + font_cfg.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF(robotoData.data(), robotoData.size(), baseFontSize, &font_cfg); + + ImFontConfig icons_config; + icons_config.MergeMode = true; + icons_config.PixelSnapH = true; + icons_config.GlyphMinAdvanceX = iconFontSize; + icons_config.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF(faData.data(), faData.size(), iconFontSize, &icons_config, icons_ranges); + } + + // Small font (14 px Roboto + FA icons) + { + ImFontConfig font_cfg; + font_cfg.FontDataOwnedByAtlas = false; + s_node_font = io.Fonts->AddFontFromMemoryTTF(robotoData.data(), robotoData.size(), smallFontSize, &font_cfg); + + ImFontConfig icons_config; + icons_config.MergeMode = true; + icons_config.PixelSnapH = true; + icons_config.GlyphMinAdvanceX = smallIconFontSize; + icons_config.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF(faData.data(), faData.size(), smallIconFontSize, &icons_config, icons_ranges); + } +} + +void ImGuiManager::render([[maybe_unused]] WGPURenderPassEncoder renderPass) +{ + ImGui_ImplWGPU_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); + + draw(); + + ImGui::Render(); + ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), renderPass); +} + +void ImGuiManager::shutdown() +{ + qDebug() << "Releasing ImGuiManager..."; + ImGui_ImplWGPU_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + ImNodes::DestroyContext(); + ImGui::DestroyContext(); +} + +bool ImGuiManager::want_capture_keyboard() { return ImGui::GetIO().WantCaptureKeyboard; } + +bool ImGuiManager::want_capture_mouse() { return ImGui::GetIO().WantCaptureMouse; } + +void ImGuiManager::on_sdl_event(SDL_Event& event) { ImGui_ImplSDL2_ProcessEvent(&event); } + +void ImGuiManager::set_gui_visibility(bool visible) { m_gui_visible = visible; } + +bool ImGuiManager::get_gui_visibility() const { return m_gui_visible; } + +float ImGuiManager::s_tool_button_y = 0.0f; +ImFont* ImGuiManager::s_node_font = nullptr; +std::unordered_map ImGuiManager::s_picker_states; + +#ifdef __EMSCRIPTEN__ +void ImGuiManager::on_file_uploaded(const std::string& filename, const std::string& tag) +{ + auto it = s_picker_states.find(tag); + if (it != s_picker_states.end() && it->second.is_open) + it->second.pending.push_back(filename); +} +#endif + +bool ImGuiManager::FilePicker(const char* dialog_id, + const char* title, + const char* filters, + bool wants_open, + std::vector& out_paths, + bool allow_multiple, + const char* initial_path, + FilePickerMode mode, + const char* default_filename) +{ +#ifdef __EMSCRIPTEN__ + if (mode == FilePickerMode::Save) { + // Web: no file-system dialog for saves; return a synthetic MEMFS path immediately. + if (wants_open) { + out_paths.push_back(std::string("/download/") + default_filename); + return true; + } + return false; + } + auto& state = s_picker_states[dialog_id]; + if (wants_open) { + state.is_open = true; + state.pending.clear(); + WebInterop::instance().open_file_dialog(filters, dialog_id, allow_multiple); + } + if (state.is_open && !state.pending.empty()) { + out_paths = std::move(state.pending); + state.pending.clear(); + state.is_open = false; + return true; + } + return false; +#else + if (wants_open) { + IGFD::FileDialogConfig config; + config.path = initial_path; + config.flags = ImGuiFileDialogFlags_Modal; + if (mode == FilePickerMode::Save) { + config.countSelectionMax = 1; + config.flags |= ImGuiFileDialogFlags_ConfirmOverwrite; + config.fileName = default_filename; + } else { + config.countSelectionMax = allow_multiple ? 0 : 1; + } + ImGuiFileDialog::Instance()->OpenDialog(dialog_id, title, filters, config); + } + const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + const ImVec2 vp = ImGui::GetMainViewport()->Size; + const ImVec2 dialog_size(vp.x < 1000.0f ? vp.x * 0.9f : vp.x * 0.5f, vp.y < 1000.0f ? vp.y * 0.9f : vp.y * 0.5f); + if (ImGuiFileDialog::Instance()->Display(dialog_id, ImGuiWindowFlags_NoCollapse, dialog_size, dialog_size)) { + if (ImGuiFileDialog::Instance()->IsOk()) { + if (mode == FilePickerMode::Save) { + out_paths.push_back(ImGuiFileDialog::Instance()->GetFilePathName()); + } else { + for (auto& [name, path] : ImGuiFileDialog::Instance()->GetSelection()) + out_paths.push_back(path); + } + } + ImGuiFileDialog::Instance()->Close(); + return !out_paths.empty(); + } + return false; +#endif +} + +void ImGuiManager::finalize_save(const std::string& path) +{ +#ifdef __EMSCRIPTEN__ + WebInterop::download_file(path); +#else + (void)path; +#endif +} + +bool ImGuiManager::FloatingToggleButton(const char* id, const char* icon, const char* tooltip, uint32_t* enabled) +{ + const bool on = *enabled != 0u; + + // Claim the next floating tool-button slot (bottom-left, stacking upward). + ImVec2 button_pos(10, s_tool_button_y); + s_tool_button_y -= 48 + 10; + ImGui::SetNextWindowPos(button_pos, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(on ? 0.5f : 0.2f); // fade the background when disabled + ImGui::SetNextWindowSize(ImVec2(48, 48)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::Begin(id, nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // fully transparent + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.2f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.0f, 0.0f, 0.0f, 0.2f)); + ImGui::PushStyleColor(ImGuiCol_Text, on ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 0.5f)); // fade the icon when disabled + + const bool clicked = ImGui::Button(icon, ImVec2(48, 48)); + if (clicked) + *enabled = on ? 0u : 1u; + const bool hovered = ImGui::IsItemHovered(); + + ImGui::PopStyleColor(4); + ImGui::End(); + ImGui::PopStyleVar(); + + if (hovered) + ImGui::SetTooltip("%s", tooltip); + return clicked; +} + +void ImGuiManager::draw() +{ + if (!m_gui_visible) + return; + + // Reset the floating tool-button stack for this frame (bottom-left, stacking upward). + s_tool_button_y = ImGui::GetIO().DisplaySize.y - 48.0f - 40.0f; + + // Main sidebar window with CollapsingHeader sections. + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 430, 0)); // Set position to top-right corner + ImGui::SetNextWindowSize(ImVec2(430, ImGui::GetIO().DisplaySize.y)); // Set height to full screen height, width as desired + ImGui::Begin("weBIGeo", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + + for (auto& panel : m_panels) + panel->draw_panel(); + + ImGui::End(); + + for (auto& panel : m_panels) + panel->draw(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ImGuiManager.h b/apps/webgpu_app/ImGuiManager.h new file mode 100644 index 000000000..2cc9b62ff --- /dev/null +++ b/apps/webgpu_app/ImGuiManager.h @@ -0,0 +1,105 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +struct ImFont; +#include +#include +#include +#include +#include + +#include "ui/ImGuiPanel.h" + +namespace webgpu_app { + +class App; + +class ImGuiManager : public QObject { + Q_OBJECT +public: + explicit ImGuiManager(App* terrain_renderer); + + void init(SDL_Window* window, WGPUDevice device, WGPUTextureFormat swapchainFormat, WGPUTextureFormat depthTextureFormat); + void ready(); + void render(WGPURenderPassEncoder renderPass); + void shutdown(); + + bool want_capture_keyboard(); + bool want_capture_mouse(); + void on_sdl_event(SDL_Event& event); + + void set_gui_visibility(bool visible); + bool get_gui_visibility() const; + + // Top-left Y for the next floating tool button + static float s_tool_button_y; + + // Smaller font for compact UIs like the node graph editor (12 px) + static ImFont* s_node_font; + + // Draws a floating 48x48 icon tool button at the next bottom-left stack slot (claims s_tool_button_y). + static bool FloatingToggleButton(const char* id, const char* icon, const char* tooltip, uint32_t* enabled); + + enum class FilePickerMode { Open, Save }; + + // ImGui-style file picker. Call every frame inside an ImGui frame. wants_open=true triggers the dialog to open. + // Returns true (once) when the user has confirmed a selection; out_paths is filled with selected paths. + // In Save mode on web, returns true immediately with a synthetic /download/ path. + // NOTE: Uses WebInterop to be platform independent + static bool FilePicker(const char* dialog_id, + const char* title, + const char* filters, + bool wants_open, + std::vector& out_paths, + bool allow_multiple = false, + const char* initial_path = ".", + FilePickerMode mode = FilePickerMode::Open, + const char* default_filename = ""); + + // No-op on native; triggers a browser file download on web for the given MEMFS path. + static void finalize_save(const std::string& path); + +#ifdef __EMSCRIPTEN__ +private slots: + void on_file_uploaded(const std::string& filename, const std::string& tag); +#endif + +private: + struct FilePickerState { + bool is_open = false; + std::vector pending; + }; + static std::unordered_map s_picker_states; + + SDL_Window* m_window = nullptr; + WGPUDevice m_device = {}; + App* m_terrain_renderer = nullptr; + bool m_gui_visible = true; + + std::vector> m_panels; + + void draw(); + void install_fonts(); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/RenderingContext.cpp b/apps/webgpu_app/RenderingContext.cpp new file mode 100644 index 000000000..1d15aae0b --- /dev/null +++ b/apps/webgpu_app/RenderingContext.cpp @@ -0,0 +1,233 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RenderingContext.h" + +#include "nucleus/DataQuerier.h" +#include "nucleus/tile/SchedulerDirector.h" +#include "nucleus/tile/Texture3DScheduler.h" +#include "nucleus/tile/TileLoadService.h" +#include "nucleus/tile/setup.h" +#include "webgpu/engine/Context.h" +#include "webgpu/engine/cloud/CloudRenderer.h" +#include "webgpu/engine/overlay/HeightLinesOverlay.h" +#include "webgpu/engine/overlay/OverlayRenderer.h" +#include "webgpu/engine/overlay/TextureOverlay.h" +#include "webgpu/engine/tile_mesh/TileMeshRenderer.h" + +#ifdef ALP_WEBGPU_APP_ENABLE_COMPUTE +#include "compute/OverlayRenderNode.h" +#include "webgpu/compute/NodeRegistry.h" +#endif + +namespace webgpu_app { + +RenderingContext::RenderingContext() +{ + + using TilePattern = nucleus::tile::TileLoadService::UrlPattern; + assert(QThread::currentThread() == QCoreApplication::instance()->thread()); + +#ifdef ALP_ENABLE_THREADING + m_scheduler_thread = std::make_unique(); + m_scheduler_thread->setObjectName("scheduler_thread"); +#endif + + m_scheduler_director = std::make_unique(); + + // m->ortho_service.reset(new TileLoadService("https://tiles.bergfex.at/styles/bergfex-osm/", TileLoadService::UrlPattern::ZXY_yPointingSouth, + // ".jpeg")); m->ortho_service.reset(new TileLoadService("https://alpinemaps.cg.tuwien.ac.at/tiles/ortho/", + // TileLoadService::UrlPattern::ZYX_yPointingSouth, ".jpeg")); + // m->ortho_service.reset(new TileLoadService("https://maps%1.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/", + // TileLoadService::UrlPattern::ZYX_yPointingSouth, + // ".jpeg", + // {"", "1", "2", "3", "4"})); + m_aabb_decorator = nucleus::tile::setup::aabb_decorator(); + { + auto geometry_service + = std::make_unique("https://alpinemaps.cg.tuwien.ac.at/tiles/alpine_png/", TilePattern::ZXY, ".png"); + m_geometry_scheduler_holder = nucleus::tile::setup::geometry_scheduler(std::move(geometry_service), m_aabb_decorator, m_scheduler_thread.get()); + m_geometry_scheduler_holder.scheduler->set_gpu_quad_limit(256); // TODO + m_scheduler_director->check_in("geometry", m_geometry_scheduler_holder.scheduler); + m_data_querier = std::make_shared(&m_geometry_scheduler_holder.scheduler->ram_cache()); + auto ortho_service + = std::make_unique("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/", TilePattern::ZYX_yPointingSouth, ".jpeg"); + // auto ortho_service = std::make_unique("https://maps.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/", + // TilePattern::ZYX_yPointingSouth, ".jpeg"); auto ortho_service = + // std::make_unique("https://mapsneu.wien.gv.at/basemap/bmapgelaende/grau/google3857/", TilePattern::ZYX_yPointingSouth, + // ".jpeg"); + m_ortho_scheduler_holder = nucleus::tile::setup::texture_scheduler(std::move(ortho_service), m_aabb_decorator, m_scheduler_thread.get()); + m_ortho_scheduler_holder.scheduler->set_gpu_quad_limit(256); // TODO + m_scheduler_director->check_in("ortho", m_ortho_scheduler_holder.scheduler); + + auto cloud_service = std::make_unique("", TilePattern::ZXY, ".ktx2"); + m_cloud_scheduler_holder = nucleus::tile::setup::texture_scheduler_3d(std::move(cloud_service), + m_aabb_decorator, + m_scheduler_thread.get(), + { .tile_resolution = webgpu_engine::clouds::TILE_RESOLUTION_XY, .max_zoom_level = 10, .gpu_quad_limit = 1024 }); + m_cloud_scheduler_holder.scheduler->set_gpu_quad_limit(webgpu_engine::clouds::LOADED_TILE_LIMIT); + m_scheduler_director->check_in("cloud", m_cloud_scheduler_holder.scheduler); + } + m_geometry_scheduler_holder.scheduler->set_dataquerier(m_data_querier); + + if (QNetworkInformation::loadDefaultBackend() && QNetworkInformation::instance()) { + QNetworkInformation* n = QNetworkInformation::instance(); + m_geometry_scheduler_holder.scheduler->set_network_reachability(n->reachability()); + m_ortho_scheduler_holder.scheduler->set_network_reachability(n->reachability()); + m_cloud_scheduler_holder.scheduler->set_network_reachability(n->reachability()); + // clang-format off + connect(n, &QNetworkInformation::reachabilityChanged, m_geometry_scheduler_holder.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); + connect(n, &QNetworkInformation::reachabilityChanged, m_ortho_scheduler_holder.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); + connect(n, &QNetworkInformation::reachabilityChanged, m_cloud_scheduler_holder.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); + // clang-format on + } +#ifdef ALP_ENABLE_THREADING + qDebug() << "Scheduler thread: " << m_scheduler_thread.get(); + m_scheduler_thread->start(); +#endif + + m_clous_manager = std::make_unique(); + connect(m_clous_manager.get(), &clouds::Manager::slot_ready, this, [this](const clouds::TileSetInfo& slot) { + QString new_url = m_clous_manager->server_url() + "/" + slot.folder + "/tiles/"; + nucleus::utils::thread::async_call( + m_cloud_scheduler_holder.tile_service.get(), [this, new_url]() { m_cloud_scheduler_holder.tile_service->set_base_url(new_url); }); + nucleus::utils::thread::async_call(m_cloud_scheduler_holder.scheduler.get(), [this]() { + m_cloud_scheduler_holder.scheduler->clear_full_cache(); + m_cloud_scheduler_holder.scheduler->set_enabled(true); + }); + }); + + m_search_service = std::make_unique(); +} + +void RenderingContext::initialize(webgpu::Context& ctx) +{ + auto tile_mesh_renderer = std::make_shared(65, 512); + tile_mesh_renderer->set_tile_limit(1024); + auto cloud_renderer = std::make_shared(); + // This doesn't really make any sense if you think about it + cloud_renderer->set_tile_limit(webgpu_engine::clouds::LOADED_TILE_LIMIT); + + m_engine_context = std::make_unique(); + m_engine_context->set_webgpu_ctx(ctx); + m_engine_context->set_aabb_decorator(m_aabb_decorator); + m_engine_context->set_tile_mesh_renderer(tile_mesh_renderer); + m_engine_context->set_cloud_renderer(cloud_renderer); + auto overlay_renderer = std::make_shared(); + m_engine_context->set_overlay_renderer(overlay_renderer); + auto track_renderer = std::make_shared(); + m_engine_context->set_track_renderer(track_renderer); + auto atmosphere_renderer = std::make_shared(); + m_engine_context->set_atmosphere_renderer(atmosphere_renderer); + + connect(m_geometry_scheduler_holder.scheduler.get(), + &nucleus::tile::GeometryScheduler::gpu_tiles_updated, + m_engine_context->tile_mesh_renderer(), + &webgpu_engine::TileMeshRenderer::update_gpu_tiles_height); + connect(m_ortho_scheduler_holder.scheduler.get(), + &nucleus::tile::TextureScheduler::gpu_tiles_updated, + m_engine_context->tile_mesh_renderer(), + &webgpu_engine::TileMeshRenderer::update_gpu_tiles_ortho); + connect(m_cloud_scheduler_holder.scheduler.get(), + &nucleus::tile::Texture3DScheduler::gpu_tiles_updated, + m_engine_context->cloud_renderer(), + &webgpu_engine::CloudRenderer::update_gpu_tiles_cloud); + nucleus::utils::thread::async_call(m_geometry_scheduler_holder.scheduler.get(), [this]() { m_geometry_scheduler_holder.scheduler->set_enabled(true); }); + + // TODO: texture compression + nucleus::utils::thread::async_call(m_ortho_scheduler_holder.scheduler.get(), [this]() { + m_ortho_scheduler_holder.scheduler->set_texture_compression_algorithm(nucleus::utils::ColourTexture::Format::Uncompressed_RGBA); + m_ortho_scheduler_holder.scheduler->set_enabled(true); + }); + + // TODO do we need to connect some destroy signals? in gl app we do this: + + // clang-format off + //connect(QOpenGLContext::currentContext(), &QOpenGLContext::aboutToBeDestroyed, m->engine_context.get(), &nucleus::EngineContext::destroy); + //connect(QOpenGLContext::currentContext(), &QOpenGLContext::aboutToBeDestroyed, this, &RenderingContext::destroy); + //connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &RenderingContext::destroy); + // clang-format on + + m_engine_context->initialise(); + + // ToDo: Maybe the Compute should get its own context to do the following: +#ifdef ALP_WEBGPU_APP_ENABLE_COMPUTE + // Compute shaders are bundled by the webgpu_compute target; register its local source dir for hot-reload. + ctx.resource_registry().set_local_shader_path("webgpu_compute", ALP_SHADER_DIR_WEBGPU_COMPUTE); + + // The terminal node that forwards compute-graph results onto the overlay renderer needs to be registered + webgpu_compute::NodeRegistry::instance().register_node( + "OverlayRenderNode", [ctx = m_engine_context.get()](webgpu::Context&) { return std::make_unique(*ctx); }); +#endif + + nucleus::utils::thread::async_call(this, [this]() { emit this->initialised(); }); +} + +void RenderingContext::destroy() +{ + if (!m_geometry_scheduler_holder.scheduler) + return; + + if (m_engine_context) + m_engine_context->destroy(); + + if (m_scheduler_thread) { + nucleus::utils::thread::sync_call(m_geometry_scheduler_holder.scheduler.get(), [this]() { + m_geometry_scheduler_holder.scheduler.reset(); + m_ortho_scheduler_holder.scheduler.reset(); + m_cloud_scheduler_holder.scheduler.reset(); + }); + nucleus::utils::thread::sync_call(m_geometry_scheduler_holder.tile_service.get(), [this]() { + m_geometry_scheduler_holder.tile_service.reset(); + m_ortho_scheduler_holder.tile_service.reset(); + m_cloud_scheduler_holder.tile_service.reset(); + }); + m_scheduler_thread->quit(); + m_scheduler_thread->wait(500); // msec + m_scheduler_thread.reset(); + } +} + +webgpu_engine::Context* RenderingContext::engine_context() { return m_engine_context.get(); } + +nucleus::tile::utils::AabbDecorator* RenderingContext::aabb_decorator() { return m_aabb_decorator.get(); } + +nucleus::DataQuerier* RenderingContext::data_querier() { return m_data_querier.get(); } + +nucleus::tile::GeometryScheduler* RenderingContext::geometry_scheduler() { return m_geometry_scheduler_holder.scheduler.get(); } + +nucleus::tile::TileLoadService* RenderingContext::geometry_tile_load_service() { return m_geometry_scheduler_holder.tile_service.get(); } + +nucleus::tile::TextureScheduler* RenderingContext::ortho_scheduler() { return m_ortho_scheduler_holder.scheduler.get(); } + +nucleus::tile::Texture3DScheduler* RenderingContext::cloud_scheduler() { return m_cloud_scheduler_holder.scheduler.get(); } + +nucleus::tile::SchedulerDirector* RenderingContext::scheduler_director() { return m_scheduler_director.get(); } + +nucleus::tile::TileLoadService* RenderingContext::ortho_tile_load_service() { return m_ortho_scheduler_holder.tile_service.get(); } + +nucleus::tile::TileLoadService* RenderingContext::cloud_tile_load_service() { return m_cloud_scheduler_holder.tile_service.get(); } + +clouds::Manager* RenderingContext::clouds_manager() { return m_clous_manager.get(); } + +SearchService* RenderingContext::search_service() { return m_search_service.get(); } + +} // namespace webgpu_app diff --git a/apps/webgpu_app/RenderingContext.h b/apps/webgpu_app/RenderingContext.h new file mode 100644 index 000000000..b47658375 --- /dev/null +++ b/apps/webgpu_app/RenderingContext.h @@ -0,0 +1,97 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +#include "cloud/CloudsManager.h" +#include "nucleus/tile/SchedulerDirector.h" +#include "nucleus/tile/setup.h" +#include "util/SearchService.h" +#include "webgpu/webgpu.h" + +namespace webgpu { +class Context; +} + +namespace webgpu_engine { +class Context; +} + +namespace nucleus { +class DataQuerier; +} + +namespace nucleus::tile { +class Texture3DScheduler; +class GeometryScheduler; +class TextureScheduler; +class SchedulerDirector; +} // namespace nucleus::tile + +namespace nucleus::tile::utils { +class AabbDecorator; +} + +namespace webgpu_app { + +class RenderingContext : public QObject { + Q_OBJECT +public: + RenderingContext(); + + void initialize(webgpu::Context& ctx); + void destroy(); + + webgpu_engine::Context* engine_context(); + nucleus::tile::utils::AabbDecorator* aabb_decorator(); + nucleus::DataQuerier* data_querier(); + nucleus::tile::GeometryScheduler* geometry_scheduler(); + nucleus::tile::TileLoadService* geometry_tile_load_service(); + nucleus::tile::TextureScheduler* ortho_scheduler(); + nucleus::tile::Texture3DScheduler* cloud_scheduler(); + nucleus::tile::SchedulerDirector* scheduler_director(); + nucleus::tile::TileLoadService* ortho_tile_load_service(); + nucleus::tile::TileLoadService* cloud_tile_load_service(); + clouds::Manager* clouds_manager(); + SearchService* search_service(); + +signals: + void initialised(); + +private: + std::unique_ptr m_engine_context; + + std::shared_ptr m_aabb_decorator; + std::shared_ptr m_data_querier; + nucleus::tile::setup::GeometrySchedulerHolder m_geometry_scheduler_holder; + nucleus::tile::setup::TextureSchedulerHolder m_ortho_scheduler_holder; + nucleus::tile::setup::Texture3DSchedulerHolder m_cloud_scheduler_holder; + std::unique_ptr m_scheduler_director; + std::unique_ptr m_clous_manager; + std::unique_ptr m_search_service; + + std::unique_ptr m_scheduler_thread; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/atmosphere/AtmospherePanel.cpp b/apps/webgpu_app/atmosphere/AtmospherePanel.cpp new file mode 100644 index 000000000..06863d4f0 --- /dev/null +++ b/apps/webgpu_app/atmosphere/AtmospherePanel.cpp @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "AtmospherePanel.h" + +#include "ImGuiManager.h" +#include + +#include + +namespace webgpu_app { + +AtmospherePanel::AtmospherePanel(webgpu_engine::Context* context) + : m_context(context) +{ +} + +void AtmospherePanel::draw() +{ + auto& cfg = m_context->shared_config(); + if (ImGuiManager::FloatingToggleButton("ToggleAtmosphereButton", ICON_FA_GLOBE, "Atmosphere", &cfg.m_atmosphere_enabled)) + m_context->request_redraw(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/atmosphere/AtmospherePanel.h b/apps/webgpu_app/atmosphere/AtmospherePanel.h new file mode 100644 index 000000000..92a3d0eb3 --- /dev/null +++ b/apps/webgpu_app/atmosphere/AtmospherePanel.h @@ -0,0 +1,38 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ui/ImGuiPanel.h" + +namespace webgpu_engine { +class Context; +} + +namespace webgpu_app { + +class AtmospherePanel : public ImGuiPanel { +public: + explicit AtmospherePanel(webgpu_engine::Context* context); + void draw() override; + +private: + webgpu_engine::Context* m_context; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/cloud/CloudPanel.cpp b/apps/webgpu_app/cloud/CloudPanel.cpp new file mode 100644 index 000000000..d08790141 --- /dev/null +++ b/apps/webgpu_app/cloud/CloudPanel.cpp @@ -0,0 +1,136 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CloudPanel.h" + +#include "ImGuiManager.h" +#include +#include + +#include "cloud/CloudsManager.h" +#include +#include + +namespace webgpu_app { + +CloudPanel::CloudPanel(webgpu_engine::Context* context, clouds::Manager* clouds_manager, webgpu_engine::CloudRenderer* cloud_renderer) + : m_context(context) + , m_clouds_manager(clouds_manager) + , m_cloud_renderer(cloud_renderer) +{ +} + +void CloudPanel::draw() +{ + auto& cfg = m_context->shared_config(); + if (ImGuiManager::FloatingToggleButton("ToggleCloudsButton", ICON_FA_CLOUD, "Clouds", &cfg.m_clouds_enabled)) + m_context->request_redraw(); +} + +void CloudPanel::draw_panel() +{ + if (!m_context->shared_config().m_clouds_enabled) + return; + + const auto& tilesets = m_clouds_manager->get_slots(); + auto selected_slot = m_clouds_manager->selected_time_slot(); + + if (ImGui::CollapsingHeader(ICON_FA_CLOUD " Clouds")) { + + ImGui::SeparatorText("Data"); + + if (tilesets.empty()) { + if (m_clouds_manager->is_loading()) { + ImGui::Text("Loading cloud data..."); + } else { + ImGui::Text("No cloud data available."); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_SYNC "##reload_clouds")) { + m_clouds_manager->refresh_tileset_list(); + } + } + } else { + std::string preview_str = "Select time"; + if (!selected_slot.id.isEmpty()) { + preview_str = selected_slot.format_string(); + } + if (ImGui::BeginCombo("(UTC)", preview_str.c_str())) { + for (int n = 0; n < (int)tilesets.size(); n++) { + const auto& slot = tilesets[n]; + ImGui::PushID(slot.id.toStdString().c_str()); + const bool is_selected = slot.id == selected_slot.id; + std::string label = slot.format_string(); + if (ImGui::Selectable(label.c_str(), is_selected)) { + m_clouds_manager->select_time_slot(tilesets[n]); + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + ImGui::PopID(); + } + ImGui::EndCombo(); + } + + ImGui::SameLine(); + const bool loading = m_clouds_manager->is_loading(); + if (loading) + ImGui::BeginDisabled(); + if (ImGui::Button(ICON_FA_SYNC "##reload_clouds")) { + m_clouds_manager->refresh_tileset_list(); + } + if (loading) + ImGui::EndDisabled(); + } + + ImGui::SeparatorText("Shading"); + auto& shader_params = m_cloud_renderer->shader_params; + ImGui::Text("Step Size"); + ImGui::Indent(); + ImGui::DragFloat("Minimum", &shader_params.step_size_min, 1.0f, 0.0f, 10000.0f); + float inv_dist_fact = 1.0f / shader_params.step_size_distance_factor; + if (ImGui::DragFloat("Distance Factor", &inv_dist_fact, 1.0f, 0.0f, 10000.0f)) { + shader_params.step_size_distance_factor = 1.0f / inv_dist_fact; + } + ImGui::DragFloat("Horizon Factor", &shader_params.step_size_horizon_factor, 1.0f, 0.0f, 10000.0f); + ImGui::Unindent(); + ImGui::Text("Scattering"); + ImGui::Indent(); + ImGui::SliderFloat("Scattering Coeff", &shader_params.scattering_coeff, -1.0f, 1.0f); + ImGui::SliderFloat("Extinction Coeff", &shader_params.extinction_coeff, 0.0f, 1.0f, "%.5f"); + ImGui::SliderFloat("Albedo", &shader_params.albedo, 0.0f, 1.0f); + ImGui::Unindent(); + ImGui::Text("Lighting"); + ImGui::Indent(); + ImGui::DragFloat("Sun Light Scale", &shader_params.sun_light_scale, 1.0f, 0.0f, 10000.0f); + ImGui::DragFloat("Ambient Light Scale", &shader_params.ambient_light_scale, 0.01f, 0.0f, 10000.0f); + ImGui::DragFloat("Atmospheric Light Scale", &shader_params.atmospheric_light_scale, 0.01f, 0.0f, 10000.0f); + ImGui::DragFloat("Shadow Extinction Scale", &shader_params.shadow_extinction_scale, 0.01f, 0.0f, 10000.0f); + ImGui::SliderFloat("Powder Effect Scale", &shader_params.powder_scale, 0.0f, 1.0f); + ImGui::Unindent(); + ImGui::Text("Visibility"); + ImGui::Indent(); + ImGui::SliderFloat("Fade", &shader_params.fade_factor, 0.001f, 1.0f); + ImGui::Unindent(); + ImGui::Text("Accumulation"); + ImGui::Indent(); + ImGui::SliderInt("Stable Frames Limit", &shader_params.stable_frames_limit, 1, 256); + ImGui::Unindent(); + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/cloud/CloudPanel.h b/apps/webgpu_app/cloud/CloudPanel.h new file mode 100644 index 000000000..570d09ea0 --- /dev/null +++ b/apps/webgpu_app/cloud/CloudPanel.h @@ -0,0 +1,48 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ui/ImGuiPanel.h" + +namespace webgpu_engine { +class CloudRenderer; +class Context; +} // namespace webgpu_engine + +namespace webgpu_app { + +namespace clouds { + class Manager; +} + +class CloudPanel : public ImGuiPanel { +public: + CloudPanel(webgpu_engine::Context* context, clouds::Manager* clouds_manager, webgpu_engine::CloudRenderer* cloud_renderer); + + void draw() override; + void draw_panel() override; + +private: + webgpu_engine::Context* m_context; + clouds::Manager* m_clouds_manager; + webgpu_engine::CloudRenderer* m_cloud_renderer; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/cloud/CloudsManager.cpp b/apps/webgpu_app/cloud/CloudsManager.cpp new file mode 100644 index 000000000..a638c230b --- /dev/null +++ b/apps/webgpu_app/cloud/CloudsManager.cpp @@ -0,0 +1,195 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CloudsManager.h" + +#include +#include +#include +#include +#include +#include + +namespace webgpu_app::clouds { + +std::string TileSetInfo::format_string() const +{ + if (id.isEmpty()) + return "invalid"; + double mib = size / (1024.0 * 1024.0); + char buf[64]; + std::snprintf(buf, sizeof(buf), "%02d.%02d.%04d %02d:00 (+%02d, %.0f MiB)", date.day, date.month, date.year, date.hour, step, mib); + return std::string(buf); +} + +APIService::APIService(QObject* parent) + : QObject(parent) + , m_network_manager(new QNetworkAccessManager(this)) +{ +} + +const QVector& APIService::get_slots() const { return m_slots; } + +const QHash& APIService::get_slots_map() const { return m_id_to_index; } + +TileSetInfo APIService::get_slot(const QString& id) const +{ + if (!m_id_to_index.contains(id)) + return {}; + return m_slots[m_id_to_index[id]]; +} + +DateComponents APIService::parse_timestamp_id(const QString& id) +{ + // ID Format: YYYYMMDDHH (10 chars) + if (id.length() != 10) + return { 0, 0, 0, 0 }; + return { id.mid(0, 4).toInt(), id.mid(4, 2).toInt(), id.mid(6, 2).toInt(), id.mid(8, 2).toInt() }; +} + +void APIService::refresh_tileset_list() +{ + qDebug() << "[CloudAPI] Fetching tileset list..."; + QNetworkRequest request(QUrl(m_server_url + "/tilesets?status=ready")); + QNetworkReply* reply = m_network_manager->get(request); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[CloudAPI] Failed to fetch tilesets:" << reply->errorString(); + emit tileset_list_loaded(false); + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (!doc.isObject()) { + emit tileset_list_loaded(false); + return; + } + + m_slots.clear(); + m_id_to_index.clear(); + + QJsonArray items = doc.object()["items"].toArray(); + for (const auto& val : items) { + QJsonObject item = val.toObject(); + QString id = item["id"].toString(); + if (id.length() != 10) + continue; + + TileSetInfo slot; + slot.id = id; + slot.date = parse_timestamp_id(id); + slot.folder = item["folder"].toString(); + slot.size = item["size"].toVariant().toLongLong(); + + int underscore = slot.folder.lastIndexOf('_'); + if (underscore >= 0) + slot.step = slot.folder.mid(underscore + 1).toInt(); + + m_id_to_index[id] = m_slots.size(); + m_slots.push_back(slot); + } + + qDebug() << "[CloudAPI] Loaded" << m_slots.size() << "ready tilesets."; + emit tileset_list_loaded(true); + }); +} + +void APIService::fetch_shadow_texture(const QString& id) +{ + if (!m_id_to_index.contains(id)) { + qWarning() << "[CloudAPI] fetch_shadow_texture called with unknown ID:" << id; + return; + } + + TileSetInfo slot = m_slots[m_id_to_index[id]]; // copy to avoid dangling ref in lambda + + if (slot.folder.isEmpty()) { + qWarning() << "[CloudAPI] Slot folder is empty for:" << id; + return; + } + + qDebug() << "[CloudAPI] Fetching shadow texture for" << id << "from" << slot.folder; + QString full_url = m_server_url + "/" + slot.folder + "/shadow.ktx2"; + QNetworkRequest request(full_url); + QNetworkReply* reply = m_network_manager->get(request); + + connect(reply, &QNetworkReply::finished, this, [this, reply, slot]() { + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[CloudAPI] Shadow texture download failed:" << reply->errorString(); + return; + } + emit shadow_texture_loaded(slot, reply->readAll()); + }); +} + +Manager::Manager(QObject* parent) + : QObject(parent) + , m_api_service(std::make_unique()) +{ + connect(m_api_service.get(), &APIService::shadow_texture_loaded, this, [this](const TileSetInfo& slot, const QByteArray& data) { + if (m_selected_slot_id == slot.id) { + emit shadow_texture_ready(data); + } + }); + + connect(m_api_service.get(), &APIService::tileset_list_loaded, this, [this](bool ok) { + m_loading = false; + if (!ok) + return; + + const auto& tilesets = m_api_service->get_slots(); + auto current_date = QDateTime::currentDateTimeUtc(); + auto ymd = QCalendar().partsFromDate(current_date.date()); + int hour = current_date.time().hour(); + for (const auto& slot : tilesets) { + if (slot.date.year == ymd.year && slot.date.month == ymd.month && slot.date.day == ymd.day && slot.date.hour == hour) { + select_time_slot(slot); + break; + } + } + }); + + m_api_service->refresh_tileset_list(); +} + +void Manager::select_time_slot(const TileSetInfo& slot) +{ + if (m_selected_slot_id == slot.id) + return; + m_selected_slot_id = slot.id; + m_api_service->fetch_shadow_texture(slot.id); + emit slot_ready(slot); +} + +void Manager::refresh_tileset_list() +{ + m_loading = true; + m_api_service->refresh_tileset_list(); +} + +TileSetInfo Manager::selected_time_slot() const { return m_api_service->get_slot(m_selected_slot_id); } + +const QVector& Manager::get_slots() const { return m_api_service->get_slots(); } + +const QString& Manager::server_url() const { return m_api_service->server_url(); } + +} // namespace webgpu_app::clouds diff --git a/apps/webgpu_app/cloud/CloudsManager.h b/apps/webgpu_app/cloud/CloudsManager.h new file mode 100644 index 000000000..d76c77bd1 --- /dev/null +++ b/apps/webgpu_app/cloud/CloudsManager.h @@ -0,0 +1,102 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace webgpu_app::clouds { +struct DateComponents { + int year; + int month; + int day; + int hour; +}; + +struct TileSetInfo { + QString id; // "2026040812" (YYYYMMDDHH, target hour) + DateComponents date; + QString folder; // "2026040809_003" (on-disk folder name) + int step = 0; // extracted from folder suffix (_SSS) + qint64 size = 0; // bytes + + [[nodiscard]] std::string format_string() const; +}; + +class APIService : public QObject { + Q_OBJECT +public: + explicit APIService(QObject* parent = nullptr); + + void refresh_tileset_list(); + + [[nodiscard]] const QVector& get_slots() const; + [[nodiscard]] const QHash& get_slots_map() const; + [[nodiscard]] TileSetInfo get_slot(const QString& id) const; + + [[nodiscard]] const QString& server_url() const { return m_server_url; } + + void fetch_shadow_texture(const QString& id); + +signals: + // fired once after refresh_tileset_list() completes; ok=false on network/parse error + void tileset_list_loaded(bool ok); + // fired when the shadow.ktx2 binary for slot has been fully downloaded + void shadow_texture_loaded(const TileSetInfo& slot, const QByteArray& data); + +private: + static DateComponents parse_timestamp_id(const QString& id); + + QNetworkAccessManager* m_network_manager; + + // ordered list of ready tile sets (ascending target time) + QVector m_slots; + // maps TileSetInfo::id -> index into m_slots + QHash m_id_to_index; + + const QString m_server_url = "https://atlas.cg.tuwien.ac.at/webigeo-clouds/v2"; // http://localhost:8000/v2, https://atlas.cg.tuwien.ac.at/webigeo-clouds/v2 +}; + +class Manager : public QObject { + Q_OBJECT +public: + explicit Manager(QObject* parent = nullptr); + + void select_time_slot(const TileSetInfo& slot); + void refresh_tileset_list(); + + [[nodiscard]] TileSetInfo selected_time_slot() const; + [[nodiscard]] const QVector& get_slots() const; + [[nodiscard]] bool is_loading() const { return m_loading; } + [[nodiscard]] const QString& server_url() const; + +signals: + void slot_ready(const TileSetInfo& slot); + void shadow_texture_ready(const QByteArray& data); + +private: + std::unique_ptr m_api_service; + QString m_selected_slot_id = ""; + bool m_loading = true; +}; +} // namespace webgpu_app::clouds diff --git a/apps/webgpu_app/compute/NodeGraphPanel.cpp b/apps/webgpu_app/compute/NodeGraphPanel.cpp new file mode 100644 index 000000000..6522f64f0 --- /dev/null +++ b/apps/webgpu_app/compute/NodeGraphPanel.cpp @@ -0,0 +1,895 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeGraphPanel.h" + +#include "ImGuiManager.h" +#include "nodes/NodeRendererFactory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +NodeGraphPanel::NodeGraphPanel(webgpu_engine::Context* context) + : m_context(context) + , m_presets({ + { "Snow", ":/graphs/snow.json" }, + { "Avalanche simulation", ":/graphs/avalanche_simulation.json" }, + { "Avalanche simulation (with exports)", ":/graphs/avalanche_simulation_with_exports.json" }, + { "Iterative simulation (WIP)", ":/graphs/iterative_simulation_wip.json" }, + }) +{ +} + +void NodeGraphPanel::ready() { load_preset(":/graphs/avalanche_simulation.json"); } + +void NodeGraphPanel::attach_graph(std::unique_ptr graph) +{ + m_owned_graph = std::move(graph); + m_node_graph = m_owned_graph.get(); + + QObject::connect(m_node_graph, &nodes::NodeGraph::run_completed, m_context, [this](webgpu_compute::GraphRunContext) { + if (!m_pending_first_run_notice.empty()) { + m_notice_state = { true, std::move(m_pending_first_run_notice) }; + m_pending_first_run_notice.clear(); + } + m_context->request_redraw(); + }); + QObject::connect(m_node_graph, &nodes::NodeGraph::run_failed, m_context, [this](nodes::GraphRunFailureInfo info) { + qWarning() << "graph run failed. " << info.node_name() << ": " << info.node_run_failure_info().message(); + m_error_state.text = "Execution of pipeline failed.\n\nNode \"" + info.node_name() + "\" reported \"" + info.node_run_failure_info().message() + "\""; + m_error_state.should_open = true; + m_context->request_redraw(); + }); + + init(*m_node_graph); +} + +void NodeGraphPanel::load_preset(const std::string& resource_path) +{ + QFile file(QString::fromStdString(resource_path)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + m_error_state.text = "Cannot open preset resource:\n" + resource_path; + m_error_state.should_open = true; + return; + } + const QByteArray data = file.readAll(); + file.close(); + import_graph_json(data, resource_path); +} + +void NodeGraphPanel::new_graph() { attach_graph(std::make_unique()); } + +void NodeGraphPanel::import_graph_json(const QByteArray& data, const std::string& source_name) +{ + QJsonParseError parse_err; + const QJsonDocument doc = QJsonDocument::fromJson(data, &parse_err); + if (doc.isNull()) { + m_error_state.text = "Failed to parse \"" + source_name + "\":\n" + parse_err.errorString().toStdString(); + m_error_state.should_open = true; + return; + } + if (!doc.isObject()) { + m_error_state.text = "Invalid graph file \"" + source_name + "\": JSON root is not an object"; + m_error_state.should_open = true; + return; + } + + auto result = webgpu_compute::nodes::deserialize_node_graph(doc.object(), m_context->webgpu_ctx()); + if (!result) { + m_error_state.text = "Failed to load \"" + source_name + "\":\n" + result.error(); + m_error_state.should_open = true; + return; + } + + const QJsonObject root = doc.object(); + const QJsonObject ui_nodes = root["ui"].toObject()["nodes"].toObject(); + + const QString notice = root["first_run_notice"].toString(); + m_pending_first_run_notice = notice.isEmpty() ? std::string{} : notice.toStdString(); + + attach_graph(std::move(*result)); + + if (!ui_nodes.isEmpty()) { + for (auto& [name, renderer] : m_node_renderers) { + const QString key = QString::fromStdString(name); + if (ui_nodes.contains(key)) + renderer->deserialize_ui(ui_nodes[key].toObject()); + } + m_force_node_positions_on_next_frame = true; + } else { + const ImVec2 center(m_window_size.x * 0.5f, m_window_size.y * 0.5f); + for (auto& [nodePtr, nr] : m_node_renderers_by_node) + nr->set_position(center); + m_force_node_positions_on_next_frame = true; + m_pending_auto_layout = true; + } +} + +void NodeGraphPanel::render_open_dialog() +{ + std::vector open_paths; + if (ImGuiManager::FilePicker("open_graph_dialog", "Load Graph", "Graph files{.json}", m_open_dialog_wants_open, open_paths)) { + QFile file(QString::fromStdString(open_paths[0])); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + const QByteArray data = file.readAll(); + file.close(); + import_graph_json(data, open_paths[0]); + } else { + m_error_state.text = "Cannot open file:\n" + open_paths[0]; + m_error_state.should_open = true; + } + } + m_open_dialog_wants_open = false; +} + +void NodeGraphPanel::init(nodes::NodeGraph& node_graph) +{ + m_node_renderers.clear(); + m_node_renderers_by_node.clear(); + m_links.clear(); + + m_node_graph = &node_graph; + auto& nodes = m_node_graph->get_nodes(); + for (auto& [name, node] : nodes) { + auto renderer = NodeRendererFactory::create(name, *node.get()); + m_node_renderers.emplace(name, std::move(renderer)); + m_node_renderers_by_node.emplace(node.get(), m_node_renderers.at(name).get()); + } + + rebuild_socket_id_maps(); + rebuild_links(); +} + +static std::string type_to_default_name(const std::string& type_name) +{ + // "ComputeSnowNode" -> "compute_snow_node" + std::string result; + for (size_t i = 0; i < type_name.size(); ++i) { + if (i > 0 && std::isupper((unsigned char)type_name[i])) + result += '_'; + result += (char)std::tolower((unsigned char)type_name[i]); + } + return result; +} + +static std::string generate_node_name(const std::string& type_base, const webgpu_compute::nodes::NodeGraph* graph) +{ + for (int n = 1;; ++n) { + std::string candidate = type_base + "_" + std::to_string(n); + if (!graph->exists_node(candidate)) + return candidate; + } +} + +void NodeGraphPanel::render_add_node_popup() +{ + static const char* POPUP_ID = "Add Node"; + + if (m_open_add_node_request) { + if (m_registered_node_types.empty()) + m_registered_node_types = webgpu_compute::NodeRegistry::instance().get_registered_types(); + ImGui::OpenPopup(POPUP_ID); + m_open_add_node_request = false; + } + + if (!m_open_add_node_modal) + ImGui::SetNextWindowPos(m_add_node_popup_pos, ImGuiCond_Appearing); + const bool open = m_open_add_node_modal ? ImGui::BeginPopupModal(POPUP_ID, nullptr, ImGuiWindowFlags_AlwaysAutoResize) : ImGui::BeginPopup(POPUP_ID); + if (!open) + return; + + const auto& types = m_registered_node_types; + ImGui::SetNextItemWidth(260.0f); + if (ImGui::BeginCombo("Type", types[m_add_node_selected_idx].c_str())) { + for (int i = 0; i < (int)types.size(); ++i) { + const bool sel = (i == m_add_node_selected_idx); + if (ImGui::Selectable(types[i].c_str(), sel)) + m_add_node_selected_idx = i; + if (sel) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + const bool add_pressed = ImGui::Button("Add") || ImGui::IsKeyPressed(ImGuiKey_Enter, false); + if (m_open_add_node_modal) { + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + } + + if (add_pressed) { + const std::string type_name = types[m_add_node_selected_idx]; + const std::string name = generate_node_name(type_to_default_name(type_name), m_node_graph); + auto node = webgpu_compute::NodeRegistry::instance().try_create(type_name, m_context->webgpu_ctx()); + if (node) { + m_node_graph->add_node(name, std::move(node)); + auto renderer = NodeRendererFactory::create(name, m_node_graph->get_node(name)); + ImVec2 pan = ImNodes::EditorContextGetPanning(); + ImVec2 pos; + if (!m_open_add_node_modal) + pos = ImVec2(m_add_node_popup_pos.x - m_canvas_origin.x - pan.x, m_add_node_popup_pos.y - m_canvas_origin.y - pan.y); + else + pos = ImVec2(m_window_size.x * 0.5f - m_canvas_origin.x - pan.x, m_window_size.y * 0.5f - m_canvas_origin.y - pan.y); + renderer->set_position(pos); + m_node_renderers_by_node.emplace(&m_node_graph->get_node(name), renderer.get()); + m_node_renderers.emplace(name, std::move(renderer)); + rebuild_socket_id_maps(); + m_force_node_positions_on_next_frame = true; + } + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + if (!ImGui::IsPopupOpen(POPUP_ID)) + m_open_add_node_modal = false; +} + +QByteArray NodeGraphPanel::export_graph_json() const +{ + QJsonObject root = webgpu_compute::nodes::serialize_node_graph(*m_node_graph); + + QJsonObject ui_nodes; + for (const auto& [name, renderer] : m_node_renderers) + ui_nodes[QString::fromStdString(name)] = renderer->serialize_ui(); + QJsonObject ui; + ui["nodes"] = ui_nodes; + root["ui"] = ui; + + return QJsonDocument(root).toJson(QJsonDocument::Indented); +} + +void NodeGraphPanel::render_save_dialog() +{ + std::vector save_paths; + if (ImGuiManager::FilePicker("save_graph_dialog", + "Save Graph", + "Graph files{.json}", + m_save_dialog_wants_open, + save_paths, + false, + ".", + ImGuiManager::FilePickerMode::Save, + "graph.json")) { + QFile file(QString::fromStdString(save_paths[0])); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + file.write(export_graph_json()); + file.close(); + ImGuiManager::finalize_save(save_paths[0]); + } else { + m_error_state.text = "Failed to open file for writing:\n" + save_paths[0]; + m_error_state.should_open = true; + } + } + m_save_dialog_wants_open = false; +} + +void NodeGraphPanel::calculate_window_size() +{ + if (!ImGui::GetCurrentContext()) { + m_window_size = ImVec2(0.0f, 0.0f); + return; + } + m_window_size = ImGui::GetIO().DisplaySize; + m_window_size.x -= 430; +} + +void NodeGraphPanel::calculate_auto_layout() +{ + m_target_layout.clear(); + + // Step 1: Identify root nodes (no inputs) and enqueue them at x = 0 + std::queue> queue; + for (auto& [name, nr] : m_node_renderers) { + nodes::Node* node = nr->get_node(); + if (node->input_sockets().empty()) { + int x = 0; + m_target_layout[node] = ImVec2(x, 0); + queue.emplace(nr.get(), x); + } + } + + // Step 2: Perform BFS to assign x-layer positions based on the maximum distance from root nodes + while (!queue.empty()) { + auto [nr, current_x] = queue.front(); + queue.pop(); + + nodes::Node* current_node = nr->get_node(); + const auto& outputs = current_node->output_sockets(); + for (const auto& os : outputs) { + for (auto* conn : os.connected_sockets()) { + nodes::Node* target_node = &conn->node(); + NodeRenderer* target = m_node_renderers_by_node[target_node]; + + int x = current_x + 1; + m_target_layout[target_node] = ImVec2(x, 0); + queue.emplace(target, x); + } + } + } + + // Step 3: Group nodes by x-layer and assign sequential y-indices + std::unordered_map> x_buckets; + x_buckets.reserve(m_node_renderers.size()); + + for (auto& [name, nr] : m_node_renderers) { + int x = (int)m_target_layout[nr->get_node()].x; + x_buckets[x].push_back(nr.get()); + } + + for (auto& [x, vec] : x_buckets) { + int y = 0; + for (auto* r : vec) + m_target_layout[r->get_node()] = ImVec2((float)x, (float)y++); + } + + // Step 4: Compute pixel layout. Determine column widths, x-offsets, and center vertically + std::vector cols; + cols.reserve(x_buckets.size()); + for (auto& kv : x_buckets) + cols.push_back(kv.first); + std::sort(cols.begin(), cols.end()); + + std::unordered_map col_width; + col_width.reserve(cols.size()); + + for (int cx : cols) { + float wmax = 0.f; + for (NodeRenderer* r : x_buckets[cx]) { + ImVec2 sz = r->get_size(); + wmax = std::max(wmax, sz.x); + } + col_width[cx] = wmax; + } + + std::unordered_map col_x_offset; + col_x_offset.reserve(cols.size()); + + float x_cursor = 0.f; + for (size_t i = 0; i < cols.size(); ++i) { + int cx = cols[i]; + if (i == 0) + col_x_offset[cx] = x_cursor; + else + col_x_offset[cx] = (x_cursor += col_width[cols[i - 1]] + m_initial_node_spacing.x); + } + + float frame_height = 0.f; + for (int cx : cols) { + float hsum = 0.f; + for (NodeRenderer* r : x_buckets[cx]) { + ImVec2 sz = r->get_size(); + hsum += sz.y + m_initial_node_spacing.y; + } + if (!x_buckets[cx].empty()) + hsum -= m_initial_node_spacing.y; + frame_height = std::max(frame_height, hsum); + } + + for (int cx : cols) { + float column_height = 0.f; + for (NodeRenderer* r : x_buckets[cx]) + column_height += r->get_size().y + m_initial_node_spacing.y; + + if (!x_buckets[cx].empty()) + column_height -= m_initial_node_spacing.y; + + float y_cursor = (frame_height - column_height) * 0.5f; + + for (NodeRenderer* r : x_buckets[cx]) { + ImVec2 sz = r->get_size(); + m_target_layout[r->get_node()] = ImVec2(col_x_offset[cx], y_cursor); + y_cursor += sz.y + m_initial_node_spacing.y; + } + } + + center_target_layout(); +} + +void NodeGraphPanel::recenter_graph() +{ + m_target_layout.clear(); + for (auto& [nodePtr, nr] : m_node_renderers_by_node) + m_target_layout[nodePtr] = nr->get_position(); + center_target_layout(); + for (auto& [nodePtr, pos] : m_target_layout) + m_node_renderers_by_node[nodePtr]->set_position(pos); + m_force_node_positions_on_next_frame = true; +} + +void NodeGraphPanel::reset_graph_layout() +{ + calculate_auto_layout(); + for (auto& [nodePtr, pos] : m_target_layout) + m_node_renderers_by_node[nodePtr]->set_position(pos); + m_force_node_positions_on_next_frame = true; +} + +void NodeGraphPanel::center_target_layout() +{ + // Get AABB of target layout + ImVec4 aabb(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (auto& [nodePtr, pos] : m_target_layout) { + ImVec2 s = m_node_renderers_by_node[nodePtr]->get_size(); + aabb.x = std::min(aabb.x, pos.x); // minX + aabb.y = std::min(aabb.y, pos.y); // minY + aabb.z = std::max(aabb.z, pos.x + s.x); // maxX + aabb.w = std::max(aabb.w, pos.y + s.y); // maxY + } + float graph_width = aabb.z - aabb.x; + float graph_height = aabb.w - aabb.y; + float offset_x = (m_window_size.x - graph_width) * 0.5f - aabb.x; + float offset_y = (m_window_size.y - graph_height) * 0.5f - aabb.y; + // Apply offset to target layout + for (auto& [nodePtr, pos] : m_target_layout) { + pos.x += offset_x; + pos.y += offset_y; + } +} + +void NodeGraphPanel::push_style() +{ + // Always use transparent grid background + ImNodes::PushColorStyle(ImNodesCol_GridBackground, IM_COL32(50, 50, 50, 0)); + + if (m_render_mode == GraphRenderingMode::Default) { + ImNodes::PushColorStyle(ImNodesCol_GridLine, IM_COL32(200, 200, 200, 40)); // light gray + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)); // default ImGui bg + + } else if (m_render_mode == GraphRenderingMode::Transparent) { + ImNodes::PushColorStyle(ImNodesCol_GridLine, IM_COL32(200, 200, 200, 40)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // fully transparent + + } else if (m_render_mode == GraphRenderingMode::White) { + ImNodes::PushColorStyle(ImNodesCol_GridLine, IM_COL32(200, 200, 200, 40)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + } +} + +void NodeGraphPanel::pop_style() +{ + ImGui::PopStyleColor(); // ImGui window background + ImNodes::PopColorStyle(); // Grid line + ImNodes::PopColorStyle(); // Grid background +} + +void NodeGraphPanel::draw() +{ + if (m_pending_preset_path) { + load_preset(*m_pending_preset_path); + m_pending_preset_path.reset(); + m_context->request_redraw(); + } + + ImGuiManager::FloatingToggleButton("###ToggleGraphRenderer", ICON_FA_NETWORK_WIRED, "Toggle compute graph editor", &m_editor_visible); + render_error_modal(); + render_first_run_notice_modal(); + render_save_dialog(); + render_open_dialog(); + + if (!m_editor_visible) + return; + + calculate_window_size(); + + if (m_pending_auto_layout) { + const bool all_measured = std::all_of(m_node_renderers.begin(), m_node_renderers.end(), [](const auto& kv) { return kv.second->is_size_known(); }); + if (all_measured) { + calculate_auto_layout(); + for (auto& [nodePtr, pos] : m_target_layout) + m_node_renderers_by_node[nodePtr]->set_position(pos); + m_force_node_positions_on_next_frame = true; + m_pending_auto_layout = false; + } + } + + push_style(); + + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(m_window_size, ImGuiCond_Always); + + ImGui::Begin("Compute Graph Editor", + nullptr, + ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus + | ImGuiWindowFlags_NoTitleBar); + + render_menu(); + + m_canvas_origin = ImGui::GetCursorScreenPos(); + if (m_use_small_font && ImGuiManager::s_node_font) + ImGui::PushFont(ImGuiManager::s_node_font); + ImNodes::BeginNodeEditor(); + + // NOTE: This is a hack to disable interactions when the cursor is inside the settings panel. + // which is sadly necessary because imnodes doesnt respect the Imguis z order + { + ImGuiWindow* settings_win = ImGui::FindWindowByName("Node Settings"); + if (settings_win && settings_win->Rect().Contains(ImGui::GetIO().MousePos)) { + GImNodes->MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + GImNodes->LeftMouseClicked = false; + GImNodes->LeftMouseDragging = false; + } + } + + // draw nodes + const bool apply_positions = m_force_node_positions_on_next_frame; + m_force_node_positions_on_next_frame = false; + for (auto& [name, node_renderer] : m_node_renderers) { + node_renderer->render(apply_positions); + } + + // draw links + for (size_t i = 0; i < m_links.size(); ++i) { + const auto& [input_attr_id, output_attr_id] = m_links[i]; + if (auto it = m_input_socket_by_id.find(input_attr_id); it != m_input_socket_by_id.end()) { + const ImU32 color = NodeRenderer::pin_color_for_type(it->second->type()); + ImNodes::PushColorStyle(ImNodesCol_Link, color); + ImNodes::PushColorStyle(ImNodesCol_LinkHovered, color); + ImNodes::PushColorStyle(ImNodesCol_LinkSelected, color); + ImNodes::Link(int(i), input_attr_id, output_attr_id); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + } else { + ImNodes::Link(int(i), input_attr_id, output_attr_id); + } + } + + ImNodes::MiniMap(0.1f, ImNodesMiniMapLocation_BottomRight); + ImNodes::EndNodeEditor(); + if (m_use_small_font && ImGuiManager::s_node_font) + ImGui::PopFont(); + + int start_attr_id, end_attr_id; + if (ImNodes::IsLinkCreated(&start_attr_id, &end_attr_id)) { + nodes::OutputSocket* output_socket = nullptr; + nodes::InputSocket* input_socket = nullptr; + + if (m_output_socket_by_id.count(start_attr_id) && m_input_socket_by_id.count(end_attr_id)) { + output_socket = m_output_socket_by_id.at(start_attr_id); + input_socket = m_input_socket_by_id.at(end_attr_id); + } else if (m_output_socket_by_id.count(end_attr_id) && m_input_socket_by_id.count(start_attr_id)) { + output_socket = m_output_socket_by_id.at(end_attr_id); + input_socket = m_input_socket_by_id.at(start_attr_id); + } + + if (output_socket && input_socket && output_socket->type() == input_socket->type()) { + input_socket->connect(*output_socket); + m_node_graph->connect_node_signals_and_slots(); + rebuild_links(); + } + } + + ImGui::End(); + + pop_style(); + + render_settings_panel(); + + for (auto& [name, node_renderer] : m_node_renderers) { + node_renderer->render_dialogs(); + } + + poll_keyboard_shortcuts(); + render_add_node_popup(); +} + +void NodeGraphPanel::poll_keyboard_shortcuts() +{ + if (!ImGui::GetIO().WantTextInput) { + const bool alt = ImGui::GetIO().KeyAlt; + const bool shift = ImGui::GetIO().KeyShift; + if (alt && ImGui::IsKeyPressed(ImGuiKey_M, false)) + m_render_mode = static_cast((static_cast(m_render_mode) + 1) % 3); + if (alt && ImGui::IsKeyPressed(ImGuiKey_F, false)) + reset_graph_layout(); + if (alt && ImGui::IsKeyPressed(ImGuiKey_C, false)) + recenter_graph(); + if (shift && ImGui::IsKeyPressed(ImGuiKey_R, false)) + m_node_graph->run(); + if (ImGui::IsKeyPressed(ImGuiKey_Delete)) + delete_selected_nodes(); + if (shift && ImGui::IsKeyPressed(ImGuiKey_A, false)) { + m_add_node_popup_pos = ImGui::GetMousePos(); + m_open_add_node_modal = false; + m_open_add_node_request = true; + } + } +} + +void NodeGraphPanel::delete_selected_nodes() +{ + const int num_selected = ImNodes::NumSelectedNodes(); + if (num_selected == 0) + return; + + std::vector selected_ids(num_selected); + ImNodes::GetSelectedNodes(selected_ids.data()); + + std::vector to_delete; + for (int node_id : selected_ids) { + for (auto& [name, renderer] : m_node_renderers) { + if (renderer->get_node_id() == node_id) { + to_delete.push_back(name); + break; + } + } + } + + for (const auto& name : to_delete) { + auto it = m_node_renderers.find(name); + if (it == m_node_renderers.end()) + continue; + m_node_renderers_by_node.erase(it->second->get_node()); + m_node_renderers.erase(it); + m_node_graph->remove_node(name); + } + + rebuild_socket_id_maps(); + rebuild_links(); + if (!m_node_graph->get_nodes().empty()) + m_node_graph->connect_node_signals_and_slots(); +} + +void NodeGraphPanel::rename_selected_node(const std::string& old_name, const std::string& new_name) +{ + m_node_graph->rename_node(old_name, new_name); + + auto it = m_node_renderers.find(old_name); + auto renderer = std::move(it->second); + renderer->rename(new_name); + m_node_renderers.erase(it); + m_node_renderers.emplace(new_name, std::move(renderer)); + + rebuild_socket_id_maps(); + rebuild_links(); + + m_rename_current_node = new_name; +} + +void NodeGraphPanel::rebuild_links() +{ + m_links.clear(); + auto& nodes = m_node_graph->get_nodes(); + for (auto& [name, node_renderer] : m_node_renderers) { + const auto& node = *nodes.at(name).get(); + for (const auto& input_socket : node.input_sockets()) { + if (!input_socket.is_socket_connected()) + continue; + const int input_attr_id = node_renderer->get_input_socket_id(input_socket.name()); + const nodes::Node& connected_node = input_socket.connected_socket().node(); + const NodeRenderer* connected_renderer = m_node_renderers_by_node.at(&connected_node); + const int output_attr_id = connected_renderer->get_output_socket_id(input_socket.connected_socket().name()); + m_links.emplace_back(input_attr_id, output_attr_id); + } + } +} + +void NodeGraphPanel::rebuild_socket_id_maps() +{ + m_input_socket_by_id.clear(); + m_output_socket_by_id.clear(); + auto& nodes = m_node_graph->get_nodes(); + for (auto& [name, node_renderer] : m_node_renderers) { + auto& node = *nodes.at(name).get(); + for (auto& socket : node.input_sockets()) + m_input_socket_by_id[node_renderer->get_input_socket_id(socket.name())] = &socket; + for (auto& socket : node.output_sockets()) + m_output_socket_by_id[node_renderer->get_output_socket_id(socket.name())] = &socket; + } +} + +void NodeGraphPanel::render_error_modal() +{ + if (m_error_state.should_open) { + ImGui::OpenPopup("Error"); + m_error_state.should_open = false; + } + + // Always center this window when appearing + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (ImGui::BeginPopupModal("Error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::PushTextWrapPos(30.0f * ImGui::GetFontSize()); + ImGui::Text("%s", m_error_state.text.c_str()); + ImGui::PopTextWrapPos(); + + ImGui::Separator(); + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void NodeGraphPanel::render_first_run_notice_modal() +{ + if (m_notice_state.should_open) { + ImGui::OpenPopup("first_run_notice"); + m_notice_state.should_open = false; + } + + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(400, 0)); + + if (ImGui::BeginPopupModal("first_run_notice", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x); + ImGui::TextWrapped("%s", m_notice_state.text.c_str()); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + const float button_width = 150.0f; + ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - button_width + ImGui::GetStyle().WindowPadding.x); + if (ImGui::Button("OK", ImVec2(button_width, 0))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } +} + +void NodeGraphPanel::render_menu() +{ + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem(ICON_FA_FILE " New Graph")) + new_graph(); + ImGui::Separator(); + if (ImGui::BeginMenu(ICON_FA_FOLDER_OPEN " Load Preset")) { + for (const auto& preset : m_presets) { + if (ImGui::MenuItem(preset.name.c_str())) + m_pending_preset_path = preset.resource_path; + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem(ICON_FA_FILE_IMPORT " Load from File...")) + m_open_dialog_wants_open = true; + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_SAVE " Save to File...")) + m_save_dialog_wants_open = true; + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Graph")) { + if (ImGui::MenuItem(ICON_FA_PLAY " Run Full Graph", "Shift+R")) + m_node_graph->run(); + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_PLUS " Add Node", "Shift+A")) { + m_open_add_node_modal = true; + m_open_add_node_request = true; + } + if (ImGui::MenuItem(ICON_FA_TRASH " Delete Selected", "Del", false, ImNodes::NumSelectedNodes() > 0)) + delete_selected_nodes(); + ImGui::Separator(); + if (ImGui::MenuItem(ICON_FA_TH " Apply Auto-Layout", "Alt+F")) + reset_graph_layout(); + if (ImGui::MenuItem(ICON_FA_COMPRESS_ARROWS_ALT " Recenter Graph", "Alt+C")) + recenter_graph(); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) { + if (ImGui::MenuItem(ICON_FA_ADJUST " Toggle Background Mode", "Alt+M")) { + m_render_mode = static_cast((static_cast(m_render_mode) + 1) % 3); + } + ImGui::Separator(); + const char* mode_name = m_render_mode == GraphRenderingMode::Default ? "Default" + : m_render_mode == GraphRenderingMode::Transparent ? "Transparent" + : "White"; + ImGui::Text("Current Mode: %s", mode_name); + ImGui::Separator(); + ImGui::MenuItem(ICON_FA_FONT " Small Font", nullptr, &m_use_small_font); + ImGui::EndMenu(); + } + + ImGui::EndMenuBar(); + } +} + +NodeRenderer* NodeGraphPanel::find_selected_node_renderer() const +{ + if (ImNodes::NumSelectedNodes() != 1) + return nullptr; + int node_id; + ImNodes::GetSelectedNodes(&node_id); + for (const auto& [name, renderer] : m_node_renderers) { + if (renderer->get_node_id() == node_id) + return renderer.get(); + } + return nullptr; +} + +void NodeGraphPanel::render_settings_panel() +{ + NodeRenderer* selected = find_selected_node_renderer(); + + constexpr float panel_width = 430.0f; + constexpr float panel_margin = 11.0f; + constexpr float panel_margin_top = 26.0f; + ImGui::SetNextWindowPos({ m_window_size.x - panel_width - panel_margin, panel_margin + panel_margin_top }, ImGuiCond_Always); + ImGui::SetNextWindowSizeConstraints({ panel_width, 0 }, { panel_width, m_window_size.y - panel_margin * 2 }); + ImGui::Begin("Node Settings", + nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar); + + if (selected) { + // Sync buffer when a different node is selected + const std::string& raw_name = selected->get_name(); + if (m_rename_current_node != raw_name) { + m_rename_current_node = raw_name; + strncpy(m_rename_buf, raw_name.c_str(), sizeof(m_rename_buf) - 1); + m_rename_buf[sizeof(m_rename_buf) - 1] = '\0'; + } + + const std::string buf_str(m_rename_buf); + const bool is_valid = !buf_str.empty() && (buf_str == raw_name || !m_node_graph->exists_node(buf_str)); + + if (!is_valid) + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.55f, 0.1f, 0.1f, 1.0f)); + + const float input_width = std::max(80.0f, ImGui::CalcTextSize(m_rename_buf).x + 16.0f); + ImGui::SetNextItemWidth(input_width); + const bool changed = ImGui::InputText("##nodename", m_rename_buf, sizeof(m_rename_buf)); + + if (!is_valid) + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(40, 70, 120, 200)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(40, 70, 120, 200)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(40, 70, 120, 200)); + ImGui::SmallButton(selected->get_node()->get_type_name().c_str()); + ImGui::PopStyleColor(3); + + if (changed) { + // Read m_rename_buf AFTER InputText has written the new value into it + const std::string new_name(m_rename_buf); + const bool new_valid = !new_name.empty() && (new_name == raw_name || !m_node_graph->exists_node(new_name)); + if (new_valid && new_name != raw_name) + rename_selected_node(raw_name, new_name); + } + + ImGui::Separator(); + bool enabled = selected->get_node()->is_enabled(); + if (ImGui::Checkbox("Enabled", &enabled)) + selected->get_node()->set_enabled(enabled); + if (selected->has_settings()) { + ImGui::Separator(); + selected->render_settings_content(); + } + } else { + ImGui::TextDisabled("Select a node to view settings."); + } + + ImGui::End(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/NodeGraphPanel.h b/apps/webgpu_app/compute/NodeGraphPanel.h new file mode 100644 index 000000000..5cae41f99 --- /dev/null +++ b/apps/webgpu_app/compute/NodeGraphPanel.h @@ -0,0 +1,169 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nodes/NodeRenderer.h" +#include "ui/ImGuiPanel.h" +#include + +namespace webgpu_engine { +class Context; +} + +namespace webgpu_compute::nodes { +class InputSocket; +class OutputSocket; +} // namespace webgpu_compute::nodes + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class NodeGraphPanel : public ImGuiPanel { +public: + explicit NodeGraphPanel(webgpu_engine::Context* context); + + // Loads the default pipeline preset into the contexts compute graph (called after init). + void ready() override; + void draw() override; + + enum class GraphRenderingMode { Default, Transparent, White }; + +private: + // (Re)builds the node renderers for the currently loaded graph. + void init(nodes::NodeGraph& node_graph); + + // Loads a preset graph from a Qt resource path, wires signals, and inits. + void load_preset(const std::string& resource_path); + + // Replaces the current graph with a new empty graph. + void new_graph(); + + struct GraphPreset { + std::string name; + std::string resource_path; + }; + + void render_error_modal(); + void render_first_run_notice_modal(); + + webgpu_engine::Context* m_context = nullptr; + std::unique_ptr m_owned_graph; // the panel owns the active compute graph + nodes::NodeGraph* m_node_graph = nullptr; + + std::vector m_presets; + std::optional m_pending_preset_path; + + uint32_t m_editor_visible = 0; + + struct ErrorModalState { + bool should_open = false; + std::string text; + }; + ErrorModalState m_error_state; + + struct NoticeModalState { + bool should_open = false; + std::string text; + }; + NoticeModalState m_notice_state; + std::string m_pending_first_run_notice; + + ImVec2 m_window_size = ImVec2(0, 0); + + std::unordered_map m_target_layout; + + bool m_force_node_positions_on_next_frame = false; + + ImVec2 m_initial_node_spacing = ImVec2(50.0f, 50.0f); + ImVec2 m_canvas_origin = { 0, 0 }; // screen-space top-left of the ImNodes canvas + + std::unordered_map> m_node_renderers; + std::unordered_map m_node_renderers_by_node; + std::vector> m_links; + + std::unordered_map m_input_socket_by_id; + std::unordered_map m_output_socket_by_id; + + // Current rendering mode for the graph background and grid. + GraphRenderingMode m_render_mode = GraphRenderingMode::Default; + + bool m_use_small_font = true; + + bool m_save_dialog_wants_open = false; + bool m_open_dialog_wants_open = false; + bool m_pending_auto_layout = false; + + // Add-node popup (Shift+A) and modal (menu) + bool m_open_add_node_request = false; + bool m_open_add_node_modal = false; + ImVec2 m_add_node_popup_pos = { 0, 0 }; + std::vector m_registered_node_types; // populated lazily on first open + int m_add_node_selected_idx = 0; + + // Inline rename state (settings panel) + char m_rename_buf[128] = {}; + std::string m_rename_current_node; // raw name of node whose name is in m_rename_buf + +private: + // Serializes the current graph (engine + UI positions) as indented JSON bytes. + QByteArray export_graph_json() const; + void render_save_dialog(); + void render_open_dialog(); + void render_add_node_popup(); + + // Takes ownership of a new graph, wires run/error signals, and calls init(). + void attach_graph(std::unique_ptr graph); + + // Parses JSON bytes, deserializes the graph, applies UI positions (or auto-layouts), + // and swaps it in. Shows the error modal and keeps the current graph on any failure. + void import_graph_json(const QByteArray& data, const std::string& source_name); + + void calculate_window_size(); + void center_target_layout(); + void calculate_auto_layout(); + + void recenter_graph(); + void reset_graph_layout(); + + void push_style(); + void pop_style(); + + void render_menu(); + void render_settings_panel(); + void poll_keyboard_shortcuts(); + + void rebuild_links(); + void rebuild_socket_id_maps(); + void delete_selected_nodes(); + void rename_selected_node(const std::string& old_name, const std::string& new_name); + + NodeRenderer* find_selected_node_renderer() const; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/OverlayRenderNode.cpp b/apps/webgpu_app/compute/OverlayRenderNode.cpp new file mode 100644 index 000000000..877338623 --- /dev/null +++ b/apps/webgpu_app/compute/OverlayRenderNode.cpp @@ -0,0 +1,88 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlayRenderNode.h" + +#include "nucleus/srs.h" +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +OverlayRenderNode::OverlayRenderNode(webgpu_engine::Context& context) + : OverlayRenderNode(context, OverlaySettings {}) +{ +} + +OverlayRenderNode::OverlayRenderNode(webgpu_engine::Context& context, const OverlaySettings& settings) + : Node({ InputSocket(*this, "texture", data_type()), + InputSocket(*this, "region aabb", data_type*>()) }, + {}) + , m_context(&context) + , m_settings(settings) +{ +} + +OverlayRenderNode::~OverlayRenderNode() +{ + if (auto overlay = m_result_overlay.lock()) + overlay->link_texture(nullptr); +} + +void OverlayRenderNode::run_impl() +{ + if (input_socket("texture").is_socket_connected() && input_socket("region aabb").is_socket_connected()) { + const auto* texture = std::get()>(input_socket("texture").get_connected_data()); + const auto* aabb = std::get*>()>(input_socket("region aabb").get_connected_data()); + + bool copy = m_settings.copy; + if (copy && texture && !(texture->texture().descriptor().usage & WGPUTextureUsage_CopySrc)) { + qWarning() << "OverlayRenderNode: source texture lacks CopySrc usage; falling back to linking instead of copying."; + copy = false; + } + + auto overlay = m_result_overlay.lock(); + if (!overlay) { // first run, or the user deleted it from the panel -> (re)create + overlay = std::make_shared(); + overlay->name = "Compute Result"; + m_context->overlay_renderer()->add_overlay(overlay); + m_result_overlay = overlay; + } + + // TODO: the stitch node ignores the last col/row; trim the aabb to match. This + // correction should eventually move into the stitch node's "region aabb" output. + radix::geometry::Aabb<2, double> trimmed = *aabb; + trimmed.max -= glm::dvec2(nucleus::srs::tile_width(18) / 65, nucleus::srs::tile_height(18) / 65); + overlay->settings.aabb = trimmed; + if (texture) { + if (copy) { + overlay->load_texture(*texture); + } else { + overlay->link_texture(texture); + } + } + overlay->update_gpu_settings(); + m_context->request_redraw(); + } + complete_run(); +} + +} // namespace webgpu_compute::nodes diff --git a/apps/webgpu_app/compute/OverlayRenderNode.h b/apps/webgpu_app/compute/OverlayRenderNode.h new file mode 100644 index 000000000..34b78e30b --- /dev/null +++ b/apps/webgpu_app/compute/OverlayRenderNode.h @@ -0,0 +1,69 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace webgpu_engine { +class Context; +class TextureOverlay; +} // namespace webgpu_engine + +namespace webgpu_compute::nodes { + +// A custom node that forwards the graph result to a TextureOverlay managed by the OverlayRenderer. +// Unlike a base compute node it knows the rendering layer. +class OverlayRenderNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(OverlayRenderNode) + + struct OverlaySettings { + // false: link the source texture directly (non-owning). + // true: copy the source into the overlay's own texture (requires CopySrc on the source). + bool copy = false; + }; + + explicit OverlayRenderNode(webgpu_engine::Context& context); + OverlayRenderNode(webgpu_engine::Context& context, const OverlaySettings& settings); + ~OverlayRenderNode() override; + + void set_settings(const OverlaySettings& settings) { m_settings = settings; } + const OverlaySettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override { out["copy"] = m_settings.copy; } + void deserialize_settings(const QJsonObject& in) override + { + if (in.contains("copy")) + m_settings.copy = in["copy"].toBool(m_settings.copy); + } + +public slots: + void run_impl() override; + +private: + webgpu_engine::Context* m_context; + OverlaySettings m_settings; + std::weak_ptr m_result_overlay; // weak: the user may delete it in the gui +}; + +} // namespace webgpu_compute::nodes diff --git a/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.cpp new file mode 100644 index 000000000..1c026dbda --- /dev/null +++ b/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.cpp @@ -0,0 +1,69 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "BufferToTextureNodeRenderer.h" + +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +BufferToTextureNodeRenderer::BufferToTextureNodeRenderer(const std::string& name, nodes::BufferToTextureNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ +} + +void BufferToTextureNodeRenderer::render_settings_content() +{ + auto& s = m_node->settings(); + bool changed = false; + + changed |= ImGui::Checkbox("Alpha Blending", &s.use_transparency_buffer); + if (s.use_transparency_buffer) { + ImGui::DragFloat2("Alpha Bounds", &s.transparency_map_bounds.x, 1.0f, 0.0f, 1000.0f, "%.2f"); + if (s.transparency_map_bounds.x > s.transparency_map_bounds.y) + s.transparency_map_bounds.x = s.transparency_map_bounds.y; + changed |= ImGui::IsItemDeactivatedAfterEdit(); + } + + bool interpolation = (s.texture_filter_mode == WGPUFilterMode_Linear); + if (ImGui::Checkbox("Interpolation & MipMaps", &interpolation)) { + if (interpolation) { + s.texture_filter_mode = WGPUFilterMode_Linear; + s.texture_mipmap_filter_mode = WGPUMipmapFilterMode_Linear; + s.texture_max_aniostropy = 16; + s.create_mipmaps = true; + } else { + s.texture_filter_mode = WGPUFilterMode_Nearest; + s.texture_mipmap_filter_mode = WGPUMipmapFilterMode_Nearest; + s.texture_max_aniostropy = 1; + s.create_mipmaps = false; + } + changed = true; + } + + changed |= ImGui::DragFloat2("Color Bounds", &s.color_map_bounds.x, 1.0f, -10000.0f, 10000.0f, "%.2f"); + changed |= ImGui::Checkbox("Bin Interpolation", &s.use_bin_interpolation); + + if (changed) + m_node->rerun(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.h b/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.h new file mode 100644 index 000000000..9cfd32f3c --- /dev/null +++ b/apps/webgpu_app/compute/nodes/BufferToTextureNodeRenderer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class BufferToTextureNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class BufferToTextureNodeRenderer : public NodeRenderer { +public: + BufferToTextureNodeRenderer(const std::string& name, nodes::BufferToTextureNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::BufferToTextureNode* m_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.cpp new file mode 100644 index 000000000..96444a9cf --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.cpp @@ -0,0 +1,109 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeAvalancheTrajectoriesNodeRenderer.h" + +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +using Node = nodes::ComputeAvalancheTrajectoriesNode; + +ComputeAvalancheTrajectoriesNodeRenderer::ComputeAvalancheTrajectoriesNodeRenderer(const std::string& name, nodes::ComputeAvalancheTrajectoriesNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ +} + +void ComputeAvalancheTrajectoriesNodeRenderer::render_settings_content() +{ + auto settings = m_node->get_settings(); + bool settings_changed = false; + bool rerun = false; + + // --- General --- + const uint32_t min_res = 1, max_res = 32; + settings_changed |= ImGui::SliderScalar("Output Resolution", ImGuiDataType_U32, &settings.resolution_multiplier, &min_res, &max_res, "%ux"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + const uint32_t min_steps = 1, max_steps = 20000; + settings_changed |= ImGui::DragScalar("Num steps", ImGuiDataType_U32, &settings.num_steps, 1.0f, &min_steps, &max_steps, "%u"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + const uint32_t min_paths = 1, max_paths = 2048; + settings_changed + |= ImGui::DragScalar("Num particles per cell", ImGuiDataType_U32, &settings.num_paths_per_release_cell, 1.0f, &min_paths, &max_paths, "%u"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + const uint32_t min_runs = 1, max_runs = 1000; + settings_changed |= ImGui::DragScalar("Number of Runs", ImGuiDataType_U32, &settings.num_runs, 1.0f, &min_runs, &max_runs, "%u"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + const uint32_t min_seed = 1, max_seed = 1000000; + settings_changed |= ImGui::DragScalar("Random seed", ImGuiDataType_U32, &settings.random_seed, 1.0f, &min_seed, &max_seed); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + ImGui::Separator(); + + // --- Physics model --- + if (ImGui::Combo("Model", (int*)&settings.active_model, "weBIGeo Avalanche Simulation\0Physics Less Simple\0")) { + settings_changed = rerun = true; + } + + if (settings.active_model == Node::PhysicsModelType::WEBIGEO_AVALANCHE_SIMULATION) { + float perturbation_deg = glm::degrees(settings.max_perturbation); + if (ImGui::DragFloat("Max Perturbation", &perturbation_deg, 0.1f, 0.0f, 90.0f, "%.1f°")) { + settings.max_perturbation = glm::radians(perturbation_deg); + settings_changed = true; + } + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + settings_changed |= ImGui::DragFloat("Persistence", &settings.persistence_contribution, 0.01f, 0.0f, 0.99f, "%.2f"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + settings_changed |= ImGui::DragFloat("Alpha", &settings.runout_flowpy.alpha, 0.01f, 0.0f, 90.0f, "%.2f°"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + } else if (settings.active_model == Node::PhysicsModelType::PHYSICS_LESS_SIMPLE) { + settings_changed |= ImGui::SliderFloat("Gravity", &settings.model2.gravity, 0.0f, 15.0f, "%.2f"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + settings_changed |= ImGui::SliderFloat("Mass", &settings.model2.mass, 0.0f, 100.0f, "%.2f"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + settings_changed |= ImGui::SliderFloat("Drag coeff", &settings.model2.drag_coeff, 1.0f, 10000.0f, "%.0f"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + settings_changed |= ImGui::SliderFloat("Friction coeff", &settings.model2.friction_coeff, 0.0f, 0.5f, "%.3f"); + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + if (ImGui::Combo("Friction model", (int*)&settings.active_runout_model, "Coulomb\0Voellmy\0Voellmy Min Shear\0SamosAt\0")) { + settings_changed = rerun = true; + } + } + + if (settings_changed) + m_node->set_settings(settings); + if (rerun) + m_node->rerun(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.h b/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.h new file mode 100644 index 000000000..bc62b4f26 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeAvalancheTrajectoriesNodeRenderer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class ComputeAvalancheTrajectoriesNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class ComputeAvalancheTrajectoriesNodeRenderer : public NodeRenderer { +public: + ComputeAvalancheTrajectoriesNodeRenderer(const std::string& name, nodes::ComputeAvalancheTrajectoriesNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::ComputeAvalancheTrajectoriesNode* m_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.cpp new file mode 100644 index 000000000..79d67a061 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.cpp @@ -0,0 +1,62 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeReleasePointsNodeRenderer.h" + +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +ComputeReleasePointsNodeRenderer::ComputeReleasePointsNodeRenderer(const std::string& name, nodes::ComputeReleasePointsNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ +} + +void ComputeReleasePointsNodeRenderer::render_settings_content() +{ + auto settings = m_node->get_settings(); + bool settings_changed = false; + bool rerun = false; + + int interval = (int)settings.sampling_interval.x; + if (ImGui::SliderInt("Interval", &interval, 1, 64, "%u")) { + settings.sampling_interval = glm::uvec2(interval); + settings_changed = true; + } + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + float min_deg = glm::degrees(settings.min_slope_angle); + float max_deg = glm::degrees(settings.max_slope_angle); + if (ImGui::DragFloatRange2("Steepness", &min_deg, &max_deg, 0.1f, 0.0f, 90.0f, "Min: %.1f°", "Max: %.1f°", ImGuiSliderFlags_AlwaysClamp)) { + settings.min_slope_angle = glm::radians(min_deg); + settings.max_slope_angle = glm::radians(max_deg); + settings_changed = true; + } + rerun |= ImGui::IsItemDeactivatedAfterEdit(); + + if (settings_changed) + m_node->set_settings(settings); + if (rerun) + m_node->rerun(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.h b/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.h new file mode 100644 index 000000000..e6bc0dc30 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeReleasePointsNodeRenderer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class ComputeReleasePointsNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class ComputeReleasePointsNodeRenderer : public NodeRenderer { +public: + ComputeReleasePointsNodeRenderer(const std::string& name, nodes::ComputeReleasePointsNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::ComputeReleasePointsNode* m_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.cpp new file mode 100644 index 000000000..9cd55f57a --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.cpp @@ -0,0 +1,52 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeSnowNodeRenderer.h" + +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +ComputeSnowNodeRenderer::ComputeSnowNodeRenderer(const std::string& name, nodes::ComputeSnowNode& node) + : NodeRenderer(name, node) + , m_snow_node(&node) +{ +} + +void ComputeSnowNodeRenderer::render_settings_content() +{ + auto settings = m_snow_node->get_settings(); + bool changed = false; + + changed + |= ImGui::DragFloatRange2("Ang.-limit", &settings.min_angle, &settings.max_angle, 0.1f, 0.0f, 90.0f, "%.1f°", "%.1f°", ImGuiSliderFlags_AlwaysClamp); + changed |= ImGui::SliderFloat("Ang.-blend", &settings.angle_blend, 0.0f, 90.0f, "%.1f°"); + changed |= ImGui::SliderFloat("Alt.-limit", &settings.min_altitude, 0.0f, 4000.0f, "%.1fm"); + changed |= ImGui::SliderFloat("Alt.-variation", &settings.altitude_variation, 0.0f, 1000.0f, "%.1fm"); + changed |= ImGui::SliderFloat("Alt.-blend", &settings.altitude_blend, 0.0f, 1000.0f, "%.1fm"); + + if (changed) { + m_snow_node->set_settings(settings); + m_snow_node->rerun(); + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.h b/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.h new file mode 100644 index 000000000..3574fdace --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ComputeSnowNodeRenderer.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class ComputeSnowNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class ComputeSnowNodeRenderer : public NodeRenderer { +public: + ComputeSnowNodeRenderer(const std::string& name, nodes::ComputeSnowNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::ComputeSnowNode* m_snow_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ExportNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/ExportNodeRenderer.cpp new file mode 100644 index 000000000..a65d28da1 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ExportNodeRenderer.cpp @@ -0,0 +1,68 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ExportNodeRenderer.h" + +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +ExportNodeRenderer::ExportNodeRenderer(const std::string& name, nodes::ExportNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ + const auto& s = m_node->get_settings(); + std::strncpy(m_buffer_buf, s.buffer_output_file.c_str(), sizeof(m_buffer_buf) - 1); + std::strncpy(m_texture_buf, s.texture_output_file.c_str(), sizeof(m_texture_buf) - 1); + std::strncpy(m_aabb_buf, s.aabb_output_file.c_str(), sizeof(m_aabb_buf) - 1); +} + +void ExportNodeRenderer::render_settings_content() +{ + auto settings = m_node->get_settings(); + bool changed = false; + + auto field = [&](const char* label, char* buf, size_t buf_size, std::string& target, const char* socket_name) { + bool connected = m_node->input_socket(socket_name).is_socket_connected(); + ImGui::TextUnformatted(label); + if (!connected) { + ImGui::SameLine(); + ImGui::TextDisabled("(not connected)"); + } + ImGui::SetNextItemWidth(-1); + if (ImGui::InputText((std::string("##") + socket_name).c_str(), buf, buf_size)) { + target = buf; + changed = true; + } + }; + + field("Buffer Output:", m_buffer_buf, sizeof(m_buffer_buf), settings.buffer_output_file, "buffer"); + field("Texture Output:", m_texture_buf, sizeof(m_texture_buf), settings.texture_output_file, "texture"); + field("AABB Output:", m_aabb_buf, sizeof(m_aabb_buf), settings.aabb_output_file, "region aabb"); + + if (changed) + m_node->set_settings(settings); + + ImGui::Spacing(); + ImGui::TextDisabled("Placeholders: {node_name}, {run_datetime}, {run_id}"); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/ExportNodeRenderer.h b/apps/webgpu_app/compute/nodes/ExportNodeRenderer.h new file mode 100644 index 000000000..ca0f3b841 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/ExportNodeRenderer.h @@ -0,0 +1,43 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class ExportNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class ExportNodeRenderer : public NodeRenderer { +public: + ExportNodeRenderer(const std::string& name, nodes::ExportNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::ExportNode* m_node; + char m_buffer_buf[512] = {}; + char m_texture_buf[512] = {}; + char m_aabb_buf[512] = {}; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.cpp new file mode 100644 index 000000000..db5dbffb7 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.cpp @@ -0,0 +1,74 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "GPXTrackNodeRenderer.h" + +#include +#include +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +GPXTrackNodeRenderer::GPXTrackNodeRenderer(const std::string& name, nodes::GPXTrackNode& node) + : NodeRenderer(name, node) + , m_node(&node) + , m_dialog_id("gpxnode_" + std::to_string(get_node_id())) +{ + const std::string& path = m_node->get_settings().file_path; + std::strncpy(m_path_buffer.data(), path.c_str(), m_path_buffer.size() - 1); +} + +void GPXTrackNodeRenderer::render_settings_content() +{ + auto settings = m_node->get_settings(); + + const float btn_w = ImGui::CalcTextSize("Browse...").x + ImGui::GetStyle().FramePadding.x * 2.0f; + ImGui::SetNextItemWidth(-btn_w - ImGui::GetStyle().ItemSpacing.x); + ImGui::InputText("##gpx_path", m_path_buffer.data(), m_path_buffer.size()); + if (ImGui::IsItemDeactivatedAfterEdit()) { + settings.file_path = m_path_buffer.data(); + m_node->set_settings(settings); + m_node->rerun(); + } + ImGui::SameLine(); + m_want_open_dialog = ImGui::Button("Browse..."); + + if (ImGui::Checkbox("Cache (reload only on path change)", &settings.enable_caching)) { + m_node->set_settings(settings); + m_node->rerun(); + } +} + +void GPXTrackNodeRenderer::render_dialogs() +{ + m_picked_files.clear(); + if (ImGuiManager::FilePicker( + m_dialog_id.c_str(), "Choose GPX File", ".gpx,.*", m_want_open_dialog, m_picked_files, /*allow_multiple=*/false, m_last_dialog_directory.c_str())) { + m_last_dialog_directory = std::filesystem::path(m_picked_files[0]).parent_path().string(); + auto settings = m_node->get_settings(); + settings.file_path = m_picked_files[0]; + std::strncpy(m_path_buffer.data(), settings.file_path.c_str(), m_path_buffer.size() - 1); + m_node->set_settings(settings); + m_node->rerun(); + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.h b/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.h new file mode 100644 index 000000000..13672d920 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/GPXTrackNodeRenderer.h @@ -0,0 +1,49 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" +#include +#include +#include + +namespace webgpu_compute::nodes { +class GPXTrackNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class GPXTrackNodeRenderer : public NodeRenderer { +public: + GPXTrackNodeRenderer(const std::string& name, nodes::GPXTrackNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + void render_dialogs() override; + +private: + nodes::GPXTrackNode* m_node; + std::array m_path_buffer {}; + std::string m_last_dialog_directory = "."; + std::string m_dialog_id; + std::vector m_picked_files; + bool m_want_open_dialog = false; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/NodeRenderer.cpp b/apps/webgpu_app/compute/nodes/NodeRenderer.cpp new file mode 100644 index 000000000..228e284ec --- /dev/null +++ b/apps/webgpu_app/compute/nodes/NodeRenderer.cpp @@ -0,0 +1,257 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeRenderer.h" + +#include +#include +#include +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +static std::hash hasher; + +ImU32 NodeRenderer::pin_color_for_type(nodes::DataType type) +{ + switch (type) { + case 0: + return IM_COL32(255, 160, 50, 255); // tile ID list -> orange + case 1: + return IM_COL32(150, 220, 50, 255); // QByteArray list -> yellow-green + case 2: + return IM_COL32(70, 130, 255, 255); // TileStorageTexture -> blue + case 3: + return IM_COL32(190, 80, 255, 255); // RawBuffer -> purple + case 4: + return IM_COL32(50, 210, 210, 255); // TextureWithSampler -> cyan + case 5: + return IM_COL32(255, 80, 80, 255); // Aabb -> red + case 6: + return IM_COL32(200, 200, 200, 255); // uvec2 -> gray + default: + return IM_COL32(255, 255, 255, 255); + } +} + +ImNodesPinShape NodeRenderer::pin_shape_for_type(nodes::DataType type) +{ + switch (type) { + case 0: + return ImNodesPinShape_CircleFilled; // tile ID list + case 1: + return ImNodesPinShape_CircleFilled; // QByteArray list + case 2: + return ImNodesPinShape_QuadFilled; // TileStorageTexture + case 3: + return ImNodesPinShape_Quad; // RawBuffer + case 4: + return ImNodesPinShape_TriangleFilled; // TextureWithSampler + case 5: + return ImNodesPinShape_Triangle; // Aabb + case 6: + return ImNodesPinShape_Circle; // uvec2 + default: + return ImNodesPinShape_CircleFilled; + } +} + +std::string NodeRenderer::format_ms(int duration_in_ms) +{ + std::ostringstream ss; + ss << std::fixed << std::setprecision(2); + + if (duration_in_ms < 1000) { + ss.str(""); + ss.clear(); + ss << duration_in_ms << " ms"; + } else { + double seconds = duration_in_ms / 1000.0; + ss.str(""); + ss.clear(); + ss << seconds << " s"; + } + + return ss.str(); +} + +NodeRenderer::NodeRenderer(const std::string& name, nodes::Node& node) + : m_name(name) + , m_node(&node) + , m_node_id(int(hasher(m_name))) +{ + for (const auto& socket : m_node->input_sockets()) { + const int socket_id = int(hasher(m_name + socket.name())); + m_input_socket_ids.push_back(socket_id); + } + + for (const auto& socket : m_node->output_sockets()) { + const int socket_id = int(hasher(m_name + socket.name())); + m_output_socket_ids.push_back(socket_id); + } +} + +ImVec2 NodeRenderer::get_size() const { return m_size; } + +void NodeRenderer::render(bool reset_position) +{ + if (reset_position) { + ImNodes::SetNodeEditorSpacePos(m_node_id, m_position); + } + + bool is_disabled = !m_node->is_enabled(); + bool is_running = m_node->is_running(); + + if (is_disabled) { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3f); + ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(100, 100, 100, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, IM_COL32(100, 100, 100, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, IM_COL32(100, 100, 100, 255)); + } else if (is_running) { + // Dark green title bar while running + ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(30, 100, 30, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, IM_COL32(40, 120, 40, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, IM_COL32(50, 140, 50, 255)); + } + + ImNodes::BeginNode(m_node_id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted(m_name.c_str()); + ImNodes::EndNodeTitleBar(); + + render_sockets(); + + ImNodes::EndNode(); + + // Get size of the node + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImGui::GetItemRectMax(); + m_size = ImVec2(max.x - min.x, max.y - min.y); + + // Context menu (right-click on node) + std::string popup_id = "##ctx_" + m_name; + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) + ImGui::OpenPopup(popup_id.c_str()); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f, 8.0f)); + if (ImGui::BeginPopup(popup_id.c_str())) { + if (ImGui::MenuItem(ICON_FA_REDO " Rerun")) + m_node->rerun(); + bool enabled = m_node->is_enabled(); + if (ImGui::MenuItem(enabled ? ICON_FA_TOGGLE_OFF " Disable" : ICON_FA_TOGGLE_ON " Enable")) + m_node->set_enabled(!enabled); + ImGui::EndPopup(); + } + ImGui::PopStyleVar(2); + + // Get position of the node + m_position = ImNodes::GetNodeEditorSpacePos(m_node_id); + + // Pop color/style stacks + if (is_disabled) { + ImNodes::PopColorStyle(); // selected + ImNodes::PopColorStyle(); // hovered + ImNodes::PopColorStyle(); // normal + ImGui::PopStyleVar(); + } else if (is_running) { + ImNodes::PopColorStyle(); // selected + ImNodes::PopColorStyle(); // hovered + ImNodes::PopColorStyle(); // normal + } +} + +void NodeRenderer::render_sockets() +{ + for (size_t i = 0; i < m_input_socket_ids.size(); i++) { + const nodes::DataType type = m_node->input_sockets().at(i).type(); + ImNodes::PushColorStyle(ImNodesCol_Pin, pin_color_for_type(type)); + ImNodes::PushColorStyle(ImNodesCol_PinHovered, pin_color_for_type(type)); + ImNodes::BeginInputAttribute(m_input_socket_ids.at(i), pin_shape_for_type(type)); + ImGui::Text("%s", m_node->input_sockets().at(i).name().c_str()); + ImNodes::EndInputAttribute(); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + } + + const float node_content_width = m_size.x >= 0 ? m_size.x - 1.0f : 0.0f; + for (size_t i = 0; i < m_output_socket_ids.size(); i++) { + const nodes::DataType type = m_node->output_sockets().at(i).type(); + ImNodes::PushColorStyle(ImNodesCol_Pin, pin_color_for_type(type)); + ImNodes::PushColorStyle(ImNodesCol_PinHovered, pin_color_for_type(type)); + ImNodes::BeginOutputAttribute(m_output_socket_ids.at(i), pin_shape_for_type(type)); + const char* label = m_node->output_sockets().at(i).name().c_str(); + const float text_width = ImGui::CalcTextSize(label).x; + const float text_indent = std::max(0.0f, node_content_width - text_width); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + text_indent); + ImGui::TextUnformatted(label); + ImNodes::EndOutputAttribute(); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + } +} + +QJsonObject NodeRenderer::serialize_ui() const { return QJsonObject { { "position", QJsonArray { m_position.x, m_position.y } } }; } + +void NodeRenderer::deserialize_ui(const QJsonObject& obj) +{ + if (obj.contains("position")) { + const auto arr = obj["position"].toArray(); + if (arr.size() == 2) + m_position = { static_cast(arr[0].toDouble()), static_cast(arr[1].toDouble()) }; + } +} + +void NodeRenderer::rename(const std::string& new_name) +{ + m_name = new_name; + // Node ID and socket IDs are intentionally left unchanged to keep imnodes selection stable. +} + +int NodeRenderer::get_input_socket_id(const std::string& input_socket_name) const +{ + assert(m_node->has_input_socket(input_socket_name)); + + for (size_t i = 0; i < m_node->input_sockets().size(); i++) { + if (input_socket_name == m_node->input_sockets().at(i).name()) { + return m_input_socket_ids.at(i); + } + } + qFatal() << "tried to get non-existing input socket " << input_socket_name << " from node renderer for node " << m_name; + return -1; +} + +int NodeRenderer::get_output_socket_id(const std::string& output_socket_name) const +{ + assert(m_node->has_output_socket(output_socket_name)); + + for (size_t i = 0; i < m_node->output_sockets().size(); i++) { + if (output_socket_name == m_node->output_sockets().at(i).name()) { + return m_output_socket_ids.at(i); + } + } + qFatal() << "tried to get non-existing output socket " << output_socket_name << " from node renderer for node " << m_name; + return -1; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/NodeRenderer.h b/apps/webgpu_app/compute/nodes/NodeRenderer.h new file mode 100644 index 000000000..145119806 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/NodeRenderer.h @@ -0,0 +1,81 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class NodeRenderer { +public: + NodeRenderer(const std::string& name, nodes::Node& node); + virtual ~NodeRenderer() = default; + + void render(bool reset_position = false); + void render_sockets(); + + virtual bool has_settings() const { return false; } + virtual void render_settings_content() { } + // Called outside ImNodes::BeginNodeEditor/EndNodeEditor for proper z-ordering + virtual void render_dialogs() { } + + // UI-layer serialization: node position (and subclass-specific data). + virtual QJsonObject serialize_ui() const; + virtual void deserialize_ui(const QJsonObject& obj); + + int get_node_id() const { return m_node_id; } + const std::string& get_name() const { return m_name; } + void rename(const std::string& new_name); + + int get_input_socket_id(const std::string& input_socket_name) const; + int get_output_socket_id(const std::string& output_socket_name) const; + + void set_position(const ImVec2& position) { m_position = position; } + ImVec2 get_position() const { return m_position; } + ImVec2 get_size() const; + bool is_size_known() const { return m_size.x >= 0; } + + nodes::Node* get_node() const { return m_node; } + + static std::string format_ms(int duration_in_ms); + + static ImU32 pin_color_for_type(nodes::DataType type); + static ImNodesPinShape pin_shape_for_type(nodes::DataType type); + +private: + std::string m_name; + nodes::Node* m_node = nullptr; + int m_node_id = 0; + std::vector m_input_socket_ids; + std::vector m_output_socket_ids; + + ImVec2 m_position = { 0, 0 }; + ImVec2 m_size = { -1, -1 }; // Initialized after first frame +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/NodeRendererFactory.cpp b/apps/webgpu_app/compute/nodes/NodeRendererFactory.cpp new file mode 100644 index 000000000..8fbb031ba --- /dev/null +++ b/apps/webgpu_app/compute/nodes/NodeRendererFactory.cpp @@ -0,0 +1,68 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeRendererFactory.h" + +#include "BufferToTextureNodeRenderer.h" +#include "ComputeAvalancheTrajectoriesNodeRenderer.h" +#include "ComputeReleasePointsNodeRenderer.h" +#include "ComputeSnowNodeRenderer.h" +#include "ExportNodeRenderer.h" +#include "GPXTrackNodeRenderer.h" +#include "NodeRenderer.h" +#include "OverlayNodeRenderer.h" +#include "RequestTilesNodeRenderer.h" +#include "SelectTilesNodeRenderer.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compute/OverlayRenderNode.h" + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +std::unique_ptr NodeRendererFactory::create(const std::string& name, nodes::Node& node) +{ + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + if (auto* n = dynamic_cast(&node)) + return std::make_unique(name, *n); + return std::make_unique(name, node); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/NodeRendererFactory.h b/apps/webgpu_app/compute/nodes/NodeRendererFactory.h new file mode 100644 index 000000000..b65205700 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/NodeRendererFactory.h @@ -0,0 +1,38 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu_compute::nodes { +class Node; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class NodeRenderer; + +class NodeRendererFactory { +public: + static std::unique_ptr create(const std::string& name, nodes::Node& node); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.cpp new file mode 100644 index 000000000..9c10db026 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.cpp @@ -0,0 +1,47 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlayNodeRenderer.h" + +#include "compute/OverlayRenderNode.h" +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +OverlayNodeRenderer::OverlayNodeRenderer(const std::string& name, nodes::OverlayRenderNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ +} + +void OverlayNodeRenderer::render_settings_content() +{ + auto settings = m_node->get_settings(); + + const char* mode_items[] = { "Linked", "Copy" }; + int mode_idx = settings.copy ? 1 : 0; + ImGui::SetNextItemWidth(-1); + if (ImGui::Combo("##overlay_mode", &mode_idx, mode_items, 2)) { + settings.copy = (mode_idx == 1); + m_node->set_settings(settings); + } + ImGui::TextDisabled("Linked: sample source directly. Copy: own a copy (needs CopySrc)."); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.h b/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.h new file mode 100644 index 000000000..dbcc3d563 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/OverlayNodeRenderer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class OverlayRenderNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class OverlayNodeRenderer : public NodeRenderer { +public: + OverlayNodeRenderer(const std::string& name, nodes::OverlayRenderNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::OverlayRenderNode* m_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.cpp new file mode 100644 index 000000000..9ed52b9eb --- /dev/null +++ b/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.cpp @@ -0,0 +1,49 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RequestTilesNodeRenderer.h" + +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +RequestTilesNodeRenderer::RequestTilesNodeRenderer(const std::string& name, nodes::RequestTilesNode& node) + : NodeRenderer(name, node) + , m_node(&node) + , m_options({ + { "DTM tiles", { "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/", nucleus::tile::TileLoadService::UrlPattern::ZXY, ".png" } }, + { "DSM tiles", { "https://alpinemaps.cg.tuwien.ac.at/tiles/alpine_png/", nucleus::tile::TileLoadService::UrlPattern::ZXY, ".png" } }, + }) +{ +} + +void RequestTilesNodeRenderer::render_settings_content() +{ + std::string combo_items; + for (const auto& opt : m_options) + combo_items += opt.name + '\0'; + combo_items += '\0'; + + if (ImGui::Combo("Tile source", &m_selected_index, combo_items.c_str())) { + m_node->set_settings(m_options[m_selected_index].settings); + m_node->rerun(); + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.h b/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.h new file mode 100644 index 000000000..1dcd5db59 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/RequestTilesNodeRenderer.h @@ -0,0 +1,46 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" +#include +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class RequestTilesNodeRenderer : public NodeRenderer { +public: + struct TileSourceOption { + std::string name; + nodes::RequestTilesNode::RequestTilesNodeSettings settings; + }; + + RequestTilesNodeRenderer(const std::string& name, nodes::RequestTilesNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::RequestTilesNode* m_node; + std::vector m_options; + int m_selected_index = 0; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.cpp b/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.cpp new file mode 100644 index 000000000..783d71c16 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.cpp @@ -0,0 +1,46 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "SelectTilesNodeRenderer.h" + +#include +#include + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +SelectTilesNodeRenderer::SelectTilesNodeRenderer(const std::string& name, nodes::SelectTilesNode& node) + : NodeRenderer(name, node) + , m_node(&node) +{ +} + +void SelectTilesNodeRenderer::render_settings_content() +{ + static uint32_t zoom_level = m_node->get_settings().zoomlevel; + + const uint32_t min_zoomlevel = 1; + const uint32_t max_zoomlevel = 18; + ImGui::SliderScalar("Zoom level", ImGuiDataType_U32, &zoom_level, &min_zoomlevel, &max_zoomlevel); + if (ImGui::IsItemDeactivatedAfterEdit()) { + m_node->set_settings(nodes::SelectTilesNode::SelectTilesNodeSettings { zoom_level }); + m_node->rerun(); + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.h b/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.h new file mode 100644 index 000000000..93e3a0c70 --- /dev/null +++ b/apps/webgpu_app/compute/nodes/SelectTilesNodeRenderer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeRenderer.h" + +namespace webgpu_compute::nodes { +class SelectTilesNode; +} + +namespace webgpu_app { +namespace nodes = webgpu_compute::nodes; + +class SelectTilesNodeRenderer : public NodeRenderer { +public: + SelectTilesNodeRenderer(const std::string& name, nodes::SelectTilesNode& node); + bool has_settings() const override { return true; } + void render_settings_content() override; + +private: + nodes::SelectTilesNode* m_node; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/main.cpp b/apps/webgpu_app/main.cpp new file mode 100644 index 000000000..b1d053668 --- /dev/null +++ b/apps/webgpu_app/main.cpp @@ -0,0 +1,58 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "App.h" +#include "util/SearchService.h" +#include "util/error_logging.h" +#include + +void print_logo() +{ + const char* logo = R"( +%4 _ . , . . * * . +%4 * / \_ * / \_ %1 . ___ %2___%3 ___ . %4 /\'__ +%4 / \ / \, %1__ __ _____| _ )%2_ _%3/ __|___ ___ %4 . _/ / \ *'. +%4 . /\/\ /\/ :' __ \_ %1\ V V / -_) _ \%2| |%3 (_ / -_) _ \%4 _^/ ^/ `--. +%4 / \/ \ _/ \-'\%1 \_/\_/\___|___/%2___%3\___\___\___/%4 /.' ^_ \_ .'\ +============================================================================== +)"; + QString formatted_logo = QString(logo).remove(0, 1); + formatted_logo = formatted_logo.arg("\033[36m").arg("\033[38;5;245m").arg("\033[0m").arg("\033[38;5;245m"); + std::cout << formatted_logo.toStdString(); +} + +int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) +{ +#ifndef __EMSCRIPTEN__ + print_logo(); +#endif + // Init QCoreApplication is necessary as it declares the current thread + // as a Qt-Thread. Otherwise functionalities like QTimers wouldnt work. + // It basically initializes the Qt environment + QCoreApplication app(argc, argv); + + // Set custom logging handler for Qt + qInstallMessageHandler(qt_logging_callback); + + webgpu_app::App renderer; + renderer.start(); + // NOTE: Please be aware that for WEB-Deployment renderer.start() is non-blocking!! + // So Code at this point will run after initialization + return 0; +} diff --git a/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.cpp b/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.cpp new file mode 100644 index 000000000..1bfbf246a --- /dev/null +++ b/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.cpp @@ -0,0 +1,48 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "HeightLinesOverlayImGuiRenderer.h" + +#include + +namespace webgpu_app { + +HeightLinesOverlayImGuiRenderer::HeightLinesOverlayImGuiRenderer(webgpu_engine::HeightLinesOverlay& overlay) + : OverlayImGuiRenderer(overlay) + , m_height_lines_overlay(&overlay) +{ +} + +bool HeightLinesOverlayImGuiRenderer::render_custom_settings() +{ + auto& s = m_height_lines_overlay->settings; + bool changed = false; + + changed |= ImGui::SliderFloat("Primary Interval (m)", &s.primary_interval, 10.0f, 1000.0f); + changed |= ImGui::SliderFloat("Secondary Interval (m)", &s.secondary_interval, 5.0f, 500.0f); + changed |= ImGui::SliderFloat("Base Width", &s.base_width, 0.5f, 10.0f); + changed |= ImGui::SliderFloat("Minor Opacity", &s.minor_opacity, 0.0f, 1.0f); + changed |= ImGui::ColorEdit4("Line Color", (float*)&s.line_color); + + if (changed) + m_height_lines_overlay->update_settings(); + + return changed; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.h b/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.h new file mode 100644 index 000000000..f8044df2c --- /dev/null +++ b/apps/webgpu_app/overlay/HeightLinesOverlayImGuiRenderer.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include + +namespace webgpu_app { + +class HeightLinesOverlayImGuiRenderer : public OverlayImGuiRenderer { +public: + explicit HeightLinesOverlayImGuiRenderer(webgpu_engine::HeightLinesOverlay& overlay); + + std::string display_name() const override { return "Height Lines"; } + bool render_custom_settings() override; + +private: + webgpu_engine::HeightLinesOverlay* m_height_lines_overlay; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlayImGuiRenderer.cpp b/apps/webgpu_app/overlay/OverlayImGuiRenderer.cpp new file mode 100644 index 000000000..2f5b5bb25 --- /dev/null +++ b/apps/webgpu_app/overlay/OverlayImGuiRenderer.cpp @@ -0,0 +1,50 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlayImGuiRenderer.h" + +#include +#include + +namespace webgpu_app { + +OverlayImGuiRenderer::OverlayImGuiRenderer(webgpu_engine::Overlay& overlay) + : m_overlay(&overlay) +{ +} + +std::string OverlayImGuiRenderer::effective_name() const { return m_overlay->name.empty() ? display_name() : m_overlay->name; } + +bool OverlayImGuiRenderer::render_settings() +{ + bool changed = false; + + char buf[128]; + std::snprintf(buf, sizeof(buf), "%s", m_overlay->name.c_str()); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputTextWithHint("##overlay_name", display_name().c_str(), buf, sizeof(buf))) { + m_overlay->name = buf; + changed = true; + } + ImGui::Separator(); + + changed |= render_custom_settings(); + return changed; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlayImGuiRenderer.h b/apps/webgpu_app/overlay/OverlayImGuiRenderer.h new file mode 100644 index 000000000..02637dd10 --- /dev/null +++ b/apps/webgpu_app/overlay/OverlayImGuiRenderer.h @@ -0,0 +1,48 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu_app { + +/// Base class for per-overlay ImGui UI renderers. +class OverlayImGuiRenderer { +public: + explicit OverlayImGuiRenderer(webgpu_engine::Overlay& overlay); + virtual ~OverlayImGuiRenderer() = default; + + virtual std::string display_name() const { return "Overlay"; } + // The user-set name if any, otherwise display_name() + std::string effective_name() const; + // Renders the general name field followed by overlay-specific settings (render_custom_settings); returns true if redraw needed. + bool render_settings(); + +protected: + // Subclasses override this to to render custom ImGui Settings + virtual bool render_custom_settings() { return false; } + + webgpu_engine::Overlay* overlay() const { return m_overlay; } + +protected: + webgpu_engine::Overlay* m_overlay; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.cpp b/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.cpp new file mode 100644 index 000000000..602a772e6 --- /dev/null +++ b/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.cpp @@ -0,0 +1,46 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlayImGuiRendererFactory.h" + +#include "HeightLinesOverlayImGuiRenderer.h" +#include "ScreenSpaceSnowOverlayImGuiRenderer.h" +#include "TextureOverlayImGuiRenderer.h" +#include "TileDebugOverlayImGuiRenderer.h" + +#include +#include +#include +#include + +namespace webgpu_app { + +std::unique_ptr OverlayImGuiRendererFactory::create(webgpu_engine::Overlay& overlay) +{ + if (auto* o = dynamic_cast(&overlay)) + return std::make_unique(*o); + if (auto* o = dynamic_cast(&overlay)) + return std::make_unique(*o); + if (auto* o = dynamic_cast(&overlay)) + return std::make_unique(*o); + if (auto* o = dynamic_cast(&overlay)) + return std::make_unique(*o); + return std::make_unique(overlay); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.h b/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.h new file mode 100644 index 000000000..d5f21289e --- /dev/null +++ b/apps/webgpu_app/overlay/OverlayImGuiRendererFactory.h @@ -0,0 +1,31 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include + +namespace webgpu_app { + +class OverlayImGuiRendererFactory { +public: + static std::unique_ptr create(webgpu_engine::Overlay& overlay); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlaysPanel.cpp b/apps/webgpu_app/overlay/OverlaysPanel.cpp new file mode 100644 index 000000000..5ce9d0934 --- /dev/null +++ b/apps/webgpu_app/overlay/OverlaysPanel.cpp @@ -0,0 +1,296 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlaysPanel.h" + +#include "OverlayImGuiRendererFactory.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace webgpu_app { + +enum AddType { ADD_HEIGHT_LINES = 0, ADD_TEXTURE_OVERLAY = 1, ADD_TILE_DEBUG = 2, ADD_SCREEN_SPACE_SNOW = 3 }; +constexpr const char* ADD_ITEMS[] = { "Height Lines", "Texture Overlay", "Tile Debug", "Screen-Space Snow" }; +constexpr const char* ADD_POPUP_ID = "Add Overlay###add_overlay"; + +OverlaysPanel::OverlaysPanel(webgpu_engine::Context* context) + : m_context(context) + , m_overlay_renderer(context->overlay_renderer()) +{ + rebuild_renderers(); +} + +void OverlaysPanel::rebuild_renderers() +{ + m_renderers.clear(); + for (auto& overlay : m_overlay_renderer->overlays()) + m_renderers.push_back(OverlayImGuiRendererFactory::create(*overlay)); + if (m_selected_engine_idx >= static_cast(m_renderers.size())) + m_selected_engine_idx = -1; +} + +// GUI shows overlays in descending z_index order with a virtual shading row at position P + +static int display_to_engine(int d, int N) { return N - 1 - d; } + +void OverlaysPanel::do_move(int gui_row, int direction, int P, int N) +{ + const int other = gui_row + direction; + if (other < 0 || other > N) + return; + + m_selected_engine_idx = -1; + + auto gui_slots = m_overlay_renderer->overlays(); // ascending by z_index + std::reverse(gui_slots.begin(), gui_slots.end()); // descending: post-shading first, then pre-shading + gui_slots.insert(gui_slots.begin() + P, nullptr); // divider at slot P -> size becomes N+1 + + std::swap(gui_slots[gui_row], gui_slots[other]); + + int new_P = 0; + for (int i = 0; i < static_cast(gui_slots.size()); ++i) + if (!gui_slots[i]) { + new_P = i; + break; + } + + for (int i = 0; i < static_cast(gui_slots.size()); ++i) { + if (i == new_P) + continue; + if (i < new_P) + gui_slots[i]->z_index = new_P - i; + else + gui_slots[i]->z_index = -(i - new_P); + } + + m_overlay_renderer->sort_overlays(); // re-sort m_overlays by the updated z_indices + + rebuild_renderers(); + m_context->request_redraw(); +} + +void OverlaysPanel::add_overlay_of_type(int type) +{ + std::shared_ptr new_overlay; + if (type == ADD_HEIGHT_LINES) + new_overlay = std::make_shared(); + else if (type == ADD_TEXTURE_OVERLAY) + new_overlay = std::make_shared(); + else if (type == ADD_SCREEN_SPACE_SNOW) + new_overlay = std::make_shared(); + else + new_overlay = std::make_shared(); + // add_overlay() auto-assigns the topmost z_index + m_overlay_renderer->add_overlay(new_overlay); + rebuild_renderers(); + m_selected_engine_idx = static_cast(m_overlay_renderer->overlays().size()) - 1; + m_context->request_redraw(); +} + +void OverlaysPanel::draw_add_overlay_popup() +{ + if (!m_add_popup_modal) + ImGui::SetNextWindowPos(m_add_popup_pos, ImGuiCond_Appearing); + + const bool open = m_add_popup_modal ? ImGui::BeginPopupModal(ADD_POPUP_ID, nullptr, ImGuiWindowFlags_AlwaysAutoResize) : ImGui::BeginPopup(ADD_POPUP_ID); + if (!open) + return; + + if (!m_add_popup_modal) + ImGui::TextDisabled("Add Overlay"); + + ImGui::SetNextItemWidth(200.0f); + ImGui::Combo("##add_type", &m_add_type_index, ADD_ITEMS, IM_ARRAYSIZE(ADD_ITEMS)); + ImGui::SameLine(); + if (ImGui::Button("Add")) { + add_overlay_of_type(m_add_type_index); + ImGui::CloseCurrentPopup(); + } + if (m_add_popup_modal) { + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); +} + +void OverlaysPanel::draw_panel() +{ + if (!m_overlay_renderer) + return; + + // Overlays can be added/removed outside the panel (e.g. by a compute OverlayNode), so we check if the list converges: + if (m_renderers.size() != m_overlay_renderer->overlays().size()) + rebuild_renderers(); + + if (!ImGui::CollapsingHeader(ICON_FA_LAYER_GROUP " Overlays", ImGuiTreeNodeFlags_DefaultOpen)) + return; + + if (ImGui::Button(ICON_FA_PLUS " Add Overlay [Shift + O]", ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) { + m_add_popup_modal = true; + m_open_chooser_request = true; + } + + ImGui::Separator(); + + // OVERLAY LIST ==== + const auto& overlays = m_overlay_renderer->overlays(); + const int N = static_cast(overlays.size()); + + const float row_h = ImGui::GetTextLineHeightWithSpacing() + 6.0f; + + if (N == 0) { + ImGui::Selectable("No overlay configured", false, ImGuiSelectableFlags_Disabled, ImVec2(0.0, row_h)); + return; + } + + int P = 0; // count of post-shading overlays (z_index > 0) + for (const auto& o : overlays) + if (o->z_index > 0) + ++P; + + const float btn_w = row_h; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float btns_w = btn_w * 3.0f + spacing * 2.0f; + + // Pending actions, applied after the loop (mutating the list mid-iteration is unsafe). + int move_row = -1, move_dir = 0; + int delete_engine_idx = -1; + + // There are N+1 GUI rows total (N overlays + the divider) + for (int gui_row = 0; gui_row <= N; ++gui_row) { + + // SHADING DIVIDER + if (gui_row == P) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.78f, 0.78f, 0.82f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f)); + ImGui::Selectable( + "---------- " ICON_FA_SUN " SHADING ----------", false, ImGuiSelectableFlags_Disabled, ImVec2(ImGui::GetContentRegionAvail().x, row_h)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + continue; + } + + // OVERLAY ENTRY + { + const int d = (gui_row < P) ? gui_row : gui_row - 1; + const int engine_idx = display_to_engine(d, N); + ImGui::PushID(engine_idx); + + const bool selected = (m_selected_engine_idx == engine_idx); + const std::string name = (engine_idx < static_cast(m_renderers.size())) ? m_renderers[engine_idx]->effective_name() : "Overlay"; + + if (selected) + m_selected_row_screen_y = ImGui::GetCursorScreenPos().y; + + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.0f, 0.5f)); + if (ImGui::Selectable(name.c_str(), selected, ImGuiSelectableFlags_None, ImVec2(ImGui::GetContentRegionAvail().x - btns_w - spacing, row_h))) + m_selected_engine_idx = selected ? -1 : engine_idx; + ImGui::PopStyleVar(); + ImGui::SameLine(); + + const bool can_up = (gui_row > 0); + const bool can_down = (gui_row < N); + + // MOVE UP BUTTON + ImGui::BeginDisabled(!can_up); + if (ImGui::Button(ICON_FA_ARROW_UP, ImVec2(btn_w, row_h))) { + move_row = gui_row; + move_dir = -1; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + + // MOVE DOWN BUTTON + ImGui::BeginDisabled(!can_down); + if (ImGui::Button(ICON_FA_ARROW_DOWN, ImVec2(btn_w, row_h))) { + move_row = gui_row; + move_dir = +1; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + + // DELETE BUTTON + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + if (ImGui::Button(ICON_FA_TRASH, ImVec2(btn_w, row_h))) + delete_engine_idx = engine_idx; + ImGui::PopStyleColor(2); + + ImGui::PopID(); + } + } + + // Apply the action defered here, such that we dont mutate the list while rendering + if (move_dir != 0) { + do_move(move_row, move_dir, P, N); + } else if (delete_engine_idx >= 0) { + if (m_selected_engine_idx == delete_engine_idx) + m_selected_engine_idx = -1; + m_overlay_renderer->remove_overlay(static_cast(delete_engine_idx)); + rebuild_renderers(); + m_context->request_redraw(); + } +} + +void OverlaysPanel::draw() +{ + if (!ImGui::GetIO().WantTextInput && ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_O, false)) { + m_add_popup_modal = false; + m_add_popup_pos = ImGui::GetMousePos(); + m_open_chooser_request = true; + } + // NOTE: OpenPopup must share scope with BeginPopup* below, so it lives here rather than in draw_panel(). + if (m_open_chooser_request) { + ImGui::OpenPopup(ADD_POPUP_ID); + m_open_chooser_request = false; + } + draw_add_overlay_popup(); + + if (m_overlay_renderer && m_selected_engine_idx >= 0 && m_selected_engine_idx < static_cast(m_renderers.size())) { + auto& renderer = m_renderers[m_selected_engine_idx]; + + const float popup_w = 430.0f; + const float sidebar_x = ImGui::GetIO().DisplaySize.x - 430.0f; + ImGui::SetNextWindowPos(ImVec2(sidebar_x - popup_w - 8.0f, m_selected_row_screen_y), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(popup_w, 0.0f)); + + bool open = true; + const std::string title = renderer->effective_name() + "###overlay_settings"; + ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoSavedSettings); + + if (renderer->render_settings()) + m_context->request_redraw(); + + ImGui::End(); + + if (!open) + m_selected_engine_idx = -1; + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/OverlaysPanel.h b/apps/webgpu_app/overlay/OverlaysPanel.h new file mode 100644 index 000000000..2cbfca34a --- /dev/null +++ b/apps/webgpu_app/overlay/OverlaysPanel.h @@ -0,0 +1,58 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include "ui/ImGuiPanel.h" +#include +#include +#include + +namespace webgpu_engine { +class Context; +class OverlayRenderer; +} // namespace webgpu_engine + +namespace webgpu_app { + +class OverlaysPanel : public ImGuiPanel { +public: + explicit OverlaysPanel(webgpu_engine::Context* context); + void draw_panel() override; + void draw() override; + +private: + void rebuild_renderers(); + void do_move(int gui_row, int direction, int Q, int N); + + void draw_add_overlay_popup(); + void add_overlay_of_type(int type); + + webgpu_engine::Context* m_context; + webgpu_engine::OverlayRenderer* m_overlay_renderer = nullptr; + std::vector> m_renderers; + int m_selected_engine_idx = -1; // engine index of selected overlay (-1 = none) + float m_selected_row_screen_y = 0.0f; // screen Y of selected row (for settings popup) + int m_add_type_index = 0; + bool m_add_popup_modal = false; // true: button (modal); false: Shift+A (non-modal at cursor) + ImVec2 m_add_popup_pos {}; + bool m_open_chooser_request = false; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.cpp b/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.cpp new file mode 100644 index 000000000..795282e73 --- /dev/null +++ b/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.cpp @@ -0,0 +1,50 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ScreenSpaceSnowOverlayImGuiRenderer.h" + +#include + +namespace webgpu_app { + +ScreenSpaceSnowOverlayImGuiRenderer::ScreenSpaceSnowOverlayImGuiRenderer(webgpu_engine::ScreenSpaceSnowOverlay& overlay) + : OverlayImGuiRenderer(overlay) + , m_snow_overlay(&overlay) +{ +} + +bool ScreenSpaceSnowOverlayImGuiRenderer::render_custom_settings() +{ + auto& s = m_snow_overlay->settings; + bool changed = false; + + changed |= ImGui::DragFloatRange2("Angle limit", &s.angle_min, &s.angle_max, 0.1f, 0.0f, 90.0f, "Min: %.1f°", "Max: %.1f°", ImGuiSliderFlags_AlwaysClamp); + changed |= ImGui::SliderFloat("Angle blend", &s.angle_blend, 0.0f, 90.0f, "%.1f°"); + changed |= ImGui::SliderFloat("Altitude limit", &s.altitude_limit, 0.0f, 4000.0f, "%.1fm"); + changed |= ImGui::SliderFloat("Altitude variation", &s.altitude_variation, 0.0f, 1000.0f, "%.1fm"); + changed |= ImGui::SliderFloat("Altitude blend", &s.altitude_blend, 0.0f, 1000.0f, "%.1fm"); + changed |= ImGui::SliderFloat("Transparency", &s.transparency, 0.0f, 1.0f); + + if (changed) + m_snow_overlay->update_settings(); + + return changed; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.h b/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.h new file mode 100644 index 000000000..0683bd061 --- /dev/null +++ b/apps/webgpu_app/overlay/ScreenSpaceSnowOverlayImGuiRenderer.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include + +namespace webgpu_app { + +class ScreenSpaceSnowOverlayImGuiRenderer : public OverlayImGuiRenderer { +public: + explicit ScreenSpaceSnowOverlayImGuiRenderer(webgpu_engine::ScreenSpaceSnowOverlay& overlay); + + std::string display_name() const override { return "Screen-Space Snow"; } + bool render_custom_settings() override; + +private: + webgpu_engine::ScreenSpaceSnowOverlay* m_snow_overlay; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.cpp b/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.cpp new file mode 100644 index 000000000..686f7c2b8 --- /dev/null +++ b/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.cpp @@ -0,0 +1,173 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TextureOverlayImGuiRenderer.h" + +#include +#include +#include +#include + +#include +#include + +namespace webgpu_app { + +static int s_instance_counter = 0; + +TextureOverlayImGuiRenderer::TextureOverlayImGuiRenderer(webgpu_engine::TextureOverlay& overlay) + : OverlayImGuiRenderer(overlay) + , m_texture_overlay(&overlay) +{ + m_dialog_id = "textureoverlay_" + std::to_string(s_instance_counter++); +} + +void TextureOverlayImGuiRenderer::apply_image_file(const std::string& path) +{ + const auto qpath = QString::fromStdString(path); + + if (const auto image = nucleus::utils::image_loader::rgba8(qpath)) { + bool likely_encoded = false; + m_texture_overlay->settings.float_decode_range = nucleus::utils::geopng::scan_encoded_float_range(*image, likely_encoded); + if (likely_encoded) { + m_texture_overlay->settings.mode = webgpu_engine::TextureOverlay::Mode::EncodedFloat; + m_texture_overlay->settings.filter_mode = webgpu_engine::TextureOverlay::FilterMode::Nearest; + } else { + m_texture_overlay->settings.mode = webgpu_engine::TextureOverlay::Mode::AlphaBlend; + m_texture_overlay->settings.filter_mode = webgpu_engine::TextureOverlay::FilterMode::Linear; + } + } + + m_texture_overlay->load_image(qpath); + m_loaded_image_path = path; + +#ifndef __EMSCRIPTEN__ + const auto fspath = std::filesystem::path(path); + m_last_dialog_directory = fspath.parent_path().string(); + bool aabb_found = false; + for (const auto& candidate : nucleus::utils::geopng::possible_aabb_paths(fspath)) { + if (!std::filesystem::exists(candidate)) + continue; + const auto result = nucleus::utils::geopng::load_aabb_from_file(candidate); + if (result.has_value()) { + m_texture_overlay->settings.aabb = result.value(); + aabb_found = true; + break; + } + } + if (!aabb_found) + qWarning() << "No sidecar AABB file found for" << qpath; +#endif + + m_texture_overlay->update_gpu_settings(); + m_needs_redraw = true; +} + +void TextureOverlayImGuiRenderer::apply_aabb_from_file(const std::string& path) +{ + const auto result = nucleus::utils::geopng::load_aabb_from_file(path); + if (result.has_value()) { + m_texture_overlay->settings.aabb = result.value(); + m_texture_overlay->update_gpu_settings(); + } else { + qWarning() << "Failed to load AABB:" << QString::fromStdString(result.error()); + } + m_needs_redraw = true; +} + +bool TextureOverlayImGuiRenderer::render_custom_settings() +{ + auto& s = m_texture_overlay->settings; + bool changed = false; + const bool linked = m_texture_overlay->is_linked(); + + if (!linked) { + if (m_loaded_image_path.empty()) + ImGui::TextDisabled("No image loaded"); + else + ImGui::TextUnformatted(std::filesystem::path(m_loaded_image_path).filename().string().c_str()); + + const float full_w = ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x; + const bool btn = ImGui::Button("Load Files...", ImVec2(full_w, 0)); + m_picked_files.clear(); + if (ImGuiManager::FilePicker(m_dialog_id.c_str(), + "Choose PNG and/or AABB", + ".png,.txt,.*", + btn, + m_picked_files, + /*allow_multiple=*/true, + m_last_dialog_directory.empty() ? "." : m_last_dialog_directory.c_str())) { + for (const auto& path : m_picked_files) { + const auto ext = std::filesystem::path(path).extension().string(); + if (ext == ".png" || ext == ".PNG") + apply_image_file(path); + else + apply_aabb_from_file(path); + } + } + } + + ImGui::PushItemWidth(-1); + if (ImGui::DragScalarN("##aabb", ImGuiDataType_Double, &s.aabb.min.x, 4, 0.0001f, nullptr, nullptr, "%.5f")) { + m_texture_overlay->update_gpu_settings(); + changed = true; + } + ImGui::PopItemWidth(); + ImGui::TextDisabled("AABB: min_x min_y max_x max_y"); + + ImGui::Separator(); + + if (ImGui::SliderFloat("Opacity", &s.opacity, 0.0f, 1.0f)) { + m_texture_overlay->update_gpu_settings(); + changed = true; + } + + const char* mode_items[] = { "Alpha-Blend", "Encoded Float" }; + int mode_idx = (s.mode == webgpu_engine::TextureOverlay::Mode::EncodedFloat) ? 1 : 0; + if (ImGui::Combo("Mode", &mode_idx, mode_items, 2)) { + s.mode = (mode_idx == 1) ? webgpu_engine::TextureOverlay::Mode::EncodedFloat : webgpu_engine::TextureOverlay::Mode::AlphaBlend; + m_texture_overlay->update_gpu_settings(); + changed = true; + } + + if (s.mode == webgpu_engine::TextureOverlay::Mode::EncodedFloat) { + if (ImGui::DragFloat2("Float Range", &s.float_decode_range.x, 1.0f, -10000.0f, 10000.0f, "%.1f")) { + m_texture_overlay->update_gpu_settings(); + changed = true; + } + } + + // When a texture is linked the sampler settings below arent necessary and hidden + if (!linked) { + const char* filter_items[] = { "Nearest", "Linear" }; + int filter_idx = (s.filter_mode == webgpu_engine::TextureOverlay::FilterMode::Linear) ? 1 : 0; + if (ImGui::Combo("Filter Mode", &filter_idx, filter_items, 2)) + s.filter_mode = (filter_idx == 1) ? webgpu_engine::TextureOverlay::FilterMode::Linear : webgpu_engine::TextureOverlay::FilterMode::Nearest; + + ImGui::Checkbox("Use Mipmaps", &s.use_mipmaps); + ImGui::SameLine(); + ImGui::TextDisabled("(takes effect on next image load)"); + } + + changed |= m_needs_redraw; + m_needs_redraw = false; + + return changed; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.h b/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.h new file mode 100644 index 000000000..2a7b3b1c5 --- /dev/null +++ b/apps/webgpu_app/overlay/TextureOverlayImGuiRenderer.h @@ -0,0 +1,47 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include +#include +#include + +namespace webgpu_app { + +class TextureOverlayImGuiRenderer : public OverlayImGuiRenderer { +public: + explicit TextureOverlayImGuiRenderer(webgpu_engine::TextureOverlay& overlay); + + std::string display_name() const override { return "Texture Overlay"; } + bool render_custom_settings() override; + +private: + void apply_image_file(const std::string& path); + void apply_aabb_from_file(const std::string& path); + + webgpu_engine::TextureOverlay* m_texture_overlay; + std::string m_last_dialog_directory; + std::string m_loaded_image_path; + bool m_needs_redraw = false; + std::string m_dialog_id; + std::vector m_picked_files; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.cpp b/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.cpp new file mode 100644 index 000000000..b480a04d9 --- /dev/null +++ b/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.cpp @@ -0,0 +1,53 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TileDebugOverlayImGuiRenderer.h" + +#include + +namespace webgpu_app { + +TileDebugOverlayImGuiRenderer::TileDebugOverlayImGuiRenderer(webgpu_engine::TileDebugOverlay& overlay) + : OverlayImGuiRenderer(overlay) + , m_tile_debug_overlay(&overlay) +{ +} + +bool TileDebugOverlayImGuiRenderer::render_custom_settings() +{ + auto& s = m_tile_debug_overlay->settings; + bool changed = false; + + // Combo order must match TileDebugOverlay::Mode + static const char* mode_items[] = { "Normals", "Tiles", "Zoomlevel", "Vertex-ID" }; + int current = s.mode - 1; + if (ImGui::Combo("Mode", ¤t, mode_items, IM_ARRAYSIZE(mode_items))) { + s.mode = current + 1; + m_tile_debug_overlay->update_settings(); // pushes the new mode into shared_config + changed = true; + } + + if (ImGui::SliderFloat("Strength", &s.strength, 0.0f, 1.0f)) { + m_tile_debug_overlay->update_settings(); + changed = true; + } + + return changed; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.h b/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.h new file mode 100644 index 000000000..4fe7a95cc --- /dev/null +++ b/apps/webgpu_app/overlay/TileDebugOverlayImGuiRenderer.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "OverlayImGuiRenderer.h" +#include + +namespace webgpu_app { + +class TileDebugOverlayImGuiRenderer : public OverlayImGuiRenderer { +public: + explicit TileDebugOverlayImGuiRenderer(webgpu_engine::TileDebugOverlay& overlay); + + std::string display_name() const override { return "Tile Debug"; } + bool render_custom_settings() override; + +private: + webgpu_engine::TileDebugOverlay* m_tile_debug_overlay; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/resources.qrc b/apps/webgpu_app/resources.qrc new file mode 100644 index 000000000..1f4a59f2c --- /dev/null +++ b/apps/webgpu_app/resources.qrc @@ -0,0 +1,21 @@ + + + resources/icons/logo32.png + + + resources/fonts/Roboto-Regular.ttf + resources/fonts/fa5-solid-900.ttf + + + resources/gfx/sujet_shadow.png + + + resources/gpx/breite_ries.gpx + + + resources/graphs/snow.json + resources/graphs/avalanche_simulation.json + resources/graphs/avalanche_simulation_with_exports.json + resources/graphs/iterative_simulation_wip.json + + diff --git a/apps/webgpu_app/resources/fonts/Roboto-Regular.ttf b/apps/webgpu_app/resources/fonts/Roboto-Regular.ttf new file mode 100644 index 000000000..7d9a6c4c3 Binary files /dev/null and b/apps/webgpu_app/resources/fonts/Roboto-Regular.ttf differ diff --git a/apps/webgpu_app/resources/fonts/fa5-solid-900.ttf b/apps/webgpu_app/resources/fonts/fa5-solid-900.ttf new file mode 100644 index 000000000..4dd119932 Binary files /dev/null and b/apps/webgpu_app/resources/fonts/fa5-solid-900.ttf differ diff --git a/apps/webgpu_app/resources/gfx/sujet_shadow.png b/apps/webgpu_app/resources/gfx/sujet_shadow.png new file mode 100644 index 000000000..c375794b2 Binary files /dev/null and b/apps/webgpu_app/resources/gfx/sujet_shadow.png differ diff --git a/apps/webgpu_app/resources/gpx/breite_ries.gpx b/apps/webgpu_app/resources/gpx/breite_ries.gpx new file mode 100644 index 000000000..5316e5a32 --- /dev/null +++ b/apps/webgpu_app/resources/gpx/breite_ries.gpx @@ -0,0 +1,109 @@ + + + + weBIGeo Testtrack #Breite Ries + + + komoot + text/html + + + + + weBIGeo Testtrack #Breite Ries + + + 1930.678418 + + + + 1930.678418 + + + + 1914.691256 + + + + 1887.403781 + + + + 1860.116381 + + + + 1832.749614 + + + + 1781.974012 + + + + 1755.534032 + + + + 1729.070110 + + + + 1702.630259 + + + + 1675.520684 + + + + 1620.296746 + + + + 1595.299570 + + + + 1569.957680 + + + + 1544.009971 + + + + 1518.020504 + + + + 1492.072896 + + + + 1466.125339 + + + + 1421.605744 + + + + 1382.039429 + + + + 1364.249893 + + + + 1364.249893 + + + + 1364.249893 + + + + + \ No newline at end of file diff --git a/apps/webgpu_app/resources/graphs/avalanche_simulation.json b/apps/webgpu_app/resources/graphs/avalanche_simulation.json new file mode 100644 index 000000000..099d57d26 --- /dev/null +++ b/apps/webgpu_app/resources/graphs/avalanche_simulation.json @@ -0,0 +1,421 @@ +{ + "connections": [ + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "height texture" + } + }, + { + "from": { + "node": "Release Points", + "socket": "release point texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "release point texture" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "Color Mapping", + "socket": "raster dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer1_zdelta" + }, + "to": { + "node": "Color Mapping", + "socket": "storage buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer2_cellCounts" + }, + "to": { + "node": "Color Mapping", + "socket": "transparency buffer" + } + }, + { + "from": { + "node": "Stitch Tiles", + "socket": "texture" + }, + "to": { + "node": "Height Decode", + "socket": "encoded texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Height Decode", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Normals", + "socket": "bounds" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Normals", + "socket": "height texture" + } + }, + { + "from": { + "node": "Color Mapping", + "socket": "texture" + }, + "to": { + "node": "Overlay", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Overlay", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Release Points", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Request Height", + "socket": "tile ids" + } + }, + { + "from": { + "node": "GPX Input", + "socket": "region" + }, + "to": { + "node": "Select Tiles", + "socket": "region" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Stitch Tiles", + "socket": "tile ids" + } + }, + { + "from": { + "node": "Request Height", + "socket": "tile data" + }, + "to": { + "node": "Stitch Tiles", + "socket": "texture data" + } + } + ], + "first_run_notice": "WARNING: RESEARCH PREVIEW\n\nThe avalanche simulation and visualization is part of a research project and should not be used as a basis for decision-making during actual route planning.\n\nSimulations always contain uncertainty and their results may differ drastically from reality in some cases.\n\nWe exclude liability for any accidents or damages in connection to this service.", + "format": "webigeo/node-graph", + "name": "trajectories_with_export_compute_graph", + "nodes": [ + { + "enabled": true, + "name": "Avalanche Simulation", + "settings": { + "active_model": 0, + "active_runout_model": 2, + "max_perturbation": 0.4363323152065277, + "model2": { + "drag_coeff": 4000, + "friction_coeff": 0.1550000011920929, + "gravity": 9.8100004196167, + "mass": 10 + }, + "num_paths_per_release_cell": 1024, + "num_runs": 1, + "num_steps": 10000, + "output_layer": { + "layer1_zdelta_enabled": 1, + "layer2_cellCounts_enabled": 1, + "layer3_travelLength_enabled": 1, + "layer4_travelAngle_enabled": 1, + "layer5_altitudeDifference_enabled": 1 + }, + "persistence_contribution": 0.8999999761581421, + "random_seed": 1, + "resolution_multiplier": 8, + "runout_flowpy": { + "alpha": 25 + }, + "runout_perla": { + "g": 9.8100004196167, + "l": 1, + "md": 40, + "my": 0.10999999940395355 + }, + "step_length": 0.10000000149011612 + }, + "type": "ComputeAvalancheTrajectoriesNode" + }, + { + "enabled": true, + "name": "Color Mapping", + "settings": { + "color_map_bounds": [ + 0, + 40 + ], + "create_mipmaps": true, + "texture_filter_mode": "Linear", + "texture_format": "RGBA8Unorm", + "texture_max_aniostropy": 1, + "texture_mipmap_filter_mode": "Linear", + "texture_usage": [ + "CopySrc", + "TextureBinding", + "StorageBinding" + ], + "transparency_map_bounds": [ + 0, + 1 + ], + "use_bin_interpolation": false, + "use_transparency_buffer": true + }, + "type": "BufferToTextureNode" + }, + { + "enabled": true, + "name": "GPX Input", + "settings": { + "enable_caching": true, + "file_path": ":/gpx/breite_ries.gpx" + }, + "type": "GPXTrackNode" + }, + { + "enabled": true, + "name": "Height Decode", + "settings": { + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "HeightDecodeNode" + }, + { + "enabled": true, + "name": "Normals", + "settings": { + "format": "RGBA8Unorm", + "usage": [ + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeNormalsNode" + }, + { + "enabled": true, + "name": "Overlay", + "settings": { + "copy": false + }, + "type": "OverlayRenderNode" + }, + { + "enabled": true, + "name": "Release Points", + "settings": { + "max_slope_angle": 0.7853981852531433, + "min_slope_angle": 0.5235987901687622, + "sampling_interval": [ + 8, + 8 + ], + "texture_format": "RGBA8Unorm", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeReleasePointsNode" + }, + { + "enabled": true, + "name": "Request Height", + "settings": { + "file_extension": ".png", + "tile_path": "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/", + "url_pattern": "ZXY" + }, + "type": "RequestTilesNode" + }, + { + "enabled": true, + "name": "Select Tiles", + "settings": { + "zoomlevel": 15 + }, + "type": "SelectTilesNode" + }, + { + "enabled": true, + "name": "Stitch Tiles", + "settings": { + "stitch_inverted_y": true, + "texture_format": "RGBA8Uint", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ], + "tile_has_border": true, + "tile_size": [ + 65, + 65 + ] + }, + "type": "TileStitchNode" + } + ], + "ui": { + "nodes": { + "Avalanche Simulation": { + "position": [ + 950.5470581054688, + 374.4296875 + ] + }, + "Color Mapping": { + "position": [ + 1153.997314453125, + 464.4296875 + ] + }, + "GPX Input": { + "position": [ + 112.54681396484375, + 668.1796875 + ] + }, + "Height Decode": { + "position": [ + 500.24664306640625, + 320.4296875 + ] + }, + "Normals": { + "position": [ + 626.2467041015625, + 424.6171875 + ] + }, + "Overlay": { + "position": [ + 1314.24755859375, + 604.5546875 + ] + }, + "Release Points": { + "position": [ + 769.19677734375, + 526.6171875 + ] + }, + "Request Height": { + "position": [ + 242.99664306640625, + 276.6171875 + ] + }, + "Select Tiles": { + "position": [ + 237.54681396484375, + 652.6171875 + ] + }, + "Stitch Tiles": { + "position": [ + 386.99664306640625, + 281.6171875 + ] + } + } + }, + "version": 1 +} diff --git a/apps/webgpu_app/resources/graphs/avalanche_simulation_with_exports.json b/apps/webgpu_app/resources/graphs/avalanche_simulation_with_exports.json new file mode 100644 index 000000000..3dcfa158d --- /dev/null +++ b/apps/webgpu_app/resources/graphs/avalanche_simulation_with_exports.json @@ -0,0 +1,709 @@ +{ + "connections": [ + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "height texture" + } + }, + { + "from": { + "node": "Release Points", + "socket": "release point texture" + }, + "to": { + "node": "Avalanche Simulation", + "socket": "release point texture" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "Color Mapping", + "socket": "raster dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer1_zdelta" + }, + "to": { + "node": "Color Mapping", + "socket": "storage buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer2_cellCounts" + }, + "to": { + "node": "Color Mapping", + "socket": "transparency buffer" + } + }, + { + "from": { + "node": "Stitch Tiles", + "socket": "texture" + }, + "to": { + "node": "Height Decode", + "socket": "encoded texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Height Decode", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Stitch Tiles", + "socket": "texture" + }, + "to": { + "node": "Height Export", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Height Export", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer1_zdelta" + }, + "to": { + "node": "L1_ZDelta", + "socket": "buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "L1_ZDelta", + "socket": "dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer2_cellCounts" + }, + "to": { + "node": "L2_CellCounts", + "socket": "buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "L2_CellCounts", + "socket": "dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer3_travelLength" + }, + "to": { + "node": "L3_TravelLength", + "socket": "buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "L3_TravelLength", + "socket": "dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer4_travelAngle" + }, + "to": { + "node": "L4_TravelAngle", + "socket": "buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "L4_TravelAngle", + "socket": "dimensions" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "layer5_altitudeDifference" + }, + "to": { + "node": "L5_AltitudeDiff", + "socket": "buffer" + } + }, + { + "from": { + "node": "Avalanche Simulation", + "socket": "raster dimensions" + }, + "to": { + "node": "L5_AltitudeDiff", + "socket": "dimensions" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Normals", + "socket": "bounds" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Normals", + "socket": "height texture" + } + }, + { + "from": { + "node": "Color Mapping", + "socket": "texture" + }, + "to": { + "node": "Overlay", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Overlay", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Color Mapping", + "socket": "texture" + }, + "to": { + "node": "Overlay Export", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Overlay Export", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Release Points", + "socket": "release point texture" + }, + "to": { + "node": "RP Export", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "RP Export", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Release Points", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Request Height", + "socket": "tile ids" + } + }, + { + "from": { + "node": "GPX Input", + "socket": "region" + }, + "to": { + "node": "Select Tiles", + "socket": "region" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Stitch Tiles", + "socket": "tile ids" + } + }, + { + "from": { + "node": "Request Height", + "socket": "tile data" + }, + "to": { + "node": "Stitch Tiles", + "socket": "texture data" + } + } + ], + "first_run_notice": "WARNING: RESEARCH PREVIEW\n\nThe avalanche simulation and visualization is part of a research project and should not be used as a basis for decision-making during actual route planning.\n\nSimulations always contain uncertainty and their results may differ drastically from reality in some cases.\n\nWe exclude liability for any accidents or damages in connection to this service.", + "format": "webigeo/node-graph", + "name": "trajectories_with_export_compute_graph", + "nodes": [ + { + "enabled": true, + "name": "Avalanche Simulation", + "settings": { + "active_model": 0, + "active_runout_model": 2, + "max_perturbation": 0.4363323152065277, + "model2": { + "drag_coeff": 4000, + "friction_coeff": 0.1550000011920929, + "gravity": 9.8100004196167, + "mass": 10 + }, + "num_paths_per_release_cell": 1024, + "num_runs": 1, + "num_steps": 10000, + "output_layer": { + "layer1_zdelta_enabled": 1, + "layer2_cellCounts_enabled": 1, + "layer3_travelLength_enabled": 1, + "layer4_travelAngle_enabled": 1, + "layer5_altitudeDifference_enabled": 1 + }, + "persistence_contribution": 0.8999999761581421, + "random_seed": 1, + "resolution_multiplier": 8, + "runout_flowpy": { + "alpha": 25 + }, + "runout_perla": { + "g": 9.8100004196167, + "l": 1, + "md": 40, + "my": 0.10999999940395355 + }, + "step_length": 0.10000000149011612 + }, + "type": "ComputeAvalancheTrajectoriesNode" + }, + { + "enabled": true, + "name": "Color Mapping", + "settings": { + "color_map_bounds": [ + 0, + 40 + ], + "create_mipmaps": true, + "texture_filter_mode": "Linear", + "texture_format": "RGBA8Unorm", + "texture_max_aniostropy": 1, + "texture_mipmap_filter_mode": "Linear", + "texture_usage": [ + "CopySrc", + "TextureBinding", + "StorageBinding" + ], + "transparency_map_bounds": [ + 0, + 1 + ], + "use_bin_interpolation": false, + "use_transparency_buffer": true + }, + "type": "BufferToTextureNode" + }, + { + "enabled": true, + "name": "GPX Input", + "settings": { + "enable_caching": true, + "file_path": ":/gpx/breite_ries.gpx" + }, + "type": "GPXTrackNode" + }, + { + "enabled": true, + "name": "Height Decode", + "settings": { + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "HeightDecodeNode" + }, + { + "enabled": true, + "name": "Height Export", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "L1_ZDelta", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "L2_CellCounts", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "L3_TravelLength", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "L4_TravelAngle", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "L5_AltitudeDiff", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "Normals", + "settings": { + "format": "RGBA8Unorm", + "usage": [ + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeNormalsNode" + }, + { + "enabled": true, + "name": "Overlay", + "settings": { + "copy": false + }, + "type": "OverlayRenderNode" + }, + { + "enabled": true, + "name": "Overlay Export", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "RP Export", + "settings": { + "aabb_output_file": "export/{run_datetime}_{run_id}/exp_aabb.txt", + "buffer_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png", + "texture_output_file": "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png" + }, + "type": "ExportNode" + }, + { + "enabled": true, + "name": "Release Points", + "settings": { + "max_slope_angle": 0.7853981852531433, + "min_slope_angle": 0.5235987901687622, + "sampling_interval": [ + 8, + 8 + ], + "texture_format": "RGBA8Unorm", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeReleasePointsNode" + }, + { + "enabled": true, + "name": "Request Height", + "settings": { + "file_extension": ".png", + "tile_path": "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/", + "url_pattern": "ZXY" + }, + "type": "RequestTilesNode" + }, + { + "enabled": true, + "name": "Select Tiles", + "settings": { + "zoomlevel": 15 + }, + "type": "SelectTilesNode" + }, + { + "enabled": true, + "name": "Stitch Tiles", + "settings": { + "stitch_inverted_y": true, + "texture_format": "RGBA8Uint", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ], + "tile_has_border": true, + "tile_size": [ + 65, + 65 + ] + }, + "type": "TileStitchNode" + } + ], + "ui": { + "nodes": { + "Avalanche Simulation": { + "position": [ + 911.75, + 322.25 + ] + }, + "Color Mapping": { + "position": [ + 1165.3001708984375, + 86.5 + ] + }, + "GPX Input": { + "position": [ + 21.1497802734375, + 485.5 + ] + }, + "Height Decode": { + "position": [ + 487.0496826171875, + 394.25 + ] + }, + "Height Export": { + "position": [ + 487.0496826171875, + 523.5 + ] + }, + "L1_ZDelta": { + "position": [ + 1165.3001708984375, + 382 + ] + }, + "L2_CellCounts": { + "position": [ + 1165.3001708984375, + 234.25 + ] + }, + "L3_TravelLength": { + "position": [ + 1165.3001708984375, + 529.75 + ] + }, + "L4_TravelAngle": { + "position": [ + 1165.3001708984375, + 677 + ] + }, + "L5_AltitudeDiff": { + "position": [ + 1165.3001708984375, + 824.75 + ] + }, + "Normals": { + "position": [ + 624.0496826171875, + 467.9375 + ] + }, + "Overlay": { + "position": [ + 1320.8502197265625, + 547.5 + ] + }, + "Overlay Export": { + "position": [ + 1320.8502197265625, + 400.25 + ] + }, + "RP Export": { + "position": [ + 911.75, + 595.5 + ] + }, + "Release Points": { + "position": [ + 752.19970703125, + 476.9375 + ] + }, + "Request Height": { + "position": [ + 239.5997314453125, + 476.9375 + ] + }, + "Select Tiles": { + "position": [ + 125.1497802734375, + 467.9375 + ] + }, + "Stitch Tiles": { + "position": [ + 372.5997314453125, + 467.9375 + ] + } + } + }, + "version": 1 +} diff --git a/apps/webgpu_app/resources/graphs/iterative_simulation_wip.json b/apps/webgpu_app/resources/graphs/iterative_simulation_wip.json new file mode 100644 index 000000000..445979fea --- /dev/null +++ b/apps/webgpu_app/resources/graphs/iterative_simulation_wip.json @@ -0,0 +1,306 @@ +{ + "connections": [ + { + "from": { + "node": "Stitch Tiles", + "socket": "texture" + }, + "to": { + "node": "Height Decode", + "socket": "encoded texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Height Decode", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Normals", + "socket": "bounds" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Normals", + "socket": "height texture" + } + }, + { + "from": { + "node": "flowpy", + "socket": "texture" + }, + "to": { + "node": "Overlay", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Overlay", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Release Points", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Request Height", + "socket": "tile ids" + } + }, + { + "from": { + "node": "GPX Input", + "socket": "region" + }, + "to": { + "node": "Select Tiles", + "socket": "region" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Stitch Tiles", + "socket": "tile ids" + } + }, + { + "from": { + "node": "Request Height", + "socket": "tile data" + }, + "to": { + "node": "Stitch Tiles", + "socket": "texture data" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "flowpy", + "socket": "height texture" + } + }, + { + "from": { + "node": "Release Points", + "socket": "release point texture" + }, + "to": { + "node": "flowpy", + "socket": "release point texture" + } + } + ], + "format": "webigeo/node-graph", + "name": "iterative_simulation_compute_graph", + "nodes": [ + { + "enabled": true, + "name": "GPX Input", + "settings": { + "enable_caching": true, + "file_path": ":/gpx/breite_ries.gpx" + }, + "type": "GPXTrackNode" + }, + { + "enabled": true, + "name": "Height Decode", + "settings": { + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "HeightDecodeNode" + }, + { + "enabled": true, + "name": "Normals", + "settings": { + "format": "RGBA8Unorm", + "usage": [ + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeNormalsNode" + }, + { + "enabled": true, + "name": "Overlay", + "settings": { + "copy": false + }, + "type": "OverlayRenderNode" + }, + { + "enabled": true, + "name": "Release Points", + "settings": { + "max_slope_angle": 0.7853981852531433, + "min_slope_angle": 0.5235987901687622, + "sampling_interval": [ + 8, + 8 + ], + "texture_format": "RGBA8Unorm", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeReleasePointsNode" + }, + { + "enabled": true, + "name": "Request Height", + "settings": { + "file_extension": ".png", + "tile_path": "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/", + "url_pattern": "ZXY" + }, + "type": "RequestTilesNode" + }, + { + "enabled": true, + "name": "Select Tiles", + "settings": { + "zoomlevel": 15 + }, + "type": "SelectTilesNode" + }, + { + "enabled": true, + "name": "Stitch Tiles", + "settings": { + "stitch_inverted_y": true, + "texture_format": "RGBA8Uint", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ], + "tile_has_border": true, + "tile_size": [ + 65, + 65 + ] + }, + "type": "TileStitchNode" + }, + { + "enabled": true, + "name": "flowpy", + "settings": { + "max_num_iterations": 16 + }, + "type": "IterativeSimulationNode" + } + ], + "ui": { + "nodes": { + "GPX Input": { + "position": [ + 150.7249755859375, + 538.109375 + ] + }, + "Height Decode": { + "position": [ + 660.4249877929688, + 429.359375 + ] + }, + "Normals": { + "position": [ + 810.4249877929688, + 527.484375 + ] + }, + "Overlay": { + "position": [ + 1275.8250732421875, + 613.484375 + ] + }, + "Release Points": { + "position": [ + 941.125, + 565.484375 + ] + }, + "Request Height": { + "position": [ + 384.7249755859375, + 334.484375 + ] + }, + "Select Tiles": { + "position": [ + 247.7249755859375, + 529.484375 + ] + }, + "Stitch Tiles": { + "position": [ + 533.7249755859375, + 364.484375 + ] + }, + "flowpy": { + "position": [ + 1100.1251220703125, + 470.484375 + ] + } + } + }, + "version": 1 +} diff --git a/apps/webgpu_app/resources/graphs/snow.json b/apps/webgpu_app/resources/graphs/snow.json new file mode 100644 index 000000000..ce13e41cc --- /dev/null +++ b/apps/webgpu_app/resources/graphs/snow.json @@ -0,0 +1,292 @@ +{ + "connections": [ + { + "from": { + "node": "Stitch Tiles", + "socket": "texture" + }, + "to": { + "node": "Height Decode", + "socket": "encoded texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Height Decode", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Normals", + "socket": "bounds" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Normals", + "socket": "height texture" + } + }, + { + "from": { + "node": "Snow", + "socket": "snow texture" + }, + "to": { + "node": "Overlay", + "socket": "texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Overlay", + "socket": "region aabb" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Request Height", + "socket": "tile ids" + } + }, + { + "from": { + "node": "GPX Input", + "socket": "region" + }, + "to": { + "node": "Select Tiles", + "socket": "region" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "region aabb" + }, + "to": { + "node": "Snow", + "socket": "bounds" + } + }, + { + "from": { + "node": "Normals", + "socket": "normal texture" + }, + "to": { + "node": "Snow", + "socket": "normal texture" + } + }, + { + "from": { + "node": "Height Decode", + "socket": "decoded texture" + }, + "to": { + "node": "Snow", + "socket": "height texture" + } + }, + { + "from": { + "node": "Select Tiles", + "socket": "tile ids" + }, + "to": { + "node": "Stitch Tiles", + "socket": "tile ids" + } + }, + { + "from": { + "node": "Request Height", + "socket": "tile data" + }, + "to": { + "node": "Stitch Tiles", + "socket": "texture data" + } + } + ], + "format": "webigeo/node-graph", + "name": "snow_compute_graph", + "nodes": [ + { + "enabled": true, + "name": "GPX Input", + "settings": { + "enable_caching": true, + "file_path": ":/gpx/breite_ries.gpx" + }, + "type": "GPXTrackNode" + }, + { + "enabled": true, + "name": "Height Decode", + "settings": { + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "HeightDecodeNode" + }, + { + "enabled": true, + "name": "Normals", + "settings": { + "format": "RGBA8Unorm", + "usage": [ + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeNormalsNode" + }, + { + "enabled": true, + "name": "Overlay", + "settings": { + "copy": false + }, + "type": "OverlayRenderNode" + }, + { + "enabled": true, + "name": "Request Height", + "settings": { + "file_extension": ".png", + "tile_path": "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/", + "url_pattern": "ZXY" + }, + "type": "RequestTilesNode" + }, + { + "enabled": true, + "name": "Select Tiles", + "settings": { + "zoomlevel": 15 + }, + "type": "SelectTilesNode" + }, + { + "enabled": true, + "name": "Snow", + "settings": { + "altitude_blend": 200, + "altitude_variation": 200, + "angle_blend": 0, + "format": "RGBA8Unorm", + "max_angle": 45, + "min_altitude": 1000, + "min_angle": 0, + "usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ] + }, + "type": "ComputeSnowNode" + }, + { + "enabled": true, + "name": "Stitch Tiles", + "settings": { + "stitch_inverted_y": true, + "texture_format": "RGBA8Uint", + "texture_usage": [ + "CopySrc", + "CopyDst", + "TextureBinding", + "StorageBinding" + ], + "tile_has_border": true, + "tile_size": [ + 65, + 65 + ] + }, + "type": "TileStitchNode" + } + ], + "ui": { + "nodes": { + "GPX Input": { + "position": [ + 245.57504272460938, + 457.125 + ] + }, + "Height Decode": { + "position": [ + 747.574951171875, + 393.375 + ] + }, + "Normals": { + "position": [ + 898.574951171875, + 464.375 + ] + }, + "Overlay": { + "position": [ + 1181.3748779296875, + 622.375 + ] + }, + "Request Height": { + "position": [ + 472.6750183105469, + 325.375 + ] + }, + "Select Tiles": { + "position": [ + 356.5750427246094, + 457.375 + ] + }, + "Snow": { + "position": [ + 1038.4749755859375, + 535.375 + ] + }, + "Stitch Tiles": { + "position": [ + 614.675048828125, + 352.375 + ] + } + } + }, + "version": 1 +} diff --git a/apps/webgpu_app/resources/icons/logo32.png b/apps/webgpu_app/resources/icons/logo32.png new file mode 100644 index 000000000..4e0ab3ba5 Binary files /dev/null and b/apps/webgpu_app/resources/icons/logo32.png differ diff --git a/apps/webgpu_app/shell/favicon.ico b/apps/webgpu_app/shell/favicon.ico new file mode 100644 index 000000000..8798c1b02 Binary files /dev/null and b/apps/webgpu_app/shell/favicon.ico differ diff --git a/apps/webgpu_app/shell/touch-emulator.js b/apps/webgpu_app/shell/touch-emulator.js new file mode 100644 index 000000000..c45c8d8e9 --- /dev/null +++ b/apps/webgpu_app/shell/touch-emulator.js @@ -0,0 +1,371 @@ +(function(window, document, exportName, undefined) { + "use strict"; + + var isMultiTouch = false; + var multiTouchStartPos; + var eventTarget; + var touchElements = {}; + + // polyfills + if(!document.createTouch) { + document.createTouch = function(view, target, identifier, pageX, pageY, screenX, screenY, clientX, clientY) { + // auto set + if(clientX == undefined || clientY == undefined) { + clientX = pageX - window.pageXOffset; + clientY = pageY - window.pageYOffset; + } + + return new Touch(target, identifier, { + pageX: pageX, + pageY: pageY, + screenX: screenX, + screenY: screenY, + clientX: clientX, + clientY: clientY + }); + }; + } + + if(!document.createTouchList) { + document.createTouchList = function() { + var touchList = new TouchList(); + for (var i = 0; i < arguments.length; i++) { + touchList[i] = arguments[i]; + } + touchList.length = arguments.length; + return touchList; + }; + } + + /** + * create an touch point + * @constructor + * @param target + * @param identifier + * @param pos + * @param deltaX + * @param deltaY + * @returns {Object} touchPoint + */ + function Touch(target, identifier, pos, deltaX, deltaY) { + deltaX = deltaX || 0; + deltaY = deltaY || 0; + + this.identifier = identifier; + this.target = target; + this.clientX = pos.clientX + deltaX; + this.clientY = pos.clientY + deltaY; + this.screenX = pos.screenX + deltaX; + this.screenY = pos.screenY + deltaY; + this.pageX = pos.pageX + deltaX; + this.pageY = pos.pageY + deltaY; + } + + /** + * create empty touchlist with the methods + * @constructor + * @returns touchList + */ + function TouchList() { + var touchList = []; + + touchList.item = function(index) { + return this[index] || null; + }; + + // specified by Mozilla + touchList.identifiedTouch = function(id) { + return this[id + 1] || null; + }; + + return touchList; + } + + + /** + * Simple trick to fake touch event support + * this is enough for most libraries like Modernizr and Hammer + */ + function fakeTouchSupport() { + var objs = [window, document.documentElement]; + var props = ['ontouchstart', 'ontouchmove', 'ontouchcancel', 'ontouchend']; + + for(var o=0; o 2; // pointer events + } + + /** + * disable mouseevents on the page + * @param ev + */ + function preventMouseEvents(ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + + /** + * only trigger touches when the left mousebutton has been pressed + * @param touchType + * @returns {Function} + */ + function onMouse(touchType) { + return function(ev) { + if (TouchEmulator.ignoreTags.indexOf(ev.target.tagName) < 0) { + // prevent mouse events + preventMouseEvents(ev); + } + + if (ev.which !== 1) { + return; + } + + // The EventTarget on which the touch point started when it was first placed on the surface, + // even if the touch point has since moved outside the interactive area of that element. + // also, when the target doesnt exist anymore, we update it + if (ev.type == 'mousedown' || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)) { + if(ev.composedPath() && ev.composedPath().length > 0){ + eventTarget = ev.composedPath()[0] + }else { + eventTarget = ev.target; + } + } + + // shiftKey has been lost, so trigger a touchend + if (isMultiTouch && !ev.shiftKey) { + triggerTouch('touchend', ev); + isMultiTouch = false; + } + + triggerTouch(touchType, ev); + + // we're entering the multi-touch mode! + if (!isMultiTouch && ev.shiftKey) { + isMultiTouch = true; + multiTouchStartPos = { + pageX: ev.pageX, + pageY: ev.pageY, + clientX: ev.clientX, + clientY: ev.clientY, + screenX: ev.screenX, + screenY: ev.screenY + }; + triggerTouch('touchstart', ev); + } + + // reset + if (ev.type == 'mouseup') { + multiTouchStartPos = null; + isMultiTouch = false; + eventTarget = null; + } + } + } + + /** + * trigger a touch event + * @param eventName + * @param mouseEv + */ + function triggerTouch(eventName, mouseEv) { + var touchEvent = document.createEvent('Event'); + touchEvent.initEvent(eventName, true, true); + + touchEvent.altKey = mouseEv.altKey; + touchEvent.ctrlKey = mouseEv.ctrlKey; + touchEvent.metaKey = mouseEv.metaKey; + touchEvent.shiftKey = mouseEv.shiftKey; + + touchEvent.touches = getActiveTouches(mouseEv, eventName); + touchEvent.targetTouches = getActiveTouches(mouseEv, eventName); + touchEvent.changedTouches = getChangedTouches(mouseEv, eventName); + + eventTarget.dispatchEvent(touchEvent); + } + + /** + * create a touchList based on the mouse event + * @param mouseEv + * @returns {TouchList} + */ + function createTouchList(mouseEv) { + var touchList = new TouchList(); + + if (isMultiTouch) { + var f = TouchEmulator.multiTouchOffset; + var deltaX = multiTouchStartPos.pageX - mouseEv.pageX; + var deltaY = multiTouchStartPos.pageY - mouseEv.pageY; + + touchList.push(new Touch(eventTarget, 1, multiTouchStartPos, (deltaX*-1) - f, (deltaY*-1) + f)); + touchList.push(new Touch(eventTarget, 2, multiTouchStartPos, deltaX+f, deltaY-f)); + } else { + touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0)); + } + + return touchList; + } + + /** + * receive all active touches + * @param mouseEv + * @returns {TouchList} + */ + function getActiveTouches(mouseEv, eventName) { + // empty list + if (mouseEv.type == 'mouseup') { + return new TouchList(); + } + + var touchList = createTouchList(mouseEv); + if(isMultiTouch && mouseEv.type != 'mouseup' && eventName == 'touchend') { + touchList.splice(1, 1); + } + return touchList; + } + + /** + * receive a filtered set of touches with only the changed pointers + * @param mouseEv + * @param eventName + * @returns {TouchList} + */ + function getChangedTouches(mouseEv, eventName) { + var touchList = createTouchList(mouseEv); + + // we only want to return the added/removed item on multitouch + // which is the second pointer, so remove the first pointer from the touchList + // + // but when the mouseEv.type is mouseup, we want to send all touches because then + // no new input will be possible + if(isMultiTouch && mouseEv.type != 'mouseup' && + (eventName == 'touchstart' || eventName == 'touchend')) { + touchList.splice(0, 1); + } + + return touchList; + } + + /** + * show the touchpoints on the screen + */ + function showTouches(ev) { + var touch, i, el, styles; + + // first all visible touches + for(i = 0; i < ev.touches.length; i++) { + touch = ev.touches[i]; + el = touchElements[touch.identifier]; + if(!el) { + el = touchElements[touch.identifier] = document.createElement("div"); + document.body.appendChild(el); + } + + styles = TouchEmulator.template(touch); + for(var prop in styles) { + el.style[prop] = styles[prop]; + } + } + + // remove all ended touches + if(ev.type == 'touchend' || ev.type == 'touchcancel') { + for(i = 0; i < ev.changedTouches.length; i++) { + touch = ev.changedTouches[i]; + el = touchElements[touch.identifier]; + if(el) { + el.parentNode.removeChild(el); + delete touchElements[touch.identifier]; + } + } + } + } + + /** + * TouchEmulator initializer + */ + function TouchEmulator() { + if (hasTouchSupport()) { + //return; + } + + fakeTouchSupport(); + + window.addEventListener("mousedown", onMouse('touchstart'), true); + window.addEventListener("mousemove", onMouse('touchmove'), true); + window.addEventListener("mouseup", onMouse('touchend'), true); + + window.addEventListener("mouseenter", preventMouseEvents, true); + window.addEventListener("mouseleave", preventMouseEvents, true); + window.addEventListener("mouseout", preventMouseEvents, true); + window.addEventListener("mouseover", preventMouseEvents, true); + + // it uses itself! + window.addEventListener("touchstart", showTouches, true); + window.addEventListener("touchmove", showTouches, true); + window.addEventListener("touchend", showTouches, true); + window.addEventListener("touchcancel", showTouches, true); + } + + // start distance when entering the multitouch mode + TouchEmulator.multiTouchOffset = 75; + + // tags that shouldn't swallow mouse events + TouchEmulator.ignoreTags = ['TEXTAREA', 'INPUT', 'SELECT']; + + /** + * css template for the touch rendering + * @param touch + * @returns object + */ + TouchEmulator.template = function(touch) { + var size = 30; + var transform = 'translate('+ (touch.clientX-(size/2)) +'px, '+ (touch.clientY-(size/2)) +'px)'; + return { + position: 'fixed', + left: 0, + top: 0, + background: '#fff', + border: 'solid 1px #999', + opacity: .6, + borderRadius: '100%', + height: size + 'px', + width: size + 'px', + padding: 0, + margin: 0, + display: 'block', + overflow: 'hidden', + pointerEvents: 'none', + webkitUserSelect: 'none', + mozUserSelect: 'none', + userSelect: 'none', + webkitTransform: transform, + mozTransform: transform, + transform: transform, + zIndex: 100 + } + }; + + // export + if (typeof define == "function" && define.amd) { + define(function() { + return TouchEmulator; + }); + } else if (typeof module != "undefined" && module.exports) { + module.exports = TouchEmulator; + } else { + window[exportName] = TouchEmulator; + } +})(window, document, "TouchEmulator"); diff --git a/apps/webgpu_app/shell/webgpu_app.css b/apps/webgpu_app/shell/webgpu_app.css new file mode 100644 index 000000000..1c4531566 --- /dev/null +++ b/apps/webgpu_app/shell/webgpu_app.css @@ -0,0 +1,90 @@ + /* Make the html body cover the entire (visual) viewport with no scroll bars. */ + html, + body { + padding: 0; + margin: 0; + overflow: hidden; + height: 100%; + background-color: #0e1217; + color: #ffffff; + font: 12pt Trebuchet MS; + line-height: 150%; + } + + #screen { + width: 100%; + height: 100%; + display: none !important; + } + + #qtstatus { + font-style: italic; + } + + #qtspinner { + overflow: visible; + position: absolute; + width: 100%; + text-align: center; + } + + #webgpucanvas { + position: absolute; + left: 0; + top: 0; + z-index: 0; + } + + #logwrapper { + position: absolute; + z-index: 1; + left: 0; + top: 70%; + width: 100%; + height: 30%; + background-color: rgba(40, 40, 40, 0.9); + font-size: 12px; + border: rgba(1, 1, 1, 0.01) 1em solid; + box-sizing: border-box; + display: none; + } + + #log { + color: white; + overflow: auto; + overflow-y: scroll; + width: 100%; + height: 100%; + font-family: 'Consolas', 'Courier New', Courier, monospace; + line-height: 1.1em; + } + + #logwrapper.noselect { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + #logbuttonwrapper { + position: absolute; + z-index: 2; + right: 2em; + bottom: 0; + padding: 0.5em; + } + + .button { + padding: 0.5em 2em 0.5em 2em; + margin: 0.5em; + background-color: rgba(40, 40, 40, 0.7); + cursor: pointer; + color: white; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 0.5em; + } + + .button:hover { + background-color: rgba(15, 55, 78, 0.9); + } \ No newline at end of file diff --git a/apps/webgpu_app/shell/webgpu_app.html b/apps/webgpu_app/shell/webgpu_app.html new file mode 100644 index 000000000..0083fea4a --- /dev/null +++ b/apps/webgpu_app/shell/webgpu_app.html @@ -0,0 +1,108 @@ + + + + + + + + + + + weBIGeo - Geospatial Visualization Tool + + + + + +
+
+ weBIGeo
+ + +
+
+
+
+
+
Touch-Emulator
+
Hide Log [^]
+
Clear Log
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/apps/webgpu_app/shell/webgpu_hacks.js b/apps/webgpu_app/shell/webgpu_hacks.js new file mode 100644 index 000000000..b4dfdcb63 --- /dev/null +++ b/apps/webgpu_app/shell/webgpu_hacks.js @@ -0,0 +1,159 @@ +const JS_MAX_TOUCHES = 3; + +class WeBIGeoHacks { + + constructor(webgpuCanvas, logWrapper, logElement) { + this.logWrapper = logWrapper; + this.logElement = logElement; + this.webgpuCanvas = webgpuCanvas; + + logWrapper.querySelector('#butclearlog').onclick = () => this.clearLog(); + logWrapper.querySelector('#buthidelog').onclick = () => this.hideLog(); + logWrapper.querySelector('#buttouchemulator').onclick = () => { + // import touch-emulator.js + const script = document.createElement('script'); + script.src = 'touch-emulator.js'; + document.head.appendChild(script); + script.onload = () => { TouchEmulator(); } + logWrapper.querySelector('#buttouchemulator').style.display = 'none'; + }; + + window.addEventListener('keydown', (event) => this.handleKeydownEvent(event)); + + // Not implemented with SDL yet. Also maybe there is a better way to handle this. (create SDL events?) + // webgpuCanvas.addEventListener('mousedown', (event) => this.handleMousedownEvent(event)); + + // Prevent right-click context menu + webgpuCanvas.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); }); + this.hideLog(); + } + + async handleKeydownEvent(event) { + // Toggle display of the log by pressing F10 + if (event.key === 'Dead') this.toggleLog(); + } + + async setEminstance(eminstance) { + this.eminstance = eminstance; + this.debugBuild = eminstance.hasOwnProperty("getCallStack") || eminstance.hasOwnProperty("getCallstack"); + } + + async checkWebGPU() { + this.webgpuAvailable = true; + this.webgpuTimingsAvailable = false; + + if (navigator.gpu === undefined) { + this.webgpuAvailable = false; + return; + } + + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + this.webgpuAvailable = false; + return; + } + const device = await adapter.requestDevice({ requiredFeatures: ['timestamp-query'] }); + if (!device) { + this.webgpuAvailable = false; + return; + } + + if (!device.features.has('timestamp-query')) + return; + + const commandEncoder = device.createCommandEncoder(); + if (typeof commandEncoder.writeTimestamp === 'undefined') + return; + this.webgpuTimingsAvailable = true; + } + + uploadFilesWithDialog(filter, tag, multiple) { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.multiple = !!multiple; + if (typeof filter === 'string') fileInput.accept = filter; + + fileInput.addEventListener('change', async function (event) { + try { eminstance.FS.mkdir('/upload'); } catch (e) {} + for (const file of Array.from(event.target.files)) { + const data = await new Promise(resolve => { + const reader = new FileReader(); + reader.onload = e => resolve(new Uint8Array(e.target.result)); + reader.readAsArrayBuffer(file); + }); + const dstFileName = '/upload/' + file.name; + eminstance.FS.writeFile(dstFileName, data); + await eminstance.ccall('global_file_uploaded', null, + ['string', 'string'], [dstFileName, tag], { async: true }); + } + fileInput.remove(); + }); + + document.body.appendChild(fileInput); + fileInput.click(); + } + + downloadFile(path, mime) { + try { + const data = eminstance.FS.readFile(path); + const blob = new Blob([data], { type: mime || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = path.split('/').pop(); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (e) { + console.error('downloadFile failed:', e); + } + } + + log(text) { + if (this.debug) console.log(text); + if (this.logElement) { + let htmlText = this.prepareAnsiLogString(text + '\n'); + this.logElement.innerHTML += htmlText; + this.logElement.scrollTop = this.logElement.scrollHeight; // focus on bottom + } + } + + showLog() { this.logWrapper.style.display = 'block'; } + hideLog() { this.logWrapper.style.display = 'none'; } + + toggleLog() { + if (this.logWrapper.style.display === 'none') this.showLog(); + else this.hideLog(); + } + clearLog() { this.logElement.innerHTML = ''; } + + prepareAnsiLogString(text) { + const replacementMap = { + '&': '&', + '<': '<', + '>': '>', + '\n': '
', + ' ': ' ', + '\x1b[30m': '', + '\x1b[31m': '', + '\x1b[32m': '', + '\x1b[33m': '', + '\x1b[34m': '', + '\x1b[35m': '', + '\x1b[36m': '', + '\x1b[37m': '', + '\x1b[0m': '' // Reset code to close the font tag + }; + + for (const char in replacementMap) { + if (replacementMap.hasOwnProperty(char)) { + const replacement = replacementMap[char]; + text = text.split(char).join(replacement); + } + } + + return text; + } + +} diff --git a/apps/webgpu_app/shell/webigeo_logo.svg b/apps/webgpu_app/shell/webigeo_logo.svg new file mode 100644 index 000000000..8113104d2 --- /dev/null +++ b/apps/webgpu_app/shell/webigeo_logo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/webgpu_app/track/TrackPanel.cpp b/apps/webgpu_app/track/TrackPanel.cpp new file mode 100644 index 000000000..d41a04179 --- /dev/null +++ b/apps/webgpu_app/track/TrackPanel.cpp @@ -0,0 +1,86 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TrackPanel.h" + +#include +#include + +#include +#include + +#include "App.h" +#include +#include +#include +#include + +namespace webgpu_app { + +TrackPanel::TrackPanel(webgpu_engine::Context* context, App* terrain_renderer) + : m_context(context) + , m_terrain_renderer(terrain_renderer) + , m_track_renderer(context->track_renderer()) +{ +} + +void TrackPanel::ready() +{ +#if defined(QT_DEBUG) + load_track_and_focus(webgpu_engine::TrackRenderer::DEFAULT_GPX_TRACK_PATH); +#endif +} + +void TrackPanel::load_track_and_focus(const std::string& path) +{ + const radix::geometry::Aabb3d world_aabb = m_track_renderer->load_track(path); + + auto* camera_controller = m_terrain_renderer->get_camera_controller(); + camera_controller->set_model_matrix(nucleus::camera::Definition::looking_down_at_aabb(world_aabb, camera_controller->definition().viewport_size())); + + if (m_context->shared_config().m_track_render_mode == 0) + m_context->shared_config().m_track_render_mode = 1; + m_context->request_redraw(); +} + +void TrackPanel::draw_panel() +{ + if (ImGui::CollapsingHeader(ICON_FA_ROUTE " Track", ImGuiTreeNodeFlags_DefaultOpen)) { + const bool btn = ImGui::Button("Open GPX file ...", ImVec2(250, 0)); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(106 / 255.0f, 112 / 255.0f, 115 / 255.0f, 1.00f)); + if (ImGui::Button("Open Preset ...", ImVec2(100, 0))) { + load_track_and_focus(webgpu_engine::TrackRenderer::DEFAULT_GPX_TRACK_PATH); + } + ImGui::PopStyleColor(1); + + m_picked_files.clear(); + if (ImGuiManager::FilePicker( + "TrackFileDlgKey", "Choose GPX File", ".gpx,.*", btn, m_picked_files, /*allow_multiple=*/false, m_last_dialog_directory.c_str())) { + m_last_dialog_directory = std::filesystem::path(m_picked_files[0]).parent_path().string(); + load_track_and_focus(m_picked_files[0]); + } + + const char* items = "none\0without depth test\0with depth test\0semi-transparent\0"; + if (ImGui::Combo("Line render mode", (int*)&(m_context->shared_config().m_track_render_mode), items)) { + m_context->request_redraw(); + } + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/track/TrackPanel.h b/apps/webgpu_app/track/TrackPanel.h new file mode 100644 index 000000000..4e16b6502 --- /dev/null +++ b/apps/webgpu_app/track/TrackPanel.h @@ -0,0 +1,50 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ui/ImGuiPanel.h" +#include + +namespace webgpu_engine { +class Context; +class TrackRenderer; +} // namespace webgpu_engine + +namespace webgpu_app { + +class App; + +class TrackPanel : public ImGuiPanel { +public: + TrackPanel(webgpu_engine::Context* context, App* terrain_renderer); + void ready() override; + void draw_panel() override; + +private: + // Loads the track for rendering and points the camera down at its bounding box. + void load_track_and_focus(const std::string& path); + + webgpu_engine::Context* m_context; + App* m_terrain_renderer = nullptr; + webgpu_engine::TrackRenderer* m_track_renderer = nullptr; + std::string m_last_dialog_directory = "."; + std::vector m_picked_files; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/AboutPanel.cpp b/apps/webgpu_app/ui/AboutPanel.cpp new file mode 100644 index 000000000..c0c888ca3 --- /dev/null +++ b/apps/webgpu_app/ui/AboutPanel.cpp @@ -0,0 +1,162 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "AboutPanel.h" + +#include +#include + +namespace webgpu_app { + +void AboutPanel::draw() +{ + draw_copyright_box(); + draw_about_popup(); +} + +void AboutPanel::draw_copyright_box() +{ // Draw the copyright box + // Position the white box in the bottom-left corner + ImGui::SetNextWindowPos(ImVec2(0, ImGui::GetIO().DisplaySize.y - 30), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.5f); // Semi-transparent background + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 4)); // Reduce padding + // Set border color to transparent + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // Transparent border + ImGui::Begin("CopyrightBox", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + // Set up a button with no hover effect by temporarily changing colors + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.0f)); // Transparent background + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 1.0f, 0.2f)); // No hover effect + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.0f, 0.0f, 1.0f, 0.1f)); // No active effect + + if (ImGui::Button("About")) { + m_show_about_popup = true; // necessary due to ImGui-Window-Scope + } + + ImGui::PopStyleColor(3); + ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); // Restore padding +} + +void AboutPanel::draw_about_popup() +{ + if (m_show_about_popup) { + m_show_about_popup = false; + ImGui::OpenPopup("about_webigeo"); + } + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, { 0.5f, 0.5f }); + ImGui::SetNextWindowSize({ 450, 478 }); + + if (ImGui::BeginPopupModal("about_webigeo", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) { + const char* title = "About weBIGeo"; + float windowWidth = ImGui::GetWindowSize().x; + float textWidth = ImGui::CalcTextSize(title).x; + ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); + ImGui::Text("%s", title); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::TextWrapped("weBIGeo is a research project to show that processing and " + "visualizing of large datasets is possible in the browser near real-time."); + + ImGui::Spacing(); + ImGui::Text("This project is based on "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("AlpineMaps.org", "https://github.com/AlpineMapsOrg/renderer"); + + ImGui::Spacing(); + ImGui::Text("It is licensed under the "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("GPLv3", "https://www.gnu.org/licenses/gpl-3.0.html#license-text"); + + ImGui::Spacing(); + ImGui::TextLinkOpenURL("GitHub repository", "https://github.com/weBIGeo/webigeo"); + ImGui::TextLinkOpenURL("netidee project page", "https://www.netidee.at/webigeo"); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Text("Authors:"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Adam Celarek", "https://github.com/adam-ce"); + ImGui::SameLine(); + ImGui::Text("(2022-2025)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Gerald Kimmersdorfer", "https://github.com/GeraldKimmersdorfer"); + ImGui::SameLine(); + ImGui::Text("(2023-2026)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Jakob Lindner", "https://github.com/JakobLindner"); + ImGui::SameLine(); + ImGui::Text("(2023)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Patrick Komon", "https://github.com/pkomon"); + ImGui::SameLine(); + ImGui::Text("(2024-2026)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Jakob Maier", "https://github.com/Gro2mi"); + ImGui::SameLine(); + ImGui::Text("(2024)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Markus Rampp", "https://github.com/gue-ni"); + ImGui::SameLine(); + ImGui::Text("(2025)"); + ImGui::Text(" - "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Wendelin Muth", "https://github.com/Qendolin"); + ImGui::SameLine(); + ImGui::Text("(2026)"); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Text("Height and ortho data is provided by "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("basemap.at", "https://basemap.at/"); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::TextWrapped("If you have feedback or ideas for collaboration, contact us!"); + ImGui::Text("E-Mail: "); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("alpinemaps@cg.tuwien.ac.at", "mailto:alpinemaps@cg.tuwien.ac.at"); + ImGui::TextLinkOpenURL("Join us on Discord", "https://discord.gg/j4MRrrbh"); + + ImGui::Spacing(); + ImGui::Spacing(); + + ImGui::SetCursorPosX(windowWidth - 200 - ImGui::GetStyle().WindowPadding.x); + if (ImGui::Button("Close", { 200, 0 })) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/AboutPanel.h b/apps/webgpu_app/ui/AboutPanel.h new file mode 100644 index 000000000..3afb27593 --- /dev/null +++ b/apps/webgpu_app/ui/AboutPanel.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" + +namespace webgpu_app { +class AboutPanel : public ImGuiPanel { +public: + AboutPanel() = default; + + void draw() override; + +private: + bool m_show_about_popup = false; + + void draw_copyright_box(); + void draw_about_popup(); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/AppPanel.cpp b/apps/webgpu_app/ui/AppPanel.cpp new file mode 100644 index 000000000..2637484d1 --- /dev/null +++ b/apps/webgpu_app/ui/AppPanel.cpp @@ -0,0 +1,92 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2025 Markus Rampp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "AppPanel.h" + +#include +#include + +#include "App.h" +#include "RenderingContext.h" +#include + +namespace webgpu_app { + +AppPanel::AppPanel(App* terrain_renderer) + : m_terrain_renderer(terrain_renderer) +{ +} + +void AppPanel::ready() +{ + m_terrain_renderer->get_webgpu_window()->set_max_zoom_level(m_max_zoom_level); + m_terrain_renderer->get_camera_controller()->update(); +} + +void AppPanel::draw_panel() +{ + if (ImGui::CollapsingHeader(ICON_FA_COG " App Settings")) { + m_terrain_renderer->render_gui(); + static float render_quality = 0.5f; + if (ImGui::SliderFloat("Level of Detail", &render_quality, 0.1f, 2.0f)) { + const auto permissible_error = 1.0f / render_quality; + m_terrain_renderer->get_camera_controller()->set_pixel_error_threshold(permissible_error); + m_terrain_renderer->update_camera(); + qDebug() << "Setting permissible error to " << permissible_error; + } + + const uint32_t min_max_zoom_lvl = 1; + const uint32_t max_max_zoom_lvl = 18; + if (ImGui::SliderScalar("Max zoom level", ImGuiDataType_U32, &m_max_zoom_level, &min_max_zoom_lvl, &max_max_zoom_lvl, "%u")) { + m_terrain_renderer->get_webgpu_window()->set_max_zoom_level(m_max_zoom_level); + m_terrain_renderer->get_camera_controller()->update(); + } + + static int geometry_tile_source_index = 0; // 0 ... DSM, 1 ... DTM + if (ImGui::Combo("Geometry Tiles", &geometry_tile_source_index, "AlpineMaps DSM\0AlpineMaps DTM\0")) { + auto geometry_load_service = m_terrain_renderer->get_rendering_context()->geometry_tile_load_service(); + if (geometry_tile_source_index == 0) { + geometry_load_service->set_base_url("https://alpinemaps.cg.tuwien.ac.at/tiles/alpine_png/"); + } else if (geometry_tile_source_index == 1) { + geometry_load_service->set_base_url("https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/"); + } + m_terrain_renderer->get_rendering_context()->geometry_scheduler()->clear_full_cache(); + m_terrain_renderer->get_camera_controller()->update(); + } + + static int ortho_tile_source_index = 0; + if (ImGui::Combo("Ortho Tiles", &ortho_tile_source_index, "Gataki Ortho\0Basemap Ortho\0Basemap Gelände\0Basemap Oberfläche\0")) { + auto ortho_load_service = m_terrain_renderer->get_rendering_context()->ortho_tile_load_service(); + if (ortho_tile_source_index == 0) { + ortho_load_service->set_base_url("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/"); + } else if (ortho_tile_source_index == 1) { + ortho_load_service->set_base_url("https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/"); + } else if (ortho_tile_source_index == 2) { + ortho_load_service->set_base_url("https://mapsneu.wien.gv.at/basemap/bmapgelaende/grau/google3857/"); + } else if (ortho_tile_source_index == 3) { + ortho_load_service->set_base_url("https://mapsneu.wien.gv.at/basemap/bmapoberflaeche/grau/google3857/"); + } + m_terrain_renderer->get_rendering_context()->ortho_scheduler()->clear_full_cache(); + m_terrain_renderer->get_camera_controller()->update(); + } + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/AppPanel.h b/apps/webgpu_app/ui/AppPanel.h new file mode 100644 index 000000000..92aeb41a7 --- /dev/null +++ b/apps/webgpu_app/ui/AppPanel.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" +#include + +namespace webgpu_app { + +class App; + +class AppPanel : public ImGuiPanel { +public: + explicit AppPanel(App* terrain_renderer); + + void ready() override; + void draw_panel() override; + +private: + App* m_terrain_renderer; + uint32_t m_max_zoom_level = 18; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/CameraPanel.cpp b/apps/webgpu_app/ui/CameraPanel.cpp new file mode 100644 index 000000000..658778e4e --- /dev/null +++ b/apps/webgpu_app/ui/CameraPanel.cpp @@ -0,0 +1,81 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CameraPanel.h" + +#include +#include + +#include "App.h" +#include +#include +#include + +namespace webgpu_app { + +CameraPanel::CameraPanel(App* terrain_renderer) + : m_terrain_renderer(terrain_renderer) +{ + // Lets build a vector of std::string... + const auto position_storage = nucleus::camera::PositionStorage::instance(); + const QList position_storage_list = position_storage->getPositionList(); + for (const auto& position : position_storage_list) { + m_camera_preset_names.push_back(position.toStdString()); + } +} + +void CameraPanel::draw_panel() +{ + if (ImGui::CollapsingHeader(ICON_FA_CAMERA " Camera")) { + if (ImGui::BeginCombo("Preset", m_camera_preset_names[m_selected_camera_preset].c_str())) { + for (size_t n = 0; n < m_camera_preset_names.size(); n++) { + bool is_selected = (size_t(m_selected_camera_preset) == n); + if (ImGui::Selectable(m_camera_preset_names[n].c_str(), is_selected)) { + m_selected_camera_preset = int(n); + + const auto position_storage = nucleus::camera::PositionStorage::instance(); + const auto camera_controller = m_terrain_renderer->get_camera_controller(); + auto new_definition = position_storage->get_by_index(m_selected_camera_preset); + auto old_vp_size = camera_controller->definition().viewport_size(); + new_definition.set_viewport_size(old_vp_size); + camera_controller->set_model_matrix(new_definition); + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + { + auto& camera = m_terrain_renderer->get_camera_controller()->definition(); + auto pos = camera.position(); + glm::vec3 posf = pos; + ImGui::InputFloat3("Position", glm::value_ptr(posf), "%.2f", ImGuiInputTextFlags_ReadOnly); + glm::vec3 coords = nucleus::srs::world_to_lat_long_alt(pos); + ImGui::InputFloat3("Coords", glm::value_ptr(coords), "%.6f", ImGuiInputTextFlags_ReadOnly); + float fov = camera.field_of_view(); + if (ImGui::SliderFloat("FoV", &fov, 1.0f, 179.0f)) { + m_terrain_renderer->get_camera_controller()->set_field_of_view(fov); + } + } + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/CameraPanel.h b/apps/webgpu_app/ui/CameraPanel.h new file mode 100644 index 000000000..bccf0e973 --- /dev/null +++ b/apps/webgpu_app/ui/CameraPanel.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" +#include +#include + +namespace webgpu_app { + +class App; + +class CameraPanel : public ImGuiPanel { +public: + explicit CameraPanel(App* terrain_renderer); + + void draw_panel() override; + +private: + App* m_terrain_renderer; + std::vector m_camera_preset_names; + int m_selected_camera_preset = 0; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/CompassPanel.cpp b/apps/webgpu_app/ui/CompassPanel.cpp new file mode 100644 index 000000000..0cdc51d9d --- /dev/null +++ b/apps/webgpu_app/ui/CompassPanel.cpp @@ -0,0 +1,99 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CompassPanel.h" + +#include "ImGuiManager.h" +#include +#include + +#include "App.h" +#include +#include + +namespace webgpu_app { + +CompassPanel::CompassPanel(App* terrain_renderer) + : m_terrain_renderer(terrain_renderer) +{ +} + +void CompassPanel::draw() +{ + // === ROTATE NORTH BUTTON === + // Claim the next floating tool-button slot (bottom-left, stacking upward). + ImVec2 button_pos(10, ImGuiManager::s_tool_button_y); + ImGuiManager::s_tool_button_y -= 48 + 10; + ImGui::SetNextWindowPos(button_pos, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.5f); // Semi-transparent background + ImGui::SetNextWindowSize(ImVec2(48, 48)); // Set button size + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // No padding for better icon alignment + ImGui::Begin("RotateNorthButton", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // fully transparent + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.2f)); // black with alpha 0.2 + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.0f, 0.0f, 0.0f, 0.2f)); // same for active + + auto camController = m_terrain_renderer->get_camera_controller(); + + if (ImGui::Button("###RotateNorthButton", ImVec2(48, 48))) { + camController->rotate_north(); + } + const bool hovered = ImGui::IsItemHovered(); + + ImGui::PopStyleColor(3); + + // Drawing the arrow icon manually with rotation + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const auto rectMin = ImGui::GetItemRectMin(); + + auto cameraFrontAxis = camController->definition().z_axis(); + auto degFromNorth = glm::degrees(glm::acos(glm::dot(glm::normalize(glm::dvec3(cameraFrontAxis.x, cameraFrontAxis.y, 0)), glm::dvec3(0, -1, 0)))); + float cameraAngle = cameraFrontAxis.x > 0 ? degFromNorth : -degFromNorth; + + ImVec2 center = ImVec2(rectMin.x + 24, rectMin.y + 24); // Center of the button + float rotation_angle = cameraAngle * (glm::pi() / 180.0f); + + // Define arrow vertices relative to the center + float arrow_length = 16.0f; // Size of the arrow + ImVec2 points[3] = { + ImVec2(0.0f, -arrow_length), // Arrow tip + ImVec2(-arrow_length * 0.5f, arrow_length * 0.5f), // Left base + ImVec2(arrow_length * 0.5f, arrow_length * 0.5f), // Right base + }; + + // Rotate and translate arrow vertices to draw at the specified angle + for (int i = 0; i < 3; ++i) { + float rotated_x = cos(rotation_angle) * points[i].x - sin(rotation_angle) * points[i].y; + float rotated_y = sin(rotation_angle) * points[i].x + cos(rotation_angle) * points[i].y; + points[i] = ImVec2(center.x + rotated_x, center.y + rotated_y); + } + + // Draw the rotated arrow + draw_list->AddTriangleFilled(points[0], points[1], points[2], IM_COL32(255, 255, 255, 255)); // White color for the arrow + + ImGui::End(); + ImGui::PopStyleVar(); // Restore padding + + if (hovered) + ImGui::SetTooltip("Rotate North"); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/CompassPanel.h b/apps/webgpu_app/ui/CompassPanel.h new file mode 100644 index 000000000..71289a201 --- /dev/null +++ b/apps/webgpu_app/ui/CompassPanel.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" + +namespace webgpu_app { + +class App; + +class CompassPanel : public ImGuiPanel { +public: + explicit CompassPanel(App* terrain_renderer); + + void draw() override; + +private: + App* m_terrain_renderer; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/ImGuiPanel.h b/apps/webgpu_app/ui/ImGuiPanel.h new file mode 100644 index 000000000..540eeb88a --- /dev/null +++ b/apps/webgpu_app/ui/ImGuiPanel.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2026 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +namespace webgpu_app { + +class ImGuiPanel : public QObject { + Q_OBJECT +public: + virtual ~ImGuiPanel() = default; + + // Called once after initialization + virtual void ready() { } + + // Called outside the main sidebar window + virtual void draw() { } + + // Called inside the main sidebar ImGui::Begin/End block + virtual void draw_panel() { } +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/LogoPanel.cpp b/apps/webgpu_app/ui/LogoPanel.cpp new file mode 100644 index 000000000..f49312477 --- /dev/null +++ b/apps/webgpu_app/ui/LogoPanel.cpp @@ -0,0 +1,84 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "LogoPanel.h" + +#include +#include + +#include +#include + +namespace webgpu_app { + +LogoPanel::LogoPanel(WGPUDevice device) + : m_device(device) +{ + init_logo(); +} + +void LogoPanel::init_logo() +{ + nucleus::Raster logo = nucleus::utils::image_loader::rgba8(":/gfx/sujet_shadow.png").value(); + m_webigeo_logo_size = ImVec2(float(logo.width()), float(logo.height())); + + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "webigeo logo texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { uint32_t(logo.width()), uint32_t(logo.height()), uint32_t(1) }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = WGPUTextureFormat::WGPUTextureFormat_RGBA8Unorm; + texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + + m_webigeo_logo = std::make_unique(m_device, texture_desc); + auto queue = wgpuDeviceGetQueue(m_device); + m_webigeo_logo->write(queue, logo); + m_webigeo_logo_view = m_webigeo_logo->create_view(); +} + +void LogoPanel::draw() +{ // weBIGeo LOGO + ImVec2 viewportSize = ImGui::GetMainViewport()->Size; + float viewportWidth = viewportSize.x; + const float minWidth = 800.0f; + const float maxWidth = 1920.0f; + + float scaleFactor = 1.0f; + if (viewportWidth <= minWidth) { + scaleFactor = 0.5f; + } else if (viewportWidth >= maxWidth) { + scaleFactor = 1.0f; + } else { + scaleFactor = 0.5f + 0.5f * ((viewportWidth - minWidth) / (maxWidth - minWidth)); + } + ImVec2 scaledSize = ImVec2(m_webigeo_logo_size.x * scaleFactor, m_webigeo_logo_size.y * scaleFactor); + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.0f); + ImGui::Begin("weBIGeo-Logo", + nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::BringWindowToDisplayBack(ImGui::GetCurrentWindow()); + + ImGui::Image((ImTextureID)m_webigeo_logo_view->handle(), scaledSize); + ImGui::End(); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/LogoPanel.h b/apps/webgpu_app/ui/LogoPanel.h new file mode 100644 index 000000000..caec1bbee --- /dev/null +++ b/apps/webgpu_app/ui/LogoPanel.h @@ -0,0 +1,49 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" +#include +#include +#include + +namespace webgpu::raii { +class Texture; +class TextureView; +} // namespace webgpu::raii + +namespace webgpu_app { + +class LogoPanel : public ImGuiPanel { +public: + explicit LogoPanel(WGPUDevice device); + + void draw() override; + +private: + WGPUDevice m_device; + + ImVec2 m_webigeo_logo_size = {}; + std::unique_ptr m_webigeo_logo; + std::unique_ptr m_webigeo_logo_view; + + void init_logo(); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/SearchPanel.cpp b/apps/webgpu_app/ui/SearchPanel.cpp new file mode 100644 index 000000000..87c3a6d3f --- /dev/null +++ b/apps/webgpu_app/ui/SearchPanel.cpp @@ -0,0 +1,140 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "SearchPanel.h" + +#include "App.h" +#include "IconsFontAwesome5.h" +#include +#include +#include + +namespace webgpu_app { + +SearchPanel::SearchPanel(App* renderer) + : m_terrain_renderer(renderer) +{ +} + +void SearchPanel::draw() +{ + const size_t max_num_search_results_at_once = 10; + const int window_height = ImGui::GetTextLineHeightWithSpacing() + 2 * ImGui::GetStyle().WindowPadding.y + 2 * 3 + + std::min(m_search_results.size(), max_num_search_results_at_once) * ImGui::GetTextLineHeightWithSpacing() + (m_search_results.empty() ? 0 : 12) + + (m_show_no_results ? ImGui::GetTextLineHeightWithSpacing() : 0); + const int search_button_width = 30; + const int search_text_width = 340; + const int window_width = search_text_width + search_button_width + 2 * ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().ItemSpacing.x; + + ImVec2 window_pos((ImGui::GetIO().DisplaySize.x - 215) / 2 - window_width / 2, ImGui::GetTextLineHeightWithSpacing()); + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(window_width, window_height)); + + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_is_active ? 1.0f : 0.4f); + if (ImGui::Begin( + "search_panel", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove)) { + ImGui::BringWindowToDisplayBack(ImGui::GetCurrentWindow()); + m_is_active = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f)); + ImGui::PushItemWidth(search_text_width); + + // NOTE: The following is necessary to clear some form of imgui cache which leads to + // reseting of the input buffer not properly displayed + if (m_clear_input_requested) { + m_clear_input_requested = false; + ImGui::ClearActiveID(); + } + + if (m_set_focus_on_text) { + m_set_focus_on_text = false; + ImGui::SetKeyboardFocusHere(); + } + if (ImGui::InputText("##search_input", m_search_buffer.data(), m_search_buffer.size(), ImGuiInputTextFlags_EnterReturnsTrue)) { + qDebug() << "search requested" << m_search_buffer.data(); + m_show_no_results = false; + m_search_results.clear(); + emit search_requested(std::string(m_search_buffer.data())); + m_set_focus_on_text = true; + } + + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_SEARCH, ImVec2(search_button_width, 0))) { + qDebug() << "search requested" << m_search_buffer.data(); + m_show_no_results = false; + m_search_results.clear(); + emit search_requested(std::string(m_search_buffer.data())); + } + + if (m_show_no_results) { + ImGui::TextDisabled("No results found."); + } + + if (!m_search_results.empty()) { + int item_selected_idx = -1; + int item_highlighted_idx = -1; + + if (ImGui::BeginListBox("##search_results", + ImVec2(-FLT_MIN, + std::min(m_search_results.size(), max_num_search_results_at_once) * ImGui::GetTextLineHeightWithSpacing() + + 2 * ImGui::GetStyle().ItemInnerSpacing.y))) { + for (int i = 0; i < int(m_search_results.size()); i++) { + bool is_selected = (item_selected_idx == i); + ImGuiSelectableFlags flags = (item_highlighted_idx == i) ? ImGuiSelectableFlags_Highlight : 0; + if (ImGui::Selectable(m_search_results.at(i).name.c_str(), is_selected, flags)) { + item_selected_idx = i; + select_result(m_search_results.at(i)); + } + + if (ImGui::IsItemHovered()) { + item_highlighted_idx = i; + } + } + ImGui::EndListBox(); + } + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleVar(); +} + +void SearchPanel::select_result(const SearchResult& result) +{ + qDebug() << "result selected" << result.name; + emit search_result_selected(result.latitude, result.longitude); + m_search_buffer.fill('\0'); + m_search_results.clear(); + m_show_no_results = false; + m_clear_input_requested = true; + m_is_active = false; +} + +void SearchPanel::display_search_results(const std::vector& search_results) +{ + if (search_results.size() == 1) { + select_result(search_results[0]); + return; + } + m_show_no_results = search_results.empty(); + m_search_results = search_results; +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/SearchPanel.h b/apps/webgpu_app/ui/SearchPanel.h new file mode 100644 index 000000000..7a73b6472 --- /dev/null +++ b/apps/webgpu_app/ui/SearchPanel.h @@ -0,0 +1,57 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +#include "ImGuiPanel.h" +#include "util/SearchService.h" + +namespace webgpu_app { + +class App; // fwd decl + +class SearchPanel : public ImGuiPanel { + Q_OBJECT +public: + explicit SearchPanel(App* renderer); + + void draw() override; + +public slots: + void display_search_results(const std::vector& search_results); + +signals: + void search_requested(const std::string& searchText); + void search_result_selected(double latitude, double longitude); + +private: + void select_result(const SearchResult& result); + App* m_terrain_renderer; + std::vector m_search_results; + std::array m_search_buffer {}; + bool m_set_focus_on_text = true; + bool m_is_active = false; + bool m_show_no_results = false; + bool m_clear_input_requested = false; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/ShadingPanel.cpp b/apps/webgpu_app/ui/ShadingPanel.cpp new file mode 100644 index 000000000..7eef1bd0f --- /dev/null +++ b/apps/webgpu_app/ui/ShadingPanel.cpp @@ -0,0 +1,73 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ShadingPanel.h" + +#include "ImGuiManager.h" +#include +#include +#include + +#include + +namespace webgpu_app { + +ShadingPanel::ShadingPanel(webgpu_engine::Context* context) + : m_context(context) +{ +} + +void ShadingPanel::draw() +{ + auto& cfg = m_context->shared_config(); + if (ImGuiManager::FloatingToggleButton("ToggleShadingButton", ICON_FA_SUN, "Shading", &cfg.m_shading_enabled)) + m_context->request_redraw(); +} + +void ShadingPanel::draw_panel() +{ + if (!m_context->shared_config().m_shading_enabled) + return; + + if (ImGui::CollapsingHeader(ICON_FA_SUN " Shading")) { + auto& cfg = m_context->shared_config(); + + bool changed = ImGui::Combo("Normal Mode", (int*)&cfg.m_normal_mode, "None\0Flat\0Smooth\0\0"); + ImGui::Separator(); + changed |= ImGui::ColorEdit3("Light Color", (float*)&cfg.m_sun_light); + changed |= ImGui::SliderFloat("Light Intensity", &cfg.m_sun_light.w, 0.0f, 10.0f); + changed |= ImGui::DragFloat3("Light Direction", (float*)&cfg.m_sun_light_dir, 0.01f, -1.0f, 1.0f); + ImGui::Separator(); + changed |= ImGui::ColorEdit3("Ambient Color", (float*)&cfg.m_amb_light); + changed |= ImGui::SliderFloat("Ambient Intensity", &cfg.m_amb_light.w, 0.0f, 10.0f); + ImGui::Separator(); + changed |= ImGui::ColorEdit4("Material Color", (float*)&cfg.m_material_color); + ImGui::Separator(); + changed |= ImGui::SliderFloat("Ambient Strength", &cfg.m_material_light_response.x, 0.0f, 5.0f); + changed |= ImGui::SliderFloat("Diffuse Strength", &cfg.m_material_light_response.y, 0.0f, 5.0f); + changed |= ImGui::SliderFloat("Specular Strength", &cfg.m_material_light_response.z, 0.0f, 5.0f); + changed |= ImGui::SliderFloat("Shininess", &cfg.m_material_light_response.w, 1.0f, 256.0f); + + if (changed) { + cfg.m_sun_light_dir = glm::normalize(cfg.m_sun_light_dir); + m_context->request_redraw(); + } + } +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/ShadingPanel.h b/apps/webgpu_app/ui/ShadingPanel.h new file mode 100644 index 000000000..7c9675280 --- /dev/null +++ b/apps/webgpu_app/ui/ShadingPanel.h @@ -0,0 +1,39 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" + +namespace webgpu_engine { +class Context; +} + +namespace webgpu_app { + +class ShadingPanel : public ImGuiPanel { +public: + explicit ShadingPanel(webgpu_engine::Context* context); + void draw() override; + void draw_panel() override; + +private: + webgpu_engine::Context* m_context; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/TimingPanel.cpp b/apps/webgpu_app/ui/TimingPanel.cpp new file mode 100644 index 000000000..ce7d1d72a --- /dev/null +++ b/apps/webgpu_app/ui/TimingPanel.cpp @@ -0,0 +1,102 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TimingPanel.h" + +#include +#include + +#include "App.h" + +namespace webgpu_app { + +TimingPanel::TimingPanel(App* terrain_renderer) + : m_terrain_renderer(terrain_renderer) +{ +} + +void TimingPanel::draw_panel() +{ + if (ImGui::CollapsingHeader(ICON_FA_STOPWATCH " Timing")) { + + const webgpu::timing::GuiTimerWrapper* selected_timer = nullptr; + if (!m_selected_timer.empty()) { + uint32_t first_selected_timer_id = *m_selected_timer.begin(); + selected_timer = m_terrain_renderer->get_timer_manager()->get_timer_by_id(first_selected_timer_id); + } + if (selected_timer) { + const auto* tmr = selected_timer->timer.get(); + if (tmr->get_sample_count() > 2) { + ImVec4 timer_color = *(ImVec4*)(void*)&selected_timer->color; + ImGui::PushStyleColor(ImGuiCol_PlotLines, timer_color); + ImGui::PlotLines("##SelTimerGraph", &tmr->get_results()[0], (int)tmr->get_sample_count(), 0, nullptr, 0.0f, tmr->get_max(), ImVec2(380, 80)); + ImGui::PopStyleColor(); + } + } + + auto group_list = m_terrain_renderer->get_timer_manager()->get_groups(); + for (const auto& group : group_list) { + bool showGroup = true; + if (group.name != "") { + ImGui::Indent(); + showGroup = ImGui::CollapsingHeader(group.name.c_str(), ImGuiTreeNodeFlags_DefaultOpen); + } + if (showGroup) { + for (const auto& tmr : group.timers) { + const uint32_t tmr_id = tmr.timer->get_id(); + ImVec4 color(0.8f, 0.8f, 0.8f, 1.0f); + if (is_timer_selected(tmr_id)) { + color = *(ImVec4*)(void*)&tmr.color; + } + + if (ImGui::ColorButton( + ("##t" + std::to_string(tmr_id)).c_str(), color, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, ImVec2(10, 10))) { + toggle_timer(tmr_id); + } + ImGui::SameLine(); + ImGui::Text("%s: %s ±%s [%zu]", + tmr.name.c_str(), + webgpu::timing::format_time(tmr.timer->get_average()).c_str(), + webgpu::timing::format_time(tmr.timer->get_standard_deviation()).c_str(), + tmr.timer->get_sample_count()); + } + } + if (group.name != "") + ImGui::Unindent(); + } + if (ImGui::Button("Reset All Timers")) { + for (const auto& group : group_list) + for (const auto& tmr : group.timers) + tmr.timer->clear_results(); + } + } +} + +void TimingPanel::toggle_timer(uint32_t timer_id) +{ + if (is_timer_selected(timer_id)) { + m_selected_timer.erase(timer_id); + } else { + m_selected_timer.clear(); // Remove if multiple selection possible + m_selected_timer.insert(timer_id); + } +} + +bool TimingPanel::is_timer_selected(uint32_t timer_id) const { return m_selected_timer.find(timer_id) != m_selected_timer.end(); } + +} // namespace webgpu_app diff --git a/apps/webgpu_app/ui/TimingPanel.h b/apps/webgpu_app/ui/TimingPanel.h new file mode 100644 index 000000000..c9d2ef018 --- /dev/null +++ b/apps/webgpu_app/ui/TimingPanel.h @@ -0,0 +1,42 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiPanel.h" +#include + +namespace webgpu_app { + +class App; + +class TimingPanel : public ImGuiPanel { +public: + explicit TimingPanel(App* terrain_renderer); + + void draw_panel() override; + +private: + App* m_terrain_renderer; + std::set m_selected_timer = {}; + + void toggle_timer(uint32_t timer_id); + bool is_timer_selected(uint32_t timer_id) const; +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/util/InputMapper.cpp b/apps/webgpu_app/util/InputMapper.cpp new file mode 100644 index 000000000..df88ab83a --- /dev/null +++ b/apps/webgpu_app/util/InputMapper.cpp @@ -0,0 +1,283 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "InputMapper.h" +#include "nucleus/camera/Controller.h" +#include + +namespace webgpu_app { + +InputMapper::InputMapper(QObject* parent, nucleus::camera::Controller* camera_controller, ImGuiManager* gui_manager, ViewportSizeCallback vp_size_callback) + : QObject(parent) + , m_gui_manager(gui_manager) + , m_viewport_size_callback(vp_size_callback) +{ + m_keymap[SDLK_a] = Qt::Key_A; + m_keymap[SDLK_b] = Qt::Key_B; + m_keymap[SDLK_c] = Qt::Key_C; + m_keymap[SDLK_d] = Qt::Key_D; + m_keymap[SDLK_e] = Qt::Key_E; + m_keymap[SDLK_f] = Qt::Key_F; + m_keymap[SDLK_g] = Qt::Key_G; + m_keymap[SDLK_h] = Qt::Key_H; + m_keymap[SDLK_i] = Qt::Key_I; + m_keymap[SDLK_j] = Qt::Key_J; + m_keymap[SDLK_k] = Qt::Key_K; + m_keymap[SDLK_l] = Qt::Key_L; + m_keymap[SDLK_m] = Qt::Key_M; + m_keymap[SDLK_n] = Qt::Key_N; + m_keymap[SDLK_o] = Qt::Key_O; + m_keymap[SDLK_p] = Qt::Key_P; + m_keymap[SDLK_q] = Qt::Key_Q; + m_keymap[SDLK_r] = Qt::Key_R; + m_keymap[SDLK_s] = Qt::Key_S; + m_keymap[SDLK_t] = Qt::Key_T; + m_keymap[SDLK_u] = Qt::Key_U; + m_keymap[SDLK_v] = Qt::Key_V; + m_keymap[SDLK_w] = Qt::Key_W; + m_keymap[SDLK_x] = Qt::Key_X; + m_keymap[SDLK_y] = Qt::Key_Y; + m_keymap[SDLK_z] = Qt::Key_Z; + + m_keymap[SDLK_0] = Qt::Key_0; + m_keymap[SDLK_1] = Qt::Key_1; + m_keymap[SDLK_2] = Qt::Key_2; + m_keymap[SDLK_3] = Qt::Key_3; + m_keymap[SDLK_4] = Qt::Key_4; + m_keymap[SDLK_5] = Qt::Key_5; + m_keymap[SDLK_6] = Qt::Key_6; + m_keymap[SDLK_7] = Qt::Key_7; + m_keymap[SDLK_8] = Qt::Key_8; + m_keymap[SDLK_9] = Qt::Key_9; + + m_keymap[SDLK_RETURN] = Qt::Key_Return; + m_keymap[SDLK_ESCAPE] = Qt::Key_Escape; + m_keymap[SDLK_BACKSPACE] = Qt::Key_Backspace; + m_keymap[SDLK_TAB] = Qt::Key_Tab; + m_keymap[SDLK_SPACE] = Qt::Key_Space; + + m_keymap[SDLK_LEFT] = Qt::Key_Left; + m_keymap[SDLK_RIGHT] = Qt::Key_Right; + m_keymap[SDLK_UP] = Qt::Key_Up; + m_keymap[SDLK_DOWN] = Qt::Key_Down; + + m_keymap[SDLK_LCTRL] = Qt::Key_Control; + m_keymap[SDLK_RCTRL] = Qt::Key_Control; + m_keymap[SDLK_LSHIFT] = Qt::Key_Shift; + m_keymap[SDLK_RSHIFT] = Qt::Key_Shift; + m_keymap[SDLK_LALT] = Qt::Key_Alt; + m_keymap[SDLK_RALT] = Qt::Key_Alt; + + m_keymap[SDLK_F1] = Qt::Key_F1; + m_keymap[SDLK_F2] = Qt::Key_F2; + m_keymap[SDLK_F3] = Qt::Key_F3; + m_keymap[SDLK_F4] = Qt::Key_F4; + m_keymap[SDLK_F5] = Qt::Key_F5; + m_keymap[SDLK_F6] = Qt::Key_F6; + m_keymap[SDLK_F7] = Qt::Key_F7; + m_keymap[SDLK_F8] = Qt::Key_F8; + m_keymap[SDLK_F9] = Qt::Key_F9; + m_keymap[SDLK_F10] = Qt::Key_F10; + m_keymap[SDLK_F11] = Qt::Key_F11; + m_keymap[SDLK_F12] = Qt::Key_F12; + + // Initialize buttonmap + m_buttonmap.fill(Qt::NoButton); + m_buttonmap[SDL_BUTTON_LEFT] = Qt::LeftButton; + m_buttonmap[SDL_BUTTON_RIGHT] = Qt::RightButton; + m_buttonmap[SDL_BUTTON_MIDDLE] = Qt::MiddleButton; + m_buttonmap[SDL_BUTTON_X1] = Qt::XButton1; + m_buttonmap[SDL_BUTTON_X2] = Qt::XButton2; + + if (camera_controller) { + connect(this, &InputMapper::key_pressed, camera_controller, &nucleus::camera::Controller::key_press); + connect(this, &InputMapper::key_released, camera_controller, &nucleus::camera::Controller::key_release); + connect(this, &InputMapper::mouse_moved, camera_controller, &nucleus::camera::Controller::mouse_move); + connect(this, &InputMapper::mouse_pressed, camera_controller, &nucleus::camera::Controller::mouse_press); + connect(this, &InputMapper::wheel_turned, camera_controller, &nucleus::camera::Controller::wheel_turn); + connect(this, &InputMapper::touch, camera_controller, &nucleus::camera::Controller::touch); + } +} + +void InputMapper::on_sdl_event(const SDL_Event& event) +{ + // Check if it's a keyboard event + if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { + handle_key_event(event); + } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { + handle_mouse_button_event(event); + } else if (event.type == SDL_MOUSEMOTION) { + handle_mouse_motion_event(event); + } else if (event.type == SDL_MOUSEWHEEL) { + handle_mouse_wheel_event(event); + } else if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERUP || event.type == SDL_FINGERMOTION) { + handle_touch_event(event); + } +} + +void InputMapper::handle_key_event(const SDL_Event& event) +{ + if (m_gui_manager && m_gui_manager->want_capture_keyboard()) + return; + + auto it = m_keymap.find(event.key.keysym.sym); + if (it == m_keymap.end()) { + qWarning() << "Key not mapped " << event.key.keysym.sym; + return; + } + + Qt::Key qtKey = it->second; + QKeyCombination combination(qtKey); + + if (event.type == SDL_KEYDOWN) { + emit key_pressed(combination); + } else if (event.type == SDL_KEYUP) { + emit key_released(combination); + } +} + +void InputMapper::handle_mouse_button_event(const SDL_Event& event) +{ + const int button = event.button.button; + const int action = (event.type == SDL_MOUSEBUTTONDOWN) ? SDL_PRESSED : SDL_RELEASED; + + assert(button >= 0 && (size_t)button < m_buttonmap.size()); + + if (m_gui_manager && m_gui_manager->want_capture_mouse()) + return; + + m_mouse.point.last_position = m_mouse.point.position; + m_mouse.point.position = { event.button.x, event.button.y }; + + const auto qtButton = m_buttonmap[button]; + + if (action == SDL_RELEASED) { + m_mouse.buttons &= ~qtButton; // Unset the button bit + } else if (action == SDL_PRESSED) { + m_mouse.buttons |= qtButton; // Set the button bit + } + emit mouse_pressed(m_mouse); +} + +void InputMapper::handle_mouse_motion_event(const SDL_Event& event) +{ + if (m_gui_manager && m_gui_manager->want_capture_mouse()) + return; + + m_mouse.point.last_position = m_mouse.point.position; + m_mouse.point.position = { event.motion.x, event.motion.y }; + emit mouse_moved(m_mouse); +} + +void InputMapper::handle_mouse_wheel_event(const SDL_Event& event) +{ + if (event.type != SDL_MOUSEWHEEL) + return; + if (m_gui_manager && m_gui_manager->want_capture_mouse()) + return; + + nucleus::event_parameter::Wheel wheel {}; + wheel.angle_delta = QPoint(static_cast(event.wheel.x), static_cast(event.wheel.y) * 200.0f); + + int xpos, ypos; + SDL_GetMouseState(&xpos, &ypos); + wheel.point.position = { xpos, ypos }; + + emit wheel_turned(wheel); +} + +// NOTE: ABOUT MAPPING SDL TOUCH EVENTS TO NUCLEUS TOUCH EVENTS: +// The nucleus touch events are based on the Qt touch events. In qt a list of current touch points is maintained. +// SDL touch events are executed for each finger. So we need to maintain a list of ongoing touch points. All touch points +// that have been released will be removed from the list. (Such that they will be emitted only once with the released state) +// Additional information like pressure or gestures are not relevant in the nucleus, but this mapping function could be +// extended to forward such information aswell. +void InputMapper::handle_touch_event(const SDL_Event& event) +{ + nucleus::event_parameter::Touch touchParams; + + // First step: Remove all touch points that are not longer active (state = Released) + for (auto it = m_touchmap.begin(); it != m_touchmap.end();) { + if (it->second.state == nucleus::event_parameter::TouchPointReleased) + it = m_touchmap.erase(it); + else { + // set all to stationary that have been added in the last call + if (it->second.state == nucleus::event_parameter::TouchPointPressed) + it->second.state = nucleus::event_parameter::TouchPointStationary; + ++it; + } + } + + // Calculate position of the touch event in screen space + glm::vec2 pos_screen = { event.tfinger.x, event.tfinger.y }; + pos_screen *= m_viewport_size_callback(); + + switch (event.type) { + case SDL_FINGERDOWN: { + nucleus::event_parameter::EventPoint point; + point.state = nucleus::event_parameter::TouchPointPressed; + point.position = point.press_position = point.last_position = pos_screen; + + // Add the touch point to the m_touchmap + m_touchmap[event.tfinger.fingerId] = point; + + touchParams.is_begin_event = true; + break; + } + + case SDL_FINGERUP: { + auto it = m_touchmap.find(event.tfinger.fingerId); + if (it != m_touchmap.end()) { + it->second.state = nucleus::event_parameter::TouchPointReleased; + it->second.position = it->second.press_position = pos_screen; + } + + touchParams.is_end_event = true; + + // This means the last finger has been released and we can stop the touch interaction + // if (m_touchmap.size() == 1) + break; + } + + case SDL_FINGERMOTION: { + auto it = m_touchmap.find(event.tfinger.fingerId); + if (it != m_touchmap.end()) { + it->second.state = nucleus::event_parameter::TouchPointMoved; + it->second.last_position = it->second.position; + it->second.position = it->second.press_position = pos_screen; + } + + touchParams.is_update_event = true; + break; + } + + default: + qWarning() << "Unknown touch event type" << event.type; + break; + } + + // Populate touchParams with the ongoing touch points + touchParams.points.reserve(m_touchmap.size()); + for (const auto& [key, value] : m_touchmap) { + touchParams.points.push_back(value); + } + + emit touch(touchParams); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/util/InputMapper.h b/apps/webgpu_app/util/InputMapper.h new file mode 100644 index 000000000..7a57948b7 --- /dev/null +++ b/apps/webgpu_app/util/InputMapper.h @@ -0,0 +1,67 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "ImGuiManager.h" +#include "nucleus/event_parameter.h" +#include +#include +#include +#include + +namespace nucleus::camera { +class Controller; +} + +namespace webgpu_app { + +class InputMapper : public QObject { + Q_OBJECT + +public: + using ViewportSizeCallback = std::function; + + InputMapper(QObject* parent, nucleus::camera::Controller* camera_controller, ImGuiManager* gui_manager, ViewportSizeCallback vp_size_callback); + void on_sdl_event(const SDL_Event& event); + +signals: + void key_pressed(QKeyCombination key); + void key_released(QKeyCombination key); + void mouse_moved(nucleus::event_parameter::Mouse mouse); + void mouse_pressed(nucleus::event_parameter::Mouse mouse); + void wheel_turned(nucleus::event_parameter::Wheel wheel); + void touch(nucleus::event_parameter::Touch touch); + +private: + ImGuiManager* m_gui_manager = nullptr; + ViewportSizeCallback m_viewport_size_callback; + + nucleus::event_parameter::Mouse m_mouse; + std::map m_keymap; + std::array m_buttonmap; // 5 to cover all SDL mouse buttons + std::map m_touchmap; + + void handle_key_event(const SDL_Event& event); + void handle_mouse_button_event(const SDL_Event& event); + void handle_mouse_motion_event(const SDL_Event& event); + void handle_mouse_wheel_event(const SDL_Event& event); + void handle_touch_event(const SDL_Event& event); +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/util/SearchService.cpp b/apps/webgpu_app/util/SearchService.cpp new file mode 100644 index 000000000..052ae80c4 --- /dev/null +++ b/apps/webgpu_app/util/SearchService.cpp @@ -0,0 +1,111 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "SearchService.h" +#include +#include +#include +#include +#include + +namespace webgpu_app { + +SearchService::SearchService() + : m_network_manager(std::make_unique(this)) +{ + connect(m_network_manager.get(), &QNetworkAccessManager::finished, this, &SearchService::http_reply_received); +} + +void SearchService::search(const std::string& search_term) +{ + const std::string format = "geojson"; + + auto url = QUrl("https://nominatim.openstreetmap.org/search"); + const auto url_query = QUrlQuery({ + { "q", QString::fromStdString(search_term) }, + { "limit", QString::number(m_limit) }, + { "format", QString::fromStdString(format) }, + { "countrycodes", QString::fromStdString(m_region_country_code) }, + }); + url.setQuery(url_query); + + auto request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, "webigeo/1.0"); + m_network_manager->get(request); +} + +void SearchService::http_reply_received(QNetworkReply* reply) +{ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + // TODO maybe emit some signal on error? + qWarning() << "network request returned error " << reply->error() << ", request content: " << reply->readAll(); + return; + } + + QJsonParseError parse_error {}; + + const auto responseContent = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(responseContent, &parse_error); + if (doc.isNull()) { + // TODO maybe emit some signal on error? + qWarning() << "json parsing returned error at offset " << parse_error.offset << ": " << parse_error.errorString() + << ", response content: " << responseContent; + return; + } + + // TODO refactor: move geojson parsing/validation elsewhere + + if (!doc.isObject()) { + // TODO maybe emit some signal on error? + qWarning() << "error: expected json object, got " << responseContent; + return; + } + const auto jsonObject = doc.object(); + const auto features = jsonObject.value("features"); + if (features.isUndefined() || !features.isArray()) { + qWarning() << "error: expected key \"features\" to be present and contain an array, got" << responseContent; + return; + } + + auto features_array = features.toArray(); + std::vector results; + results.reserve(features_array.size()); + for (auto it = features_array.begin(); it != features_array.end(); it++) { + // TODO validate + const auto feature = it->toObject(); + const auto properties = feature.value("properties").toObject(); + const QString display_name = properties.value("display_name").toString(); + const auto geometry = feature.value("geometry").toObject(); + if (geometry.value("type").toString() != "Point") { + continue; + } + const double longitude = geometry.value("coordinates").toArray().begin()->toDouble(); + const double latitude = (geometry.value("coordinates").toArray().begin() + 1)->toDouble(); + results.emplace_back(display_name.toStdString(), longitude, latitude); + } + + for (auto it = results.begin(); it != results.end(); it++) { + qInfo() << it->name << it->longitude << it->latitude; + } + + emit search_results_arrived(results); +} + +} // namespace webgpu_app diff --git a/apps/webgpu_app/util/SearchService.h b/apps/webgpu_app/util/SearchService.h new file mode 100644 index 000000000..5adeedc83 --- /dev/null +++ b/apps/webgpu_app/util/SearchService.h @@ -0,0 +1,58 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace webgpu_app { + +struct SearchResult { + std::string name; + + // TODO use classes that already exist in nucleus if there is any? + double longitude; + double latitude; +}; + +class SearchService : public QObject { + Q_OBJECT + +public: + SearchService(); + +public slots: + void search(const std::string& search_term); + +signals: + void search_results_arrived(const std::vector& results); + +private slots: + void http_reply_received(QNetworkReply* reply); + +private: + std::unique_ptr m_network_manager; + size_t m_limit = 15; + // server-side filtering via Nominatim countrycodes parameter + std::string m_region_country_code = "at"; // only search in Austria +}; + +} // namespace webgpu_app diff --git a/apps/webgpu_app/util/WebInterop.cpp b/apps/webgpu_app/util/WebInterop.cpp new file mode 100644 index 000000000..cd5f8bf0d --- /dev/null +++ b/apps/webgpu_app/util/WebInterop.cpp @@ -0,0 +1,64 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "WebInterop.h" +#include +#include +#include +#include + +void global_file_uploaded(const char* filename, const char* tag) { WebInterop::_file_uploaded(filename, tag); } + +void WebInterop::_file_uploaded(const char* filename, const char* tag) +{ + std::string filename_str(filename); + std::string tag_str(tag); + qDebug() << "File uploaded: " << filename_str << " with tag: " << tag_str; + emit WebInterop::instance().file_uploaded(filename_str, tag_str); +} + +void WebInterop::open_file_dialog(const std::string& filter, const std::string& tag, bool allow_multiple) +{ + EM_ASM_({ eminstance.hacks.uploadFilesWithDialog(UTF8ToString($0), UTF8ToString($1), !!$2); }, filter.c_str(), tag.c_str(), (int)allow_multiple); +} + +void WebInterop::download_file(const std::string& path, const std::string& mime) +{ + EM_ASM_({ eminstance.hacks.downloadFile(UTF8ToString($0), UTF8ToString($1)); }, path.c_str(), mime.c_str()); +} + +glm::uvec2 WebInterop::get_body_size() +{ + double w, h; + emscripten_get_element_css_size("body", &w, &h); + return glm::uvec2(w, h); +} + +static EM_BOOL _body_size_changed([[maybe_unused]] int event_type, [[maybe_unused]] const EmscriptenUiEvent* event, [[maybe_unused]] void* user_data) +{ + // NOTE: We could debounce this event, as it gets called rather often which means + // the swapchain will be recreated very often + emit WebInterop::instance().body_size_changed(WebInterop::instance().get_body_size()); + return 0; +} + +WebInterop::WebInterop() +{ + // Setup _web_display_size_changed to be called when the window is resized + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, _body_size_changed); +} diff --git a/apps/webgpu_app/util/WebInterop.h b/apps/webgpu_app/util/WebInterop.h new file mode 100644 index 000000000..9bdc189be --- /dev/null +++ b/apps/webgpu_app/util/WebInterop.h @@ -0,0 +1,72 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ +#pragma once + +#include +#include +#include +#include + +#define JS_MAX_TOUCHES 3 // also needs changes in WebInterop.cpp and the shell and global_touch_event! + +// NOTE: We switched from emscripten bindings to ccall because those can be called asynchronously. +// Otherwise we ended up with issues when functions are called from js event loop inside the WASM core. +// https://github.com/weBIGeo/webigeo/issues/25 +extern "C" { +EMSCRIPTEN_KEEPALIVE +void global_file_uploaded(const char* filename, const char* tag); +} + +// The WebInterop class acts as bridge between the C++ code and the JavaScript code. +// It maps the exposed Javascript functions to signals on the singleton which can be used in our QObjects. +class WebInterop : public QObject { + Q_OBJECT + +public: + // Deleted copy constructor and copy assignment operator + WebInterop(const WebInterop&) = delete; + WebInterop& operator=(const WebInterop&) = delete; + + // Static method to get the instance of the class + static WebInterop& instance() + { + static WebInterop _instance; + return _instance; + } + + static void _mouse_button_event(int button, int action, int mods, double xpos, double ypos); + static void _mouse_position_event(int button, double xpos, double ypos); + + static void _file_uploaded(const char* filename, const char* tag); + + void open_file_dialog(const std::string& filter, const std::string& tag, bool allow_multiple = false); + + // Reads the file at the given MEMFS path and triggers a browser download. + static void download_file(const std::string& path, const std::string& mime = "application/octet-stream"); + + glm::uvec2 get_body_size(); + +signals: + void body_size_changed(glm::uvec2 size); + + void file_uploaded(const std::string& filename, const std::string& tag); + +private: + // Private constructor + WebInterop(); +}; diff --git a/apps/webgpu_app/util/dark_mode.cpp b/apps/webgpu_app/util/dark_mode.cpp new file mode 100644 index 000000000..ece18992c --- /dev/null +++ b/apps/webgpu_app/util/dark_mode.cpp @@ -0,0 +1,136 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "dark_mode.h" + +#if defined(_WIN32) || defined(_WIN64) // Windows +#include +#include +#pragma comment(lib, "dwmapi.lib") + +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#endif + +#include + +namespace webgpu_app::util { + +void enable_darkmode_on_windows(SDL_Window* window) +{ + if (!window) + return; + +#if defined(_WIN32) || defined(_WIN64) + // Windows: Enable dark mode for the title bar + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(window, &info)) { + HWND hwnd = info.info.win.window; + if (hwnd) { + BOOL useDarkMode = TRUE; + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &useDarkMode, sizeof(useDarkMode)); + + // Force the window to refresh by changing its size slightly + RECT rect; + if (GetWindowRect(hwnd, &rect)) { + int width = rect.right - rect.left; + int height = rect.bottom - rect.top; + SetWindowPos(hwnd, nullptr, rect.left, rect.top, width + 1, height, SWP_NOZORDER | SWP_NOMOVE); + SetWindowPos(hwnd, nullptr, rect.left, rect.top, width, height, SWP_NOZORDER | SWP_NOMOVE); + } + + // Alternatively, force redraw using RedrawWindow + RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_FRAME); + } + } + +#endif +} + +void setup_darkmode_imgui_style() +{ + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4* colors = style.Colors; + + colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.7f, 0.7f, 0.7f, 0.8f); + colors[ImGuiCol_WindowBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + colors[ImGuiCol_ChildBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + colors[ImGuiCol_PopupBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + + colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + + colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); // Hover Accent + colors[ImGuiCol_FrameBgActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + + colors[ImGuiCol_TitleBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + + colors[ImGuiCol_CheckMark] = ImVec4(0 / 255.0f, 101 / 255.0f, 153 / 255.0f, 1.00f); // Checkbox tick + colors[ImGuiCol_SliderGrab] = ImVec4(0 / 255.0f, 101 / 255.0f, 153 / 255.0f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0 / 255.0f, 101 / 255.0f, 153 / 255.0f, 1.00f); + + colors[ImGuiCol_Button] = ImVec4(0 / 255.0f, 101 / 255.0f, 153 / 255.0f, 1.00f); // Button color + colors[ImGuiCol_ButtonHovered] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + + // Keep Headers Unchanged + colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + + colors[ImGuiCol_Separator] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + + colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.24f, 1.00f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + + colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_TabActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_TabUnfocused] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_TabUnfocusedActive] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + + colors[ImGuiCol_TextSelectedBg] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_DragDropTarget] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_NavHighlight] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(78 / 255.0f, 163 / 255.0f, 196 / 255.0f, 1.00f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.80f); + + style.WindowRounding = 0.0f; + style.FrameRounding = 0.0f; + style.GrabRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + style.TabRounding = 0.0f; + style.FramePadding = ImVec2(8.0f, 5.0f); +} + +} // namespace webgpu_app::util diff --git a/apps/webgpu_app/util/dark_mode.h b/apps/webgpu_app/util/dark_mode.h new file mode 100644 index 000000000..651d550e7 --- /dev/null +++ b/apps/webgpu_app/util/dark_mode.h @@ -0,0 +1,28 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +namespace webgpu_app::util { + +void enable_darkmode_on_windows(SDL_Window* window); + +void setup_darkmode_imgui_style(); +}; // namespace webgpu_app::util diff --git a/apps/webgpu_app/util/error_logging.cpp b/apps/webgpu_app/util/error_logging.cpp new file mode 100644 index 000000000..703b855a4 --- /dev/null +++ b/apps/webgpu_app/util/error_logging.cpp @@ -0,0 +1,146 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#define LOG_MESSAGE_FILTERING 1 + +#include "error_logging.h" + +#include +#include +#include + +#if LOG_MESSAGE_FILTERING +static const std::map logMessageFilters = { { QtWarningMsg, "QNetworkAccess: got HTTP status code 0" } }; +#endif + +void qt_logging_callback(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ +#if LOG_MESSAGE_FILTERING + for (const auto& filter : logMessageFilters) { + if (type == filter.first && msg.contains(filter.second)) { + return; + } + } +#endif + + QByteArray localMsg = msg.toLocal8Bit(); + // const char *function = context.function ? context.function : ""; + const char* typeStr = nullptr; + const char* colorCode = nullptr; + std::ostream* stream = nullptr; + + switch (type) { + case QtDebugMsg: + typeStr = "Debug "; + colorCode = ASCII_COLOR_CYAN; + stream = &std::cout; + break; + case QtInfoMsg: + typeStr = "Info "; + colorCode = ASCII_COLOR_BLUE; + stream = &std::cout; + break; + case QtWarningMsg: + typeStr = "Warning "; + colorCode = ASCII_COLOR_YELLOW; + stream = &std::cout; + break; + case QtCriticalMsg: + typeStr = "Critical"; + colorCode = ASCII_COLOR_RED; +#ifdef __EMSCRIPTEN__ // on emscripten we use std::cout for critical messages for the colors to work + stream = &std::cout; +#else + stream = &std::cerr; +#endif + break; + case QtFatalMsg: + typeStr = "Fatal "; + colorCode = ASCII_COLOR_RED; + stream = &std::cerr; + break; + } + + QString fileName = context.file ? QFileInfo(context.file).fileName() : ""; + +#ifdef __EMSCRIPTEN__ // Full row color for web + QString logMessage = "%1%2 | %3%4 | %5" ASCII_COLOR_RESET; +#else + QString logMessage = "%1%2 | %3%4 |" ASCII_COLOR_RESET " %5"; + if (type == QtDebugMsg) // gray message for debug messages + logMessage = "%1%2 | %3%4 | " ASCII_COLOR_GRAY "%5" ASCII_COLOR_RESET; +#endif + logMessage = logMessage.arg(colorCode) + .arg(QDateTime::currentDateTime().toString("hh:mm:ss")) + .arg(typeStr) + .arg(fileName.isEmpty() ? "" : " | " + fileName + ":" + QString::number(context.line), fileName.isEmpty() ? 0 : -28) + .arg(localMsg.constData()); + + (*stream) << logMessage.toStdString() << std::endl; + // Note: We could use Logging Categories at some point. (when we have >100 employees maybe) + // QString category = context.category ? context.category : "default"; + // (*stream) << logMessage.toStdString() << category.toStdString() << std::endl; + + if (type == QtFatalMsg) { + abort(); + } + + stream->flush(); +} + +std::map wgpu_error_map = { + { WGPUErrorType_NoError, "NoError" }, + { WGPUErrorType_Validation, "Validation" }, + { WGPUErrorType_OutOfMemory, "OutOfMemory" }, + { WGPUErrorType_Internal, "Internal" }, + { WGPUErrorType_Unknown, "Unknown" }, + { WGPUErrorType_Force32, "Force32" }, +}; + +void webgpu_device_error_callback( + [[maybe_unused]] const WGPUDevice* device, WGPUErrorType type, WGPUStringView message, [[maybe_unused]] void* userdata1, [[maybe_unused]] void* userdata2) +{ + const auto& typeStr = wgpu_error_map[type]; + + QString logMessage = ASCII_COLOR_MAGENTA "%1 | WebGPU | %2 |" ASCII_COLOR_RESET " %3"; + logMessage = logMessage.arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(typeStr, -25).arg(message.data); + + std::cout << logMessage.toStdString() << std::endl; +} + +std::map wgpu_device_lost_reason_map = { + { WGPUDeviceLostReason_Unknown, "Unknown" }, + { WGPUDeviceLostReason_Destroyed, "Destroyed" }, + { WGPUDeviceLostReason_CallbackCancelled, "CallbackCancelled" }, + { WGPUDeviceLostReason_FailedCreation, "FailedCreation" }, + { WGPUDeviceLostReason_Force32, "Force32" }, +}; + +void webgpu_device_lost_callback([[maybe_unused]] const WGPUDevice* device, + WGPUDeviceLostReason reason, + WGPUStringView message, + [[maybe_unused]] void* userdata1, + [[maybe_unused]] void* userdata2) +{ + const auto& typeStr = wgpu_device_lost_reason_map[reason]; + + QString logMessage = ASCII_COLOR_MAGENTA "%1 | WebGPU | %2 |" ASCII_COLOR_RESET " %3"; + logMessage = logMessage.arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(typeStr, -25).arg(message.data); + + std::cout << logMessage.toStdString() << std::endl; +} diff --git a/apps/webgpu_app/util/error_logging.h b/apps/webgpu_app/util/error_logging.h new file mode 100644 index 000000000..aaf6354d4 --- /dev/null +++ b/apps/webgpu_app/util/error_logging.h @@ -0,0 +1,38 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#define QT_LOGGING_TO_CONSOLE 1 + +#include +#include + +#define ASCII_COLOR_CYAN "\033[36m" +#define ASCII_COLOR_BLUE "\033[34m" +#define ASCII_COLOR_YELLOW "\033[33m" +#define ASCII_COLOR_RED "\033[31m" +#define ASCII_COLOR_GRAY "\033[38;5;245m" // Gray for file names in debug +#define ASCII_COLOR_MAGENTA "\033[35m" +#define ASCII_COLOR_RESET "\033[0m" + +void qt_logging_callback(QtMsgType type, const QMessageLogContext& context, const QString& msg); + +void webgpu_device_error_callback(const WGPUDevice* device, WGPUErrorType type, WGPUStringView message, void* userdata1, void* userdata2); + +void webgpu_device_lost_callback(const WGPUDevice* device, WGPUDeviceLostReason reason, WGPUStringView message, void* userdata1, void* userdata2); diff --git a/cmake/alp_add_git_repository_new.cmake b/cmake/alp_add_git_repository_new.cmake new file mode 100644 index 000000000..9c1df9525 --- /dev/null +++ b/cmake/alp_add_git_repository_new.cmake @@ -0,0 +1,150 @@ +############################################################################# +# Alpine Radix +# Copyright (C) 2023 Adam Celarek +# Copyright (C) 2024 Gerald Kimmersdorfer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +find_package(Git 2.22 REQUIRED) + +# CMake's FetchContent caches information about the downloads / clones in the build dir. +# Therefore it walks over the clones every time we switch the build type (release, debug, webassembly, android etc), +# which takes forever. Moreover, it messes up changes to subprojects. This function, on the other hand, checks whether +# we are on a branch and in that case only issues a warning. Use origin/main or similar, if you want to stay up-to-date +# with upstream. + + +# SHORT RANT: I wanted to edit the alp_add_git_repository function, but it was not working to change it until i figured out +# that another version of the function is pulled with radix and at some point just overwrites the definition in this repo. +# So I now added this function with 'new' at the end of the name. This is not a good solution, but it works for now. +# A better solution would be to replace this function with the one in radix (and maybe even more repos?) +function(alp_add_git_repository_new name) + set(options DO_NOT_ADD_SUBPROJECT NOT_SYSTEM EXCLUDE_FROM_ALL SHALLOW_CLONE) + set(oneValueArgs URL COMMITISH DESTINATION_PATH) + set(multiValueArgs ) + cmake_parse_arguments(PARSE_ARGV 1 PARAM "${options}" "${oneValueArgs}" "${multiValueArgs}") + + + file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}) + set(repo_dir ${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/${name}) + set(short_repo_dir ${ALP_EXTERN_DIR}/${name}) + if (DEFINED PARAM_DESTINATION_PATH AND NOT PARAM_DESTINATION_PATH STREQUAL "") + set(repo_dir ${CMAKE_SOURCE_DIR}/${PARAM_DESTINATION_PATH}) + set(short_repo_dir ${PARAM_DESTINATION_PATH}) + endif() + + set(${name}_SOURCE_DIR "${repo_dir}" PARENT_SCOPE) + + if(EXISTS "${repo_dir}/.git") + message(STATUS "Updating git repo in ${short_repo_dir}") + execute_process(COMMAND ${GIT_EXECUTABLE} fetch + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_FETCH_RESULT) + if (NOT ${GIT_FETCH_RESULT}) + message(STATUS "Fetching ${name} was successfull.") + + execute_process(COMMAND ${GIT_EXECUTABLE} branch --show-current + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_BRANCH_RESULT + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE GIT_BRANCH_OUTPUT) + if (${GIT_BRANCH_RESULT}) + message(FATAL_ERROR "${repo_dir}: git branch --show-current not successfull") + endif() + if (GIT_BRANCH_OUTPUT STREQUAL "") + execute_process(COMMAND ${GIT_EXECUTABLE} checkout --quiet ${PARAM_COMMITISH} + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_CHECKOUT_RESULT) + if (NOT ${GIT_CHECKOUT_RESULT}) + message(STATUS "In ${name}, checking out ${PARAM_COMMITISH} was successfull.") + else() + message(FATAL_ERROR "In ${name}, checking out ${PARAM_COMMITISH} was NOT successfull!") + endif() + else() + message(WARNING "${short_repo_dir} is on branch ${GIT_BRANCH_OUTPUT}, leaving it there. " + "NOT checking out ${PARAM_COMMITISH}! Use origin/main or similar if you want to stay up-to-date with upstream.") + endif() + else() + message(WARNING "Fetching ${name} was NOT successfull!") + endif() + else() + + if(NOT ${PARAM_SHALLOW_CLONE}) + message(STATUS "Cloning ${PARAM_URL} to ${short_repo_dir}.") + execute_process(COMMAND ${GIT_EXECUTABLE} clone --recurse-submodules ${PARAM_URL} ${repo_dir} + RESULT_VARIABLE GIT_CLONE_RESULT) + if (NOT ${GIT_CLONE_RESULT}) + execute_process(COMMAND ${GIT_EXECUTABLE} checkout --quiet ${PARAM_COMMITISH} + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_CHECKOUT_RESULT) + if (NOT ${GIT_CHECKOUT_RESULT}) + message(STATUS "Checking out ${PARAM_COMMITISH} was successfull.") + else() + message(FATAL_ERROR "In ${name}, checking out ${PARAM_COMMITISH} was NOT successfull!") + endif() + else() + message(FATAL_ERROR "Cloning ${name} was NOT successfull!") + endif() + else() + # The shallow clone method is inspired by https://github.com/eliemichel/WebGPU-distribution/blob/dawn/cmake/FetchDawn.cmake + # We only fetch the specific commit/branch and not the whole history and then reset to the fetched head. + # NOTE: Also for the not shallow clone we could think of applying this method to speed up the cloning process. + message(STATUS "Shallow Cloning ${PARAM_URL} to ${short_repo_dir}.") + file(MAKE_DIRECTORY ${repo_dir}) # make sure the directory repo_dir exist + execute_process(COMMAND ${GIT_EXECUTABLE} init + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_INIT_RESULT) + + if(NOT ${GIT_INIT_RESULT}) + execute_process(COMMAND ${GIT_EXECUTABLE} fetch --depth=1 --quiet ${PARAM_URL} ${PARAM_COMMITISH} + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_FETCH_RESULT) + + if(NOT ${GIT_FETCH_RESULT}) + execute_process(COMMAND ${GIT_EXECUTABLE} reset --hard FETCH_HEAD + WORKING_DIRECTORY ${repo_dir} + RESULT_VARIABLE GIT_RESET_RESULT) + + if (NOT ${GIT_RESET_RESULT}) + message(STATUS "Shallow Checkout of ${PARAM_URL} to ${short_repo_dir} was successfull.") + else() + message(FATAL_ERROR "Reset to FETCH_HEAD in ${name} was NOT successful!") + endif() + else() + message(FATAL_ERROR "Fetching ${PARAM_COMMITISH} from ${PARAM_URL} was NOT successful!") + endif() + else() + message(FATAL_ERROR "Initialization of git repository in ${name} was NOT successful!") + endif() + endif() + endif() + + if (NOT ${PARAM_DO_NOT_ADD_SUBPROJECT}) + if (NOT ${PARAM_EXCLUDE_FROM_ALL}) + if (NOT ${PARAM_NOT_SYSTEM}) + # DEFAULT CONFIGURATION + add_subdirectory(${repo_dir} ${CMAKE_BINARY_DIR}/alp_external/${name} SYSTEM) + else() + add_subdirectory(${repo_dir} ${CMAKE_BINARY_DIR}/alp_external/${name}) + endif() + else() + if (NOT ${PARAM_NOT_SYSTEM}) + add_subdirectory(${repo_dir} ${CMAKE_BINARY_DIR}/alp_external/${name} SYSTEM EXCLUDE_FROM_ALL) + else() + add_subdirectory(${repo_dir} ${CMAKE_BINARY_DIR}/alp_external/${name} EXCLUDE_FROM_ALL) + endif() + endif() + endif() +endfunction() diff --git a/cmake/alp_find_dawn_dxc.cmake b/cmake/alp_find_dawn_dxc.cmake new file mode 100644 index 000000000..cfd06972d --- /dev/null +++ b/cmake/alp_find_dawn_dxc.cmake @@ -0,0 +1,183 @@ +include_guard(GLOBAL) + +function(_alp_dawn_dxc_arch_dirs out_var) + if (CMAKE_GENERATOR_PLATFORM MATCHES "(^|,)([Aa][Rr][Mm]64|aarch64)($|,)") + set(ALP_DAWN_DXC_PRIMARY_ARCH arm64) + elseif(CMAKE_GENERATOR_PLATFORM MATCHES "(^|,)([Ww]in32|x86)($|,)") + set(ALP_DAWN_DXC_PRIMARY_ARCH x86) + elseif(CMAKE_GENERATOR_PLATFORM MATCHES "(^|,)(x64|X64|amd64|AMD64)($|,)") + set(ALP_DAWN_DXC_PRIMARY_ARCH x64) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") + set(ALP_DAWN_DXC_PRIMARY_ARCH arm64) + elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(ALP_DAWN_DXC_PRIMARY_ARCH x86) + else() + set(ALP_DAWN_DXC_PRIMARY_ARCH x64) + endif() + + set(ALP_DAWN_DXC_ARCH_DIRS "${ALP_DAWN_DXC_PRIMARY_ARCH}" x64 arm64 x86) + list(REMOVE_DUPLICATES ALP_DAWN_DXC_ARCH_DIRS) + set(${out_var} "${ALP_DAWN_DXC_ARCH_DIRS}" PARENT_SCOPE) +endfunction() + +function(_alp_append_existing_dir out_var dir) + if (dir) + file(TO_CMAKE_PATH "${dir}" ALP_DAWN_DXC_DIR_CMAKE) + if (EXISTS "${ALP_DAWN_DXC_DIR_CMAKE}") + set(ALP_DAWN_DXC_DIRS "${${out_var}}" "${ALP_DAWN_DXC_DIR_CMAKE}") + set(${out_var} "${ALP_DAWN_DXC_DIRS}" PARENT_SCOPE) + endif() + endif() +endfunction() + +function(_alp_dawn_dxc_windows_sdk_roots out_var) + set(ALP_DAWN_DXC_SDK_ROOTS) + + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{WINDOWSSDKDIR}") + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{WindowsSdkDir}") + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{WIN10_SDK_PATH}") + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{CMAKE_WINDOWS_KITS_10_DIR}") + + cmake_host_system_information( + RESULT ALP_DAWN_DXC_REGISTRY_SDK_ROOT + QUERY WINDOWS_REGISTRY "HKLM/SOFTWARE/Microsoft/Windows Kits/Installed Roots" + VALUE "KitsRoot10" + VIEW BOTH + ERROR_VARIABLE ALP_DAWN_DXC_REGISTRY_ERROR + ) + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "${ALP_DAWN_DXC_REGISTRY_SDK_ROOT}") + + set(ALP_DAWN_DXC_PROGRAM_FILES_X86_ENV "ProgramFiles(x86)") + if (DEFINED ENV{${ALP_DAWN_DXC_PROGRAM_FILES_X86_ENV}}) + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{${ALP_DAWN_DXC_PROGRAM_FILES_X86_ENV}}/Windows Kits/10") + endif() + _alp_append_existing_dir(ALP_DAWN_DXC_SDK_ROOTS "$ENV{ProgramFiles}/Windows Kits/10") + + if (ALP_DAWN_DXC_SDK_ROOTS) + list(REMOVE_DUPLICATES ALP_DAWN_DXC_SDK_ROOTS) + endif() + set(${out_var} "${ALP_DAWN_DXC_SDK_ROOTS}" PARENT_SCOPE) +endfunction() + +function(_alp_dawn_dxc_sdk_versions out_var sdk_root) + set(ALP_DAWN_DXC_SDK_VERSIONS) + + if (CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + list(APPEND ALP_DAWN_DXC_SDK_VERSIONS "${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + endif() + + foreach(ALP_DAWN_DXC_VERSION_ENV IN ITEMS WindowsSDKVersion WIN10_SDK_VERSION) + if (DEFINED ENV{${ALP_DAWN_DXC_VERSION_ENV}} AND NOT "$ENV{${ALP_DAWN_DXC_VERSION_ENV}}" STREQUAL "") + set(ALP_DAWN_DXC_ENV_SDK_VERSION "$ENV{${ALP_DAWN_DXC_VERSION_ENV}}") + string(REGEX REPLACE "[/\\\\]+$" "" ALP_DAWN_DXC_ENV_SDK_VERSION "${ALP_DAWN_DXC_ENV_SDK_VERSION}") + list(APPEND ALP_DAWN_DXC_SDK_VERSIONS "${ALP_DAWN_DXC_ENV_SDK_VERSION}") + endif() + endforeach() + + foreach(ALP_DAWN_DXC_VERSION IN LISTS ALP_DAWN_DXC_SDK_VERSIONS) + if (IS_DIRECTORY "${sdk_root}/Include/${ALP_DAWN_DXC_VERSION}.0") + list(APPEND ALP_DAWN_DXC_SDK_VERSIONS "${ALP_DAWN_DXC_VERSION}.0") + endif() + endforeach() + + file(GLOB ALP_DAWN_DXC_INSTALLED_SDK_DIRS LIST_DIRECTORIES true "${sdk_root}/Include/10.*") + if (ALP_DAWN_DXC_INSTALLED_SDK_DIRS) + list(SORT ALP_DAWN_DXC_INSTALLED_SDK_DIRS COMPARE NATURAL ORDER DESCENDING) + foreach(ALP_DAWN_DXC_INSTALLED_SDK_DIR IN LISTS ALP_DAWN_DXC_INSTALLED_SDK_DIRS) + get_filename_component(ALP_DAWN_DXC_INSTALLED_SDK_VERSION "${ALP_DAWN_DXC_INSTALLED_SDK_DIR}" NAME) + list(APPEND ALP_DAWN_DXC_SDK_VERSIONS "${ALP_DAWN_DXC_INSTALLED_SDK_VERSION}") + endforeach() + endif() + + if (ALP_DAWN_DXC_SDK_VERSIONS) + list(REMOVE_DUPLICATES ALP_DAWN_DXC_SDK_VERSIONS) + endif() + set(${out_var} "${ALP_DAWN_DXC_SDK_VERSIONS}" PARENT_SCOPE) +endfunction() + +function(alp_find_dawn_dxc_dlls out_var) + if (NOT WIN32) + message(FATAL_ERROR "alp_find_dawn_dxc_dlls can only be used on Windows") + endif() + + set(ALP_DAWN_DXC_DIR "" CACHE PATH "Directory containing dxcompiler.dll and dxil.dll for Dawn's DirectX backend") + set(ALP_DAWN_DXC_HINT_DIRS) + set(ALP_DAWN_DXC_SEARCH_LABELS) + + if (ALP_DAWN_DXC_DIR) + file(TO_CMAKE_PATH "${ALP_DAWN_DXC_DIR}" ALP_DAWN_DXC_EXPLICIT_DIR) + list(APPEND ALP_DAWN_DXC_HINT_DIRS "${ALP_DAWN_DXC_EXPLICIT_DIR}") + list(APPEND ALP_DAWN_DXC_SEARCH_LABELS "${ALP_DAWN_DXC_EXPLICIT_DIR}") + else() + _alp_dawn_dxc_arch_dirs(ALP_DAWN_DXC_ARCH_DIRS) + _alp_dawn_dxc_windows_sdk_roots(ALP_DAWN_DXC_SDK_ROOTS) + + foreach(ALP_DAWN_DXC_SDK_ROOT IN LISTS ALP_DAWN_DXC_SDK_ROOTS) + _alp_dawn_dxc_sdk_versions(ALP_DAWN_DXC_SDK_VERSIONS "${ALP_DAWN_DXC_SDK_ROOT}") + + foreach(ALP_DAWN_DXC_ARCH_DIR IN LISTS ALP_DAWN_DXC_ARCH_DIRS) + list(APPEND ALP_DAWN_DXC_HINT_DIRS "${ALP_DAWN_DXC_SDK_ROOT}/Redist/D3D/${ALP_DAWN_DXC_ARCH_DIR}") + endforeach() + + foreach(ALP_DAWN_DXC_SDK_VERSION IN LISTS ALP_DAWN_DXC_SDK_VERSIONS) + foreach(ALP_DAWN_DXC_ARCH_DIR IN LISTS ALP_DAWN_DXC_ARCH_DIRS) + list(APPEND ALP_DAWN_DXC_HINT_DIRS "${ALP_DAWN_DXC_SDK_ROOT}/bin/${ALP_DAWN_DXC_SDK_VERSION}/${ALP_DAWN_DXC_ARCH_DIR}") + endforeach() + endforeach() + + foreach(ALP_DAWN_DXC_ARCH_DIR IN LISTS ALP_DAWN_DXC_ARCH_DIRS) + list(APPEND ALP_DAWN_DXC_HINT_DIRS "${ALP_DAWN_DXC_SDK_ROOT}/bin/${ALP_DAWN_DXC_ARCH_DIR}") + endforeach() + endforeach() + endif() + + if (ALP_DAWN_DXC_HINT_DIRS) + list(REMOVE_DUPLICATES ALP_DAWN_DXC_HINT_DIRS) + foreach(ALP_DAWN_DXC_HINT_DIR IN LISTS ALP_DAWN_DXC_HINT_DIRS) + if (EXISTS "${ALP_DAWN_DXC_HINT_DIR}") + list(APPEND ALP_DAWN_DXC_SEARCH_LABELS "${ALP_DAWN_DXC_HINT_DIR}") + endif() + endforeach() + endif() + + find_file(_ALP_DAWN_DXCOMPILER_DLL + NAMES dxcompiler.dll + HINTS ${ALP_DAWN_DXC_HINT_DIRS} + NO_DEFAULT_PATH + NO_CACHE + ) + find_file(_ALP_DAWN_DXIL_DLL + NAMES dxil.dll + HINTS ${ALP_DAWN_DXC_HINT_DIRS} + NO_DEFAULT_PATH + NO_CACHE + ) + + if (NOT ALP_DAWN_DXC_DIR) + if (NOT _ALP_DAWN_DXCOMPILER_DLL) + find_file(_ALP_DAWN_DXCOMPILER_DLL NAMES dxcompiler.dll NO_CACHE) + endif() + if (NOT _ALP_DAWN_DXIL_DLL) + find_file(_ALP_DAWN_DXIL_DLL NAMES dxil.dll NO_CACHE) + endif() + list(APPEND ALP_DAWN_DXC_SEARCH_LABELS "CMake default search paths") + endif() + + if (NOT _ALP_DAWN_DXCOMPILER_DLL OR NOT _ALP_DAWN_DXIL_DLL) + if (ALP_DAWN_DXC_SEARCH_LABELS) + list(REMOVE_DUPLICATES ALP_DAWN_DXC_SEARCH_LABELS) + string(REPLACE ";" "\n " ALP_DAWN_DXC_SEARCH_PATHS "${ALP_DAWN_DXC_SEARCH_LABELS}") + else() + set(ALP_DAWN_DXC_SEARCH_PATHS "No candidate paths were found") + endif() + + message(FATAL_ERROR + "Dawn's Windows package does not ship the DirectX Shader Compiler runtime DLLs, " + "and CMake could not find dxcompiler.dll and dxil.dll.\n" + "Set -DALP_DAWN_DXC_DIR=.\n" + "Searched:\n ${ALP_DAWN_DXC_SEARCH_PATHS}" + ) + endif() + + set(${out_var} "${_ALP_DAWN_DXCOMPILER_DLL}" "${_ALP_DAWN_DXIL_DLL}" PARENT_SCOPE) +endfunction() diff --git a/cmake/alp_target_add_dawn.cmake b/cmake/alp_target_add_dawn.cmake new file mode 100644 index 000000000..5af447c4e --- /dev/null +++ b/cmake/alp_target_add_dawn.cmake @@ -0,0 +1,58 @@ +############################################################################# +# weBIGeo +# Copyright (C) 2024 Gerald Kimmersdorfer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +# alp_target_add_dawn(target, dawn_path, scope) +# Configures a target to link against and include Dawn graphics libraries. +# - target: Target to configure. +# - dawn_path: Path to Dawn installation. (Builds are expected to be in build/debug or build/release) +# - scope: Linkage and include scope (PRIVATE, PUBLIC, INTERFACE). +function(alp_target_add_dawn target dawn_path scope) + + set(DAWN_DIR "${dawn_path}") + + # Determine the correct DAWN_BIN based on the build type + set(DAWN_BIN "${DAWN_DIR}/build/release") + if(CMAKE_BUILD_TYPE MATCHES Debug) + set(DAWN_BIN "${DAWN_DIR}/build/debug") + endif() + + # Check if DAWN_DIR exists + if(NOT EXISTS "${DAWN_DIR}") + message(FATAL_ERROR "DAWN could not be found at: ${DAWN_DIR}") + endif() + + # Check if DAWN_BIN exists + if(NOT EXISTS "${DAWN_BIN}") + message(FATAL_ERROR "DAWN Binaries could not be found at: ${DAWN_BIN}. Did you build DAWN?") + endif() + + # Note: Dawn constitutes of multiple libraries, so you have to link all of them + # in order to not end up with unresolved symbols. A subset of the libraries might + # be sufficient, but finding this is not trivial, and may change with future versions. + # Therefore, linking all libraries might be the easiest/best approach + + # Find all .lib files in the Dawn build directory + file(GLOB_RECURSE DAWN_LIBRARIES "${DAWN_BIN}/*.lib") + + # Link all found libraries to the specified target + target_link_libraries(${target} ${scope} ${DAWN_LIBRARIES}) + + # Add the header files as well as the generated headers to the include directories + target_include_directories(${target} ${scope} "${DAWN_DIR}/include") + target_include_directories(${target} ${scope} "${DAWN_BIN}/gen/include") +endfunction(alp_target_add_dawn) diff --git a/docs/app.md b/docs/app.md new file mode 100644 index 000000000..401003044 --- /dev/null +++ b/docs/app.md @@ -0,0 +1,47 @@ +# AlpineMaps (app) + +This is the software behind [alpinemaps.org](https://alpinemaps.org). It uses `gl_engine` for rendering and Qt Quick (QML) for the UI. + +A developer version (trunk) is released [here](https://alpinemapsorg.github.io/renderer/), including APKs for android. Be aware that it can break at any time! + +[If looking at the issues, best to filter out projects!](https://github.com/AlpineMapsOrg/renderer/issues?q=is%3Aissue%20state%3Aopen%20no%3Aproject) + +## Dependencies + +* Qt 6.11.1, or greater +* g++ 12+, clang or msvc +* OpenGL +* Qt Positioning and Charts modules +* Some other dependencies will be pulled automatically during building. + +## Cloning + +```sh +git clone git@github.com:AlpineMapsOrg/renderer.git +``` + +After that it should be a normal cmake project. That is, you run cmake to generate a project or build file and then run your favourite tool. All dependencies should be pulled automatically while you run CMake. +We use Qt Creator (with mingw on Windows), which is the only tested setup atm and makes setup of Android and WebAssembly builds reasonably easy. If you have questions, please go to Discord. + +## Building the native version + +Just run cmake and build. + +## Building the android version + +See also: [Creating APK signing keys](app_creating_apk_keys.md) + +* We are usually building with Qt Creator, because it works relatively out of the box. However, it should also work on the command line or other IDEs if you set it up correctly. +* You need a Java JDK before you can do anything else. Not all Java versions work, and the error messages might be surprising (or non-existant). I'm running with Java 19, and I can compile for old devices. Iirc a newer version of Java caused issues. [Android documents the required Java version](https://developer.android.com/build/jdks), but as said, for me Java 19 works as well. It might change in the future. +* Once you have Java, go to Qt Creator Preferences -> Devices -> Android. There click "Set Up SDK" to automatically download and install an Android SDK. +* Finally, you might need to click on SDK Manager to install a fitting SDK Platform (take the newest, it also works for older devices), and ndk (newest as well). +* Then Google the internet to find out how to enable the developer mode on Android. +* On linux, you'll have to setup some udev rules. Run `Android/SDK/platform-tools/adb devices` and you should get instructions. +* If there are problems, check out the [documentation from Qt](https://doc.qt.io/qt-6/android-getting-started.html) +* Finally, you are welcome to ask in discord if something is not working! + +## Building the WebAssembly version + +* [The Qt documentation is quite good on how to get it to run](https://doc-snapshots.qt.io/qt6-dev/wasm.html#installing-emscripten). +* Be aware that only specific versions of emscripten work for specific versions of Qt, and the error messages are not helpfull. +* [More info on building and getting Hotreload to work](https://github.com/AlpineMapsOrg/documentation/blob/main/WebAssembly_local_build.md) diff --git a/creating_apk_keys.md b/docs/app_creating_apk_keys.md similarity index 100% rename from creating_apk_keys.md rename to docs/app_creating_apk_keys.md diff --git a/docs/webgpu_app.md b/docs/webgpu_app.md new file mode 100644 index 000000000..2975a0c9a --- /dev/null +++ b/docs/webgpu_app.md @@ -0,0 +1,109 @@ +# weBIGeo (webgpu_app) + +[![Discord](https://img.shields.io/badge/discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/p8T9XzVwRa) [![Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://webigeo.alpinemaps.org/) + +`webgpu_app` (branded as **weBIGeo**) is a research application built on the AlpineMaps.org rendering infrastructure, focused on real-time 3D computer graphics, large data visualization, and human-computer interaction over geographical datasets. Further information: [netidee.at/webigeo](https://www.netidee.at/webigeo). + +## [Developer Guide](webgpu_app_dev.md) +For an overview of the internal architecture, see the [Developer Guide](webgpu_app_dev.md). + +## Setup + +weBIGeo can be deployed to the web via emscripten and additionally we support native builds on Windows, using [Dawn](https://dawn.googlesource.com/) and [SDL2](https://github.com/libsdl-org/SDL/tree/SDL2). + +### CMake Presets +| Preset | Description | +|--------|-------------| +| **msvc-debug** | MSVC Debug build for Windows (native) | +| **msvc-release** | MSVC Release build for Windows (native) | +| **msvc-debug-test** | MSVC Debug build for unit tests | +| **msvc-release-test** | MSVC Release build for unit tests | +| **wasm-debug** | WebAssembly Debug build | +| **wasm-release** | WebAssembly Release build | +| **wasm-publish** | WebAssembly Production build with minified shaders and no debug output | + +### Building the web version + +#### Dependencies +* Qt 6.10.1 with + * WebAssembly (multi-threaded) pre-built binaries +* Python 3 +* cmake and ninja (come with Qt) +* emsdk 4.0.7 ([emscripten](https://emscripten.org/docs/getting_started/downloads.html)) + ``` + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + emsdk install 4.0.7 + emsdk activate 4.0.7 + ``` + +#### Configuration +> [!IMPORTANT] +> If you're using Qt Creator, you can simply use the default kit *WebAssembly Qt 6.10.1 (multi-threaded)* - no additional setup is needed. + +Before building, you need to ensure the following paths are correctly configured in [CMakePresets.json](../CMakePresets.json): + +1. **Ninja**: Make sure Ninja is in your system PATH, OR update the `PATH` environment variable in the `emscripten-base` preset to point to your Ninja installation (e.g., `C:/Qt/Tools/Ninja`). + +2. **EMSDK**: The `EMSDK` environment variable in the `emscripten-base` preset must point to your emsdk installation directory (e.g., `C:/tmp/webigeo/emsdk`). + +3. **Toolchain file**: The `toolchainFile` path in the `emscripten-base` preset might need to be adapted depending on where Qt is installed (e.g., `C:/Qt/6.10.1/wasm_multithread/lib/cmake/Qt6/qt.toolchain.cmake`). + +#### Serving the WASM Build +After building, you can use the `serve_wasm.py` script to serve the build files for the WebAssembly build locally. This script sets up a local server with the correct headers required for WebAssembly. + +### Building the native version + +#### Dependencies +* Windows +* Qt 6.10.1 with + * MSVC2022 pre-built binaries +* Python 3 +* Microsoft Visual C++ Compiler 17.6 (aka. MSVC2022, comes with Visual Studio 2022) +* cmake and ninja (come with Qt) + +#### Configuration +> [!IMPORTANT] +> If you're using Qt Creator, you can simply use the default kit *Desktop Qt 6.10.1 MSVC2022 (64-bit)* - no additional setup is needed. + +Before building, you need to ensure the following paths are correctly configured in [CMakePresets.json](../CMakePresets.json): + +1. **Qt6_DIR**: Verify that the `Qt6_DIR` in the `msvc-base` preset points to your actual Qt installation's CMake directory (e.g., `C:/Qt/6.10.1/msvc2022_64/lib/cmake/Qt6`). + +#### Troubleshoot +- MY CONFIGURATION TAKES FOREVER: Upon first cmake configuration DAWN as well as SDL is being pulled, build and installed. This might take a while. (~10-40 min) +- Dawn and SDL installation as well as fetching the custom dawn port for emscripten now happens in the python scripts inside the respective folder. They get executed by the CMAKE-Setup. A change requires a reconfiguration of CMAKE as well as the deletion of the directory in the `extern` directory. +- If you have issues with your currently installed Vulkan SDK you may try one or all of the following: + - disable `DDAWN_FORCE_SYSTEM_COMPONENT_LOAD` + - Try with a different DAWN backend + - copying the include files from your sdk into the respective folders in the dawn binaries `dawn\third_party\vulkan-utility-libraries\src\include\vulkan\utility\` and `dawn\third_party\vulkan-headers\src\include\vulkan\` and rebuild dawn. + +#### About DAWN Backends +Per default we opt for an only Vulkan-Backend Build for two reasons: +- Vulkan is probably the most supported Backend running on most devices +- We have more knowledge about Vulkan which comes to play when we use GPU debuggers + +That being said you may enable different Backends in the `install_dawn.py` script. + +### Install Targets +Install targets are now available for both web and native builds. These targets install all necessary files into the install directory, making it easy to deploy or distribute the built application. + +To use the install target: +```bash +cmake --build build/ --target install +``` + +The install directory is automatically configured in the CMake presets and will be located at: +- Native builds: `install/msvc-debug` or `install/msvc-release` +- Web builds: `install/wasm-debug`, `install/wasm-release`, or `install/wasm-publish` + +### Tested Coding Environments +The following development environments have been tested and are known to work with this project: + +- **Qt Creator 18** [recommended] + - With Qt Creator we recommend using the default Kits (see above) +- **Visual Studio Code** with the following extensions: + - [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) + - [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools) + - [WGSL](https://marketplace.visualstudio.com/items?itemName=PolyMeilex.wgsl) +- **Visual Studio 2022 Community** diff --git a/docs/webgpu_app_dev.md b/docs/webgpu_app_dev.md new file mode 100644 index 000000000..d10703249 --- /dev/null +++ b/docs/webgpu_app_dev.md @@ -0,0 +1,95 @@ +# webgpu_app - Developer Guide + +## Application structure + +`apps/webgpu_app/main.cpp` creates the `webgpu_app::App`, which owns the SDL window, the WebGPU device, and the render loop (`App.cpp`). Each frame: + +1. `ImGuiManager::render()` records the ImGui draw commands for the UI on top of the scene. +2. The 3D scene (terrain, overlays) is rendered via [`webgpu_engine`](webgpu_engine.md) only on demand (e.g. on camera change). Both `webgpu_engine` and the compute graph depend on [`webgpu_base`](webgpu_base.md) for shader preprocessing, GPU resource management, and RAII wrappers. + +```mermaid +graph LR + App("App") + WebGPUCtx("webgpu::Context") + Window("webgpu_engine::Window") + CameraCtrl("camera::Controller") + + ImGuiMgr("ImGuiManager") + OverlaysPanel("OverlaysPanel") + NodeGraphPanel("NodeGraphPanel *") + OverlayImGuiR[["OverlayImGuiRenderer[ ]"]] + NodeGraph(["NodeGraph *"]) + NodeRenderers[["NodeRenderer[ ] *"]] + panels[["Panels[ ]"]] + + RenderCtx("RenderingContext") + EngineCtx("webgpu_engine::Context") + Schedulers("Schedulers / CloudsManager / SearchService") + + App --> WebGPUCtx + App --> Window + App --> CameraCtrl + App --> ImGuiMgr + App --> RenderCtx + + ImGuiMgr --> panels + panels --> OverlaysPanel + panels --> NodeGraphPanel + + OverlaysPanel --> OverlayImGuiR + OverlaysPanel -.-> EngineCtx + + NodeGraphPanel --> NodeGraph + NodeGraphPanel --> NodeRenderers + NodeGraphPanel -.-> EngineCtx + + RenderCtx --> EngineCtx + RenderCtx --> Schedulers +``` + +*\* ... only compiled when `ALP_WEBGPU_APP_ENABLE_COMPUTE` is enabled.* + +### ImGuiManager + +This class is tying the UI together: +- Initializes the Dear ImGui / ImNodes contexts and fonts. +- Owns the list of `ImGuiPanel`s and draws them every frame. +- Forwards SDL events to ImGui +- Offers some static helper functions + +### Panels + +All panels implement the `ImGuiPanel` interface (`apps/webgpu_app/ui/ImGuiPanel.h`) + +In general we follow a feature-based directory layout - each feature folder owns its panel (e.g. `overlay/OverlaysPanel`, `compute/NodeGraphPanel`). General-purpose panels live in `apps/webgpu_app/ui/`. New panels must be manually instantiated and registered in `ImGuiManager` (`apps/webgpu_app/ImGuiManager.cpp`). + +### OverlayImGuiRenderer + +`OverlaysPanel` is the ImGui panel used to configure which overlays are active and their settings - the actual rendering is done by the corresponding [`webgpu_engine::OverlayRenderer`](webgpu_engine.md#overlays). Each overlay type can have a matching `OverlayImGuiRenderer` subclass (`apps/webgpu_app/overlay/`) that draws its settings controls. + +If no specific subclass is registered, the base `OverlayImGuiRenderer` serves as a fallback. + +> [!NOTE] +> After adding a new Overlay to the engine you have to: +> - **`OverlaysPanel.cpp`**: extend the `AddType` enum and `ADD_ITEMS[]` array and add a branch in `add_overlay_of_type()` to instantiate the new type. +> +> **and optionally** +> - **UI renderer**: create a matching `OverlayImGuiRenderer` subclass in `apps/webgpu_app/overlay/` +> - **`OverlayImGuiRendererFactory.cpp`**: add a `dynamic_cast` branch in `OverlayImGuiRendererFactory::create()` + + +### Compute + +The `NodeGraphPanel` (`apps/webgpu_app/compute/`) manages the active `NodeGraph` and owns one `NodeRenderer` per node instance, providing the visual/interactive representation in the node-graph editor. Each node type can have a matching `NodeRenderer` subclass (`apps/webgpu_app/compute/nodes/`) + +If no specific subclass is registered, the base `NodeRenderer` is used as a fallback (renders the node with sockets but no settings panel). + +> [!NOTE] +> When adding a new compute node type, three files must be touched: +> 1. **Compute node**: create a `Node` subclass in `webgpu/compute/nodes/` (the engine-side compute logic). +> 2. **Node registry**: call `register_node()` in `NodeRegistry::NodeRegistry()` so the node can be instantiated by name (required for graph serialization / the add-node dialog). +> 3. **UI renderer**: optionally create a `NodeRenderer` subclass in `apps/webgpu_app/compute/nodes/` and add a `dynamic_cast` branch in `NodeRendererFactory::create()` + +#### OverlayRenderNode + +`OverlayRenderNode` (`apps/webgpu_app/compute/OverlayRenderNode.h`) is a special node that bridges the compute graph and the rendering system. Unlike regular compute nodes it lives in the app layer because it holds a reference to `webgpu_engine::Context`. When executed, it forwards the graph's output texture to a `TextureOverlay` managed by the engine's `OverlayRenderer`, making compute results visible in the 3D viewport. diff --git a/docs/webgpu_base.md b/docs/webgpu_base.md new file mode 100644 index 000000000..1c718c610 --- /dev/null +++ b/docs/webgpu_base.md @@ -0,0 +1,111 @@ +# webgpu_base + +`webgpu/base/` is the foundational library shared by `webgpu_engine` and `webgpu_compute`. It provides: + +- **RAII wrappers** for all raw WebGPU handles (`WGPUTexture`, `WGPUBuffer`, `WGPURenderPipeline`, …) +- A **custom shader preprocessor** for WGSL files with file inclusion and conditional compilation +- A **GPU resource registry** that owns shaders, bind group layouts, and pipeline constructors and can recreate them all in dependency order +- A lightweight **timing infrastructure** for CPU and GPU performance profiling +- A `webgpu::Context` object to bundle all raw WebGPU handles together with the `RenderResourceRegistry` and the appropriate `ShaderPreprocessor` + +```mermaid +graph LR + Ctx("webgpu::Context") + Reg("RenderResourceRegistry") + Pre("ShaderPreprocessor") + + + Ctx --> Reg + Reg --> Pre +``` + +*`Context` owns the `RenderResourceRegistry`, which in turn owns the `ShaderPreprocessor`.* + +--- + +## Shader Preprocessor + +**Class:** `webgpu::util::ShaderPreprocessor` +**Files:** [webgpu/base/util/ShaderPreprocessor.h](../webgpu/base/util/ShaderPreprocessor.h), [ShaderPreprocessor.cpp](../webgpu/base/util/ShaderPreprocessor.cpp) + +### Design principle + +All preprocessor directives use a `///` comment prefix so that `.wgsl` files remain syntactically valid WGSL. Tooling (formatters, syntax highlighters) sees them as ordinary line comments; the preprocessor intercepts them before the GPU compiler. + +### Directives + +| Directive | Effect | +|-----------|--------| +| `///use relpath` | Include another shader file (same namespace) | +| `///use target::relpath` | Include a shader from a different namespace | +| `///define SYMBOL` | Define a symbol (value defaults to `"1"`) | +| `///define SYMBOL value` | Define a symbol with an explicit string value | +| `///ifdef SYMBOL` / `///ifndef SYMBOL` | Conditional block on presence of a symbol | +| `///if SYMBOL value` / `///elif SYMBOL value` | Conditional block on symbol's value | +| `///else` / `///endif` | Close a conditional block | + +> [!Important] +Each file is included at most once per top-level call (`pragma-once` behaviour). + +### Namespace-based file resolution + +Shader files are identified by a logical name in the form `namespace::relative/path` (without `.wgsl`). The preprocessor resolves these names to physical files using a two-level lookup: + +1. **Local filesystem path**: +If `set_local_shader_path("webgpu_engine", "/path/to/shaders")` has been called for the target namespace, the file is read from disk. This enables hot-reload during development. +2. **Qt resource (QRC) fallback**: +If no local path is configured, the file is read from the embedded resource `:/shaders/namespace/relative/path.wgsl`. + +A `///use` directive without an explicit namespace inherits the namespace of the file that contains it: + +e.g. inside `webgpu_engine::tile_mesh/render_tiles.wgsl`: + - `///use util/shared_config`-> `webgpu/engine/shaders/util/shared_config.wgsl` + - `///use webgpu::hashing` -> `webgpu/base/shaders/hasing.wgsl` + +This means shader libraries can be structured into per-module namespaces (`webgpu`, `webgpu_engine`, `webgpu_compute`) and reference each other without hardcoding absolute paths. + +### Platform defines + +The constructor automatically defines symbols for the current build environment, so shaders can use `///ifdef __EMSCRIPTEN__` or `///ifdef _WIN32` the same way C++ code uses `#ifdef`. + + +--- + +## GPU Resource Registry + +**Class:** `webgpu::RenderResourceRegistry` +**Files:** [webgpu/base/RenderResourceRegistry.h](../webgpu/base/RenderResourceRegistry.h), [RenderResourceRegistry.cpp](../webgpu/base/RenderResourceRegistry.cpp) + +### Responsibilities + +The registry centralises a few GPU resources that need to be recreated together, for example on device loss or when shaders are hot-reloaded. It tracks three resource categories: + +| Category | Storage | +|----------|---------| +| Shader modules | Preprocessed + compiled WGSL | +| Bind group layouts | Factory-built `WGPUBindGroupLayout` | +| Pipeline constructors | Callbacks that build pipelines | + +> [!WARNING] +> Pipelines are **not** stored inside the registry. The callback owns the pipeline object and stores it in the caller (typically a Renderer). This avoids the registry needing to know about the different concrete pipeline types, and additionally saves us an additional mapping access when we want to use a pipeline. + +### Recreation order + +`recreate_all` always recreates resources in dependency order: + +```mermaid +graph LR + S["1. Shaders\n(preprocess + compile)"] + L["2. Bind Group Layouts\n(factory callbacks)"] + P["3. Pipelines\n(pipeline callbacks)"] + + S --> L --> P +``` + +### Inline shader compilation + +For one-off shaders that don't need to be reloaded, `compile_shader_from_code()` runs the preprocessor on raw WGSL code and returns a ready-to-use `ShaderModule` without registering it: + +```cpp +auto module = reg.compile_shader_from_code(device, wgslSource, "my_inline_shader"); +``` \ No newline at end of file diff --git a/docs/webgpu_engine.md b/docs/webgpu_engine.md new file mode 100644 index 000000000..af7e795bb --- /dev/null +++ b/docs/webgpu_engine.md @@ -0,0 +1,102 @@ +# webgpu_engine - Rendering Pipeline + +## Overview + +`webgpu_engine` implements the 3D rendering pipeline for the terrain viewer. It builds on top of [webgpu_base](webgpu_base.md) for shader preprocessing, GPU resource management, and RAII wrappers. The central ownership structure is `webgpu_engine::Context`, which holds all renderers as `std::shared_ptr`. `webgpu_engine::Window` acts as the glue layer that drives the per-frame render sequence by calling into Context in a fixed order. + +```mermaid +graph LR + Window("Window") + Context("Context") + + AtmR("AtmosphereRenderer") + TileR("TileMeshRenderer") + CloudR("CloudRenderer") + TrackR("TrackRenderer") + OvlR("OverlayRenderer") + + Overlays[["Overlay[ ]"]] + HeightLines("HeightLinesOverlay") + Snow("ScreenSpaceSnowOverlay") + Texture("TextureOverlay") + TileDebug("TileDebugOverlay") + + Window -.-> Context + + Context --> AtmR + Context --> TileR + Context --> CloudR + Context --> TrackR + Context --> OvlR + + OvlR --> Overlays + Overlays --> HeightLines + Overlays --> Snow + Overlays --> Texture + Overlays --> TileDebug +``` + +*Solid arrows denote ownership. The dashed arrow from Window to Context is a non-owning reference -> Window receives Context via `set_context()` but does not own the renderers.* + +## Render sequence + +`Window::paint()` drives the frame in this fixed order: + +```mermaid +graph LR + classDef highlight fill:#e8a838,stroke:#b07a1a,color:#000 + + Atm(["AtmosphereRenderer"]) + Tile(["TileMeshRenderer"]) + Cloud(["CloudRenderer"]) + Ovl(["OverlayRenderer"]) + Compose(["Compose pass"]):::highlight + + Atm --> Tile --> Cloud --> Ovl --> Compose +``` + +## Renderers + +A **Renderer** represents a self-contained stage of the rendering pipeline. It may own geometry, textures, compute pipelines, or multi-pass algorithms. Renderers write to shared G-buffer slots or intermediate render targets that later stages read from. + +Current renderers and their responsibilities: + +| Class | Location | Role | +|-------|----------|------| +| `AtmosphereRenderer` | `webgpu/engine/atmosphere/` | Sky dome and atmospheric scattering | +| `TileMeshRenderer` | `webgpu/engine/tile_mesh/` | Terrain tiles with height maps and orthophoto textures | +| `CloudRenderer` | `webgpu/engine/cloud/` | Volumetric clouds | +| `TrackRenderer` | `webgpu/engine/track/` | GPX tracks | +| `OverlayRenderer` | `webgpu/engine/overlay/` | Orchestrates overlay compositing (see below) | + +Context exposes a typed setter for each renderer (`set_tile_mesh_renderer()`, etc.) so the app layer can inject or replace implementations at startup. + +## Overlays + +An **Overlay** is a purely screen-space effect layered on top of the rendered geometry. It does **not** draw geometry or manage 3D state. It reads the current colour/depth buffer and writes a modified version. + +> [!WARNING] +> The ping-pong contract requires every overlay stage to write **every pixel** of `target_output`. Leaving pixels unwritten produces undefined results because the output texture is not cleared between stages. +> +> Overlay stages should be implemented as **compute pipelines** wherever possible. A traditional render pipeline is only acceptable when a compute path is not feasible (e.g. `TextureOverlay` uses a render pipeline for hardware blending). + +The `OverlayRenderer` owns the list of active `Overlay` instances and sorts them by `z_index` before compositing: + +- **`z_index < 0`**: pre-shading - composited before lighting/atmosphere affects the image. +- **`z_index >= 0`**: post-shading - composited after the full scene is lit. + +Current overlay implementations: + +| Class | Location | Effect | +|-------|----------|--------| +| `HeightLinesOverlay` | `webgpu/engine/overlay/` | Contour lines derived from depth buffer | +| `ScreenSpaceSnowOverlay` | `webgpu/engine/overlay/` | Snow accumulation on flat surfaces in screen space | +| `TextureOverlay` | `webgpu/engine/overlay/` | Overlays Rasterdata when provided appropriate AABB data | +| `TileDebugOverlay` | `webgpu/engine/overlay/` | Debug visualisation for gbuffer | + + + +> [!NOTE] +> When adding a new **Overlay**, register it via `OverlayRenderer::add_overlay()` and optionally create a matching `OverlayImGuiRenderer` subclass in `apps/webgpu_app/overlay/` for settings UI (see [webgpu_app_dev.md](webgpu_app_dev.md#overlayimguirenderer)). +> +> When adding a new **Renderer**, add a typed accessor and setter to `webgpu_engine::Context`, instantiate it in `RenderingContext::initialize()` (`apps/webgpu_app/RenderingContext.cpp`), and call it from `Window::paint()` at the appropriate step. diff --git a/license_header_template.txt b/misc/license_header_template.txt similarity index 100% rename from license_header_template.txt rename to misc/license_header_template.txt diff --git a/sanitizer_supressions/linux_leak.supp b/misc/sanitizer_suppressions/linux_leak.supp similarity index 100% rename from sanitizer_supressions/linux_leak.supp rename to misc/sanitizer_suppressions/linux_leak.supp diff --git a/misc/scripts/fetch_dawn_native.py b/misc/scripts/fetch_dawn_native.py new file mode 100644 index 000000000..b19859c38 --- /dev/null +++ b/misc/scripts/fetch_dawn_native.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import shutil +import sys +import tarfile +import tempfile +import urllib.request + +from setup_utils import log, fail, download + + +def safe_extract(tar_path, extract_to): + extract_root = os.path.abspath(extract_to) + try: + with tarfile.open(tar_path, "r:gz") as archive: + for member in archive.getmembers(): + member_path = os.path.abspath(os.path.join(extract_root, member.name)) + if not member_path.startswith(extract_root + os.sep): + fail(f"Unsafe path in Dawn archive: {member.name}") + archive.extractall(extract_root) + except Exception as exc: + fail(f"Failed to extract {tar_path}: {exc}") + + +def release_assets(version): + url = f"https://api.github.com/repos/google/dawn/releases/tags/v{version}" + request = urllib.request.Request(url, headers={"User-Agent": "webigeo-build"}) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response).get("assets", []) + except Exception as exc: + fail(f"Failed to read Dawn release metadata for v{version}: {exc}") + + +def find_package_root(root): + for current, _, files in os.walk(root): + parts = os.path.normpath(current).split(os.sep) + if "DawnConfig.cmake" in files and len(parts) >= 3 and parts[-2:] == ["cmake", "Dawn"]: + return os.path.abspath(os.path.join(current, "..", "..", "..")) + fail("DawnConfig.cmake was not found in the native Dawn package") + + +def main(): + parser = argparse.ArgumentParser(description="Fetch a prebuilt native Dawn package") + parser.add_argument("--extern-dir", required=True, help="External dependencies directory") + parser.add_argument("--dawn-version", required=True, help="Dawn version to fetch") + parser.add_argument("--build-type", required=True, choices=["Debug", "Release"], help="Dawn package configuration") + parser.add_argument("--platform", default="ubuntu-latest", help="Dawn release platform asset name") + args = parser.parse_args() + + extern_dir = os.path.abspath(args.extern_dir) + dawn_dir = os.path.join(extern_dir, "dawn") + install_dir = os.path.join(dawn_dir, "install", args.build_type) + asset_suffix = f"-{args.platform}-{args.build_type}.tar.gz" + + matches = [ + asset for asset in release_assets(args.dawn_version) + if asset.get("name", "").endswith(asset_suffix) + ] + if not matches: + fail(f"No Dawn release asset matching *{asset_suffix} for v{args.dawn_version}") + if len(matches) > 1: + fail(f"Multiple Dawn release assets match *{asset_suffix} for v{args.dawn_version}") + + asset = matches[0] + tmp_dir = tempfile.mkdtemp(prefix="fetchdawn_native_") + archive_path = os.path.join(tmp_dir, asset["name"]) + + try: + download(asset["browser_download_url"], archive_path) + safe_extract(archive_path, tmp_dir) + package_root = find_package_root(tmp_dir) + + if os.path.exists(install_dir): + log(f"Removing existing Dawn native package: {install_dir}") + shutil.rmtree(install_dir) + os.makedirs(os.path.dirname(install_dir), exist_ok=True) + log(f"Installing Dawn native package to {install_dir}") + shutil.move(package_root, install_dir) + + log(f"Successfully fetched native Dawn to {install_dir}") + return 0 + except Exception as exc: + log(f"Native Dawn fetch failed: {exc}") + if os.path.exists(install_dir): + shutil.rmtree(install_dir, ignore_errors=True) + sys.exit(1) + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/misc/scripts/fetch_dawn_port.py b/misc/scripts/fetch_dawn_port.py new file mode 100644 index 000000000..c5f872922 --- /dev/null +++ b/misc/scripts/fetch_dawn_port.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import os +import tempfile +import shutil + +from setup_utils import log, fail, download, extract_zip + + +def main(): + parser = argparse.ArgumentParser(description="Fetch Dawn Emscripten port package") + parser.add_argument("--extern-dir", required=True, help="External dependencies directory") + parser.add_argument("--dawn-version", required=True, help="Dawn version to fetch") + args = parser.parse_args() + + extern_dir = os.path.abspath(args.extern_dir) + pkg_dir = os.path.join(extern_dir, "emdawnwebgpu_pkg") + port_file = os.path.join(pkg_dir, "emdawnwebgpu.port.py") + + tmp = tempfile.mkdtemp(prefix="fetchdawn_") + + zipname = f"emdawnwebgpu_pkg-v{args.dawn_version}.zip" + url = f"https://github.com/google/dawn/releases/download/v{args.dawn_version}/{zipname}" + zippath = os.path.join(tmp, zipname) + + try: + download(url, zippath) + extract_zip(zippath, tmp) + + found_pkg = None + for root, dirs, _ in os.walk(tmp): + if "emdawnwebgpu_pkg" in dirs: + found_pkg = os.path.join(root, "emdawnwebgpu_pkg") + break + + if not found_pkg: + fail("emdawnwebgpu_pkg not found in the extracted Dawn package") + + if os.path.exists(pkg_dir): + log(f"Removing existing package directory: {pkg_dir}") + shutil.rmtree(pkg_dir) + + log(f"Moving package to {pkg_dir}") + shutil.move(found_pkg, pkg_dir) + + if not os.path.exists(port_file): + fail("emdawnwebgpu.port.py missing after installation") + + log(f"Successfully fetched Dawn port to {pkg_dir}") + return 0 + + except Exception as e: + fail(str(e)) + + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/misc/scripts/install_dawn.py b/misc/scripts/install_dawn.py new file mode 100644 index 000000000..28838c4fd --- /dev/null +++ b/misc/scripts/install_dawn.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import os +import tempfile +import shutil + +from setup_utils import log, fail, download, extract_zip, run_command + + +def main(): + parser = argparse.ArgumentParser(description="Install Dawn WebGPU library from source") + parser.add_argument("--extern-dir", required=True, help="External dependencies directory") + parser.add_argument("--dawn-version", required=True, help="Dawn version to install") + parser.add_argument("--cmake-path", default="cmake", help="Path to CMake executable") + parser.add_argument("--ninja-path", default="ninja", help="Path to Ninja executable") + args = parser.parse_args() + + extern_dir = os.path.abspath(args.extern_dir) + dawn_dir = os.path.join(extern_dir, "dawn") + + tmp_dir = tempfile.mkdtemp(prefix="installdawn_") + zip_name = f"v{args.dawn_version}.zip" + zip_path = os.path.join(tmp_dir, zip_name) + url = f"https://github.com/google/dawn/archive/refs/tags/{zip_name}" + + try: + download(url, zip_path) + extract_zip(zip_path, extern_dir) + os.unlink(zip_path) + + extracted_dir = None + for entry in os.listdir(extern_dir): + full = os.path.join(extern_dir, entry) + if os.path.isdir(full) and entry.startswith("dawn-"): + extracted_dir = full + break + + if not extracted_dir: + fail("Cannot find extracted dawn-* directory") + + if os.path.exists(dawn_dir): + log(f"Removing existing Dawn directory: {dawn_dir}") + shutil.rmtree(dawn_dir) + + log(f"Moving extracted Dawn to {dawn_dir}") + shutil.move(extracted_dir, dawn_dir) + + for build_type in ["Debug", "Release"]: + log(f"Building Dawn ({build_type} configuration)") + + out_dir = os.path.join(dawn_dir, "out", build_type) + install_prefix = os.path.join(dawn_dir, "install", build_type) + os.makedirs(out_dir, exist_ok=True) + + cmake_args = [ + args.cmake_path, + "-G", "Ninja", + dawn_dir, + "-B", out_dir, + "-DDAWN_BUILD_MONOLITHIC_LIBRARY=STATIC", + "-DDAWN_FORCE_SYSTEM_COMPONENT_LOAD=ON", + "-DDAWN_FETCH_DEPENDENCIES=ON", + "-DDAWN_ENABLE_INSTALL=ON", + f"-DCMAKE_BUILD_TYPE={build_type}", + "-DTINT_BUILD_SPV_READER=OFF", + "-DTINT_BUILD_TESTS=OFF", + "-DTINT_BUILD_FUZZERS=OFF", + "-DTINT_BUILD_BENCHMARKS=OFF", + "-DTINT_BUILD_AS_OTHER_OS=OFF", + "-DDAWN_BUILD_SAMPLES=OFF", + "-DDAWN_ENABLE_D3D11=OFF", + "-DDAWN_ENABLE_D3D12=OFF", + "-DDAWN_ENABLE_METAL=OFF", + "-DDAWN_ENABLE_NULL=OFF", + "-DDAWN_ENABLE_DESKTOP_GL=OFF", + "-DDAWN_ENABLE_OPENGLES=OFF", + "-DDAWN_ENABLE_VULKAN=ON", + "-DDAWN_USE_WINDOWS_UI=OFF", + "-DDAWN_USE_GLFW=OFF" + ] + + run_command( + cmake_args, + description=f"Configuring Dawn ({build_type})" + ) + + run_command( + [args.cmake_path, "--build", out_dir], + description=f"Building Dawn ({build_type})" + ) + + run_command( + [args.cmake_path, "--install", out_dir, "--prefix", install_prefix], + description=f"Installing Dawn ({build_type})" + ) + + log("Cleaning up Dawn build files and sources") + for entry in os.listdir(dawn_dir): + if entry != "install": + full_path = os.path.join(dawn_dir, entry) + if os.path.isdir(full_path): + shutil.rmtree(full_path, ignore_errors=True) + else: + os.remove(full_path) + + log(f"Successfully installed Dawn to {dawn_dir}/install") + return 0 + + except Exception as e: + log(f"Installation failed: {e}") + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir, ignore_errors=True) + if os.path.exists(dawn_dir): + shutil.rmtree(dawn_dir, ignore_errors=True) + sys.exit(1) + + finally: + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/misc/scripts/install_sdl.py b/misc/scripts/install_sdl.py new file mode 100644 index 000000000..51b9e05ba --- /dev/null +++ b/misc/scripts/install_sdl.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import os +import tempfile +import shutil + +from setup_utils import log, fail, run_command + + +def main(): + parser = argparse.ArgumentParser(description="Install SDL2 from source") + parser.add_argument("--install-prefix", required=True, help="Installation prefix directory") + parser.add_argument("--cmake-path", default="cmake", help="Path to CMake executable") + parser.add_argument("--ninja-path", default="ninja", help="Path to Ninja executable") + parser.add_argument("--git-path", default="git", help="Path to Git executable") + args = parser.parse_args() + + install_prefix = os.path.abspath(args.install_prefix) + tmp_dir = tempfile.mkdtemp(prefix="installsdl_") + clone_dir = os.path.join(tmp_dir, "SDL") + repo_url = "https://github.com/libsdl-org/SDL.git" + + try: + log(f"Cloning SDL2 repository from {repo_url}") + run_command( + [args.git_path, "clone", repo_url, clone_dir], + description="Cloning SDL repository" + ) + + log("Checking out SDL2 branch") + run_command( + [args.git_path, "checkout", "SDL2"], + cwd=clone_dir, + description="Switching to SDL2 branch" + ) + + for build_type in ["Release"]: + log(f"Building SDL2 ({build_type} configuration)") + build_dir = os.path.join(clone_dir, "build", build_type) + os.makedirs(build_dir, exist_ok=True) + + run_command( + [ + args.cmake_path, clone_dir, + "-G", "Ninja", + f"-DCMAKE_BUILD_TYPE={build_type}", + f"-DCMAKE_INSTALL_PREFIX={install_prefix}" + ], + cwd=build_dir, + description=f"Configuring SDL2 ({build_type})" + ) + + run_command( + [args.ninja_path], + cwd=build_dir, + description=f"Building SDL2 ({build_type})" + ) + + run_command( + [args.ninja_path, "install"], + cwd=build_dir, + description=f"Installing SDL2 ({build_type})" + ) + + log("Cleaning up temporary build files") + shutil.rmtree(tmp_dir, ignore_errors=True) + + log(f"Successfully installed SDL2 to {install_prefix}") + return 0 + + except Exception as exc: + log(f"Installation failed: {exc}") + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir, ignore_errors=True) + sys.exit(1) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/misc/scripts/minify_shaders.py b/misc/scripts/minify_shaders.py new file mode 100644 index 000000000..946e68af5 --- /dev/null +++ b/misc/scripts/minify_shaders.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import re +import sys +from pathlib import Path +from setup_utils import log, fail + +def remove_comments(code): + # Strip block comments first. + code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL) + # Strip line comments, but preserve preprocessor directive lines (`///...`), + # which use a comment prefix so the files stay valid WGSL. + out_lines = [] + for line in code.split('\n'): + if line.lstrip().startswith('///'): + out_lines.append(line) + else: + out_lines.append(re.sub(r'//.*$', '', line)) + return '\n'.join(out_lines) + +def remove_empty_lines(code): + lines = [line for line in code.split('\n') if line.strip()] + return '\n'.join(lines) + +def minify_shader_directory(input_dir, output_dir): + input_dir = Path(input_dir) + output_dir = Path(output_dir) + shader_files = list(input_dir.rglob('*.wgsl')) + log(f"Minifying {len(shader_files)} shader files...") + total_original_size = 0 + total_minified_size = 0 + for shader_file in shader_files: + try: + with open(shader_file, 'r', encoding='utf-8') as f: + original_code = f.read() + except Exception as e: + fail(f"Failed to read {shader_file}: {e}") + original_size = len(original_code) + total_original_size += original_size + minified_code = remove_comments(original_code) + minified_code = remove_empty_lines(minified_code) + total_minified_size += len(minified_code) + relative_path = shader_file.relative_to(input_dir) + output_file = output_dir / relative_path + try: + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(output_file, 'w', encoding='utf-8') as f: + f.write(minified_code) + except Exception as e: + fail(f"Failed to write {output_file}: {e}") + total_reduction = (1 - total_minified_size / total_original_size) * 100 if total_original_size > 0 else 0 + log(f"Total: {total_original_size} -> {total_minified_size} bytes ({total_reduction:.1f}% reduction)") + +if __name__ == '__main__': + if len(sys.argv) != 3: + fail("Usage: python minify_shaders.py ") + input_dir = Path(sys.argv[1]) + output_dir = Path(sys.argv[2]) + if not input_dir.exists(): + fail(f"Input directory '{input_dir}' does not exist") + minify_shader_directory(input_dir, output_dir) diff --git a/misc/scripts/serve_wasm.py b/misc/scripts/serve_wasm.py new file mode 100644 index 000000000..7d355126e --- /dev/null +++ b/misc/scripts/serve_wasm.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +import http.server +import socketserver +import os +import sys +import subprocess +import platform +import time +import threading +from pathlib import Path + +SUPPORTED_TARGETS = [ + "wasm-debug", + "wasm-release", + "wasm-publish", +] + + +def auto_detect_build_type(project_root): + build_dirs = {} + + for target in SUPPORTED_TARGETS: + target_dir = project_root / "build" / target / "apps" / "webgpu_app" + if target_dir.exists(): + build_dirs[target] = target_dir.stat().st_mtime + + if not build_dirs: + return None + + most_recent_target = max(build_dirs.items(), key=lambda x: x[1])[0] + return most_recent_target + + +def get_html_file(build_dir): + html_file = build_dir / "webgpu_app.html" + return html_file if html_file.exists() else None + + +def get_css_files(build_dir): + return list(build_dir.glob("*.css")) + + +def open_browser(url): + try: + if platform.system() == "Windows": + subprocess.Popen(["cmd", "/c", "start", url], shell=True) + elif platform.system() == "Darwin": + subprocess.Popen(["open", url]) + else: + subprocess.Popen(["xdg-open", url]) + except Exception as e: + print(f"Could not open browser: {e}") + print(f"Open manually: {url}\n") + + +def serve_wasm(port=8000): + project_root = Path(__file__).parent.parent.parent + + while True: + build_type = auto_detect_build_type(project_root) + if build_type is None: + print("Error: No WebAssembly build found.") + sys.exit(1) + + print(f"Auto-detected build target: {build_type}") + build_dir = project_root / "build" / build_type / "apps" / "webgpu_app" + + html_file = get_html_file(build_dir) + if not html_file: + print(f"Error: No HTML file found in {build_dir}") + sys.exit(1) + + css_files = get_css_files(build_dir) + css_mtimes = {f: f.stat().st_mtime for f in css_files} + + last_mtime = html_file.stat().st_mtime + last_build_type = build_type + pending_change_time = None + should_restart = False + + os.chdir(build_dir) + + class WasmHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + # Disable all caching + self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + super().end_headers() + + extensions_map = { + **http.server.SimpleHTTPRequestHandler.extensions_map, + '.wasm': 'application/wasm', + '.js': 'application/javascript', + } + + def log_message(self, format, *args): + pass + + def monitor_changes(): + nonlocal last_mtime, last_build_type, build_dir, html_file, css_files, css_mtimes, pending_change_time, should_restart + + while True: + time.sleep(1) + + current_build_type = auto_detect_build_type(project_root) + if current_build_type != last_build_type: + print(f"\n[RELOAD] Build type changed: {last_build_type} -> {current_build_type}") + print("Restarting server...\n") + should_restart = True + httpd.shutdown() + return + + if html_file.exists(): + current_mtime = html_file.stat().st_mtime + if current_mtime != last_mtime: + pending_change_time = time.time() + last_mtime = current_mtime + elif pending_change_time is not None: + if time.time() - pending_change_time >= 1.0: + print(f"\n[RELOAD] Build completed, opening browser...") + pending_change_time = None + url = f"http://localhost:{port}/webgpu_app.html" + open_browser(url) + + current_css_files = get_css_files(build_dir) + for css_file in current_css_files: + if css_file.exists(): + current_mtime = css_file.stat().st_mtime + if css_file not in css_mtimes or current_mtime != css_mtimes[css_file]: + pending_change_time = time.time() + css_mtimes[css_file] = current_mtime + + with socketserver.TCPServer(("", port), WasmHandler) as httpd: + url = f"http://localhost:{port}/webgpu_app.html" + print(f"Serving: {build_dir}") + print(f"Monitoring: {html_file.name}") + if css_files: + print(f"Monitoring CSS: {[f.name for f in css_files]}") + print(f"Opening: {url}\n") + + open_browser(url) + + monitor_thread = threading.Thread(target=monitor_changes, daemon=True) + monitor_thread.start() + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") + sys.exit(0) + + if not should_restart: + break + + +if __name__ == "__main__": + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 + serve_wasm(port) diff --git a/misc/scripts/setup_utils.py b/misc/scripts/setup_utils.py new file mode 100644 index 000000000..b737a1b6f --- /dev/null +++ b/misc/scripts/setup_utils.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +# A bunch of utility functions for the setup scripts + +import sys +import subprocess +import urllib.request +import shutil +import zipfile + +# added to all outputs for better clarity in the cmake output +LOG_PREFIX = "----" + + +# immediately flushes a message to stdout +def log(msg): + print(f"{LOG_PREFIX} {msg}", flush=True) + + +# immediately flushes a message to stderr and exits with an errorcode +def fail(msg): + print(f"{LOG_PREFIX} ERROR: {msg}", file=sys.stderr, flush=True) + sys.exit(1) + + +# downloads a file from a url to a destination path with proper logging +def download(url, dest): + log(f"Downloading {url}") + try: + with urllib.request.urlopen(url) as r, open(dest, "wb") as f: + shutil.copyfileobj(r, f) + log(f"Downloaded to {dest}") + except Exception as e: + fail(f"Failed to download {url}: {e}") + + +# extracts a zip file to a destination path with proper logging +def extract_zip(zip_path, extract_to): + log(f"Extracting {zip_path}") + try: + with zipfile.ZipFile(zip_path, "r") as z: + z.extractall(extract_to) + log(f"Extracted to {extract_to}") + except Exception as e: + fail(f"Failed to extract {zip_path}: {e}") + + +# runs a command and logs the output with the LOG_PREFIX +def run_command(cmd, cwd=None, description=None): + if description: + log(description) + + cmd_str = ' '.join(cmd) if isinstance(cmd, list) else cmd + log(f"Running: {cmd_str}" + (f" (cwd={cwd})" if cwd else "")) + + try: + process = subprocess.Popen( + cmd, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1 + ) + + for line in process.stdout: + print(f"{LOG_PREFIX} {line.rstrip()}", flush=True) + + return_code = process.wait() + + if return_code != 0: + fail(f"Command failed with exit code {return_code}: {cmd_str}") + + except FileNotFoundError: + fail(f"Command not found: {cmd[0]}") + except Exception as e: + fail(f"Failed to run command '{cmd_str}': {e}") diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index a50455b78..dcdd609b8 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -19,7 +19,7 @@ # along with this program. If not, see . ############################################################################# -project(alpine-renderer-nucleus LANGUAGES CXX) +project(alpine-renderer-nucleus LANGUAGES C CXX) alp_add_git_repository(stb_slim URL https://github.com/AlpineMapsOrgDependencies/stb_slim.git COMMITISH 547fade2a12793e1bea4733d59646b4f436e25a4) alp_add_git_repository(radix URL https://github.com/AlpineMapsOrg/radix.git COMMITISH e939e10c5a40866950b68a0bc04c851bcfcf5dad NOT_SYSTEM) @@ -29,6 +29,38 @@ if(ALP_ENABLE_LABELS) alp_add_git_repository(vector_tiles URL https://github.com/AlpineMapsOrg/vector-tile.git COMMITISH faba88257716c4bc01ebd44d8b8b98f711ecb78c) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) +set(KTX_FEATURE_TESTS OFF CACHE BOOL "" FORCE) +set(KTX_FEATURE_TOOLS OFF CACHE BOOL "" FORCE) +set(KTX_FEATURE_DOC OFF CACHE BOOL "" FORCE) +set(KTX_FEATURE_JS OFF CACHE BOOL "" FORCE) +set(CMAKE_INSTALL_BINDIR_SAVED ${CMAKE_INSTALL_BINDIR}) +set(CMAKE_INSTALL_BINDIR "." CACHE STRING "" FORCE) +alp_add_git_repository(libktx URL https://github.com/KhronosGroup/KTX-Software.git COMMITISH 952d74f1d53452e4e976a2b7698ff7af6c13a9ed) +set(CMAKE_INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR_SAVED} CACHE STRING "" FORCE) +if (EMSCRIPTEN AND ALP_ENABLE_THREADING) + target_compile_options(ktx PUBLIC -pthread) + target_link_options(ktx PUBLIC -pthread) +endif() +if (EMSCRIPTEN AND TARGET ktx) + get_target_property(KTX_INTERFACE_LINK_OPTIONS ktx INTERFACE_LINK_OPTIONS) + if (KTX_INTERFACE_LINK_OPTIONS) + list(FILTER KTX_INTERFACE_LINK_OPTIONS EXCLUDE REGEX "STACK_SIZE=96kb") + set_target_properties(ktx PROPERTIES INTERFACE_LINK_OPTIONS "${KTX_INTERFACE_LINK_OPTIONS}") + endif() +endif() +if (NOT EMSCRIPTEN) + # NOTE: KTX builds into an additional Release/Debug directory. The following + # moves the builds directory for the targets ktx and ktx_read to the binary dir + foreach(ktx_target ktx ktx_read) + if (TARGET ${ktx_target}) + set_target_properties(${ktx_target} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}" + ) + endif() + endforeach() +endif() add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -62,6 +94,7 @@ qt_add_library(nucleus STATIC AbstractRenderWindow.h event_parameter.h Raster.h + Raster3D.h srs.h srs.cpp tile/utils.h tile/utils.cpp tile/DrawListGenerator.h tile/DrawListGenerator.cpp @@ -99,18 +132,24 @@ qt_add_library(nucleus STATIC timing/TimerInterface.h timing/TimerInterface.cpp timing/CpuTimer.h timing/CpuTimer.cpp utils/ColourTexture.h utils/ColourTexture.cpp + utils/ColourTexture3D.h utils/ColourTexture3D.cpp EngineContext.h EngineContext.cpp track/Manager.h track/Manager.cpp track/GPX.cpp track/GPX.h utils/image_loader.h utils/image_loader.cpp + utils/image_writer.h utils/image_writer.cpp + utils/geopng_decoder.h utils/geopng_decoder.cpp utils/thread.h camera/RecordedAnimation.h camera/RecordedAnimation.cpp camera/recording.h camera/recording.cpp tile/setup.h + tile/GpuTileId.h tile/GpuTileId.cpp tile/GpuArrayHelper.h tile/GpuArrayHelper.cpp tile/TextureScheduler.h tile/TextureScheduler.cpp + tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp tile/GeometryScheduler.h tile/GeometryScheduler.cpp + utils/easing.h utils/error.h utils/lang.h tile/SchedulerDirector.h tile/SchedulerDirector.cpp @@ -147,7 +186,7 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch -target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc) +target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc ktx) qt_add_resources(nucleus "icons" PREFIX "/map_icons" @@ -190,5 +229,7 @@ if (MSVC) # /WX fails with an unreachable code warning/error in zpp_bits.h. the system property doesn't seem to work (even though it appears in the build log as # "-external:ID:\a\renderer\renderer\extern\zppbits -external:W0") else() - target_compile_options(nucleus PUBLIC -Wall -Wextra -pedantic -Werror) + # imgui does not compile with -Werror. so we have to make it private for now. + # better solutions welcome + target_compile_options(nucleus PRIVATE -Wall -Wextra -pedantic -Werror) endif() diff --git a/nucleus/Raster3D.h b/nucleus/Raster3D.h new file mode 100644 index 000000000..cd5b86275 --- /dev/null +++ b/nucleus/Raster3D.h @@ -0,0 +1,110 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2022 Adam Celarek + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Lucas Dworschak + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace nucleus { +template +class Raster3D { + std::vector m_data; + unsigned m_width = 0; + unsigned m_height = 0; + unsigned m_depth = 0; + +public: + Raster3D() = default; + Raster3D(unsigned square_side_length, unsigned depth, std::vector&& vector) + : m_data(std::move(vector)) + , m_width(square_side_length) + , m_height(square_side_length) + , m_depth(depth) + { + assert(m_data.size() == m_width * m_height * m_depth); + } + Raster3D(unsigned square_side_length, unsigned depth) + : m_data(square_side_length * square_side_length * depth) + , m_width(square_side_length) + , m_height(square_side_length) + , m_depth(depth) + { + } + Raster3D(const glm::uvec3& size) + : m_data(size.x * size.y * size.z) + , m_width(size.x) + , m_height(size.y) + , m_depth(size.z) + { + } + Raster3D(const glm::uvec3& size, const T& fill_value) + : m_data(size.x * size.y * size.z, fill_value) + , m_width(size.x) + , m_height(size.y) + , m_depth(size.z) + { + } + + [[nodiscard]] const std::vector& buffer() const { return m_data; } + [[nodiscard]] std::vector& buffer() { return m_data; } + [[nodiscard]] unsigned width() const { return m_width; } + [[nodiscard]] unsigned height() const { return m_height; } + [[nodiscard]] unsigned depth() const { return m_depth; } + [[nodiscard]] glm::uvec3 size() const { return { m_width, m_height, m_depth }; } + [[nodiscard]] size_t size_in_bytes() const { return m_data.size() * sizeof(T); } + [[nodiscard]] size_t size_per_line() const { return m_width * sizeof(T); } + [[nodiscard]] size_t size_per_slice() const { return m_width * m_height * sizeof(T); } + [[nodiscard]] size_t buffer_length() const { return m_data.size(); } + [[nodiscard]] const T& pixel(const glm::uvec3& position) const + { + assert(position.x < m_width); + assert(position.y < m_height); + assert(position.z < m_depth); + return m_data[position.x + m_width * position.y + m_width * m_height * position.z]; + } + [[nodiscard]] T& pixel(const glm::uvec3& position) + { + assert(position.x < m_width); + assert(position.y < m_height); + assert(position.z < m_depth); + return m_data[position.x + m_width * position.y + m_width * m_height * position.z]; + } + [[nodiscard]] const uint8_t* bytes() const { return reinterpret_cast(m_data.data()); } + [[nodiscard]] uint8_t* bytes() { return reinterpret_cast(m_data.data()); } + + void fill(const T& value) { std::fill(begin(), end(), value); } + + auto begin() { return m_data.begin(); } + auto end() { return m_data.end(); } + auto begin() const { return m_data.begin(); } + auto end() const { return m_data.end(); } + auto cbegin() const { return m_data.cbegin(); } + auto cend() const { return m_data.cend(); } + + const T* data() const { return m_data.data(); } + T* data() { return m_data.data(); } +}; +} // namespace nucleus \ No newline at end of file diff --git a/nucleus/camera/Controller.cpp b/nucleus/camera/Controller.cpp index 1c861944e..37fcbdd3c 100644 --- a/nucleus/camera/Controller.cpp +++ b/nucleus/camera/Controller.cpp @@ -111,7 +111,10 @@ void Controller::update() const void Controller::mouse_press(const event_parameter::Mouse& e) { - report_global_cursor_position(QPointF(e.point.position.x, e.point.position.y)); + // Note: This function queries for the position a second time. + // It was meant to report back the position to gui but is not in use so far + // If we reactivate it we should find a way to avoid the unnecessary second query + //report_global_cursor_position({ e.position.x, e.position.y }); if (m_animation_style) { m_animation_style.reset(); diff --git a/nucleus/camera/Definition.cpp b/nucleus/camera/Definition.cpp index e605b828e..5814355cd 100644 --- a/nucleus/camera/Definition.cpp +++ b/nucleus/camera/Definition.cpp @@ -55,6 +55,24 @@ Definition::Definition(const glm::dvec3& position, set_perspective_params(75, m_viewport_size, m_near_clipping); } +Definition Definition::looking_down_at_aabb(const geometry::Aabb<3, double>& aabb, const glm::uvec2& viewport_size) +{ + const auto aabb_size = aabb.size(); + Definition definition = { aabb.centre() + glm::dvec3 { 0, 0, std::max(aabb_size.x, aabb_size.y) }, aabb.centre() }; + definition.set_viewport_size(viewport_size); + return definition; +} + +Definition Definition::looking_down_at_aabb(const geometry::Aabb<2, double>& aabb, const glm::uvec2& viewport_size) +{ + const glm::dvec2 centre = (aabb.min + aabb.max) / 2.0; + const auto size_x = aabb.max.x - aabb.min.x; + const auto size_y = aabb.max.y - aabb.min.y; + Definition definition = { glm::dvec3 { centre.x, centre.y, std::max(size_x, size_y) }, { centre.x, centre.y, 0 } }; + definition.set_viewport_size(viewport_size); + return definition; +} + glm::dmat4 Definition::camera_matrix() const { return glm::inverse(m_camera_transformation); diff --git a/nucleus/camera/Definition.h b/nucleus/camera/Definition.h index c87f0072e..0528ce801 100644 --- a/nucleus/camera/Definition.h +++ b/nucleus/camera/Definition.h @@ -37,6 +37,10 @@ class Definition { public: Definition(); Definition(const glm::dvec3& position, const glm::dvec3& view_at_point); + + // returns a camera position above the centre of the given world-space region, looking straight down fitting the whole region + [[nodiscard]] static Definition looking_down_at_aabb(const radix::geometry::Aabb<3, double>& aabb, const glm::uvec2& viewport_size); + [[nodiscard]] static Definition looking_down_at_aabb(const radix::geometry::Aabb<2, double>& aabb, const glm::uvec2& viewport_size); [[nodiscard]] glm::dmat4 camera_matrix() const; [[nodiscard]] glm::dmat4 model_matrix() const; void set_model_matrix(const glm::dmat4& new_camera_transformation); diff --git a/nucleus/camera/PositionStorage.h b/nucleus/camera/PositionStorage.h index b26ca1d25..499deab58 100644 --- a/nucleus/camera/PositionStorage.h +++ b/nucleus/camera/PositionStorage.h @@ -1,7 +1,8 @@ - /***************************************************************************** +/***************************************************************************** * Alpine Renderer * Copyright (C) 2023 Adam Celarek * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -68,6 +69,20 @@ inline nucleus::camera::Definition schneeberg() return {{coords.x + 2500, coords.y - 100, coords.z + 100}, {coords.x, coords.y, coords.z - 100}}; } +inline nucleus::camera::Definition heiligenblut_popping() +{ + + const auto coords = srs::lat_long_alt_to_world({ 47.05179073901546, 12.81791073526902, 2000 }); + return { { coords }, { coords.x - 1000, coords.y - 500, coords.z - 500 } }; +} + +inline nucleus::camera::Definition heiligenblut_stepping() +{ + const auto look_at = srs::lat_long_alt_to_world({ 47.040076, 12.818552, 1010.33 }); + const auto position = srs::lat_long_alt_to_world({ 47.042571, 12.825959, 1277.64 }); + return { position, look_at }; +} + inline nucleus::camera::Definition karwendel() { const auto coords = srs::lat_long_alt_to_world({47.416665, 11.4666648, 2000}); diff --git a/nucleus/camera/RecordedAnimation.cpp b/nucleus/camera/RecordedAnimation.cpp index c66e757ba..fe24e5f74 100644 --- a/nucleus/camera/RecordedAnimation.cpp +++ b/nucleus/camera/RecordedAnimation.cpp @@ -43,7 +43,7 @@ std::optional nucleus::camera::RecordedAnimation::u const auto fr = *(frame_right_iter); // there are no frames if the camera stops moving. mixing causes a slow drift to the next position. doesn't look good. // const auto mix = float(current_time - fl.msec) / float(fr.msec - fl.msec); - const auto mix = 0.f; + const auto mix = 0.5f; const auto new_matrix = fl.camera_to_world_matrix * double(1 - mix) + fr.camera_to_world_matrix * double(mix); camera.set_model_matrix(new_matrix); diff --git a/nucleus/camera/gesture.h b/nucleus/camera/gesture.h index 1dd03294a..fa2fa30f9 100644 --- a/nucleus/camera/gesture.h +++ b/nucleus/camera/gesture.h @@ -405,9 +405,9 @@ class PinchAndRotateDetector : public Detector { const auto current_angle = current_angles.at(id); auto diff = current_angle - last_angle; if (diff > M_PI) - diff -= 2 * M_PI; + diff -= 2.0f * (float)M_PI; if (diff < -M_PI) - diff += 2 * M_PI; + diff += 2.0f * (float)M_PI; rotation_val += diff; } rotation_val = glm::degrees(rotation_val / m_touch_ids.size()); diff --git a/nucleus/camera/recording.cpp b/nucleus/camera/recording.cpp index b0fbdad16..56696d0d0 100644 --- a/nucleus/camera/recording.cpp +++ b/nucleus/camera/recording.cpp @@ -18,6 +18,20 @@ #include "recording.h" +// NOTE: This smoothing code was a last minute addition for the VIS conference to +// have smooth camera looped animations. It does smooth the the camera definitions +// directly which end up in weird shit if they are far apart from each other. A correct +// way would probably to decompose it and calculate the inbetween frames with quaternions +// I'll still leave this code here but with the: +// TODO: Implement actual/correct camera smoothing +#define SIMPLE_SMOOTHING_ENABLED 0 + +#if SIMPLE_SMOOTHING_ENABLED +#include +#include +#include +#endif + nucleus::camera::recording::Device::Device() { } std::vector nucleus::camera::recording::Device::recording() const { return m_frames; } @@ -41,4 +55,114 @@ void nucleus::camera::recording::Device::start() m_stopwatch.restart(); } -void nucleus::camera::recording::Device::stop() { m_enabled = false; } +void nucleus::camera::recording::Device::stop() +{ + m_enabled = false; + +#if SIMPLE_SMOOTHING_ENABLED + // --- Configuration constants --- + const bool REMOVE_DUPLICATES = true; + const double DUPLICATE_EPSILON = 1e-6; + const bool ADD_LOOP_FRAMES = true; + const int EXTRA_FRAMES = 0; + const int SMOOTH_WINDOW = 60; + const double SMOOTH_STRENGTH = 1.0; + const bool ENABLE_SMOOTHING = true; + + if (m_frames.size() < 3) + return; + + // --- Step 1: Remove consecutive duplicate frames --- + if (REMOVE_DUPLICATES) { + std::vector filtered; + filtered.reserve(m_frames.size()); + + filtered.push_back(m_frames.front()); + uint time_shift = 0; + + for (size_t i = 1; i < m_frames.size(); ++i) { + const glm::dmat4& prev = m_frames[i - 1].camera_to_world_matrix; + const glm::dmat4& curr = m_frames[i].camera_to_world_matrix; + + double diff_sum = 0.0; + for (int r = 0; r < 4; ++r) + for (int c = 0; c < 4; ++c) + diff_sum += std::abs(curr[r][c] - prev[r][c]); + + if (diff_sum < DUPLICATE_EPSILON) { + time_shift += (m_frames[i].msec - m_frames[i - 1].msec); + } else { + Frame adjusted = m_frames[i]; + adjusted.msec -= time_shift; + filtered.push_back(adjusted); + } + } + + m_frames.swap(filtered); + } + + // --- Step 2: Optional loop extension --- + if (ADD_LOOP_FRAMES) { + std::vector extended = m_frames; + const glm::dmat4 first = m_frames.front().camera_to_world_matrix; + const glm::dmat4 last = m_frames.back().camera_to_world_matrix; + const uint base_time = m_frames.back().msec; + const uint delta_t = (m_frames.size() > 1) ? (m_frames.back().msec - m_frames[m_frames.size() - 2].msec) : 16; + + for (int i = 1; i <= EXTRA_FRAMES; ++i) { + double t = double(i) / double(EXTRA_FRAMES + 1); + glm::dmat4 interp; + for (int r = 0; r < 4; ++r) + for (int c = 0; c < 4; ++c) + interp[r][c] = (1.0 - t) * last[r][c] + t * first[r][c]; + extended.push_back({ base_time + i * delta_t, interp }); + } + + m_frames.swap(extended); + } + + // --- Step 3: Optional smoothing --- + if (ENABLE_SMOOTHING) { + const bool CYCLIC_SMOOTHING = ADD_LOOP_FRAMES; + const size_t N = m_frames.size(); + + std::vector smoothed; + smoothed.reserve(N); + + for (size_t i = 0; i < N; ++i) { + glm::dmat4 accum(0.0); + double wsum = 0.0; + + for (int offset = -SMOOTH_WINDOW; offset <= SMOOTH_WINDOW; ++offset) { + int j = static_cast(i) + offset; + + if (CYCLIC_SMOOTHING) { + if (j < 0) + j = static_cast(N) + j; + else if (j >= static_cast(N)) + j -= static_cast(N); + } + + if (j < 0 || j >= static_cast(N)) + continue; + + double dist = double(offset); + double w = std::exp(-0.5 * (dist / SMOOTH_WINDOW) * (dist / SMOOTH_WINDOW)); + accum += w * m_frames[j].camera_to_world_matrix; + wsum += w; + } + + glm::dmat4 blended = accum / wsum; + + glm::dmat4 result; + for (int r = 0; r < 4; ++r) + for (int c = 0; c < 4; ++c) + result[r][c] = (1.0 - SMOOTH_STRENGTH) * m_frames[i].camera_to_world_matrix[r][c] + SMOOTH_STRENGTH * blended[r][c]; + + smoothed.push_back({ m_frames[i].msec, result }); + } + + m_frames.swap(smoothed); + } +#endif +} diff --git a/nucleus/event_parameter.h b/nucleus/event_parameter.h index 30d58aed7..6d6bfd86e 100644 --- a/nucleus/event_parameter.h +++ b/nucleus/event_parameter.h @@ -36,10 +36,10 @@ namespace nucleus::event_parameter { // compatible to QEventPoint::State enum TouchPointState { TouchPointUnknownState = 0x00, - TouchPointPressed = 0x01, - TouchPointMoved = 0x02, + TouchPointPressed = 0x01, + TouchPointMoved = 0x02, TouchPointStationary = 0x04, - TouchPointReleased = 0x08 + TouchPointReleased = 0x08 }; // loosely based on QEventPoint @@ -105,8 +105,7 @@ inline Touch make(QTouchEvent* e) return touch; } -inline Mouse make(QMouseEvent* e) -{ +inline Mouse make(QMouseEvent* e) { Mouse mouse; mouse.is_begin_event = e->isBeginEvent(); mouse.is_end_event = e->isEndEvent(); @@ -117,8 +116,7 @@ inline Mouse make(QMouseEvent* e) return mouse; } -inline Wheel make(QWheelEvent* e) -{ +inline Wheel make(QWheelEvent* e) { Wheel wheel; wheel.is_begin_event = e->isBeginEvent(); wheel.is_end_event = e->isEndEvent(); @@ -129,3 +127,4 @@ inline Wheel make(QWheelEvent* e) } } // namespace nucleus::event_parameter #endif + diff --git a/nucleus/srs.cpp b/nucleus/srs.cpp index 8512b7052..1ac85da2d 100644 --- a/nucleus/srs.cpp +++ b/nucleus/srs.cpp @@ -27,16 +27,48 @@ constexpr double cOriginShift = cEarthCircumference / 2.0; namespace nucleus::srs { +double tile_width(int zoom_level) { return cEarthCircumference / number_of_horizontal_tiles_for_zoom_level(zoom_level); } + +double tile_height(int zoom_level) { return cEarthCircumference / number_of_vertical_tiles_for_zoom_level(zoom_level); } + tile::SrsBounds tile_bounds(const tile::Id& tile) { - const auto width_of_a_tile = cEarthCircumference / number_of_horizontal_tiles_for_zoom_level(tile.zoom_level); - const auto height_of_a_tile = cEarthCircumference / number_of_vertical_tiles_for_zoom_level(tile.zoom_level); + const auto width_of_a_tile = tile_width(tile.zoom_level); + const auto height_of_a_tile = tile_height(tile.zoom_level); glm::dvec2 absolute_min = { -cOriginShift, -cOriginShift }; const auto min = absolute_min + glm::dvec2 { tile.coords.x * width_of_a_tile, tile.coords.y * height_of_a_tile }; const auto max = absolute_min + glm::dvec2 { (tile.coords.x + 1) * width_of_a_tile, (tile.coords.y + 1) * height_of_a_tile }; return { min, max }; } +tile::Id world_xy_to_tile_id(const glm::dvec2& world_xy, unsigned int zoomlevel) +{ + const double width_of_a_tile = cEarthCircumference / number_of_horizontal_tiles_for_zoom_level(zoomlevel); + const double height_of_a_tile = cEarthCircumference / number_of_vertical_tiles_for_zoom_level(zoomlevel); + const glm::dvec2 absolute_min = { -cOriginShift, -cOriginShift }; + const glm::dvec2 tile_size = { width_of_a_tile, height_of_a_tile }; + const glm::dvec2 tile_coords = glm::floor((world_xy - absolute_min) / tile_size); + return { zoomlevel, glm::uvec2(tile_coords) }; +} + +glm::dvec2 tile_id_to_world_xy(const glm::uvec2& coords, unsigned int zoomlevel) +{ + const auto width_of_a_tile = tile_width(zoomlevel); + const auto height_of_a_tile = tile_height(zoomlevel); + glm::dvec2 absolute_min = { -cOriginShift, -cOriginShift }; + return absolute_min + glm::dvec2 { coords.x * width_of_a_tile, coords.y * height_of_a_tile }; +} + +glm::dvec2 world_xy_to_tile_uv(const glm::dvec2& world_xy, unsigned int zoomlevel) +{ + const double width_of_a_tile = cEarthCircumference / number_of_horizontal_tiles_for_zoom_level(zoomlevel); + const double height_of_a_tile = cEarthCircumference / number_of_vertical_tiles_for_zoom_level(zoomlevel); + const glm::dvec2 absolute_min = { -cOriginShift, -cOriginShift }; + const glm::dvec2 tile_size = { width_of_a_tile, height_of_a_tile }; + const glm::dvec2 tile_uv = glm::fract((world_xy - absolute_min) / tile_size); + return { tile_uv }; +} + bool overlap(const tile::Id& a, const tile::Id& b) { const auto& smaller_zoom_tile = (a.zoom_level < b.zoom_level) ? a : b; @@ -118,4 +150,5 @@ tile::Id unpack(const glm::vec<2, uint32_t>& packed) id.coords.y = packed.y & ((1u << (32 - 3)) - 1); return id; } -} + +} // namespace nucleus::srs diff --git a/nucleus/srs.h b/nucleus/srs.h index db6b3eaa7..891ad64c0 100644 --- a/nucleus/srs.h +++ b/nucleus/srs.h @@ -36,7 +36,12 @@ namespace nucleus::srs { inline unsigned number_of_horizontal_tiles_for_zoom_level(unsigned z) { return 1 << z; } inline unsigned number_of_vertical_tiles_for_zoom_level(unsigned z) { return 1 << z; } +double tile_width(int zoom_level); +double tile_height(int zoom_level); tile::SrsBounds tile_bounds(const tile::Id& tile); +tile::Id world_xy_to_tile_id(const glm::dvec2& coords, unsigned int zoomlevel); +glm::dvec2 tile_id_to_world_xy(const glm::uvec2& coords, unsigned int zoomlevel); +glm::dvec2 world_xy_to_tile_uv(const glm::dvec2& world_xy, unsigned int zoomlevel); bool overlap(const tile::Id& a, const tile::Id& b); glm::dvec2 lat_long_to_world(const glm::dvec2& lat_long); diff --git a/nucleus/stb/stb_image.cpp b/nucleus/stb/stb_image.cpp new file mode 100644 index 000000000..d9cf2558a --- /dev/null +++ b/nucleus/stb/stb_image.cpp @@ -0,0 +1,34 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// Limit the dimensions of images to 8192x8192. This is already quite restricting +// in terms of that a lot of GPUs don't support textures that large. Make sure +// you know what you are doing, before you change this value. +#define STBI_MAX_DIMENSIONS 8192 + +// Only include the code for the formats we need. This reduces the code footprint +// and allows for faster compilation times. Add more formats here, if you need them. +// For possible options check the stb_image.h file. +#define STBI_ONLY_JPEG +#define STBI_ONLY_PNG + +// Remove if you intend to use stbi_failure_reason() +#define STBI_NO_FAILURE_STRINGS + +#define STB_IMAGE_IMPLEMENTATION +#include diff --git a/nucleus/stb/stb_image_loader.cpp b/nucleus/stb/stb_image_loader.cpp new file mode 100644 index 000000000..5d19a9be1 --- /dev/null +++ b/nucleus/stb/stb_image_loader.cpp @@ -0,0 +1,67 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "stb_image_loader.h" +#include +#include +#include + +namespace nucleus::stb { + +Raster load_8bit_rgba_image_from_memory(const QByteArray& byteArray) +{ + int width, height, channels; + const int requested_channels = 4; // Request 4 channels to always get RGBA8 images + const stbi_uc* source_data = reinterpret_cast(byteArray.constData()); + unsigned char* data = stbi_load_from_memory( + source_data, + byteArray.size(), + &width, &height, &channels, + requested_channels + ); + + if (data == nullptr) { + throw std::runtime_error("Failed to load image from bytearray"); + } + + // NOTE: We copy the contents of the data pointer into a Raster object. Sadly + // we can't use the allocated memory directly, because for that we would need a custom + // allocator for the std::vector class. + Raster raster(glm::uvec2(width, height)); + memcpy(raster.data(), data, raster.size_in_bytes()); + stbi_image_free(data); + + return raster; +} + +Raster load_8bit_rgba_image_from_file(const QString& filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + throw std::runtime_error("Failed to open file: " + filename.toStdString()); + } + + QByteArray byteArray = file.readAll(); + file.close(); + + // NOTE: We don't use the stb_image loader directly, because QFile can load from ressources + return load_8bit_rgba_image_from_memory(byteArray); +} + + +} // namespace nucleus diff --git a/nucleus/stb/stb_image_loader.h b/nucleus/stb/stb_image_loader.h new file mode 100644 index 000000000..5c3201179 --- /dev/null +++ b/nucleus/stb/stb_image_loader.h @@ -0,0 +1,32 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace nucleus::stb { + +Raster load_8bit_rgba_image_from_memory(const QByteArray& byteArray); + +Raster load_8bit_rgba_image_from_file(const QString& filename); + +} // namespace nucleus + + diff --git a/nucleus/stb/stb_image_write.cpp b/nucleus/stb/stb_image_write.cpp new file mode 100644 index 000000000..c7a52e417 --- /dev/null +++ b/nucleus/stb/stb_image_write.cpp @@ -0,0 +1,21 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// include implementation for stb_image_write.h +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include diff --git a/nucleus/stb/stb_image_writer.cpp b/nucleus/stb/stb_image_writer.cpp new file mode 100644 index 000000000..c017d1690 --- /dev/null +++ b/nucleus/stb/stb_image_writer.cpp @@ -0,0 +1,34 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "stb_image_writer.h" +#include + +#include + +namespace nucleus::stb { + +void write_8bit_rgba_image_to_file_bmp(const QByteArray& data, unsigned int width, unsigned int height, const QString& filename) +{ + assert(width > 0); + assert(height > 0); + + stbi_write_bmp(filename.toUtf8().constData(), width, height, 4, data.data()); +} + +} // namespace nucleus::stb diff --git a/nucleus/stb/stb_image_writer.h b/nucleus/stb/stb_image_writer.h new file mode 100644 index 000000000..d01c03b72 --- /dev/null +++ b/nucleus/stb/stb_image_writer.h @@ -0,0 +1,28 @@ +/***************************************************************************** + * Alpine Maps and weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace nucleus::stb { + +void write_8bit_rgba_image_to_file_bmp(const QByteArray& data, unsigned int width, unsigned int height, const QString& filename); + +} // namespace nucleus::stb diff --git a/nucleus/tile/GpuArrayHelper.cpp b/nucleus/tile/GpuArrayHelper.cpp index 15fa29bbc..8183a85e3 100644 --- a/nucleus/tile/GpuArrayHelper.cpp +++ b/nucleus/tile/GpuArrayHelper.cpp @@ -24,6 +24,7 @@ GpuArrayHelper::GpuArrayHelper() { } unsigned GpuArrayHelper::add_tile(const tile::Id& id) { + // Note (Wendelin): These asserts don't work really, because Qt catches the exception and does weird stuff. Use if + __debugbreak(). assert(!m_id_to_layer.contains(id)); const auto t = std::find(m_array.begin(), m_array.end(), tile::Id { unsigned(-1), {} }); assert(t != m_array.end()); @@ -64,6 +65,11 @@ GpuArrayHelper::LayerInfo GpuArrayHelper::layer(Id tile_id) const return { tile_id, m_id_to_layer.at(tile_id) }; } +bool GpuArrayHelper::contains(Id tile_id) const +{ + return m_id_to_layer.contains(tile_id); +} + GpuArrayHelper::Dictionary GpuArrayHelper::generate_dictionary() const { const auto hash_to_pixel = [](uint16_t hash) { return glm::uvec2(hash & 255, hash >> 8); }; diff --git a/nucleus/tile/GpuArrayHelper.h b/nucleus/tile/GpuArrayHelper.h index 4d0f534eb..b077a667c 100644 --- a/nucleus/tile/GpuArrayHelper.h +++ b/nucleus/tile/GpuArrayHelper.h @@ -44,6 +44,7 @@ class GpuArrayHelper { unsigned int n_occupied() const; Dictionary generate_dictionary() const; LayerInfo layer(Id tile_id) const; + bool contains(Id tile_id) const; private: std::vector m_array; diff --git a/nucleus/tile/GpuTileId.cpp b/nucleus/tile/GpuTileId.cpp new file mode 100644 index 000000000..318907c15 --- /dev/null +++ b/nucleus/tile/GpuTileId.cpp @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "GpuTileId.h" + +namespace nucleus::tile { + +GpuTileId::GpuTileId(uint32_t x, uint32_t y, uint32_t zoomlevel) + : x { x } + , y { y } + , zoomlevel { zoomlevel } +{ +} + +GpuTileId::GpuTileId(const radix::tile::Id& tile_id) + : x { tile_id.coords.x } + , y { tile_id.coords.y } + , zoomlevel { tile_id.zoom_level } +{ +} + +} // namespace nucleus::tile diff --git a/nucleus/tile/GpuTileId.h b/nucleus/tile/GpuTileId.h new file mode 100644 index 000000000..5fe8fc4e9 --- /dev/null +++ b/nucleus/tile/GpuTileId.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "radix/tile.h" +#include +#include + +namespace nucleus::tile { + +struct GpuTileId { + uint32_t x; + uint32_t y; + uint32_t zoomlevel; + uint32_t alignment = std::numeric_limits::max(); + + GpuTileId() = default; + GpuTileId(uint32_t x, uint32_t y, uint32_t zoomlevel); + GpuTileId(const radix::tile::Id& tile_id); + + bool operator==(const GpuTileId& other) const { return x == other.x && y == other.y && zoomlevel == other.zoomlevel; } +}; + +} // namespace nucleus::tile diff --git a/nucleus/tile/QuadAssembler.h b/nucleus/tile/QuadAssembler.h index 0ea775f1d..39d139691 100644 --- a/nucleus/tile/QuadAssembler.h +++ b/nucleus/tile/QuadAssembler.h @@ -1,45 +1,45 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2023 Adam Celarek - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - *****************************************************************************/ - -#pragma once - -#include -#include -#include "types.h" - -namespace nucleus::tile { - -class QuadAssembler : public QObject { - Q_OBJECT - using TileId2QuadMap = std::unordered_map; - - TileId2QuadMap m_quads; - -public: - explicit QuadAssembler(QObject* parent = nullptr); - [[nodiscard]] size_t n_items_in_flight() const; - -public slots: - void load(const tile::Id& tile_id); - void deliver_tile(const Data& tile); - -signals: - void tile_requested(const tile::Id& tile_id); - void quad_loaded(const DataQuad& tile); -}; -} +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2023 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include "types.h" + +namespace nucleus::tile { + +class QuadAssembler : public QObject { + Q_OBJECT + using TileId2QuadMap = std::unordered_map; + + TileId2QuadMap m_quads; + +public: + explicit QuadAssembler(QObject* parent = nullptr); + [[nodiscard]] size_t n_items_in_flight() const; + +public slots: + void load(const tile::Id& tile_id); + void deliver_tile(const Data& tile); + +signals: + void tile_requested(const tile::Id& tile_id); + void quad_loaded(const DataQuad& tile); +}; +} diff --git a/nucleus/tile/Scheduler.cpp b/nucleus/tile/Scheduler.cpp index c2ce0826a..b39466174 100644 --- a/nucleus/tile/Scheduler.cpp +++ b/nucleus/tile/Scheduler.cpp @@ -241,6 +241,19 @@ void Scheduler::set_name(const QString& new_name) m_name = new_name; } +void Scheduler::clear_full_cache() +{ + auto old_gpu_quad_limit = m.gpu_quad_limit; + auto old_ram_quad_limit = m.ram_quad_limit; + set_gpu_quad_limit(0); + set_ram_quad_limit(0); + update_gpu_quads(); + purge_ram_cache(); + persist_tiles(); + set_gpu_quad_limit(old_gpu_quad_limit); + set_ram_quad_limit(old_ram_quad_limit); +} + tl::expected Scheduler::read_disk_cache() { if (m_name == "unnamed" || m_name.isEmpty()) { diff --git a/nucleus/tile/Scheduler.h b/nucleus/tile/Scheduler.h index 1d918a14e..a58239635 100644 --- a/nucleus/tile/Scheduler.h +++ b/nucleus/tile/Scheduler.h @@ -96,6 +96,9 @@ class Scheduler : public QObject { [[nodiscard]] const QString& name() const; void set_name(const QString& new_name); + // a hacky way to clear the gpu/ram and file cache by temporarily setting the limits to 0 + void clear_full_cache(); + signals: void statistics_updated(Statistics stats); void stats_ready(const QString& scheduler_name, const QVariantMap& new_stats); diff --git a/nucleus/tile/Texture3DScheduler.cpp b/nucleus/tile/Texture3DScheduler.cpp new file mode 100644 index 000000000..8478049f9 --- /dev/null +++ b/nucleus/tile/Texture3DScheduler.cpp @@ -0,0 +1,85 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Texture3DScheduler.h" +#include "conversion.h" +#include +#include + + +namespace nucleus::tile { + +Texture3DScheduler::Texture3DScheduler(const Scheduler::Settings& settings) + : nucleus::tile::Scheduler(settings) +{ +} + +Texture3DScheduler::~Texture3DScheduler() = default; + +void Texture3DScheduler::transform_and_emit(const std::vector& new_quads, const std::vector& deleted_quads) +{ + // Stitching 2x2 3d tiles is possible, but it doesn't really make sense (to me). + std::vector new_gpu_tiles; + new_gpu_tiles.reserve(new_quads.size() * 4); + + for (const auto& quad : new_quads) { + for (size_t i = 0; i < 4; i++) { + const auto& tile = quad.tiles[i]; + + if (tile.data->size() == 0) + continue; + + // NOTE: This implementation is quite specific to the cloud texture loading. Not intended for general purpose use. + + GpuTexture3DTile gpu_tile; + gpu_tile.id = tile.id; + auto&& texture = std::make_shared(); + gpu_tile.texture = texture; + + ktxTexture* ktx = nullptr; + ktxTexture_CreateFromMemory(reinterpret_cast(tile.data.get()->data()), tile.data->size(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktx); + + texture->reserve(ktx->numLevels); + ktxTexture_IterateLevelFaces(ktx, [](int /*miplevel*/, int /*face*/, int width, int height, int depth, ktx_uint64_t faceLodSize, void *pixels, void *userdata) { + auto* result = static_cast*>(userdata); + std::span byte_span{static_cast(pixels), static_cast(faceLodSize)}; + result->emplace_back(byte_span, width, height, depth, nucleus::utils::ColourTexture3D::Format::BC4_UNORM); + return KTX_SUCCESS; + }, texture.get()); + + new_gpu_tiles.push_back(gpu_tile); + } + } + + std::vector deleted_tiles; + deleted_tiles.reserve(deleted_quads.size() * 4); + for (const auto & deleted_quad : deleted_quads) { + auto&& children = deleted_quad.children(); + deleted_tiles.push_back(children[0]); + deleted_tiles.push_back(children[1]); + deleted_tiles.push_back(children[2]); + deleted_tiles.push_back(children[3]); + } + + // we are merging the tiles. so deleted quads become deleted tiles. + emit gpu_tiles_updated(deleted_tiles, new_gpu_tiles); +} + + +} // namespace nucleus::tile diff --git a/nucleus/tile/Texture3DScheduler.h b/nucleus/tile/Texture3DScheduler.h new file mode 100644 index 000000000..7b4012709 --- /dev/null +++ b/nucleus/tile/Texture3DScheduler.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Scheduler.h" +#include "nucleus/Raster3D.h" +#include "types.h" + +namespace nucleus::tile { + +class Texture3DScheduler : public Scheduler { + Q_OBJECT +public: + Texture3DScheduler(const Scheduler::Settings& settings); + ~Texture3DScheduler() override; + +signals: + void gpu_tiles_updated(const std::vector& deleted_tiles, const std::vector& new_tiles); + +protected: + void transform_and_emit(const std::vector& new_quads, const std::vector& deleted_quads) override; +}; + +} // namespace nucleus::tile diff --git a/nucleus/tile/TileLoadService.cpp b/nucleus/tile/TileLoadService.cpp index d94a9a8cf..4ba5875e5 100644 --- a/nucleus/tile/TileLoadService.cpp +++ b/nucleus/tile/TileLoadService.cpp @@ -110,3 +110,5 @@ void TileLoadService::set_transfer_timeout(unsigned int new_transfer_timeout) assert(new_transfer_timeout < unsigned(std::numeric_limits::max())); m_transfer_timeout = new_transfer_timeout; } + +void TileLoadService::set_base_url(const QString& base_url) { m_base_url = base_url; } diff --git a/nucleus/tile/TileLoadService.h b/nucleus/tile/TileLoadService.h index b9541d0ba..dba697b9e 100644 --- a/nucleus/tile/TileLoadService.h +++ b/nucleus/tile/TileLoadService.h @@ -45,6 +45,8 @@ class TileLoadService : public QObject { [[nodiscard]] unsigned int transfer_timeout() const; void set_transfer_timeout(unsigned int new_transfer_timeout); + void set_base_url(const QString& base_url); + public slots: void load(const tile::Id& tile_id) const; diff --git a/nucleus/tile/drawing.h b/nucleus/tile/drawing.h index ae8fc90fc..857d1e961 100644 --- a/nucleus/tile/drawing.h +++ b/nucleus/tile/drawing.h @@ -23,7 +23,7 @@ #include namespace nucleus::tile::drawing { -constexpr uint max_n_tiles = 1024; +// constexpr uint max_n_tiles = 1024; std::vector generate_list(const camera::Definition& camera, utils::AabbDecoratorPtr aabb_decorator, unsigned max_zoom_level); std::vector compute_bounds(const std::vector& tiles, utils::AabbDecoratorPtr aabb_decorator); diff --git a/nucleus/tile/setup.h b/nucleus/tile/setup.h index 3348a8032..b9d5de220 100644 --- a/nucleus/tile/setup.h +++ b/nucleus/tile/setup.h @@ -1,6 +1,7 @@ /***************************************************************************** * AlpineMaps.org * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2026 Wendelin Muth * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +23,7 @@ #include "QuadAssembler.h" #include "RateLimiter.h" #include "SlotLimiter.h" +#include "Texture3DScheduler.h" #include "TextureScheduler.h" #include "TileLoadService.h" #include "utils.h" @@ -89,12 +91,13 @@ struct TextureSchedulerHolder { TileLoadServicePtr tile_service; }; -inline TextureSchedulerHolder texture_scheduler(TileLoadServicePtr tile_service, const tile::utils::AabbDecoratorPtr& aabb_decorator, QThread* thread = nullptr) +struct Texture3DSchedulerHolder { + std::shared_ptr scheduler; + TileLoadServicePtr tile_service; +}; + +inline TextureSchedulerHolder texture_scheduler(TileLoadServicePtr tile_service, const tile::utils::AabbDecoratorPtr& aabb_decorator, QThread* thread = nullptr, Scheduler::Settings settings = {.tile_resolution = 256, .max_zoom_level = 20, .gpu_quad_limit = 1024 }) { - Scheduler::Settings settings; - settings.max_zoom_level = 20; - settings.tile_resolution = 256; - settings.gpu_quad_limit = 1024; auto scheduler = std::make_unique(settings); scheduler->set_aabb_decorator(aabb_decorator); @@ -138,6 +141,52 @@ inline TextureSchedulerHolder texture_scheduler(TileLoadServicePtr tile_service, return { std::move(scheduler), std::move(tile_service) }; } + +inline Texture3DSchedulerHolder texture_scheduler_3d(TileLoadServicePtr tile_service, const tile::utils::AabbDecoratorPtr& aabb_decorator, QThread* thread = nullptr, Scheduler::Settings settings = {.tile_resolution = 256, .max_zoom_level = 20, .gpu_quad_limit = 1024 }) +{ + auto scheduler = std::make_unique(settings); + scheduler->set_aabb_decorator(aabb_decorator); + + { + using nucleus::tile::QuadAssembler; + using nucleus::tile::RateLimiter; + using nucleus::tile::SlotLimiter; + using nucleus::tile::TileLoadService; + auto* sch = scheduler.get(); + auto* sl = new SlotLimiter(sch); + auto* rl = new RateLimiter(sch); + auto* qa = new QuadAssembler(sch); + + QObject::connect(sch, &Scheduler::quads_requested, sl, &SlotLimiter::request_quads); + QObject::connect(sl, &SlotLimiter::quad_requested, rl, &RateLimiter::request_quad); + QObject::connect(rl, &RateLimiter::quad_requested, qa, &QuadAssembler::load); + QObject::connect(qa, &QuadAssembler::tile_requested, tile_service.get(), &TileLoadService::load); + QObject::connect(tile_service.get(), &TileLoadService::load_finished, qa, &QuadAssembler::deliver_tile); + + QObject::connect(qa, &QuadAssembler::quad_loaded, sl, &SlotLimiter::deliver_quad); + QObject::connect(sl, &SlotLimiter::quad_delivered, sch, &TextureScheduler::receive_quad); + } + if (QNetworkInformation::loadDefaultBackend() && QNetworkInformation::instance()) { + QNetworkInformation* n = QNetworkInformation::instance(); + scheduler->set_network_reachability(n->reachability()); + QObject::connect(n, &QNetworkInformation::reachabilityChanged, scheduler.get(), &Scheduler::set_network_reachability); + } + + Q_UNUSED(thread); +#ifdef ALP_ENABLE_THREADING +#ifdef __EMSCRIPTEN__ // make request from main thread on webassembly due to QTBUG-109396 + tile_service->moveToThread(QCoreApplication::instance()->thread()); +#else + if (thread) + tile_service->moveToThread(thread); +#endif + if (thread) + scheduler->moveToThread(thread); +#endif + + return { std::move(scheduler), std::move(tile_service) }; +} + inline utils::AabbDecoratorPtr aabb_decorator() { QFile file(":/map/height_data.atb"); diff --git a/nucleus/tile/types.h b/nucleus/tile/types.h index a8beaae9e..ef61f335f 100644 --- a/nucleus/tile/types.h +++ b/nucleus/tile/types.h @@ -18,9 +18,11 @@ #pragma once + #include #include +#include #include #include @@ -92,6 +94,12 @@ struct GpuTextureTile { }; static_assert(NamedTile); +struct GpuTexture3DTile { + tile::Id id; + std::shared_ptr texture; +}; +static_assert(NamedTile); + struct TileBounds { tile::Id id; tile::SrsAndHeightBounds bounds = {}; diff --git a/nucleus/track/GPX.cpp b/nucleus/track/GPX.cpp index 257aa6ce1..e811d17e4 100644 --- a/nucleus/track/GPX.cpp +++ b/nucleus/track/GPX.cpp @@ -301,4 +301,15 @@ void reduce_point_count(std::vector& points, float threshold) } } +BoundingBox compute_world_aabb(const Gpx& gpx) +{ + BoundingBox aabb { glm::dvec3(std::numeric_limits::max()), glm::dvec3(std::numeric_limits::min()) }; + for (const auto& segment : gpx.track) { + for (const auto& point : segment) { + aabb.expand_by(srs::lat_long_alt_to_world({ point.latitude, point.longitude, point.elevation })); + } + } + return aabb; +} + } // namespace nucleus::track diff --git a/nucleus/track/GPX.h b/nucleus/track/GPX.h index ebd51c21a..42b4f17bd 100644 --- a/nucleus/track/GPX.h +++ b/nucleus/track/GPX.h @@ -19,6 +19,7 @@ #pragma once +#include "nucleus/srs.h" #include #include #include @@ -42,6 +43,7 @@ struct Point { using Segment = std::vector; using Type = std::vector; +using BoundingBox = radix::geometry::Aabb<3, double>; struct Gpx { Type track; @@ -77,6 +79,8 @@ std::vector to_world_points(const Gpx& gpx); std::vector to_world_points(const track::Segment& segment); +BoundingBox compute_world_aabb(const Gpx& gpx); + // for rendering with GL_TRIANGLE_STRIP std::vector triangle_strip_ribbon(const std::vector& points, float width); diff --git a/nucleus/utils/ColourTexture3D.cpp b/nucleus/utils/ColourTexture3D.cpp new file mode 100644 index 000000000..5beefc841 --- /dev/null +++ b/nucleus/utils/ColourTexture3D.cpp @@ -0,0 +1,38 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ColourTexture3D.h" + +nucleus::utils::ColourTexture3D::ColourTexture3D(const nucleus::Raster3D& image, Format format) + : m_data(reinterpret_cast(image.data()), reinterpret_cast(image.data()) + image.size_in_bytes()) + , m_width(unsigned(image.width())) + , m_height(unsigned(image.height())) + , m_depth(unsigned(image.depth())) + , m_format(format) +{ +} + +nucleus::utils::ColourTexture3D::ColourTexture3D(std::span data, unsigned width, unsigned height, unsigned depth, Format format) + : m_data(data.begin(), data.end()) + , m_width(width) + , m_height(height) + , m_depth(depth) + , m_format(format) +{ +} \ No newline at end of file diff --git a/nucleus/utils/ColourTexture3D.h b/nucleus/utils/ColourTexture3D.h new file mode 100644 index 000000000..3f2e6eab2 --- /dev/null +++ b/nucleus/utils/ColourTexture3D.h @@ -0,0 +1,55 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "nucleus/Raster3D.h" + +#include +#include +#include + +namespace nucleus::utils { + +class ColourTexture3D { +public: + enum class Format { R8_UNORM, BC4_UNORM }; + +private: + std::vector m_data; + unsigned m_width = 0; + unsigned m_height = 0; + unsigned m_depth = 0; + Format m_format = Format::R8_UNORM; + +public: + explicit ColourTexture3D(const nucleus::Raster3D& data, Format format); + ColourTexture3D(std::span data, unsigned width, unsigned height, unsigned depth, Format format); + [[nodiscard]] const uint8_t* data() const { return m_data.data(); } + [[nodiscard]] size_t n_bytes() const { return m_data.size(); } + [[nodiscard]] unsigned width() const { return m_width; } + [[nodiscard]] unsigned height() const { return m_height; } + [[nodiscard]] unsigned depth() const { return m_depth; } + [[nodiscard]] Format format() const { return m_format; } +}; + +using MipmappedColourTexture3D = std::vector; + +} // namespace nucleus::utils diff --git a/nucleus/utils/easing.h b/nucleus/utils/easing.h new file mode 100644 index 000000000..cd39d8170 --- /dev/null +++ b/nucleus/utils/easing.h @@ -0,0 +1,71 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +// Easing functions from https://easings.net/ + +namespace nucleus::utils::easing { + +inline float easeInQuad(float x) { return x * x; } + +inline float easeOutQuad(float x) { return 1.0f - (1.0f - x) * (1.0f - x); } + +inline float easeOutElastic(float x) +{ + constexpr float c4 = (2.0f * 3.14159265359f) / 3.0f; + if (x == 0.0f) + return 0.0f; + if (x == 1.0f) + return 1.0f; + return std::pow(2.0f, -10.0f * x) * std::sin((x * 10.0f - 0.75f) * c4) + 1.0f; +} + +inline float easeInElastic(float x) +{ + constexpr float c4 = (2.0f * 3.14159265359f) / 3.0f; + if (x == 0.0f) + return 0.0f; + if (x == 1.0f) + return 1.0f; + return -std::pow(2.0f, 10.0f * x - 10.0f) * std::sin((x * 10.0f - 10.75f) * c4); +} + +inline float easeOutBounce(float x) +{ + constexpr float n1 = 7.5625f; + constexpr float d1 = 2.75f; + if (x < 1.0f / d1) { + return n1 * x * x; + } else if (x < 2.0f / d1) { + x -= 1.5f / d1; + return n1 * x * x + 0.75f; + } else if (x < 2.5f / d1) { + x -= 2.25f / d1; + return n1 * x * x + 0.9375f; + } else { + x -= 2.625f / d1; + return n1 * x * x + 0.984375f; + } +} + +inline float easeInBounce(float x) { return 1.0f - easeOutBounce(1.0f - x); } + +} // namespace nucleus::utils::easing diff --git a/nucleus/utils/geopng_decoder.cpp b/nucleus/utils/geopng_decoder.cpp new file mode 100644 index 000000000..d53c75a56 --- /dev/null +++ b/nucleus/utils/geopng_decoder.cpp @@ -0,0 +1,123 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "geopng_decoder.h" + +#include "image_writer.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::utils::geopng { + +std::vector possible_aabb_paths(const std::filesystem::path& image_path) +{ + const auto dir = image_path.parent_path(); + const std::string stem = image_path.stem().string(); + + std::vector candidates; + candidates.push_back(dir / (stem + "_aabb.txt")); + + const size_t us = stem.find('_'); + if (us != std::string::npos) { + auto track_candidate = dir / (stem.substr(0, us) + "_aabb.txt"); + if (track_candidate != candidates.front()) + candidates.push_back(std::move(track_candidate)); + } + + candidates.push_back(dir / "aabb.txt"); + return candidates; +} + +tl::expected, std::string> load_aabb_from_file(const std::filesystem::path& file_path) +{ + const std::string path_str = file_path.string(); + + QFile aabb_file(QString::fromStdString(path_str)); + if (!aabb_file.open(QIODevice::ReadOnly)) { + return tl::make_unexpected("Failed to open file " + path_str); + } + QTextStream file_contents(&aabb_file); + + std::array contents; + bool float_conversion_ok = false; + for (size_t i = 0; i < contents.size(); i++) { + QString line = file_contents.readLine(); + contents[i] = line.toFloat(&float_conversion_ok); + if (!float_conversion_ok) { + return tl::make_unexpected("Failed to parse file " + path_str + ": Could not convert \"" + line.toStdString() + "\" to float"); + } + } + + if (contents[0] >= contents[2]) { + return tl::make_unexpected("Failed to parse file " + path_str + ": x_min (" + std::to_string(contents[0]) + ") must not be >= x_max (" + std::to_string(contents[2]) + ")"); + } + + if (contents[1] >= contents[3]) { + return tl::make_unexpected("Failed to parse file " + path_str + ": y_min (" + std::to_string(contents[1]) + ") must not be >= y_max (" + std::to_string(contents[3]) + ")"); + } + + return radix::geometry::Aabb<2, double> { { contents[0], contents[1] }, { contents[2], contents[3] } }; +} + +void write_encoded_float_png(const Raster& data, const QString& filename) +{ + constexpr float range = ENCODED_FLOAT_RANGE_MAX - ENCODED_FLOAT_RANGE_MIN; + Raster out(glm::uvec2(data.width(), data.height())); + for (size_t i = 0; i < data.buffer().size(); ++i) { + const float clamped = std::clamp(data.buffer()[i], ENCODED_FLOAT_RANGE_MIN, ENCODED_FLOAT_RANGE_MAX); + const uint32_t packed = static_cast((clamped - ENCODED_FLOAT_RANGE_MIN) / range * static_cast(std::numeric_limits::max())); + out.buffer()[i] = glm::u8vec4((packed >> 24) & 0xFF, (packed >> 16) & 0xFF, (packed >> 8) & 0xFF, packed & 0xFF); + } + image_writer::rgba8_as_png(out, filename); +} + +glm::vec2 scan_encoded_float_range(const Raster& image, bool& likely_encoded_float) +{ + constexpr float range = ENCODED_FLOAT_RANGE_MAX - ENCODED_FLOAT_RANGE_MIN; + float min_val = std::numeric_limits::max(); + float max_val = std::numeric_limits::lowest(); + size_t zero_count = 0; + + for (const glm::u8vec4& px : image) { + const uint32_t packed = (uint32_t(px.x) << 24) | (uint32_t(px.y) << 16) | (uint32_t(px.z) << 8) | uint32_t(px.w); + if (packed == 0) + continue; + const float value = ENCODED_FLOAT_RANGE_MIN + (float(packed) / float(std::numeric_limits::max())) * range; + if (std::abs(value) < 0.01f) + ++zero_count; + min_val = std::min(min_val, value); + max_val = std::max(max_val, value); + } + + const size_t total = image.buffer().size(); + const bool has_range = min_val <= max_val; + const bool many_zeros = total > 0 && float(zero_count) / float(total) >= 0.01f; + const bool same_sign = has_range && (min_val >= 0.0f || max_val <= 0.0f); + likely_encoded_float = many_zeros || same_sign; + + return has_range ? glm::vec2(min_val, max_val) : glm::vec2(ENCODED_FLOAT_RANGE_MIN, ENCODED_FLOAT_RANGE_MAX); +} + +} // namespace nucleus::utils::geopng diff --git a/nucleus/utils/geopng_decoder.h b/nucleus/utils/geopng_decoder.h new file mode 100644 index 000000000..ef29911db --- /dev/null +++ b/nucleus/utils/geopng_decoder.h @@ -0,0 +1,61 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QString; + +namespace nucleus::utils::geopng { + +// Encoding range for the u32 linear float-in-RGBA encoding. +inline constexpr float ENCODED_FLOAT_RANGE_MIN = -10000.0f; +inline constexpr float ENCODED_FLOAT_RANGE_MAX = 10000.0f; + +// Returns candidate AABB .txt file paths for the given geo-PNG image path. +// Tries in order: +// - %filename%_aabb.txt +// - %filename before first _%_aabb.txt +// - aabb.txt +std::vector possible_aabb_paths(const std::filesystem::path& image_path); + +// Parses a sidecar AABB .txt file describing the world-space extent of a geo-PNG. +// The file contains exactly four lines: min_x, min_y, max_x, max_y +// Returns the parsed AABB, or an error message on failure +tl::expected, std::string> load_aabb_from_file(const std::filesystem::path& file_path); + +// Encodes a Raster as a geo-PNG: each float is clamped to +// [ENCODED_FLOAT_RANGE_MIN, ENCODED_FLOAT_RANGE_MAX], mapped to [0,1], packed +// as a u32, and stored across the R,G,B,A channels of a u8 PNG. +void write_encoded_float_png(const Raster& data, const QString& filename); + +// Scans an RGBA-encoded float image. Returns {min, max} of decoded values. +// Determines whether its likely_encoded_float with the heuristic that either: +// - >= 1% of decoded float values are approx. 0.0 +// - all decoded values share the same sign +glm::vec2 scan_encoded_float_range(const Raster& image, bool& likely_encoded_float); + +} // namespace nucleus::utils::geopng diff --git a/nucleus/utils/image_writer.cpp b/nucleus/utils/image_writer.cpp new file mode 100644 index 000000000..0fb55f7c8 --- /dev/null +++ b/nucleus/utils/image_writer.cpp @@ -0,0 +1,66 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "image_writer.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION // Enable implementation +#include + +#include +#include + +namespace nucleus::utils::image_writer { + +void rgba8_as_png(const Raster& data, const QString& filename) +{ + assert(data.width() > 0); + assert(data.height() > 0); + + int result = stbi_write_png(filename.toUtf8().constData(), // File name + data.width(), // Image width + data.height(), // Image height + 4, // Number of color components (e.g., 2 for grayscale with alpha) + data.data(), // Pointer to the image data + data.width() * 4 // Stride in bytes (width * number of components) + ); + + if (result == 0) { + qCritical() << "Failed to write image" << filename; + } +} + +void rgba8_as_png(const QByteArray& data, const glm::uvec2& resolution, const QString& filename) +{ + assert(resolution.x > 0); + assert(resolution.y > 0); + assert(data.size() == static_cast(resolution.x * resolution.y * 4)); // Ensure data size matches resolution + + int result = stbi_write_png(filename.toUtf8().constData(), // File name + static_cast(resolution.x), // Image width + static_cast(resolution.y), // Image height + 4, // Number of color components (RGBA) + data.constData(), // Pointer to the image data + static_cast(resolution.x * 4) // Stride in bytes (width * number of components) + ); + + if (result == 0) { + qCritical() << "Failed to write image" << filename; + } +} + +} // namespace nucleus::utils::image_writer diff --git a/nucleus/utils/image_writer.h b/nucleus/utils/image_writer.h new file mode 100644 index 000000000..1b9db3624 --- /dev/null +++ b/nucleus/utils/image_writer.h @@ -0,0 +1,30 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace nucleus::utils::image_writer { + +void rgba8_as_png(const Raster& data, const QString& filename); + +void rgba8_as_png(const QByteArray& data, const glm::uvec2& resolution, const QString& filename); + +} // namespace nucleus::utils::image_writer diff --git a/nucleus/utils/terrain_mesh_index_generator.h b/nucleus/utils/terrain_mesh_index_generator.h index 6dad4c337..075dd495f 100644 --- a/nucleus/utils/terrain_mesh_index_generator.h +++ b/nucleus/utils/terrain_mesh_index_generator.h @@ -56,11 +56,11 @@ std::vector surface_quads(unsigned vertex_side_length) for (size_t row = 0; row < height - 1; row++) { for (size_t col = 0; col < width; col++) { - indices.push_back(index_for(row, col)); - indices.push_back(index_for(row + 1, col)); + indices.push_back(Index(index_for(row, col))); + indices.push_back(Index(index_for(row + 1, col))); } - indices.push_back(index_for(row + 1, width - 1)); - indices.push_back(index_for(row + 1, 0)); + indices.push_back(Index(index_for(row + 1, width - 1))); + indices.push_back(Index(index_for(row + 1, 0))); } indices.resize(indices.size() - 2); return indices; @@ -79,22 +79,22 @@ std::vector surface_quads_with_curtains(unsigned vertex_side_length) const auto first_curtain_index = curtain_index; for (size_t row = height - 1; row >= 1; row--) { - indices.push_back(index_for(row, width - 1)); + indices.push_back(Index(index_for(row, width - 1))); indices.push_back(curtain_index++); } for (size_t col = width - 1; col >= 1; col--) { - indices.push_back(index_for(0, col)); + indices.push_back(Index(index_for(0, col))); indices.push_back(curtain_index++); } for (size_t row = 0; row < height - 1; row++) { - indices.push_back(index_for(row, 0)); + indices.push_back(Index(index_for(row, 0))); indices.push_back(curtain_index++); } for (size_t col = 0; col < width - 1; col++) { - indices.push_back(index_for(height - 1, col)); + indices.push_back(Index(index_for(height - 1, col))); indices.push_back(curtain_index++); } indices.push_back(index_for(height - 1, width - 1)); diff --git a/nucleus/vector_tile/types.h b/nucleus/vector_tile/types.h index bbc058366..a03fcc302 100644 --- a/nucleus/vector_tile/types.h +++ b/nucleus/vector_tile/types.h @@ -35,7 +35,7 @@ struct PointOfInterest { public: enum class Type { Unknown = 0, Peak, Settlement, AlpineHut, Webcam, NumberOfElements }; Q_ENUM(Type) - uint64_t id = -1; + uint64_t id = UINT64_MAX; Type type = Type::Unknown; QString name; glm::dvec3 lat_long_alt = glm::dvec3(0); diff --git a/unittests/CMakeLists.txt b/unittests/CMakeLists.txt index 2b050595a..57ce02303 100644 --- a/unittests/CMakeLists.txt +++ b/unittests/CMakeLists.txt @@ -26,3 +26,7 @@ add_subdirectory(nucleus) if (ALP_ENABLE_GL_ENGINE) add_subdirectory(gl_engine) endif() + +if (ALP_WEBGPU_ENGINE) + add_subdirectory(webgpu_engine) +endif() diff --git a/unittests/webgpu_engine/CMakeLists.txt b/unittests/webgpu_engine/CMakeLists.txt new file mode 100644 index 000000000..4193edd9c --- /dev/null +++ b/unittests/webgpu_engine/CMakeLists.txt @@ -0,0 +1,51 @@ +############################################################################# +# Alpine Terrain Renderer +# Copyright (C) 2023 Adam Celarek +# Copyright (C) 2023 Gerald Kimmersdorfer +# Copyright (C) 2024 Patrick Komon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +project(alpine-renderer-unittests_webgpu_engine LANGUAGES CXX) + +alp_add_unittest(unittests_webgpu_engine + UnittestWebgpuContext.h UnittestWebgpuContext.cpp + test_GpuShaderFunctions.cpp + test_ShaderPreprocessor.cpp +) + +target_link_libraries(unittests_webgpu_engine PUBLIC webgpu_engine) + +# Copy necessary DLLs to the output directory on Windows +if (WIN32 AND NOT EMSCRIPTEN) + # Copy Qt DLLs + add_custom_command(TARGET unittests_webgpu_engine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + COMMENT "Copying Qt6Core DLL to unittests" + ) + + # Copy SDL2 DLL + if (EXISTS "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll") + add_custom_command(TARGET unittests_webgpu_engine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl/bin/SDL2.dll" + "$" + COMMENT "Copying SDL2.dll to unittests" + ) + endif() +endif() + diff --git a/unittests/webgpu_engine/UnittestWebgpuContext.cpp b/unittests/webgpu_engine/UnittestWebgpuContext.cpp new file mode 100644 index 000000000..b396ef9dd --- /dev/null +++ b/unittests/webgpu_engine/UnittestWebgpuContext.cpp @@ -0,0 +1,138 @@ +/***************************************************************************** + * Alpine Renderer + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "UnittestWebgpuContext.h" +#include "webgpu/base/webgpu_interface.hpp" +#include +#include +#include +#include + +WGPULimits UnittestWebgpuContext::default_limits() +{ + WGPULimits required_limits {}; + WGPULimits supported_limits {}; + wgpuAdapterGetLimits(adapter, &supported_limits); + // irrelevant for us, but needs to be set + required_limits.minStorageBufferOffsetAlignment = supported_limits.minStorageBufferOffsetAlignment; + required_limits.minUniformBufferOffsetAlignment = supported_limits.minUniformBufferOffsetAlignment; + required_limits.maxInterStageShaderVariables = WGPU_LIMIT_U32_UNDEFINED; // required for current version of Chrome Canary (2025-04-03) + return required_limits; +} + +void webgpu_device_error_callback( + [[maybe_unused]] const WGPUDevice* device, WGPUErrorType type, WGPUStringView message, [[maybe_unused]] void* userdata1, [[maybe_unused]] void* userdata2) +{ + const char* typeStr = "Unknown"; + switch (type) { + case WGPUErrorType_NoError: typeStr = "NoError"; break; + case WGPUErrorType_Validation: typeStr = "Validation"; break; + case WGPUErrorType_OutOfMemory: typeStr = "OutOfMemory"; break; + case WGPUErrorType_Internal: typeStr = "Internal"; break; + case WGPUErrorType_Unknown: typeStr = "Unknown"; break; + default: break; + } + + std::cout << "WebGPU Error [" << typeStr << "]: " << std::string_view(message.data, message.length) << std::endl; +} + +void webgpu_device_lost_callback([[maybe_unused]] const WGPUDevice* device, + WGPUDeviceLostReason reason, + WGPUStringView message, + [[maybe_unused]] void* userdata1, + [[maybe_unused]] void* userdata2) +{ + const char* typeStr = "Unknown"; + switch (reason) { + case WGPUDeviceLostReason_Unknown: typeStr = "Unknown"; break; + case WGPUDeviceLostReason_Destroyed: typeStr = "Destroyed"; break; + case WGPUDeviceLostReason_CallbackCancelled: typeStr = "CallbackCancelled"; break; + case WGPUDeviceLostReason_FailedCreation: typeStr = "FailedCreation"; break; + default: break; + } + + std::cout << "WebGPU Device Lost [" << typeStr << "]: " << std::string_view(message.data, message.length) << std::endl; +} + +UnittestWebgpuContext::UnittestWebgpuContext(bool use_default_limits, WGPULimits required_limits) +{ + instance_desc = {}; + instance_desc.nextInChain = nullptr; + +#ifndef __EMSCRIPTEN__ + WGPUDawnTogglesDescriptor dawnToggles; + dawnToggles.chain.next = nullptr; + dawnToggles.chain.sType = WGPUSType_DawnTogglesDescriptor; + + std::vector enabledToggles = { "allow_unsafe_apis" }; + + dawnToggles.enabledToggles = enabledToggles.data(); + dawnToggles.enabledToggleCount = enabledToggles.size(); + dawnToggles.disabledToggleCount = 0; + + instance_desc.nextInChain = &dawnToggles.chain; +#endif + + const auto timed_wait_feature = WGPUInstanceFeatureName_TimedWaitAny; + instance_desc.requiredFeatureCount = 1; + instance_desc.requiredFeatures = &timed_wait_feature; + + instance = wgpuCreateInstance(&instance_desc); + assert(instance); + + WGPURequestAdapterOptions adapter_opts {}; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + adapter_opts.compatibleSurface = nullptr; + adapter = webgpu::requestAdapterSync(instance, adapter_opts); + assert(adapter); + + std::vector requiredFeatures; + requiredFeatures.push_back(WGPUFeatureName_TimestampQuery); + + WGPUDeviceDescriptor device_desc {}; + device_desc.label = WGPUStringView { .data = "webigeo device", .length = WGPU_STRLEN }; + device_desc.requiredFeatures = requiredFeatures.data(); + device_desc.requiredFeatureCount = (uint32_t)requiredFeatures.size(); + if (use_default_limits) + required_limits = default_limits(); + device_desc.requiredLimits = &required_limits; + device_desc.defaultQueue.label = WGPUStringView { .data = "webigeo queue", .length = WGPU_STRLEN }; + device_desc.uncapturedErrorCallbackInfo = WGPUUncapturedErrorCallbackInfo { + .nextInChain = nullptr, + .callback = webgpu_device_error_callback, + .userdata1 = nullptr, + .userdata2 = nullptr, + }; + device_desc.deviceLostCallbackInfo = WGPUDeviceLostCallbackInfo { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = webgpu_device_lost_callback, + .userdata1 = nullptr, + .userdata2 = nullptr, + }; + + device = webgpu::requestDeviceSync(instance, adapter, device_desc); + assert(device); + + queue = wgpuDeviceGetQueue(device); + assert(queue); + + ctx.init(instance, device, adapter, nullptr, queue); + ctx.resource_registry().recreate_all(device); +} diff --git a/unittests/webgpu_engine/UnittestWebgpuContext.h b/unittests/webgpu_engine/UnittestWebgpuContext.h new file mode 100644 index 000000000..d1a69f17e --- /dev/null +++ b/unittests/webgpu_engine/UnittestWebgpuContext.h @@ -0,0 +1,39 @@ +/***************************************************************************** + * Alpine Renderer + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "webgpu/webgpu.h" +#include + +struct UnittestWebgpuContext { + WGPULimits default_limits(); + + UnittestWebgpuContext(bool use_default_limits = true, WGPULimits required_limits = {}); + + WGPUInstanceDescriptor instance_desc; + + WGPUInstance instance = nullptr; + WGPUSurface surface = nullptr; + WGPUAdapter adapter = nullptr; + WGPUDevice device = nullptr; + WGPUQueue queue = nullptr; + + webgpu::Context ctx; +}; diff --git a/unittests/webgpu_engine/test_GpuShaderFunctions.cpp b/unittests/webgpu_engine/test_GpuShaderFunctions.cpp new file mode 100644 index 000000000..783df97b9 --- /dev/null +++ b/unittests/webgpu_engine/test_GpuShaderFunctions.cpp @@ -0,0 +1,203 @@ +/***************************************************************************** + * Alpine Renderer + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "UnittestWebgpuContext.h" +#include "webgpu/base/webgpu_interface.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_CASE("encoder functions") +{ + UnittestWebgpuContext context; + + SECTION("octahedron normal encoding") + { + // Tests octahedron normal encoding/decoding by generating 214 test normals (14 edge cases + 200 random), + // encoding them to 2x16-bit integers via compute shader, decoding back, and verifying each decoded + // normal is within epsilon of the original. + const int random_normals_count = 200; + + const char* wgsl_single_thread_octahedron_test = R"( + ///use webgpu::encoder + + @group(0) @binding(0) var input_buffer: array; + @group(0) @binding(1) var output_buffer: array; + + @compute @workgroup_size(1) + fn computeMain(@builtin(global_invocation_id) id: vec3) { + let input_size = u32(input_buffer[0].w); + + // Initialize index 0 as success (it contains metadata, not a normal) + output_buffer[0] = 1u; + + // Go through all normals and encode/decode them and see if the result is similar + // Write to the output_buffer a 1 if the encoding/decoding was successful, otherwise 0 + for (var i: u32 = 1u; i < input_size; i++) { + let normal = input_buffer[i].xyz; + let encoded = octNormalEncode2u16(normal); + let decoded = octNormalDecode2u16(encoded); + + if (length(normal - decoded) <= 0.001) { // Threshold for floating point comparison + output_buffer[i] = 1u; // Success + } else { + output_buffer[i] = 0u; // Failure + } + } + } + )"; + + // ==== GENERATE RANDOM TEST SET WITH ADDITIONAL EDGE CASES ==== + std::vector test_normals_buffer_data; + { + std::vector testNormals = { glm::vec3(1, 0, 0), + glm::vec3(0, 1, 0), + glm::vec3(0, 0, 1), + glm::vec3(1, 1, 0), + glm::vec3(1, 0, 1), + glm::vec3(0, 1, 1), + glm::vec3(1, 1, 1), + glm::vec3(-1, 0, 0), + glm::vec3(0, -1, 0), + glm::vec3(0, 0, -1), + glm::vec3(-1, -1, 0), + glm::vec3(-1, 0, -1), + glm::vec3(0, -1, -1), + glm::vec3(-1, -1, -1) }; + + for (size_t i = 0; i < random_normals_count; i++) { + testNormals.push_back(glm::ballRand(1.0f)); // Generates a random point inside a unit sphere + } + + // Normalize and write to buffer data array. Vec4 is used as vec3 causes alignment issues! Again: NEVER USE VEC3 + // Store the total count in the w component of all elements + const float total_count = static_cast(testNormals.size()); + for (size_t i = 0; i < testNormals.size(); i++) { + test_normals_buffer_data.push_back(glm::vec4(glm::normalize(testNormals[i]), total_count)); + } + } + + const webgpu::raii::CommandEncoder encoder(context.device, {}); + + const auto output_buffer = std::make_unique>( + context.device, WGPUBufferUsage_Storage | WGPUBufferUsage_CopySrc, test_normals_buffer_data.size(), "output buffer"); + const auto input_buffer = std::make_unique>( + context.device, WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, test_normals_buffer_data.size(), "input buffer"); + + // Upload the test data to the input buffer + input_buffer->write(context.queue, test_normals_buffer_data.data(), test_normals_buffer_data.size(), 0); + + // ==== CREATE BINDING LAYOUT AND BIND GROUP ==== + WGPUBindGroupLayoutEntry compute_input_binding {}; + compute_input_binding.binding = 0; + compute_input_binding.visibility = WGPUShaderStage_Compute; + compute_input_binding.buffer.type = WGPUBufferBindingType_Storage; + compute_input_binding.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry compute_output_binding {}; + compute_output_binding.binding = 1; + compute_output_binding.visibility = WGPUShaderStage_Compute; + compute_output_binding.buffer.type = WGPUBufferBindingType_Storage; + compute_output_binding.buffer.minBindingSize = 0; + + auto compute_bind_group_layout = std::make_unique( + context.device, std::vector { compute_input_binding, compute_output_binding }, "octahedron test bind group layout"); + + std::vector bindgroup_entries = { WGPUBindGroupEntry { .nextInChain = nullptr, + .binding = 0, + .buffer = input_buffer->handle(), + .offset = 0, + .size = input_buffer->size_in_byte(), + .sampler = nullptr, + .textureView = nullptr }, + WGPUBindGroupEntry { .nextInChain = nullptr, + .binding = 1, + .buffer = output_buffer->handle(), + .offset = 0, + .size = output_buffer->size_in_byte(), + .sampler = nullptr, + .textureView = nullptr } }; + + auto compute_bind_group + = std::make_unique(context.device, *compute_bind_group_layout, bindgroup_entries, "octahedron test bindgroup"); + + // ==== CREATE SHADER MODULE AND PIPELINE ==== + std::unique_ptr compute_shader_module + = context.ctx.resource_registry().compile_shader_from_code(context.device, wgsl_single_thread_octahedron_test, "oct shader"); + + auto compute_pipeline = std::make_unique( + context.device, *compute_shader_module, std::vector { compute_bind_group_layout.get() }); + + // ==== RUN THE COMPUTE PIPELINE ==== + // NOTE: Needs to be in a separate scope to ensure the encoder is finished before submitting + { + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), {}); + const glm::uvec3& workgroup_counts = { 1, 1, 1 }; + compute_pipeline->set_binding(0, *compute_bind_group.get()); + compute_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "octahedron test command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(context.queue, 1, &command); + wgpuCommandBufferRelease(command); + + // ==== WAIT FOR THE WORK TO BE DONE AND FOR BUFFERS TO BE MAPPED ==== + bool done = false; + WGPUQueueWorkDoneStatus work_done_status = WGPUQueueWorkDoneStatus_Success; + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata1, void* userdata2) { + *reinterpret_cast(userdata2) = status; + *reinterpret_cast(userdata1) = true; + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = &done, + .userdata2 = &work_done_status, + }; + + WGPUFuture work_done_future = wgpuQueueOnSubmittedWorkDone(context.queue, callback_info); + WGPUFutureWaitInfo wait_info { .future = work_done_future, .completed = false }; + WGPUWaitStatus wait_status = wgpuInstanceWaitAny(context.instance, 1, &wait_info, 1000 * 1000 * 1000); + REQUIRE(wait_status == WGPUWaitStatus_Success); + REQUIRE(work_done_status == WGPUQueueWorkDoneStatus_Success); + + std::vector output; + output_buffer->read_back_sync(context.instance, context.device, output); + REQUIRE(output.size() == test_normals_buffer_data.size()); + + int failed_normals = 0; + for (size_t i = 0; i < output.size(); i++) { + if (output[i] != 1) { + failed_normals++; + // std::cout << "Normal encoding/decoding failed for normal index " << i << ": " << glm::to_string(glm::vec3(test_normals_buffer_data[i])) << + // std::endl; + } + } + CHECK(failed_normals == 0); // None of the normals should have failed the encoding/decoding process + } +} diff --git a/unittests/webgpu_engine/test_ShaderPreprocessor.cpp b/unittests/webgpu_engine/test_ShaderPreprocessor.cpp new file mode 100644 index 000000000..bd3472df4 --- /dev/null +++ b/unittests/webgpu_engine/test_ShaderPreprocessor.cpp @@ -0,0 +1,756 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace webgpu::util; + +// Helper function to set up error callback for tests +inline void setup_error_callback(ShaderPreprocessor& preprocessor) +{ + preprocessor.set_error_callback([](const std::string& message) { FAIL(message); }); +} + +// Helper class to create a mock file system for testing +class MockFileSystem { +public: + void add_file(const std::string& name, const std::string& content) { m_files[name] = content; } + + std::string read_file(const std::string& name) const + { + auto it = m_files.find(name); + if (it == m_files.end()) { + throw std::runtime_error("File not found: " + name); + } + return it->second; + } + +private: + std::map m_files; +}; + +TEST_CASE("ShaderPreprocessor - Include Statement Tests") +{ + ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + MockFileSystem fs; + + SECTION("Simple include") + { + fs.add_file("header", "// Header content\nfn helper() -> f32 { return 1.0; }"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +// Main file +///use header + +fn main() -> f32 { + return helper(); +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("fn helper() -> f32") != std::string::npos); + REQUIRE(result.find("fn main() -> f32") != std::string::npos); + REQUIRE(result.find("#include") == std::string::npos); + } + + SECTION("Multiple includes of the same file (pragma once behavior)") + { + fs.add_file("common", "const PI: f32 = 3.14159;"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +///use common +///use common +///use common + +fn area(r: f32) -> f32 { + return PI * r * r; +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + // Count occurrences of "const PI" + size_t count = 0; + size_t pos = 0; + while ((pos = result.find("const PI", pos)) != std::string::npos) { + count++; + pos += 8; + } + + REQUIRE(count == 1); // Should only appear once due to pragma once behavior + } + + SECTION("Nested includes with pragma once") + { + fs.add_file("constants", "const E: f32 = 2.71828;"); + fs.add_file("math_utils", "///use constants\nfn exp_approx(x: f32) -> f32 { return E; }"); + fs.add_file("physics", "///use constants\nfn decay(t: f32) -> f32 { return E; }"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +///use math_utils +///use physics +///use constants + +fn main() {} +)"; + + std::string result = preprocessor.preprocess_code(input); + + // Count occurrences of "const E" + size_t count = 0; + size_t pos = 0; + while ((pos = result.find("const E", pos)) != std::string::npos) { + count++; + pos += 7; + } + + REQUIRE(count == 1); // Should only appear once even though included multiple times + } + + SECTION("Circular include detection") + { + fs.add_file("a", "///use b\nfn a() {}"); + fs.add_file("b", "///use c\nfn b() {}"); + fs.add_file("c", "///use a\nfn c() {}"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = "///use a"; + + // Should not throw or infinite loop - pragma once behavior should prevent cycles + std::string result; + REQUIRE_NOTHROW(result = preprocessor.preprocess_code(input)); + + // Verify all three files are included (each once) + REQUIRE(result.find("fn a()") != std::string::npos); + REQUIRE(result.find("fn b()") != std::string::npos); + REQUIRE(result.find("fn c()") != std::string::npos); + } + + SECTION("Include with path separators") + { + fs.add_file("util/helpers", "fn utility() -> i32 { return 42; }"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +///use util/helpers + +fn test() -> i32 { + return utility(); +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("fn utility()") != std::string::npos); + REQUIRE(result.find("#include") == std::string::npos); + } +} + +TEST_CASE("ShaderPreprocessor - Conditional Compilation Tests") +{ + ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + + SECTION("Simple ifdef - symbol defined") + { + preprocessor.define("FEATURE_ENABLED"); + + std::string input = R"( +fn main() { +///ifdef FEATURE_ENABLED + let x = 1.0; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let x = 1.0;") != std::string::npos); + REQUIRE(result.find("///ifdef") == std::string::npos); + REQUIRE(result.find("///endif") == std::string::npos); + } + + SECTION("Simple ifdef - symbol not defined") + { + std::string input = R"( +fn main() { +///ifdef FEATURE_DISABLED + let x = 1.0; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let x = 1.0;") == std::string::npos); + REQUIRE(result.find("///ifdef") == std::string::npos); + } + + SECTION("Simple ifndef - symbol not defined") + { + std::string input = R"( +fn main() { +///ifndef DEBUG_MODE + let optimized = true; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let optimized = true;") != std::string::npos); + REQUIRE(result.find("///ifndef") == std::string::npos); + } + + SECTION("Simple ifndef - symbol defined") + { + preprocessor.define("DEBUG_MODE"); + + std::string input = R"( +fn main() { +///ifndef DEBUG_MODE + let optimized = true; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let optimized = true;") == std::string::npos); + } + + SECTION("ifdef with else - true branch") + { + preprocessor.define("USE_FAST_PATH"); + + std::string input = R"( +fn compute() { +///ifdef USE_FAST_PATH + let result = fast_compute(); +///else + let result = slow_compute(); +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("fast_compute()") != std::string::npos); + REQUIRE(result.find("slow_compute()") == std::string::npos); + REQUIRE(result.find("///ifdef") == std::string::npos); + REQUIRE(result.find("///else") == std::string::npos); + } + + SECTION("ifdef with else - false branch") + { + std::string input = R"( +fn compute() { +///ifdef USE_FAST_PATH + let result = fast_compute(); +///else + let result = slow_compute(); +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("fast_compute()") == std::string::npos); + REQUIRE(result.find("slow_compute()") != std::string::npos); + } + + SECTION("Nested ifdef") + { + preprocessor.define("OUTER_FEATURE"); + preprocessor.define("INNER_FEATURE"); + + std::string input = R"( +fn main() { +///ifdef OUTER_FEATURE + let outer = 1; +///ifdef INNER_FEATURE + let inner = 2; +///endif + let outer_end = 3; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let outer = 1;") != std::string::npos); + REQUIRE(result.find("let inner = 2;") != std::string::npos); + REQUIRE(result.find("let outer_end = 3;") != std::string::npos); + } + + SECTION("Nested ifdef - outer false") + { + preprocessor.define("INNER_FEATURE"); + + std::string input = R"( +fn main() { +///ifdef OUTER_FEATURE + let outer = 1; +///ifdef INNER_FEATURE + let inner = 2; +///endif + let outer_end = 3; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + // Outer is false, so nothing inside should be included + REQUIRE(result.find("let outer = 1;") == std::string::npos); + REQUIRE(result.find("let inner = 2;") == std::string::npos); + REQUIRE(result.find("let outer_end = 3;") == std::string::npos); + } + + SECTION("Multiple independent conditionals") + { + preprocessor.define("FEATURE_A"); + preprocessor.define("FEATURE_C"); + + std::string input = R"( +fn main() { +///ifdef FEATURE_A + let a = 1; +///endif + +///ifdef FEATURE_B + let b = 2; +///endif + +///ifdef FEATURE_C + let c = 3; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let a = 1;") != std::string::npos); + REQUIRE(result.find("let b = 2;") == std::string::npos); + REQUIRE(result.find("let c = 3;") != std::string::npos); + } + + SECTION("Undefine symbol") + { + preprocessor.define("TEMPORARY"); + REQUIRE(preprocessor.is_defined("TEMPORARY")); + + preprocessor.undefine("TEMPORARY"); + REQUIRE_FALSE(preprocessor.is_defined("TEMPORARY")); + } +} + +TEST_CASE("ShaderPreprocessor - Combined Include and Conditional Tests") +{ + ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + MockFileSystem fs; + + SECTION("Include file with conditionals") + { + preprocessor.define("ENABLE_SHADOWS"); + + fs.add_file("lighting", R"( +///ifdef ENABLE_SHADOWS +fn compute_shadows() -> f32 { return 0.5; } +///else +fn compute_shadows() -> f32 { return 1.0; } +///endif +)"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +///use lighting + +fn main() { + let shadow = compute_shadows(); +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("return 0.5;") != std::string::npos); + REQUIRE(result.find("return 1.0;") == std::string::npos); + } + + SECTION("Conditional include") + { + preprocessor.define("USE_ADVANCED_MATH"); + + fs.add_file("basic_math", "fn add(a: f32, b: f32) -> f32 { return a + b; }"); + fs.add_file("advanced_math", "fn multiply(a: f32, b: f32) -> f32 { return a * b; }"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + std::string input = R"( +///ifdef USE_ADVANCED_MATH +///use advanced_math +///else +///use basic_math +///endif + +fn main() {} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("multiply") != std::string::npos); + REQUIRE(result.find("add") == std::string::npos); + } +} + +TEST_CASE("ShaderPreprocessor - Error Handling") +{ + auto test_error = [](const std::string& input) { + ShaderPreprocessor preprocessor; + bool error_called = false; + preprocessor.set_error_callback([&error_called](const std::string&) { error_called = true; }); + preprocessor.preprocess_code(input); + REQUIRE(error_called); + }; + + SECTION("Unclosed ifdef") { test_error("fn main() {\n///ifdef FEATURE\n let x = 1;\n}\n"); } + + SECTION("endif without ifdef") { test_error("fn main() {\n let x = 1;\n///endif\n}\n"); } + + SECTION("else without ifdef") { test_error("fn main() {\n///else\n let x = 1;\n///endif\n}\n"); } +} + +TEST_CASE("ShaderPreprocessor - Cache Management") +{ + ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + MockFileSystem fs; + + fs.add_file("cacheable", "const VALUE: i32 = 42;"); + + preprocessor.set_file_reader([&fs](const std::string& name) { return fs.read_file(name); }); + + SECTION("Cache behavior") + { + std::string input = "///use cacheable"; + + // First preprocess - should cache the file + std::string result1 = preprocessor.preprocess_code(input); + REQUIRE(result1.find("const VALUE") != std::string::npos); + + // Modify the file in the mock filesystem + fs.add_file("cacheable", "const VALUE: i32 = 99;"); + + // Second preprocess - should use cached version + std::string result2 = preprocessor.preprocess_code(input); + REQUIRE(result2.find("const VALUE: i32 = 42;") != std::string::npos); + + // Clear cache + preprocessor.clear_cache(); + + // Third preprocess - should read new version + std::string result3 = preprocessor.preprocess_code(input); + REQUIRE(result3.find("const VALUE: i32 = 99;") != std::string::npos); + } +} + +TEST_CASE("ShaderPreprocessor - Define and Macro Replacement Tests") +{ + ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + + SECTION("///define with value and macro replacement") + { + std::string input = R"( +///define PI 3.14159 +///define RADIUS 10.0 + +fn area() -> f32 { + return PI * RADIUS * RADIUS; +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("3.14159") != std::string::npos); + REQUIRE(result.find("10.0") != std::string::npos); + REQUIRE(result.find("PI") == std::string::npos); + REQUIRE(result.find("RADIUS") == std::string::npos); + REQUIRE(result.find("///define") == std::string::npos); + } + + SECTION("///define without value defaults to 1") + { + std::string input = R"( +///define ENABLED + +fn test() -> i32 { + return ENABLED; +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("return 1;") != std::string::npos); + REQUIRE(result.find("ENABLED") == std::string::npos); + } + + SECTION("///if with value comparison") + { + std::string input = R"( +///define MODUS 1 + +fn main() { +///if MODUS 1 + let x = 1.0; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let x = 1.0;") != std::string::npos); + } + + SECTION("///if with value comparison - false") + { + std::string input = R"( +///define MODUS 1 + +fn main() { +///if MODUS 2 + let x = 1.0; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let x = 1.0;") == std::string::npos); + } + + SECTION("///if with ///elif chain") + { + std::string input = R"( +///define MODUS 2 + +fn main() { +///if MODUS 1 + let msg = first; +///elif MODUS 2 + let msg = second; +///elif MODUS 3 + let msg = third; +///else + let msg = default; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let msg = second;") != std::string::npos); + REQUIRE(result.find("let msg = first;") == std::string::npos); + REQUIRE(result.find("let msg = third;") == std::string::npos); + REQUIRE(result.find("let msg = default;") == std::string::npos); + } + + SECTION("///elif falls through to ///else when no match") + { + std::string input = R"( +///define MODUS 99 + +fn main() { +///if MODUS 1 + let msg = first; +///elif MODUS 2 + let msg = second; +///else + let msg = default; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let msg = default;") != std::string::npos); + REQUIRE(result.find("let msg = first;") == std::string::npos); + REQUIRE(result.find("let msg = second;") == std::string::npos); + } + + SECTION("Combined ///ifdef and ///define") + { + std::string input = R"( +///define PI 3.14159 +///define MODUS 1 + +fn main() { +///ifdef PI + let pi = PI; +///endif +///if MODUS 1 + let mode = MODUS; +///else + let mode = 0; +///endif +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let pi = 3.14159;") != std::string::npos); + REQUIRE(result.find("let mode = 1;") != std::string::npos); + REQUIRE(result.find("let mode = 0;") == std::string::npos); + } + + SECTION("Global define vs local define") + { + preprocessor.define("GLOBAL_VAR", "100"); + + std::string input = R"( +///define LOCAL_VAR 200 + +fn test() -> i32 { + return GLOBAL_VAR + LOCAL_VAR; +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("return 100 + 200;") != std::string::npos); + + // Local define should not persist + std::string input2 = R"( +fn test2() -> i32 { + return GLOBAL_VAR; +} +)"; + + std::string result2 = preprocessor.preprocess_code(input2); + + REQUIRE(result2.find("return 100;") != std::string::npos); + REQUIRE(result2.find("LOCAL_VAR") == std::string::npos); + } + + SECTION("Local define overrides global define") + { + preprocessor.define("VALUE", "global"); + + std::string input = R"( +///define VALUE local + +fn test() { + let v = VALUE; +} +)"; + + std::string result = preprocessor.preprocess_code(input); + + REQUIRE(result.find("let v = local;") != std::string::npos); + REQUIRE(result.find("global") == std::string::npos); + } +} + +TEST_CASE("ShaderPreprocessor - Benchmark") +{ + namespace fs = std::filesystem; + + fs::path shader_dir = ALP_SHADER_DIR_WEBGPU_ENGINE; + + if (!fs::exists(shader_dir)) { + SKIP("Shader directory not found - skipping benchmark"); + } + + std::vector shader_files; + + // collect shader files in the root directory only, addressed by their logical + // name "webgpu_engine::" (extension omitted), matching the new scheme. + for (const auto& entry : fs::directory_iterator(shader_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".wgsl") { + shader_files.push_back("webgpu_engine::" + entry.path().stem().string()); + } + } + + if (shader_files.empty()) { + SKIP("No shader files found - skipping benchmark"); + } + + // Mimics RenderResourceRegistry::read_shader_source: strip the "target::" prefix + // and resolve ".wgsl" against the shader directory. + auto create_file_reader = [&shader_dir]() { + return [&shader_dir](const std::string& name) -> std::string { + std::string relpath = name; + if (const auto sep = name.find("::"); sep != std::string::npos) + relpath = name.substr(sep + 2); + fs::path file_path = shader_dir / (relpath + ".wgsl"); + std::ifstream file(file_path); + if (!file.is_open()) { + throw std::runtime_error("File not found: " + name); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + }; + }; + + BENCHMARK("Preprocess all shaders (cache enabled)") + { + static ShaderPreprocessor preprocessor; + setup_error_callback(preprocessor); + preprocessor.set_file_reader(create_file_reader()); + + size_t total = 0; + for (const auto& shader_file : shader_files) { + total += preprocessor.preprocess_file(shader_file).size(); + } + return total; + }; + + BENCHMARK("Preprocess all shaders (cache disabled)") + { + ShaderPreprocessor preprocessor; + preprocessor.set_cache_enabled(false); + preprocessor.set_file_reader(create_file_reader()); + + size_t total = 0; + for (const auto& shader_file : shader_files) { + total += preprocessor.preprocess_file(shader_file).size(); + } + return total; + }; +} diff --git a/webgpu/base/Buffer.h b/webgpu/base/Buffer.h new file mode 100644 index 000000000..b68f76be5 --- /dev/null +++ b/webgpu/base/Buffer.h @@ -0,0 +1,51 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +namespace webgpu { + +/// Generic class for buffers that are backed by a member variable. +template +class Buffer { +public: + // Creates a Buffer object representing a region in GPU memory. + Buffer(WGPUDevice device, WGPUBufferUsage flags) + : m_raw_buffer(device, flags, 1) + { + } + + // Refills the GPU Buffer + void update_gpu_data(WGPUQueue queue) { m_raw_buffer.write(queue, &data, 1, 0); } + + const webgpu::raii::RawBuffer& raw_buffer() const { return m_raw_buffer; } + +public: + // Contains the buffer data + T data; + +protected: + webgpu::raii::RawBuffer m_raw_buffer; +}; + +} // namespace webgpu diff --git a/webgpu/base/CMakeLists.txt b/webgpu/base/CMakeLists.txt new file mode 100644 index 000000000..72da0f01d --- /dev/null +++ b/webgpu/base/CMakeLists.txt @@ -0,0 +1,322 @@ +############################################################################# +# weBIGeo +# Copyright (C) 2024 Gerald Kimmersdorfer +# Copyright (C) 2025 Patrick Komon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +project(alpine-renderer-webgpu LANGUAGES C CXX) + +# NOTE: The idea behind this target is to accumulate all the webgpu related things that are platform +# dependent such that we only have the webgpu cmake target for the other projects to link against. + +set(ALP_DAWN_VERSION "20260420.230544") # Version of Dawn to download (see https://github.com/google/dawn/releases) +set(ALP_DAWN_DIR "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/dawn") +set(ALP_SDL_INSTALL_DIR "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/sdl") +set(ALP_DAWN_PORT_DIR "${CMAKE_SOURCE_DIR}/${ALP_EXTERN_DIR}/emdawnwebgpu_pkg/emdawnwebgpu.port.py") + +set(SOURCES + + raii/base_types.h + raii/Texture.h raii/Texture.cpp + raii/TextureWithSampler.h raii/TextureWithSampler.cpp + raii/RawBuffer.h + Buffer.h + raii/TextureView.h raii/TextureView.cpp + raii/Sampler.h raii/Sampler.cpp + raii/BindGroup.h raii/BindGroup.cpp + raii/BindGroupLayout.h raii/BindGroupLayout.cpp + raii/Pipeline.h raii/Pipeline.cpp + raii/PipelineLayout.h raii/PipelineLayout.cpp + raii/CombinedComputePipeline.h raii/CombinedComputePipeline.cpp + raii/RenderPassEncoder.h raii/RenderPassEncoder.cpp + + util/VertexBufferInfo.h util/VertexBufferInfo.cpp + util/VertexFormat.h + util/string_cast.h util/string_cast.cpp + util/ShaderPreprocessor.h util/ShaderPreprocessor.cpp + + timing/GuiTimerManager.h timing/GuiTimerManager.cpp + timing/TimerInterface.h timing/TimerInterface.cpp + timing/CpuTimer.h timing/CpuTimer.cpp + timing/WebGpuTimer.h timing/WebGpuTimer.cpp + + Framebuffer.h Framebuffer.cpp + + Context.h + RenderResourceRegistry.h RenderResourceRegistry.cpp + gpu_utils.h gpu_utils.cpp + + webgpu_interface.hpp webgpu_interface.cpp) + +add_library(webgpu STATIC ${SOURCES}) + +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +function(alp_find_local_dawn_package out_var) + set(ALP_DAWN_INSTALL_DIR "${ALP_DAWN_DIR}/install/${ALP_DAWN_CONFIG}") + set(ALP_DAWN_VERSION_FILE "${ALP_DAWN_INSTALL_DIR}/.alp_dawn_version") + if (NOT EXISTS "${ALP_DAWN_VERSION_FILE}") + set(${out_var} "" PARENT_SCOPE) + return() + endif() + + file(READ "${ALP_DAWN_VERSION_FILE}" ALP_DAWN_INSTALLED_VERSION) + string(STRIP "${ALP_DAWN_INSTALLED_VERSION}" ALP_DAWN_INSTALLED_VERSION) + if (NOT ALP_DAWN_INSTALLED_VERSION STREQUAL ALP_DAWN_VERSION) + set(${out_var} "" PARENT_SCOPE) + return() + endif() + + foreach(ALP_DAWN_LIB_DIR lib lib64) + set(ALP_DAWN_PACKAGE_CANDIDATE "${ALP_DAWN_INSTALL_DIR}/${ALP_DAWN_LIB_DIR}/cmake/Dawn") + if (EXISTS "${ALP_DAWN_PACKAGE_CANDIDATE}/DawnConfig.cmake") + set(${out_var} "${ALP_DAWN_PACKAGE_CANDIDATE}" PARENT_SCOPE) + return() + endif() + endforeach() + set(${out_var} "" PARENT_SCOPE) +endfunction() + +function(alp_mark_local_dawn_package) + foreach(ALP_DAWN_MARK_CONFIG Debug Release) + set(ALP_DAWN_MARK_DIR "${ALP_DAWN_DIR}/install/${ALP_DAWN_MARK_CONFIG}") + if (EXISTS "${ALP_DAWN_MARK_DIR}") + file(WRITE "${ALP_DAWN_MARK_DIR}/.alp_dawn_version" "${ALP_DAWN_VERSION}\n") + endif() + endforeach() +endfunction() + +if (EMSCRIPTEN) + + # Ensure Dawn port exists, otherwise fetch it + if (NOT EXISTS "${ALP_DAWN_PORT_DIR}") + message(STATUS "Dawn port not found, fetching...") + + execute_process( + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_SOURCE_DIR}/misc/scripts/fetch_dawn_port.py" + --dawn-version "${ALP_DAWN_VERSION}" + --extern-dir "${ALP_EXTERN_DIR}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE FETCH_RESULT + ) + + if (NOT FETCH_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to fetch Dawn Emscripten port.") + endif() + endif() + + + + target_compile_options(webgpu PUBLIC + "--use-port=sdl2" + "--use-port=${ALP_DAWN_PORT_DIR}" + ) + + + target_link_options(webgpu PUBLIC + "-sASYNCIFY=1" + "--use-port=sdl2" + "--use-port=${ALP_DAWN_PORT_DIR}" + ) + +else() + + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(ALP_DAWN_CONFIG "Debug") + else() + set(ALP_DAWN_CONFIG "Release") + endif() + + if (WIN32) + set(ALP_DAWN_NATIVE_PLATFORM "windows-latest") + elseif(APPLE) + set(ALP_DAWN_NATIVE_PLATFORM "macos-latest") + else() + set(ALP_DAWN_NATIVE_PLATFORM "ubuntu-latest") + endif() + + alp_find_local_dawn_package(ALP_DAWN_CMAKE_PACKAGE) + + if (NOT ALP_DAWN_CMAKE_PACKAGE) + message(STATUS "Dawn installation missing - fetching native Dawn package...") + execute_process( + COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/misc/scripts/fetch_dawn_native.py" + --dawn-version "${ALP_DAWN_VERSION}" + --extern-dir "${ALP_EXTERN_DIR}" + --build-type "${ALP_DAWN_CONFIG}" + --platform "${ALP_DAWN_NATIVE_PLATFORM}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE DAWN_FETCH_RESULT + ) + + if (NOT DAWN_FETCH_RESULT EQUAL 0) + message(STATUS "Fetching native Dawn package failed - running install_dawn.py...") + + if (NOT DEFINED ALP_CMAKE_EXECUTABLE) + find_program(ALP_CMAKE_EXECUTABLE cmake) + if (NOT ALP_CMAKE_EXECUTABLE) + message(FATAL_ERROR "cmake not found in PATH") + endif() + endif() + + if (NOT DEFINED ALP_NINJA_EXECUTABLE) + find_program(ALP_NINJA_EXECUTABLE ninja) + if (NOT ALP_NINJA_EXECUTABLE) + message(FATAL_ERROR "ninja not found in PATH") + endif() + endif() + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/misc/scripts/install_dawn.py" + --dawn-version ${ALP_DAWN_VERSION} + --extern-dir "${ALP_EXTERN_DIR}" + --cmake-path "${ALP_CMAKE_EXECUTABLE}" + --ninja-path "${ALP_NINJA_EXECUTABLE}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE DAWN_RESULT + ) + + if (NOT DAWN_RESULT EQUAL 0) + message(FATAL_ERROR "install_dawn.py failed.") + endif() + endif() + + alp_mark_local_dawn_package() + alp_find_local_dawn_package(ALP_DAWN_CMAKE_PACKAGE) + endif() + + if (NOT ALP_DAWN_CMAKE_PACKAGE) + message(FATAL_ERROR "Dawn ${ALP_DAWN_VERSION} was not found in ${ALP_DAWN_DIR}/install/${ALP_DAWN_CONFIG}.") + endif() + + set(Dawn_DIR "${ALP_DAWN_CMAKE_PACKAGE}") + find_package(Dawn REQUIRED CONFIG PATHS "${ALP_DAWN_CMAKE_PACKAGE}" NO_DEFAULT_PATH) + target_link_libraries(webgpu PUBLIC dawn::webgpu_dawn) + + # Ensure SDL is installed + if (NOT EXISTS "${ALP_SDL_INSTALL_DIR}") + message(STATUS "SDL not found - running install_sdl.py...") + + if (NOT DEFINED ALP_CMAKE_EXECUTABLE) + find_program(ALP_CMAKE_EXECUTABLE cmake) + if (NOT ALP_CMAKE_EXECUTABLE) + message(FATAL_ERROR "cmake not found") + endif() + endif() + + if (NOT DEFINED ALP_NINJA_EXECUTABLE) + find_program(ALP_NINJA_EXECUTABLE ninja) + if (NOT ALP_NINJA_EXECUTABLE) + message(FATAL_ERROR "ninja not found") + endif() + endif() + + if (NOT DEFINED ALP_GIT_EXECUTABLE) + find_program(ALP_GIT_EXECUTABLE git) + if (NOT ALP_GIT_EXECUTABLE) + message(FATAL_ERROR "git not found") + endif() + endif() + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/misc/scripts/install_sdl.py" + --install-prefix "${ALP_SDL_INSTALL_DIR}" + --cmake-path "${ALP_CMAKE_EXECUTABLE}" + --ninja-path "${ALP_NINJA_EXECUTABLE}" + --git-path "${ALP_GIT_EXECUTABLE}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE SDL_RESULT + ) + if (NOT SDL_RESULT EQUAL 0) + message(FATAL_ERROR "install_sdl.py failed.") + endif() + endif() + + # find and add SDL2 (you may install it to the default install folder or ALP_SDL_INSTALL_DIR) + # Add ALP_SDL_INSTALL_DIR to CMake's search path for find_package + list(APPEND CMAKE_PREFIX_PATH "${ALP_SDL_INSTALL_DIR}") + find_package(SDL2 REQUIRED) + target_link_libraries(webgpu PUBLIC SDL2::SDL2 SDL2::SDL2main) + + # TODO verify whether this is still needed and adapt accordingly + # If tint_lang_hlsl_writer.lib within ${ALP_DAWN_DIR}/build/debug/src exists DAWN was compiled with DX-Backends. + file(GLOB_RECURSE TINT_LIB_PATH "${ALP_DAWN_DIR}/out/Debug/src/tint/tint_lang_hlsl_writer.lib") + if (TINT_LIB_PATH) + # NOTE: With Direct3D Backends we additionally need to link against dx. + message(STATUS "Found tint_lang_hlsl_writer.lib, DAWN seems to be compiled with DX-Backends. Will link against dxguid.lib.") + find_library(DXGUID_LIB dxguid.lib) + if (DXGUID_LIB) + target_link_libraries(webgpu PUBLIC ${DXGUID_LIB}) + else() + message(FATAL_ERROR "dxguid.lib not found.") + endif() + endif() +endif() + +# set public Dawn backend flags +target_compile_definitions(webgpu PUBLIC IMGUI_IMPL_WEBGPU_BACKEND_DAWN WEBGPU_BACKEND_DAWN) + +# --- WGSL shaders owned by the webgpu target: shared util/* helpers + the mipmap shader --- +if (ALP_ENABLE_WGSL_MINIFICATION) + set(WEBGPU_SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") + set(WEBGPU_SHADER_MINIFIED_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders_minified") + set(MINIFY_SCRIPT "${CMAKE_SOURCE_DIR}/misc/scripts/minify_shaders.py") + + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${WEBGPU_SHADER_SOURCE_DIR}" "${WEBGPU_SHADER_MINIFIED_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE MINIFY_RESULT + ) + if(NOT MINIFY_RESULT EQUAL 0) + message(FATAL_ERROR "Shader minification failed") + endif() + + file(GLOB_RECURSE WEBGPU_SHADER_SOURCES "${WEBGPU_SHADER_SOURCE_DIR}/*.wgsl") + add_custom_target(minify_webgpu_shaders + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${WEBGPU_SHADER_SOURCE_DIR}" "${WEBGPU_SHADER_MINIFIED_DIR}" + DEPENDS ${WEBGPU_SHADER_SOURCES} ${MINIFY_SCRIPT} + COMMENT "Minifying webgpu shaders" + VERBATIM + ) + add_dependencies(webgpu minify_webgpu_shaders) + + set(WEBGPU_SHADER_BASE_DIR "${WEBGPU_SHADER_MINIFIED_DIR}") +else() + set(WEBGPU_SHADER_BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") +endif() + +qt_add_resources(webgpu "webgpu_shaders" + PREFIX "/shaders/webgpu" + BASE "${WEBGPU_SHADER_BASE_DIR}" + FILES + "${WEBGPU_SHADER_BASE_DIR}/mipmap.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/snow.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/general.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/tile_util.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/normals_util.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/hashing.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/encoder.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/filtering.wgsl" + "${WEBGPU_SHADER_BASE_DIR}/noise.wgsl" + ) + +target_compile_definitions(webgpu PUBLIC ALP_SHADER_DIR_WEBGPU="${CMAKE_CURRENT_SOURCE_DIR}/shaders/") + +target_link_libraries(webgpu PUBLIC Qt::Core nucleus) +target_include_directories(webgpu PRIVATE .) diff --git a/webgpu/base/Context.h b/webgpu/base/Context.h new file mode 100644 index 000000000..79c53c183 --- /dev/null +++ b/webgpu/base/Context.h @@ -0,0 +1,65 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "RenderResourceRegistry.h" +#include + +namespace webgpu { + +class Context { +public: + Context() = default; + ~Context() = default; + + Context(const Context&) = delete; + Context& operator=(const Context&) = delete; + + WGPUInstance instance() const { return m_instance; } + WGPUDevice device() const { return m_device; } + WGPUAdapter adapter() const { return m_adapter; } + WGPUSurface surface() const { return m_surface; } + WGPUQueue queue() const { return m_queue; } + WGPUTextureFormat surface_texture_format() const { return m_surface_texture_format; } + void set_surface_texture_format(WGPUTextureFormat format) { m_surface_texture_format = format; } + + void init(WGPUInstance instance, WGPUDevice device, WGPUAdapter adapter, WGPUSurface surface, WGPUQueue queue) + { + m_instance = instance; + m_device = device; + m_adapter = adapter; + m_surface = surface; + m_queue = queue; + } + + RenderResourceRegistry& resource_registry() { return m_registry; } + const RenderResourceRegistry& resource_registry() const { return m_registry; } + +private: + WGPUInstance m_instance = nullptr; + WGPUDevice m_device = nullptr; + WGPUAdapter m_adapter = nullptr; + WGPUSurface m_surface = nullptr; + WGPUQueue m_queue = nullptr; + WGPUTextureFormat m_surface_texture_format = WGPUTextureFormat_BGRA8Unorm; + + RenderResourceRegistry m_registry; +}; + +} // namespace webgpu diff --git a/webgpu/base/Framebuffer.cpp b/webgpu/base/Framebuffer.cpp new file mode 100644 index 000000000..b8f1a9ed2 --- /dev/null +++ b/webgpu/base/Framebuffer.cpp @@ -0,0 +1,168 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Framebuffer.h" + +#include + +namespace webgpu { + +Framebuffer::Framebuffer(WGPUDevice device, const FramebufferFormat& format) + : m_device { device } + , m_format { format } + , m_color_textures(format.color_formats.size()) + , m_color_texture_views(format.color_formats.size()) +{ + recreate_all_textures(); +} + +void Framebuffer::resize(const glm::uvec2& size) +{ + m_format.size = size; + recreate_all_textures(); +} + +void Framebuffer::recreate_depth_texture() +{ + if (m_format.depth_format == WGPUTextureFormat_Undefined) { + return; + } + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = { .data = "framebuffer depth texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.format = m_format.depth_format; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.size = { m_format.size.x, m_format.size.y, 1 }; + // TODO WGPUTextureUsage_TextureBinding currently only needed for line rendering + // maybe add parameters, so we dont need every depth texture to be able to be used as texture binding + // (to mitigate performance impact) + texture_desc.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding; + texture_desc.viewFormatCount = 1; + texture_desc.viewFormats = &m_format.depth_format; + m_depth_texture = std::make_unique(m_device, texture_desc); + + WGPUTextureViewDescriptor view_desc {}; + view_desc.aspect = WGPUTextureAspect::WGPUTextureAspect_DepthOnly; + view_desc.arrayLayerCount = 1; + view_desc.baseArrayLayer = 0; + view_desc.mipLevelCount = 1; + view_desc.baseMipLevel = 0; + view_desc.dimension = WGPUTextureViewDimension::WGPUTextureViewDimension_2D; + view_desc.format = texture_desc.format; + m_depth_texture_view = m_depth_texture->create_view(view_desc); +} + +void Framebuffer::recreate_color_texture(size_t index) +{ + assert(index < m_format.color_formats.size()); + + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "framebuffer color texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.format = m_format.color_formats[index]; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.size = { m_format.size.x, m_format.size.y, 1 }; + texture_desc.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopySrc; + texture_desc.viewFormatCount = 1; + texture_desc.viewFormats = &m_format.color_formats[index]; + + m_color_textures[index] = std::make_unique(m_device, texture_desc); + m_color_texture_views[index] = m_color_textures.at(index)->create_view(); +} + +void Framebuffer::recreate_all_textures() +{ + recreate_depth_texture(); + for (size_t i = 0; i < m_format.color_formats.size(); i++) { + recreate_color_texture(i); + } +} + +glm::uvec2 Framebuffer::size() const { return m_format.size; } + +const raii::TextureView& Framebuffer::color_texture_view(size_t index) +{ + return *m_color_texture_views.at(index); +} + +const raii::Texture& Framebuffer::color_texture(size_t index) +{ + return *m_color_textures.at(index); +} + +const raii::TextureView& Framebuffer::depth_texture_view() +{ + assert(m_depth_texture_view); + return *m_depth_texture_view.get(); +} + +const raii::Texture& Framebuffer::depth_texture() +{ + assert(m_depth_texture); + return *m_depth_texture.get(); +} + +std::unique_ptr Framebuffer::begin_render_pass(WGPUCommandEncoder encoder) +{ + std::vector render_pass_color_attachments; + for (const auto& color_texture_view : m_color_texture_views) { + WGPURenderPassColorAttachment render_pass_color_attachment {}; + render_pass_color_attachment.view = color_texture_view->handle(); + render_pass_color_attachment.resolveTarget = nullptr; + render_pass_color_attachment.loadOp = WGPULoadOp::WGPULoadOp_Clear; + render_pass_color_attachment.storeOp = WGPUStoreOp::WGPUStoreOp_Store; + render_pass_color_attachment.clearValue = WGPUColor { 0.0, 0.0, 0.0, 0.0 }; + render_pass_color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + render_pass_color_attachments.emplace_back(render_pass_color_attachment); + } + + WGPURenderPassDescriptor render_pass_desc {}; + render_pass_desc.colorAttachmentCount = render_pass_color_attachments.size(); + render_pass_desc.colorAttachments = render_pass_color_attachments.data(); + + WGPURenderPassDepthStencilAttachment depth_stencil_attachment {}; + if (m_format.depth_format != WGPUTextureFormat_Undefined) + { + depth_stencil_attachment.view = m_depth_texture_view->handle(); + depth_stencil_attachment.depthClearValue = 0.0f; + depth_stencil_attachment.depthLoadOp = WGPULoadOp::WGPULoadOp_Clear; + depth_stencil_attachment.depthStoreOp = WGPUStoreOp::WGPUStoreOp_Store; + depth_stencil_attachment.depthReadOnly = false; + depth_stencil_attachment.stencilClearValue = 0; + depth_stencil_attachment.stencilLoadOp = WGPULoadOp::WGPULoadOp_Undefined; + depth_stencil_attachment.stencilStoreOp = WGPUStoreOp::WGPUStoreOp_Undefined; + depth_stencil_attachment.stencilReadOnly = true; + render_pass_desc.depthStencilAttachment = &depth_stencil_attachment; + } else { + render_pass_desc.depthStencilAttachment = nullptr; + } + render_pass_desc.timestampWrites = nullptr; + return std::make_unique(encoder, render_pass_desc); +} + +// ToDo: Implement this function +glm::vec4 Framebuffer::read_colour_attachment_pixel(size_t index, const glm::dvec2& normalised_device_coordinates) { + assert(index < m_color_textures.size()); + assert(normalised_device_coordinates.x >= 0.0 && normalised_device_coordinates.x <= 1.0); + return {}; +} + +} // namespace webgpu diff --git a/webgpu/base/Framebuffer.h b/webgpu/base/Framebuffer.h new file mode 100644 index 000000000..10654f336 --- /dev/null +++ b/webgpu/base/Framebuffer.h @@ -0,0 +1,65 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "raii/RenderPassEncoder.h" +#include "raii/Texture.h" +#include + +namespace webgpu { + +struct FramebufferFormat { + glm::uvec2 size; + WGPUTextureFormat depth_format; + std::vector color_formats; +}; + +class Framebuffer { +public: + Framebuffer(WGPUDevice device, const FramebufferFormat& format); + + void resize(const glm::uvec2& size); + void recreate_depth_texture(); + void recreate_color_texture(size_t index); + void recreate_all_textures(); + + glm::uvec2 size() const; + + const raii::TextureView& color_texture_view(size_t index); + const raii::Texture& color_texture(size_t index); + + const raii::TextureView& depth_texture_view(); + const raii::Texture& depth_texture(); + + std::unique_ptr begin_render_pass(WGPUCommandEncoder encoder); + + // ToDo: Make generic for different types + glm::vec4 read_colour_attachment_pixel(size_t index, const glm::dvec2& normalised_device_coordinates); + +private: + WGPUDevice m_device; + FramebufferFormat m_format; + std::unique_ptr m_depth_texture; + std::unique_ptr m_depth_texture_view; + std::vector> m_color_textures; + std::vector> m_color_texture_views; +}; + +} // namespace webgpu diff --git a/webgpu/base/RenderResourceRegistry.cpp b/webgpu/base/RenderResourceRegistry.cpp new file mode 100644 index 000000000..056aa27fc --- /dev/null +++ b/webgpu/base/RenderResourceRegistry.cpp @@ -0,0 +1,159 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RenderResourceRegistry.h" + +#include +#include +#include +#include + +namespace webgpu { + +RenderResourceRegistry::RenderResourceRegistry() +{ + m_preprocessor.set_file_reader([this](const std::string& name) { return read_shader_source(name); }); + m_preprocessor.set_error_callback([](const std::string& message) { qFatal("%s", message.c_str()); }); +} + +void RenderResourceRegistry::set_local_shader_path(const std::string& target, const std::string& path) { m_local_shader_paths[target] = path; } + +void RenderResourceRegistry::register_shader(const std::string& name, const std::string& source_path) +{ + if (has_shader(name)) // treat re-registration as a no-op + return; + m_shader_index[name] = m_shaders.size(); + m_shaders.push_back({ source_path, nullptr }); + if (m_device != nullptr) + m_shaders.back().module = compile_shader(m_device, source_path); +} + +bool RenderResourceRegistry::has_shader(const std::string& name) const { return m_shader_index.find(name) != m_shader_index.end(); } + +const raii::ShaderModule& RenderResourceRegistry::shader(const std::string& name) const +{ + auto it = m_shader_index.find(name); + assert(it != m_shader_index.end()); + return *m_shaders[it->second].module; +} + +void RenderResourceRegistry::register_bind_group_layout(const std::string& name, std::function(WGPUDevice)> factory) +{ + if (has_bind_group_layout(name)) // treat re-registration as a no-op + return; + m_layout_index[name] = m_layouts.size(); + m_layouts.push_back({ name, factory, nullptr }); + if (m_device != nullptr) + m_layouts.back().layout = factory(m_device); +} + +bool RenderResourceRegistry::has_bind_group_layout(const std::string& name) const { return m_layout_index.find(name) != m_layout_index.end(); } + +const raii::BindGroupLayout& RenderResourceRegistry::bind_group_layout(const std::string& name) const +{ + auto it = m_layout_index.find(name); + assert(it != m_layout_index.end()); + return *m_layouts[it->second].layout; +} + +void RenderResourceRegistry::register_pipeline(std::function recreate_fn) +{ + m_pipeline_fns.push_back(recreate_fn); + if (m_device != nullptr) + recreate_fn(m_device, *this); +} + +void RenderResourceRegistry::recreate_all(WGPUDevice device) +{ + m_device = device; + + auto start = std::chrono::high_resolution_clock::now(); + + m_preprocessor.clear_cache(); + + for (auto& entry : m_shaders) + entry.module = compile_shader(device, entry.source_path); + + for (auto& entry : m_layouts) + entry.layout = entry.factory(device); + + for (auto& fn : m_pipeline_fns) + fn(device, *this); + + auto end = std::chrono::high_resolution_clock::now(); + qDebug() << "RenderResourceRegistry::recreate_all took" << std::chrono::duration_cast(end - start).count() << "ms"; +} + +std::string RenderResourceRegistry::read_shader_source(const std::string& source_path) const +{ + // source_path is a logical shader name "target::relpath" (extension omitted). + std::string target; + std::string relpath = source_path; + if (const auto sep = source_path.find("::"); sep != std::string::npos) { + target = source_path.substr(0, sep); + relpath = source_path.substr(sep + 2); + } + const std::string file_rel = relpath + ".wgsl"; + + if (const auto it = m_local_shader_paths.find(target); it != m_local_shader_paths.end() && !it->second.empty()) { + const std::string local = it->second + file_rel; + QFile local_file(QString::fromStdString(local)); + if (local_file.open(QIODevice::ReadOnly | QIODevice::Text)) + return local_file.readAll().toStdString(); + } + + const std::string qrc = std::string(QRC_PREFIX) + target + "/" + file_rel; + QFile file(QString::fromStdString(qrc)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + qFatal("Could not open shader file %s", qrc.c_str()); + return file.readAll().toStdString(); +} + +std::unique_ptr RenderResourceRegistry::compile_shader_from_code(WGPUDevice device, const std::string& code, const std::string& label) +{ + const std::string preprocessed = m_preprocessor.preprocess_code(code); + + WGPUShaderSourceWGSL wgsl_desc {}; + wgsl_desc.code = WGPUStringView { .data = preprocessed.c_str(), .length = WGPU_STRLEN }; + wgsl_desc.chain.next = nullptr; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + + WGPUShaderModuleDescriptor desc {}; + desc.label = WGPUStringView { .data = label.c_str(), .length = WGPU_STRLEN }; + desc.nextInChain = &wgsl_desc.chain; + + return std::make_unique(device, desc); +} + +std::unique_ptr RenderResourceRegistry::compile_shader(WGPUDevice device, const std::string& source_path) +{ + const std::string code = m_preprocessor.preprocess_file(source_path); + + WGPUShaderSourceWGSL wgsl_desc {}; + wgsl_desc.code = WGPUStringView { .data = code.c_str(), .length = WGPU_STRLEN }; + wgsl_desc.chain.next = nullptr; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + + WGPUShaderModuleDescriptor desc {}; + desc.label = WGPUStringView { .data = source_path.c_str(), .length = WGPU_STRLEN }; + desc.nextInChain = &wgsl_desc.chain; + + return std::make_unique(device, desc); +} + +} // namespace webgpu diff --git a/webgpu/base/RenderResourceRegistry.h b/webgpu/base/RenderResourceRegistry.h new file mode 100644 index 000000000..c2a4dd097 --- /dev/null +++ b/webgpu/base/RenderResourceRegistry.h @@ -0,0 +1,92 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu { + +class RenderResourceRegistry { +public: + RenderResourceRegistry(); + + // Set the local filesystem path prefix for a targets shaders (for hot-reload). + void set_local_shader_path(const std::string& target, const std::string& path); + + // Register a shader by name and source path. + void register_shader(const std::string& name, const std::string& source_path); + [[nodiscard]] bool has_shader(const std::string& name) const; + const raii::ShaderModule& shader(const std::string& name) const; + + // Register a bind group layout by name and factory. + void register_bind_group_layout(const std::string& name, std::function(WGPUDevice)> factory); + [[nodiscard]] bool has_bind_group_layout(const std::string& name) const; + const raii::BindGroupLayout& bind_group_layout(const std::string& name) const; + + // Register a pipeline constructor + // NOTE: Since we have multiple types of Pipelines its easier that the caller owns the pipeline object + void register_pipeline(std::function recreate_fn); + + // Recreate order: shaders -> layouts -> pipelines + void recreate_all(WGPUDevice device); + + // Compile inline WGSL code (with #include preprocessing) without registering it. + std::unique_ptr compile_shader_from_code(WGPUDevice device, const std::string& code, const std::string& label); + +private: + std::string read_shader_source(const std::string& source_path) const; + std::unique_ptr compile_shader(WGPUDevice device, const std::string& source_path); + +private: + static constexpr const char* QRC_PREFIX = ":/shaders/"; + std::unordered_map m_local_shader_paths; // target namespace -> local dir prefix + + webgpu::util::ShaderPreprocessor m_preprocessor; + + struct ShaderEntry { + std::string source_path; + std::unique_ptr module; + }; + struct LayoutEntry { + std::string name; + std::function(WGPUDevice)> factory; + std::unique_ptr layout; + }; + + std::unordered_map m_shader_index; + std::vector m_shaders; + + std::unordered_map m_layout_index; + std::vector m_layouts; + + std::vector> m_pipeline_fns; + + // Set by recreate_all(); non-null means resources can be created on demand. + WGPUDevice m_device = nullptr; +}; + +} // namespace webgpu diff --git a/webgpu/base/gpu_utils.cpp b/webgpu/base/gpu_utils.cpp new file mode 100644 index 000000000..5cc80f602 --- /dev/null +++ b/webgpu/base/gpu_utils.cpp @@ -0,0 +1,144 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "gpu_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace webgpu { + +namespace { + // Registers the mipmap-creation shader and bind group layout once (idempotent), so the + // utility is self-contained and does not depend on any higher-level target initialising it. + void ensure_mipmap_resources(RenderResourceRegistry& reg) + { + if (!reg.has_shader("mipmap_creation")) + reg.register_shader("mipmap_creation", "webgpu::mipmap"); + + if (!reg.has_bind_group_layout("mipmap_creation")) + reg.register_bind_group_layout("mipmap_creation", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry input_entry {}; + input_entry.binding = 0; + input_entry.visibility = WGPUShaderStage_Compute; + input_entry.texture.sampleType = WGPUTextureSampleType_Float; + input_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry output_entry {}; + output_entry.binding = 1; + output_entry.visibility = WGPUShaderStage_Compute; + output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + output_entry.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + + return std::make_unique( + device, std::vector { input_entry, output_entry }, "mipmap creation bind group layout"); + }); + } +} // namespace + +void compute_mipmaps_for_texture(Context& ctx, const raii::Texture* texture) { compute_mipmaps_for_texture(ctx, texture, {}); } + +void compute_mipmaps_for_texture(Context& ctx, const raii::Texture* texture, WGPUQueueWorkDoneCallbackInfo on_done) +{ + WGPUDevice device = ctx.device(); + WGPUQueue queue = ctx.queue(); + auto& reg = ctx.resource_registry(); + ensure_mipmap_resources(reg); + + glm::uvec2 baseSize = { texture->width(), texture->height() }; + uint32_t mipLevelCount = texture->mip_level_count(); + + if (mipLevelCount == 1) { + qDebug() << "No mipmaps to compute"; + return; + } else { + qDebug() << "Computing" << mipLevelCount << "mipmaps for texture"; + } + + raii::CombinedComputePipeline pipeline(device, + reg.shader("mipmap_creation"), + std::vector { ®.bind_group_layout("mipmap_creation") }, + "mipmap creation compute pipeline"); + + std::vector> textureMipViews; + std::vector mipSizes(mipLevelCount); + + for (uint32_t i = 0; i < mipLevelCount; i++) { + WGPUTextureViewDescriptor viewDesc {}; + viewDesc.dimension = WGPUTextureViewDimension::WGPUTextureViewDimension_2D; + viewDesc.format = WGPUTextureFormat::WGPUTextureFormat_RGBA8Unorm; + viewDesc.baseMipLevel = i; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + textureMipViews.push_back(std::make_unique(texture->handle(), viewDesc)); + + mipSizes[i].width = std::max(1u, baseSize.x >> i); + mipSizes[i].height = std::max(1u, baseSize.y >> i); + mipSizes[i].depthOrArrayLayers = 1; + } + + std::vector> bindGroups; + for (uint32_t i = 0; i < mipLevelCount - 1; i++) { + std::vector bgEntries { + textureMipViews[i]->create_bind_group_entry(0), + textureMipViews[i + 1]->create_bind_group_entry(1), + }; + bindGroups.push_back(std::make_unique(device, reg.bind_group_layout("mipmap_creation"), bgEntries, "mipmap creation bindgroup")); + } + + constexpr glm::uvec3 SHADER_WORKGROUP_SIZE = { 8, 8, 1 }; + { + WGPUCommandEncoderDescriptor descriptor {}; + raii::CommandEncoder encoder(device, descriptor); + + for (uint32_t i = 0; i < mipLevelCount - 1; i++) { + WGPUComputePassDescriptor compute_pass_desc {}; + raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(mipSizes[i + 1].width, mipSizes[i + 1].height, 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, bindGroups[i]->handle(), 0, nullptr); + pipeline.run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "MipMap command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(queue, 1, &command); + wgpuCommandBufferRelease(command); + } + + if (on_done.callback) + wgpuQueueOnSubmittedWorkDone(queue, on_done); +} + +} // namespace webgpu diff --git a/webgpu/base/gpu_utils.h b/webgpu/base/gpu_utils.h new file mode 100644 index 000000000..8702059ef --- /dev/null +++ b/webgpu/base/gpu_utils.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +namespace webgpu { + +class Context; +namespace raii { + class Texture; +} + +// Computes the mipmap chain for the given RGBA8Unorm texture using a compute shader +void compute_mipmaps_for_texture(Context& ctx, const raii::Texture* texture); + +// Async overload: calls on_done after the mipmap work is submitted to the queue. +void compute_mipmaps_for_texture(Context& ctx, const raii::Texture* texture, WGPUQueueWorkDoneCallbackInfo on_done); + +} // namespace webgpu diff --git a/webgpu/base/raii/BindGroup.cpp b/webgpu/base/raii/BindGroup.cpp new file mode 100644 index 000000000..d907e109f --- /dev/null +++ b/webgpu/base/raii/BindGroup.cpp @@ -0,0 +1,71 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "BindGroup.h" + +namespace webgpu::raii { + +BindGroup::BindGroup(WGPUDevice device, WGPUBindGroupLayout layout, const std::initializer_list& entries, const std::string& label) + : GpuResource(device, + WGPUBindGroupDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.c_str(), .length = label.length() }, + .layout = layout, + .entryCount = entries.size(), + .entries = entries.begin(), + }) +{ +} + +BindGroup::BindGroup(WGPUDevice device, WGPUBindGroupLayout layout, const std::vector& entries, const std::string& label) + : GpuResource(device, + WGPUBindGroupDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.c_str(), .length = label.length() }, + .layout = layout, + .entryCount = entries.size(), + .entries = entries.data(), + }) +{ +} + +BindGroup::BindGroup(WGPUDevice device, const BindGroupLayout& layout, const std::initializer_list& entries, const std::string& label) + : GpuResource(device, + WGPUBindGroupDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.c_str(), .length = label.length() }, + .layout = layout.handle(), + .entryCount = entries.size(), + .entries = entries.begin(), + }) +{ +} + +BindGroup::BindGroup(WGPUDevice device, const BindGroupLayout& layout, const std::vector& entries, const std::string& label) + : GpuResource(device, + WGPUBindGroupDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.c_str(), .length = label.length() }, + .layout = layout.handle(), + .entryCount = entries.size(), + .entries = entries.data(), + }) +{ +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/BindGroup.h b/webgpu/base/raii/BindGroup.h new file mode 100644 index 000000000..a199f1994 --- /dev/null +++ b/webgpu/base/raii/BindGroup.h @@ -0,0 +1,43 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "BindGroupLayout.h" +#include "base_types.h" +#include +#include + +namespace webgpu::raii { + +class BindGroup : public GpuResource { +public: + using GpuResource::GpuResource; + + BindGroup( + WGPUDevice device, WGPUBindGroupLayout layout, const std::initializer_list& entries, const std::string& label = "not assigned"); + + BindGroup(WGPUDevice device, WGPUBindGroupLayout layout, const std::vector& entries, const std::string& label = "not assigned"); + + BindGroup( + WGPUDevice device, const BindGroupLayout& layout, const std::initializer_list& entries, const std::string& label = "not assigned"); + + BindGroup(WGPUDevice device, const BindGroupLayout& layout, const std::vector& entries, const std::string& label = "not assigned"); +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/BindGroupLayout.cpp b/webgpu/base/raii/BindGroupLayout.cpp new file mode 100644 index 000000000..857fa0be3 --- /dev/null +++ b/webgpu/base/raii/BindGroupLayout.cpp @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "BindGroupLayout.h" + +#include +#include + +namespace webgpu::raii { + +BindGroupLayout::BindGroupLayout(WGPUDevice device, const std::vector& entries, const std::string& label) + : GpuResource(device, + WGPUBindGroupLayoutDescriptor { + .nextInChain = nullptr, + .label = { .data = label.data(), .length = label.length() }, + .entryCount = entries.size(), + .entries = entries.data(), + }) +{ +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/BindGroupLayout.h b/webgpu/base/raii/BindGroupLayout.h new file mode 100644 index 000000000..b624325dc --- /dev/null +++ b/webgpu/base/raii/BindGroupLayout.h @@ -0,0 +1,33 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" +#include +#include + +namespace webgpu::raii { + +class BindGroupLayout : public GpuResource { +public: + using GpuResource::GpuResource; + BindGroupLayout(WGPUDevice device, const std::vector& entries, const std::string& label = "not assigned"); +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/CombinedComputePipeline.cpp b/webgpu/base/raii/CombinedComputePipeline.cpp new file mode 100644 index 000000000..7687f4dda --- /dev/null +++ b/webgpu/base/raii/CombinedComputePipeline.cpp @@ -0,0 +1,60 @@ +#include "CombinedComputePipeline.h" + +#include "util/string_cast.h" +#include +#include + +namespace webgpu::raii { + +CombinedComputePipeline::CombinedComputePipeline( + WGPUDevice device, const raii::ShaderModule& shader_module, const std::vector& bind_group_layouts, const std::string& label) +{ + std::vector bind_group_layout_handles; + std::transform(bind_group_layouts.begin(), bind_group_layouts.end(), std::back_insert_iterator(bind_group_layout_handles), + [](const raii::BindGroupLayout* layout) { return layout->handle(); }); + m_layout = std::make_unique(device, bind_group_layout_handles); + + WGPUComputePipelineDescriptor desc {}; + desc.label = WGPUStringView { .data = label.c_str(), .length = WGPU_STRLEN }; + desc.compute = {}; + desc.compute.entryPoint = WGPUStringView { .data = "computeMain", .length = WGPU_STRLEN }; // TODO + desc.compute.module = shader_module.handle(); + desc.layout = m_layout->handle(); + m_pipeline = std::make_unique(device, desc); +} + +CombinedComputePipeline::CombinedComputePipeline( + WGPUDevice device, const std::vector& bind_group_layouts, WGPUComputePipelineDescriptor desc) +{ + std::vector bind_group_layout_handles; + std::transform(bind_group_layouts.begin(), bind_group_layouts.end(), std::back_insert_iterator(bind_group_layout_handles), + [](const raii::BindGroupLayout* layout) { return layout->handle(); }); + m_layout = std::make_unique(device, bind_group_layout_handles); + desc.layout = m_layout->handle(); + m_pipeline = std::make_unique(device, desc); +} + +void CombinedComputePipeline::run(const raii::CommandEncoder& encoder, const glm::uvec3& workgroup_counts) const +{ + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "compute pass", .length = WGPU_STRLEN }; // TODO + raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + run(compute_pass, workgroup_counts); +} + +void CombinedComputePipeline::run(const raii::ComputePassEncoder& compute_pass, const glm::uvec3& workgroup_counts) const +{ + wgpuComputePassEncoderSetPipeline(compute_pass.handle(), m_pipeline->handle()); + for (const auto& [location, bind_group] : m_bindings) { + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), location, bind_group->handle(), 0, nullptr); + } + wgpuComputePassEncoderDispatchWorkgroups(compute_pass.handle(), workgroup_counts.x, workgroup_counts.y, workgroup_counts.z); +}; + +void CombinedComputePipeline::set_binding(uint32_t location, const raii::BindGroup& binding) +{ + // TODO could validate against pipeline layout (= list of bind group layouts) here + m_bindings[location] = &binding; +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/CombinedComputePipeline.h b/webgpu/base/raii/CombinedComputePipeline.h new file mode 100644 index 000000000..597d235b0 --- /dev/null +++ b/webgpu/base/raii/CombinedComputePipeline.h @@ -0,0 +1,39 @@ +#pragma once + +#include "BindGroup.h" +#include "BindGroupLayout.h" +#include "PipelineLayout.h" +#include "base_types.h" +#include +#include +#include + +namespace webgpu::raii { + +/// Convenience wrapper for compute pipeline +/// Usage: +/// - initialize with shader module and bind group layouts +/// - set pipeline input/output: specify what bind groups to use for what location +/// - run pipeline: calling run binds pipeline, sets bind groups and runs pipeline +class CombinedComputePipeline { +public: + CombinedComputePipeline(WGPUDevice device, const raii::ShaderModule& shader_module, const std::vector& bind_group_layouts, + const std::string& label = "CombinedComputePipeline [no name]"); + + CombinedComputePipeline(WGPUDevice device, const std::vector& bind_group_layouts, WGPUComputePipelineDescriptor desc); + + [[nodiscard]] WGPUComputePipeline handle() const { return m_pipeline->handle(); } + + // creates a new compute pass and only uses that to render + void run(const raii::CommandEncoder& encoder, const glm::uvec3& workgroup_counts) const; + void run(const raii::ComputePassEncoder& compute_pass, const glm::uvec3& workgroup_counts) const; + + void set_binding(uint32_t location, const raii::BindGroup& binding); + +protected: + std::unique_ptr m_layout; + std::unique_ptr m_pipeline; + std::map m_bindings; +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Pipeline.cpp b/webgpu/base/raii/Pipeline.cpp new file mode 100644 index 000000000..32481caf7 --- /dev/null +++ b/webgpu/base/raii/Pipeline.cpp @@ -0,0 +1,111 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Pipeline.h" + +#include +#include +#include + +namespace webgpu::raii { + +GenericRenderPipeline::GenericRenderPipeline(WGPUDevice device, const ShaderModule& vertex_shader, const ShaderModule& fragment_shader, + const VertexBufferInfos& vertex_buffer_infos, const FramebufferFormat& framebuffer_format, const BindGroupLayouts& bind_group_layouts, + const std::vector>& blend_states) + : m_framebuffer_format { framebuffer_format } +{ + assert(blend_states.size() <= framebuffer_format.color_formats.size()); + + std::vector color_target_states; + + for (size_t i = 0; i < framebuffer_format.color_formats.size(); i++) { + WGPUColorTargetState color_target_state {}; + color_target_state.format = framebuffer_format.color_formats.at(i); + if (i < blend_states.size() && blend_states.at(i).has_value()) { + color_target_state.blend = &blend_states.at(i).value(); + } + color_target_state.writeMask = WGPUColorWriteMask_All; + color_target_states.push_back(color_target_state); + } + + WGPUFragmentState fragment_state {}; + fragment_state.module = fragment_shader.handle(); + fragment_state.entryPoint = WGPUStringView { .data = "fragmentMain", .length = WGPU_STRLEN }; + fragment_state.constantCount = 0; + fragment_state.constants = nullptr; + fragment_state.targetCount = color_target_states.size(); + fragment_state.targets = color_target_states.data(); + + std::vector bind_group_layout_handles; + std::transform(bind_group_layouts.begin(), bind_group_layouts.end(), std::back_insert_iterator(bind_group_layout_handles), + [](const BindGroupLayout* layout) { return layout->handle(); }); + m_pipeline_layout = std::make_unique(device, bind_group_layout_handles); + + std::vector layouts; + std::transform(vertex_buffer_infos.begin(), vertex_buffer_infos.end(), std::back_insert_iterator(layouts), + [](const util::SingleVertexBufferInfo& info) { return info.vertex_buffer_layout(); }); + + WGPURenderPipelineDescriptor pipeline_desc {}; + pipeline_desc.vertex.module = vertex_shader.handle(); + pipeline_desc.vertex.entryPoint = WGPUStringView { .data = "vertexMain", .length = WGPU_STRLEN }; + pipeline_desc.vertex.bufferCount = layouts.size(); + pipeline_desc.vertex.buffers = layouts.data(); + pipeline_desc.vertex.constantCount = 0; + pipeline_desc.vertex.constants = nullptr; + pipeline_desc.primitive.topology = WGPUPrimitiveTopology::WGPUPrimitiveTopology_TriangleStrip; + pipeline_desc.primitive.stripIndexFormat = WGPUIndexFormat::WGPUIndexFormat_Uint16; + pipeline_desc.primitive.frontFace = WGPUFrontFace::WGPUFrontFace_CCW; + pipeline_desc.primitive.cullMode = WGPUCullMode::WGPUCullMode_None; + pipeline_desc.fragment = &fragment_state; + + WGPUStencilFaceState stencil_face_state {}; + WGPUDepthStencilState depth_stencil_state {}; + if (framebuffer_format.depth_format != WGPUTextureFormat_Undefined) { + // needed to disable stencil test (for dawn), see https://github.com/ocornut/imgui/issues/7232 + stencil_face_state.compare = WGPUCompareFunction::WGPUCompareFunction_Always; + stencil_face_state.depthFailOp = WGPUStencilOperation::WGPUStencilOperation_Keep; + stencil_face_state.failOp = WGPUStencilOperation::WGPUStencilOperation_Keep; + stencil_face_state.passOp = WGPUStencilOperation::WGPUStencilOperation_Keep; + + depth_stencil_state.depthCompare = WGPUCompareFunction::WGPUCompareFunction_GreaterEqual; + depth_stencil_state.depthWriteEnabled + = (framebuffer_format.depth_format != WGPUTextureFormat_Undefined) ? WGPUOptionalBool_True : WGPUOptionalBool_False; + depth_stencil_state.stencilReadMask = 0; + depth_stencil_state.stencilWriteMask = 0; + depth_stencil_state.stencilFront = stencil_face_state; + depth_stencil_state.stencilBack = stencil_face_state; + depth_stencil_state.format = framebuffer_format.depth_format; + pipeline_desc.depthStencil = &depth_stencil_state; + } else { + pipeline_desc.depthStencil = nullptr; + } + + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = ~0u; + pipeline_desc.multisample.alphaToCoverageEnabled = false; + pipeline_desc.layout = m_pipeline_layout->handle(); + + m_pipeline = std::make_unique(device, pipeline_desc); +} + +const RenderPipeline& GenericRenderPipeline::pipeline() const { return *m_pipeline; } + +const FramebufferFormat& GenericRenderPipeline::framebuffer_format() const { return m_framebuffer_format; } + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Pipeline.h b/webgpu/base/raii/Pipeline.h new file mode 100644 index 000000000..109d3d613 --- /dev/null +++ b/webgpu/base/raii/Pipeline.h @@ -0,0 +1,50 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "BindGroupLayout.h" +#include "PipelineLayout.h" +#include "base_types.h" +#include "webgpu/base/Framebuffer.h" +#include "webgpu/base/util/VertexBufferInfo.h" +#include +#include + +namespace webgpu::raii { + +class GenericRenderPipeline { +public: + using VertexBufferInfos = std::vector; + using BindGroupLayouts = std::vector; + + GenericRenderPipeline(WGPUDevice device, const ShaderModule& vertex_shader, const ShaderModule& fragment_shader, + const VertexBufferInfos& vertex_buffer_infos, const FramebufferFormat& framebuffer_format, const BindGroupLayouts& bind_group_layouts, + const std::vector>& blend_states = {}); + + const RenderPipeline& pipeline() const; + const FramebufferFormat& framebuffer_format() const; + +private: + std::unique_ptr m_pipeline; + std::unique_ptr m_pipeline_layout; + + FramebufferFormat m_framebuffer_format; +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/PipelineLayout.cpp b/webgpu/base/raii/PipelineLayout.cpp new file mode 100644 index 000000000..dd3f1a58f --- /dev/null +++ b/webgpu/base/raii/PipelineLayout.cpp @@ -0,0 +1,34 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "PipelineLayout.h" + +namespace webgpu::raii { + +PipelineLayout::PipelineLayout(WGPUDevice device, const std::vector& layouts, const std::string& label) + : GpuResource(device, + WGPUPipelineLayoutDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.data(), .length = label.length() }, + .bindGroupLayoutCount = layouts.size(), + .bindGroupLayouts = layouts.data(), + }) +{ +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/PipelineLayout.h b/webgpu/base/raii/PipelineLayout.h new file mode 100644 index 000000000..b2bcc8a5b --- /dev/null +++ b/webgpu/base/raii/PipelineLayout.h @@ -0,0 +1,34 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" +#include +#include + +namespace webgpu::raii { + +class PipelineLayout : public GpuResource { +public: + using GpuResource::GpuResource; + + PipelineLayout(WGPUDevice device, const std::vector& layouts, const std::string& label = "not assigned"); +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/RawBuffer.h b/webgpu/base/raii/RawBuffer.h new file mode 100644 index 000000000..d24d88e42 --- /dev/null +++ b/webgpu/base/raii/RawBuffer.h @@ -0,0 +1,220 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" +#include +#include +#include +#include +#include +#include + +namespace webgpu::raii { + +/// Generic class for GPU buffer handles complying to RAII. +/// This class does not store the value to be written on CPU side. +template class RawBuffer : public GpuResource { +public: + using ReadBackCallback = std::function)>; + + struct ReadBackState { + ReadBackCallback callback; + std::unique_ptr> staging_buffer; + }; + + // m_size in num objects + RawBuffer(WGPUDevice device, WGPUBufferUsage usage, size_t size, const std::string& label = "label not set") + : GpuResource(device, + WGPUBufferDescriptor { + .nextInChain = nullptr, + .label = WGPUStringView { .data = label.c_str(), .length = WGPU_STRLEN }, + .usage = usage, + .size = size * sizeof(T), + .mappedAtCreation = false, + }) + , m_size(size) + { + } + + // count and offset in number of elements of size sizeof(T) + void write(WGPUQueue queue, const T* data, size_t count = 1, size_t offset = 0) + { + assert(count <= m_size); + wgpuQueueWriteBuffer(queue, m_handle, offset * sizeof(T), data, count * sizeof(T)); // takes size in bytes + } + + void clear(WGPUDevice device, WGPUQueue queue) + { + // bind GPU resources and run pipeline + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "buffer clear command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(device, descriptor); + + clear(encoder.handle()); + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "buffer clear command buffer", .length = WGPU_STRLEN }; // TODO add buffer label here + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(queue, 1, &command); + wgpuCommandBufferRelease(command); + } + void clear(WGPUCommandEncoder encoder) { wgpuCommandEncoderClearBuffer(encoder, m_handle, 0, m_size * sizeof(T)); } + void clear(WGPUCommandEncoder encoder, size_t count, size_t offset = 0) { wgpuCommandEncoderClearBuffer(encoder, m_handle, offset, count * sizeof(T)); } + + /// copy contents from this buffer into other buffer + template + void copy_to_buffer(WGPUCommandEncoder encoder, size_t src_offset_bytes, const raii::RawBuffer& dst, size_t dst_offset_bytes, size_t size_bytes) + { + wgpuCommandEncoderCopyBufferToBuffer(encoder, handle(), src_offset_bytes, dst.handle(), dst_offset_bytes, size_bytes); + } + template + void copy_to_buffer(WGPUDevice device, size_t src_offset_bytes, const raii::RawBuffer& dst, size_t dst_offset_bytes, size_t size_bytes) + { + WGPUCommandEncoderDescriptor desc {}; + desc.label = WGPUStringView { .data = "copy texture to buffer command encoder", .length = WGPU_STRLEN }; + raii::CommandEncoder encoder(device, desc); + + copy_to_buffer(encoder.handle(), src_offset_bytes, dst, dst_offset_bytes, size_bytes); + + // submit to queue + WGPUCommandBufferDescriptor cmd_buffer_desc {}; + WGPUCommandBuffer cmd_buffer = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_desc); + WGPUQueue queue = wgpuDeviceGetQueue(device); + wgpuQueueSubmit(queue, 1, &cmd_buffer); + // TODO release cmd buffer -> use raii + } + + /// Copy all contents of this buffer into other buffer + template void copy_to_buffer(WGPUCommandEncoder encoder, const raii::RawBuffer& dst) + { + copy_to_buffer(encoder, 0, dst, 0, size_in_byte()); + } + template void copy_to_buffer(WGPUDevice device, const raii::RawBuffer& dst) + { + copy_to_buffer(device, 0, dst, 0, size_in_byte()); + } + +#ifndef __EMSCRIPTEN__ + // wgpuBufferGetMapState buggy on web, see https://github.com/weBIGeo/webigeo/issues/26#issuecomment-2259959378 + WGPUBufferMapState map_state() { return wgpuBufferGetMapState(handle()); } +#else + WGPUBufferMapState map_state() { return m_buffer_mapping_state; } +#endif + + /// Read back buffer asynchronously. Callback is called by webGPU when buffer is mapped. + WGPUFuture read_back_async(WGPUDevice device, ReadBackCallback callback) + { + auto on_buffer_mapped = [](WGPUMapAsyncStatus status, WGPUStringView message, void* user_data, [[maybe_unused]] void* user_data2) { + RawBuffer* _this = reinterpret_cast*>(user_data); + const auto& callback_state = _this->m_read_back_callbacks.front(); + std::vector buffer_data; + + if (status != WGPUMapAsyncStatus_Success) { + // ToDo eventually caller should be in charge of error report + qCritical() << "failed buffer mapping -" << webgpu::util::bufferMapAsyncStatusToString(status) << ", message: " << message.data; + } else { + WGPUBuffer buffer_handle = _this->descriptor().usage & WGPUBufferUsage_MapRead ? _this->handle() : callback_state.staging_buffer->handle(); + auto raw_buffer_data = static_cast(wgpuBufferGetConstMappedRange(buffer_handle, 0, _this->size_in_byte())); + buffer_data.insert(buffer_data.end(), raw_buffer_data, raw_buffer_data + _this->size()); + wgpuBufferUnmap(buffer_handle); + } +#ifdef __EMSCRIPTEN__ + _this->m_buffer_mapping_state = WGPUBufferMapState_Unmapped; +#endif + callback_state.callback(status, buffer_data); + + _this->m_read_back_callbacks.pop(); // also deletes staging buffer, if one was used + }; + + WGPUBufferMapCallbackInfo on_buffer_mapped_callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_buffer_mapped, + .userdata1 = this, + .userdata2 = nullptr, + }; + + // if possible, maps buffer directly, otherwise creates staging buffer, copies to staging buffer and maps staging buffer + if (descriptor().usage & WGPUBufferUsage_MapRead) { // can read directly + m_read_back_callbacks.emplace(callback); +#ifdef __EMSCRIPTEN__ + m_buffer_mapping_state = WGPUBufferMapState_Pending; +#endif + return wgpuBufferMapAsync(handle(), WGPUMapMode_Read, 0, size_in_byte(), on_buffer_mapped_callback_info); + } else if (descriptor().usage & WGPUBufferUsage_CopySrc) { + m_read_back_callbacks.emplace(callback, + std::make_unique>(device, WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead, size(), "buffer readback staging buffer")); + copy_to_buffer(device, *(m_read_back_callbacks.back().staging_buffer)); + return wgpuBufferMapAsync( + m_read_back_callbacks.back().staging_buffer->handle(), WGPUMapMode_Read, 0, size_in_byte(), on_buffer_mapped_callback_info); + } else { + qFatal("Cannot initialise buffer read back. Buffer requires MapRead or CopySrc usage"); + return {}; + } + } + + /// Read back buffer synchronously. Blocks until buffer is mapped and read back but at most max_timeout_ms. + WGPUMapAsyncStatus read_back_sync(WGPUInstance instance, WGPUDevice device, std::vector& result, uint32_t max_timeout_ms = 1000) + { + WGPUMapAsyncStatus return_status = WGPUMapAsyncStatus_Error; + bool work_done = false; + + WGPUFuture read_back_future = read_back_async(device, [&return_status, &result, &work_done](WGPUMapAsyncStatus status, std::vector async_buffer) { + return_status = status; + if (status == WGPUMapAsyncStatus_Success) { + result.swap(async_buffer); + } + work_done = true; + }); + + WGPUFutureWaitInfo wait_info { .future = read_back_future, .completed = false }; + WGPUWaitStatus wait_status = wgpuInstanceWaitAny(instance, 1, &wait_info, max_timeout_ms * 1000 * 1000); + if (wait_status != WGPUWaitStatus_Success) { + qCritical() << "Failed to map buffer, WGPUWaitStatus was " << wait_status; + } + + return return_status; + } + + size_t size() const { return m_size; } + size_t size_in_byte() const { return m_size * sizeof(T); }; + + WGPUBindGroupEntry create_bind_group_entry(uint32_t binding) const + { + WGPUBindGroupEntry entry {}; + entry.binding = binding; + entry.buffer = m_handle; + entry.size = size_in_byte(); + entry.offset = 0; + entry.nextInChain = nullptr; + return entry; + } + +private: + size_t m_size; + std::queue m_read_back_callbacks; + +#ifdef __EMSCRIPTEN__ + WGPUBufferMapState m_buffer_mapping_state = WGPUBufferMapState_Unmapped; +#endif +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/RenderPassEncoder.cpp b/webgpu/base/raii/RenderPassEncoder.cpp new file mode 100644 index 000000000..3a604f222 --- /dev/null +++ b/webgpu/base/raii/RenderPassEncoder.cpp @@ -0,0 +1,58 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RenderPassEncoder.h" + +namespace webgpu::raii { + +WGPURenderPassDescriptor RenderPassEncoder::create_render_pass_descriptor( + WGPUTextureView color_attachment, WGPUTextureView depth_attachment, WGPUPassTimestampWrites* timestamp_writes) +{ + m_color_attachment = {}; + m_color_attachment.view = color_attachment; + m_color_attachment.resolveTarget = nullptr; + m_color_attachment.loadOp = WGPULoadOp::WGPULoadOp_Clear; + m_color_attachment.storeOp = WGPUStoreOp::WGPUStoreOp_Store; + m_color_attachment.clearValue = WGPUColor { 0.0, 0.0, 0.0, 0.0 }; + m_color_attachment.depthSlice = UINT32_MAX; + + WGPURenderPassDescriptor render_pass_desc {}; + render_pass_desc.colorAttachmentCount = 1; + render_pass_desc.colorAttachments = &m_color_attachment; + + if (depth_attachment != nullptr) { + m_depth_stencil_attachment = {}; + m_depth_stencil_attachment.view = depth_attachment; + m_depth_stencil_attachment.depthClearValue = 1.0f; + m_depth_stencil_attachment.depthLoadOp = WGPULoadOp::WGPULoadOp_Clear; + m_depth_stencil_attachment.depthStoreOp = WGPUStoreOp::WGPUStoreOp_Store; + m_depth_stencil_attachment.depthReadOnly = false; + m_depth_stencil_attachment.stencilClearValue = 0; + m_depth_stencil_attachment.stencilLoadOp = WGPULoadOp::WGPULoadOp_Undefined; + m_depth_stencil_attachment.stencilStoreOp = WGPUStoreOp::WGPUStoreOp_Undefined; + m_depth_stencil_attachment.stencilReadOnly = true; + render_pass_desc.depthStencilAttachment = &m_depth_stencil_attachment; + } + + render_pass_desc.timestampWrites = timestamp_writes; + + return render_pass_desc; +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/RenderPassEncoder.h b/webgpu/base/raii/RenderPassEncoder.h new file mode 100644 index 000000000..149d6d9dd --- /dev/null +++ b/webgpu/base/raii/RenderPassEncoder.h @@ -0,0 +1,52 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" + +namespace webgpu::raii { + +class RenderPassEncoder : public GpuResource { +public: + // Default constructor + RenderPassEncoder(WGPUCommandEncoder context, const WGPURenderPassDescriptor& descriptor) + : GpuResource(context, descriptor) + { + } + + // Creates a default render pass for a given color and depth attachment + // The render pass will clear the color and depth attachments + RenderPassEncoder( + WGPUCommandEncoder encoder, WGPUTextureView color_attachment, WGPUTextureView depth_attachment, WGPUPassTimestampWrites* timestamp_writes = nullptr) + : GpuResource(encoder, create_render_pass_descriptor(color_attachment, depth_attachment, timestamp_writes)) + { + } + +private: + // We need those to live outside the scope of create_render_pass_descriptor + // Better solutions welcome! + WGPURenderPassColorAttachment m_color_attachment; + WGPURenderPassDepthStencilAttachment m_depth_stencil_attachment; + + WGPURenderPassDescriptor create_render_pass_descriptor( + WGPUTextureView color_attachment, WGPUTextureView depth_attachment, WGPUPassTimestampWrites* timestamp_writes = nullptr); +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Sampler.cpp b/webgpu/base/raii/Sampler.cpp new file mode 100644 index 000000000..85304937e --- /dev/null +++ b/webgpu/base/raii/Sampler.cpp @@ -0,0 +1,32 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Sampler.h" + +namespace webgpu::raii { + +WGPUBindGroupEntry Sampler::create_bind_group_entry(uint32_t binding) const +{ + WGPUBindGroupEntry entry {}; + entry.binding = binding; + entry.sampler = m_handle; + entry.offset = 0; + entry.nextInChain = nullptr; + return entry; +} +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Sampler.h b/webgpu/base/raii/Sampler.h new file mode 100644 index 000000000..660117b0e --- /dev/null +++ b/webgpu/base/raii/Sampler.h @@ -0,0 +1,32 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" + +namespace webgpu::raii { + +class Sampler : public GpuResource { +public: + using GpuResource::GpuResource; + + WGPUBindGroupEntry create_bind_group_entry(uint32_t binding) const; +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Texture.cpp b/webgpu/base/raii/Texture.cpp new file mode 100644 index 000000000..0bf9d52dd --- /dev/null +++ b/webgpu/base/raii/Texture.cpp @@ -0,0 +1,302 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Texture.h" + +#include "nucleus/utils/ColourTexture3D.h" +#include + +#include +#include +#include + +namespace webgpu::raii { + +uint8_t Texture::get_bytes_per_element(WGPUTextureFormat format) +{ + switch (format) { + // 8-bit formats + case WGPUTextureFormat_R8Unorm: + case WGPUTextureFormat_R8Snorm: + case WGPUTextureFormat_R8Uint: + case WGPUTextureFormat_R8Sint: + return 1; + + // 16-bit formats + case WGPUTextureFormat_R16Uint: + case WGPUTextureFormat_R16Sint: + case WGPUTextureFormat_R16Float: + case WGPUTextureFormat_RG8Unorm: + case WGPUTextureFormat_RG8Snorm: + case WGPUTextureFormat_RG8Uint: + case WGPUTextureFormat_RG8Sint: + return 2; + + // 32-bit formats + case WGPUTextureFormat_R32Uint: + case WGPUTextureFormat_R32Sint: + case WGPUTextureFormat_R32Float: + case WGPUTextureFormat_RG16Uint: + case WGPUTextureFormat_RG16Sint: + case WGPUTextureFormat_RG16Float: + case WGPUTextureFormat_RGBA8Unorm: + case WGPUTextureFormat_RGBA8UnormSrgb: + case WGPUTextureFormat_RGBA8Snorm: + case WGPUTextureFormat_RGBA8Uint: + case WGPUTextureFormat_RGBA8Sint: + case WGPUTextureFormat_BGRA8Unorm: + case WGPUTextureFormat_BGRA8UnormSrgb: + // Packed 32-bit formats + case WGPUTextureFormat_RGB9E5Ufloat: + case WGPUTextureFormat_RGB10A2Uint: + case WGPUTextureFormat_RGB10A2Unorm: + case WGPUTextureFormat_RG11B10Ufloat: + return 4; + + // 64-bit formats + case WGPUTextureFormat_RG32Uint: + case WGPUTextureFormat_RG32Sint: + case WGPUTextureFormat_RG32Float: + case WGPUTextureFormat_RGBA16Uint: + case WGPUTextureFormat_RGBA16Sint: + case WGPUTextureFormat_RGBA16Float: + return 8; + + // 128-bit formats + case WGPUTextureFormat_RGBA32Uint: + case WGPUTextureFormat_RGBA32Sint: + case WGPUTextureFormat_RGBA32Float: + return 16; + + default: + qFatal("tried to get texture size for unsupoorted format"); + return 0; + } +} + +const uint16_t Texture::BYTES_PER_ROW_PADDING = 256u; + +uint32_t Texture::max_mip_level_count(glm::uvec2 size) +{ + const uint32_t m = std::max(size.x, size.y); + return std::max(1u, std::bit_width(m)); +} + +void Texture::write(WGPUQueue queue, const nucleus::utils::ColourTexture& data, uint32_t layer) +{ + assert(static_cast(data.width()) == m_descriptor.size.width); + assert(static_cast(data.height()) == m_descriptor.size.height); + assert(data.format() == nucleus::utils::ColourTexture::Format::Uncompressed_RGBA); // TODO compressed textures + + WGPUTexelCopyTextureInfo image_copy_texture {}; + image_copy_texture.texture = m_handle; + image_copy_texture.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + image_copy_texture.mipLevel = 0; + image_copy_texture.origin = WGPUOrigin3D { 0, 0, layer }; + + WGPUTexelCopyBufferLayout texture_data_layout {}; + texture_data_layout.bytesPerRow = 4 * data.width(); // for uncompressed RGBA + texture_data_layout.rowsPerImage = data.height(); + texture_data_layout.offset = 0; + + WGPUExtent3D copy_extent { m_descriptor.size.width, m_descriptor.size.height, 1 }; + + wgpuQueueWriteTexture(queue, &image_copy_texture, data.data(), data.n_bytes(), &texture_data_layout, ©_extent); +} + +void Texture::write(WGPUQueue queue, const nucleus::utils::ColourTexture3D& data, glm::uvec3 offset, uint32_t base_mip_level) +{ + assert(offset.x % 4 == 0); + assert(offset.y % 4 == 0); + assert(data.width() % 4 == 0); + assert(data.height() % 4 == 0); + WGPUTexelCopyTextureInfo image_copy_texture {}; + image_copy_texture.texture = m_handle; + image_copy_texture.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + image_copy_texture.mipLevel = base_mip_level; + image_copy_texture.origin = WGPUOrigin3D { offset.x, offset.y, offset.z }; + + WGPUTexelCopyBufferLayout texture_data_layout {}; + switch (data.format()) { + case nucleus::utils::ColourTexture3D::Format::R8_UNORM: + texture_data_layout.bytesPerRow = data.width(); + texture_data_layout.rowsPerImage = data.height(); + break; + case nucleus::utils::ColourTexture3D::Format::BC4_UNORM: + texture_data_layout.bytesPerRow = ((data.width() + 3) / 4) * 8; // for BC4 its ceil(width/4) * 8 + texture_data_layout.rowsPerImage = (data.height() + 3) / 4; // Also different for BC4 + break; + default: + assert(false && "Texture format not Implemented"); + } + texture_data_layout.offset = 0; + + WGPUExtent3D copy_extent { data.width(), data.height(), data.depth() }; + + wgpuQueueWriteTexture(queue, &image_copy_texture, data.data(), data.n_bytes(), &texture_data_layout, ©_extent); +} + +void Texture::copy_to_texture(WGPUCommandEncoder encoder, uint32_t source_layer, const Texture& target_texture, uint32_t target_layer) const +{ + WGPUTexelCopyTextureInfo source {}; + source.texture = m_handle; + source.mipLevel = 0; + source.origin = WGPUOrigin3D { .x = 0, .y = 0, .z = source_layer }; + source.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyTextureInfo destination {}; + destination.texture = target_texture.handle(); + destination.mipLevel = 0; + destination.origin = WGPUOrigin3D { .x = 0, .y = 0, .z = target_layer }; + destination.aspect = WGPUTextureAspect_All; + + const WGPUExtent3D extent { .width = m_descriptor.size.width, .height = m_descriptor.size.height, .depthOrArrayLayers = 1 }; + wgpuCommandEncoderCopyTextureToTexture(encoder, &source, &destination, &extent); +} + +void Texture::read_back_async(WGPUDevice device, size_t layer_index, ReadBackCallback callback) const +{ + ReadBackState* read_back_state = new ReadBackState { + this, + std::make_unique>( + device, WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead, single_layer_size_in_bytes(), "texture read back staging buffer"), + callback, + layer_index, + }; + + copy_to_buffer(device, *(read_back_state->buffer), glm::uvec3(0, 0, uint32_t(layer_index))); + + auto on_buffer_mapped = [](WGPUMapAsyncStatus status, WGPUStringView message, void* user_data, [[maybe_unused]] void* user_data2) { + ReadBackState* current_state = reinterpret_cast(user_data); + + if (status != WGPUMapAsyncStatus_Success) { + qCritical() << "error: failed mapping buffer for ComputeTileStorage read back, message: " << message.data; + delete current_state; + return; + } + + const Texture* texture = current_state->texture; + const char* buffer_data = (const char*)wgpuBufferGetConstMappedRange(current_state->buffer->handle(), 0, current_state->buffer->size_in_byte()); + auto array = std::make_shared(); + for (uint32_t i = 0; i < texture->m_descriptor.size.height; i++) { + array->append(&buffer_data[i * texture->bytes_per_row()], texture->m_descriptor.size.width * get_bytes_per_element(texture->m_descriptor.format)); + } + + current_state->callback(current_state->layer_index, array); + wgpuBufferUnmap(current_state->buffer->handle()); + + delete current_state; + }; + + WGPUBufferMapCallbackInfo on_buffer_mapped_callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_buffer_mapped, + .userdata1 = read_back_state, + .userdata2 = nullptr, + }; + + wgpuBufferMapAsync( + read_back_state->buffer->handle(), WGPUMapMode_Read, 0, uint32_t(read_back_state->buffer->size_in_byte()), on_buffer_mapped_callback_info); +} + +void Texture::save_to_file(WGPUDevice device, const std::string& filename, size_t layer_index) +{ + read_back_async(device, layer_index, [this, filename]([[maybe_unused]] size_t layer_index, std::shared_ptr data) { + switch (this->m_descriptor.format) { + case WGPUTextureFormat::WGPUTextureFormat_RGBA8Unorm: + case WGPUTextureFormat::WGPUTextureFormat_RGBA8UnormSrgb: + case WGPUTextureFormat::WGPUTextureFormat_RGBA8Uint: + nucleus::utils::image_writer::rgba8_as_png(*data, glm::uvec2(width(), height()), QString::fromStdString(filename)); + break; + + // NOTE: If single float format we output the min/max/avg values (for the possibility of a first check + // and then crop the data and normalize it such that we can write it as uint32_t split into the rgb channels. + // We do the same with the overlays, and therefore can use the python script to convert + // this image back to a tiff for investigation. + case WGPUTextureFormat::WGPUTextureFormat_R32Float: { + const float* float_data = reinterpret_cast(data->data()); + const size_t num_floats = data->size() / sizeof(float); + nucleus::Raster raster(glm::uvec2(width(), height())); + std::copy(float_data, float_data + num_floats, raster.buffer().data()); + nucleus::utils::geopng::write_encoded_float_png(raster, QString::fromStdString(filename)); + } break; + + default: + qCritical() << "Cannot save texture to file: unsupported format."; + return; + } + + qDebug() << "Texture saved to file: " << QString::fromStdString(filename); + }); +} + +WGPUTextureViewDescriptor Texture::default_texture_view_descriptor() const +{ + // TODO make utility function + auto determineViewDimension = [](const WGPUTextureDescriptor& texture_desc) { + if (texture_desc.dimension == WGPUTextureDimension_1D) + return WGPUTextureViewDimension_1D; + else if (texture_desc.dimension == WGPUTextureDimension_3D) + return WGPUTextureViewDimension_3D; + else if (texture_desc.dimension == WGPUTextureDimension_2D) { + return texture_desc.size.depthOrArrayLayers > 1 ? WGPUTextureViewDimension_2DArray : WGPUTextureViewDimension_2D; + // note: if texture_desc.size.depthOrArrayLayers is 6, the view type can also be WGPUTextureViewDimension_Cube + // or, for any multiple of 6 the view type could be WGPUTextureViewDimension_CubeArray - we don't support this here for now + } + return WGPUTextureViewDimension_Undefined; // hopefully this logs an error when webgpu is validating + }; + + WGPUTextureViewDescriptor view_desc {}; + view_desc.aspect = WGPUTextureAspect_All; + view_desc.dimension = determineViewDimension(m_descriptor); + view_desc.format = m_descriptor.format; + view_desc.baseArrayLayer = 0; + // arrayLayerCount must be 1 for 3d textures, webGPU does not (yet) support 3d texture arrays + view_desc.arrayLayerCount = m_descriptor.dimension == WGPUTextureDimension_3D ? 1u : m_descriptor.size.depthOrArrayLayers; + view_desc.baseMipLevel = 0; + view_desc.mipLevelCount = m_descriptor.mipLevelCount; + // m_descriptor.mipLevelCount; + return view_desc; +} + +std::unique_ptr Texture::create_view() const { return create_view(default_texture_view_descriptor()); } + +std::unique_ptr Texture::create_view(const WGPUTextureViewDescriptor& desc) const { return std::make_unique(m_handle, desc); } + +size_t Texture::width() const { return m_descriptor.size.width; } + +size_t Texture::height() const { return m_descriptor.size.height; } + +size_t Texture::depth_or_num_layers() const { return m_descriptor.size.depthOrArrayLayers; } + +uint32_t Texture::mip_level_count() const { return m_descriptor.mipLevelCount; } + +size_t Texture::size_in_bytes() const { return single_layer_size_in_bytes() * m_descriptor.size.depthOrArrayLayers; } + +size_t Texture::bytes_per_row() const +{ + return size_t(std::ceil(double(m_descriptor.size.width) * double(get_bytes_per_element(m_descriptor.format)) / double(BYTES_PER_ROW_PADDING)) + * BYTES_PER_ROW_PADDING); // rows are padded to 256 bytes +} + +size_t Texture::single_layer_size_in_bytes() const { return bytes_per_row() * m_descriptor.size.height; } + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/Texture.h b/webgpu/base/raii/Texture.h new file mode 100644 index 000000000..1454a0c3c --- /dev/null +++ b/webgpu/base/raii/Texture.h @@ -0,0 +1,151 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "RawBuffer.h" +#include "TextureView.h" +#include "base_types.h" +#include "nucleus/Raster.h" +#include "nucleus/utils/ColourTexture.h" +#include "nucleus/utils/ColourTexture3D.h" + +#include + +namespace webgpu::raii { + +/// Represents (web)GPU texture. +/// Provides RAII semantics without ref-counting (free memory on deletion, disallow copy). +/// Preferably to be used with std::unique_ptr or std::shared_ptr. +class Texture : public GpuResource { +public: + using ReadBackCallback = std::function)>; + + struct ReadBackState { + const Texture* texture; + std::unique_ptr> buffer; + ReadBackCallback callback; + size_t layer_index; + }; + + static uint8_t get_bytes_per_element(WGPUTextureFormat format); + static uint32_t max_mip_level_count(glm::uvec2 size); + + static const uint16_t BYTES_PER_ROW_PADDING; + +public: + using GpuResource::GpuResource; + + template void write(WGPUQueue queue, const nucleus::Raster& data, uint32_t layer = 0) + { + // TODO maybe assert if RasterElementT and WGPUTextureFormat of this texture are compatible? + + assert(static_cast(data.width()) == m_descriptor.size.width); + assert(static_cast(data.height()) == m_descriptor.size.height); + + WGPUTexelCopyTextureInfo texel_copy_texture_info {}; + texel_copy_texture_info.texture = m_handle; + texel_copy_texture_info.mipLevel = 0; + texel_copy_texture_info.origin = WGPUOrigin3D { 0, 0, layer }; + texel_copy_texture_info.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + + WGPUTexelCopyBufferLayout texture_data_layout_info {}; + texture_data_layout_info.offset = 0; + texture_data_layout_info.bytesPerRow = uint32_t(sizeof(RasterElementT) * data.width()); + texture_data_layout_info.rowsPerImage = uint32_t(data.height()); + + WGPUExtent3D copy_extent { m_descriptor.size.width, m_descriptor.size.height, 1 }; + + wgpuQueueWriteTexture(queue, &texel_copy_texture_info, data.bytes(), uint32_t(data.size_in_bytes()), &texture_data_layout_info, ©_extent); + } + + void write(WGPUQueue queue, const nucleus::utils::ColourTexture& data, uint32_t layer = 0); + void write(WGPUQueue queue, const nucleus::utils::ColourTexture3D& data, glm::uvec3 offset = glm::uvec3(0), uint32_t base_mip_level = 0); + + // submits to default queue of device + template + void copy_to_buffer(WGPUDevice device, const RawBuffer& buffer, glm::uvec3 origin = glm::uvec3(0), glm::uvec2 extent = glm::uvec2(0)) const + { + WGPUCommandEncoderDescriptor desc {}; + desc.label = WGPUStringView { .data = "copy texture to buffer command encoder", .length = WGPU_STRLEN }; + raii::CommandEncoder encoder(device, desc); + copy_to_buffer(encoder.handle(), buffer, origin, extent); + WGPUCommandBufferDescriptor cmd_buffer_desc {}; + WGPUCommandBuffer cmd_buffer = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_desc); + WGPUQueue queue = wgpuDeviceGetQueue(device); + wgpuQueueSubmit(queue, 1, &cmd_buffer); + // TODO release cmd buffer -> use raii + } + + template + void copy_to_buffer(WGPUCommandEncoder encoder, const RawBuffer& buffer, glm::uvec3 origin = glm::uvec3(0), glm::uvec2 extent = glm::uvec2(0)) const + { + if (extent.x == 0 || extent.y == 0) { + extent = glm::uvec2(m_descriptor.size.width, m_descriptor.size.height); + } + + // the row for the destination buffer needs to be aligned to 256 byte. Meaning: If we have + // a texture that does not fit those requirements we need to have a buffer with appropriate + // padding per row. + uint32_t bytes_per_extent_row = uint32_t( + std::ceil(double(extent.x) * double(get_bytes_per_element(m_descriptor.format)) / double(BYTES_PER_ROW_PADDING)) * BYTES_PER_ROW_PADDING); + + assert(bytes_per_extent_row * extent.y <= buffer.size_in_byte()); + + WGPUTexelCopyTextureInfo source {}; + source.texture = m_handle; + source.mipLevel = 0; + source.origin = { .x = origin.x, .y = origin.y, .z = origin.z }; + source.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyBufferInfo destination {}; + destination.buffer = buffer.handle(); + destination.layout.offset = 0; + destination.layout.bytesPerRow = bytes_per_extent_row; // this has to be a multiple of 256 + destination.layout.rowsPerImage = extent.y; + + const WGPUExtent3D wgpu_extent { .width = extent.x, .height = extent.y, .depthOrArrayLayers = 1 }; + + wgpuCommandEncoderCopyTextureToBuffer(encoder, &source, &destination, &wgpu_extent); + } + + void copy_to_texture(WGPUCommandEncoder encoder, uint32_t source_layer, const Texture& target_texture, uint32_t target_layer = 0) const; + + /// read back single texture layer of this texture + void read_back_async(WGPUDevice device, size_t layer_index, ReadBackCallback callback) const; + + /// should only be used for debugging purposes + void save_to_file(WGPUDevice device, const std::string& filename, size_t layer_index = 0); + + WGPUTextureViewDescriptor default_texture_view_descriptor() const; + + std::unique_ptr create_view() const; + std::unique_ptr create_view(const WGPUTextureViewDescriptor& desc) const; + + size_t width() const; + size_t height() const; + size_t depth_or_num_layers() const; + uint32_t mip_level_count() const; + size_t size_in_bytes() const; + size_t bytes_per_row() const; + size_t single_layer_size_in_bytes() const; + +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/TextureView.cpp b/webgpu/base/raii/TextureView.cpp new file mode 100644 index 000000000..03a94a325 --- /dev/null +++ b/webgpu/base/raii/TextureView.cpp @@ -0,0 +1,33 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TextureView.h" + +namespace webgpu::raii { + +WGPUBindGroupEntry TextureView::create_bind_group_entry(uint32_t binding) const +{ + WGPUBindGroupEntry entry {}; + entry.binding = binding; + entry.textureView = m_handle; + entry.offset = 0; + entry.nextInChain = nullptr; + return entry; +} + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/TextureView.h b/webgpu/base/raii/TextureView.h new file mode 100644 index 000000000..91e03e90a --- /dev/null +++ b/webgpu/base/raii/TextureView.h @@ -0,0 +1,32 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "base_types.h" + +namespace webgpu::raii { + +class TextureView : public GpuResource { +public: + using GpuResource::GpuResource; + + WGPUBindGroupEntry create_bind_group_entry(uint32_t binding) const; +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/TextureWithSampler.cpp b/webgpu/base/raii/TextureWithSampler.cpp new file mode 100644 index 000000000..aee8503ff --- /dev/null +++ b/webgpu/base/raii/TextureWithSampler.cpp @@ -0,0 +1,38 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TextureWithSampler.h" + +namespace webgpu::raii { + +TextureWithSampler::TextureWithSampler(WGPUDevice device, const WGPUTextureDescriptor& texture_desc, const WGPUSamplerDescriptor& sampler_desc) + : m_texture(std::make_unique(device, texture_desc)) + , m_texture_view(m_texture->create_view()) + , m_sampler(std::make_unique(device, sampler_desc)) +{ +} + +Texture& TextureWithSampler::texture() { return *m_texture; } + +const Texture& TextureWithSampler::texture() const { return *m_texture; } + +const TextureView& TextureWithSampler::texture_view() const { return *m_texture_view; } + +const Sampler& TextureWithSampler::sampler() const { return *m_sampler; } + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/TextureWithSampler.h b/webgpu/base/raii/TextureWithSampler.h new file mode 100644 index 000000000..ea02e3583 --- /dev/null +++ b/webgpu/base/raii/TextureWithSampler.h @@ -0,0 +1,44 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Sampler.h" +#include "Texture.h" +#include "TextureView.h" +#include + +namespace webgpu::raii { + +/// Convenience wrapper for a texture and a sampler sampling from the default view of that texture. +class TextureWithSampler { +public: + TextureWithSampler(WGPUDevice device, const WGPUTextureDescriptor& texture_desc, const WGPUSamplerDescriptor& sampler_desc); + + Texture& texture(); + const Texture& texture() const; + const TextureView& texture_view() const; + const Sampler& sampler() const; + +private: + std::unique_ptr m_texture; + std::unique_ptr m_texture_view; + std::unique_ptr m_sampler; +}; + +} // namespace webgpu::raii diff --git a/webgpu/base/raii/base_types.h b/webgpu/base/raii/base_types.h new file mode 100644 index 000000000..37e482f60 --- /dev/null +++ b/webgpu/base/raii/base_types.h @@ -0,0 +1,165 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu::raii { + +template struct GpuFuncs { +public: + // is_same_v needed to prevent evaluation of default template instantiation + // see https://stackoverflow.com/questions/5246049/c11-static-assert-and-template-instantiation + static HandleT create(ContextT, const DescriptorT&) { static_assert(std::is_same_v, "default instantiation should not be used!"); }; + static void release(HandleT) { static_assert(std::is_same_v, "default instantiation should not be used!"); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateTexture(context, &descriptor); } + static void release(auto handle) + { + wgpuTextureDestroy(handle); + wgpuTextureRelease(handle); + } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuTextureCreateView(context, &descriptor); } + static void release(auto handle) { wgpuTextureViewRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateSampler(context, &descriptor); } + static void release(auto handle) { wgpuSamplerRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateBuffer(context, &descriptor); } + static void release(auto handle) + { + wgpuBufferDestroy(handle); + wgpuBufferRelease(handle); + } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateShaderModule(context, &descriptor); } + static void release(auto handle) { wgpuShaderModuleRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateBindGroup(context, &descriptor); } + static void release(auto handle) { wgpuBindGroupRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateBindGroupLayout(context, &descriptor); } + static void release(auto handle) { wgpuBindGroupLayoutRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreatePipelineLayout(context, &descriptor); } + static void release(auto handle) { wgpuPipelineLayoutRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateRenderPipeline(context, &descriptor); } + static void release(auto handle) { wgpuRenderPipelineRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuCommandEncoderBeginRenderPass(context, &descriptor); } + static void release(auto handle) + { + wgpuRenderPassEncoderEnd(handle); + wgpuRenderPassEncoderRelease(handle); + } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuCommandEncoderBeginComputePass(context, &descriptor); } + static void release(auto handle) + { + wgpuComputePassEncoderEnd(handle); + wgpuComputePassEncoderRelease(handle); + } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateComputePipeline(context, &descriptor); } + static void release(auto handle) { wgpuComputePipelineRelease(handle); } +}; + +template <> struct GpuFuncs { + static auto create(auto context, auto descriptor) { return wgpuDeviceCreateCommandEncoder(context, &descriptor); } + static void release(auto handle) { wgpuCommandEncoderRelease(handle); } +}; + +/// TODO document +/// Represents a (web)GPU render pipeline object. +/// Provides RAII semantics without ref-counting (free memory on deletion, disallow copy). +template class GpuResource { +public: + GpuResource(ContextHandleT context, const DescriptorT& descriptor) + : m_handle(GpuFuncs::create(context, descriptor)) + , m_descriptor(descriptor) + { + // Warning/ToDo: m_descriptor might contain pointers to memory that is managed by the caller. + // To make sure all of the pointer types inside the descriptor should be set to nullptr. + // It might be possible with some dark template magic. If not we'll leave it as is, but + // need to be aware that such pointers might cause issues. + } + + ~GpuResource() { GpuFuncs::release(m_handle); } + + // delete copy constructor and copy-assignment operator + GpuResource(const GpuResource& other) = delete; + GpuResource& operator=(const GpuResource& other) = delete; + + HandleT handle() const { return m_handle; } + DescriptorT descriptor() const { return m_descriptor; } + +protected: + HandleT m_handle; + DescriptorT m_descriptor; +}; + +// using BindGroup = GpuResource; +// using BindGroupLayout = GpuResource; +using ShaderModule = GpuResource; +// using PipelineLayout = GpuResource; +using RenderPipeline = GpuResource; +// using Buffer = GpuResource; +// using Texture = GpuResource; +// using TextureView = GpuResource; +// using Sampler = GpuResource; +// using RenderPassEncoder = GpuResource; +using ComputePipeline = GpuResource; +using ComputePassEncoder = GpuResource; +using CommandEncoder = GpuResource; + +// Also SwapChain is different because it needs Device and surface as parameters... +//using SwapChain = GpuResource; + +// Surface is not as easy, as we get it from sdl +// using Surface = GpuResource; + +} // namespace webgpu::raii diff --git a/webgpu/base/shaders/encoder.wgsl b/webgpu/base/shaders/encoder.wgsl new file mode 100644 index 000000000..8d72967da --- /dev/null +++ b/webgpu/base/shaders/encoder.wgsl @@ -0,0 +1,104 @@ +/***************************************************************************** +* Alpine Renderer +* Copyright (C) 2022 Adam Celarek +* Copyright (C) 2023 Gerald Kimmersdorfer +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +// Implementation of the octahedral normal encoding based on the paper +// "A Survey of Efficient Representations for Independent Unit Vector" by Cigolle et al. +// see https://jcgt.org/published/0003/02/01/ listings 1 and 2 + +// Custom implementation of the sign function for a vec2f which in contrast to the standard sign function +// returns 1.0 for 0.0 as input. +// - v: A vec2f representing the input vector. +// - Returns: A vec2f containing the componentwise sign of the input vector. +fn signNotZero(v: vec2f) -> vec2f { + return sign(v) + vec2f(v == vec2f(0.0)); +} + +// Converts a normalized 3D vector into 2D octahedral coordinates. +// - n: A normalized vec3f representing the input normal vector. +// - Returns: A vec2f representing the normal vector encoded in the range [-1, 1] in an octahedral projection. +// - Requirement: n must be normalized. +fn v3f32_to_oct(n: vec3) -> vec2 { + var en: vec2 = n.xy * (1.0 / (abs(n.x) + abs(n.y) + abs(n.z))); + if n.z <= 0.0 { + en = (1.0 - abs(en.yx)) * signNotZero(en); + } + return en; +} + +// Converts 2D octahedral coordinates back to a normalized 3D vector. +// - en: A vec2f representing the octahedral projection coordinates. +// - Returns: A normalized vec3f containing the decoded normal vector. +fn oct_to_v3f32(en: vec2) -> vec3 { + var n: vec3 = vec3f(en.xy, 1.0 - abs(en.x) - abs(en.y)); + if n.z < 0.0 { + n = vec3f((1.0 - abs(n.yx)) * signNotZero(n.xy), n.z); + } + return normalize(n); +} + +// Encodes a 2D vector into a 2D unsigned integer vector using a 16-bit range. +// - v: A vec2f representing the input vector in the range [0, 1]. +// - Returns: A vec2u32 representing the encoded values with each component scaled to the range [0, 65535]. +// - Requirement: v must be in the range [0, 1]. +fn v2f32_to_v2u16(v: vec2) -> vec2 { + return vec2(u32(v.x * 65535.0), u32(v.y * 65535.0)); +} + +// Decodes a 2D unsigned integer vector into a normalized 2D vector in the range [0, 1]. +// - e: A vec2u32 representing the encoded values with each component in the range [0, 65535]. +// - Returns: A vec2f representing the decoded values in the range [0, 1]. +fn v2u16_to_v2f32(e: vec2) -> vec2 { + return vec2(f32(e.x) / 65535.0, f32(e.y) / 65535.0); +} + +// Encodes a normalized 3D vector into a 2D unsigned integer vector using octahedral projection. +// - n: A normalized vec3f representing the input normal vector. +// - Returns: A vec2u32 representing the encoded values using octahedral projection with each component in the range [0, 65535]. +// - Requirement: n must be normalized. +fn octNormalEncode2u16(n: vec3) -> vec2 { + return v2f32_to_v2u16(fma(v3f32_to_oct(n), vec2f(0.5), vec2f(0.5))); +} + +// Decodes a 2D unsigned integer vector into a normalized 3D vector using octahedral projection. +// - e: A vec2u32 representing the encoded octahedral projection coordinates with each component in the range [0, 65535]. +// - Returns: A vec3f representing the normalized decoded normal vector. +fn octNormalDecode2u16(e: vec2) -> vec3 { + return oct_to_v3f32(fma(v2u16_to_v2f32(e), vec2f(2.0), vec2f(- 1.0))); +} + +// Common range constants +const U32_ENCODING_RANGE_NORM: vec2f = vec2f(0.0, 1.0); + +// Encodes a float value into a 32-bit unsigned integer using a range +// - value: Input value to encode +// - range: vec2f containing (min, max) of input range (default: [0,1]) +// - Returns: u32 in [0, 4294967295] +fn range_to_u32(value: f32, range: vec2f) -> u32 { + let normalized = clamp((value - range.x) / (range.y - range.x), 0.0, 1.0); + return u32(normalized * f32(0xFFFFFFFFu)); +} + +// Decodes a u32 back to original range +// - encoded: u32 to decode +// - range: vec2f containing (min, max) of output range (default: [0,1]) +// - Returns: Reconstructed float value +fn u32_to_range(encoded: u32, range: vec2f) -> f32 { + let normalized = f32(encoded) / f32(0xFFFFFFFFu); + return mix(range.x, range.y, normalized); +} \ No newline at end of file diff --git a/webgpu/base/shaders/filtering.wgsl b/webgpu/base/shaders/filtering.wgsl new file mode 100644 index 000000000..5dd527000 --- /dev/null +++ b/webgpu/base/shaders/filtering.wgsl @@ -0,0 +1,49 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +fn bilinear_sample_vec4f(texture_array: texture_2d_array, texture_sampler: sampler, uv: vec2f, layer: u32) -> vec4f { + let texture_dimensions: vec2u = textureDimensions(texture_array); + + // weights need to match the texels that are chosen by textureGather - this does NOT align perfectly, and introduces some artifacts + // adding offset fixes this issue, see https://www.reedbeta.com/blog/texture-gathers-and-coordinate-precision/ + const offset = 1.0f / 512.0f; + + let weights: vec2f = fract(uv * vec2f(texture_dimensions) - 0.5 + offset); + + let x = dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), + vec4f(textureGather(0, texture_array, texture_sampler, uv, layer))); + let y = dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), + vec4f(textureGather(1, texture_array, texture_sampler, uv, layer))); + let z = dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), + vec4f(textureGather(2, texture_array, texture_sampler, uv, layer))); + let w = dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), + vec4f(textureGather(3, texture_array, texture_sampler, uv, layer))); + + return vec4f(x, y, z, w); +} + +fn bilinear_sample_u32(texture_array: texture_2d_array, texture_sampler: sampler, uv: vec2f, layer: u32) -> u32 { + // weights need to match the texels that are chosen by textureGather - this does NOT align perfectly, and introduces some artifacts + // adding offset fixes this issue, see https://www.reedbeta.com/blog/texture-gathers-and-coordinate-precision/ + const TEXTURE_GATHER_OFFSET = 1.0f / 512.0f; + + let texture_dimensions = textureDimensions(texture_array); + let weights: vec2f = fract(uv * vec2f(texture_dimensions) - 0.5 + TEXTURE_GATHER_OFFSET); // -0.5 to make relative to texel center + let texel_values = vec4f(textureGather(0, texture_array, texture_sampler, uv, layer)); + return u32(dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), texel_values)); +} \ No newline at end of file diff --git a/webgpu/base/shaders/general.wgsl b/webgpu/base/shaders/general.wgsl new file mode 100644 index 000000000..e1013bd5a --- /dev/null +++ b/webgpu/base/shaders/general.wgsl @@ -0,0 +1,37 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2026 Gerald Kimmersdorfer +* Copyright (C) 2024 Patrick Komon +* Copyright (C) 2022 Adam Celarek +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +//General-purpose, dependency-free helper functions shared across shaders. + +//Linear falloff from 1.0 at `lower` to 0.0 at `upper`, clamped to [0, 1]. +fn calculate_falloff(dist: f32, lower: f32, upper: f32) -> f32 { + return clamp(1.0 - (dist - lower) / (upper - lower), 0.0, 1.0); +} + +//Smoothed band: 1.0 inside [min, max], falling off over `smoothf` beyond each edge. +fn calculate_band_falloff(val: f32, min: f32, max: f32, smoothf: f32) -> f32 { + if val < min { + return calculate_falloff(val, min + smoothf, min); + } else if val > max { + return calculate_falloff(val, max, max + smoothf); + } else { + return 1.0; + } +} diff --git a/webgpu/base/shaders/hashing.wgsl b/webgpu/base/shaders/hashing.wgsl new file mode 100644 index 000000000..c2a887661 --- /dev/null +++ b/webgpu/base/shaders/hashing.wgsl @@ -0,0 +1,33 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +fn compute_hash(a: u32) -> u32 { + var b = (a + 2127912214u) + (a << 12u); + b = (b ^ 3345072700u) ^ (b >> 19u); + b = (b + 374761393u) + (b << 5u); + b = (b + 3551683692u) ^ (b << 9u); + b = (b + 4251993797u) + (b << 3u); + b = (b ^ 3042660105u) ^ (b >> 16u); + return b; +} + +fn color_from_id_hash(a: u32) -> vec3f { + var hash = compute_hash(a); + return vec3f(f32(hash & 255u), f32((hash >> 8u) & 255u), f32((hash >> 16u) & 255u)) / 255.0; +} diff --git a/webgpu/base/shaders/mipmap.wgsl b/webgpu/base/shaders/mipmap.wgsl new file mode 100644 index 000000000..7a09eb4c9 --- /dev/null +++ b/webgpu/base/shaders/mipmap.wgsl @@ -0,0 +1,30 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +@group(0) @binding(0) var previousMipLevel: texture_2d; +@group(0) @binding(1) var nextMipLevel: texture_storage_2d; + +@compute @workgroup_size(8, 8, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + let offset = vec2(0, 1); + let color = (textureLoad(previousMipLevel, 2 * id.xy + offset.xx, 0) + + textureLoad(previousMipLevel, 2 * id.xy + offset.xy, 0) + + textureLoad(previousMipLevel, 2 * id.xy + offset.yx, 0) + + textureLoad(previousMipLevel, 2 * id.xy + offset.yy, 0)) * 0.25; + textureStore(nextMipLevel, id.xy, color); +} diff --git a/webgpu/base/shaders/noise.wgsl b/webgpu/base/shaders/noise.wgsl new file mode 100644 index 000000000..2c1f09bfd --- /dev/null +++ b/webgpu/base/shaders/noise.wgsl @@ -0,0 +1,42 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +fn mod289_f32(x: f32) -> f32 { return x - floor(x * (1.0 / 289.0)) * 289.0; } +fn mod289_vec3f(x: vec3f) -> vec3f { return x - floor(x * (1.0 / 289.0)) * 289.0; } +fn mod289_vec4f(x: vec4f) -> vec4f { return x - floor(x * (1.0 / 289.0)) * 289.0; } +fn perm(x: vec4f) -> vec4f { return mod289_vec4f(((x * 34.0) + 1.0) * x); } + +fn noise(p: vec3f) -> f32 { + let p1 = mod289_vec3f(p); + let a = floor(p1); + var d = p1 - a; + d = d * d * (3.0 - 2.0 * d); + let b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); + let k1 = perm(b.xyxy); + let k2 = perm(k1.xyxy + b.zzww); + let c = k2 + a.zzzz; + let k3 = perm(c); + let k4 = perm(c + 1.0); + let o1 = fract(k3 * (1.0 / 41.0)); + let o2 = fract(k4 * (1.0 / 41.0)); + let o3 = o2 * d.z + o1 * (1.0 - d.z); + let o4 = o3.yw * d.x + o3.xz * (1.0 - d.x); + return o4.y * d.y + o4.x * (1.0 - d.y); +} diff --git a/webgpu/base/shaders/normals_util.wgsl b/webgpu/base/shaders/normals_util.wgsl new file mode 100644 index 000000000..ac16c06c4 --- /dev/null +++ b/webgpu/base/shaders/normals_util.wgsl @@ -0,0 +1,105 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use tile_util + +fn normal_by_finite_difference_method( + uv: vec2, + quad_width: f32, + quad_height: f32, + altitude_correction_factor: f32, + texture_layer: i32, + texture_array: texture_2d_array +) -> vec3 { + let height_texture_size = textureDimensions(texture_array); + // from here: https://stackoverflow.com/questions/6656358/calculating-normals-in-a-triangle-mesh/21660173#21660173 + + // 0 is texel center of first texel, 1 is texel center of last texel + let uv_tex = vec2i(floor(uv * vec2f(height_texture_size - 1) + 0.5)); + + let upper_bounds = vec2(height_texture_size - 1); + let lower_bounds = vec2(0, 0); + let hL_uv = clamp(uv_tex - vec2(1, 0), lower_bounds, upper_bounds); + let hL_sample = textureLoad(texture_array, hL_uv, texture_layer, 0); + let hL = f32(hL_sample.r) * altitude_correction_factor; + + let hR_uv = clamp(uv_tex + vec2(1, 0), lower_bounds, upper_bounds); + let hR_sample = textureLoad(texture_array, hR_uv, texture_layer, 0); + let hR = f32(hR_sample.r) * altitude_correction_factor; + + let hD_uv = clamp(uv_tex + vec2(0, 1), lower_bounds, upper_bounds); + let hD_sample = textureLoad(texture_array, hD_uv, texture_layer, 0); + let hD = f32(hD_sample.r) * altitude_correction_factor; + + let hU_uv = clamp(uv_tex - vec2(0, 1), lower_bounds, upper_bounds); + let hU_sample = textureLoad(texture_array, hU_uv, texture_layer, 0); + let hU = f32(hU_sample.r) * altitude_correction_factor; + + let threshold = 0.5 / (vec2f(height_texture_size) - 1.0); + + // half on the edge of a packed_tile_id, as the height texture is clamped + var actual_quad_width = select(quad_width, quad_width / 2, uv.x < threshold.x || uv.x > 1.0 - threshold.x); + var actual_quad_height = select(quad_height, quad_height / 2, uv.y < threshold.y || uv.y > 1.0 - threshold.y); + + return normalize(vec3f((hL - hR) / actual_quad_width, (hD - hU) / actual_quad_height, 2.0)); +} + +fn normal_by_finite_difference_method_texture_f32( + pos: vec2, + quad_width: f32, + quad_height: f32, + altitude_correction_factor: f32, + texture: texture_2d +) -> vec3 { + let height_texture_size = textureDimensions(texture); + // from here: https://stackoverflow.com/questions/6656358/calculating-normals-in-a-triangle-mesh/21660173#21660173 + let height = quad_width + quad_height; + + let upper_bounds = vec2(height_texture_size - 1); + let lower_bounds = vec2(0, 0); + let hL_uv = clamp(vec2i(pos) - vec2(1, 0), lower_bounds, upper_bounds); + let hL_sample = textureLoad(texture, hL_uv, 0); + let hL = f32(hL_sample.r) * altitude_correction_factor; + + let hR_uv = clamp(vec2i(pos) + vec2(1, 0), lower_bounds, upper_bounds); + let hR_sample = textureLoad(texture, hR_uv, 0); + let hR = f32(hR_sample.r) * altitude_correction_factor; + + let hD_uv = clamp(vec2i(pos) + vec2(0, 1), lower_bounds, upper_bounds); + let hD_sample = textureLoad(texture, hD_uv, 0); + let hD = f32(hD_sample.r) * altitude_correction_factor; + + let hU_uv = clamp(vec2i(pos) - vec2(0, 1), lower_bounds, upper_bounds); + let hU_sample = textureLoad(texture, hU_uv, 0); + let hU = f32(hU_sample.r) * altitude_correction_factor; + + return normalize(vec3(hL - hR, hD - hU, height)); +} + +fn get_gradient(normal: vec3f) -> vec3f { + let up = vec3f(0, 0, 1); + let right = cross(up, normal); + let gradient = cross(right, normal); + return gradient; +} + +// returns slope angle in radians based on surface normal (0 is horizontal, pi/2 is vertical) +fn get_slope_angle(normal: vec3f) -> f32 { + return acos(normal.z); +} \ No newline at end of file diff --git a/webgpu/base/shaders/snow.wgsl b/webgpu/base/shaders/snow.wgsl new file mode 100644 index 000000000..032f03c81 --- /dev/null +++ b/webgpu/base/shaders/snow.wgsl @@ -0,0 +1,50 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2023 Gerald Kimmersdorfer +* Copyright (C) 2024 Adam Celarek +* Copyright (C) 2024 Patrick Komon +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use noise +///use normals_util +///use general + +fn overlay_snow(normal: vec3f, pos_ws: vec3f, snow_settings_angle: vec4f, snow_settings_alt: vec4f) -> vec4f { + //Calculate steepness in deg where 90.0 = vertical (90°) and 0.0 = flat (0°) + let steepness_deg = degrees(get_slope_angle(normal)); + + let steepness_based_alpha = calculate_band_falloff( + steepness_deg, + snow_settings_angle.y, + snow_settings_angle.z, + snow_settings_angle.w + ); + + let lat_long_alt = world_to_lat_long_alt(pos_ws); + let pos_noise_hf = noise(pos_ws / 70.0); + let pos_noise_lf = noise(pos_ws / 500.0); + let snow_border = snow_settings_alt.x + + (snow_settings_alt.y * (2.0 * pos_noise_lf - 0.5)) + + (snow_settings_alt.y * (0.5 * (pos_noise_hf - 0.5))); + let altitude_based_alpha = calculate_falloff( + lat_long_alt.z, + snow_border, + snow_border - snow_settings_alt.z * pos_noise_lf + ); + + let snow_color = vec3f(1.0); + return vec4f(snow_color, altitude_based_alpha * steepness_based_alpha); +} diff --git a/webgpu/base/shaders/tile_util.wgsl b/webgpu/base/shaders/tile_util.wgsl new file mode 100644 index 000000000..7bfed7e48 --- /dev/null +++ b/webgpu/base/shaders/tile_util.wgsl @@ -0,0 +1,183 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022 Adam Celarek + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +const PI: f32 = 3.1415926535897932384626433; +const SEMI_MAJOR_AXIS: f32 = 6378137; +const EARTH_CIRCUMFERENCE: f32 = 2 * PI * SEMI_MAJOR_AXIS; +const ORIGIN_SHIFT: f32 = PI * SEMI_MAJOR_AXIS; + +struct TileId { + x: u32, + y: u32, + zoomlevel: u32, + alignment: u32, +} + +const EMPTY_TILE_ZOOMLEVEL: u32 = 4294967295u; + +fn tile_ids_equal(a: TileId, b: TileId) -> bool { return a.x == b.x && a.y == b.y && a.zoomlevel == b.zoomlevel; } +fn tile_id_empty(id: TileId) -> bool { return id.zoomlevel == EMPTY_TILE_ZOOMLEVEL; } + +struct Bounds { + min: vec2f, + max: vec2f, +} + +fn y_to_lat(y: f32) -> f32 { + let mercN = y * PI / ORIGIN_SHIFT; + let latRad = 2.f * (atan(exp(mercN)) - (PI / 4.0)); + return latRad; +} + +fn calc_altitude_correction_factor(y: f32) -> f32 { return 0.125 / cos(y_to_lat(y)); } + +// equivalent of nucleus::srs::number_of_horizontal_tiles_for_zoom_level +fn number_of_horizontal_tiles_for_zoom_level(z: u32) -> u32 { return u32(1 << z); } + +// equivalent of nucleus::srs::number_of_vertical_tiles_for_zoom_level +fn number_of_vertical_tiles_for_zoom_level(z: u32) -> u32 { return u32(1 << z); } + +// equivalent of nucleus::srs::tile_bounds(tile::Id) +fn calculate_bounds(tile_id: TileId) -> vec4 { + const absolute_min = vec2f(-ORIGIN_SHIFT, -ORIGIN_SHIFT); + let width_of_a_tile: f32 = EARTH_CIRCUMFERENCE / f32(number_of_horizontal_tiles_for_zoom_level(tile_id.zoomlevel)); + let height_of_a_tile: f32 = EARTH_CIRCUMFERENCE / f32(number_of_vertical_tiles_for_zoom_level(tile_id.zoomlevel)); + let min = absolute_min + vec2f(f32(tile_id.x) * width_of_a_tile, f32(tile_id.y) * height_of_a_tile); + let max = min + vec2f(width_of_a_tile, height_of_a_tile); + return vec4f(min.x, min.y, max.x, max.y); +} + +fn world_to_lat_long_alt(pos_ws: vec3f) -> vec3f { + let mercN = pos_ws.y * PI / ORIGIN_SHIFT; + let latRad = 2.0 * (atan(exp(mercN)) - (PI / 4.0)); + let latitude = latRad * 180.0 / PI; + let longitude = (pos_ws.x + ORIGIN_SHIFT) / (ORIGIN_SHIFT / 180.0) - 180.0; + let altitude = pos_ws.z * cos(latitude * PI / 180.0); + return vec3f(latitude, longitude, altitude); +} + +// ported from alpine maps +fn decrease_zoom_level_by_one( + input_tile_id: TileId, + input_uv: vec2f, + output_tile_id: ptr, + output_uv: ptr, +) -> bool { + if input_tile_id.zoomlevel == 0u { + return false; + } + + let x_border = f32(input_tile_id.x & 1u) / 2.0; + let y_border = f32((input_tile_id.y & 1u) == 0u) / 2.0; + + *output_tile_id = TileId(input_tile_id.x / 2u, input_tile_id.y / 2u, input_tile_id.zoomlevel - 1u, 0); + *output_uv = input_uv / 2.0 + vec2f(x_border, y_border); + + return true; +} + +// ported from alpine maps +fn decrease_zoom_level_until( + input_tile_id: TileId, + input_uv: vec2f, + zoomlevel: u32, + output_tile_id: ptr, + output_uv: ptr, +) -> bool { + if input_tile_id.zoomlevel <= zoomlevel { + *output_tile_id = input_tile_id; + *output_uv = input_uv; + return false; + } + + let z_delta: u32 = input_tile_id.zoomlevel - zoomlevel; + let border_mask: u32 = (1u << z_delta) - 1u; + let x_border = f32(input_tile_id.x & border_mask) / f32(1u << z_delta); + let y_border = f32((input_tile_id.y ^ border_mask) & border_mask) / f32(1u << z_delta); + + *output_tile_id = TileId( + input_tile_id.x >> z_delta, + input_tile_id.y >> z_delta, + input_tile_id.zoomlevel - z_delta, + 0 + ); + *output_uv = input_uv / f32(1u << z_delta) + vec2f(x_border, y_border); + + return true; +} + +fn increase_zoom_level_by_one( + input_tile_id: TileId, + input_uv: vec2f, + output_tile_id: ptr, + output_uv: ptr, +) -> bool { + if input_tile_id.zoomlevel == 18u { + return false; + } + + let x_border = select(0u, 1u, input_uv.x >= 0.5); + let y_border = select(0u, 1u, input_uv.y >= 0.5); + + var higher_zoomlevel_tile_id: TileId; + higher_zoomlevel_tile_id.x = 2u * input_tile_id.x + x_border; + higher_zoomlevel_tile_id.y = 2u * input_tile_id.y - y_border + 1; + higher_zoomlevel_tile_id.zoomlevel = input_tile_id.zoomlevel + 1u; + *output_tile_id = higher_zoomlevel_tile_id; + *output_uv = 2.0 * input_uv - vec2f(f32(x_border), f32(y_border)); + return true; +} + +//TODO optimize similarly to decrease_zoom_level_until +fn increase_zoom_level_until( + input_tile_id: TileId, + input_uv: vec2f, + zoomlevel: u32, + output_tile_id: ptr, + output_uv: ptr, +) -> bool { + if input_tile_id.zoomlevel >= zoomlevel { + return false; + } + + var output_zoomlevel = input_tile_id.zoomlevel; + while output_zoomlevel < zoomlevel { + increase_zoom_level_by_one(input_tile_id, input_uv, output_tile_id, output_uv); + output_zoomlevel++; + } + return true; +} + +fn calc_tile_id_and_uv_for_zoom_level( + input_tile_id: TileId, + input_uv: vec2f, + zoomlevel: u32, + output_tile_id: ptr, + output_uv: ptr, +) { + if input_tile_id.zoomlevel == zoomlevel { + *output_tile_id = input_tile_id; + *output_uv = input_uv; + } else if input_tile_id.zoomlevel < zoomlevel { + increase_zoom_level_until(input_tile_id, input_uv, zoomlevel, output_tile_id, output_uv); + } else { + decrease_zoom_level_until(input_tile_id, input_uv, zoomlevel, output_tile_id, output_uv); + } +} \ No newline at end of file diff --git a/webgpu/base/timing/CpuTimer.cpp b/webgpu/base/timing/CpuTimer.cpp new file mode 100644 index 000000000..d4bc92bd5 --- /dev/null +++ b/webgpu/base/timing/CpuTimer.cpp @@ -0,0 +1,37 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CpuTimer.h" + +namespace webgpu::timing { + +CpuTimer::CpuTimer(int queue_size) + : TimerInterface(queue_size) +{ +} + +void CpuTimer::start() { m_ticks[0] = std::chrono::high_resolution_clock::now(); } + +void CpuTimer::stop() +{ + m_ticks[1] = std::chrono::high_resolution_clock::now(); + const float duration = std::chrono::duration(m_ticks[1] - m_ticks[0]).count(); + add_result(duration); +} + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/CpuTimer.h b/webgpu/base/timing/CpuTimer.h new file mode 100644 index 000000000..4724869ce --- /dev/null +++ b/webgpu/base/timing/CpuTimer.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "TimerInterface.h" +#include "chrono" + +namespace webgpu::timing { + +/// The CpuTimer class measures times on the c++ side using the std::chronos library +class CpuTimer : public TimerInterface { +public: + CpuTimer(int queue_size); + + void start(); + + void stop(); + +protected: +private: + std::chrono::time_point m_ticks[2]; +}; + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/GuiTimerManager.cpp b/webgpu/base/timing/GuiTimerManager.cpp new file mode 100644 index 000000000..f9545bfa5 --- /dev/null +++ b/webgpu/base/timing/GuiTimerManager.cpp @@ -0,0 +1,43 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "GuiTimerManager.h" + +namespace webgpu::timing { + +GuiTimerManager::GuiTimerManager() = default; + +std::shared_ptr GuiTimerManager::add_timer(std::shared_ptr tmr) +{ + // Your implementation here if needed + return tmr; +} + +const GuiTimerWrapper* GuiTimerManager::get_timer_by_id(uint32_t timer_id) const +{ + for (const auto& group : m_groups) { + for (const auto& tmr : group.timers) { + if (tmr.timer->get_id() == timer_id) { + return &tmr; + } + } + } + return nullptr; +} + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/GuiTimerManager.h b/webgpu/base/timing/GuiTimerManager.h new file mode 100644 index 000000000..c3dbe7ebf --- /dev/null +++ b/webgpu/base/timing/GuiTimerManager.h @@ -0,0 +1,96 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "TimerInterface.h" +#include +#include +#include +#include +#include +#include + +namespace webgpu::timing { + +struct GuiTimerWrapper { + std::shared_ptr timer; + std::string name; + std::string group; + glm::vec4 color; +}; + +struct GuiTimerGroup { + std::string name; + std::vector timers; +}; + +class GuiTimerManager : public QObject { + Q_OBJECT + +public: + // Adds the given timer + std::shared_ptr add_timer(std::shared_ptr tmr); + + template ::value>> + void add_timer(std::shared_ptr tmr, const std::string& name, const std::string& group = "", const glm::vec4& color = glm::vec4(-1.0f)) + { + std::shared_ptr timer = std::dynamic_pointer_cast(tmr); + if (timer) { + auto tmrColor = color; + if (tmrColor.x < 0.0f) { + tmrColor = glm::vec4(timer_colors[timer->get_id() % 12], 1.0f); + } + auto it = std::find_if(m_groups.begin(), m_groups.end(), [&](const GuiTimerGroup& g) { return g.name == group; }); + if (it != m_groups.end()) { + it->timers.push_back({ timer, name, group, tmrColor }); + } else { + GuiTimerGroup newGroup { group, { { timer, name, group, tmrColor } } }; + m_groups.push_back(newGroup); + } + } else { + qCritical() << "Timer can't be added as it's not initialized correctly"; + } + } + [[nodiscard]] const GuiTimerWrapper* get_timer_by_id(uint32_t timer_id) const; + + [[nodiscard]] const std::vector& get_groups() const { return m_groups; } + + GuiTimerManager(); + +private: + // Contains the timer groups + std::vector m_groups; + + static constexpr glm::vec3 timer_colors[] = { + glm::vec3(1.0f, 0.0f, 0.0f), // red + glm::vec3(0.0f, 1.0f, 1.0f), // cyan + glm::vec3(0.49f, 0.0f, 1.0f), // violet + glm::vec3(0.49f, 1.0f, 0.0f), // spring green + glm::vec3(1.0f, 0.0f, 1.0f), // magenta + glm::vec3(0.0f, 0.49f, 1.0f), // ocean + glm::vec3(0.0f, 1.0f, 0.0f), // green + glm::vec3(1.0f, 0.49f, 0.0f), // orange + glm::vec3(0.0f, 0.0f, 1.0f), // blue + glm::vec3(0.0f, 1.0f, 0.49f), // turquoise + glm::vec3(1.0f, 1.0f, 0.0f), // yellow + glm::vec3(1.0f, 0.0f, 0.49f) // raspberry + }; +}; + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/TimerInterface.cpp b/webgpu/base/timing/TimerInterface.cpp new file mode 100644 index 000000000..d2c90989f --- /dev/null +++ b/webgpu/base/timing/TimerInterface.cpp @@ -0,0 +1,117 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TimerInterface.h" +#include +#include +#include +#include +#include + +namespace webgpu::timing { + +std::string format_time(float time, int precision) +{ + std::ostringstream oss; + if (time > 0.5) { + oss << std::setprecision(precision) << time << " s"; + } else if (time > 0.0005) { + oss << std::fixed << std::setprecision(precision) << time * 1000 << " ms"; + } else if (time > 0.0000005) { + oss << std::fixed << std::setprecision(precision) << time * 1000000 << " us"; + } else { + oss << std::fixed << std::setprecision(precision) << time * 1000000000 << " ns"; + } + return oss.str(); +} + +TimerInterface::TimerInterface(size_t capacity) + : m_capacity(capacity) + , m_id(s_next_id++) +{ + clear_results(); + m_results.reserve(capacity); +#ifdef ALP_ENABLE_TRACK_OBJECT_LIFECYCLE + qDebug() << "nucleus::timing::TimerInterface(name=" << get_name().c_str() << ")"; +#endif +} + +TimerInterface::~TimerInterface() +{ +#ifdef ALP_ENABLE_TRACK_OBJECT_LIFECYCLE + qDebug() << "nucleus::timing::~TimerInterface(name=" << get_name().c_str() << ")"; +#endif +} + +uint32_t TimerInterface::get_id() { return m_id; } +float TimerInterface::get_last_measurement() { return this->m_results.back(); } +size_t TimerInterface::get_capacity() { return m_capacity; } + +float TimerInterface::get_average() { return m_sum / m_results.size(); } + +float TimerInterface::get_standard_deviation() +{ + size_t n = m_results.size(); + if (n == 0) + return 0.0f; + float mean = m_sum / n; + return std::sqrt((m_sum_of_squares / n) - (mean * mean)); +} + +size_t TimerInterface::get_sample_count() const { return m_results.size(); } + +float TimerInterface::get_max() const { return m_max; } +float TimerInterface::get_min() const { return m_min; } + +void TimerInterface::clear_results() +{ + m_results.clear(); + m_sum = 0.0f; + m_sum_of_squares = 0.0f; + m_max = std::numeric_limits::min(); + m_min = std::numeric_limits::max(); +} + +std::string TimerInterface::to_string() +{ + std::ostringstream oss; + oss << "T" << get_id() << ": " << format_time(get_average()) << " ±" << format_time(get_standard_deviation()) << " [" << get_sample_count() << "]"; + return oss.str(); +} + +void TimerInterface::add_result(float result) +{ + if (m_results.size() == m_results.capacity()) { + float oldest = m_results.front(); + m_sum -= oldest; + m_sum_of_squares -= oldest * oldest; + m_results.erase(m_results.begin()); + } + m_results.push_back(result); + m_sum += result; + m_sum_of_squares += result * result; + m_max = std::max(m_max, result); + m_min = std::min(m_min, result); + emit tick(result); +} + +const std::vector& TimerInterface::get_results() const { return m_results; } + +uint32_t TimerInterface::s_next_id = 0; + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/TimerInterface.h b/webgpu/base/timing/TimerInterface.h new file mode 100644 index 000000000..ef8fdbdcb --- /dev/null +++ b/webgpu/base/timing/TimerInterface.h @@ -0,0 +1,71 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +namespace webgpu::timing { + +std::string format_time(float time, int precision = 2); + +class TimerInterface : public QObject { + Q_OBJECT + +public: + TimerInterface(size_t capacity); + virtual ~TimerInterface(); + + uint32_t get_id(); + float get_last_measurement(); + size_t get_capacity(); + + float get_average(); + float get_standard_deviation(); + size_t get_sample_count() const; + + float get_max() const; + float get_min() const; + + void clear_results(); + const std::vector& get_results() const; + + std::string to_string(); + +signals: + void tick(float result); + +protected: + void add_result(float result); + +private: + std::vector m_results; + size_t m_capacity; + float m_sum = 0.0f; + float m_sum_of_squares = 0.0f; + float m_max; + float m_min; + + uint32_t m_id; + + static uint32_t s_next_id; +}; + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/WebGpuTimer.cpp b/webgpu/base/timing/WebGpuTimer.cpp new file mode 100644 index 000000000..b0c740fdb --- /dev/null +++ b/webgpu/base/timing/WebGpuTimer.cpp @@ -0,0 +1,112 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "WebGpuTimer.h" + +#include + +namespace webgpu::timing { + +#ifdef QT_DEBUG +const char* readback_timer_names(uint32_t id) +{ + if (id == 0) + return "Timestamp Readback 1"; + else if (id == 1) + return "Timestamp Readback 2"; + else if (id == 2) + return "Timestamp Readback 3"; + else if (id == 3) + return "Timestamp Readback 4"; + else + return "Timestamp Readback X"; +} +#else +inline const char* readback_timer_names([[maybe_unused]] uint32_t id) { return "Timestamp Readback"; } +#endif + +WebGpuTimer::WebGpuTimer(WGPUDevice device, uint32_t ring_buffer_size, size_t capacity) + : TimerInterface(capacity) + , m_device(device) +{ + m_timestamp_query_desc = { + .nextInChain = nullptr, + .label = WGPUStringView { .data = "Timing Query", .length = WGPU_STRLEN }, + .type = WGPUQueryType_Timestamp, + .count = 2, // start and end + }; + m_timestamp_queries = wgpuDeviceCreateQuerySet(m_device, &m_timestamp_query_desc); + m_timestamp_writes = { + .nextInChain = nullptr, + .querySet = m_timestamp_queries, + .beginningOfPassWriteIndex = 0, + .endOfPassWriteIndex = 1, + }; + m_timestamp_resolve + = std::make_unique>(device, WGPUBufferUsage_QueryResolve | WGPUBufferUsage_CopySrc, 2, "Timestamp GPU Buffer"); + + for (uint32_t i = 0; i < ring_buffer_size; ++i) { + m_timestamp_readback_buffer.push_back( + std::make_unique>(device, WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead, 2, readback_timer_names(i))); + } +} + +void WebGpuTimer::start(WGPUCommandEncoder encoder) +{ + // Query the GPU for the start timestamp + wgpuCommandEncoderWriteTimestamp(encoder, m_timestamp_queries, 0); +} + +void WebGpuTimer::stop(WGPUCommandEncoder encoder) +{ + const uint32_t size_2_uint64 = static_cast(m_timestamp_resolve->size_in_byte()); + // Query the GPU for the stop timestamp + wgpuCommandEncoderWriteTimestamp(encoder, m_timestamp_queries, 1); + // Resolve the query set into the resolve buffer + wgpuCommandEncoderResolveQuerySet(encoder, m_timestamp_queries, 0, 2, m_timestamp_resolve->handle(), 0); + // Copy the resolve buffer to the result buffer + const auto i = m_ringbuffer_index_write; + if (m_timestamp_readback_buffer[i]->map_state() == WGPUBufferMapState_Unmapped) { + m_timestamp_resolve->copy_to_buffer(encoder, 0, *m_timestamp_readback_buffer[i], 0, size_2_uint64); + m_ringbuffer_index_read = i; + increment_index(m_ringbuffer_index_write); + } +#ifdef QT_DEBUG + else { + m_dbg_dropped_measurement_count++; + if (m_dbg_dropped_measurement_count == 100) { + qWarning() << "WebGPUTimer" << this->get_id() << "already dropped 100 measurements. Consider increasing ring buffer size."; + } + } +#endif +} + +void WebGpuTimer::resolve() +{ + if (m_ringbuffer_index_read < 0) + return; // Nothing to resolve + m_timestamp_readback_buffer[m_ringbuffer_index_read]->read_back_async(m_device, [this](WGPUMapAsyncStatus status, std::vector data) { + if (status == WGPUMapAsyncStatus_Success) { + const float result_in_s = (data[1] - data[0]) / 1e9; + add_result(result_in_s); + } + }); + m_ringbuffer_index_read = -1; +} + +} // namespace webgpu::timing diff --git a/webgpu/base/timing/WebGpuTimer.h b/webgpu/base/timing/WebGpuTimer.h new file mode 100644 index 000000000..1a3151284 --- /dev/null +++ b/webgpu/base/timing/WebGpuTimer.h @@ -0,0 +1,56 @@ +/***************************************************************************** + * Alpine Terrain Renderer + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "../raii/RawBuffer.h" +#include "TimerInterface.h" +#include +#include +#include + +namespace webgpu::timing { + +class WebGpuTimer : public TimerInterface { + +public: + WebGpuTimer(WGPUDevice device, uint32_t ring_buffer_size, size_t capacity); + void start(WGPUCommandEncoder encoder); + void stop(WGPUCommandEncoder encoder); + /// Readback of last value. Needs to be called after queue submit! + void resolve(); + +private: + WGPUQuerySetDescriptor m_timestamp_query_desc; + WGPUQuerySet m_timestamp_queries; + WGPUPassTimestampWrites m_timestamp_writes; + WGPUDevice m_device; + + std::unique_ptr> m_timestamp_resolve; + std::vector>> m_timestamp_readback_buffer; + + int m_ringbuffer_index_write = 0; // next write index + int m_ringbuffer_index_read = -1; // next index to read + inline void increment_index(int& index) { index = (index + 1) % this->m_timestamp_readback_buffer.size(); } + +#ifdef QT_DEBUG + uint32_t m_dbg_dropped_measurement_count = 0; +#endif +}; + +} // namespace webgpu::timing diff --git a/webgpu/base/util/ShaderPreprocessor.cpp b/webgpu/base/util/ShaderPreprocessor.cpp new file mode 100644 index 000000000..b711ceeaf --- /dev/null +++ b/webgpu/base/util/ShaderPreprocessor.cpp @@ -0,0 +1,452 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ +#include "ShaderPreprocessor.h" + +#include +#include +#include +#include +#include + +namespace webgpu::util { + +namespace { + // Helper function to trim leading and trailing whitespace + inline std::string trim_whitespace(std::string str) + { + str.erase(0, str.find_first_not_of(" \t")); + str.erase(str.find_last_not_of(" \t") + 1); + return str; + } + + // Fast directive guard: returns the index of the leading "///" (skipping leading + // whitespace) if the line is a directive, or npos otherwise. Cheap enough to run on + // every line - it only scans the (usually empty) leading whitespace; the regex is then + // matched from this offset so the anchored patterns need no change and no substring is copied. + inline size_t directive_offset(const std::string& line) + { + const size_t first = line.find_first_not_of(" \t"); + if (first != std::string::npos && line.compare(first, 3, "///") == 0) + return first; + return std::string::npos; + } +} // namespace + +ShaderPreprocessor::ShaderPreprocessor() { initialize_platform_defines(); } + +void ShaderPreprocessor::initialize_platform_defines() +{ + // NOTE: Add more platform-specific defines as needed if you want to use them in shaders +#ifdef QT_DEBUG + m_global_defines["QT_DEBUG"] = "1"; +#endif + +#ifdef __EMSCRIPTEN__ + m_global_defines["__EMSCRIPTEN__"] = "1"; +#endif + +#ifdef ALP_ENABLE_DEV_TOOLS + m_global_defines["ALP_ENABLE_DEV_TOOLS"] = "1"; +#endif + +#ifdef _WIN32 + m_global_defines["_WIN32"] = "1"; +#endif + +#ifdef _WIN64 + m_global_defines["_WIN64"] = "1"; +#endif + +#ifdef __linux__ + m_global_defines["__linux__"] = "1"; +#endif + +#ifdef __ANDROID__ + m_global_defines["__ANDROID__"] = "1"; +#endif +} + +void ShaderPreprocessor::define(const std::string& symbol) { m_global_defines[symbol] = "1"; } + +void ShaderPreprocessor::define(const std::string& symbol, const std::string& value) { m_global_defines[symbol] = value; } + +void ShaderPreprocessor::undefine(const std::string& symbol) { m_global_defines.erase(symbol); } + +bool ShaderPreprocessor::is_defined(const std::string& symbol) const { return m_global_defines.contains(symbol); } + +std::string ShaderPreprocessor::get_value(const std::string& symbol) const +{ + auto it = m_global_defines.find(symbol); + if (it != m_global_defines.end()) { + return it->second; + } + return ""; +} + +void ShaderPreprocessor::clear_cache() { m_shader_name_to_code.clear(); } + +void ShaderPreprocessor::set_cache_enabled(bool enabled) +{ + m_cache_enabled = enabled; + if (!enabled) { + clear_cache(); + } +} + +void ShaderPreprocessor::set_file_reader(std::function reader) { m_file_reader = std::move(reader); } + +void ShaderPreprocessor::set_error_callback(std::function callback) { m_error_callback = std::move(callback); } + +void ShaderPreprocessor::report_error(const std::string& message) +{ + if (m_error_callback) { + m_error_callback(message); + } +} + +std::string ShaderPreprocessor::get_file_contents_with_cache(const std::string& name) +{ + if (m_cache_enabled) { + const auto found_it = m_shader_name_to_code.find(name); + if (found_it != m_shader_name_to_code.end()) { + return found_it->second; + } + } + + if (!m_file_reader) { + report_error("No file reader set for ShaderPreprocessor"); + return ""; + } + + const auto file_contents = m_file_reader(name); + + if (m_cache_enabled) { + m_shader_name_to_code[name] = file_contents; + } + + return file_contents; +} + +std::string ShaderPreprocessor::process_defines(const std::string& code, std::map& local_defines) +{ + static const std::regex define_regex(R"(^///define\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(.*)$)"); + + std::istringstream input(code); + std::ostringstream output; + std::string line; + + while (std::getline(input, line)) { + // Quick Check: skip regex if the line isn't a directive (ignoring leading whitespace) + const size_t off = directive_offset(line); + if (off == std::string::npos) { + output << line << '\n'; + continue; + } + + std::smatch match; + if (std::regex_match(line.cbegin() + static_cast(off), line.cend(), match, define_regex)) { + const std::string symbol = match[1].str(); + std::string value = trim_whitespace(match[2].str()); + if (value.empty()) + value = "1"; // default to "1" + local_defines[symbol] = value; + // Don't output the #define directive itself + } else { + output << line << '\n'; + } + } + + return output.str(); +} + +std::string ShaderPreprocessor::process_includes(const std::string& code, + std::unordered_set& already_included, + std::map& local_defines, + const std::string& current_namespace) +{ + // ///use relpath -> include from current_namespace + // ///use target::relpath -> include from the given target namespace + static const std::regex use_regex(R"(^///use\s+(?:([a-zA-Z_][a-zA-Z0-9_]*)::)?([/a-zA-Z0-9 ._-]+?)\s*$)"); + + std::istringstream input(code); + std::ostringstream output; + std::string line; + + while (std::getline(input, line)) { + std::smatch match; + const size_t off = directive_offset(line); + if (off != std::string::npos && std::regex_match(line.cbegin() + static_cast(off), line.cend(), match, use_regex)) { + const std::string included_namespace = match[1].matched ? match[1].str() : current_namespace; + const std::string relpath = match[2].str(); + // When no namespace is in play (e.g. inline shaders / tests) the name is just the relpath. + const std::string full_name = included_namespace.empty() ? relpath : included_namespace + "::" + relpath; + + if (already_included.contains(full_name)) + continue; // pragma-once: skip files already pulled in + + // NOTE: mark as included BEFORE processing to prevent infinite recursion + already_included.insert(full_name); + const std::string included_file_contents = get_file_contents_with_cache(full_name); + output << process_includes(included_file_contents, already_included, local_defines, included_namespace) << '\n'; + } else { + output << line << '\n'; + } + } + + return output.str(); +} + +std::string ShaderPreprocessor::process_conditionals(const std::string& code, const std::map& local_defines) +{ + // NOTE: static for better performance + static const std::regex ifdef_regex(R"(^///ifdef\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*$)"); + static const std::regex ifndef_regex(R"(^///ifndef\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*$)"); + static const std::regex if_regex(R"(^///if\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$)"); + static const std::regex elif_regex(R"(^///elif\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$)"); + static const std::regex endif_regex(R"(^///endif\s*$)"); + static const std::regex else_regex(R"(^///else\s*$)"); + + std::istringstream input(code); + std::ostringstream output; + std::string line; + + // Stack to track conditional compilation state + struct ConditionalState { + bool is_active; // Should we output code in this block? + bool was_any_branch_taken; // Has any branch been taken in this if/else chain? + }; + std::stack condition_stack; + + bool is_currently_active = true; + + auto is_symbol_defined = [&](const std::string& symbol) -> bool { return m_global_defines.contains(symbol) || local_defines.contains(symbol); }; + + auto get_symbol_value = [&](const std::string& symbol) -> std::string { + auto it = local_defines.find(symbol); + if (it != local_defines.end()) { + return it->second; + } + it = m_global_defines.find(symbol); + if (it != m_global_defines.end()) { + return it->second; + } + return ""; + }; + + while (std::getline(input, line)) { + // Quick check: if line doesn't start with a directive prefix, it's regular code + const size_t off = directive_offset(line); + if (off == std::string::npos) { + if (is_currently_active) + output << line << '\n'; + continue; + } + + // Match the anchored directive regexes from the first non-whitespace char (no copy). + const auto dbegin = line.cbegin() + static_cast(off); + const auto dend = line.cend(); + std::smatch match; + + if (std::regex_match(dbegin, dend, match, ifdef_regex)) { + // #ifdef directive + const std::string symbol = match[1].str(); + const bool symbol_defined = is_symbol_defined(symbol); + const bool parent_active = is_currently_active; + const bool this_active = parent_active && symbol_defined; + + condition_stack.push({ this_active, symbol_defined }); + is_currently_active = this_active; + + } else if (std::regex_match(dbegin, dend, match, ifndef_regex)) { + // #ifndef directive + const std::string symbol = match[1].str(); + const bool symbol_not_defined = !is_symbol_defined(symbol); + const bool parent_active = is_currently_active; + const bool this_active = parent_active && symbol_not_defined; + + condition_stack.push({ this_active, symbol_not_defined }); + is_currently_active = this_active; + + } else if (std::regex_match(dbegin, dend, match, if_regex)) { + // #if directive - compares symbol value with a string + const std::string symbol = match[1].str(); + const std::string expected_value = trim_whitespace(match[2].str()); + const std::string actual_value = get_symbol_value(symbol); + const bool condition_met = (actual_value == expected_value); + const bool parent_active = is_currently_active; + const bool this_active = parent_active && condition_met; + + condition_stack.push({ this_active, condition_met }); + is_currently_active = this_active; + + } else if (std::regex_match(dbegin, dend, match, elif_regex)) { + // #elif directive + if (condition_stack.empty()) { + report_error("Shader preprocessing error: #elif without matching #if/#ifdef/#ifndef"); + continue; + } + + auto& state = condition_stack.top(); + + // Determine parent activity + bool parent_active = true; + if (condition_stack.size() > 1) { + auto stack_copy = condition_stack; + stack_copy.pop(); + parent_active = stack_copy.top().is_active; + } + + // Only evaluate elif if parent is active and no branch was taken yet + if (parent_active && !state.was_any_branch_taken) { + const std::string symbol = match[1].str(); + const std::string expected_value = trim_whitespace(match[2].str()); + + const std::string actual_value = get_symbol_value(symbol); + const bool condition_met = (actual_value == expected_value); + + is_currently_active = condition_met; + state.is_active = condition_met; + if (condition_met) { + state.was_any_branch_taken = true; + } + } else { + is_currently_active = false; + } + + } else if (std::regex_match(dbegin, dend, else_regex)) { + // #else directive + if (condition_stack.empty()) { + report_error("Shader preprocessing error: #else without matching #if/#ifdef/#ifndef"); + continue; + } + + auto& state = condition_stack.top(); + + // Determine parent activity + bool parent_active = true; + if (condition_stack.size() > 1) { + auto stack_copy = condition_stack; + stack_copy.pop(); + parent_active = stack_copy.top().is_active; + } + + // #else is active if parent is active and no previous branch was taken + is_currently_active = parent_active && !state.was_any_branch_taken; + state.is_active = is_currently_active; + state.was_any_branch_taken = true; + + } else if (std::regex_match(dbegin, dend, endif_regex)) { + if (condition_stack.empty()) { + report_error("Shader preprocessing error: #endif without matching #if/#ifdef/#ifndef"); + continue; + } + + condition_stack.pop(); + + // Restore parent activity state + if (condition_stack.empty()) { + is_currently_active = true; + } else { + is_currently_active = condition_stack.top().is_active; + } + + } else { + // starts with '///' but doesn't match any directive - treat as regular comment/code + if (is_currently_active) { + output << line << '\n'; + } + } + } + + if (!condition_stack.empty()) { + report_error("Shader preprocessing error: unclosed #if/#ifdef/#ifndef directive"); + } + + return output.str(); +} + +std::string ShaderPreprocessor::replace_macros(const std::string& code, const std::map& local_defines) +{ + // Merge global and local defines (local takes precedence) + std::map all_defines = m_global_defines; + for (const auto& [symbol, value] : local_defines) { + all_defines[symbol] = value; + } + + // NOTE: We process in reverse order of symbol length to handle longer symbols first + // for the case where one symbol is a part of another (e.g., VALUE and VAL) + std::vector> sorted_defines(all_defines.begin(), all_defines.end()); + std::sort(sorted_defines.begin(), sorted_defines.end(), [](const auto& a, const auto& b) { return a.first.length() > b.first.length(); }); + + std::string result; + result.reserve(code.length()); // Reserve space to avoid reallocations + + size_t pos = 0; + while (pos < code.length()) { + bool replaced = false; + + // Check each symbol at current position + for (const auto& [symbol, value] : sorted_defines) { + const size_t sym_len = symbol.length(); + + // Check if symbol matches at current position + if (pos + sym_len <= code.length() && code.compare(pos, sym_len, symbol) == 0) { + // Check word boundaries: previous character must not be alphanumeric/underscore + const bool prev_ok = (pos == 0 || (!std::isalnum(code[pos - 1]) && code[pos - 1] != '_')); + // Next character must not be alphanumeric/underscore + const bool next_ok = (pos + sym_len >= code.length() || (!std::isalnum(code[pos + sym_len]) && code[pos + sym_len] != '_')); + + if (prev_ok && next_ok) { + result += value; + pos += sym_len; + replaced = true; + break; + } + } + } + + if (!replaced) { + result += code[pos]; + ++pos; + } + } + + return result; +} + +std::string ShaderPreprocessor::preprocess_file(const std::string& name) +{ + const std::string code = get_file_contents_with_cache(name); + // The root file's namespace (the part before "::") is inherited by its bare `///use` includes. + const auto sep = name.find("::"); + const std::string current_namespace = (sep != std::string::npos) ? name.substr(0, sep) : std::string(); + return preprocess_code(code, current_namespace); +} + +std::string ShaderPreprocessor::preprocess_code(const std::string& code, const std::string& current_namespace) +{ + std::map local_defines; + std::string code_with_defines = process_defines(code, local_defines); + std::unordered_set already_included; + std::string code_with_includes = process_includes(code_with_defines, already_included, local_defines, current_namespace); + std::string code_with_conditionals = process_conditionals(code_with_includes, local_defines); + std::string final_code = replace_macros(code_with_conditionals, local_defines); + return final_code; +} + +} // namespace webgpu::util diff --git a/webgpu/base/util/ShaderPreprocessor.h b/webgpu/base/util/ShaderPreprocessor.h new file mode 100644 index 000000000..c55827649 --- /dev/null +++ b/webgpu/base/util/ShaderPreprocessor.h @@ -0,0 +1,130 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include + +namespace webgpu::util { + +/** + * A text-based shader preprocessor for intended use with WebGPU shaders. + * + * IMPORTANT: Directives use a `///`-comment prefix so the files stay valid WGSL! + * + * Features: + * - File inclusion with ///use relpath (or ///use target::relpath) + * - Automatic pragma once behavior (each file included only once) + * - Conditional compilation with ///ifdef / ///ifndef / ///else / ///endif + * - Value-based conditionals with ///if / ///elif / ///else / ///endif (equality only) + * - Macro definitions with ///define SYMBOL or ///define SYMBOL value + * - Environment variable defines (global, persist across calls) + * - File content caching + */ +class ShaderPreprocessor { +public: + ShaderPreprocessor(); + ~ShaderPreprocessor() = default; + + /** + * Preprocesses a shader file by name, resolving includes and conditional compilation. + * @param name The name of the shader file to preprocess + */ + std::string preprocess_file(const std::string& name); + + /** + * Preprocesses shader code directly, resolving includes and conditional compilation. + * @param code The shader code to preprocess + * @param current_namespace Target namespace that bare `///use` includes inherit (e.g. "webgpu_engine"). + */ + std::string preprocess_code(const std::string& code, const std::string& current_namespace = ""); + + /** + * Defines a global preprocessor symbol for use in #ifdef/#ifndef directives. + */ + void define(const std::string& symbol); + + /** + * Defines a global preprocessor symbol with a value. + */ + void define(const std::string& symbol, const std::string& value); + + /** + * Undefines a global preprocessor symbol. + */ + void undefine(const std::string& symbol); + + /** + * Checks if a symbol is defined globally. + */ + bool is_defined(const std::string& symbol) const; + + /** + * Gets the value of a defined symbol. Returns empty string if not defined or has no value. + */ + std::string get_value(const std::string& symbol) const; + + /** + * Clears the file cache. Useful for hot-reloading shaders during development. + */ + void clear_cache(); + + /** + * Enables or disables file caching. When disabled, files are read fresh each time. + * Caching is enabled by default. + */ + void set_cache_enabled(bool enabled); + + /** + * Sets the file reading callback. This allows the preprocessor to read files + * without depending on specific file I/O implementations. + */ + void set_file_reader(std::function reader); + + /** + * Sets the error callback. Called when a preprocessing error occurs. + * The preprocessor will try to continue processing after an error when possible. + * The callback can terminate the program if desired (e.g., by calling qFatal). + */ + void set_error_callback(std::function callback); + +private: + std::string get_file_contents_with_cache(const std::string& name); + std::string process_defines(const std::string& code, std::map& local_defines); + std::string process_includes(const std::string& code, + std::unordered_set& already_included, + std::map& local_defines, + const std::string& current_namespace); + std::string process_conditionals(const std::string& code, const std::map& local_defines); + std::string replace_macros(const std::string& code, const std::map& local_defines); + void initialize_platform_defines(); + + void report_error(const std::string& message); + +private: + std::map m_shader_name_to_code; // Cache of file contents + std::map m_global_defines; // Global preprocessor symbols and their values (persist across calls, "1" if no value specified) + std::function m_file_reader; // Callback for reading files + std::function m_error_callback; // Callback for error reporting + bool m_cache_enabled = true; +}; + +} // namespace webgpu::util diff --git a/webgpu/base/util/VertexBufferInfo.cpp b/webgpu/base/util/VertexBufferInfo.cpp new file mode 100644 index 000000000..a95728638 --- /dev/null +++ b/webgpu/base/util/VertexBufferInfo.cpp @@ -0,0 +1,53 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "util/VertexBufferInfo.h" + +#include + +namespace webgpu::util { + +WGPUVertexBufferLayout SingleVertexBufferInfo::vertex_buffer_layout() const +{ + assert(m_step_mode != WGPUVertexStepMode_Undefined); + assert(m_vertex_attributes.size() != 0); + assert(m_stride != 0); + + WGPUVertexBufferLayout vertex_buffer_layout {}; + vertex_buffer_layout.arrayStride = m_stride; + vertex_buffer_layout.attributeCount = m_vertex_attributes.size(); + vertex_buffer_layout.attributes = m_vertex_attributes.data(); + vertex_buffer_layout.stepMode = m_step_mode; + return vertex_buffer_layout; +} + +SingleVertexBufferInfo::SingleVertexBufferInfo(WGPUVertexStepMode step_mode) + : m_stride(0) + , m_explicit_stride(false) + , m_step_mode(step_mode) +{ +} + +SingleVertexBufferInfo::SingleVertexBufferInfo(WGPUVertexStepMode step_mode, uint32_t stride) + : m_stride(stride) + , m_explicit_stride(true) + , m_step_mode(step_mode) +{ +} + +} // namespace webgpu::util diff --git a/webgpu/base/util/VertexBufferInfo.h b/webgpu/base/util/VertexBufferInfo.h new file mode 100644 index 000000000..947f579e3 --- /dev/null +++ b/webgpu/base/util/VertexBufferInfo.h @@ -0,0 +1,60 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "VertexFormat.h" +#include +#include + +namespace webgpu::util { + +class SingleVertexBufferInfo { +public: + SingleVertexBufferInfo(WGPUVertexStepMode step_mode); + SingleVertexBufferInfo(WGPUVertexStepMode step_mode, uint32_t stride); + + /// Adds attribute to this vertex buffer. + /// T is component type and N is number of components + /// For example, use add_attribute(0) to add an attribute with 4 floats at shader location 0. + template void add_attribute(uint32_t shader_location, uint32_t offset = 0); + + WGPUVertexBufferLayout vertex_buffer_layout() const; + +private: + std::vector m_vertex_attributes; + size_t m_stride = 0; + bool m_explicit_stride = false; + WGPUVertexStepMode m_step_mode = WGPUVertexStepMode_Undefined; +}; + +template void SingleVertexBufferInfo::add_attribute(uint32_t shader_location, uint32_t offset) +{ + WGPUVertexAttribute attribute {}; + attribute.shaderLocation = shader_location; + attribute.format = VertexFormat::format(); + attribute.offset = offset; + + m_vertex_attributes.push_back(attribute); + + if (!m_explicit_stride) { + m_stride += VertexFormat::size(); + } +} + +} // namespace webgpu::util diff --git a/webgpu/base/util/VertexFormat.h b/webgpu/base/util/VertexFormat.h new file mode 100644 index 000000000..61fdecbe5 --- /dev/null +++ b/webgpu/base/util/VertexFormat.h @@ -0,0 +1,59 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu::util { + +template struct VertexFormat { + static constexpr WGPUVertexFormat format(); + static constexpr size_t size() { return sizeof(T) * N; } +}; + +template constexpr WGPUVertexFormat VertexFormat::format() +{ + static_assert(sizeof N != sizeof N, "tried to get unmapped vertex format"); + return static_cast(0); +} +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Float32; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Float32x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Float32x3; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Float32x4; } + +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint32; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint32x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint32x3; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint32x4; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint16x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint16x4; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint8x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Uint8x4; } + +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint32; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint32x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint32x3; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint32x4; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint16x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint16x4; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint8x2; } +template <> constexpr WGPUVertexFormat VertexFormat::format() { return WGPUVertexFormat_Sint8x4; } + +} // namespace webgpu::util diff --git a/webgpu/base/util/string_cast.cpp b/webgpu/base/util/string_cast.cpp new file mode 100644 index 000000000..f3fd25975 --- /dev/null +++ b/webgpu/base/util/string_cast.cpp @@ -0,0 +1,41 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "util/string_cast.h" +#include + +namespace webgpu::util { + +const char* bufferMapAsyncStatusToString(WGPUMapAsyncStatus status) +{ + static const std::map statusToStringMap = { + { WGPUMapAsyncStatus_Success, "Success" }, + { WGPUMapAsyncStatus_CallbackCancelled, "CallbackCancelled" }, + { WGPUMapAsyncStatus_Error, "Error" }, + { WGPUMapAsyncStatus_Aborted, "Aborted" }, + { WGPUMapAsyncStatus_Force32, "Force32" }, + }; + + auto it = statusToStringMap.find(status); + if (it != statusToStringMap.end()) { + return it->second; + } + return "UnknownStatus"; +} + +} // namespace webgpu::util diff --git a/webgpu/base/util/string_cast.h b/webgpu/base/util/string_cast.h new file mode 100644 index 000000000..b3627aa02 --- /dev/null +++ b/webgpu/base/util/string_cast.h @@ -0,0 +1,28 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu::util { + +const char* bufferMapAsyncStatusToString(WGPUMapAsyncStatus status); + +} // namespace webgpu::util diff --git a/webgpu/base/webgpu_interface.cpp b/webgpu/base/webgpu_interface.cpp new file mode 100644 index 000000000..9c0cef025 --- /dev/null +++ b/webgpu/base/webgpu_interface.cpp @@ -0,0 +1,358 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022-2023 Elie Michel and the wgpu-native authors + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +/** + * This is an extension of SDL for WebGPU, abstracting away the details of + * OS-specific operations. + * + * This file is part of the "Learn WebGPU for C++" book. + * https://eliemichel.github.io/LearnWebGPU + * + * Most of this code comes from the wgpu-native triangle example: + * https://github.com/gfx-rs/wgpu-native/blob/master/examples/triangle/main.c + * + * MIT License + * Copyright (c) 2022-2023 Elie Michel and the wgpu-native authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "webgpu_interface.hpp" + +#include "util/string_cast.h" +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +// needed for emscripten_sleep +#include +#include +#else +// include Dawn only for non-emscripten build +#include +#include +#endif + + +#define WGPU_TARGET_MACOS 1 +#define WGPU_TARGET_LINUX_X11 2 +#define WGPU_TARGET_WINDOWS 3 +#define WGPU_TARGET_LINUX_WAYLAND 4 +#define WGPU_TARGET_EMSCRIPTEN 5 + +#if defined(SDL_VIDEO_DRIVER_COCOA) +#include +#include +#include +#elif defined(SDL_VIDEO_DRIVER_UIKIT) +#include +#include +#include +#include +#endif + +#include + +WGPUSurface SDL_GetWGPUSurface(WGPUInstance instance, SDL_Window* window) +{ + SDL_SysWMinfo windowWMInfo; + SDL_VERSION(&windowWMInfo.version); + SDL_GetWindowWMInfo(window, &windowWMInfo); + +#if defined(SDL_VIDEO_DRIVER_COCOA) + { + id metal_layer = NULL; + NSWindow* ns_window = windowWMInfo.info.cocoa.window; + [ns_window.contentView setWantsLayer:YES]; + metal_layer = [CAMetalLayer layer]; + [ns_window.contentView setLayer:metal_layer]; + +#ifdef WEBGPU_BACKEND_DAWN + WGPUSurfaceSourceMetalLayer fromMetalLayer; + fromMetalLayer.chain.sType = WGPUSType_SurfaceSourceMetalLayer; +#else + WGPUSurfaceDescriptorFromMetalLayer fromMetalLayer; + fromMetalLayer.chain.sType = WGPUSType_SurfaceDescriptorFromMetalLayer; +#endif + fromMetalLayer.chain.next = NULL; + fromMetalLayer.layer = metal_layer; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &fromMetalLayer.chain; + surfaceDescriptor.label = WGPUStringView {}; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#elif defined(SDL_VIDEO_DRIVER_UIKIT) + { + UIWindow* ui_window = windowWMInfo.info.uikit.window; + UIView* ui_view = ui_window.rootViewController.view; + CAMetalLayer* metal_layer = [CAMetalLayer new]; + metal_layer.opaque = true; + metal_layer.frame = ui_view.frame; + metal_layer.drawableSize = ui_view.frame.size; + + [ui_view.layer addSublayer:metal_layer]; + +#ifdef WEBGPU_BACKEND_DAWN + WGPUSurfaceSourceMetalLayer fromMetalLayer; + fromMetalLayer.chain.sType = WGPUSType_SurfaceSourceMetalLayer; +#else + WGPUSurfaceDescriptorFromMetalLayer fromMetalLayer; + fromMetalLayer.chain.sType = WGPUSType_SurfaceDescriptorFromMetalLayer; +#endif + fromMetalLayer.chain.next = NULL; + fromMetalLayer.layer = metal_layer; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &fromMetalLayer.chain; + surfaceDescriptor.label = WGPUStringView {}; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#elif defined(SDL_VIDEO_DRIVER_X11) + { + Display* x11_display = windowWMInfo.info.x11.display; + Window x11_window = windowWMInfo.info.x11.window; + +#ifdef WEBGPU_BACKEND_DAWN + WGPUSurfaceSourceXlibWindow fromXlibWindow; + fromXlibWindow.chain.sType = WGPUSType_SurfaceSourceXlibWindow; +#else + WGPUSurfaceDescriptorFromXlibWindow fromXlibWindow; + fromXlibWindow.chain.sType = WGPUSType_SurfaceDescriptorFromXlibWindow; +#endif + fromXlibWindow.chain.next = NULL; + fromXlibWindow.display = x11_display; + fromXlibWindow.window = x11_window; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &fromXlibWindow.chain; + surfaceDescriptor.label = WGPUStringView {}; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#elif defined(SDL_VIDEO_DRIVER_WAYLAND) + { + struct wl_display* wayland_display = windowWMInfo.info.wl.display; + struct wl_surface* wayland_surface = windowWMInfo.info.wl.surface; + +#ifdef WEBGPU_BACKEND_DAWN + WGPUSurfaceSourceWaylandSurface fromWaylandSurface; + fromWaylandSurface.chain.sType = WGPUSType_SurfaceSourceWaylandSurface; +#else + WGPUSurfaceDescriptorFromWaylandSurface fromWaylandSurface; + fromWaylandSurface.chain.sType = WGPUSType_SurfaceDescriptorFromWaylandSurface; +#endif + fromWaylandSurface.chain.next = NULL; + fromWaylandSurface.display = wayland_display; + fromWaylandSurface.surface = wayland_surface; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &fromWaylandSurface.chain; + surfaceDescriptor.label = WGPUStringView {}; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#elif defined(SDL_VIDEO_DRIVER_WINDOWS) + { + HWND hwnd = windowWMInfo.info.win.window; + HINSTANCE hinstance = GetModuleHandle(NULL); + + WGPUSurfaceSourceWindowsHWND hwndDesc {}; + hwndDesc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; + hwndDesc.chain.next = NULL; + hwndDesc.hinstance = hinstance; + hwndDesc.hwnd = hwnd; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &hwndDesc.chain; + surfaceDescriptor.label = WGPUStringView { .data = "default surface", .length = WGPU_STRLEN }; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#elif defined(SDL_VIDEO_DRIVER_EMSCRIPTEN) + { + WGPUEmscriptenSurfaceSourceCanvasHTMLSelector fromCanvasHTMLSelector {}; + fromCanvasHTMLSelector.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; + fromCanvasHTMLSelector.chain.next = nullptr; + fromCanvasHTMLSelector.selector = WGPUStringView { .data = "#canvas", .length = WGPU_STRLEN }; + + WGPUSurfaceDescriptor surfaceDescriptor; + surfaceDescriptor.nextInChain = &fromCanvasHTMLSelector.chain; + surfaceDescriptor.label = WGPUStringView { .data = "default surface", .length = WGPU_STRLEN }; + + return wgpuInstanceCreateSurface(instance, &surfaceDescriptor); + } +#else +// TODO: See SDL_syswm.h for other possible enum values! +#error "Unsupported WGPU_TARGET" +#endif +} + +namespace webgpu { + +bool timerSupportFlag = false; +std::atomic_int sleeping_counter = 0; + +// NOTE: USE WITH CAUTION! +void sleep([[maybe_unused]] const WGPUDevice& device, [[maybe_unused]] int milliseconds) +{ + sleeping_counter++; +#ifdef __EMSCRIPTEN__ + emscripten_sleep(1); // using asyncify to return to js event loop +#else + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + wgpuDeviceTick(device); // polling events for DAWN +#endif + sleeping_counter--; +} + +bool isSleeping() { return sleeping_counter > 0; } + +void waitForFlag(const WGPUDevice& device, bool* flag, int sleepInterval, int timeout) +{ + int time = 0; + while (!*flag) { + webgpu::sleep(device, sleepInterval); + time += sleepInterval; + if (time > timeout) { + qCritical() << "Timeout while waiting for flag"; + return; + } + } +} + +void checkForTimingSupport(const WGPUAdapter& adapter, const WGPUDevice& device) +{ + // Check wether timing is supported +#ifdef __EMSCRIPTEN__ + timerSupportFlag = false; + emscripten::val module_value = emscripten::val::module_property("webgpuTimingsAvailable"); + if (!module_value.isUndefined()) { + timerSupportFlag = (module_value.as() == 1); + if (!timerSupportFlag) + qWarning() << "Timestamp queries are not supported! (JS based check failed)"; + } else { + qCritical() << "Timestamp query flag couldn't be found as a module property!"; + } +#else + timerSupportFlag = true; + if (!wgpuAdapterHasFeature(adapter, WGPUFeatureName_TimestampQuery)) { + qWarning() << "Timestamp queries are not supported! (Not necessary adapter feature)"; + timerSupportFlag = false; + } else if (!wgpuDeviceHasFeature(device, WGPUFeatureName_TimestampQuery)) { + qWarning() << "Timestamp queries are not supported! (Not necessary device feature)"; + timerSupportFlag = false; + } +#endif +} + +bool isTimingSupported() { return timerSupportFlag; } + +// Request webgpu adapter synchronously. Adapted from webgpu.hpp to vanilla webGPU types. +WGPUAdapter requestAdapterSync(WGPUInstance instance, const WGPURequestAdapterOptions& options) +{ + struct AdapterRequestEndedData { + WGPUAdapter adapter = nullptr; + bool request_ended = false; + } request_ended_data; + + auto on_adapter_request_ended + = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + AdapterRequestEndedData* request_ended_data = reinterpret_cast(userdata); + if (status == WGPURequestAdapterStatus::WGPURequestAdapterStatus_Success) { + request_ended_data->adapter = adapter; + } else { + request_ended_data->adapter = nullptr; + } + request_ended_data->request_ended = true; + }; + + WGPURequestAdapterCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_WaitAnyOnly, + .callback = on_adapter_request_ended, + .userdata1 = &request_ended_data, + .userdata2 = nullptr, + }; + + WGPUFuture request_adapter_future = wgpuInstanceRequestAdapter(instance, &options, callback_info); + WGPUFutureWaitInfo future_wait_info { request_adapter_future, false }; + WGPUWaitStatus status = wgpuInstanceWaitAny(instance, 1, &future_wait_info, 10e9); // wait for 10s max + if (status != WGPUWaitStatus_Success) { + qFatal() << "Failed to obtain instance, WGPUWaitStatus was " << status; + } + + assert(request_ended_data.request_ended); + return request_ended_data.adapter; +} + +// Request webgpu device synchronously. Adapted from webgpu.hpp to vanilla webGPU types. +WGPUDevice requestDeviceSync(WGPUInstance instance, WGPUAdapter adapter, const WGPUDeviceDescriptor& descriptor) +{ + WGPUDevice device = nullptr; + + auto on_device_request_ended + = [](WGPURequestDeviceStatus status, WGPUDevice device, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + WGPUDevice* device_handle = reinterpret_cast(userdata); + + if (status == WGPURequestDeviceStatus::WGPURequestDeviceStatus_Success) { + *device_handle = device; + } else { + qCritical() << "requesting WebGPU device failed, error message: " << message.data; + } + }; + + WGPURequestDeviceCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_WaitAnyOnly, + .callback = on_device_request_ended, + .userdata1 = &device, + .userdata2 = nullptr, + }; + + WGPUFuture device_request_future = wgpuAdapterRequestDevice(adapter, &descriptor, callback_info); + WGPUFutureWaitInfo future_wait_info { device_request_future, false }; + WGPUWaitStatus status = wgpuInstanceWaitAny(instance, 1, &future_wait_info, 10e9); // timeout 10s + if (status != WGPUWaitStatus_Success) { + qFatal() << "Failed to obtain Webgpu device, WGPUWaitStatus was " << status; + } + + return device; +} + +} // namespace webgpu diff --git a/webgpu/base/webgpu_interface.hpp b/webgpu/base/webgpu_interface.hpp new file mode 100644 index 000000000..5e702c6e2 --- /dev/null +++ b/webgpu/base/webgpu_interface.hpp @@ -0,0 +1,99 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022-2023 Elie Michel and the wgpu-native authors + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +/** + * This is an extension of SDL for WebGPU, abstracting away the details of + * OS-specific operations. + * + * This file is part of the "Learn WebGPU for C++" book. + * https://eliemichel.github.io/LearnWebGPU + * + * MIT License + * Copyright (c) 2022-2023 Elie Michel and the wgpu-native authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* NOTE: This file offers platform-specific operations for WebGPU, depending on the + * target (web/native), such that the code in webgpu_app and webgpu_engine can + * be kept as generic as possible. + */ +#pragma once + +#include +#include + +extern "C" { + +/** + * Get a WGPUSurface from a GLFW window. + */ +// WGPUSurface glfwGetWGPUSurface(WGPUInstance instance, GLFWwindow* window); +WGPUSurface SDL_GetWGPUSurface(WGPUInstance instance, SDL_Window* window); +} + +namespace webgpu { + +/** + * A sleep function which works with webassembly by using asyncify and native by utilizing + * thread::sleep_for. Use with caution! It blocks the main thread and asyncify imposes a great overhead. + * In the best case, this function should only be used for test purposes. + * @param milliseconds The number of milliseconds to sleep. + */ +void sleep(const WGPUDevice& device, int milliseconds); + +/** + * Uses webgpuSleep to wait for a flag to be set to true. USE WITH CAUTION! + * @param device The webgpu device for polling + * @param flag Pointer to the boolean flag + * @param sleepInterval The interval in milliseconds between each poll + * @param timeout The maximum time to wait for the flag to be set + */ +void waitForFlag(const WGPUDevice& device, bool* flag, int sleepInterval = 1, int timeout = 1000); + +// I don't like this. Better suggestions welcome +[[nodiscard]] bool isTimingSupported(); +void checkForTimingSupport(const WGPUAdapter& adapter, const WGPUDevice& device); + +/** + * Returns true if the application is currently in a sleeping state. This is possible + * if the javascript event loop calls c++ callbacks. + * @return returns true if the application is currently sleeping + */ +bool isSleeping(); + +WGPUAdapter requestAdapterSync(WGPUInstance instance, const WGPURequestAdapterOptions& options); +WGPUDevice requestDeviceSync(WGPUInstance instance, WGPUAdapter adapter, const WGPUDeviceDescriptor& descriptor); +} // namespace webgpu diff --git a/webgpu/compute/CMakeLists.txt b/webgpu/compute/CMakeLists.txt new file mode 100644 index 000000000..98097723a --- /dev/null +++ b/webgpu/compute/CMakeLists.txt @@ -0,0 +1,98 @@ +############################################################################# +# weBIGeo +# Copyright (C) 2026 Gerald Kimmersdorfer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +project(alpine-renderer-webgpu_compute LANGUAGES C CXX) + +set(SOURCES + GpuTileStorage.h GpuTileStorage.cpp + RectangularTileRegion.h RectangularTileRegion.cpp + GraphRunContext.h + NodeGraph.h NodeGraph.cpp + NodeGraphSerialization.h NodeGraphSerialization.cpp + NodeRegistry.h NodeRegistry.cpp + + nodes/Node.h nodes/Node.cpp + nodes/SelectTilesNode.h nodes/SelectTilesNode.cpp + nodes/RequestTilesNode.h nodes/RequestTilesNode.cpp + nodes/ComputeNormalsNode.h nodes/ComputeNormalsNode.cpp + nodes/ComputeSnowNode.h nodes/ComputeSnowNode.cpp + nodes/ComputeAvalancheTrajectoriesNode.h nodes/ComputeAvalancheTrajectoriesNode.cpp + nodes/ExportNode.h nodes/ExportNode.cpp + nodes/GPXTrackNode.h nodes/GPXTrackNode.cpp + nodes/BufferToTextureNode.h nodes/BufferToTextureNode.cpp + nodes/ComputeReleasePointsNode.h nodes/ComputeReleasePointsNode.cpp + nodes/TileStitchNode.h nodes/TileStitchNode.cpp + nodes/HeightDecodeNode.h nodes/HeightDecodeNode.cpp + nodes/IterativeSimulationNode.h nodes/IterativeSimulationNode.cpp + nodes/LoadTextureNode.h nodes/LoadTextureNode.cpp + nodes/util.h nodes/util.cpp +) + +qt_add_library(webgpu_compute STATIC ${SOURCES}) + +if (ALP_ENABLE_WGSL_MINIFICATION) + set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") + set(SHADER_MINIFIED_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders_minified") + set(MINIFY_SCRIPT "${CMAKE_SOURCE_DIR}/misc/scripts/minify_shaders.py") + + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${SHADER_SOURCE_DIR}" "${SHADER_MINIFIED_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE MINIFY_RESULT + ) + if(NOT MINIFY_RESULT EQUAL 0) + message(FATAL_ERROR "Shader minification failed") + endif() + + file(GLOB_RECURSE SHADER_SOURCES "${SHADER_SOURCE_DIR}/*.wgsl") + add_custom_target(minify_webgpu_compute_shaders + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${SHADER_SOURCE_DIR}" "${SHADER_MINIFIED_DIR}" + DEPENDS ${SHADER_SOURCES} ${MINIFY_SCRIPT} + COMMENT "Minifying webgpu_compute shaders" + VERBATIM + ) + add_dependencies(webgpu_compute minify_webgpu_compute_shaders) + + set(SHADER_BASE_DIR "${SHADER_MINIFIED_DIR}") +else() + set(SHADER_BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") +endif() + +qt_add_resources(webgpu_compute "webgpu_compute_shaders" + PREFIX "/shaders/webgpu_compute" + BASE "${SHADER_BASE_DIR}" + FILES + "${SHADER_BASE_DIR}/normals_compute.wgsl" + "${SHADER_BASE_DIR}/snow_compute.wgsl" + "${SHADER_BASE_DIR}/avalanche_trajectories_compute.wgsl" + "${SHADER_BASE_DIR}/buffer_to_texture_compute.wgsl" + "${SHADER_BASE_DIR}/compute_release_points.wgsl" + "${SHADER_BASE_DIR}/height_decode_compute.wgsl" + "${SHADER_BASE_DIR}/iterative_simulation_compute.wgsl" + + "${SHADER_BASE_DIR}/tile_hashmap.wgsl" + "${SHADER_BASE_DIR}/color_mapping.wgsl" + "${SHADER_BASE_DIR}/random.wgsl" + ) + +target_link_libraries(webgpu_compute PUBLIC nucleus Qt::Core webgpu) +target_include_directories(webgpu_compute PRIVATE .) + +target_compile_definitions(webgpu_compute PUBLIC ALP_SHADER_DIR_WEBGPU_COMPUTE="${CMAKE_CURRENT_SOURCE_DIR}/shaders/") diff --git a/webgpu/compute/GpuTileStorage.cpp b/webgpu/compute/GpuTileStorage.cpp new file mode 100644 index 000000000..3626a01c7 --- /dev/null +++ b/webgpu/compute/GpuTileStorage.cpp @@ -0,0 +1,164 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "GpuTileStorage.h" + +#include "nucleus/tile/conversion.h" +#include "nucleus/utils/image_loader.h" + +namespace webgpu_compute { + +TileStorageTexture::TileStorageTexture(WGPUDevice device, WGPUTextureDescriptor texture_desc, WGPUSamplerDescriptor sampler_desc) + : m_device { device } + , m_queue { wgpuDeviceGetQueue(device) } + , m_resolution { texture_desc.size.width, texture_desc.size.height } + , m_capacity { texture_desc.size.depthOrArrayLayers } + , m_layers_used(m_capacity, false) +{ + m_texture_array = std::make_unique(m_device, texture_desc, sampler_desc); +} + +TileStorageTexture::TileStorageTexture(WGPUDevice device, const glm::uvec2& resolution, size_t capacity, WGPUTextureFormat format, WGPUTextureUsage usage) + : webgpu_compute::TileStorageTexture(device, create_default_texture_descriptor(resolution, capacity, format, usage), create_default_sampler_descriptor()) +{ +} + +void TileStorageTexture::store(size_t layer, const QByteArray& data) +{ + assert(layer < m_capacity); + + // convert to raster and store in texture array + const nucleus::Raster height_image = nucleus::utils::image_loader::rgba8(data).value(); + const auto heightraster = nucleus::tile::conversion::to_u16raster(height_image); + m_texture_array->texture().write(m_queue, heightraster, uint32_t(layer)); + + set_layer_used(layer); +} + +size_t TileStorageTexture::store(const QByteArray& data) +{ + size_t layer_index = find_unused_layer_index(); + store(layer_index, data); + return layer_index; +} + +void TileStorageTexture::reserve(size_t layer) +{ + assert(!m_layers_used[layer]); + + set_layer_used(layer); +} + +size_t TileStorageTexture::reserve() +{ + size_t layer_index = find_unused_layer_index(); + set_layer_used(layer_index); + return layer_index; +} + +void TileStorageTexture::clear() +{ + m_num_stored = 0; + m_layers_used.clear(); + m_layers_used.resize(m_capacity, false); +} + +void TileStorageTexture::clear(size_t layer) +{ + assert(layer < m_capacity); + + // update used layers + if (m_layers_used.at(layer)) { + m_num_stored--; + m_layers_used[layer] = false; + } +} + +size_t TileStorageTexture::width() const { return m_texture_array->texture().descriptor().size.width; } + +size_t TileStorageTexture::height() const { return m_texture_array->texture().descriptor().size.height; } + +size_t TileStorageTexture::num_used() const { return used_layer_indices().size(); } + +size_t TileStorageTexture::capacity() const { return m_capacity; } + +std::vector TileStorageTexture::used_layer_indices() const +{ + std::vector indices; + indices.reserve(m_capacity); + for (uint32_t i = 0; i < m_capacity; i++) { + if (m_layers_used.at(i)) { + indices.emplace_back(i); + } + } + return indices; +} + +webgpu::raii::TextureWithSampler& TileStorageTexture::texture() { return *m_texture_array; } + +const webgpu::raii::TextureWithSampler& TileStorageTexture::texture() const { return *m_texture_array; } + +size_t TileStorageTexture::find_unused_layer_index() const +{ + assert(m_num_stored < m_capacity); + + auto found_at = std::find(m_layers_used.begin(), m_layers_used.end(), false); + return found_at - m_layers_used.begin(); +} + +void TileStorageTexture::set_layer_used(size_t layer) +{ + // update used layers + if (!m_layers_used.at(layer)) { + m_num_stored++; + m_layers_used[layer] = true; + } +} + +WGPUTextureDescriptor TileStorageTexture::create_default_texture_descriptor( + const glm::uvec2& resolution, size_t capacity, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "compute storage texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { uint32_t(resolution.x), uint32_t(resolution.y), uint32_t(capacity) }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + return texture_desc; +} + +WGPUSamplerDescriptor TileStorageTexture::create_default_sampler_descriptor() +{ + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "compute storage sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + return sampler_desc; +} + +} // namespace webgpu_compute diff --git a/webgpu/compute/GpuTileStorage.h b/webgpu/compute/GpuTileStorage.h new file mode 100644 index 000000000..891958f3c --- /dev/null +++ b/webgpu/compute/GpuTileStorage.h @@ -0,0 +1,72 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +namespace webgpu_compute { + +/// Minimal wrapper over texture array for more convenient usage (intended for storing tile textures). +class TileStorageTexture { + +public: + TileStorageTexture(WGPUDevice device, WGPUTextureDescriptor texture_desc, WGPUSamplerDescriptor sampler_desc); + + // convenience wrapper + TileStorageTexture(WGPUDevice device, + const glm::uvec2& resolution, + size_t capacity, + WGPUTextureFormat format, + WGPUTextureUsage usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst); + + void store(size_t layer, const QByteArray& data); + size_t store(const QByteArray& data); // store at next free spot + void reserve(size_t layer); // acts like store, but doesnt write anything (useful for reservering on CPU side and writing to indices from shader) + size_t reserve(); // reserve next free spot + void clear(); // clear all + void clear(size_t layer); + + size_t width() const; + size_t height() const; + size_t num_used() const; + size_t capacity() const; + std::vector used_layer_indices() const; + + webgpu::raii::TextureWithSampler& texture(); + const webgpu::raii::TextureWithSampler& texture() const; + +private: + size_t find_unused_layer_index() const; + void set_layer_used(size_t layer); + + static WGPUTextureDescriptor create_default_texture_descriptor( + const glm::uvec2& resolution, size_t capacity, WGPUTextureFormat format, WGPUTextureUsage usage); + static WGPUSamplerDescriptor create_default_sampler_descriptor(); + +private: + WGPUDevice m_device; + WGPUQueue m_queue; + glm::uvec2 m_resolution; + size_t m_capacity; + size_t m_num_stored = 0; // number of stored textures + std::vector m_layers_used; // CPU buffer for tracking which layers are currently used + std::unique_ptr m_texture_array; +}; + +} // namespace webgpu_compute diff --git a/webgpu/compute/GraphRunContext.h b/webgpu/compute/GraphRunContext.h new file mode 100644 index 000000000..8c53d8bf6 --- /dev/null +++ b/webgpu/compute/GraphRunContext.h @@ -0,0 +1,31 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace webgpu_compute { + +struct GraphRunContext { + uint64_t run_id = 0; + std::string run_datetime; // format: YYYY-mm-ddTHH-MM-SS +}; + +} // namespace webgpu_compute diff --git a/webgpu/compute/NodeGraph.cpp b/webgpu/compute/NodeGraph.cpp new file mode 100644 index 000000000..c08061970 --- /dev/null +++ b/webgpu/compute/NodeGraph.cpp @@ -0,0 +1,184 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Markus Rampp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeGraph.h" + +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +GraphRunFailureInfo::GraphRunFailureInfo(const std::string& node_name, NodeRunFailureInfo node_run_failure_info) + : m_node_name(node_name) + , m_node_run_failure_info(node_run_failure_info) +{ +} + +const std::string& GraphRunFailureInfo::node_name() const { return m_node_name; } + +const NodeRunFailureInfo& GraphRunFailureInfo::node_run_failure_info() const { return m_node_run_failure_info; } + +Node* NodeGraph::add_node(const std::string& name, std::unique_ptr node) +{ + assert(!m_nodes.contains(name)); + node->set_node_name(name); + m_nodes.emplace(name, std::move(node)); + return m_nodes.at(name).get(); +} + +void NodeGraph::remove_node(const std::string& name) +{ + auto it = m_nodes.find(name); + assert(it != m_nodes.end()); + Node* node = it->second.get(); + + for (auto& socket : node->input_sockets()) + socket.disconnect(); + + for (auto& socket : node->output_sockets()) { + auto connected = socket.connected_sockets(); + for (auto* input : connected) + input->disconnect(); + } + + m_nodes.erase(it); +} + +void NodeGraph::rename_node(const std::string& old_name, const std::string& new_name) +{ + assert(m_nodes.contains(old_name)); + assert(!m_nodes.contains(new_name)); + auto node = std::move(m_nodes.at(old_name)); + m_nodes.erase(old_name); + node->set_node_name(new_name); + m_nodes.emplace(new_name, std::move(node)); +} + +Node& NodeGraph::get_node(const std::string& node_name) { return *m_nodes.at(node_name); } + +const Node& NodeGraph::get_node(const std::string& node_name) const { return *m_nodes.at(node_name); } + +bool NodeGraph::exists_node(const std::string& node_name) const { return m_nodes.find(node_name) != m_nodes.end(); } + +std::unordered_map>& NodeGraph::get_nodes() { return m_nodes; } + +const std::unordered_map>& NodeGraph::get_nodes() const { return m_nodes; } + +tl::expected, std::string> NodeGraph::compute_topological_order() +{ + // basic idea: find topological ordering by counting in-coming edges (in-degree) + // 1. start with nodes that have no incoming edges + // 2. select node with 0 incoming edges + // 3. add it to topological order + // 4. "remove node" from graph, i.e. update in-degrees of nodes that are connected to outputs of this node + // -> this decreases in-degree of other nodes + // -> if some nodes reaches zero, add it to queue to for processing next + // + // known as https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + + if (m_nodes.empty()) + return tl::unexpected(std::string("node graph is empty")); + + std::unordered_map in_degrees; + std::queue node_queue; + std::vector topological_ordering; + + for (auto& [_, node] : m_nodes) { + uint32_t in_degree = 0; + for (auto& socket : node->input_sockets()) { + if (socket.is_socket_connected()) { + in_degree++; + } + } + in_degrees[node.get()] = in_degree; + if (in_degree == 0) { + node_queue.push(node.get()); + } + } + + while (!node_queue.empty()) { + Node* node = node_queue.front(); + node_queue.pop(); + topological_ordering.push_back(node); + for (auto& output_socket : node->output_sockets()) { + for (auto& connected_socket : output_socket.connected_sockets()) { + auto& connected_node = connected_socket->node(); + in_degrees[&connected_node]--; + if (in_degrees[&connected_node] == 0) { + node_queue.push(&connected_node); + } + } + } + } + + for (auto& [node, in_degree] : in_degrees) { + if (in_degree) { + return tl::unexpected(std::string("cycle in node graph detected")); + } + } + + return topological_ordering; +} + +void NodeGraph::connect_node_signals_and_slots() +{ + auto order_result = compute_topological_order(); + if (!order_result) { + qFatal() << "NodeGraph::connect_node_signals_and_slots:" << QString::fromStdString(order_result.error()); + } + const std::vector& topological_ordering = *order_result; + + for (auto& conn : m_topology_connections) + QObject::disconnect(conn); + m_topology_connections.clear(); + + m_topology_connections.push_back(connect(this, &NodeGraph::run_triggered, topological_ordering.front(), &Node::run)); + for (uint32_t i = 0; i < topological_ordering.size() - 1; i++) { + m_topology_connections.push_back(connect(topological_ordering[i], &Node::run_completed, topological_ordering[i + 1], &Node::run)); + } + m_topology_connections.push_back( + connect(topological_ordering.back(), &Node::run_completed, this, [this](webgpu_compute::GraphRunContext ctx) { emit run_completed(ctx); })); + + for (auto& [_, node] : m_nodes) { + m_topology_connections.push_back(connect(node.get(), &Node::run_failed, this, &NodeGraph::emit_graph_failure)); + } +} + +void NodeGraph::run() +{ + qDebug() << "running node graph ..."; + + ++m_run_id; + + std::string run_datetime = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH-mm-ss").toStdString(); + + emit run_triggered(webgpu_compute::GraphRunContext { m_run_id, run_datetime }); +} + +void NodeGraph::emit_graph_failure(NodeRunFailureInfo info) +{ + auto it = std::find_if(m_nodes.begin(), m_nodes.end(), [&info](const auto& key_value_pair) { return key_value_pair.second.get() == &info.node(); }); + assert(it != m_nodes.end()); + emit run_failed(GraphRunFailureInfo(it->first, info)); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/NodeGraph.h b/webgpu/compute/NodeGraph.h new file mode 100644 index 000000000..dbe79294f --- /dev/null +++ b/webgpu/compute/NodeGraph.h @@ -0,0 +1,96 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "GraphRunContext.h" +#include "nodes/Node.h" +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +class GraphRunFailureInfo { +public: + GraphRunFailureInfo() = delete; + GraphRunFailureInfo(const GraphRunFailureInfo&) = default; + + GraphRunFailureInfo(const std::string& node_name, NodeRunFailureInfo node_run_failure_info); + + [[nodiscard]] const std::string& node_name() const; + [[nodiscard]] const NodeRunFailureInfo& node_run_failure_info() const; + +private: + std::string m_node_name; + NodeRunFailureInfo m_node_run_failure_info; +}; + +// TODO define interface - or maybe for now, just use hardcoded graph for complete normals setup +class NodeGraph : public QObject { + Q_OBJECT + +public: + NodeGraph() = default; + + Node* add_node(const std::string& name, std::unique_ptr node); + void remove_node(const std::string& name); + void rename_node(const std::string& old_name, const std::string& new_name); + + [[nodiscard]] Node& get_node(const std::string& node_name); + [[nodiscard]] const Node& get_node(const std::string& node_name) const; + [[nodiscard]] bool exists_node(const std::string& node_name) const; + + [[nodiscard]] std::unordered_map>& get_nodes(); + [[nodiscard]] const std::unordered_map>& get_nodes() const; + + template + [[nodiscard]] NodeType& get_node_as(const std::string& node_name) + { + return static_cast(get_node(node_name)); + } + template + [[nodiscard]] const NodeType& get_node_as(const std::string& node_name) const + { + return static_cast(get_node(node_name)); + } + + // finds topological order of nodes and connects run_finished and run slots accordingly + // safe to call multiple times + [[nodiscard]] tl::expected, std::string> compute_topological_order(); + void connect_node_signals_and_slots(); + +public slots: + void run(); + void emit_graph_failure(NodeRunFailureInfo info); + +signals: + void run_triggered(webgpu_compute::GraphRunContext context); + void run_completed(webgpu_compute::GraphRunContext context); + void run_failed(GraphRunFailureInfo info); + +private: + std::unordered_map> m_nodes; + std::vector m_topology_connections; + + uint64_t m_run_id = 0; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/NodeGraphSerialization.cpp b/webgpu/compute/NodeGraphSerialization.cpp new file mode 100644 index 000000000..154f5370f --- /dev/null +++ b/webgpu/compute/NodeGraphSerialization.cpp @@ -0,0 +1,153 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeGraphSerialization.h" + +#include "NodeRegistry.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +QJsonObject serialize_node_graph(const NodeGraph& graph) +{ + QJsonObject root; + root["format"] = QLatin1String(NODE_GRAPH_JSON_FORMAT); + root["version"] = NODE_GRAPH_JSON_VERSION; + + // m_nodes is an unordered_map; sort by name for stable, diffable output + std::vector> sorted_nodes; + sorted_nodes.reserve(graph.get_nodes().size()); + for (const auto& [name, node] : graph.get_nodes()) + sorted_nodes.emplace_back(name, node.get()); + std::sort(sorted_nodes.begin(), sorted_nodes.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); + + QJsonArray nodes_array; + QJsonArray connections_array; + for (const auto& [name, node] : sorted_nodes) { + QJsonObject node_object; + node_object["name"] = QString::fromStdString(name); + node_object["type"] = QString::fromStdString(node->get_type_name()); + node_object["enabled"] = node->is_enabled(); + QJsonObject settings; + node->serialize_settings(settings); + if (!settings.isEmpty()) + node_object["settings"] = settings; + nodes_array.append(node_object); + + // Collecting connections from the input side (each input has at most one source) + // yields each connection exactly once, in deterministic order. + for (const InputSocket& input_socket : node->input_sockets()) { + if (!input_socket.is_socket_connected()) + continue; + const OutputSocket& output_socket = input_socket.connected_socket(); + QJsonObject from; + from["node"] = QString::fromStdString(output_socket.node().get_node_name()); + from["socket"] = QString::fromStdString(output_socket.name()); + QJsonObject to; + to["node"] = QString::fromStdString(name); + to["socket"] = QString::fromStdString(input_socket.name()); + QJsonObject connection; + connection["from"] = from; + connection["to"] = to; + connections_array.append(connection); + } + } + root["nodes"] = nodes_array; + root["connections"] = connections_array; + return root; +} + +tl::expected, std::string> deserialize_node_graph(const QJsonObject& root, webgpu::Context& ctx) +{ + // Format / version guard + if (root["format"].toString() != QLatin1String(NODE_GRAPH_JSON_FORMAT)) + return tl::unexpected(std::string("invalid format tag (expected \"") + NODE_GRAPH_JSON_FORMAT + "\")"); + if (root["version"].toInt(-1) != NODE_GRAPH_JSON_VERSION) + return tl::unexpected(std::string("unsupported version (expected ") + std::to_string(NODE_GRAPH_JSON_VERSION) + ")"); + + auto graph = std::make_unique(); + + // Create nodes + const QJsonArray nodes_array = root["nodes"].toArray(); + for (const QJsonValue& val : nodes_array) { + const QJsonObject node_obj = val.toObject(); + const std::string node_name = node_obj["name"].toString().toStdString(); + const std::string type_name = node_obj["type"].toString().toStdString(); + + if (node_name.empty()) + return tl::unexpected(std::string("node entry is missing \"name\"")); + if (graph->exists_node(node_name)) + return tl::unexpected("duplicate node name: \"" + node_name + "\""); + + auto node = NodeRegistry::instance().try_create(type_name, ctx); + if (!node) + return tl::unexpected("unknown node type: \"" + type_name + "\""); + + node->set_enabled(node_obj["enabled"].toBool(true)); + if (node_obj.contains("settings")) + node->deserialize_settings(node_obj["settings"].toObject()); + + graph->add_node(node_name, std::move(node)); + } + + // Wire connections + const QJsonArray connections_array = root["connections"].toArray(); + for (const QJsonValue& val : connections_array) { + const QJsonObject conn = val.toObject(); + const QJsonObject from_obj = conn["from"].toObject(); + const QJsonObject to_obj = conn["to"].toObject(); + + const std::string from_node = from_obj["node"].toString().toStdString(); + const std::string from_socket = from_obj["socket"].toString().toStdString(); + const std::string to_node = to_obj["node"].toString().toStdString(); + const std::string to_socket = to_obj["socket"].toString().toStdString(); + + if (!graph->exists_node(from_node)) + return tl::unexpected("connection references unknown source node \"" + from_node + "\""); + if (!graph->exists_node(to_node)) + return tl::unexpected("connection references unknown destination node \"" + to_node + "\""); + + Node& src = graph->get_node(from_node); + Node& dst = graph->get_node(to_node); + + if (!src.has_output_socket(from_socket)) + return tl::unexpected("node \"" + from_node + "\" has no output socket \"" + from_socket + "\""); + if (!dst.has_input_socket(to_socket)) + return tl::unexpected("node \"" + to_node + "\" has no input socket \"" + to_socket + "\""); + + OutputSocket& output = src.output_socket(from_socket); + InputSocket& input = dst.input_socket(to_socket); + + if (output.type() != input.type()) + return tl::unexpected("type mismatch: \"" + from_node + "\":\"" + from_socket + "\" -> \"" + to_node + "\":\"" + to_socket + "\""); + + input.connect(output); + } + + // Cycle / empty check before wiring Qt signals + auto topo = graph->compute_topological_order(); + if (!topo) + return tl::unexpected(topo.error()); + + graph->connect_node_signals_and_slots(); + return graph; +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/NodeGraphSerialization.h b/webgpu/compute/NodeGraphSerialization.h new file mode 100644 index 000000000..02f7132c9 --- /dev/null +++ b/webgpu/compute/NodeGraphSerialization.h @@ -0,0 +1,54 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "NodeGraph.h" +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +// JSON schema (version 1): +// { +// "format": "webigeo/node-graph", +// "version": 1, +// "name": "", +// "nodes": [ { "name": ..., "type": ..., "enabled": ..., "settings": {...} }, ... ], +// "connections": [ { "from": { "node": ..., "socket": ... }, "to": { "node": ..., "socket": ... } }, ... ], +// "ui": { "nodes": { "": {...} } } // optional, written/consumed by the app layer only +// } +// +// Unknown keys are ignored; missing settings keys keep their C++ defaults. + +inline constexpr const char* NODE_GRAPH_JSON_FORMAT = "webigeo/node-graph"; +inline constexpr int NODE_GRAPH_JSON_VERSION = 1; + +/// Serializes the engine state of the graph (no "ui" section). Nodes are sorted by +/// name so repeated saves of the same graph are byte-identical. +[[nodiscard]] QJsonObject serialize_node_graph(const NodeGraph& graph); + +/// Creates a graph from JSON, with full validation (format/version, duplicate or unknown +/// node types, unknown sockets, socket type mismatches, cycles). On success the returned +/// graph is fully wired (connect_node_signals_and_slots has been called). Ignores "ui". +[[nodiscard]] tl::expected, std::string> deserialize_node_graph(const QJsonObject& root, webgpu::Context& ctx); + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/NodeRegistry.cpp b/webgpu/compute/NodeRegistry.cpp new file mode 100644 index 000000000..792ca3fba --- /dev/null +++ b/webgpu/compute/NodeRegistry.cpp @@ -0,0 +1,96 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "NodeRegistry.h" + +#include "nodes/BufferToTextureNode.h" +#include "nodes/ComputeAvalancheTrajectoriesNode.h" +#include "nodes/ComputeNormalsNode.h" +#include "nodes/ComputeReleasePointsNode.h" +#include "nodes/ComputeSnowNode.h" +#include "nodes/ExportNode.h" +#include "nodes/GPXTrackNode.h" +#include "nodes/HeightDecodeNode.h" +#include "nodes/IterativeSimulationNode.h" +#include "nodes/LoadTextureNode.h" +#include "nodes/RequestTilesNode.h" +#include "nodes/SelectTilesNode.h" +#include "nodes/TileStitchNode.h" +#include +#include +#include + +namespace webgpu_compute { + +NodeRegistry::NodeRegistry() +{ + // ToDo: Make sure all nodes share the same constructor and create Makro for register process + register_node("GPXTrackNode", [](webgpu::Context&) { return std::make_unique(); }); + register_node("SelectTilesNode", [](webgpu::Context&) { return std::make_unique(); }); + register_node("RequestTilesNode", [](webgpu::Context&) { return std::make_unique(); }); + register_node("ComputeNormalsNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("TileStitchNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("HeightDecodeNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("ComputeReleasePointsNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("ComputeAvalancheTrajectoriesNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("BufferToTextureNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("ComputeSnowNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("IterativeSimulationNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("ExportNode", [](webgpu::Context& c) { return std::make_unique(c); }); + register_node("LoadTextureNode", [](webgpu::Context& c) { return std::make_unique(c); }); +} + +NodeRegistry& NodeRegistry::instance() +{ + static NodeRegistry registry; + return registry; +} + +void NodeRegistry::register_node(const std::string& type_name, NodeFactory factory) { m_factories[type_name] = std::move(factory); } + +bool NodeRegistry::is_registered(const std::string& type_name) const { return m_factories.find(type_name) != m_factories.end(); } + +std::vector NodeRegistry::get_registered_types() const +{ + std::vector types; + types.reserve(m_factories.size()); + for (const auto& [name, _] : m_factories) + types.push_back(name); + std::sort(types.begin(), types.end()); + return types; +} + +std::unique_ptr NodeRegistry::create(const std::string& type_name, webgpu::Context& ctx) const +{ + auto node = try_create(type_name, ctx); + if (!node) { + qCritical() << "NodeRegistry::create: no node registered for type" << QString::fromStdString(type_name); + assert(false && "NodeRegistry::create: unknown node type"); + } + return node; +} + +std::unique_ptr NodeRegistry::try_create(const std::string& type_name, webgpu::Context& ctx) const +{ + const auto it = m_factories.find(type_name); + if (it == m_factories.end()) + return nullptr; + return it->second(ctx); +} + +} // namespace webgpu_compute diff --git a/webgpu/compute/NodeRegistry.h b/webgpu/compute/NodeRegistry.h new file mode 100644 index 000000000..3defb06ca --- /dev/null +++ b/webgpu/compute/NodeRegistry.h @@ -0,0 +1,51 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "nodes/Node.h" +#include +#include +#include +#include +#include + +namespace webgpu_compute { +class NodeRegistry { +public: + using NodeFactory = std::function(webgpu::Context&)>; + + static NodeRegistry& instance(); + + void register_node(const std::string& type_name, NodeFactory factory); + [[nodiscard]] bool is_registered(const std::string& type_name) const; + [[nodiscard]] std::vector get_registered_types() const; + + // Creates a node of the given registered type. Asserts if the type is unknown. + [[nodiscard]] std::unique_ptr create(const std::string& type_name, webgpu::Context& ctx) const; + + // Non-asserting variant for data-driven loading. Returns nullptr if the type is unknown. + [[nodiscard]] std::unique_ptr try_create(const std::string& type_name, webgpu::Context& ctx) const; + +private: + NodeRegistry(); // registers all core compute nodes + + std::unordered_map m_factories; +}; + +} // namespace webgpu_compute diff --git a/webgpu/compute/RectangularTileRegion.cpp b/webgpu/compute/RectangularTileRegion.cpp new file mode 100644 index 000000000..80e1ef017 --- /dev/null +++ b/webgpu/compute/RectangularTileRegion.cpp @@ -0,0 +1,37 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RectangularTileRegion.h" + +namespace webgpu_compute { + +std::vector RectangularTileRegion::get_tiles() const +{ + assert(min.x <= max.x); + assert(min.y <= max.y); + std::vector tiles; + tiles.reserve((max.x - min.x + 1) * (max.y - min.y + 1)); + for (unsigned x = min.x; x <= max.x; x++) { + for (unsigned y = min.y; y <= max.y; y++) { + tiles.emplace_back(radix::tile::Id { zoom_level, { x, y }, scheme }); + } + } + return tiles; +} + +} // namespace webgpu_compute diff --git a/webgpu/compute/RectangularTileRegion.h b/webgpu/compute/RectangularTileRegion.h new file mode 100644 index 000000000..707b464dd --- /dev/null +++ b/webgpu/compute/RectangularTileRegion.h @@ -0,0 +1,35 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "radix/tile.h" +#include + +namespace webgpu_compute { + +struct RectangularTileRegion { + glm::uvec2 min; + glm::uvec2 max; + unsigned int zoom_level; + radix::tile::Scheme scheme; + + std::vector get_tiles() const; +}; + +} // namespace webgpu_compute diff --git a/webgpu/compute/nodes/BufferToTextureNode.cpp b/webgpu/compute/nodes/BufferToTextureNode.cpp new file mode 100644 index 000000000..b5812a34b --- /dev/null +++ b/webgpu/compute/nodes/BufferToTextureNode.cpp @@ -0,0 +1,249 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "BufferToTextureNode.h" +#include "util.h" +#include "webgpu/base/raii/Texture.h" +#include + +namespace webgpu_compute::nodes { + +glm::uvec3 BufferToTextureNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +const uint32_t BufferToTextureNode::MAX_TEXTURE_RESOLUTION = 8192; + +BufferToTextureNode::BufferToTextureNode(webgpu::Context& ctx) + : BufferToTextureNode(ctx, BufferToTextureSettings()) +{ +} + +BufferToTextureNode::BufferToTextureNode(webgpu::Context& ctx, const BufferToTextureSettings& settings) + : Node({ InputSocket(*this, "raster dimensions", data_type()), + InputSocket(*this, "storage buffer", data_type*>()), + InputSocket(*this, "transparency buffer", data_type*>()) }, + { + OutputSocket(*this, "texture", data_type(), [this]() { return m_output_textures[m_pingpong].get(); }), + }) + , m_ctx(&ctx) + , m_settings { settings } + , m_settings_uniform(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("buffer_to_texture_compute", "webgpu_compute::buffer_to_texture_compute"); + reg.register_bind_group_layout("buffer_to_texture_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + + WGPUBindGroupLayoutEntry e5 {}; + e5.binding = 5; + e5.visibility = WGPUShaderStage_Compute; + e5.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e5.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + e5.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique( + dev, std::vector { e0, e1, e2, e5 }, "buffer to texture compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("buffer_to_texture_compute"), + std::vector { ®.bind_group_layout("buffer_to_texture_compute") }); + }); +} + +void BufferToTextureNode::run_impl() +{ + + const auto input_raster_dimensions = std::get()>(input_socket("raster dimensions").get_connected_data()); + const auto& input_storage_buffer = *std::get*>()>(input_socket("storage buffer").get_connected_data()); + const auto& input_transparency_buffer + = *std::get*>()>(input_socket("transparency buffer").get_connected_data()); + + m_settings_uniform.data.input_resolution = input_raster_dimensions; + update_gpu_settings(); + + // assert input textures have same size, otherwise fail run + if (input_raster_dimensions.x > MAX_TEXTURE_RESOLUTION || input_raster_dimensions.y > MAX_TEXTURE_RESOLUTION) { + fail_run("cannot create texture: texture dimensions (" + std::to_string(input_raster_dimensions.x) + "x" + + std::to_string(input_raster_dimensions.y) + ") exceed " + std::to_string(MAX_TEXTURE_RESOLUTION)); + return; + } + + m_pingpong = 1 - m_pingpong; // write the other slot; the previously produced texture stays alive for rendering + m_output_textures[m_pingpong] = create_texture(m_ctx->device(), input_raster_dimensions.x, input_raster_dimensions.y, m_settings); + // create bind group + std::vector entries { + m_settings_uniform.raw_buffer().create_bind_group_entry(0), + input_storage_buffer.create_bind_group_entry(1), + input_transparency_buffer.create_bind_group_entry(2), + m_output_views[m_pingpong]->create_bind_group_entry(5), + }; + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("buffer_to_texture_compute"), entries, "buffer to texture compute bind group"); + // bind GPU resources and run pipeline + { + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "buffer to texture compute command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "buffer to texture compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(input_raster_dimensions.x, input_raster_dimensions.y, 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "buffer to texture compute command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + auto* node = reinterpret_cast(userdata); + if (node->m_settings.create_mipmaps) { + const auto on_mipmaps_done = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, + [[maybe_unused]] WGPUStringView message, + void* userdata, + [[maybe_unused]] void* userdata2) { reinterpret_cast(userdata)->complete_run(); }; + webgpu::compute_mipmaps_for_texture(*node->m_ctx, + &node->m_output_textures[node->m_pingpong]->texture(), + WGPUQueueWorkDoneCallbackInfo { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_mipmaps_done, + .userdata1 = node, + .userdata2 = nullptr, + }); + } else { + node->complete_run(); + } + }; + + wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), + WGPUQueueWorkDoneCallbackInfo { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }); +} + +void BufferToTextureNode::update_gpu_settings() +{ + m_settings_uniform.data.color_map_bounds = m_settings.color_map_bounds; + m_settings_uniform.data.transparency_map_bounds = m_settings.transparency_map_bounds; + m_settings_uniform.data.use_bin_interpolation = m_settings.use_bin_interpolation; + m_settings_uniform.data.use_transparency_buffer = m_settings.use_transparency_buffer; + m_settings_uniform.update_gpu_data(m_ctx->queue()); +} + +std::unique_ptr BufferToTextureNode::create_texture( + WGPUDevice device, uint32_t width, uint32_t height, BufferToTextureSettings& settings) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "buffer to texture output texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = settings.create_mipmaps ? webgpu::raii::Texture::max_mip_level_count(glm::uvec2(width, height)) : 1; + texture_desc.sampleCount = 1; + texture_desc.format = settings.texture_format; + texture_desc.usage = settings.texture_usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "buffer to texture sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = settings.texture_filter_mode; + sampler_desc.minFilter = settings.texture_filter_mode; + sampler_desc.mipmapFilter = settings.texture_mipmap_filter_mode; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = float(texture_desc.mipLevelCount); + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = settings.texture_max_aniostropy; + + auto texture_with_sampler = std::make_unique(device, texture_desc, sampler_desc); + + WGPUTextureViewDescriptor desc = texture_with_sampler->texture().default_texture_view_descriptor(); + desc.mipLevelCount = 1; + m_output_views[m_pingpong] = texture_with_sampler->texture().create_view(desc); + + return texture_with_sampler; +} + +void BufferToTextureNode::serialize_settings(QJsonObject& out) const +{ + out["texture_format"] = wgpu_format_to_string(m_settings.texture_format); + out["texture_usage"] = wgpu_usage_to_json(m_settings.texture_usage); + out["texture_filter_mode"] = wgpu_filter_mode_to_string(m_settings.texture_filter_mode); + out["texture_mipmap_filter_mode"] = wgpu_mipmap_filter_mode_to_string(m_settings.texture_mipmap_filter_mode); + out["texture_max_aniostropy"] = static_cast(m_settings.texture_max_aniostropy); + out["create_mipmaps"] = m_settings.create_mipmaps; + out["color_map_bounds"] = vec2_to_json(m_settings.color_map_bounds); + out["transparency_map_bounds"] = vec2_to_json(m_settings.transparency_map_bounds); + out["use_bin_interpolation"] = m_settings.use_bin_interpolation; + out["use_transparency_buffer"] = m_settings.use_transparency_buffer; +} + +void BufferToTextureNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("texture_format")) + m_settings.texture_format = wgpu_format_from_string(in["texture_format"].toString(), m_settings.texture_format); + if (in.contains("texture_usage")) + m_settings.texture_usage = wgpu_usage_from_json(in["texture_usage"].toArray(), m_settings.texture_usage); + if (in.contains("texture_filter_mode")) + m_settings.texture_filter_mode = wgpu_filter_mode_from_string(in["texture_filter_mode"].toString(), m_settings.texture_filter_mode); + if (in.contains("texture_mipmap_filter_mode")) + m_settings.texture_mipmap_filter_mode + = wgpu_mipmap_filter_mode_from_string(in["texture_mipmap_filter_mode"].toString(), m_settings.texture_mipmap_filter_mode); + if (in.contains("texture_max_aniostropy")) + m_settings.texture_max_aniostropy = static_cast(in["texture_max_aniostropy"].toInt(m_settings.texture_max_aniostropy)); + if (in.contains("create_mipmaps")) + m_settings.create_mipmaps = in["create_mipmaps"].toBool(m_settings.create_mipmaps); + if (in.contains("color_map_bounds")) + m_settings.color_map_bounds = vec2_from_json(in["color_map_bounds"].toArray(), m_settings.color_map_bounds); + if (in.contains("transparency_map_bounds")) + m_settings.transparency_map_bounds = vec2_from_json(in["transparency_map_bounds"].toArray(), m_settings.transparency_map_bounds); + if (in.contains("use_bin_interpolation")) + m_settings.use_bin_interpolation = in["use_bin_interpolation"].toBool(m_settings.use_bin_interpolation); + if (in.contains("use_transparency_buffer")) + m_settings.use_transparency_buffer = in["use_transparency_buffer"].toBool(m_settings.use_transparency_buffer); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/BufferToTextureNode.h b/webgpu/compute/nodes/BufferToTextureNode.h new file mode 100644 index 000000000..15578fef4 --- /dev/null +++ b/webgpu/compute/nodes/BufferToTextureNode.h @@ -0,0 +1,110 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +// Takes a list of tile ids, a tile id-to-index hashmap and a int32 storage buffer. +// The storage buffer contains raster data per tile. The resolution has to be specified as output_resolution on construction. +// +// Data for tile_id, row_index (from top to bottom) and col_index (from left to right) is stored in the buffer at index +// +// hashmap.get_index(tile_id) * tile_dimensions.x * tile_dimensions.y + row_index * tile_dimensions.x + col_index +// +// where tile_dimensions is the per-tile resolution. +// +// Each entry is expected to be between 0 and 2^32-1 (i.e. using full uint32). +// The entries are mapped to colors based on a color mapping function (defined in shader code). +// +// TODO: add settings struct, be able to change color mapping during runtime (without changing shader source and recompiling) +class BufferToTextureNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(BufferToTextureNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + static const uint32_t MAX_TEXTURE_RESOLUTION; + + struct BufferToTextureSettings { + WGPUTextureFormat texture_format = WGPUTextureFormat_RGBA8Unorm; + WGPUTextureUsage texture_usage + = (WGPUTextureUsage)(WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment); + WGPUFilterMode texture_filter_mode = WGPUFilterMode_Linear; + WGPUMipmapFilterMode texture_mipmap_filter_mode = WGPUMipmapFilterMode_Linear; + uint16_t texture_max_aniostropy = 1; + + bool create_mipmaps = true; + + glm::vec2 color_map_bounds = { 0.0f, 40.0f }; + glm::vec2 transparency_map_bounds = { 0.0f, 1.0f }; // x gets mapped to fully invisible, y to fully visible + bool use_bin_interpolation = false; // if true, use linear interpolation between color bins + bool use_transparency_buffer = true; // if true, the transparency texture is used to evaluate an alpha factor based on the alpha_remap_bounds + }; + + struct BufferToTextureSettingsUniform { + glm::uvec2 input_resolution = glm::uvec2(0u); // is set based on input "raster dimensions" + glm::vec2 color_map_bounds; + glm::vec2 transparency_map_bounds; + uint32_t use_bin_interpolation; + uint32_t use_transparency_buffer; + }; + + BufferToTextureSettings& settings() { return m_settings; } + + // settings are consumed lazily in run_impl, applying them at any time is safe + void set_settings(const BufferToTextureSettings& settings) { m_settings = settings; } + const BufferToTextureSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + + BufferToTextureNode(webgpu::Context& ctx); + BufferToTextureNode(webgpu::Context& ctx, const BufferToTextureSettings& settings); + +public slots: + void run_impl() override; + +private: + void update_gpu_settings(); + + std::unique_ptr create_texture(WGPUDevice device, uint32_t width, uint32_t height, BufferToTextureSettings& settings); + +private: + webgpu::Context* m_ctx; + + BufferToTextureSettings m_settings; + webgpu::Buffer m_settings_uniform; + std::unique_ptr m_pipeline; + + // IMPORTANT: The output needs to be double-buffered if linked to rendering + std::array, 2> m_output_textures; + // NOTE: We need a non default texture view as a storage texture can only have mipLevelCount = 1 + std::array, 2> m_output_views; + int m_pingpong = 0; // current output_texture to write to +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.cpp b/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.cpp new file mode 100644 index 000000000..d71da23ba --- /dev/null +++ b/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.cpp @@ -0,0 +1,431 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeAvalancheTrajectoriesNode.h" + +#include + +namespace webgpu_compute::nodes { + +glm::uvec3 ComputeAvalancheTrajectoriesNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +ComputeAvalancheTrajectoriesNode::ComputeAvalancheTrajectoriesNode(webgpu::Context& ctx) + : ComputeAvalancheTrajectoriesNode(ctx, AvalancheTrajectoriesSettings()) +{ +} + +ComputeAvalancheTrajectoriesNode::ComputeAvalancheTrajectoriesNode(webgpu::Context& ctx, const AvalancheTrajectoriesSettings& settings) + : Node( + { + InputSocket(*this, "region aabb", data_type*>()), + InputSocket(*this, "normal texture", data_type()), + InputSocket(*this, "height texture", data_type()), + InputSocket(*this, "release point texture", data_type()), + }, + { + OutputSocket(*this, "storage buffer", data_type*>(), [this]() { return m_output_storage_buffer.get(); }), + OutputSocket(*this, "raster dimensions", data_type(), [this]() { return m_output_dimensions; }), + OutputSocket(*this, "layer1_zdelta", data_type*>(), [this]() { return m_layer1_zdelta_buffer.get(); }), + OutputSocket(*this, "layer2_cellCounts", data_type*>(), [this]() { return m_layer2_cellCounts_buffer.get(); }), + OutputSocket( + *this, "layer3_travelLength", data_type*>(), [this]() { return m_layer3_travelLength_buffer.get(); }), + OutputSocket( + *this, "layer4_travelAngle", data_type*>(), [this]() { return m_layer4_travelAngle_buffer.get(); }), + OutputSocket(*this, + "layer5_altitudeDifference", + data_type*>(), + [this]() { return m_layer5_altitudeDifference_buffer.get(); }), + }) + , m_ctx(&ctx) + , m_settings { settings } + , m_settings_uniform(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) + , m_normal_sampler(create_normal_sampler(m_ctx->device())) + , m_height_sampler(create_height_sampler(m_ctx->device())) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("avalanche_trajectories_compute", "webgpu_compute::avalanche_trajectories_compute"); + reg.register_bind_group_layout("avalanche_trajectories_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.texture.sampleType = WGPUTextureSampleType_Float; + e1.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + e2.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e3 {}; + e3.binding = 3; + e3.visibility = WGPUShaderStage_Compute; + e3.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + e3.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e4 {}; + e4.binding = 4; + e4.visibility = WGPUShaderStage_Compute; + e4.sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutEntry e5 {}; + e5.binding = 5; + e5.visibility = WGPUShaderStage_Compute; + e5.sampler.type = WGPUSamplerBindingType_NonFiltering; + + WGPUBindGroupLayoutEntry e6 {}; + e6.binding = 6; + e6.visibility = WGPUShaderStage_Compute; + e6.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e7 {}; + e7.binding = 7; + e7.visibility = WGPUShaderStage_Compute; + e7.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e8 {}; + e8.binding = 8; + e8.visibility = WGPUShaderStage_Compute; + e8.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e9 {}; + e9.binding = 9; + e9.visibility = WGPUShaderStage_Compute; + e9.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e10 {}; + e10.binding = 10; + e10.visibility = WGPUShaderStage_Compute; + e10.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e11 {}; + e11.binding = 11; + e11.visibility = WGPUShaderStage_Compute; + e11.buffer.type = WGPUBufferBindingType_Storage; + + return std::make_unique(dev, + std::vector { e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11 }, + "avalanche trajectories compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("avalanche_trajectories_compute"), + std::vector { ®.bind_group_layout("avalanche_trajectories_compute") }); + }); +} + +void ComputeAvalancheTrajectoriesNode::update_gpu_settings(uint32_t run) +{ + m_settings_uniform.data.num_steps = m_settings.num_steps; + m_settings_uniform.data.step_length = m_settings.step_length; + m_settings_uniform.data.max_perturbation = m_settings.max_perturbation; + m_settings_uniform.data.persistence_contribution = m_settings.persistence_contribution; + + m_settings_uniform.data.model_type = m_settings.active_model; + m_settings_uniform.data.model2_gravity = m_settings.model2.gravity; + m_settings_uniform.data.model2_mass = m_settings.model2.mass; + m_settings_uniform.data.model2_friction_coeff = m_settings.model2.friction_coeff; + m_settings_uniform.data.model2_drag_coeff = m_settings.model2.drag_coeff; + + m_settings_uniform.data.runout_model_type = m_settings.active_runout_model; + m_settings_uniform.data.runout_perla_my = m_settings.runout_perla.my; + m_settings_uniform.data.runout_perla_md = m_settings.runout_perla.md; + m_settings_uniform.data.runout_perla_l = m_settings.runout_perla.l; + m_settings_uniform.data.runout_perla_g = m_settings.runout_perla.g; + m_settings_uniform.data.runout_flowpy_alpha = glm::radians(m_settings.runout_flowpy.alpha); + + // TODO: Get activation of layer if output socket is connected and following node is enabled? + m_settings_uniform.data.output_layer = m_settings.output_layer; + + m_settings_uniform.data.random_seed = m_settings.random_seed + run; + + m_settings_uniform.update_gpu_data(m_ctx->queue()); +} + +void ComputeAvalancheTrajectoriesNode::set_settings(const AvalancheTrajectoriesSettings& settings) { m_settings = settings; } + +const ComputeAvalancheTrajectoriesNode::AvalancheTrajectoriesSettings& ComputeAvalancheTrajectoriesNode::get_settings() const { return m_settings; } + +void ComputeAvalancheTrajectoriesNode::run_impl() +{ + + const auto region_aabb = std::get*>()>(input_socket("region aabb").get_connected_data()); + const auto& normal_texture = *std::get()>(input_socket("normal texture").get_connected_data()); + const auto& height_texture = *std::get()>(input_socket("height texture").get_connected_data()); + const auto& release_point_texture + = *std::get()>(input_socket("release point texture").get_connected_data()); + + const auto input_width = normal_texture.texture().width(); + const auto input_height = normal_texture.texture().height(); + + // assert input textures have same size, otherwise fail run + if (input_width != height_texture.texture().width() || input_height != height_texture.texture().height() + || input_width != release_point_texture.texture().width() || input_height != release_point_texture.texture().height()) { + fail_run("failed to compute trajectories: input texture sizes must match (normals: " + std::to_string(input_width) + "x" + std::to_string(input_height) + + ", heights: " + std::to_string(height_texture.texture().width()) + "x" + std::to_string(height_texture.texture().height()) + + ", release points: " + std::to_string(release_point_texture.texture().width()) + "x" + + std::to_string(release_point_texture.texture().height()) + ")"); + return; + } + + m_output_dimensions = glm::uvec2(input_width, input_height) * m_settings.resolution_multiplier; + + qDebug() << "input resolution: " << input_width << "x" << input_height; + qDebug() << "output resolution: " << m_output_dimensions.x << "x" << m_output_dimensions.y; + + // create output storage buffer + m_output_storage_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_output_dimensions.x * m_output_dimensions.y, + "avalanche trajectories compute output storage"); + + // create layer buffers + m_layer1_zdelta_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_settings.output_layer.layer1_zdelta_enabled ? (m_output_dimensions.x * m_output_dimensions.y) : 1, + "avalanche trajectories zdelta storage"); + m_layer2_cellCounts_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_settings.output_layer.layer2_cellCounts_enabled ? (m_output_dimensions.x * m_output_dimensions.y) : 1, + "avalanche trajectories cellCounts storage"); + m_layer3_travelLength_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_settings.output_layer.layer3_travelLength_enabled ? (m_output_dimensions.x * m_output_dimensions.y) : 1, + "avalanche trajectories travelLength storage"); + m_layer4_travelAngle_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_settings.output_layer.layer4_travelAngle_enabled ? (m_output_dimensions.x * m_output_dimensions.y) : 1, + "avalanche trajectories travelAngle storage"); + m_layer5_altitudeDifference_buffer = std::make_unique>(m_ctx->device(), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | WGPUBufferUsage_CopySrc, + m_settings.output_layer.layer5_altitudeDifference_enabled ? (m_output_dimensions.x * m_output_dimensions.y) : 1, + "avalanche trajectories altitudeDifference storage"); + + // update input settings on GPU side + m_settings_uniform.data.output_resolution = m_output_dimensions; + m_settings_uniform.data.region_size = glm::fvec2(region_aabb->size()); + update_gpu_settings(); + + // create bind group + std::vector entries { + m_settings_uniform.raw_buffer().create_bind_group_entry(0), + normal_texture.texture_view().create_bind_group_entry(1), + height_texture.texture_view().create_bind_group_entry(2), + release_point_texture.texture_view().create_bind_group_entry(3), + m_normal_sampler->create_bind_group_entry(4), + m_height_sampler->create_bind_group_entry(5), + m_output_storage_buffer->create_bind_group_entry(6), + m_layer1_zdelta_buffer->create_bind_group_entry(7), + m_layer2_cellCounts_buffer->create_bind_group_entry(8), + m_layer3_travelLength_buffer->create_bind_group_entry(9), + m_layer4_travelAngle_buffer->create_bind_group_entry(10), + m_layer5_altitudeDifference_buffer->create_bind_group_entry(11), + }; + + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("avalanche_trajectories_compute"), entries, "avalanche trajectories compute bind group"); + + // bind GPU resources and run pipeline + for (uint32_t run = 0; run < m_settings.num_runs; run++) { + update_gpu_settings(run); // change seed each run + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "avalanche trajectories compute command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "avalanche trajectories compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts + = glm::ceil(glm::vec3(input_width, input_height, m_settings.num_paths_per_release_cell) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "avalanche trajectories compute command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + ComputeAvalancheTrajectoriesNode* _this = reinterpret_cast(userdata); + _this->complete_run(); + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }; + + wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), callback_info); +} + +std::unique_ptr ComputeAvalancheTrajectoriesNode::create_normal_sampler(WGPUDevice device) +{ + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "compute trajectories normal sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + return std::make_unique(device, sampler_desc); +} + +std::unique_ptr ComputeAvalancheTrajectoriesNode::create_height_sampler(WGPUDevice device) +{ + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "compute trajectories height sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + return std::make_unique(device, sampler_desc); +} + +void ComputeAvalancheTrajectoriesNode::serialize_settings(QJsonObject& out) const +{ + const auto& s = m_settings; + out["resolution_multiplier"] = static_cast(s.resolution_multiplier); + out["num_steps"] = static_cast(s.num_steps); + out["step_length"] = static_cast(s.step_length); + out["num_paths_per_release_cell"] = static_cast(s.num_paths_per_release_cell); + out["num_runs"] = static_cast(s.num_runs); + out["max_perturbation"] = static_cast(s.max_perturbation); + out["persistence_contribution"] = static_cast(s.persistence_contribution); + out["active_model"] = static_cast(s.active_model); + out["model2"] = QJsonObject { + { "gravity", static_cast(s.model2.gravity) }, + { "mass", static_cast(s.model2.mass) }, + { "friction_coeff", static_cast(s.model2.friction_coeff) }, + { "drag_coeff", static_cast(s.model2.drag_coeff) }, + }; + out["active_runout_model"] = static_cast(s.active_runout_model); + out["runout_perla"] = QJsonObject { + { "my", static_cast(s.runout_perla.my) }, + { "md", static_cast(s.runout_perla.md) }, + { "l", static_cast(s.runout_perla.l) }, + { "g", static_cast(s.runout_perla.g) }, + }; + out["runout_flowpy"] = QJsonObject { { "alpha", static_cast(s.runout_flowpy.alpha) } }; + out["output_layer"] = QJsonObject { + { "layer1_zdelta_enabled", static_cast(s.output_layer.layer1_zdelta_enabled) }, + { "layer2_cellCounts_enabled", static_cast(s.output_layer.layer2_cellCounts_enabled) }, + { "layer3_travelLength_enabled", static_cast(s.output_layer.layer3_travelLength_enabled) }, + { "layer4_travelAngle_enabled", static_cast(s.output_layer.layer4_travelAngle_enabled) }, + { "layer5_altitudeDifference_enabled", static_cast(s.output_layer.layer5_altitudeDifference_enabled) }, + }; + out["random_seed"] = static_cast(s.random_seed); +} + +void ComputeAvalancheTrajectoriesNode::deserialize_settings(const QJsonObject& in) +{ + auto s = m_settings; + if (in.contains("resolution_multiplier")) + s.resolution_multiplier = static_cast(in["resolution_multiplier"].toInt(static_cast(s.resolution_multiplier))); + if (in.contains("num_steps")) + s.num_steps = static_cast(in["num_steps"].toInt(static_cast(s.num_steps))); + if (in.contains("step_length")) + s.step_length = static_cast(in["step_length"].toDouble(s.step_length)); + if (in.contains("num_paths_per_release_cell")) + s.num_paths_per_release_cell = static_cast(in["num_paths_per_release_cell"].toInt(static_cast(s.num_paths_per_release_cell))); + if (in.contains("num_runs")) + s.num_runs = static_cast(in["num_runs"].toInt(static_cast(s.num_runs))); + if (in.contains("max_perturbation")) + s.max_perturbation = static_cast(in["max_perturbation"].toDouble(s.max_perturbation)); + if (in.contains("persistence_contribution")) + s.persistence_contribution = static_cast(in["persistence_contribution"].toDouble(s.persistence_contribution)); + if (in.contains("active_model")) + s.active_model = static_cast(in["active_model"].toInt(static_cast(s.active_model))); + if (in.contains("model2")) { + const QJsonObject m = in["model2"].toObject(); + if (m.contains("gravity")) + s.model2.gravity = static_cast(m["gravity"].toDouble(s.model2.gravity)); + if (m.contains("mass")) + s.model2.mass = static_cast(m["mass"].toDouble(s.model2.mass)); + if (m.contains("friction_coeff")) + s.model2.friction_coeff = static_cast(m["friction_coeff"].toDouble(s.model2.friction_coeff)); + if (m.contains("drag_coeff")) + s.model2.drag_coeff = static_cast(m["drag_coeff"].toDouble(s.model2.drag_coeff)); + } + if (in.contains("active_runout_model")) + s.active_runout_model = static_cast(in["active_runout_model"].toInt(static_cast(s.active_runout_model))); + if (in.contains("runout_perla")) { + const QJsonObject p = in["runout_perla"].toObject(); + if (p.contains("my")) + s.runout_perla.my = static_cast(p["my"].toDouble(s.runout_perla.my)); + if (p.contains("md")) + s.runout_perla.md = static_cast(p["md"].toDouble(s.runout_perla.md)); + if (p.contains("l")) + s.runout_perla.l = static_cast(p["l"].toDouble(s.runout_perla.l)); + if (p.contains("g")) + s.runout_perla.g = static_cast(p["g"].toDouble(s.runout_perla.g)); + } + if (in.contains("runout_flowpy")) { + const QJsonObject f = in["runout_flowpy"].toObject(); + if (f.contains("alpha")) + s.runout_flowpy.alpha = static_cast(f["alpha"].toDouble(s.runout_flowpy.alpha)); + } + if (in.contains("output_layer")) { + const QJsonObject l = in["output_layer"].toObject(); + if (l.contains("layer1_zdelta_enabled")) + s.output_layer.layer1_zdelta_enabled + = static_cast(l["layer1_zdelta_enabled"].toInt(static_cast(s.output_layer.layer1_zdelta_enabled))); + if (l.contains("layer2_cellCounts_enabled")) + s.output_layer.layer2_cellCounts_enabled + = static_cast(l["layer2_cellCounts_enabled"].toInt(static_cast(s.output_layer.layer2_cellCounts_enabled))); + if (l.contains("layer3_travelLength_enabled")) + s.output_layer.layer3_travelLength_enabled + = static_cast(l["layer3_travelLength_enabled"].toInt(static_cast(s.output_layer.layer3_travelLength_enabled))); + if (l.contains("layer4_travelAngle_enabled")) + s.output_layer.layer4_travelAngle_enabled + = static_cast(l["layer4_travelAngle_enabled"].toInt(static_cast(s.output_layer.layer4_travelAngle_enabled))); + if (l.contains("layer5_altitudeDifference_enabled")) + s.output_layer.layer5_altitudeDifference_enabled + = static_cast(l["layer5_altitudeDifference_enabled"].toInt(static_cast(s.output_layer.layer5_altitudeDifference_enabled))); + } + if (in.contains("random_seed")) + s.random_seed = static_cast(in["random_seed"].toInt(static_cast(s.random_seed))); + set_settings(s); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.h b/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.h new file mode 100644 index 000000000..58ff173c6 --- /dev/null +++ b/webgpu/compute/nodes/ComputeAvalancheTrajectoriesNode.h @@ -0,0 +1,190 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Gerald Kimmersdorfer + * Copyright (C) 2025 Markus Rampp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" + +#include +#include +#include + +namespace webgpu_compute::nodes { + +class ComputeAvalancheTrajectoriesNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(ComputeAvalancheTrajectoriesNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + enum PhysicsModelType : uint32_t { + WEBIGEO_AVALANCHE_SIMULATION = 0, + PHYSICS_LESS_SIMPLE = 1, + }; + + enum FrictionModelType : uint32_t { + // actually: friction model: 0 coulomb, 1 voellmy, 2 voellmy minshear, 3 samosAt + Coulomb = 0, + Voellmy = 1, + VoellmyMinShear = 2, + SamosAt = 3, + }; + + struct ModelPhysicsLessSimpleParams { + float gravity = 9.81f; + float mass = 10.0f; + float friction_coeff = 0.155f; + float drag_coeff = 4000.f; + }; + + struct RunoutPerlaParams { + float my = 0.11f; // sliding friction coeff + float md = 40.0f; // M/D mass-to-drag ratio (in m) + float l = 1.0f; // distance between grid cells (in m) + float g = 9.81f; // acceleration due to gravity (in m/s^2) + }; + + /* FlowPy runout model */ + struct RunoutFlowPyParams { + float alpha = 25.0f; // in degrees + }; + + struct OutputLayerParams { + uint32_t layer1_zdelta_enabled = 1u; + uint32_t layer2_cellCounts_enabled = 1u; + uint32_t layer3_travelLength_enabled = 1u; + uint32_t layer4_travelAngle_enabled = 1u; + uint32_t layer5_altitudeDifference_enabled = 1u; + }; + + struct AvalancheTrajectoriesSettings { + uint32_t resolution_multiplier = 8; + uint32_t num_steps = 10000u; + float step_length = 0.1f; + + /* Number of trajectories to start from the same release cell. + * This only makes sense when max_perturbation is greater than 0.*/ + uint32_t num_paths_per_release_cell = 1024u; + + /* With only num_paths_per_release_cell we are limited by a maximum of dispatches + * for a single compute call. This is a workaround to allow more paths, by executing + * the compute dispatch multiple times with a different seed.*/ + uint32_t num_runs = 1u; + + /* Maximum perturbation angle (in radians) applied to the sampled normal. + 0 means no randomness. */ + float max_perturbation = glm::radians(25.0f); + + /* Persistence contribution to the (randomly offset) normal in [0,1]. + 0 means only local normal, 1 means only last normal*/ + float persistence_contribution = 0.9f; + + PhysicsModelType active_model; + ModelPhysicsLessSimpleParams model2; + + FrictionModelType active_runout_model = FrictionModelType::VoellmyMinShear; + RunoutPerlaParams runout_perla; + RunoutFlowPyParams runout_flowpy; + + OutputLayerParams output_layer; + + uint32_t random_seed = 1u; + }; + +private: + struct AvalancheTrajectoriesSettingsUniform { + glm::uvec2 output_resolution; + glm::fvec2 region_size; + + // ^^ 4 byte ^^ + + uint32_t num_steps; + float step_length; + // uint32_t num_paths_per_release_cell = 256; + + float max_perturbation; + float persistence_contribution; + + // ^^ 4 byte ^^ + + PhysicsModelType model_type; + float model2_gravity; + float model2_mass; + float model2_friction_coeff; + // ^^ 4 byte ^^ + float model2_drag_coeff; + + FrictionModelType runout_model_type; + + float runout_perla_my; + float runout_perla_md; + // ^^ 4 byte ^^ + float runout_perla_l; + float runout_perla_g; + + float runout_flowpy_alpha; + + OutputLayerParams output_layer; + // ^^ 8 byte ^^ + + uint32_t random_seed; + uint32_t _pad; // struct size must be a multiple of its alignment (8 bytes from vec2u/vec2f) + }; + +public: + ComputeAvalancheTrajectoriesNode(webgpu::Context& ctx); + ComputeAvalancheTrajectoriesNode(webgpu::Context& ctx, const AvalancheTrajectoriesSettings& settings); + + void set_settings(const AvalancheTrajectoriesSettings& settings); + const AvalancheTrajectoriesSettings& get_settings() const; + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + void update_gpu_settings(uint32_t run = 0u); + + static std::unique_ptr create_normal_sampler(WGPUDevice device); + static std::unique_ptr create_height_sampler(WGPUDevice device); + +private: + webgpu::Context* m_ctx; + + AvalancheTrajectoriesSettings m_settings; + webgpu::Buffer m_settings_uniform; + std::unique_ptr m_normal_sampler; + std::unique_ptr m_height_sampler; + std::unique_ptr m_pipeline; + std::unique_ptr> m_output_storage_buffer; + + std::unique_ptr> m_layer1_zdelta_buffer; + std::unique_ptr> m_layer2_cellCounts_buffer; + std::unique_ptr> m_layer3_travelLength_buffer; + std::unique_ptr> m_layer4_travelAngle_buffer; + std::unique_ptr> m_layer5_altitudeDifference_buffer; + + glm::uvec2 m_output_dimensions; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeNormalsNode.cpp b/webgpu/compute/nodes/ComputeNormalsNode.cpp new file mode 100644 index 000000000..cf5267f6f --- /dev/null +++ b/webgpu/compute/nodes/ComputeNormalsNode.cpp @@ -0,0 +1,183 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeNormalsNode.h" +#include "util.h" + +#include + +namespace webgpu_compute::nodes { + +glm::uvec3 ComputeNormalsNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +ComputeNormalsNode::ComputeNormalsNode(webgpu::Context& ctx) + : Node( + { + InputSocket(*this, "bounds", data_type*>()), + InputSocket(*this, "height texture", data_type()), + }, + { + OutputSocket(*this, "normal texture", data_type(), [this]() { return m_output_texture.get(); }), + }) + , m_ctx(&ctx) + , m_normals_settings_uniform_buffer(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("normals_compute", "webgpu_compute::normals_compute"); + reg.register_bind_group_layout("normals_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + e1.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e2.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + e2.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(dev, std::vector { e0, e1, e2 }, "normals compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique( + device, reg.shader("normals_compute"), std::vector { ®.bind_group_layout("normals_compute") }); + }); +} + +void ComputeNormalsNode::set_settings(const NormalSettings& settings) { m_settings = settings; } + +void ComputeNormalsNode::run_impl() +{ + + const auto& bounds = *std::get*>()>(input_socket("bounds").get_connected_data()); + const auto& height_texture = *std::get()>(input_socket("height texture").get_connected_data()); + + m_output_texture = create_normals_texture( + m_ctx->device(), uint32_t(height_texture.texture().width()), uint32_t(height_texture.texture().height()), m_settings.format, m_settings.usage); + + m_normals_settings_uniform_buffer.data.aabb_min = glm::fvec2(bounds.min); + m_normals_settings_uniform_buffer.data.aabb_max = glm::fvec2(bounds.max); + m_normals_settings_uniform_buffer.update_gpu_data(m_ctx->queue()); + + // create bind group + // TODO re-create bind groups only when input change + std::vector entries { + m_normals_settings_uniform_buffer.raw_buffer().create_bind_group_entry(0), + height_texture.texture_view().create_bind_group_entry(1), + m_output_texture->texture_view().create_bind_group_entry(2), + }; + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("normals_compute"), entries, "compute controller bind group"); + + // bind GPU resources and run pipeline + // the result is a texture array with the calculated overlays, and a hashmap that maps id to texture array index + // the shader will only writes into texture array, the hashmap is written on cpu side + { + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "compute controller command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "compute controller compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts + = glm::ceil(glm::vec3(m_output_texture->texture().width(), m_output_texture->texture().height(), 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "NormalComputeNode command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + ComputeNormalsNode* _this = reinterpret_cast(userdata); + _this->complete_run(); + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }; + + wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), callback_info); + // emit run_completed(); +} + +std::unique_ptr ComputeNormalsNode::create_normals_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "normals storage texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "normals sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(device, texture_desc, sampler_desc); +} + +void ComputeNormalsNode::serialize_settings(QJsonObject& out) const +{ + out["format"] = wgpu_format_to_string(m_settings.format); + out["usage"] = wgpu_usage_to_json(m_settings.usage); +} + +void ComputeNormalsNode::deserialize_settings(const QJsonObject& in) +{ + auto s = m_settings; + if (in.contains("format")) + s.format = wgpu_format_from_string(in["format"].toString(), s.format); + if (in.contains("usage")) + s.usage = wgpu_usage_from_json(in["usage"].toArray(), s.usage); + set_settings(s); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeNormalsNode.h b/webgpu/compute/nodes/ComputeNormalsNode.h new file mode 100644 index 000000000..fd19705b1 --- /dev/null +++ b/webgpu/compute/nodes/ComputeNormalsNode.h @@ -0,0 +1,73 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +/// GPU compute node, calling run executes code on the GPU +class ComputeNormalsNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(ComputeNormalsNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + struct NormalSettings { + WGPUTextureFormat format = WGPUTextureFormat_RGBA8Unorm; + WGPUTextureUsage usage = (WGPUTextureUsage)(WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst); + }; + + struct NormalsSettingsUniform { + glm::vec2 aabb_min; + glm::vec2 aabb_max; + }; + + explicit ComputeNormalsNode(webgpu::Context& ctx); + + void set_settings(const NormalSettings& settings); + const NormalSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + static std::unique_ptr create_normals_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage); + +private: + webgpu::Context* m_ctx; + + NormalSettings m_settings; + + webgpu::Buffer m_normals_settings_uniform_buffer; + std::unique_ptr m_pipeline; + + // output + std::unique_ptr m_output_texture; // normal texture +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeReleasePointsNode.cpp b/webgpu/compute/nodes/ComputeReleasePointsNode.cpp new file mode 100644 index 000000000..9a1bd4471 --- /dev/null +++ b/webgpu/compute/nodes/ComputeReleasePointsNode.cpp @@ -0,0 +1,198 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeReleasePointsNode.h" +#include "util.h" + +namespace webgpu_compute::nodes { + +glm::uvec3 ComputeReleasePointsNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +ComputeReleasePointsNode::ComputeReleasePointsNode(webgpu::Context& ctx) + : ComputeReleasePointsNode(ctx, ReleasePointsSettings()) +{ +} + +ComputeReleasePointsNode::ComputeReleasePointsNode(webgpu::Context& ctx, const ReleasePointsSettings& settings) + : Node( + { + InputSocket(*this, "normal texture", data_type()), + }, + { + OutputSocket(*this, "release point texture", data_type(), [this]() { return m_output_texture.get(); }), + }) + , m_ctx(&ctx) + , m_settings { settings } + , m_settings_uniform(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("release_point_compute", "webgpu_compute::compute_release_points"); + reg.register_bind_group_layout("release_point_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.texture.sampleType = WGPUTextureSampleType_Float; + e1.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e2.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + e2.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique( + dev, std::vector { e0, e1, e2 }, "release point compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique( + device, reg.shader("release_point_compute"), std::vector { ®.bind_group_layout("release_point_compute") }); + }); +} + +void ComputeReleasePointsNode::run_impl() +{ + + const auto& normal_texture = *std::get()>(input_socket("normal texture").get_connected_data()); + + // create output texture + m_output_texture = create_release_points_texture(m_ctx->device(), + uint32_t(normal_texture.texture().width()), + uint32_t(normal_texture.texture().height()), + m_settings.texture_format, + m_settings.texture_usage); + + // update settings on GPU side + m_settings_uniform.data.min_slope_angle = m_settings.min_slope_angle; + m_settings_uniform.data.max_slope_angle = m_settings.max_slope_angle; + m_settings_uniform.data.sampling_interval = m_settings.sampling_interval; + m_settings_uniform.update_gpu_data(m_ctx->queue()); + + // create bind group + WGPUBindGroupEntry input_settings_buffer_entry = m_settings_uniform.raw_buffer().create_bind_group_entry(0); + WGPUBindGroupEntry input_normal_texture_entry = normal_texture.texture_view().create_bind_group_entry(1); + WGPUBindGroupEntry output_texture_entry = m_output_texture->texture_view().create_bind_group_entry(2); + + std::vector entries { + input_settings_buffer_entry, + input_normal_texture_entry, + output_texture_entry, + }; + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("release_point_compute"), entries, "release points compute bind group"); + + // bind GPU resources and run pipeline + // the result is a texture with the calculated release points + { + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "release points compute command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "release points compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts + = glm::ceil(glm::vec3(m_output_texture->texture().width(), m_output_texture->texture().height(), 1u) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "release points compute command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + ComputeReleasePointsNode* _this = reinterpret_cast(userdata); + _this->complete_run(); + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }; + + WGPUFuture future = wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), callback_info); +} + +std::unique_ptr ComputeReleasePointsNode::create_release_points_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "release points storage texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "release points sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(device, texture_desc, sampler_desc); +} + +void ComputeReleasePointsNode::serialize_settings(QJsonObject& out) const +{ + out["texture_format"] = wgpu_format_to_string(m_settings.texture_format); + out["texture_usage"] = wgpu_usage_to_json(m_settings.texture_usage); + out["min_slope_angle"] = static_cast(m_settings.min_slope_angle); + out["max_slope_angle"] = static_cast(m_settings.max_slope_angle); + out["sampling_interval"] = uvec2_to_json(m_settings.sampling_interval); +} + +void ComputeReleasePointsNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("texture_format")) + m_settings.texture_format = wgpu_format_from_string(in["texture_format"].toString(), m_settings.texture_format); + if (in.contains("texture_usage")) + m_settings.texture_usage = wgpu_usage_from_json(in["texture_usage"].toArray(), m_settings.texture_usage); + if (in.contains("min_slope_angle")) + m_settings.min_slope_angle = static_cast(in["min_slope_angle"].toDouble(m_settings.min_slope_angle)); + if (in.contains("max_slope_angle")) + m_settings.max_slope_angle = static_cast(in["max_slope_angle"].toDouble(m_settings.max_slope_angle)); + if (in.contains("sampling_interval")) + m_settings.sampling_interval = uvec2_from_json(in["sampling_interval"].toArray(), m_settings.sampling_interval); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeReleasePointsNode.h b/webgpu/compute/nodes/ComputeReleasePointsNode.h new file mode 100644 index 000000000..0202ddedc --- /dev/null +++ b/webgpu/compute/nodes/ComputeReleasePointsNode.h @@ -0,0 +1,77 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +// TODO doc +class ComputeReleasePointsNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(ComputeReleasePointsNode) + + struct ReleasePointsSettings { + WGPUTextureFormat texture_format = WGPUTextureFormat_RGBA8Unorm; + WGPUTextureUsage texture_usage + = (WGPUTextureUsage)(WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc); + float min_slope_angle = glm::radians(30.0f); // min slope angle [rad] + float max_slope_angle = glm::radians(45.0f); // max slope angle [rad] + glm::uvec2 sampling_interval = glm::uvec2(8); // sampling interval in x and y direction [every sampling_interval texels] + }; + + struct ReleasePointsSettingsUniform { + float min_slope_angle; + float max_slope_angle; + glm::uvec2 sampling_interval; + }; + +public: + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + ComputeReleasePointsNode(webgpu::Context& ctx); + ComputeReleasePointsNode(webgpu::Context& ctx, const ReleasePointsSettings& settings); + + void set_settings(const ReleasePointsSettings& settings) { m_settings = settings; } + const ReleasePointsSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + static std::unique_ptr create_release_points_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage); + +private: + webgpu::Context* m_ctx; + + ReleasePointsSettings m_settings; + webgpu::Buffer m_settings_uniform; + std::unique_ptr m_output_texture; + std::unique_ptr m_pipeline; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeSnowNode.cpp b/webgpu/compute/nodes/ComputeSnowNode.cpp new file mode 100644 index 000000000..13767f7b0 --- /dev/null +++ b/webgpu/compute/nodes/ComputeSnowNode.cpp @@ -0,0 +1,233 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ComputeSnowNode.h" +#include "util.h" + +#include +#include + +namespace webgpu_compute::nodes { + +glm::uvec3 ComputeSnowNode::SHADER_WORKGROUP_SIZE = { 1, 16, 16 }; + +ComputeSnowNode::ComputeSnowNode(webgpu::Context& ctx) + : ComputeSnowNode(ctx, SnowSettings()) +{ +} + +ComputeSnowNode::ComputeSnowNode(webgpu::Context& ctx, const SnowSettings& settings) + : Node( + { + InputSocket(*this, "bounds", data_type*>()), + InputSocket(*this, "normal texture", data_type()), + InputSocket(*this, "height texture", data_type()), + }, + { + OutputSocket(*this, "snow texture", data_type(), [this]() { return m_output_snow_texture.get(); }), + }) + , m_ctx(&ctx) + , m_settings { settings } + , m_snow_settings_uniform_buffer(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) + , m_region_bounds_uniform_buffer(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("snow_compute", "webgpu_compute::snow_compute"); + reg.register_bind_group_layout("snow_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.texture.sampleType = WGPUTextureSampleType_Float; + e2.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e3 {}; + e3.binding = 3; + e3.visibility = WGPUShaderStage_Compute; + e3.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + e3.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e4 {}; + e4.binding = 4; + e4.visibility = WGPUShaderStage_Compute; + e4.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e4.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + e4.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique( + dev, std::vector { e0, e1, e2, e3, e4 }, "snow compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique( + device, reg.shader("snow_compute"), std::vector { ®.bind_group_layout("snow_compute") }); + }); +} + +void ComputeSnowNode::run_impl() +{ + + // read input data from input sockets + const auto& bounds = *std::get*>()>(input_socket("bounds").get_connected_data()); + const auto& heights_texture = *std::get()>(input_socket("height texture").get_connected_data()); + const auto& normals_texture = *std::get()>(input_socket("normal texture").get_connected_data()); + + // create output texture + m_output_snow_texture = create_snow_texture( + m_ctx->device(), uint32_t(heights_texture.texture().width()), uint32_t(heights_texture.texture().height()), m_settings.format, m_settings.usage); + + // update uniform buffer + m_snow_settings_uniform_buffer.data.angle.x = 1.0f; // always enabled, does not matter for compute + m_snow_settings_uniform_buffer.data.angle.y = m_settings.min_angle; + m_snow_settings_uniform_buffer.data.angle.z = m_settings.max_angle; + m_snow_settings_uniform_buffer.data.angle.w = m_settings.angle_blend; + m_snow_settings_uniform_buffer.data.alt.x = m_settings.min_altitude; + m_snow_settings_uniform_buffer.data.alt.y = m_settings.altitude_variation; + m_snow_settings_uniform_buffer.data.alt.z = m_settings.altitude_blend; + m_snow_settings_uniform_buffer.data.alt.w = 0.0f; // specular, does not matter for compute + m_snow_settings_uniform_buffer.update_gpu_data(m_ctx->queue()); + + m_region_bounds_uniform_buffer.data.aabb_min = glm::fvec2(bounds.min); + m_region_bounds_uniform_buffer.data.aabb_max = glm::fvec2(bounds.max); + m_region_bounds_uniform_buffer.update_gpu_data(m_ctx->queue()); + + // create bind group + // TODO re-create bind groups only when input handles change + // TODO adapter shader code + // TODO compute bounds in other node! + std::vector entries { + m_snow_settings_uniform_buffer.raw_buffer().create_bind_group_entry(0), + m_region_bounds_uniform_buffer.raw_buffer().create_bind_group_entry(1), + normals_texture.texture_view().create_bind_group_entry(2), + heights_texture.texture_view().create_bind_group_entry(3), + m_output_snow_texture->texture_view().create_bind_group_entry(4), + }; + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("snow_compute"), entries, "snow compute bind group"); + + // bind GPU resources and run pipeline + { + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "snow compute command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "snow compute compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts = glm::ceil( + glm::vec3(m_output_snow_texture->texture().width(), m_output_snow_texture->texture().height(), 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "SnowComputeNode command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + ComputeSnowNode* _this = reinterpret_cast(userdata); + _this->complete_run(); + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }; + + wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), callback_info); +} + +std::unique_ptr ComputeSnowNode::create_snow_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "snow storage texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "snow sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(device, texture_desc, sampler_desc); +} + +void ComputeSnowNode::serialize_settings(QJsonObject& out) const +{ + out["format"] = wgpu_format_to_string(m_settings.format); + out["usage"] = wgpu_usage_to_json(m_settings.usage); + out["min_angle"] = static_cast(m_settings.min_angle); + out["max_angle"] = static_cast(m_settings.max_angle); + out["angle_blend"] = static_cast(m_settings.angle_blend); + out["min_altitude"] = static_cast(m_settings.min_altitude); + out["altitude_variation"] = static_cast(m_settings.altitude_variation); + out["altitude_blend"] = static_cast(m_settings.altitude_blend); +} + +void ComputeSnowNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("format")) + m_settings.format = wgpu_format_from_string(in["format"].toString(), m_settings.format); + if (in.contains("usage")) + m_settings.usage = wgpu_usage_from_json(in["usage"].toArray(), m_settings.usage); + if (in.contains("min_angle")) + m_settings.min_angle = static_cast(in["min_angle"].toDouble(m_settings.min_angle)); + if (in.contains("max_angle")) + m_settings.max_angle = static_cast(in["max_angle"].toDouble(m_settings.max_angle)); + if (in.contains("angle_blend")) + m_settings.angle_blend = static_cast(in["angle_blend"].toDouble(m_settings.angle_blend)); + if (in.contains("min_altitude")) + m_settings.min_altitude = static_cast(in["min_altitude"].toDouble(m_settings.min_altitude)); + if (in.contains("altitude_variation")) + m_settings.altitude_variation = static_cast(in["altitude_variation"].toDouble(m_settings.altitude_variation)); + if (in.contains("altitude_blend")) + m_settings.altitude_blend = static_cast(in["altitude_blend"].toDouble(m_settings.altitude_blend)); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ComputeSnowNode.h b/webgpu/compute/nodes/ComputeSnowNode.h new file mode 100644 index 000000000..91d378daa --- /dev/null +++ b/webgpu/compute/nodes/ComputeSnowNode.h @@ -0,0 +1,101 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +/// GPU compute node, calling run executes code on the GPU +class ComputeSnowNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(ComputeSnowNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + struct SnowSettings { + WGPUTextureFormat format = WGPUTextureFormat_RGBA8Unorm; + WGPUTextureUsage usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc; + + float min_angle = 0; // slope angle in degrees + float max_angle = 45; // slope angle in degrees + float angle_blend = 0; // TODO doc + + float min_altitude = 1000; // minimal altitude in meters + float altitude_variation = 200; // TODO doc + float altitude_blend = 200; // TODO doc + }; + + struct SnowSettingsUniform { + glm::vec4 angle = { + 1, // snow enabled + 0, // angle lower limit// angle lower limit + 30, // angle upper limit + 0, // angle blend + }; + + glm::vec4 alt = { + 1000, // min altitude + 200, // variation + 200, // blend + 1, // specular + }; + }; + + struct RegionBoundsUniform { + glm::vec2 aabb_min; + glm::vec2 aabb_max; + }; + + ComputeSnowNode(webgpu::Context& ctx); + ComputeSnowNode(webgpu::Context& ctx, const SnowSettings& settings); + + void set_settings(const SnowSettings& settings) { m_settings = settings; } + const SnowSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + static std::unique_ptr create_snow_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage); + +private: + webgpu::Context* m_ctx; + + SnowSettings m_settings; + webgpu::Buffer m_snow_settings_uniform_buffer; + webgpu::Buffer m_region_bounds_uniform_buffer; + std::unique_ptr m_pipeline; + + // input + std::unique_ptr m_input_normals_texture; // normal texture + + // output + std::unique_ptr m_output_snow_texture; // snow texture +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ExportNode.cpp b/webgpu/compute/nodes/ExportNode.cpp new file mode 100644 index 000000000..d179c6e52 --- /dev/null +++ b/webgpu/compute/nodes/ExportNode.cpp @@ -0,0 +1,196 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ExportNode.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +static std::string resolve_placeholders(const std::string& pattern, const std::string& node_name, uint64_t run_id, const std::string& run_datetime) +{ + std::string result = pattern; + auto replace = [](std::string& s, const std::string& from, const std::string& to) { + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.length(), to); + pos += to.length(); + } + }; + replace(result, "{node_name}", node_name); + replace(result, "{run_id}", std::to_string(run_id)); + replace(result, "{run_datetime}", run_datetime); + return result; +} + +static void ensure_parent_dir(const std::string& file_path) { std::filesystem::create_directories(std::filesystem::path(file_path).parent_path()); } + +static void write_texture_file(const QByteArray& data, glm::uvec2 dims, const std::string& file_path) +{ + const uint32_t bpp = static_cast(data.size()) / (dims.x * dims.y); + nucleus::Raster raster(dims); + auto& buf = raster.buffer(); + for (uint32_t y = 0; y < dims.y; y++) { + for (uint32_t x = 0; x < dims.x; x++) { + const uint32_t idx = (y * dims.x + x) * bpp; + buf[y * dims.x + x] = glm::u8vec4(static_cast(data.at(idx + 0)), + bpp > 1 ? static_cast(data.at(idx + 1)) : 0u, + bpp > 2 ? static_cast(data.at(idx + 2)) : 0u, + bpp > 3 ? static_cast(data.at(idx + 3)) : 255u); + } + } + ensure_parent_dir(file_path); + nucleus::utils::image_writer::rgba8_as_png(raster, QString::fromStdString(file_path)); + qDebug() << "[ExportNode] texture written to" << QString::fromStdString(file_path); +} + +static void write_buffer_file(const std::vector& data, glm::uvec2 dims, const std::string& file_path) +{ + nucleus::Raster raster(dims); + for (size_t i = 0; i < data.size(); i++) + raster.buffer()[i] = static_cast(data[i]); + ensure_parent_dir(file_path); + nucleus::utils::geopng::write_encoded_float_png(raster, QString::fromStdString(file_path)); + qDebug() << "[ExportNode] buffer written to" << QString::fromStdString(file_path); +} + +static void write_aabb_file(const std::string& file_path, const radix::geometry::Aabb<2, double>& bounds) +{ + ensure_parent_dir(file_path); + QFile file(QString::fromStdString(file_path)); + if (file.open(QIODevice::WriteOnly)) { + QTextStream stream(&file); + stream.setRealNumberPrecision(30); + stream << bounds.min.x << "\n" << bounds.min.y << "\n" << bounds.max.x << "\n" << bounds.max.y << "\n"; + file.close(); + qDebug() << "[ExportNode] aabb written to" << QString::fromStdString(file_path); + } +} + +ExportNode::ExportNode(webgpu::Context& ctx) + : ExportNode(ctx, ExportSettings {}) +{ +} + +ExportNode::ExportNode(webgpu::Context& ctx, const ExportSettings& settings) + : Node( + { + InputSocket(*this, "texture", data_type()), + InputSocket(*this, "buffer", data_type*>()), + InputSocket(*this, "dimensions", data_type()), + InputSocket(*this, "region aabb", data_type*>()), + }, + {}) + , m_ctx(&ctx) + , m_settings(settings) +{ +} + +void ExportNode::set_settings(const ExportSettings& settings) { m_settings = settings; } + +void ExportNode::run_impl() +{ + const std::string node_name = get_node_name(); + const uint64_t run_id = get_run_id(); + const std::string run_datetime = get_run_datetime(); + + const bool has_texture = input_socket("texture").is_socket_connected(); + const bool has_buffer = input_socket("buffer").is_socket_connected(); + const bool has_aabb = input_socket("region aabb").is_socket_connected(); + + // Shared counter: both GPU readbacks are queued simultaneously; run_completed + // fires only when all pending async ops have finished. + auto pending = std::make_shared(0); + auto on_done = [this, pending]() { + if (--(*pending) == 0) + complete_run(); + }; + + if (has_texture) { + const auto& texture = *std::get()>(input_socket("texture").get_connected_data()); + const glm::uvec2 dims { texture.texture().width(), texture.texture().height() }; + const std::string path = resolve_placeholders(m_settings.texture_output_file, node_name, run_id, run_datetime); + (*pending)++; + texture.texture().read_back_async(m_ctx->device(), 0, [path, dims, on_done]([[maybe_unused]] size_t, std::shared_ptr data) { + write_texture_file(*data, dims, path); + on_done(); + }); + } + + if (has_buffer) { + if (!input_socket("dimensions").is_socket_connected()) { + qWarning() << "[ExportNode] Buffer needs dimensions"; + } else { + auto& buffer = *std::get*>()>(input_socket("buffer").get_connected_data()); + const auto dims = std::get()>(input_socket("dimensions").get_connected_data()); + const size_t expected = dims.x * dims.y; + + if (buffer.size() != expected) { + qWarning() << "[ExportNode] buffer size mismatch. Expected:" << expected << "Got:" << buffer.size(); + } else { + const std::string path = resolve_placeholders(m_settings.buffer_output_file, node_name, run_id, run_datetime); + (*pending)++; + buffer.read_back_async(m_ctx->device(), [path, dims, on_done](WGPUMapAsyncStatus status, std::vector data) { + if (status == WGPUMapAsyncStatus_Success) + write_buffer_file(data, dims, path); + else + qWarning() << "[ExportNode] buffer readback failed:" << status; + on_done(); + }); + } + } + } + + // AABB is synchronous -> write immediately + if (has_aabb) { + const auto& aabb = *std::get*>()>(input_socket("region aabb").get_connected_data()); + write_aabb_file(resolve_placeholders(m_settings.aabb_output_file, node_name, run_id, run_datetime), aabb); + } + + if (*pending == 0) + complete_run(); +} + +void ExportNode::serialize_settings(QJsonObject& out) const +{ + out["buffer_output_file"] = QString::fromStdString(m_settings.buffer_output_file); + out["texture_output_file"] = QString::fromStdString(m_settings.texture_output_file); + out["aabb_output_file"] = QString::fromStdString(m_settings.aabb_output_file); +} + +void ExportNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("buffer_output_file")) + m_settings.buffer_output_file = in["buffer_output_file"].toString().toStdString(); + if (in.contains("texture_output_file")) + m_settings.texture_output_file = in["texture_output_file"].toString().toStdString(); + if (in.contains("aabb_output_file")) + m_settings.aabb_output_file = in["aabb_output_file"].toString().toStdString(); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/ExportNode.h b/webgpu/compute/nodes/ExportNode.h new file mode 100644 index 000000000..8257cab04 --- /dev/null +++ b/webgpu/compute/nodes/ExportNode.h @@ -0,0 +1,60 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include + +namespace webgpu_compute::nodes { + +// General-purpose export node. Connect any combination of: +// - "texture" -> exports a GPU texture to an image file +// - "buffer" + "dimensions" -> exports a uint32 GPU buffer to an image file +// - "region aabb" -> writes a bounding-box text file +class ExportNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(ExportNode) + + struct ExportSettings { + // Supported placeholders: {node_name}, {run_datetime}, {run_id} + std::string buffer_output_file = "export/{run_datetime}_{run_id}/exp_{node_name}_buff.png"; + std::string texture_output_file = "export/{run_datetime}_{run_id}/exp_{node_name}_tex.png"; + std::string aabb_output_file = "export/{run_datetime}_{run_id}/exp_aabb.txt"; + }; + + explicit ExportNode(webgpu::Context& ctx); + ExportNode(webgpu::Context& ctx, const ExportSettings& settings); + + const ExportSettings& get_settings() const { return m_settings; } + void set_settings(const ExportSettings& settings); + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + webgpu::Context* m_ctx; + ExportSettings m_settings; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/GPXTrackNode.cpp b/webgpu/compute/nodes/GPXTrackNode.cpp new file mode 100644 index 000000000..ae629c1b6 --- /dev/null +++ b/webgpu/compute/nodes/GPXTrackNode.cpp @@ -0,0 +1,72 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "GPXTrackNode.h" + +#include "nucleus/track/GPX.h" +#include +#include + +namespace webgpu_compute::nodes { + +GPXTrackNode::GPXTrackNode() + : Node({}, + { + OutputSocket(*this, "region", data_type*>(), [this]() { return &m_output_region; }), + }) +{ +} + +void GPXTrackNode::run_impl() +{ + if (m_settings.enable_caching && m_has_cached && m_settings.file_path == m_cached_path) { + complete_run(); + return; + } + + std::unique_ptr gpx = nucleus::track::parse(QString::fromStdString(m_settings.file_path)); + if (!gpx) { + fail_run("could not parse GPX file: " + m_settings.file_path); + return; + } + + m_output_region = nucleus::track::compute_world_aabb(*gpx); + m_cached_path = m_settings.file_path; + m_has_cached = true; + + qDebug() << Qt::fixed << "gpx region=[(" << m_output_region.min.x << ", " << m_output_region.min.y << "), (" << m_output_region.max.x << ", " + << m_output_region.max.y << ")]"; + + complete_run(); +} + +void GPXTrackNode::serialize_settings(QJsonObject& out) const +{ + out["file_path"] = QString::fromStdString(m_settings.file_path); + out["enable_caching"] = m_settings.enable_caching; +} + +void GPXTrackNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("file_path")) + m_settings.file_path = in["file_path"].toString().toStdString(); + if (in.contains("enable_caching")) + m_settings.enable_caching = in["enable_caching"].toBool(m_settings.enable_caching); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/GPXTrackNode.h b/webgpu/compute/nodes/GPXTrackNode.h new file mode 100644 index 000000000..5eeb960dd --- /dev/null +++ b/webgpu/compute/nodes/GPXTrackNode.h @@ -0,0 +1,54 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" + +namespace webgpu_compute::nodes { + +class GPXTrackNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(GPXTrackNode) + + struct GPXTrackNodeSettings { + std::string file_path = ":/gpx/breite_ries.gpx"; + bool enable_caching = true; + }; + + GPXTrackNode(); + + void set_settings(const GPXTrackNodeSettings& settings) { m_settings = settings; } + const GPXTrackNodeSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + GPXTrackNodeSettings m_settings; + radix::geometry::Aabb<3, double> m_output_region; + + std::string m_cached_path; + bool m_has_cached = false; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/HeightDecodeNode.cpp b/webgpu/compute/nodes/HeightDecodeNode.cpp new file mode 100644 index 000000000..0d627f619 --- /dev/null +++ b/webgpu/compute/nodes/HeightDecodeNode.cpp @@ -0,0 +1,159 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "HeightDecodeNode.h" +#include "util.h" + +#include + +namespace webgpu_compute::nodes { + +glm::uvec3 HeightDecodeNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +HeightDecodeNode::HeightDecodeNode(webgpu::Context& ctx) + : HeightDecodeNode(ctx, HeightDecodeSettings {}) +{ +} + +webgpu_compute::nodes::HeightDecodeNode::HeightDecodeNode(webgpu::Context& ctx, HeightDecodeSettings settings) + : Node( + { + InputSocket(*this, "encoded texture", data_type()), + InputSocket(*this, "region aabb", data_type*>()), + }, + { + OutputSocket(*this, "decoded texture", data_type(), [this]() { return m_output_texture.get(); }), + }) + , m_ctx(&ctx) + , m_settings(settings) + , m_settings_uniform(m_ctx->device(), WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst) +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("height_decode_compute", "webgpu_compute::height_decode_compute"); + reg.register_bind_group_layout("height_decode_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.storageTexture.access = WGPUStorageTextureAccess_ReadOnly; + e1.storageTexture.format = WGPUTextureFormat_RGBA8Uint; + e1.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e2.storageTexture.format = WGPUTextureFormat_R32Float; + e2.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique( + dev, std::vector { e0, e1, e2 }, "height decode compute bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique( + device, reg.shader("height_decode_compute"), std::vector { ®.bind_group_layout("height_decode_compute") }); + }); +} + +void HeightDecodeNode::run_impl() +{ + + const auto region_aabb = std::get*>()>(input_socket("region aabb").get_connected_data()); + const auto& input_texture = *std::get()>(input_socket("encoded texture").get_connected_data()); + + glm::uvec2 size = glm::uvec2(input_texture.texture().width(), input_texture.texture().height()); + + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "decoded height texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { size.x, size.y, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = WGPUTextureFormat_R32Float; + texture_desc.usage = m_settings.texture_usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "decoded height sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + m_output_texture = std::make_unique(m_ctx->device(), texture_desc, sampler_desc); + + // update bounding box + m_settings_uniform.data.aabb_min = glm::uvec2(region_aabb->min); + m_settings_uniform.data.aabb_max = glm::uvec2(region_aabb->max); + m_settings_uniform.update_gpu_data(m_ctx->queue()); + + // create bind group + // TODO re-create bind groups only when input handles change + std::vector entries { + m_settings_uniform.raw_buffer().create_bind_group_entry(0), + input_texture.texture_view().create_bind_group_entry(1), + m_output_texture->texture_view().create_bind_group_entry(2), + }; + webgpu::raii::BindGroup compute_bind_group( + m_ctx->device(), m_ctx->resource_registry().bind_group_layout("height_decode_compute"), entries, "compute controller bind group"); + + { + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "compute controller command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "compute controller compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + + glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(size.x, size.y, 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "HeightDecode command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + // NOTE: Maybe this needs to be inside onsubmittedworkdone callback? But technically + // I don't think we should wait for the queue... + complete_run(); +} + +void HeightDecodeNode::serialize_settings(QJsonObject& out) const { out["texture_usage"] = wgpu_usage_to_json(m_settings.texture_usage); } + +void HeightDecodeNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("texture_usage")) + m_settings.texture_usage = wgpu_usage_from_json(in["texture_usage"].toArray(), m_settings.texture_usage); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/HeightDecodeNode.h b/webgpu/compute/nodes/HeightDecodeNode.h new file mode 100644 index 000000000..14f33e1c7 --- /dev/null +++ b/webgpu/compute/nodes/HeightDecodeNode.h @@ -0,0 +1,67 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +class HeightDecodeNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(HeightDecodeNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + struct HeightDecodeSettings { + // The usage flags of the output texture + WGPUTextureUsage texture_usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + }; + + struct HeightDecodeSettingsUniform { + glm::vec2 aabb_min; + glm::vec2 aabb_max; + }; + + explicit HeightDecodeNode(webgpu::Context& ctx); // default-configured; for the NodeRegistry + HeightDecodeNode(webgpu::Context& ctx, HeightDecodeSettings settings); + + // settings are consumed lazily in run_impl, applying them at any time is safe + void set_settings(const HeightDecodeSettings& settings) { m_settings = settings; } + const HeightDecodeSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + webgpu::Context* m_ctx; + + HeightDecodeSettings m_settings; + webgpu::Buffer m_settings_uniform; + std::unique_ptr m_output_texture; + std::unique_ptr m_pipeline; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/IterativeSimulationNode.cpp b/webgpu/compute/nodes/IterativeSimulationNode.cpp new file mode 100644 index 000000000..e6a047b3b --- /dev/null +++ b/webgpu/compute/nodes/IterativeSimulationNode.cpp @@ -0,0 +1,221 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "IterativeSimulationNode.h" + +namespace webgpu_compute::nodes { + +glm::uvec3 IterativeSimulationNode::SHADER_WORKGROUP_SIZE = { 16, 16, 1 }; + +IterativeSimulationNode::IterativeSimulationNode(webgpu::Context& ctx) + : IterativeSimulationNode(ctx, IterativeSimulationSettings()) +{ +} + +IterativeSimulationNode::IterativeSimulationNode(webgpu::Context& ctx, const IterativeSimulationSettings& settings) + : Node( + { + InputSocket(*this, "height texture", data_type()), + InputSocket(*this, "release point texture", data_type()), + }, + { + OutputSocket(*this, "texture", data_type(), [this]() { return m_output_texture.get(); }), + }) + , m_ctx(&ctx) + , m_settings { settings } +{ + auto& reg = ctx.resource_registry(); + reg.register_shader("iterative_simulation_compute", "webgpu_compute::iterative_simulation_compute"); + reg.register_bind_group_layout("iterative_simulation_compute", [](WGPUDevice dev) { + WGPUBindGroupLayoutEntry e0 {}; + e0.binding = 0; + e0.visibility = WGPUShaderStage_Compute; + e0.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry e1 {}; + e1.binding = 1; + e1.visibility = WGPUShaderStage_Compute; + e1.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + e1.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e2 {}; + e2.binding = 2; + e2.visibility = WGPUShaderStage_Compute; + e2.texture.sampleType = WGPUTextureSampleType_Float; + e2.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry e3 {}; + e3.binding = 3; + e3.visibility = WGPUShaderStage_Compute; + e3.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + + WGPUBindGroupLayoutEntry e4 {}; + e4.binding = 4; + e4.visibility = WGPUShaderStage_Compute; + e4.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e5 {}; + e5.binding = 5; + e5.visibility = WGPUShaderStage_Compute; + e5.buffer.type = WGPUBufferBindingType_Storage; + + WGPUBindGroupLayoutEntry e6 {}; + e6.binding = 6; + e6.visibility = WGPUShaderStage_Compute; + e6.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + e6.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + e6.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique( + dev, std::vector { e0, e1, e2, e3, e4, e5, e6 }, "iterative simulation bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("iterative_simulation_compute"), + std::vector { ®.bind_group_layout("iterative_simulation_compute") }); + }); +} + +void IterativeSimulationNode::run_impl() +{ + + const auto& input_height_texture = *std::get()>(input_socket("height texture").get_connected_data()); + const auto& input_release_point_texture + = *std::get()>(input_socket("release point texture").get_connected_data()); + + m_output_texture = create_texture(m_ctx->device(), + uint32_t(input_height_texture.texture().width()), + uint32_t(input_height_texture.texture().height()), + WGPUTextureFormat_RGBA8Unorm, + WGPUTextureUsage(WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding)); + + m_settings_uniform + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + + size_t buffer_size = input_height_texture.texture().width() * input_height_texture.texture().height(); + m_input_parent_buffer = std::make_unique>( + m_ctx->device(), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, buffer_size, "flow py parents buffer 1"); + m_flux_buffer = std::make_unique>( + m_ctx->device(), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, buffer_size, "flow py flow buffer 1"); + m_output_parent_buffer = std::make_unique>( + m_ctx->device(), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, buffer_size, "flow py parents buffer 2"); + + // create bind group + webgpu::raii::BindGroup compute_bind_group(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("iterative_simulation_compute"), + std::vector { + m_settings_uniform->raw_buffer().create_bind_group_entry(0), + input_height_texture.texture_view().create_bind_group_entry(1), + input_release_point_texture.texture_view().create_bind_group_entry(2), + m_input_parent_buffer->create_bind_group_entry(3), + m_flux_buffer->create_bind_group_entry(4), + m_output_parent_buffer->create_bind_group_entry(5), + m_output_texture->texture_view().create_bind_group_entry(6), + }, + "flowpy compute bind group"); + + m_settings_uniform->data.num_iteration = 0; + m_settings_uniform->update_gpu_data(m_ctx->queue()); + + m_flux_buffer->clear(m_ctx->device(), m_ctx->queue()); + + glm::uvec3 workgroup_counts + = glm::ceil(glm::vec3(input_height_texture.texture().width(), input_height_texture.texture().height(), 1) / glm::vec3(SHADER_WORKGROUP_SIZE)); + + for (uint32_t i = 0; i < m_settings.max_num_iterations; i++) { + qDebug() << "iteration" << i; + m_settings_uniform->data.num_iteration = i; + m_settings_uniform->update_gpu_data(m_ctx->queue()); + + // m_input_parent_buffer->clear(encoder.handle()); + // m_output_parent_buffer->clear(encoder.handle()); + + // bind GPU resources and run pipeline + WGPUCommandEncoderDescriptor descriptor {}; + descriptor.label = WGPUStringView { .data = "flowpy compute command encoder", .length = WGPU_STRLEN }; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), descriptor); + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "flowpy compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(encoder.handle(), compute_pass_desc); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, compute_bind_group.handle(), 0, nullptr); + m_pipeline->run(compute_pass, workgroup_counts); + } + + WGPUCommandBufferDescriptor cmd_buffer_descriptor {}; + cmd_buffer_descriptor.label = WGPUStringView { .data = "flowpy compute command buffer", .length = WGPU_STRLEN }; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_buffer_descriptor); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + } + + const auto on_work_done + = []([[maybe_unused]] WGPUQueueWorkDoneStatus status, [[maybe_unused]] WGPUStringView message, void* userdata, [[maybe_unused]] void* userdata2) { + IterativeSimulationNode* _this = reinterpret_cast(userdata); + _this->complete_run(); + }; + + WGPUQueueWorkDoneCallbackInfo callback_info { + .nextInChain = nullptr, + .mode = WGPUCallbackMode_AllowProcessEvents, + .callback = on_work_done, + .userdata1 = this, + .userdata2 = nullptr, + }; + + wgpuQueueOnSubmittedWorkDone(m_ctx->queue(), callback_info); +} + +std::unique_ptr IterativeSimulationNode::create_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "iterative avalanche node texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "iterative avalanche node sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(device, texture_desc, sampler_desc); +} + +void IterativeSimulationNode::serialize_settings(QJsonObject& out) const { out["max_num_iterations"] = static_cast(m_settings.max_num_iterations); } + +void IterativeSimulationNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("max_num_iterations")) + m_settings.max_num_iterations = static_cast(in["max_num_iterations"].toInt(static_cast(m_settings.max_num_iterations))); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/IterativeSimulationNode.h b/webgpu/compute/nodes/IterativeSimulationNode.h new file mode 100644 index 000000000..008d138b3 --- /dev/null +++ b/webgpu/compute/nodes/IterativeSimulationNode.h @@ -0,0 +1,76 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include +#include +#include + +namespace webgpu_compute::nodes { + +class IterativeSimulationNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(IterativeSimulationNode) + + static glm::uvec3 SHADER_WORKGROUP_SIZE; // TODO currently hardcoded in shader! can we somehow not hardcode it? maybe using overrides + + struct IterativeSimulationSettings { + uint32_t max_num_iterations = 16; + }; + + struct IterativeSimulationSettingsUniform { + uint32_t num_iteration; + uint32_t padding1; + uint32_t padding2; + uint32_t padding3; + }; + + IterativeSimulationNode(webgpu::Context& ctx); + IterativeSimulationNode(webgpu::Context& ctx, const IterativeSimulationSettings& settings); + + // settings are consumed lazily in run_impl, applying them at any time is safe + void set_settings(const IterativeSimulationSettings& settings) { m_settings = settings; } + const IterativeSimulationSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + static std::unique_ptr create_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage); + +private: + webgpu::Context* m_ctx; + + IterativeSimulationSettings m_settings; + std::unique_ptr m_pipeline; + + std::unique_ptr> m_settings_uniform; + std::unique_ptr> m_flux_buffer; + std::unique_ptr> m_input_parent_buffer; + std::unique_ptr> m_output_parent_buffer; + std::unique_ptr m_output_texture; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/LoadTextureNode.cpp b/webgpu/compute/nodes/LoadTextureNode.cpp new file mode 100644 index 000000000..a0e6ec2bf --- /dev/null +++ b/webgpu/compute/nodes/LoadTextureNode.cpp @@ -0,0 +1,111 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "LoadTextureNode.h" +#include "util.h" + +#include "nucleus/utils/image_loader.h" + +namespace webgpu_compute::nodes { + +LoadTextureNode::LoadTextureNode(webgpu::Context& ctx) + : LoadTextureNode(ctx, LoadTextureNodeSettings()) +{ +} + +LoadTextureNode::LoadTextureNode(webgpu::Context& ctx, const LoadTextureNodeSettings& settings) + : Node({}, + { + OutputSocket(*this, "texture", data_type(), [this]() { return m_output_texture.get(); }), + }) + , m_ctx(&ctx) + , m_settings(settings) +{ +} + +void LoadTextureNode::set_settings(const LoadTextureNodeSettings& settings) { m_settings = settings; } + +void LoadTextureNode::run_impl() +{ + + qDebug() << "loading texture from " << m_settings.file_path; + + auto path = QString::fromStdString(m_settings.file_path); + tl::expected, QString> expected_image = nucleus::utils::image_loader::rgba8(path); + if (!expected_image.has_value()) { + fail_run("Failed to load image file at " + m_settings.file_path + ": " + expected_image.error().toStdString()); + return; + } + + nucleus::Raster image = expected_image.value(); + m_output_texture = create_texture(m_ctx->device(), image.width(), image.height(), m_settings.format, m_settings.usage); + m_output_texture->texture().write(m_ctx->queue(), image); + + // TODO not sure if we need to wait for the queue here? + complete_run(); +} + +std::unique_ptr LoadTextureNode::create_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage) +{ + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "LoadTextureNode output texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = format; + texture_desc.usage = usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "LoadTextureNode sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(device, texture_desc, sampler_desc); +} + +void LoadTextureNode::serialize_settings(QJsonObject& out) const +{ + out["file_path"] = QString::fromStdString(m_settings.file_path); + out["format"] = wgpu_format_to_string(m_settings.format); + out["usage"] = wgpu_usage_to_json(m_settings.usage); +} + +void LoadTextureNode::deserialize_settings(const QJsonObject& in) +{ + auto s = m_settings; + if (in.contains("file_path")) + s.file_path = in["file_path"].toString().toStdString(); + if (in.contains("format")) + s.format = wgpu_format_from_string(in["format"].toString(), s.format); + if (in.contains("usage")) + s.usage = wgpu_usage_from_json(in["usage"].toArray(), s.usage); + set_settings(s); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/LoadTextureNode.h b/webgpu/compute/nodes/LoadTextureNode.h new file mode 100644 index 000000000..08db53467 --- /dev/null +++ b/webgpu/compute/nodes/LoadTextureNode.h @@ -0,0 +1,63 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include + +namespace webgpu_compute::nodes { + +class LoadTextureNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(LoadTextureNode) + + struct LoadTextureNodeSettings { + // path to texture to load + std::string file_path; + + // WebGPU texture parameters + WGPUTextureFormat format = WGPUTextureFormat_RGBA8Uint; + WGPUTextureUsage usage + = (WGPUTextureUsage)(WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc); + }; + + LoadTextureNode(webgpu::Context& ctx); + LoadTextureNode(webgpu::Context& ctx, const LoadTextureNodeSettings& settings); + + void set_settings(const LoadTextureNodeSettings& settings); + const LoadTextureNodeSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + static std::unique_ptr create_texture( + WGPUDevice device, uint32_t width, uint32_t height, WGPUTextureFormat format, WGPUTextureUsage usage); + +private: + webgpu::Context* m_ctx; + LoadTextureNodeSettings m_settings; + std::unique_ptr m_output_texture; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/Node.cpp b/webgpu/compute/nodes/Node.cpp new file mode 100644 index 000000000..e28cc3b9e --- /dev/null +++ b/webgpu/compute/nodes/Node.cpp @@ -0,0 +1,238 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Node.h" + +#include + +namespace webgpu_compute::nodes { + +Socket::Socket(Node& node, const std::string& name, DataType type, FlowDirection direction) + : m_node(&node) + , m_name(name) + , m_type(type) + , m_direction(direction) +{ +} + +const std::string& Socket::name() const { return m_name; } +DataType Socket::type() const { return m_type; } +const Node& Socket::node() const { return *m_node; } +Node& Socket::node() { return *m_node; } + +InputSocket::InputSocket(Node& node, const std::string& name, DataType type) + : Socket(node, name, type, Socket::FlowDirection::INPUT) +{ +} + +void InputSocket::connect(OutputSocket& output_socket) +{ + assert(type() == output_socket.type()); + if (is_socket_connected()) { + m_connected_socket->remove_connected_socket(*this); + } + m_connected_socket = &output_socket; + output_socket.m_connected_sockets.push_back(this); +} +void InputSocket::disconnect() +{ + if (m_connected_socket) { + m_connected_socket->remove_connected_socket(*this); + m_connected_socket = nullptr; + } +} + +bool InputSocket::is_socket_connected() const { return m_connected_socket != nullptr; } +OutputSocket& InputSocket::connected_socket() { return *m_connected_socket; } +const OutputSocket& InputSocket::connected_socket() const { return *m_connected_socket; } + +Data InputSocket::get_connected_data() +{ + assert(m_connected_socket != nullptr); + return m_connected_socket->get_data(); +} + +OutputSocket::OutputSocket(Node& node, const std::string& name, DataType type, OutputFunc output_func) + : Socket(node, name, type, Socket::FlowDirection::OUTPUT) + , m_output_func(output_func) +{ +} + +void OutputSocket::connect(InputSocket& input_socket) +{ + assert(type() == input_socket.type()); + m_connected_sockets.push_back(&input_socket); + input_socket.m_connected_socket = this; +} +bool OutputSocket::is_socket_connected() const { return !m_connected_sockets.empty(); } +std::vector& OutputSocket::connected_sockets() { return m_connected_sockets; } +const std::vector& OutputSocket::connected_sockets() const { return m_connected_sockets; } + +Data OutputSocket::get_data() +{ + Data output = m_output_func(); + assert(output.index() == type()); + return output; +} + +void OutputSocket::remove_connected_socket(InputSocket& input_socket) +{ + auto it = std::find(m_connected_sockets.begin(), m_connected_sockets.end(), &input_socket); + assert(it != m_connected_sockets.end()); + m_connected_sockets.erase(it); +} + +NodeRunFailureInfo::NodeRunFailureInfo(const Node& node, const std::string& message) + : m_node(&node) + , m_message(message) +{ +} + +const std::string& NodeRunFailureInfo::message() const { return m_message; } +const Node& NodeRunFailureInfo::node() const { return *m_node; } + +Node::Node(const std::vector& input_sockets, const std::vector& output_sockets) + : m_input_sockets(input_sockets) + , m_output_sockets(output_sockets) +{ +} + +void Node::rerun() { run(m_run_context); } + +void Node::run(webgpu_compute::GraphRunContext context) +{ + if (m_is_running) { + m_pending_contexts.push(context); + return; + } + m_run_context = context; + if (m_enabled) { + m_is_running = true; + m_last_run_started = std::chrono::high_resolution_clock::now(); + qDebug() << m_node_name << "started (run" << m_run_context.run_id << ")"; + emit run_started(); + run_impl(); + } else { + emit run_completed(m_run_context); + process_pending(); + } +} + +void Node::complete_run() +{ + m_last_run_finished = std::chrono::high_resolution_clock::now(); + m_last_run_duration_in_ms = static_cast(std::chrono::duration_cast(m_last_run_finished - m_last_run_started).count()); + m_is_running = false; + qDebug() << m_node_name << "done. Execution took" << m_last_run_duration_in_ms << "ms (run " << m_run_context.run_id << ")"; + emit run_completed(m_run_context); + process_pending(); +} + +void Node::fail_run(const std::string& message) +{ + m_is_running = false; + while (!m_pending_contexts.empty()) + m_pending_contexts.pop(); + emit run_failed(NodeRunFailureInfo(*this, message)); +} + +void Node::process_pending() +{ + if (!m_pending_contexts.empty()) { + GraphRunContext next = m_pending_contexts.front(); + m_pending_contexts.pop(); + run(next); + } +} + +bool Node::has_input_socket(const std::string& name) const +{ + return std::find_if(m_input_sockets.begin(), m_input_sockets.end(), [&name](const InputSocket& s) { return s.name() == name; }) != m_input_sockets.end(); +} + +InputSocket& Node::input_socket(const std::string& name) +{ + auto it = std::find_if(m_input_sockets.begin(), m_input_sockets.end(), [&name](const InputSocket& s) { return s.name() == name; }); + if (it == m_input_sockets.end()) + qWarning() << "input socket with name '" << name << "' not found"; + return *it; +} + +const InputSocket& Node::input_socket(const std::string& name) const +{ + auto it = std::find_if(m_input_sockets.begin(), m_input_sockets.end(), [&name](const InputSocket& s) { return s.name() == name; }); + if (it == m_input_sockets.end()) + qFatal() << "input socket with name '" << name << "' not found"; + return *it; +} + +bool Node::has_output_socket(const std::string& name) const +{ + return std::find_if(m_output_sockets.begin(), m_output_sockets.end(), [&name](const OutputSocket& s) { return s.name() == name; }) + != m_output_sockets.end(); +} + +OutputSocket& Node::output_socket(const std::string& name) +{ + auto it = std::find_if(m_output_sockets.begin(), m_output_sockets.end(), [&name](const OutputSocket& s) { return s.name() == name; }); + if (it == m_output_sockets.end()) + qFatal() << "output socket with name '" << name << "' not found"; + return *it; +} + +const OutputSocket& Node::output_socket(const std::string& name) const +{ + auto it = std::find_if(m_output_sockets.begin(), m_output_sockets.end(), [&name](const OutputSocket& s) { return s.name() == name; }); + if (it == m_output_sockets.end()) + qFatal() << "output socket with name '" << name << "' not found"; + return *it; +} + +std::vector& Node::input_sockets() { return m_input_sockets; } +const std::vector& Node::input_sockets() const { return m_input_sockets; } +std::vector& Node::output_sockets() { return m_output_sockets; } +const std::vector& Node::output_sockets() const { return m_output_sockets; } + +Data Node::get_output_data(const std::string& output_socket_name) +{ + if (!has_output_socket(output_socket_name)) { + qFatal() << "output socket with name '" << output_socket_name << "' not found"; + return {}; + } + return output_socket(output_socket_name).get_data(); +} + +Data Node::get_input_data(const std::string& input_socket_name) +{ + if (!has_input_socket(input_socket_name)) { + qFatal() << "input socket with name '" << input_socket_name << "' not found"; + return {}; + } + return input_socket(input_socket_name).connected_socket().get_data(); +} + +int Node::get_last_run_duration_in_ms() const { return m_last_run_duration_in_ms; } +bool Node::is_enabled() const { return m_enabled; } +void Node::set_enabled(bool enabled) { m_enabled = enabled; } +bool Node::is_running() const { return m_is_running; } +void Node::set_node_name(const std::string& name) { m_node_name = name; } +const std::string& Node::get_node_name() const { return m_node_name; } +uint64_t Node::get_run_id() const { return m_run_context.run_id; } +const std::string& Node::get_run_datetime() const { return m_run_context.run_datetime; } + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/Node.h b/webgpu/compute/nodes/Node.h new file mode 100644 index 000000000..2537f7ca2 --- /dev/null +++ b/webgpu/compute/nodes/Node.h @@ -0,0 +1,248 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "../GpuTileStorage.h" +#include "../GraphRunContext.h" +#include "radix/tile.h" +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +class Node; + +using DataType = size_t; + +/// datatypes that can be used with nodes have to be declared here. +using Data = std::variant*, + const std::vector*, + TileStorageTexture*, + webgpu::raii::RawBuffer*, + const webgpu::raii::TextureWithSampler*, + const radix::geometry::Aabb<2, double>*, + const radix::geometry::Aabb<3, double>*, + glm::uvec2>; + +using SocketIndex = size_t; + +// Get data type (DataType value) for specific C++ type +// adapted from https://stackoverflow.com/a/52303671 +template +static constexpr DataType data_type() +{ + static_assert(std::variant_size_v > index, "Type not found in variant"); + if constexpr (index == std::variant_size_v) { + return index; + } else if constexpr (std::is_same_v, T>) { + return index; + } else { + return data_type(); + } +} + +template +static constexpr DataType data_type() +{ + return data_type(); +} + +class Socket { +public: + enum class FlowDirection { + INPUT, + OUTPUT, + }; + +protected: + Socket(Node& node, const std::string& name, DataType type, FlowDirection direction); + +public: + [[nodiscard]] const Node& node() const; + [[nodiscard]] Node& node(); + + [[nodiscard]] const std::string& name() const; + [[nodiscard]] DataType type() const; + + [[nodiscard]] FlowDirection direction() const { return m_direction; } + +private: + Node* m_node; + std::string m_name; + DataType m_type; + FlowDirection m_direction; +}; + +class OutputSocket; +class InputSocket; + +class InputSocket : public Socket { + friend class OutputSocket; + +public: + InputSocket(Node& node, const std::string& name, DataType type); + + void connect(OutputSocket& output_socket); + void disconnect(); + + [[nodiscard]] bool is_socket_connected() const; + [[nodiscard]] OutputSocket& connected_socket(); + [[nodiscard]] const OutputSocket& connected_socket() const; + + [[nodiscard]] Data get_connected_data(); + +private: + OutputSocket* m_connected_socket = nullptr; +}; + +class OutputSocket : public Socket { + friend class InputSocket; + + using OutputFunc = std::function; + +public: + OutputSocket(Node& node, const std::string& name, DataType type, OutputFunc output_func); + + void connect(InputSocket& input_socket); + + [[nodiscard]] bool is_socket_connected() const; + [[nodiscard]] std::vector& connected_sockets(); + [[nodiscard]] const std::vector& connected_sockets() const; + + [[nodiscard]] Data get_data(); + +private: + void remove_connected_socket(InputSocket& input_socket); + +private: + OutputFunc m_output_func; + std::vector m_connected_sockets = {}; +}; + +class NodeRunFailureInfo { +public: + NodeRunFailureInfo() = delete; + NodeRunFailureInfo(const NodeRunFailureInfo&) = default; + + NodeRunFailureInfo(const Node& node, const std::string& message); + + [[nodiscard]] const Node& node() const; + [[nodiscard]] const std::string& message() const; + +private: + const Node* m_node; + std::string m_message; +}; + +// Place inside a Node subclass body to implement get_type_name(). +#define NODE_TYPE_NAME(ClassName) \ + std::string get_type_name() const override { return #ClassName; } + +/// Abstract base class for nodes. +/// +/// Subclasses implement run_impl() and signal completion by calling complete_run() or +/// fail_run(). The base class owns the run lifecycle: it buffers the GraphRunContext, +/// queues concurrent run() calls received while an async op is in-flight, and emits +/// run_completed / run_failed. +class Node : public QObject { + Q_OBJECT + +public: + Node(const std::vector& input_sockets, const std::vector& output_sockets); + virtual ~Node() = default; + + virtual std::string get_type_name() const = 0; + + virtual void serialize_settings(QJsonObject& out) const { } + virtual void deserialize_settings(const QJsonObject& in) { } + + [[nodiscard]] bool has_input_socket(const std::string& name) const; + [[nodiscard]] InputSocket& input_socket(const std::string& name); + [[nodiscard]] const InputSocket& input_socket(const std::string& name) const; + + [[nodiscard]] bool has_output_socket(const std::string& name) const; + [[nodiscard]] OutputSocket& output_socket(const std::string& name); + [[nodiscard]] const OutputSocket& output_socket(const std::string& name) const; + + [[nodiscard]] std::vector& input_sockets(); + [[nodiscard]] const std::vector& input_sockets() const; + + [[nodiscard]] std::vector& output_sockets(); + [[nodiscard]] const std::vector& output_sockets() const; + + /// Returns running time of the last execution of this node in ms. + [[nodiscard]] int get_last_run_duration_in_ms() const; + + [[nodiscard]] bool is_enabled() const; + void set_enabled(bool enabled); + + [[nodiscard]] bool is_running() const; + + void set_node_name(const std::string& name); + [[nodiscard]] const std::string& get_node_name() const; + [[nodiscard]] uint64_t get_run_id() const; + [[nodiscard]] const std::string& get_run_datetime() const; + +public slots: + void run(webgpu_compute::GraphRunContext context); + void rerun(); // re-runs with the last buffered context + +signals: + void run_started(); + void run_completed(webgpu_compute::GraphRunContext context); + void run_failed(NodeRunFailureInfo failed_info); + +protected: + /// Override to implement node behavior. + /// Call complete_run() on success or fail_run(message) on failure. + /// Postcondition (success): get_output_data(name) returns result. + virtual void run_impl() = 0; + + void complete_run(); + void fail_run(const std::string& message); + + [[nodiscard]] Data get_output_data(const std::string& output_socket_name); + [[nodiscard]] Data get_input_data(const std::string& input_socket_name); + +private: + void process_pending(); + +private: + std::vector m_input_sockets; + std::vector m_output_sockets; + + std::string m_node_name; + webgpu_compute::GraphRunContext m_run_context; + std::queue m_pending_contexts; + + std::chrono::high_resolution_clock::time_point m_last_run_started; + std::chrono::high_resolution_clock::time_point m_last_run_finished; + int m_last_run_duration_in_ms = 0; + + bool m_enabled = true; + bool m_is_running = false; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/RequestTilesNode.cpp b/webgpu/compute/nodes/RequestTilesNode.cpp new file mode 100644 index 000000000..a49e8497b --- /dev/null +++ b/webgpu/compute/nodes/RequestTilesNode.cpp @@ -0,0 +1,135 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "RequestTilesNode.h" +#include "util.h" + +#include +#include + +namespace webgpu_compute::nodes { + +RequestTilesNode::RequestTilesNode(const RequestTilesNodeSettings& settings) + : Node( + { + InputSocket(*this, "tile ids", data_type*>()), + }, + { + OutputSocket(*this, "tile data", data_type*>(), [this]() { return &m_received_tile_textures; }), + }) +{ + set_settings(settings); +} + +RequestTilesNode::RequestTilesNode() + : RequestTilesNode(RequestTilesNodeSettings()) +{ +} + +void RequestTilesNode::run_impl() +{ + + // get tile ids to request + // TODO maybe make get_input_data a template (so usage would become get_input_data(socket_index)) + const auto& tile_ids = *std::get*>()>(input_socket("tile ids").get_connected_data()); + + // if input tile ids didnt change from last run + std::set new_tile_ids(tile_ids.begin(), tile_ids.end()); + std::set old_tile_ids(m_requested_tile_ids.begin(), m_requested_tile_ids.end()); + if (new_tile_ids == old_tile_ids) { + qDebug() << "tiles already requested, use cache"; + check_progress_and_emit_signals(); + return; + } + + // send request for each tile + m_received_tile_textures.resize(tile_ids.size()); + m_requested_tile_ids = tile_ids; + m_num_tiles_requested = m_received_tile_textures.size(); + m_num_tiles_unavailable = 0; + m_num_signals_received = 0; + qDebug() << "requesting " << m_num_tiles_requested << " tiles ..."; + for (const auto& tile_id : tile_ids) { + m_tile_loader->load(tile_id); + } +} + +void RequestTilesNode::on_single_tile_received(const nucleus::tile::Data& tile) +{ + auto found_it = std::find(m_requested_tile_ids.begin(), m_requested_tile_ids.end(), tile.id); + if (found_it == m_requested_tile_ids.end()) { + // received tile id that was not requested + // this means, we send requested a new set of tiles before the responses for the old ones arrived + // ignore those, we are only interested in responses to the last set of requested tiles + return; + } + + m_num_signals_received++; + if (tile.network_info.status != nucleus::tile::NetworkInfo::Status::Good) { + m_num_tiles_unavailable++; + qWarning() << "failed to load tile id x=" << tile.id.coords.x << ", y=" << tile.id.coords.y << ", zoomlevel=" << tile.id.zoom_level << ": " + << (tile.network_info.status == nucleus::tile::NetworkInfo::Status::NotFound ? "Not found" : "Network error"); + } else { + size_t found_index = found_it - m_requested_tile_ids.begin(); + m_received_tile_textures[found_index] = *tile.data; + } + + check_progress_and_emit_signals(); +} + +void RequestTilesNode::set_settings(const RequestTilesNodeSettings& settings) +{ + m_settings = settings; + m_requested_tile_ids.clear(); // force re-download on next run + m_tile_loader = std::make_unique( + QString::fromStdString(settings.tile_path), settings.url_pattern, QString::fromStdString(settings.file_extension)); + connect(m_tile_loader.get(), &nucleus::tile::TileLoadService::load_finished, this, &RequestTilesNode::on_single_tile_received); +} + +void RequestTilesNode::check_progress_and_emit_signals() +{ + // when all requests are finished (either failed or successfully) + if (m_num_signals_received == m_num_tiles_requested) { + if (m_num_tiles_unavailable > 0) { + fail_run("failed to load " + std::to_string(m_num_tiles_unavailable) + " tiles from " + m_settings.tile_path); + } else { + complete_run(); + } + } +} + +void RequestTilesNode::serialize_settings(QJsonObject& out) const +{ + out["tile_path"] = QString::fromStdString(m_settings.tile_path); + out["url_pattern"] = url_pattern_to_string(m_settings.url_pattern); + out["file_extension"] = QString::fromStdString(m_settings.file_extension); +} + +void RequestTilesNode::deserialize_settings(const QJsonObject& in) +{ + auto s = m_settings; + if (in.contains("tile_path")) + s.tile_path = in["tile_path"].toString().toStdString(); + if (in.contains("url_pattern")) + s.url_pattern = url_pattern_from_string(in["url_pattern"].toString(), s.url_pattern); + if (in.contains("file_extension")) + s.file_extension = in["file_extension"].toString().toStdString(); + set_settings(s); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/RequestTilesNode.h b/webgpu/compute/nodes/RequestTilesNode.h new file mode 100644 index 000000000..219f29d15 --- /dev/null +++ b/webgpu/compute/nodes/RequestTilesNode.h @@ -0,0 +1,63 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include "nucleus/tile/TileLoadService.h" + +namespace webgpu_compute::nodes { + +class RequestTilesNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(RequestTilesNode) + + struct RequestTilesNodeSettings { + std::string tile_path = "https://alpinemaps.cg.tuwien.ac.at/tiles/at_dtm_alpinemaps/"; + nucleus::tile::TileLoadService::UrlPattern url_pattern = nucleus::tile::TileLoadService::UrlPattern::ZXY; + std::string file_extension = ".png"; + }; + + RequestTilesNode(); + RequestTilesNode(const RequestTilesNodeSettings& settings); + + void on_single_tile_received(const nucleus::tile::Data& tile); + + void set_settings(const RequestTilesNodeSettings& settings); + const RequestTilesNodeSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + + void check_progress_and_emit_signals(); + +public slots: + void run_impl() override; + +private: + RequestTilesNodeSettings m_settings; + std::unique_ptr m_tile_loader; + size_t m_num_signals_received = 0; + size_t m_num_tiles_unavailable = 0; + size_t m_num_tiles_requested = 0; + std::vector m_received_tile_textures; + std::vector m_requested_tile_ids; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/SelectTilesNode.cpp b/webgpu/compute/nodes/SelectTilesNode.cpp new file mode 100644 index 000000000..f973a337e --- /dev/null +++ b/webgpu/compute/nodes/SelectTilesNode.cpp @@ -0,0 +1,93 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "SelectTilesNode.h" + +#include "webgpu/compute/RectangularTileRegion.h" +#include +#include + +namespace webgpu_compute::nodes { + +SelectTilesNode::SelectTilesNode() + : Node({ InputSocket(*this, "region", data_type*>()) }, + { + OutputSocket(*this, "tile ids", data_type*>(), [this]() { return &m_output_tile_ids; }), + OutputSocket(*this, "region aabb", data_type*>(), [this]() { return &m_output_bounds; }), + }) + , m_output_bounds { glm::dvec2(std::numeric_limits::max()), glm::dvec2(std::numeric_limits::min()) } +{ +} + +void SelectTilesNode::run_impl() +{ + if (!input_socket("region").is_socket_connected()) { + fail_run("no region input connected"); + return; + } + + const auto* region = std::get*>()>(input_socket("region").get_connected_data()); + + if (m_has_cached && *region == m_cached_region && m_settings.zoomlevel == m_cached_zoom) { + complete_run(); + return; + } + + const auto lower_left_tile = nucleus::srs::world_xy_to_tile_id(glm::dvec2(region->min), m_settings.zoomlevel); + const auto upper_right_tile = nucleus::srs::world_xy_to_tile_id(glm::dvec2(region->max), m_settings.zoomlevel); + + webgpu_compute::RectangularTileRegion tile_region { + .min = lower_left_tile.coords, + .max = upper_right_tile.coords, + .zoom_level = upper_right_tile.zoom_level, + .scheme = radix::tile::Scheme::Tms, + }; + const auto tile_ids = tile_region.get_tiles(); + + m_output_tile_ids.clear(); + if (tile_ids.empty()) { + qWarning() << "no tiles selected"; + return; + } + qDebug() << tile_ids.size() << " tiles selected"; + + m_output_bounds = { glm::dvec2(std::numeric_limits::max()), glm::dvec2(std::numeric_limits::min()) }; + for (const auto& tile_id : tile_ids) { + m_output_bounds.expand_by(nucleus::srs::tile_bounds(tile_id)); + } + qDebug() << Qt::fixed << "selected aabb=[(" << m_output_bounds.min.x << ", " << m_output_bounds.min.y << "), (" << m_output_bounds.max.x << ", " + << m_output_bounds.max.y << ")]"; + + m_output_tile_ids.insert(m_output_tile_ids.begin(), tile_ids.begin(), tile_ids.end()); + + m_cached_region = *region; + m_cached_zoom = m_settings.zoomlevel; + m_has_cached = true; + + complete_run(); +} + +void SelectTilesNode::serialize_settings(QJsonObject& out) const { out["zoomlevel"] = static_cast(m_settings.zoomlevel); } + +void SelectTilesNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("zoomlevel")) + m_settings.zoomlevel = static_cast(in["zoomlevel"].toInt(static_cast(m_settings.zoomlevel))); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/SelectTilesNode.h b/webgpu/compute/nodes/SelectTilesNode.h new file mode 100644 index 000000000..052073134 --- /dev/null +++ b/webgpu/compute/nodes/SelectTilesNode.h @@ -0,0 +1,54 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" + +namespace webgpu_compute::nodes { + +class SelectTilesNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(SelectTilesNode) + + struct SelectTilesNodeSettings { + uint32_t zoomlevel = 15; + }; + + SelectTilesNode(); + + void set_settings(const SelectTilesNodeSettings& settings) { m_settings = settings; } + const SelectTilesNodeSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + SelectTilesNodeSettings m_settings; + std::vector m_output_tile_ids; + radix::geometry::Aabb<2, double> m_output_bounds; + + radix::geometry::Aabb<3, double> m_cached_region; + uint32_t m_cached_zoom = 0; + bool m_has_cached = false; +}; +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/TileStitchNode.cpp b/webgpu/compute/nodes/TileStitchNode.cpp new file mode 100644 index 000000000..b5ec6700e --- /dev/null +++ b/webgpu/compute/nodes/TileStitchNode.cpp @@ -0,0 +1,184 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TileStitchNode.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +TileStitchNode::TileStitchNode(webgpu::Context& ctx) + : TileStitchNode(ctx, StitchSettings {}) +{ +} + +webgpu_compute::nodes::TileStitchNode::TileStitchNode(webgpu::Context& ctx, StitchSettings settings) + : Node( + { + InputSocket(*this, "tile ids", data_type*>()), + InputSocket(*this, "texture data", data_type*>()), + }, + { OutputSocket(*this, "texture", data_type(), [this]() { return m_output_texture.get(); }) }) + , m_ctx(&ctx) + , m_settings(settings) +{ +} + +void TileStitchNode::run_impl() +{ + + // get tile ids to process + const auto& tile_ids = *std::get*>()>(input_socket("tile ids").get_connected_data()); + const auto& textures = *std::get*>()>(input_socket("texture data").get_connected_data()); + assert(tile_ids.size() == textures.size()); + + // The original tile size is as configured in the settings + glm::uvec2 so = m_settings.tile_size; + // The effective tile size depends on whether they include a 1px border + glm::uvec2 s = m_settings.tile_has_border ? so - glm::uvec2(1) : so; + // The zoom level of the stitched depends on the zoom level of the first tile inside the tile_ids (could be a setting) + uint32_t zl = tile_ids[0].zoom_level; + + // Iterate through all tiles and save the bounds for the zoom level + glm::uvec4 bounds = glm::uvec4( + std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::min(), std::numeric_limits::min()); + for (const auto& tile_id : tile_ids) { + bounds.x = std::min(bounds.x, tile_id.coords.x); + bounds.y = std::min(bounds.y, tile_id.coords.y); + bounds.z = std::max(bounds.z, tile_id.coords.x); + bounds.w = std::max(bounds.w, tile_id.coords.y); + } + + // Calculate the size in tiles and pixel + glm::uvec2 size_tiles; + glm::uvec2 size_pixels; + size_tiles = glm::uvec2(bounds.z - bounds.x + 1, bounds.w - bounds.y + 1); + size_pixels = size_tiles * s; + + qDebug() << "About to stitch " << size_tiles.x << "x" << size_tiles.y << " tiles into an image of size " << size_pixels.x << "x" << size_pixels.y + << " pixels"; + + // Check if inside bounds + if (size_pixels.x > MAX_STITCHED_IMAGE_SIZE || size_pixels.y > MAX_STITCHED_IMAGE_SIZE) { + fail_run("Stitched image size would exceeds maximum size of " + std::to_string(MAX_STITCHED_IMAGE_SIZE) + "x" + + std::to_string(MAX_STITCHED_IMAGE_SIZE) + " pixel for zoom level " + std::to_string(zl)); + return; + } + + // create output texture + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "compute storage texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { size_pixels.x, size_pixels.y, 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = m_settings.texture_format; + texture_desc.usage = m_settings.texture_usage; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "compute storage sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + m_output_texture = std::make_unique(m_ctx->device(), texture_desc, sampler_desc); + auto& tex = m_output_texture->texture(); + + // store them in this context, otherwise they get deleted too soon (might not be necessary...) + std::map> images; + // Load the tiles and upload them directly to the gpu texture + for (size_t i = 0; i < tile_ids.size(); i++) { + const auto& tile_id = tile_ids[i]; + const auto& texture_data = textures[i]; + + if (tile_id.zoom_level != zl) + continue; + + // Calculate the position of the tile in the stitched image + glm::uvec2 pos = glm::uvec2(tile_id.coords.x - bounds.x, tile_id.coords.y - bounds.y) * s; + if (m_settings.stitch_inverted_y) { + pos.y = size_pixels.y - pos.y - s.y; + } + + // Load image (NOTE: Only supports u8vec4 so far) + images[i] = nucleus::utils::image_loader::rgba8(texture_data).value(); + const auto& image = images[i]; + assert(image.width() == so.x && image.height() == so.y); + + WGPUTexelCopyTextureInfo image_copy_texture {}; + image_copy_texture.texture = tex.handle(); + image_copy_texture.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + image_copy_texture.mipLevel = 0; + image_copy_texture.origin = { pos.x, pos.y, 0 }; + + WGPUTexelCopyBufferLayout texture_data_layout {}; + texture_data_layout.bytesPerRow = uint32_t(sizeof(glm::u8vec4) * so.x); + texture_data_layout.rowsPerImage = uint32_t(so.y); + texture_data_layout.offset = 0; + + WGPUExtent3D copy_extent {}; + copy_extent.width = s.x; + copy_extent.height = s.y; + copy_extent.depthOrArrayLayers = 1; + + wgpuQueueWriteTexture(m_ctx->queue(), &image_copy_texture, image.bytes(), uint32_t(image.size_in_bytes()), &texture_data_layout, ©_extent); + } + + complete_run(); + + // Weird that this works here. Is wgpuQueueWriteTexture blocking after all? + // m_output_texture->texture().save_to_file(m_ctx->device(), "C:\\tmp\\asd.png"); +} + +void TileStitchNode::serialize_settings(QJsonObject& out) const +{ + out["tile_size"] = uvec2_to_json(m_settings.tile_size); + out["tile_has_border"] = m_settings.tile_has_border; + out["stitch_inverted_y"] = m_settings.stitch_inverted_y; + out["texture_format"] = wgpu_format_to_string(m_settings.texture_format); + out["texture_usage"] = wgpu_usage_to_json(m_settings.texture_usage); +} + +void TileStitchNode::deserialize_settings(const QJsonObject& in) +{ + if (in.contains("tile_size")) + m_settings.tile_size = uvec2_from_json(in["tile_size"].toArray(), m_settings.tile_size); + if (in.contains("tile_has_border")) + m_settings.tile_has_border = in["tile_has_border"].toBool(m_settings.tile_has_border); + if (in.contains("stitch_inverted_y")) + m_settings.stitch_inverted_y = in["stitch_inverted_y"].toBool(m_settings.stitch_inverted_y); + if (in.contains("texture_format")) + m_settings.texture_format = wgpu_format_from_string(in["texture_format"].toString(), m_settings.texture_format); + if (in.contains("texture_usage")) + m_settings.texture_usage = wgpu_usage_from_json(in["texture_usage"].toArray(), m_settings.texture_usage); +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/TileStitchNode.h b/webgpu/compute/nodes/TileStitchNode.h new file mode 100644 index 000000000..d40f6faeb --- /dev/null +++ b/webgpu/compute/nodes/TileStitchNode.h @@ -0,0 +1,72 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Node.h" +#include + +#define MAX_STITCHED_IMAGE_SIZE 8192 + +namespace webgpu_compute::nodes { + +class TileStitchNode : public Node { + Q_OBJECT + +public: + NODE_TYPE_NAME(TileStitchNode) + + struct StitchSettings { + // The size of the input tiles (65x65?) + glm::uvec2 tile_size = glm::uvec2(65); + + // If true, the right and bottom 1px wide edge will be ignored when stitching + bool tile_has_border = true; + + // For slippyMap tiles this has to be set to true as y starts from the bottom + bool stitch_inverted_y = true; + + // The format of the output texture + // IMPORTANT: The caller has to ensure that the format of the input tiles has the same bit depth + WGPUTextureFormat texture_format = WGPUTextureFormat::WGPUTextureFormat_RGBA8Unorm; + + // The usage flags of the output texture + WGPUTextureUsage texture_usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + }; + + explicit TileStitchNode(webgpu::Context& ctx); // default-configured; for the NodeRegistry + TileStitchNode(webgpu::Context& ctx, StitchSettings settings); + + // settings are consumed lazily in run_impl, applying them at any time is safe + void set_settings(const StitchSettings& settings) { m_settings = settings; } + const StitchSettings& get_settings() const { return m_settings; } + void serialize_settings(QJsonObject& out) const override; + void deserialize_settings(const QJsonObject& in) override; + +public slots: + void run_impl() override; + +private: + webgpu::Context* m_ctx; + + StitchSettings m_settings; + + std::unique_ptr m_output_texture; +}; + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/util.cpp b/webgpu/compute/nodes/util.cpp new file mode 100644 index 000000000..51df5e48f --- /dev/null +++ b/webgpu/compute/nodes/util.cpp @@ -0,0 +1,259 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "util.h" + +#include +#include +#include + +namespace webgpu_compute::nodes { + +void write_timings_to_json_file(const NodeGraph& node_graph, const std::filesystem::path& output_path) +{ + QJsonObject object; + for (const auto& [name, node] : node_graph.get_nodes()) { + object[QString::fromStdString(name)] = node.get()->get_last_run_duration_in_ms(); + } + + QJsonDocument doc; + doc.setObject(object); + + QFile output_file(output_path); + if (!output_file.open(QIODevice::WriteOnly)) + qFatal("Failed to open timings file for writing: %s", output_path.string().c_str()); + output_file.write(doc.toJson()); + output_file.close(); +} + +// ---- enum tables ---- + +namespace { + + struct TextureFormatEntry { + WGPUTextureFormat format; + const char* name; + }; + constexpr TextureFormatEntry texture_format_table[] = { + { WGPUTextureFormat_R8Unorm, "R8Unorm" }, + { WGPUTextureFormat_R8Uint, "R8Uint" }, + { WGPUTextureFormat_R32Float, "R32Float" }, + { WGPUTextureFormat_RG8Unorm, "RG8Unorm" }, + { WGPUTextureFormat_RG32Float, "RG32Float" }, + { WGPUTextureFormat_RGBA8Unorm, "RGBA8Unorm" }, + { WGPUTextureFormat_RGBA8Uint, "RGBA8Uint" }, + { WGPUTextureFormat_RGBA8Sint, "RGBA8Sint" }, + { WGPUTextureFormat_BGRA8Unorm, "BGRA8Unorm" }, + { WGPUTextureFormat_RGBA16Float, "RGBA16Float" }, + { WGPUTextureFormat_RGBA32Float, "RGBA32Float" }, + }; + + struct TextureUsageFlagEntry { + uint64_t flag; + const char* name; + }; + const TextureUsageFlagEntry texture_usage_table[] = { + { static_cast(WGPUTextureUsage_CopySrc), "CopySrc" }, + { static_cast(WGPUTextureUsage_CopyDst), "CopyDst" }, + { static_cast(WGPUTextureUsage_TextureBinding), "TextureBinding" }, + { static_cast(WGPUTextureUsage_StorageBinding), "StorageBinding" }, + { static_cast(WGPUTextureUsage_RenderAttachment), "RenderAttachment" }, + }; + + struct FilterModeEntry { + WGPUFilterMode mode; + const char* name; + }; + constexpr FilterModeEntry filter_mode_table[] = { + { WGPUFilterMode_Nearest, "Nearest" }, + { WGPUFilterMode_Linear, "Linear" }, + }; + + struct MipmapFilterModeEntry { + WGPUMipmapFilterMode mode; + const char* name; + }; + constexpr MipmapFilterModeEntry mipmap_filter_mode_table[] = { + { WGPUMipmapFilterMode_Nearest, "Nearest" }, + { WGPUMipmapFilterMode_Linear, "Linear" }, + }; + + struct UrlPatternEntry { + nucleus::tile::TileLoadService::UrlPattern pattern; + const char* name; + }; + constexpr UrlPatternEntry url_pattern_table[] = { + { nucleus::tile::TileLoadService::UrlPattern::ZXY, "ZXY" }, + { nucleus::tile::TileLoadService::UrlPattern::ZYX, "ZYX" }, + { nucleus::tile::TileLoadService::UrlPattern::ZXY_yPointingSouth, "ZXY_yPointingSouth" }, + { nucleus::tile::TileLoadService::UrlPattern::ZYX_yPointingSouth, "ZYX_yPointingSouth" }, + }; + +} // namespace + +// ---- WGPU format ---- + +QString wgpu_format_to_string(WGPUTextureFormat format) +{ + for (const auto& e : texture_format_table) { + if (e.format == format) + return QLatin1String(e.name); + } + return QString("#%1").arg(static_cast(format)); +} + +WGPUTextureFormat wgpu_format_from_string(const QString& str, WGPUTextureFormat fallback) +{ + for (const auto& e : texture_format_table) { + if (str == QLatin1String(e.name)) + return e.format; + } + qWarning() << "wgpu_format_from_string: unknown format" << str << "- using fallback"; + return fallback; +} + +// ---- WGPU usage ---- + +QJsonArray wgpu_usage_to_json(WGPUTextureUsage usage) +{ + QJsonArray arr; + uint64_t remaining = static_cast(usage); + for (const auto& e : texture_usage_table) { + if (remaining & e.flag) { + arr.append(QLatin1String(e.name)); + remaining &= ~e.flag; + } + } + if (remaining != 0) + arr.append(QString("#%1").arg(remaining)); + return arr; +} + +WGPUTextureUsage wgpu_usage_from_json(const QJsonArray& arr, WGPUTextureUsage fallback) +{ + uint64_t usage = 0; + for (const QJsonValue& jv : arr) { + if (!jv.isString()) { + qWarning() << "wgpu_usage_from_json: expected string array - using fallback"; + return fallback; + } + const QString str = jv.toString(); + bool found = false; + for (const auto& e : texture_usage_table) { + if (str == QLatin1String(e.name)) { + usage |= e.flag; + found = true; + break; + } + } + if (!found) { + qWarning() << "wgpu_usage_from_json: unknown flag" << str << "- skipping"; + } + } + return static_cast(usage); +} + +// ---- WGPU filter mode ---- + +QString wgpu_filter_mode_to_string(WGPUFilterMode mode) +{ + for (const auto& e : filter_mode_table) { + if (e.mode == mode) + return QLatin1String(e.name); + } + return QString("#%1").arg(static_cast(mode)); +} + +WGPUFilterMode wgpu_filter_mode_from_string(const QString& str, WGPUFilterMode fallback) +{ + for (const auto& e : filter_mode_table) { + if (str == QLatin1String(e.name)) + return e.mode; + } + qWarning() << "wgpu_filter_mode_from_string: unknown mode" << str << "- using fallback"; + return fallback; +} + +// ---- WGPU mipmap filter mode ---- + +QString wgpu_mipmap_filter_mode_to_string(WGPUMipmapFilterMode mode) +{ + for (const auto& e : mipmap_filter_mode_table) { + if (e.mode == mode) + return QLatin1String(e.name); + } + return QString("#%1").arg(static_cast(mode)); +} + +WGPUMipmapFilterMode wgpu_mipmap_filter_mode_from_string(const QString& str, WGPUMipmapFilterMode fallback) +{ + for (const auto& e : mipmap_filter_mode_table) { + if (str == QLatin1String(e.name)) + return e.mode; + } + qWarning() << "wgpu_mipmap_filter_mode_from_string: unknown mode" << str << "- using fallback"; + return fallback; +} + +// ---- UrlPattern ---- + +QString url_pattern_to_string(nucleus::tile::TileLoadService::UrlPattern pattern) +{ + for (const auto& e : url_pattern_table) { + if (e.pattern == pattern) + return QLatin1String(e.name); + } + return QString("#%1").arg(static_cast(pattern)); +} + +nucleus::tile::TileLoadService::UrlPattern url_pattern_from_string(const QString& str, nucleus::tile::TileLoadService::UrlPattern fallback) +{ + for (const auto& e : url_pattern_table) { + if (str == QLatin1String(e.name)) + return e.pattern; + } + qWarning() << "url_pattern_from_string: unknown pattern" << str << "- using fallback"; + return fallback; +} + +// ---- glm helpers ---- + +QJsonArray vec2_to_json(glm::vec2 v) { return { static_cast(v.x), static_cast(v.y) }; } + +glm::vec2 vec2_from_json(const QJsonArray& arr, glm::vec2 fallback) +{ + if (arr.size() != 2 || !arr[0].isDouble() || !arr[1].isDouble()) { + qWarning() << "vec2_from_json: expected [x, y] array - using fallback"; + return fallback; + } + return { static_cast(arr[0].toDouble()), static_cast(arr[1].toDouble()) }; +} + +QJsonArray uvec2_to_json(glm::uvec2 v) { return { static_cast(v.x), static_cast(v.y) }; } + +glm::uvec2 uvec2_from_json(const QJsonArray& arr, glm::uvec2 fallback) +{ + if (arr.size() != 2 || !arr[0].isDouble() || !arr[1].isDouble()) { + qWarning() << "uvec2_from_json: expected [x, y] array - using fallback"; + return fallback; + } + return { static_cast(arr[0].toInt()), static_cast(arr[1].toInt()) }; +} + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/nodes/util.h b/webgpu/compute/nodes/util.h new file mode 100644 index 000000000..b82ddbfc9 --- /dev/null +++ b/webgpu/compute/nodes/util.h @@ -0,0 +1,62 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "../NodeGraph.h" +#include +#include +#include +#include +#include +#include + +namespace webgpu_compute::nodes { + +void write_timings_to_json_file(const NodeGraph& node_graph, const std::filesystem::path& output_path); + +// ---- WGPU enum <-> JSON helpers ---- +// WGPU enum values are not stable across Dawn/webgpu.h updates, so they are stored as strings. +// WGPUTextureUsage flags are stored as a string array (the type may be 64-bit). +// On unknown strings, from_string logs a qWarning and returns the fallback. + +QString wgpu_format_to_string(WGPUTextureFormat format); +WGPUTextureFormat wgpu_format_from_string(const QString& str, WGPUTextureFormat fallback); + +QJsonArray wgpu_usage_to_json(WGPUTextureUsage usage); +WGPUTextureUsage wgpu_usage_from_json(const QJsonArray& arr, WGPUTextureUsage fallback); + +QString wgpu_filter_mode_to_string(WGPUFilterMode mode); +WGPUFilterMode wgpu_filter_mode_from_string(const QString& str, WGPUFilterMode fallback); + +QString wgpu_mipmap_filter_mode_to_string(WGPUMipmapFilterMode mode); +WGPUMipmapFilterMode wgpu_mipmap_filter_mode_from_string(const QString& str, WGPUMipmapFilterMode fallback); + +QString url_pattern_to_string(nucleus::tile::TileLoadService::UrlPattern pattern); +nucleus::tile::TileLoadService::UrlPattern url_pattern_from_string(const QString& str, nucleus::tile::TileLoadService::UrlPattern fallback); + +// ---- glm <-> JSON array helpers ---- + +QJsonArray vec2_to_json(glm::vec2 v); +glm::vec2 vec2_from_json(const QJsonArray& arr, glm::vec2 fallback); + +QJsonArray uvec2_to_json(glm::uvec2 v); +glm::uvec2 uvec2_from_json(const QJsonArray& arr, glm::uvec2 fallback); + +} // namespace webgpu_compute::nodes diff --git a/webgpu/compute/shaders/avalanche_trajectories_compute.wgsl b/webgpu/compute/shaders/avalanche_trajectories_compute.wgsl new file mode 100644 index 000000000..652fd74dd --- /dev/null +++ b/webgpu/compute/shaders/avalanche_trajectories_compute.wgsl @@ -0,0 +1,407 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Markus Rampp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::filtering +///use webgpu::tile_util +///use webgpu::normals_util +///use random +///use webgpu::encoder + +// weights need to match the texels that are chosen by textureGather - this does NOT align perfectly, and introduces some artifacts +// adding offset fixes this issue, see https://www.reedbeta.com/blog/texture-gathers-and-coordinate-precision/ +const TEXTURE_GATHER_OFFSET = 1.0f / 512.0f; +const g: f32 = 9.81; +const density: f32 = 200.0; +const slab_thickness: f32 = 1; +const cfl: f32 = 0.5; + +const mass_per_area = density * slab_thickness; +const acceleration_gravity = vec3f(0.0, 0.0, -g); +const velocity_threshold: f32 = 0.01f; + +struct AvalancheTrajectoriesSettings { + output_resolution: vec2u, + region_size: vec2f, // world space width and height of the the region we operate on + num_steps: u32, // maximum number of steps (along gradient) + step_length: f32, // length of one simulation step in world space +// num_paths_per_release_cell: u32, + max_perturbation: f32, // maximum perturbation angle in radians + persistence_contribution: f32, // persistence contribution on normal in [0,1], 0 means only local normal, 1 means only last normal + model_type: u32, // 0 = weBIGeo Avalanche Simulation, 1 = Physics Less Simple + model2_gravity: f32, + model2_mass: f32, + model2_friction_coeff: f32, + model2_drag_coeff: f32, + runout_model_type: u32, //actually: friction model: 0 coulomb, 1 voellmy, 2 voellmy minshear, 3 samosAt + runout_perla_my: f32, // sliding friction coeff + runout_perla_md: f32, // M/D mass-to-drag ratio + runout_perla_l: f32, // distance between grid cells (in m) + runout_perla_g: f32, // acceleration due to gravity (in m/s^2) + runout_flowpy_alpha: f32, // alpha runout angle in radians + layer1_zdelta_enabled: u32, // 0 = disabled, 1 = enabled + layer2_cellCounts_enabled: u32, + layer3_travelLength_enabled: u32, + layer4_travelAngle_enabled: u32, + layer5_altitudeDifference_enabled: u32, + random_seed: u32, +} + +// input +@group(0) @binding(0) var settings: AvalancheTrajectoriesSettings; +@group(0) @binding(1) var input_normal_texture: texture_2d; +@group(0) @binding(2) var input_height_texture: texture_2d; +@group(0) @binding(3) var input_release_point_texture: texture_2d; +@group(0) @binding(4) var input_normal_sampler: sampler; +@group(0) @binding(5) var input_height_sampler: sampler; + +// output +@group(0) @binding(6) var output_storage_buffer: array>; // trajectory texture + +// output layers +@group(0) @binding(7) var output_layer1_zdelta: array>; +@group(0) @binding(8) var output_layer2_cellCounts: array>; +@group(0) @binding(9) var output_layer3_travelLength: array>; +@group(0) @binding(10) var output_layer4_travelAngle: array>; +@group(0) @binding(11) var output_layer5_altitudeDifference: array>; + +// note: as of writing this, wgsl only supports atomic access for storage buffers and only for u32 and i32 +// therefore, we first write the risk value (along the trajectory as raster) into a buffer, +// then write its contents into a texture in a subsequent step (avalanche_trajectories_buffer_to_texture.wgsl) + +// Samples normal texture with bilinear filtering. +fn sample_normal_texture(uv: vec2f) -> vec3f { + return textureSampleLevel(input_normal_texture, input_normal_sampler, uv, 0).xyz * 2 - 1; +} + +// Samples height texture with bilinear filtering. +fn sample_height_texture(uv: vec2f) -> f32 { + let texture_dimensions = textureDimensions(input_height_texture); + let weights: vec2f = fract(uv * vec2f(texture_dimensions) - 0.5f + TEXTURE_GATHER_OFFSET); // -0.5 to make relative to texel center + let texel_values: vec4f = textureGather(0, input_height_texture, input_height_sampler, uv); + return dot(vec4f((1.0 - weights.x) * weights.y, weights.x * weights.y, weights.x * (1.0 - weights.y), (1.0 - weights.x) * (1.0 - weights.y)), texel_values); +} + +// Samples release point texture with nearest neighbor. +// Returns true if alpha component is greater than 0, false otherwise. +fn sample_release_point_texture(uv: vec2f) -> bool { + let texture_dimensions = textureDimensions(input_release_point_texture); + let pos = vec2u(uv * vec2f(texture_dimensions)); + let mask = textureLoad(input_release_point_texture, pos, 0).rgba; + return mask.a > 0; +} + +fn draw_line_uv(start_uv: vec2f, end_uv: vec2f, value: f32, z_delta: f32, travel_length: f32, travel_angle: f32, altitude_difference: f32) { + let start_pos = vec2u(floor(start_uv * vec2f(settings.output_resolution))); + let end_pos = vec2u(floor(end_uv * vec2f(settings.output_resolution))); + draw_line_pos(start_pos, end_pos, value, z_delta, travel_length, travel_angle, altitude_difference); +} + +// implementation of bresenham's line algorithm +// adapted from https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases (used last one, with error) +fn draw_line_pos(start_pos: vec2u, end_pos: vec2u, value: f32, z_delta: f32, travel_length: f32, travel_angle: f32, altitude_difference: f32) { + let dx = abs(i32(end_pos.x) - i32(start_pos.x)); + let sx = select(-1, 1, start_pos.x < end_pos.x); + let dy = -abs(i32(end_pos.y) - i32(start_pos.y)); + let sy = select(-1, 1, start_pos.y < end_pos.y); + var error = dx + dy; + var x = i32(start_pos.x); + var y = i32(start_pos.y); + + const layer_scaling = 1.0;//1000.0; // scaling factor for layer values, to avoid precision loss (also change in buffer_to_texture) + + while true { + let buffer_index = u32(y) * settings.output_resolution.x + u32(x); + // Commented out for a fair comparison, (other implementations also just write the other layers) + //atomicMax(&output_storage_buffer[buffer_index], range_to_u32(value, U32_ENCODING_RANGE_NORM)); // map value from [0,1] angle to [0, 2^32 - 1] + if settings.layer1_zdelta_enabled != 0 { + atomicMax(&output_layer1_zdelta[buffer_index], u32(z_delta * layer_scaling)); // zdelta in m (we lose some precision here) + } + if settings.layer2_cellCounts_enabled != 0 { + atomicAdd(&output_layer2_cellCounts[buffer_index], 1 * u32(layer_scaling)); // count number of steps in this layer + } + if settings.layer3_travelLength_enabled != 0 { + atomicMax(&output_layer3_travelLength[buffer_index], u32(travel_length * layer_scaling)); // travel length in m (we lose some precision here) + } + if settings.layer4_travelAngle_enabled != 0 { + let travel_angle_value: f32 = degrees(travel_angle); // travel angle in deg (we lose some precision here) + atomicMax(&output_layer4_travelAngle[buffer_index], u32(travel_angle_value * layer_scaling)); + } + if settings.layer5_altitudeDifference_enabled != 0 { + atomicMax(&output_layer5_altitudeDifference[buffer_index], u32(altitude_difference * layer_scaling)); + } + + if x == i32(end_pos.x) && y == i32(end_pos.y) { + break; + } + + let e2 = 2 * error; + if e2 >= dy { + error = error + dy; + x += sx; + } + if e2 <= dx { + error = error + dx; + y += sy; + } + } +} + +// ***** MODELS ***** + +// returns UV coordinates for the trajectory starting point for a thread id +fn get_starting_point_uv(id: vec3) -> vec2f { + let input_texture_size = textureDimensions(input_normal_texture); + let texel_size_uv = 1.0 / vec2f(input_texture_size); + //let uv = vec2f(f32(id.x), f32(id.y)) * texel_size_uv + texel_size_uv / 2.0; // texel center + let uv = vec2f(f32(id.x), f32(id.y)) * texel_size_uv + rand2() * texel_size_uv; // random within texel + //TODO try regular grid (?) + return uv; +} + +fn trajectory_overlay(id: vec3) { + seed(vec4u(id, settings.random_seed)); //seed PRNG with thread id + let pixel_size = vec2f(settings.region_size) / vec2f(textureDimensions(input_normal_texture)); + let dx = min(pixel_size.x, pixel_size.y); + + let uv = get_starting_point_uv(id); + // let uv = vec2f(f32(id.x), f32(id.y)) * texel_size_uv; + + if !sample_release_point_texture(uv) { + return; + } + + // get slope angle at start + let start_normal = sample_normal_texture(uv); + var velocity = vec3f(0, 0, 0); + var last_velocity = vec3f(0, 0, 0); + + let start_slope_angle = get_slope_angle(start_normal); + let trajectory_value = start_slope_angle / (PI / 2); + + // alpha-beta model state + let start_point_height: f32 = sample_height_texture(uv); + var world_space_travel_distance: f32 = 0.0; + + var last_uv = uv; + var world_space_offset = vec2f(0, 0); // offset from original world position + + var normal_t = vec3f(0, 0, 1); + + var acceleration_tangential = acceleration_gravity - g * start_normal.z * start_normal; + var acceleration_friction = vec3f(0, 0, 0); + var dt = sqrt(2 * dx / length(acceleration_tangential)); + var last_direction = vec2f(0, 0); + + var z_delta = 0f; + + var velocity_magnitude = 0f; + + for (var i: u32 = 0; i < settings.num_steps; i++) { + // compute uv coordinates for current position + let current_uv = uv + vec2f(world_space_offset.x, -world_space_offset.y) / settings.region_size; + + // quit if moved out of bounds + if current_uv.x < 0 || current_uv.x > 1 || current_uv.y < 0 || current_uv.y > 1 { + break; + } + + let current_height = sample_height_texture(current_uv); + + // check if we are still on the terrain + // avaframe examples have non rectangular terrain + // missing values are noramlly -9999 + // webigeo sets them to 0 + if current_height < 10 { + break; + } + + let normal = sample_normal_texture(current_uv); + + if i > 0 { + // calculate physical quantities + // - altitude difference from starting point + // - z_delta (kinetic energy ~ velocity) + // - delta (local runout angle) + // - distance we have already + + let height_difference = start_point_height - current_height; + let z_alpha = tan(settings.runout_flowpy_alpha) * world_space_travel_distance; + let z_gamma = height_difference; + z_delta = z_gamma - z_alpha; + let gamma = atan(height_difference / world_space_travel_distance); // will always be positive -> [ 0 , PI/2 ] + // let delta = gamma - settings.runout_flowpy_alpha; + // evaluate runout model, terminate, if necessary + if settings.model_type == 0 { + // more info: https://docs.avaframe.org/en/latest/theoryCom4FlowPy.html + if z_delta <= 0 { + break; + } + } + if settings.model_type == 1 { + z_delta = velocity_magnitude * velocity_magnitude / (2 * g); + } + + // draw line from last to current position + draw_line_uv(last_uv, current_uv, trajectory_value, z_delta, world_space_travel_distance, gamma, height_difference); + } + last_uv = current_uv; + + // sample normal and get new world space offset based on chosen model + if settings.model_type == 0 { + let perturbed_normal: vec3f = perturb(normal); + let perturbed_normal_2d: vec2f = perturbed_normal.xy; + var velocity_magnitude = sqrt(z_delta * 2 * g); + if velocity_magnitude < 1 { //check potential 0-division before normalization + velocity_magnitude = 1; + } + let current_direction = last_direction * settings.persistence_contribution + perturbed_normal_2d / velocity_magnitude * (1.0 - settings.persistence_contribution); + + let dir_magnitude = length(current_direction); + if dir_magnitude < 0.001 { //check potential 0-division before normalization + break; + } + let normalized_current_direction = current_direction / dir_magnitude; + last_direction = normalized_current_direction; + + //TODO 2 is step length here, use uniform, put into ui + let relative_trajectory = normalized_current_direction.xy * 2 * settings.step_length; + + world_space_offset = world_space_offset + relative_trajectory; + world_space_travel_distance += length(relative_trajectory); + } else if settings.model_type == 1 { + let acceleration_normal = g * normal.z * normal; + let acceleration_tangential = acceleration_gravity + acceleration_normal; + // estimate optimal timestep + dt = cfl * dx / length(velocity + acceleration_tangential * dt); + let acceleration_friction_magnitude = acceleration_by_friction(acceleration_normal, mass_per_area, velocity); + velocity = velocity + acceleration_tangential * dt; + // friction stop criterion, it has to use the new timestep + if length(velocity) < acceleration_friction_magnitude * dt { + dt = length(velocity) / acceleration_friction_magnitude; + // let relative_trajectory = dt * 0.5 * (last_velocity + velocity); + let relative_trajectory = velocity * dt; + world_space_offset = world_space_offset + relative_trajectory.xy; + world_space_travel_distance += length(relative_trajectory.xy); + break; + } + last_velocity = velocity; + velocity = velocity + acceleration_tangential * dt; + // explicit + velocity = velocity - acceleration_friction_magnitude * normalize(velocity) * dt; + // implicit + // velocity_magnitude = length(velocity); + // velocity = velocity / (1.0 - acceleration_friction / + // select(velocity_magnitude, velocity_threshold, velocity_magnitude < velocity_threshold) * dt); + // let relative_trajectory = dt * 0.5 * (last_velocity + velocity); + let relative_trajectory = velocity * dt; + world_space_offset = world_space_offset + relative_trajectory.xy; + world_space_travel_distance += length(relative_trajectory.xy); + velocity_magnitude = length(velocity); + if velocity_magnitude < velocity_threshold { + break; + } + last_velocity = velocity; + } + } +} + +// Generates a random unit vector in a cone around the given vector +fn perturb(v: vec3) -> vec3 { + let cos_max_angle_rad = cos(settings.max_perturbation); + let r = rand2(); + let u1 = r.x; + let u2 = r.y; + + // Convert from uniform random to spherical coordinates within cone + let cos_theta = cos_max_angle_rad + (1 - cos_max_angle_rad) * u1; // this is uniform + //let cos_theta = cos_max_angle_rad + (1 - cos_max_angle_rad) * sqrt(u1); // this i think should be cosine weighted + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = 2.0 * PI * u2; + + // If the vector is close to the z-axis, use a different up vector + var up = select(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), abs(v.z) >= 0.999); + + let tangent = normalize(cross(up, v)); + let bitangent = cross(v, tangent); + + // Compute perturbed direction + return v * cos_theta + + tangent * sin_theta * cos(phi) + + bitangent * sin_theta * sin(phi); +} + +fn acceleration_by_friction(acceleration_normal: vec3f, mass_per_area: f32, velocity: vec3f) -> f32 { + let velocity_magnitude = length(velocity); + if velocity_magnitude < velocity_threshold || settings.runout_model_type == 4 { + return 0.0f; + } + let model = settings.runout_model_type; + // standard 0.155, samos: standard 0.155, small 0.22, medium 0.17 + let friction_coefficient = settings.model2_friction_coeff; + let drag_coefficient = settings.model2_drag_coeff; // only used for voellmy, standard 4000. + let normal_stress = length(acceleration_normal * mass_per_area); + const min_shear_stress = 70f; + var shear_stress = 0.0f; + //actually: friction model: 0 coulomb, 1 voellmy, 2 voellmy minshear, 3 samosAt + // Coulomb friction model + if model == 0 { + shear_stress = friction_coefficient * normal_stress; + } + // Voellmy friction model + else if model == 1 { + shear_stress = friction_coefficient * normal_stress + density * g * velocity_magnitude * velocity_magnitude / drag_coefficient; + } + // Voellmy min shear friction model + else if model == 2 { + shear_stress = min_shear_stress + friction_coefficient * normal_stress + density * g * velocity_magnitude * velocity_magnitude / drag_coefficient; + } + // samosAT friction model + else if model == 3 { + let min_shear_stress = 0f; + let rs0 = 0.222; + let kappa = 0.43; + let r = 0.05; + let b = 4.13; + let rs = density * velocity_magnitude * velocity_magnitude / (normal_stress + 0.001); + var div = slab_thickness / r; + if div < 1.0 { + div = 1.0; + } + div = log(div) / kappa + b; + shear_stress = min_shear_stress + normal_stress * friction_coefficient * (1.0 + rs0 / (rs0 + rs)) + density * velocity_magnitude * velocity_magnitude / (div * div); + } + let acceleration_magnitude = shear_stress / mass_per_area; + return acceleration_magnitude; +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(texture_dimensions(output_tiles).xy / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let texture_dimensions = textureDimensions(input_release_point_texture); + if id.x >= texture_dimensions.x || id.y >= texture_dimensions.y { + return; + } + // id.xy in [0, texture_dimensions(output_tiles) - 1] + // id.z contains sample index + trajectory_overlay(id); +} \ No newline at end of file diff --git a/webgpu/compute/shaders/buffer_to_texture_compute.wgsl b/webgpu/compute/shaders/buffer_to_texture_compute.wgsl new file mode 100644 index 000000000..3801706be --- /dev/null +++ b/webgpu/compute/shaders/buffer_to_texture_compute.wgsl @@ -0,0 +1,86 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2025 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use color_mapping +///use webgpu::encoder + +// input +@group(0) @binding(0) var settings: BufferToTextureSettings; +@group(0) @binding(1) var input_storage_buffer: array; +@group(0) @binding(2) var input_transparency_buffer: array; + +// output +@group(0) @binding(5) var output_texture: texture_storage_2d; + +struct BufferToTextureSettings { + input_resolution: vec2u, + color_map_bounds: vec2f, + transparency_map_bounds: vec2f, + use_bin_interpolation: u32, + use_transparency_buffer: u32, +} + +fn index_from_pos(pos: vec2u) -> u32 { + return pos.y * settings.input_resolution.x + pos.x; +} + +fn remap_clamped(x: f32, min_val: f32, max_val: f32) -> f32 { + return clamp((x - min_val) / (max_val - min_val), 0.0, 1.0); +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(input_resolution / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + if id.x >= settings.input_resolution.x || id.y >= settings.input_resolution.y { + return; + } + // id.xy in [0, texture_dimensions(output_tiles) - 1] + + let buffer_index = index_from_pos(id.xy); + + const layer_scaling = 1.0; //1000.0; // scaling factor for layer values, to avoid precision loss (also change in avalanche_trajectories_compute) + const calculate_velocity = true; // if true, calculate velocity based on grommis formula + + var alpha: f32 = 1.0; + if bool(settings.use_transparency_buffer) { + let transparency_value = f32(input_transparency_buffer[buffer_index]) / layer_scaling; + alpha = remap_clamped(transparency_value, settings.transparency_map_bounds.x, settings.transparency_map_bounds.y); + } + + var value = f32(input_storage_buffer[buffer_index]) / layer_scaling; + if calculate_velocity { + value = sqrt(value * 2.0 * 9.81); // calculate velocity based on grommis formula + } + var output_color = color_mapping_flowpy(value, settings.color_map_bounds.x, settings.color_map_bounds.y, bool(settings.use_bin_interpolation)); + output_color.a = output_color.a * alpha; + + /* + let risk_value = u32_to_range(read_buffer_at(id.xy), U32_ENCODING_RANGE_NORM); + //let risk_value = f32(input_storage_buffer[buffer_index]) / 1000f; + + var output_color: vec4f; + if (risk_value == 0.0) { + output_color = vec4f(0.0); + } else { + output_color = vec4f(color_mapping_bergfex(risk_value), 1.0); + }*/ + textureStore(output_texture, id.xy, output_color); +} \ No newline at end of file diff --git a/webgpu/compute/shaders/color_mapping.wgsl b/webgpu/compute/shaders/color_mapping.wgsl new file mode 100644 index 000000000..ebc658ed4 --- /dev/null +++ b/webgpu/compute/shaders/color_mapping.wgsl @@ -0,0 +1,90 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// value in [0,1], represents angle from horizontal to vertical +// color map from https://www.bergfex.com/ +// values not in [0,1] are mapped to black +fn color_mapping_bergfex(value: f32) -> vec3f { + if value < 0.0 || value > 1.0 { + return vec3f(0); + } + + let deg = u32(value * 90); + + const bins = array(29, 34, 39, 44, 90); + const colors = array(vec3f(1, 1, 1), vec3f(255.0 / 255.0, 219.0 / 255.0, 17.0 / 255.0), vec3f(255.0 / 255.0, 117.0 / 255.0, 6.0 / 255.0), vec3f(213.0 / 255.0, 0.0 / 255.0, 0.0 / 255.0), vec3f(100.0 / 255.0, 0.0 / 255.0, 213.0 / 255.0)); + + var index = 0; + while deg > bins[index] { + index++; + } + return colors[index]; +} + +// value in [0,1], represents angle from horizontal to vertical +// color map from https://www.openslopemap.org/karte/ +// values not in [0,1] are mapped to black +fn color_mapping_openslopemap(value: f32) -> vec3f { + if value < 0.0 || value > 1.0 { + return vec3f(0); + } + + let deg = u32(value * 90); + + const bins = array(9, 29, 34, 39, 42, 45, 49, 54, 90); + const colors = array(vec3f(254.0 / 255.0, 249.0 / 255.0, 249.0 / 255.0), vec3f(51.0 / 255.0, 249.0 / 255.0, 49.0 / 255.0), vec3f(242.0 / 255.0, 228.0 / 255.0, 44.0 / 255.0), vec3f(255.0 / 255.0, 169.0 / 255.0, 45.0 / 255.0), vec3f(255.0 / 255.0, 48.0 / 255.0, 45.0 / 255.0), vec3f(255.0 / 255.0, 79.0 / 255.0, 249.0 / 255.0), vec3f(183.0 / 255.0, 69.0 / 255.0, 253.0 / 255.0), vec3f(135.0 / 255.0, 44.0 / 255.0, 253.0 / 255.0), vec3f(49.0 / 255.0, 49.0 / 255.0, 253.0 / 255.0)); + + var index = 0; + while deg > bins[index] { + index++; + } + return colors[index]; +} + +// value in [0,1], just use red color channel +fn color_mapping_red(value: f32) -> vec3f { + return vec3f(value, 0, 0); +} + +// the color mapping used in the flowpy paper +fn color_mapping_flowpy(value: f32, min: f32, max: f32, blend: bool) -> vec4f { + const bin_count: u32 = 20u; + + //normalize value to [0,1] + let normalized = (value - min) / (max - min); + + //define the color bins + //in hex: 404043, 454455, 504a6b, 5e4c83, 6f4b96, 7f4f9d, 9055a1, 9f5ba1, b061a1, c0669d, d16b97, e07391, ee7e89, f78e85, fca187, feb38f, fec79c, fed9ab, feecbc, fdfecf + const colors = array(vec4f(0x40 / 255.0, 0x40 / 255.0, 0x43 / 255.0, 1.0), vec4f(0x45 / 255.0, 0x44 / 255.0, 0x55 / 255.0, 1.0), vec4f(0x50 / 255.0, 0x4a / 255.0, 0x6b / 255.0, 1.0), vec4f(0x5e / 255.0, 0x4c / 255.0, 0x83 / 255.0, 1.0), vec4f(0x6f / 255.0, 0x4b / 255.0, 0x96 / 255.0, 1.0), vec4f(0x7f / 255.0, 0x4f / 255.0, 0x9d / 255.0, 1.0), vec4f(0x90 / 255.0, 0x55 / 255.0, 0xa1 / 255.0, 1.0), vec4f(0x9f / 255.0, 0x5b / 255.0, 0xa1 / 255.0, 1.0), vec4f(0xb0 / 255.0, 0x61 / 255.0, 0xa1 / 255.0, 1.0), vec4f(0xc0 / 255.0, 0x66 / 255.0, 0x9d / 255.0, 1.0), vec4f(0xd1 / 255.0, 0x6b / 255.0, 0x97 / 255.0, 1.0), vec4f(0xe0 / 255.0, 0x73 / 255.0, 0x91 / 255.0, 1.0), vec4f(0xee / 255.0, 0x7e / 255.0, 0x89 / 255.0, 1.0), vec4f(0xf7 / 255.0, 0x8e / 255.0, 0x85 / 255.0, 1.0), vec4f(0xfc / 255.0, 0xa1 / 255.0, 0x87 / 255.0, 1.0), vec4f(0xfe / 255.0, 0xb3 / 255.0, 0x8f / 255.0, 1.0), vec4f(0xfe / 255.0, 0xc7 / 255.0, 0x9c / 255.0, 1.0), vec4f(0xfe / 255.0, 0xd9 / 255.0, 0xab / 255.0, 1.0), vec4f(0xfe / 255.0, 0xec / 255.0, 0xbc / 255.0, 1.0), vec4f(0xfd / 255.0, 0xfe / 255.0, 0xcf / 255.0, 1.0)); + + // Calculate bin index and interpolation factor + let pos = normalized * (f32(bin_count) - 1.0); + let index = u32(pos); + let factor = pos - f32(index); + + if !blend || index >= bin_count - 1 { + return colors[index]; + } + else { + // Blend between current and next color + let color1 = colors[index]; + let color2 = colors[index + 1]; + return mix(color1, color2, factor); + } +} \ No newline at end of file diff --git a/webgpu/compute/shaders/compute_release_points.wgsl b/webgpu/compute/shaders/compute_release_points.wgsl new file mode 100644 index 000000000..eb4707ade --- /dev/null +++ b/webgpu/compute/shaders/compute_release_points.wgsl @@ -0,0 +1,106 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2025 Markus Rampp + * Copyright (C) 2025 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::normals_util +///use color_mapping + +///define USE_ROUGHNESS_FILTERING 1 +///define ROUGHNESS_THRESHOLD 0.01 + +// input +@group(0) @binding(0) var settings: ReleasePointSettings; +@group(0) @binding(1) var normals_texture: texture_2d; + +// output +//currently, format r8uint cannot be used for storage texture with write access (apparently only 32 bit formats can be used) +//TODO use storage buffer instead for now! +// ASAP! saves 3 byte per texel (75%) immediately, could optimize further (just 3 bit per texel for current impl) +@group(0) @binding(2) var release_points_texture: texture_storage_2d; // ASSERT: same dimensions as heights_texture + +struct ReleasePointSettings { + min_slope_angle: f32, // in rad + max_slope_angle: f32, // in rad + sampling_interval: vec2u, +} + +fn should_paint(pos: vec2u) -> bool { + return (pos.x % settings.sampling_interval.x == 0) && (pos.y % settings.sampling_interval.y == 0); +} + +///if USE_ROUGHNESS_FILTERING 1 +fn get_roughness(id: vec2u) -> f32 { + // according to doi:10.5194/nhess-16-2211-2016 + if id.x == 0 || id.y == 0 || id.x >= textureDimensions(normals_texture).x - 1 || id.y >= textureDimensions(normals_texture).y - 1 { + return 1.0; // no roughness at borders + } + var idx = 0u; + var r: array; + var r_sum = vec3f(0.0, 0.0, 0.0); + + for (var y = -1; y <= 1; y = y + 1) { + for (var x = -1; x <= 1; x = x + 1) { + let normal = textureLoad(normals_texture, vec2(id) + vec2(x, y), 0).xyz * 2 - 1; + let alpha = acos(normal.z); // slope in rad + let beta = atan2(normal.x, normal.y); // aspect in rad + r[idx].x = sin(alpha) * cos(beta); // x component of roughness vector + r[idx].y = sin(alpha) * sin(beta); // y component of roughness vector + r[idx].z = cos(alpha); // z component of roughness vector + idx = idx + 1u; + } + } + for (var i = 0u; i < 9u; i = i + 1u) { + r_sum = r_sum + r[i]; + } + let r_magnitude = length(r_sum); + let roughness = 1 - r_magnitude / 9.0; + return roughness; // returns 0 for flat terrain, 1 for very rough terrain +} +///endif + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(texture_dimensions(normals_texture).xy / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let texture_size = textureDimensions(release_points_texture); + if id.x >= texture_size.x || id.y >= texture_size.y { + return; + } + // id.xy in [0, texture_dimensions(output_tiles) - 1] + + let tex_pos = id.xy; + let normal = textureLoad(normals_texture, tex_pos, 0).xyz * 2 - 1; + let slope_angle = get_slope_angle(normal); // slope angle in rad (0 flat, pi/2 vertical) + +///if USE_ROUGHNESS_FILTERING 1 + let roughness = get_roughness(tex_pos); + + if slope_angle < settings.min_slope_angle || slope_angle > settings.max_slope_angle || !should_paint(tex_pos) || roughness > ROUGHNESS_THRESHOLD { + ///else + if slope_angle < settings.min_slope_angle || slope_angle > settings.max_slope_angle || !should_paint(tex_pos) { + ///endif + textureStore(release_points_texture, tex_pos, vec4f(0, 0, 0, 0)); + } else { + let color = color_mapping_bergfex(slope_angle / (PI / 2)); + textureStore(release_points_texture, tex_pos, vec4f(color.xyz, 1)); + //textureStore(release_points_texture, tex_pos, vec4f(1, 0, 0, 1)); + } + } diff --git a/webgpu/compute/shaders/height_decode_compute.wgsl b/webgpu/compute/shaders/height_decode_compute.wgsl new file mode 100644 index 000000000..ab54c9628 --- /dev/null +++ b/webgpu/compute/shaders/height_decode_compute.wgsl @@ -0,0 +1,55 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::tile_util + +@group(0) @binding(0) var bounds: RegionBounds; +@group(0) @binding(1) var input_texture: texture_storage_2d; +@group(0) @binding(2) var output_texture: texture_storage_2d; + +struct RegionBounds { + aabb_min: vec2f, + aabb_max: vec2f, +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // Exit if thread id is outside the image dimensions + let output_texture_size = textureDimensions(output_texture); + if id.x >= output_texture_size.x || id.y >= output_texture_size.y { + return; + } + + // Load the RGBA8 color from the input texture + let input: vec4 = textureLoad(input_texture, vec2(id.xy)); + + const scaling_factor: f32 = 8191.875 / 65535.0; + let height_value: f32 = f32((input.r << 8) | input.g) * scaling_factor; + + // calculate altitude correction factor based on latitude + //let pos_y = bounds.aabb_min.y + (bounds.aabb_max.y - bounds.aabb_min.y) * f32(id.y) / f32(output_texture_size.y); + //let altitude_correction_factor = 1 / cos(y_to_lat(pos_y)); + + // moved altitude correction to normals compute node + //TODO: figure out why we dont need corrected altitudes for the alpha runout model (see compute trajectories shader) + const altitude_correction_factor = 1; + + // Write the red channel value to the output texture + textureStore(output_texture, vec2(id.xy), vec4(height_value * altitude_correction_factor, 0.0, 0.0, 0.0)); +} diff --git a/webgpu/compute/shaders/iterative_simulation_compute.wgsl b/webgpu/compute/shaders/iterative_simulation_compute.wgsl new file mode 100644 index 000000000..e2123ac14 --- /dev/null +++ b/webgpu/compute/shaders/iterative_simulation_compute.wgsl @@ -0,0 +1,148 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// input +@group(0) @binding(0) var settings: FlowPySettings; +@group(0) @binding(1) var height_texture: texture_2d; +@group(0) @binding(2) var release_point_texture: texture_2d; +@group(0) @binding(3) var input_parents: array; + +//output +@group(0) @binding(4) var flux: array>; +@group(0) @binding(5) var output_parents: array>; +@group(0) @binding(6) var output_texture: texture_storage_2d; // ASSERT: same dimensions as height_texture + +struct FlowPySettings { + num_iteration: u32, + padding1: u32, + padding2: u32, + padding3: u32, +} + +fn pos_to_index(pos: vec2u) -> u32 { + return pos.y * textureDimensions(height_texture).x + pos.x; +} + +// test model for debugging +fn compute_flow_step_test(pos: vec2u) { + if any(pos < vec2u(1)) || any(pos > textureDimensions(height_texture) - vec2u(1)) { + // we are near border, not all neighbors are defined + return; + } + + if settings.num_iteration == 0 { + let is_release_point = textureLoad(release_point_texture, pos, 0).a > 0; + atomicStore(&flux[pos_to_index(pos)], u32(is_release_point)); + textureStore(output_texture, pos, vec4f(f32(is_release_point), 0, 0, f32(is_release_point))); + } else { + let base_cell_flux = atomicExchange(&flux[pos_to_index(pos)], 0u); + + if base_cell_flux == 0u { + return; + } + + let is_release_point = textureLoad(release_point_texture, pos, 0).a > 0; + textureStore(output_texture, pos, vec4f(f32(is_release_point), 0, 1, 1)); + + // check neighbors + let top_left = vec2u(vec2i(pos) + vec2i(-1, -1)); + let top_center = vec2u(vec2i(pos) + vec2i(0, -1)); + let top_right = vec2u(vec2i(pos) + vec2i(1, -1)); + let center_left = vec2u(vec2i(pos) + vec2i(-1, 0)); + let center_right = vec2u(vec2i(pos) + vec2i(1, 0)); + let bottom_left = vec2u(vec2i(pos) + vec2i(-1, 1)); + let bottom_center = vec2u(vec2i(pos) + vec2i(0, 1)); + let bottom_right = vec2u(vec2i(pos) + vec2i(1, 1)); + + let height_center = textureLoad(height_texture, pos, 0).r; + + let height_top_left = textureLoad(height_texture, top_left, 0).r; + let height_top_center = textureLoad(height_texture, top_center, 0).r; + let height_top_right = textureLoad(height_texture, top_right, 0).r; + let height_center_left = textureLoad(height_texture, center_left, 0).r; + let height_center_right = textureLoad(height_texture, center_right, 0).r; + let height_bottom_left = textureLoad(height_texture, bottom_left, 0).r; + let height_bottom_center = textureLoad(height_texture, bottom_center, 0).r; + let height_bottom_right = textureLoad(height_texture, bottom_right, 0).r; + + if height_center > height_top_left { + atomicAdd(&flux[pos_to_index(top_left)], base_cell_flux); + } + if height_center > height_top_center { + atomicAdd(&flux[pos_to_index(top_center)], base_cell_flux); + } + if height_center > height_top_right { + atomicAdd(&flux[pos_to_index(top_right)], base_cell_flux); + } + if height_center > height_center_left { + atomicAdd(&flux[pos_to_index(center_left)], base_cell_flux); + } + if height_center > height_center_right { + atomicAdd(&flux[pos_to_index(center_right)], base_cell_flux); + } + if height_center > height_bottom_left { + atomicAdd(&flux[pos_to_index(bottom_left)], base_cell_flux); + } + if height_center > height_bottom_center { + atomicAdd(&flux[pos_to_index(bottom_center)], base_cell_flux); + } + if height_center > height_bottom_right { + atomicAdd(&flux[pos_to_index(bottom_right)], base_cell_flux); + } + } +} + +fn compute_flow_step(pos: vec2u) { + // Computes a single step of FlowPy + // FlowPy usually computes the flow path from all starting cells separately. It then adds cells to the flow path serially. + // We, on the other hand, on each step TODO + + //TODO basically do what avaframe.com4FlowPy.flowClass, method calc_distribution() does + + let parents = input_parents[pos_to_index(pos)]; + if parents == 0 { + return; + } + + for (var i = 0u; i < 8u; i++) { + let is_parent_set = parents & (1u << i); + //if (is_parent_set) { + // last iteration, some cell added itself as parent of this cell + //} + } + + //TODO calc z delta for each neighbor + //TODO calc persistence + //TODO calc tan beta + //TODO (if not starting cell) calc fp travel angle + //TODO (if not starting cell) calc sl travel angle +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(texture_dimensions(normals_texture).xy / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let texture_size = textureDimensions(height_texture); + if id.x >= texture_size.x || id.y >= texture_size.y { + return; + } + // id.xy in [0, texture_dimensions(output_tiles) - 1] + + compute_flow_step_test(id.xy); +} diff --git a/webgpu/compute/shaders/normals_compute.wgsl b/webgpu/compute/shaders/normals_compute.wgsl new file mode 100644 index 000000000..274715ac4 --- /dev/null +++ b/webgpu/compute/shaders/normals_compute.wgsl @@ -0,0 +1,61 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::tile_util +///use webgpu::normals_util + +// input +@group(0) @binding(0) var bounds: RegionBounds; +@group(0) @binding(1) var heights_texture: texture_2d; + +// output +@group(0) @binding(2) var normals_texture: texture_storage_2d; // ASSERT: same dimensions as heights_texture + +struct RegionBounds { + aabb_min: vec2f, + aabb_max: vec2f, +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(texture_dimensions(normals_texture).xy / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let texture_size = textureDimensions(normals_texture); + if id.x >= texture_size.x || id.y >= texture_size.y { + return; + } + let texture_pos: vec2u = id.xy; // in [0, texture_dimensions(normals_texture) - 1] + + // calculate width and height of input height texture in world space + let bounds_width: f32 = bounds.aabb_max.x - bounds.aabb_min.x; + let bounds_height: f32 = bounds.aabb_max.y - bounds.aabb_min.y; + + // calculate width and height of a single input texel (quad) + let quad_width: f32 = bounds_width / f32(texture_size.x - 1); + let quad_height: f32 = bounds_height / f32(texture_size.y - 1); + + // calculate altitude correction factor based on latitude + let pos_y = bounds.aabb_min.y + (bounds.aabb_max.y - bounds.aabb_min.y) * f32(id.y) / f32(texture_size.y); + let altitude_correction_factor = 1 / cos(y_to_lat(pos_y)); + + // calculate normal for position texture_pos from height texture and store in normal texture + let normal = normal_by_finite_difference_method_texture_f32(texture_pos, quad_width, quad_height, altitude_correction_factor, heights_texture); + textureStore(normals_texture, texture_pos, vec4f(0.5 * normal + 0.5, 1)); +} diff --git a/webgpu/compute/shaders/random.wgsl b/webgpu/compute/shaders/random.wgsl new file mode 100644 index 000000000..491c0ea98 --- /dev/null +++ b/webgpu/compute/shaders/random.wgsl @@ -0,0 +1,54 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// Inspired by Path Tracer Compute Toy @https://compute.toys/view/1525 + +var m_seed: vec4u; + +fn seed(seed: vec4u) { m_seed = seed; } + +fn PCG(x: vec4u) -> vec4u { + var v = x; + + v = v * vec4(1664525) + vec4(1013904223); + + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + + v ^= v >> vec4(16); + + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + + return v; +} + +fn rand4() -> vec4f { + m_seed = PCG(m_seed); + return vec4f(m_seed) / exp2(32); +} + +fn rand() -> f32 { return rand4().x; } + +fn rand2() -> vec2f { return rand4().xy; } + +fn rand3() -> vec3f { return rand4().xyz; } \ No newline at end of file diff --git a/webgpu/compute/shaders/snow_compute.wgsl b/webgpu/compute/shaders/snow_compute.wgsl new file mode 100644 index 000000000..be18a0da8 --- /dev/null +++ b/webgpu/compute/shaders/snow_compute.wgsl @@ -0,0 +1,76 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::tile_util +///use tile_hashmap +///use webgpu::normals_util +///use webgpu::snow + +struct SnowSettings { + angle: vec4f, + alt: vec4f, +} + +struct RegionBounds { + aabb_min: vec2f, + aabb_max: vec2f, +} + +// input +@group(0) @binding(0) var snow_settings: SnowSettings; +@group(0) @binding(1) var bounds: RegionBounds; +@group(0) @binding(2) var normal_texture: texture_2d; +@group(0) @binding(3) var height_texture: texture_2d; + +// output +@group(0) @binding(4) var snow_texture: texture_storage_2d; // ASSERT: same dimensions as heights_texture + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.xy in [0, ceil(texture_dimensions(snow_texture).xy / workgroup_size.xy) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let texture_size = textureDimensions(snow_texture); + if id.x >= texture_size.x || id.y >= texture_size.y { + return; + } + // get texture pos and uv + let col = id.x; // in [0, texture_dimension(output_tiles).x - 1] + let row = id.y; // in [0, texture_dimension(output_tiles).y - 1] + let texture_pos = vec2u(col, row); + let uv = vec2f(f32(col), f32(row)) / vec2f(texture_size - 1); + + // calculate width and height of input height texture in world space + let bounds_width: f32 = bounds.aabb_max.x - bounds.aabb_min.x; + let bounds_height: f32 = bounds.aabb_max.y - bounds.aabb_min.y; + + // calculate x and y world positions + let pos_x = bounds.aabb_min.x + bounds_width * uv.x; + let pos_y = bounds.aabb_min.y + bounds_height * uv.y; + + // read normal and height (z world position) + let normal: vec3f = textureLoad(normal_texture, texture_pos, 0).xyz; + let altitude_correction_factor = 1 / cos(y_to_lat(pos_y)); //TODO currently, height decode node does no altitude correciton, so we need to account for that here + let pos_z: f32 = altitude_correction_factor * textureLoad(height_texture, texture_pos, 0).x; + + // compute snow and store in texture + let snow = overlay_snow(normal, vec3f(pos_x, pos_y, pos_z), snow_settings.angle, snow_settings.alt); + textureStore(snow_texture, texture_pos, snow); +} diff --git a/webgpu/compute/shaders/tile_hashmap.wgsl b/webgpu/compute/shaders/tile_hashmap.wgsl new file mode 100644 index 000000000..49c7e983c --- /dev/null +++ b/webgpu/compute/shaders/tile_hashmap.wgsl @@ -0,0 +1,114 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::filtering + +fn hash_tile_id_vec(id: vec3) -> u32 { + let z = id.z * 46965u + 10859u; + let x = id.x * 60197u + 12253u; + let y = id.y * 62117u + 59119u; + return (x + y + z) & 65535u; +} + +fn hash_tile_id(id: TileId) -> u32 { + return hash_tile_id_vec(vec3(id.x, id.y, id.zoomlevel)); +} + +fn get_texture_array_index(tile_id: TileId, texture_array_index: ptr, map_key_buffer: ptr>, map_value_buffer: ptr>) -> bool { + // find correct hash for tile id + var hash = hash_tile_id(tile_id); + while !tile_ids_equal(map_key_buffer[hash], tile_id) && !tile_id_empty(map_key_buffer[hash]) { + hash++; + } + + let was_found = !tile_id_empty(map_key_buffer[hash]); + if was_found { + // retrieve array layer by hash + *texture_array_index = map_value_buffer[hash]; + } else { + *texture_array_index = 0; + } + return was_found; +} + +fn get_neighboring_tile_id_and_pos(texture_size: u32, tile_id: TileId, pos: vec2, out_tile_id: ptr, out_pos: ptr>) { + var new_pos = pos; + var new_tile_id = tile_id; + + // tiles overlap! in each direction, the last value of one tile equals the first of the next + // therefore offset by 1 in respective direction + if new_pos.x == -1 { + new_pos.x = i32(texture_size - 2); + new_tile_id.x -= 1; + } else if new_pos.x == i32(texture_size) { + new_pos.x = 1; + new_tile_id.x += 1; + } + if new_pos.y == -1 { + new_pos.y = i32(texture_size - 2); + new_tile_id.y += 1; + } else if new_pos.y == i32(texture_size) { + new_pos.y = 1; + new_tile_id.y -= 1; + } + + *out_tile_id = new_tile_id; + *out_pos = vec2u(new_pos); +} + +// currently unused +fn sample_height_by_uv( + tile_id: TileId, + uv: vec2f, + map_key_buffer: ptr>, + map_value_buffer: ptr>, + height_tiles_texture: texture_2d_array, + height_tiles_sampler: sampler +) -> u32 { + var texture_array_index: u32; + let found = get_texture_array_index(tile_id, &texture_array_index, map_key_buffer, map_value_buffer); + return select(0, bilinear_sample_u32(height_tiles_texture, height_tiles_sampler, uv, texture_array_index), found); +} + +fn load_height_by_position( + tile_id: TileId, + pos: vec2u, + map_key_buffer: ptr>, + map_value_buffer: ptr>, + height_textures: texture_2d_array +) -> u32 { + var texture_array_index: u32; + let found = get_texture_array_index(tile_id, &texture_array_index, map_key_buffer, map_value_buffer); + return select(0, textureLoad(height_textures, pos, texture_array_index, 0).r, found); +} + +fn load_height_with_neighbors( + tile_id: TileId, + pos: vec2i, + map_key_buffer: ptr>, + map_value_buffer: ptr>, + height_tiles_texture: texture_2d_array, +) -> u32 { + let height_texture_size = textureDimensions(height_tiles_texture); + var target_tile_id: TileId; + var target_pos: vec2u; + get_neighboring_tile_id_and_pos(height_texture_size.x, tile_id, pos, &target_tile_id, &target_pos); + + return load_height_by_position(target_tile_id, target_pos, map_key_buffer, map_value_buffer, height_tiles_texture); +} diff --git a/webgpu/compute/shaders/upsample_textures_compute.wgsl b/webgpu/compute/shaders/upsample_textures_compute.wgsl new file mode 100644 index 000000000..3b238b898 --- /dev/null +++ b/webgpu/compute/shaders/upsample_textures_compute.wgsl @@ -0,0 +1,53 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::filtering +///use webgpu::tile_util + +// input +@group(0) @binding(0) var indices: array; +@group(0) @binding(1) var input_textures: texture_2d_array; +@group(0) @binding(2) var input_sampler: sampler; + +// output +@group(0) @binding(3) var output_textures: texture_storage_2d_array; + +@compute @workgroup_size(1, 16, 16) +fn computeMain(@builtin(global_invocation_id) id: vec3) { + // id.x in [0, num_textures] + // id.yz in [0, ceil(texture_dimensions(output_tiles).xy / workgroup_size.yz) - 1] + + // exit if thread id is outside image dimensions (i.e. thread is not supposed to be doing any work) + let output_texture_size: vec2u = textureDimensions(output_textures); + if id.y >= output_texture_size.x || id.z >= output_texture_size.y { + return; + } + // id.yz in [0, texture_dimensions(output_tiles) - 1] + + let texture_layer_index = indices[id.x]; + + let col = id.y; // in [0, texture_dimension(output_tiles).x - 1] + let row = id.z; // in [0, texture_dimension(output_tiles).y - 1] + let input_texture_size: vec2u = textureDimensions(input_textures); + let uv = vec2f(f32(col), f32(row)) / vec2f(output_texture_size - 1); + let result = bilinear_sample_vec4f(input_textures, input_sampler, uv, texture_layer_index); + + textureStore(output_textures, vec2(col, row), texture_layer_index, result); +} diff --git a/webgpu/engine/CMakeLists.txt b/webgpu/engine/CMakeLists.txt new file mode 100644 index 000000000..aa29438be --- /dev/null +++ b/webgpu/engine/CMakeLists.txt @@ -0,0 +1,100 @@ +############################################################################# +# Alpine Terrain Renderer +# Copyright (C) 2024 Adam Celarek +# Copyright (C) 2024 Gerald Kimmersdorfer +# Copyright (C) 2024 Patrick Komon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# + +option(ALP_ENABLE_WGSL_MINIFICATION "Minify WGSL shaders to reduce size" OFF) + +project(alpine-renderer-webgpu_engine LANGUAGES C CXX) + + +set(SOURCES + Window.h Window.cpp + UniformBufferObjects.h + tile_mesh/TileMeshRenderer.h tile_mesh/TileMeshRenderer.cpp + cloud/CloudRenderer.h cloud/CloudRenderer.cpp + track/TrackRenderer.h track/TrackRenderer.cpp + atmosphere/AtmosphereRenderer.h atmosphere/AtmosphereRenderer.cpp + overlay/OverlayRenderer.h overlay/OverlayRenderer.cpp + overlay/Overlay.h + overlay/HeightLinesOverlay.h overlay/HeightLinesOverlay.cpp + overlay/ScreenSpaceSnowOverlay.h overlay/ScreenSpaceSnowOverlay.cpp + overlay/TextureOverlay.h overlay/TextureOverlay.cpp + overlay/TileDebugOverlay.h overlay/TileDebugOverlay.cpp + Context.h Context.cpp +) + +qt_add_library(webgpu_engine STATIC ${SOURCES}) + +if (ALP_ENABLE_WGSL_MINIFICATION) + set(SHADER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") + set(SHADER_MINIFIED_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders_minified") + set(MINIFY_SCRIPT "${CMAKE_SOURCE_DIR}/misc/scripts/minify_shaders.py") + + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + execute_process( + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${SHADER_SOURCE_DIR}" "${SHADER_MINIFIED_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE MINIFY_RESULT + ) + if(NOT MINIFY_RESULT EQUAL 0) + message(FATAL_ERROR "Shader minification failed") + endif() + + file(GLOB_RECURSE SHADER_SOURCES "${SHADER_SOURCE_DIR}/*.wgsl") + add_custom_target(minify_shaders + COMMAND ${Python3_EXECUTABLE} "${MINIFY_SCRIPT}" "${SHADER_SOURCE_DIR}" "${SHADER_MINIFIED_DIR}" + DEPENDS ${SHADER_SOURCES} ${MINIFY_SCRIPT} + COMMENT "Minifying shaders" + VERBATIM + ) + add_dependencies(webgpu_engine minify_shaders) + + set(SHADER_BASE_DIR "${SHADER_MINIFIED_DIR}") + target_compile_definitions(webgpu_engine PUBLIC ALP_ENABLE_WGSL_MINIFICATION) +else() + set(SHADER_BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders") +endif() + +qt_add_resources(webgpu_engine "shaders" + PREFIX "/shaders/webgpu_engine" + BASE "${SHADER_BASE_DIR}" + FILES + "${SHADER_BASE_DIR}/render_tiles.wgsl" + "${SHADER_BASE_DIR}/render_atmosphere.wgsl" + "${SHADER_BASE_DIR}/render_lines.wgsl" + "${SHADER_BASE_DIR}/compose_pass.wgsl" + "${SHADER_BASE_DIR}/screen_pass_vert.wgsl" + "${SHADER_BASE_DIR}/render_clouds.wgsl" + "${SHADER_BASE_DIR}/upscale_clouds.wgsl" + + "${SHADER_BASE_DIR}/overlays/height_lines.wgsl" + "${SHADER_BASE_DIR}/overlays/texture_overlay.wgsl" + "${SHADER_BASE_DIR}/overlays/gbuffer_debug.wgsl" + "${SHADER_BASE_DIR}/overlays/screen_space_snow.wgsl" + + "${SHADER_BASE_DIR}/util/atmosphere.wgsl" + "${SHADER_BASE_DIR}/util/camera_config.wgsl" + "${SHADER_BASE_DIR}/util/shared_config.wgsl" + ) + +target_link_libraries(webgpu_engine PUBLIC nucleus Qt::Core webgpu) +target_include_directories(webgpu_engine PRIVATE .) + +target_compile_definitions(webgpu_engine PUBLIC ALP_SHADER_DIR_WEBGPU_ENGINE="${CMAKE_CURRENT_SOURCE_DIR}/shaders/") diff --git a/webgpu/engine/Context.cpp b/webgpu/engine/Context.cpp new file mode 100644 index 000000000..99a8aab63 --- /dev/null +++ b/webgpu/engine/Context.cpp @@ -0,0 +1,241 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Context.h" + +#include + +namespace webgpu_engine { + +Context::Context(QObject* parent) + : nucleus::EngineContext(parent) +{ +} + +Context::~Context() = default; + +void Context::internal_initialise() +{ + assert(m_webgpu_ctx_ptr != nullptr); + + auto& reg = webgpu_ctx().resource_registry(); + reg.set_local_shader_path("webgpu", ALP_SHADER_DIR_WEBGPU); + reg.set_local_shader_path("webgpu_engine", ALP_SHADER_DIR_WEBGPU_ENGINE); + + reg.register_bind_group_layout("shared_config", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry entry {}; + entry.binding = 0; + entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment | WGPUShaderStage_Compute; + entry.buffer.type = WGPUBufferBindingType_Uniform; + entry.buffer.minBindingSize = 0; + return std::make_unique(device, std::vector { entry }, "shared config bind group layout"); + }); + + reg.register_bind_group_layout("camera", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry entry {}; + entry.binding = 0; + entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment | WGPUShaderStage_Compute; + entry.buffer.type = WGPUBufferBindingType_Uniform; + entry.buffer.minBindingSize = 0; + return std::make_unique(device, std::vector { entry }, "camera bind group layout"); + }); + + reg.register_bind_group_layout("depth_texture", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry entry {}; + entry.binding = 0; + entry.visibility = WGPUShaderStage_Fragment | WGPUShaderStage_Compute; + entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + entry.texture.viewDimension = WGPUTextureViewDimension_2D; + return std::make_unique(device, std::vector { entry }, "depth texture bind group layout"); + }); + + reg.register_bind_group_layout("compose", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry albedo_entry {}; + albedo_entry.binding = 0; + albedo_entry.visibility = WGPUShaderStage_Fragment; + albedo_entry.texture.sampleType = WGPUTextureSampleType_Uint; + albedo_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry position_entry {}; + position_entry.binding = 1; + position_entry.visibility = WGPUShaderStage_Fragment; + position_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + position_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry normal_entry {}; + normal_entry.binding = 2; + normal_entry.visibility = WGPUShaderStage_Fragment; + normal_entry.texture.sampleType = WGPUTextureSampleType_Uint; + normal_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry atmosphere_entry {}; + atmosphere_entry.binding = 3; + atmosphere_entry.visibility = WGPUShaderStage_Fragment; + atmosphere_entry.texture.sampleType = WGPUTextureSampleType_Float; + atmosphere_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry overlay_entry {}; + overlay_entry.binding = 4; + overlay_entry.visibility = WGPUShaderStage_Fragment; + overlay_entry.texture.sampleType = WGPUTextureSampleType_Uint; + overlay_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry clouds_texture_entry {}; + clouds_texture_entry.binding = 5; + clouds_texture_entry.visibility = WGPUShaderStage_Fragment; + clouds_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + clouds_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry clouds_depth_entry {}; + clouds_depth_entry.binding = 6; + clouds_depth_entry.visibility = WGPUShaderStage_Fragment; + clouds_depth_entry.storageTexture.access = WGPUStorageTextureAccess_ReadOnly; + clouds_depth_entry.storageTexture.format = WGPUTextureFormat_R32Float; + clouds_depth_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry shadow_texture_entry {}; + shadow_texture_entry.binding = 7; + shadow_texture_entry.visibility = WGPUShaderStage_Fragment; + shadow_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + shadow_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry shadow_sampler_entry {}; + shadow_sampler_entry.binding = 8; + shadow_sampler_entry.visibility = WGPUShaderStage_Fragment; + shadow_sampler_entry.sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutEntry depth_texture_entry {}; + depth_texture_entry.binding = 9; + depth_texture_entry.visibility = WGPUShaderStage_Fragment; + depth_texture_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + depth_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry overlay_renderer_post_entry {}; + overlay_renderer_post_entry.binding = 10; + overlay_renderer_post_entry.visibility = WGPUShaderStage_Fragment; + overlay_renderer_post_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + overlay_renderer_post_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry overlay_renderer_pre_entry {}; + overlay_renderer_pre_entry.binding = 11; + overlay_renderer_pre_entry.visibility = WGPUShaderStage_Fragment; + overlay_renderer_pre_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + overlay_renderer_pre_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { + albedo_entry, + position_entry, + normal_entry, + atmosphere_entry, + overlay_entry, + clouds_texture_entry, + clouds_depth_entry, + shadow_texture_entry, + shadow_sampler_entry, + depth_texture_entry, + overlay_renderer_post_entry, + overlay_renderer_pre_entry, + }, + "compose bind group layout"); + }); + + if (m_tile_mesh_renderer) + m_tile_mesh_renderer->init(webgpu_ctx()); + if (m_atmosphere_renderer) + m_atmosphere_renderer->init(webgpu_ctx()); + if (m_cloud_renderer) + m_cloud_renderer->init(webgpu_ctx()); + if (m_overlay_renderer) + m_overlay_renderer->init(*this); + if (m_track_renderer) + m_track_renderer->init(webgpu_ctx()); + // if (m_ortho_layer) + // m_ortho_layer->init(); +} + +void Context::internal_destroy() +{ + // this is necessary for a clean shutdown (and we want a clean shutdown for the ci integration test). + // m_ortho_layer.reset(); + m_track_renderer.reset(); + m_overlay_renderer.reset(); + m_cloud_renderer.reset(); + m_atmosphere_renderer.reset(); + m_tile_mesh_renderer.reset(); +} + +TileMeshRenderer* Context::tile_mesh_renderer() const { return m_tile_mesh_renderer.get(); } + +void Context::set_tile_mesh_renderer(std::shared_ptr new_tile_mesh_renderer) +{ + assert(!is_alive()); // only set before init is called. + m_tile_mesh_renderer = std::move(new_tile_mesh_renderer); +} + +CloudRenderer* Context::cloud_renderer() const { return m_cloud_renderer.get(); } + +void Context::set_cloud_renderer(std::shared_ptr new_cloud_renderer) +{ + assert(!is_alive()); // only set before init is called. + m_cloud_renderer = std::move(new_cloud_renderer); +} + +AtmosphereRenderer* Context::atmosphere_renderer() const { return m_atmosphere_renderer.get(); } + +void Context::set_atmosphere_renderer(std::shared_ptr new_atmosphere_renderer) +{ + assert(!is_alive()); // only set before init is called. + m_atmosphere_renderer = std::move(new_atmosphere_renderer); +} + +OverlayRenderer* Context::overlay_renderer() const { return m_overlay_renderer.get(); } + +void Context::set_overlay_renderer(std::shared_ptr new_overlay_renderer) +{ + assert(!is_alive()); // only set before init is called. + m_overlay_renderer = std::move(new_overlay_renderer); +} + +TrackRenderer* Context::track_renderer() const { return m_track_renderer.get(); } + +void Context::set_track_renderer(std::shared_ptr new_track_renderer) +{ + assert(!is_alive()); // only set before init is called. + m_track_renderer = std::move(new_track_renderer); +} + +void Context::set_webgpu_ctx(webgpu::Context& ctx) { m_webgpu_ctx_ptr = &ctx; } + +nucleus::track::Manager* Context::track_manager() { return nullptr; } + +uboSharedConfig& Context::shared_config() { return m_shared_config; } + +void Context::request_redraw() { emit redraw_requested(); } + +/*TextureLayer* Context::ortho_layer() const { return m_ortho_layer.get(); } + +void Context::set_ortho_layer(std::shared_ptr new_ortho_layer) +{ + assert(!is_alive()); // only set before init is called. + m_ortho_layer = std::move(new_ortho_layer); +}*/ + +} // namespace webgpu_engine diff --git a/webgpu/engine/Context.h b/webgpu/engine/Context.h new file mode 100644 index 000000000..df1e9cd4e --- /dev/null +++ b/webgpu/engine/Context.h @@ -0,0 +1,88 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "UniformBufferObjects.h" +#include "atmosphere/AtmosphereRenderer.h" +#include "cloud/CloudRenderer.h" +#include "nucleus/EngineContext.h" +#include "nucleus/track/Manager.h" +#include "overlay/OverlayRenderer.h" +#include "tile_mesh/TileMeshRenderer.h" +#include "track/TrackRenderer.h" +#include + +namespace webgpu_engine { + +class Context : public nucleus::EngineContext { + Q_OBJECT +public: + Context(QObject* parent = nullptr); + Context(Context const&) = delete; + ~Context() override; + void operator=(Context const&) = delete; + + TileMeshRenderer* tile_mesh_renderer() const; + void set_tile_mesh_renderer(std::shared_ptr new_tile_mesh_renderer); + + CloudRenderer* cloud_renderer() const; + void set_cloud_renderer(std::shared_ptr new_cloud_renderer); + + AtmosphereRenderer* atmosphere_renderer() const; + void set_atmosphere_renderer(std::shared_ptr new_atmosphere_renderer); + + OverlayRenderer* overlay_renderer() const; + void set_overlay_renderer(std::shared_ptr new_overlay_renderer); + + TrackRenderer* track_renderer() const; + void set_track_renderer(std::shared_ptr new_track_renderer); + + webgpu::Context& webgpu_ctx() { return *m_webgpu_ctx_ptr; } + void set_webgpu_ctx(webgpu::Context& ctx); + + nucleus::track::Manager* track_manager() override; + + uboSharedConfig& shared_config(); + void request_redraw(); + + // TODO: add after getting merge to work + // TextureLayer* ortho_layer() const; + // void set_ortho_layer(std::shared_ptr new_ortho_layer); + +signals: + void redraw_requested(); + +protected: + void internal_initialise() override; + void internal_destroy() override; + +private: + webgpu::Context* m_webgpu_ctx_ptr = nullptr; + uboSharedConfig m_shared_config; + std::shared_ptr m_tile_mesh_renderer; + std::shared_ptr m_cloud_renderer; + std::shared_ptr m_atmosphere_renderer; + std::shared_ptr m_overlay_renderer; + std::shared_ptr m_track_renderer; + // std::shared_ptr m_ortho_layer; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/UniformBufferObjects.h b/webgpu/engine/UniformBufferObjects.h new file mode 100644 index 000000000..17be697ab --- /dev/null +++ b/webgpu/engine/UniformBufferObjects.h @@ -0,0 +1,74 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ +#pragma once + +#include +#include + +// STD140 ALIGNMENT! USE PADDING IF NECESSARY. EVERY BLOCK OF SAME TYPE MUST BE PADDED TO 16 BYTE! +// Also: stay away from vec3 (alignment pitfalls), and don't use bool (use 32-bit types instead). +namespace webgpu_engine { + +struct uboSharedConfig { +public: + // rgb...Color, a...intensity + glm::vec4 m_sun_light = glm::vec4(1.0, 1.0, 1.0, 0.2); + // The direction of the light/sun in WS (northwest lighting at 45 degrees) + glm::vec4 m_sun_light_dir = glm::normalize(glm::vec4(1.0, -1.0, -1.0, 0.0)); + // rgb...Color, a...intensity + glm::vec4 m_amb_light = glm::vec4(1.0, 1.0, 1.0, 0.5); + // rgba...Color of the phong-material (if a 0 -> ortho picture) + glm::vec4 m_material_color = glm::vec4(1.0, 1.0, 1.0, 0.0); + // amb, diff, spec, shininess + glm::vec4 m_material_light_response = glm::vec4(1.5, 3.0, 0.0, 32.0); + + uint32_t m_atmosphere_enabled = true; + uint32_t m_clouds_enabled = true; + uint32_t m_shading_enabled = true; + uint32_t m_normal_mode = 2; // 0...none, 1...per fragment, 2...FDM + + uint32_t m_overlay_mode = 0; // per-tile debug data packed into GBuffer slot 3 (see TileDebugOverlay) + uint32_t m_track_render_mode = 1; // 0...none, 1...without depth test, 2...with depth test, 3 semi-transparent if behind terrain + uint32_t m_padding0 = 0; // std140: pad the trailing scalar block to a 16-byte boundary + uint32_t m_padding1 = 0; +}; + +struct uboCameraConfig { + // Camera Position + glm::vec4 position; + // Camera View Matrix + glm::mat4 view_matrix; + // Camera Projection Matrix + glm::mat4 proj_matrix; + // Camera View-Projection Matrix + glm::mat4 view_proj_matrix; + // Camera Inverse View-Projection Matrix + glm::mat4 inv_view_proj_matrix; + // Camera Inverse View Matrix + glm::mat4 inv_view_matrix; + // Camera Inverse Projection Matrix + glm::mat4 inv_proj_matrix; + // Viewport Size in Pixel + glm::vec2 viewport_size; + // the distance scaling factor of the camera + float distance_scaling_factor; + float buffer2; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/Window.cpp b/webgpu/engine/Window.cpp new file mode 100644 index 000000000..d0201cb61 --- /dev/null +++ b/webgpu/engine/Window.cpp @@ -0,0 +1,373 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2025 Markus Rampp + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Window.h" +#include "nucleus/tile/drawing.h" +#include "overlay/OverlayRenderer.h" +#include "webgpu/base/raii/RenderPassEncoder.h" +#include "webgpu/engine/Context.h" +#include +#include +#include + +#include + +namespace webgpu_engine { + +Window::Window() { } + +Window::~Window() { } + +void Window::set_context(Context* context) +{ + m_context = context; + connect(m_context, &Context::redraw_requested, this, &Window::request_redraw); +} + +void Window::initialise_gpu() +{ + assert(m_context != nullptr); // just make sure that context is set + + create_buffers(); + + auto& reg = m_context->webgpu_ctx().resource_registry(); + reg.register_shader("compose_pass", "webgpu_engine::compose_pass"); + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + webgpu::FramebufferFormat format {}; + format.depth_format = WGPUTextureFormat_Depth24Plus; + format.color_formats.emplace_back(m_context->webgpu_ctx().surface_texture_format()); + m_compose_pipeline = std::make_unique(dev, + reg.shader("compose_pass"), + reg.shader("compose_pass"), + std::vector {}, + format, + std::vector { + ®.bind_group_layout("shared_config"), + ®.bind_group_layout("camera"), + ®.bind_group_layout("compose"), + }); + }); + + m_context->webgpu_ctx().resource_registry().recreate_all(m_context->webgpu_ctx().device()); + + create_bind_groups(); + + m_shadow_texture = create_shadow_texture(1, 1, 1); +} + +std::unique_ptr Window::create_shadow_texture(uint32_t width, uint32_t height, uint32_t mip_levels) +{ + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "shadow texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = mip_levels; + texture_desc.sampleCount = 1; + texture_desc.format = WGPUTextureFormat::WGPUTextureFormat_R16Float; + texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "shadow sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + sampler_desc.maxAnisotropy = 1.0; + + return std::make_unique(m_context->webgpu_ctx().device(), texture_desc, sampler_desc); +} + +void Window::on_shadow_texture_updated(const QByteArray& data) +{ + ktxTexture* ktx_texture; + KTX_error_code result = ktxTexture_CreateFromMemory( + reinterpret_cast(data.constData()), data.size(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktx_texture); + + if (result != KTX_SUCCESS) { + qWarning() << "Failed to create ktx texture from memory"; + return; + } + + m_shadow_texture = create_shadow_texture(ktx_texture->baseWidth, ktx_texture->baseHeight, ktx_texture->numLevels); + + size_t level_0_size = ktxTexture_GetLevelSize(ktx_texture, 0); + size_t level_0_offset = 0; + ktxTexture_GetImageOffset(ktx_texture, 0, 0, 0, &level_0_offset); + std::span byte_span { ktxTexture_GetData(ktx_texture) + level_0_offset, level_0_size }; + + WGPUTexelCopyTextureInfo image_copy_texture {}; + image_copy_texture.texture = m_shadow_texture->texture().handle(); + image_copy_texture.aspect = WGPUTextureAspect::WGPUTextureAspect_All; + image_copy_texture.mipLevel = 0; + image_copy_texture.origin = WGPUOrigin3D { 0, 0, 0 }; + + WGPUTexelCopyBufferLayout texture_data_layout {}; + texture_data_layout.bytesPerRow = 2 * ktx_texture->baseWidth; + texture_data_layout.rowsPerImage = ktx_texture->baseHeight; + texture_data_layout.offset = 0; + + WGPUExtent3D copy_extent { ktx_texture->baseWidth, ktx_texture->baseHeight, 1 }; + wgpuQueueWriteTexture(m_context->webgpu_ctx().queue(), &image_copy_texture, byte_span.data(), byte_span.size_bytes(), &texture_data_layout, ©_extent); + + ktxTexture_Destroy(ktx_texture); + + recreate_compose_bind_group(); +} + +void Window::resize_framebuffer(int w, int h) +{ + m_swapchain_size = glm::vec2(w, h); + + m_gbuffer_format = webgpu::FramebufferFormat(m_context->tile_mesh_renderer()->render_tiles_pipeline().framebuffer_format()); + m_gbuffer_format.size = glm::uvec2 { w, h }; + m_gbuffer = std::make_unique(m_context->webgpu_ctx().device(), m_gbuffer_format); + + m_context->atmosphere_renderer()->resize(w, h); + + m_depth_texture_bind_group = std::make_unique(m_context->webgpu_ctx().device(), + m_context->webgpu_ctx().resource_registry().bind_group_layout("depth_texture"), + std::initializer_list { + m_gbuffer->depth_texture_view().create_bind_group_entry(0), // depth + }); + + m_context->cloud_renderer()->resize(w, h); + m_context->overlay_renderer()->resize(w, h); + + recreate_compose_bind_group(); // Do late +} + +void Window::paint(webgpu::Framebuffer* framebuffer, WGPUCommandEncoder command_encoder) +{ + m_needs_redraw = false; + + // ToDo only update on change? + m_shared_config_ubo->data = m_context->shared_config(); + m_shared_config_ubo->update_gpu_data(m_context->webgpu_ctx().queue()); + + // render atmosphere to color buffer + m_context->atmosphere_renderer()->draw(command_encoder, m_camera_bind_group->handle()); + + // render tiles to geometry buffers + { + std::unique_ptr render_pass = m_gbuffer->begin_render_pass(command_encoder); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 0, m_shared_config_bind_group->handle(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 1, m_camera_bind_group->handle(), 0, nullptr); + + using namespace nucleus::tile; + const auto draw_list = drawing::compute_bounds( + drawing::limit(drawing::generate_list(m_camera, m_context->aabb_decorator(), m_max_zoom_level), 1024), m_context->aabb_decorator()); + const auto culled_draw_list = drawing::sort(drawing::cull(draw_list, m_camera), m_camera.position()); + + m_context->tile_mesh_renderer()->draw(render_pass->handle(), m_camera, culled_draw_list); + } + + // render clouds + if (m_context->shared_config().m_clouds_enabled) { + m_context->cloud_renderer()->draw( + command_encoder, m_depth_texture_bind_group->handle(), m_shared_config_bind_group->handle(), m_camera, m_paint_number); + m_needs_redraw |= m_context->cloud_renderer()->needs_redraw(); // Repaint for TAAU + } + + // render overlay textures (height lines, tile debug, etc.) + m_context->overlay_renderer()->draw(command_encoder, + m_gbuffer->color_texture_view(1), + m_gbuffer->color_texture_view(2), + m_gbuffer->color_texture_view(3), + m_shared_config_bind_group->handle(), + m_camera_bind_group->handle()); + + // render geometry buffers to target framebuffer + { + std::unique_ptr render_pass = framebuffer->begin_render_pass(command_encoder); + wgpuRenderPassEncoderSetPipeline(render_pass->handle(), m_compose_pipeline->pipeline().handle()); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 0, m_shared_config_bind_group->handle(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 1, m_camera_bind_group->handle(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 2, m_compose_bind_groups[m_paint_number % 2]->handle(), 0, nullptr); + wgpuRenderPassEncoderDraw(render_pass->handle(), 3, 1, 0, 0); + } + + // render lines to color buffer + if (m_context->shared_config().m_track_render_mode > 0) { + m_context->track_renderer()->render( + command_encoder, *m_shared_config_bind_group, *m_camera_bind_group, *m_depth_texture_bind_group, framebuffer->color_texture_view(0)); + } + + m_paint_number++; +} + +glm::vec4 Window::synchronous_position_readback(const glm::dvec2& ndc) +{ + if (m_position_readback_buffer->map_state() == WGPUBufferMapState_Unmapped) { + // A little bit silly, but we have to transform it back to device coordinates + glm::uvec2 device_coordinates = { (ndc.x + 1) * 0.5 * m_swapchain_size.x, (1 - (ndc.y + 1) * 0.5) * m_swapchain_size.y }; + + // clamp device coordinates to the swapchain size + device_coordinates = glm::clamp(device_coordinates, glm::uvec2(0), glm::uvec2(m_swapchain_size - glm::vec2(1.0))); + + const auto& src_texture = m_gbuffer->color_texture(1); + // Need to read a multiple of 16 values to fit requirement for texture_to_buffer copy + src_texture.copy_to_buffer( + m_context->webgpu_ctx().device(), *m_position_readback_buffer.get(), glm::uvec3(device_coordinates.x, device_coordinates.y, 0), glm::uvec2(16, 1)); + + std::vector pos_buffer; + WGPUMapAsyncStatus result + = m_position_readback_buffer->read_back_sync(m_context->webgpu_ctx().instance(), m_context->webgpu_ctx().device(), pos_buffer); + if (result == WGPUMapAsyncStatus_Success) { + m_last_position_readback = pos_buffer[0]; + } + } // else qDebug() << "Dropped position readback request, buffer still mapping."; + + // qDebug() << "Position:" << glm::to_string(m_last_position_readback); + return m_last_position_readback; +} + +void Window::set_max_zoom_level(uint32_t max_zoom_level) { m_max_zoom_level = max_zoom_level; } + +float Window::depth([[maybe_unused]] const glm::dvec2& normalised_device_coordinates) +{ + auto position = synchronous_position_readback(normalised_device_coordinates); + return position.z; +} + +glm::dvec3 Window::position([[maybe_unused]] const glm::dvec2& normalised_device_coordinates) +{ + // If we read position directly no reconstruction is necessary + // glm::dvec3 reconstructed = m_camera.position() + m_camera.ray_direction(normalised_device_coordinates) * (double)depth(normalised_device_coordinates); + auto position = synchronous_position_readback(normalised_device_coordinates); + return m_camera.position() + glm::dvec3(position.x, position.y, position.z); +} + +void Window::destroy() { } + +// Return this object as the depth tester +nucleus::camera::AbstractDepthTester* Window::depth_tester() { return this; } + +nucleus::utils::ColourTexture::Format Window::ortho_tile_compression_algorithm() const +{ + // TODO use compressed textures in the future + return nucleus::utils::ColourTexture::Format::Uncompressed_RGBA; +} + +void Window::update_camera([[maybe_unused]] const nucleus::camera::Definition& new_definition) +{ + // NOTE: Could also just be done on camera or viewport change! + uboCameraConfig* cc = &m_camera_config_ubo->data; + cc->position = glm::vec4(new_definition.position(), 1.0); + cc->view_matrix = new_definition.local_view_matrix(); + cc->proj_matrix = new_definition.projection_matrix(); + cc->view_proj_matrix = cc->proj_matrix * cc->view_matrix; + cc->inv_view_proj_matrix = glm::inverse(cc->view_proj_matrix); + cc->inv_view_matrix = glm::inverse(cc->view_matrix); + cc->inv_proj_matrix = glm::inverse(cc->proj_matrix); + cc->viewport_size = new_definition.viewport_size(); + cc->distance_scaling_factor = new_definition.distance_scale_factor(); + m_camera_config_ubo->update_gpu_data(m_context->webgpu_ctx().queue()); + m_camera = new_definition; + + emit update_requested(); +} + +void Window::request_redraw() { m_needs_redraw = true; } + +void Window::ready() { m_context->overlay_renderer()->ready(m_context->webgpu_ctx()); } + +void Window::reload_shaders() +{ + m_context->webgpu_ctx().resource_registry().recreate_all(m_context->webgpu_ctx().device()); + recreate_compose_bind_group(); + qDebug() << "reloading shaders done"; + request_redraw(); +} + +void Window::create_buffers() +{ + m_shared_config_ubo + = std::make_unique>(m_context->webgpu_ctx().device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + m_camera_config_ubo + = std::make_unique>(m_context->webgpu_ctx().device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + m_position_readback_buffer = std::make_unique>( + m_context->webgpu_ctx().device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead, 256 / sizeof(glm::vec4), "position readback buffer"); +} + +void Window::create_bind_groups() +{ + m_shared_config_bind_group = std::make_unique(m_context->webgpu_ctx().device(), + m_context->webgpu_ctx().resource_registry().bind_group_layout("shared_config"), + std::initializer_list { m_shared_config_ubo->raw_buffer().create_bind_group_entry(0) }); + + m_camera_bind_group = std::make_unique(m_context->webgpu_ctx().device(), + m_context->webgpu_ctx().resource_registry().bind_group_layout("camera"), + std::initializer_list { m_camera_config_ubo->raw_buffer().create_bind_group_entry(0) }); +} + +void Window::recreate_compose_bind_group() +{ + for (int i = 0; i < 2; ++i) { + m_compose_bind_groups[i] = std::make_unique(m_context->webgpu_ctx().device(), + m_context->webgpu_ctx().resource_registry().bind_group_layout("compose"), + std::initializer_list { + m_gbuffer->color_texture_view(0).create_bind_group_entry(0), // albedo texture + m_gbuffer->color_texture_view(1).create_bind_group_entry(1), // position texture + m_gbuffer->color_texture_view(2).create_bind_group_entry(2), // normal texture + m_context->atmosphere_renderer()->result_view()->create_bind_group_entry(3), // atmosphere texture + m_gbuffer->color_texture_view(3).create_bind_group_entry(4), // overlay texture + m_context->cloud_renderer()->result_color_view(i)->create_bind_group_entry(5), + m_context->cloud_renderer()->result_depth_view()->create_bind_group_entry(6), + m_shadow_texture->texture_view().create_bind_group_entry(7), + m_shadow_texture->sampler().create_bind_group_entry(8), + m_gbuffer->depth_texture_view().create_bind_group_entry(9), + m_context->overlay_renderer()->result_post_view()->create_bind_group_entry(10), // overlay post-shading output + m_context->overlay_renderer()->result_pre_view()->create_bind_group_entry(11), // overlay pre-shading output + }); + } +} + +void Window::update_required_gpu_limits(WGPULimits& limits, const WGPULimits& supported_limits) +{ + const uint32_t max_required_bind_groups = 4u; + const uint32_t min_recommended_max_texture_array_layers = 1024u; + const uint32_t min_required_max_color_attachment_bytes_per_sample = 32u; + const uint64_t min_required_max_storage_buffer_binding_size = 268435456u; + + if (supported_limits.maxColorAttachmentBytesPerSample < min_required_max_color_attachment_bytes_per_sample) { + qFatal() << "Minimum supported maxColorAttachmentBytesPerSample needs to be >=" << min_required_max_color_attachment_bytes_per_sample; + } + if (supported_limits.maxTextureArrayLayers < min_recommended_max_texture_array_layers) { + qWarning() << "Minimum supported maxTextureArrayLayers is " << supported_limits.maxTextureArrayLayers << " (" + << min_recommended_max_texture_array_layers << " recommended)!"; + } + if (supported_limits.maxBindGroups < max_required_bind_groups) { + qFatal() << "Maximum supported number of bind groups is " << supported_limits.maxBindGroups << " and " << max_required_bind_groups << " are required"; + } + if (supported_limits.maxStorageBufferBindingSize < min_required_max_storage_buffer_binding_size) { + qFatal() << "Maximum supported storage buffer binding size is " << supported_limits.maxStorageBufferBindingSize << " and " + << min_required_max_storage_buffer_binding_size << " is required"; + } + limits.maxBindGroups = std::max(limits.maxBindGroups, max_required_bind_groups); + limits.maxColorAttachmentBytesPerSample = std::max(limits.maxColorAttachmentBytesPerSample, min_required_max_color_attachment_bytes_per_sample); + limits.maxTextureArrayLayers + = std::min(std::max(limits.maxTextureArrayLayers, min_recommended_max_texture_array_layers), supported_limits.maxTextureArrayLayers); + limits.maxStorageBufferBindingSize = std::max(limits.maxStorageBufferBindingSize, supported_limits.maxStorageBufferBindingSize); +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/Window.h b/webgpu/engine/Window.h new file mode 100644 index 000000000..c52ccd5a2 --- /dev/null +++ b/webgpu/engine/Window.h @@ -0,0 +1,120 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2024 Gerald Kimmersdorfer + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ +#pragma once + +#include "Context.h" +#include "UniformBufferObjects.h" +#include "nucleus/AbstractRenderWindow.h" +#include "nucleus/camera/AbstractDepthTester.h" +#include "nucleus/camera/Definition.h" +#include "nucleus/utils/ColourTexture.h" +#include +#include +#include + +class QOpenGLFramebufferObject; + +namespace webgpu_engine { + +class Window : public nucleus::AbstractRenderWindow, public nucleus::camera::AbstractDepthTester { + Q_OBJECT +public: + Window(); + + ~Window() override; + + void set_context(Context* context); + void initialise_gpu() override; + void resize_framebuffer(int w, int h) override; + void ready(); + void paint(webgpu::Framebuffer* framebuffer, WGPUCommandEncoder command_encoder); + // void paint(WGPUTextureView target_color_texture, WGPUTextureView target_depth_texture, WGPUCommandEncoder encoder); + void paint([[maybe_unused]] QOpenGLFramebufferObject* framebuffer = nullptr) override { throw std::runtime_error("Not implemented"); } + + [[nodiscard]] float depth(const glm::dvec2& normalised_device_coordinates) override; + [[nodiscard]] glm::dvec3 position(const glm::dvec2& normalised_device_coordinates) override; + void destroy() override; + [[nodiscard]] nucleus::camera::AbstractDepthTester* depth_tester() override; + nucleus::utils::ColourTexture::Format ortho_tile_compression_algorithm() const override; + bool needs_redraw() { return m_needs_redraw; } + + void update_required_gpu_limits(WGPULimits& limits, const WGPULimits& supported_limits); + + void set_max_zoom_level(uint32_t max_zoom_level); + +public slots: + void update_camera(const nucleus::camera::Definition& new_definition) override; + void update_debug_scheduler_stats([[maybe_unused]] const QString& stats) override { } + void pick_value([[maybe_unused]] const glm::dvec2& screen_space_coordinate) override { } + + void request_redraw(); + void reload_shaders(); + void on_shadow_texture_updated(const QByteArray& data); + +signals: + void set_camera_definition_requested(nucleus::camera::Definition definition); + +private: + std::unique_ptr> m_position_readback_buffer; + glm::vec4 m_last_position_readback; + + void create_buffers(); + void create_bind_groups(); + void recreate_compose_bind_group(); + + // A helper function for the depth and position method. + // ATTENTION: This function is synchronous and will hold rendering. Use with caution! + // Note: Depth aswell as the position is saved in the gbuffer. In contrast to the gl version + // we can directly readback the content of the position buffer and don't need the readback depth + // buffer anymore. May actually increase performance as we don't need to fill the seperate buffer. + glm::vec4 synchronous_position_readback(const glm::dvec2& normalised_device_coordinates); + + std::unique_ptr create_shadow_texture(uint32_t width, uint32_t height, uint32_t mip_levels); + +private: + Context* m_context = nullptr; + + std::unique_ptr> m_shared_config_ubo; + std::unique_ptr> m_camera_config_ubo; + + std::unique_ptr m_shared_config_bind_group; + std::unique_ptr m_camera_bind_group; + std::array, 2> m_compose_bind_groups; + std::unique_ptr m_depth_texture_bind_group; + + nucleus::camera::Definition m_camera; + uint32_t m_max_zoom_level = 18; + + webgpu::FramebufferFormat m_gbuffer_format; + std::unique_ptr m_gbuffer; + + std::unique_ptr m_compose_pipeline; + + // ToDo: Swapchain should get a raii class and the size could be saved in there + glm::vec2 m_swapchain_size = glm::vec2(0.0f); + WGPUPresentMode m_swapchain_presentmode = WGPUPresentMode::WGPUPresentMode_Fifo; + + bool m_needs_redraw = true; + uint32_t m_paint_number = 0; + + std::unique_ptr m_shadow_texture; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/atmosphere/AtmosphereRenderer.cpp b/webgpu/engine/atmosphere/AtmosphereRenderer.cpp new file mode 100644 index 000000000..fcafa1e09 --- /dev/null +++ b/webgpu/engine/atmosphere/AtmosphereRenderer.cpp @@ -0,0 +1,70 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "AtmosphereRenderer.h" + +#include +#include +#include +#include + +namespace webgpu_engine { + +AtmosphereRenderer::AtmosphereRenderer() + : QObject { nullptr } +{ +} + +void AtmosphereRenderer::init(webgpu::Context& ctx) +{ + m_ctx = &ctx; + + auto& reg = ctx.resource_registry(); + reg.register_shader("render_atmosphere", "webgpu_engine::render_atmosphere"); + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + webgpu::FramebufferFormat format {}; + format.depth_format = WGPUTextureFormat_Undefined; + format.color_formats.emplace_back(WGPUTextureFormat_RGBA8Unorm); + m_pipeline = std::make_unique(dev, + reg.shader("render_atmosphere"), + reg.shader("render_atmosphere"), + std::vector {}, + format, + std::vector { ®.bind_group_layout("camera") }); + }); +} + +void AtmosphereRenderer::resize(int /*w*/, int h) +{ + webgpu::FramebufferFormat format(m_pipeline->framebuffer_format()); + format.size = glm::uvec2(1, h); + m_atmosphere_framebuffer = std::make_unique(m_ctx->device(), format); +} + +void AtmosphereRenderer::draw(const WGPUCommandEncoder& command_encoder, const WGPUBindGroup& camera_bind_group) +{ + std::unique_ptr render_pass = m_atmosphere_framebuffer->begin_render_pass(command_encoder); + wgpuRenderPassEncoderSetBindGroup(render_pass->handle(), 0, camera_bind_group, 0, nullptr); + wgpuRenderPassEncoderSetPipeline(render_pass->handle(), m_pipeline->pipeline().handle()); + wgpuRenderPassEncoderDraw(render_pass->handle(), 3, 1, 0, 0); +} + +const webgpu::raii::TextureView* AtmosphereRenderer::result_view() const { return &m_atmosphere_framebuffer->color_texture_view(0); } + +} // namespace webgpu_engine diff --git a/webgpu/engine/atmosphere/AtmosphereRenderer.h b/webgpu/engine/atmosphere/AtmosphereRenderer.h new file mode 100644 index 000000000..7ec43a144 --- /dev/null +++ b/webgpu/engine/atmosphere/AtmosphereRenderer.h @@ -0,0 +1,52 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +class AtmosphereRenderer : public QObject { + Q_OBJECT +public: + explicit AtmosphereRenderer(); + + void init(webgpu::Context& ctx); + + void resize(int w, int h); + + void draw(const WGPUCommandEncoder& command_encoder, const WGPUBindGroup& camera_bind_group); + + [[nodiscard]] const webgpu::raii::TextureView* result_view() const; + +private: + webgpu::Context* m_ctx = nullptr; + std::unique_ptr m_pipeline; + std::unique_ptr m_atmosphere_framebuffer; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/cloud/CloudRenderer.cpp b/webgpu/engine/cloud/CloudRenderer.cpp new file mode 100644 index 000000000..2be1bd257 --- /dev/null +++ b/webgpu/engine/cloud/CloudRenderer.cpp @@ -0,0 +1,520 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "CloudRenderer.h" + +#include "glm/ext/matrix_relational.hpp" +#include "nucleus/camera/Definition.h" +#include "nucleus/srs.h" +#include "nucleus/utils/terrain_mesh_index_generator.h" +#include +#include + +using namespace webgpu_engine::clouds; +namespace webgpu_engine { + +CloudRenderer::CloudRenderer() + : QObject { nullptr } +{ +} + +void CloudRenderer::init(webgpu::Context& ctx) +{ + m_ctx = &ctx; + + WGPUTextureDescriptor cloud_texture_desc {}; + cloud_texture_desc.label = WGPUStringView { .data = "cloud texture", .length = WGPU_STRLEN }; + cloud_texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_3D; + cloud_texture_desc.size = { TILE_RESOLUTION_XY * ATLAS_SCALE_XY, TILE_RESOLUTION_XY * ATLAS_SCALE_XY, TILE_RESOLUTION_Z * ATLAS_SCALE_Z }; + constexpr int SMALLEST_MIP_DIM = 4; // 4x4x2 is the smallest mip size + cloud_texture_desc.mipLevelCount = static_cast(std::ceil(std::log2(TILE_RESOLUTION_XY)) - std::log2(SMALLEST_MIP_DIM) + 1); + cloud_texture_desc.sampleCount = 1; + cloud_texture_desc.format = WGPUTextureFormat::WGPUTextureFormat_BC4RUnorm; + cloud_texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + + WGPUSamplerDescriptor cloud_sampler_desc {}; + cloud_sampler_desc.label = WGPUStringView { .data = "cloud sampler", .length = WGPU_STRLEN }; + cloud_sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + cloud_sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + cloud_sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + cloud_sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + cloud_sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + cloud_sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + cloud_sampler_desc.lodMinClamp = 0.0f; + cloud_sampler_desc.lodMaxClamp = static_cast(cloud_texture_desc.mipLevelCount); + cloud_sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + cloud_sampler_desc.maxAnisotropy = 1; + + m_cloud_atlas_texture = std::make_unique(m_ctx->device(), cloud_texture_desc); + m_cloud_atlas_view = m_cloud_atlas_texture->create_view(); + m_cloud_linear_sampler = std::make_unique(m_ctx->device(), cloud_sampler_desc); + + glm::dvec2 world_bounds_min = nucleus::srs::lat_long_to_world(BOUNDS_MIN); + m_tile_coords_offset = nucleus::srs::world_xy_to_tile_id(world_bounds_min, ZOOM_MAX).coords; + glm::dvec2 world_bounds_min_aligned = nucleus::srs::tile_id_to_world_xy(m_tile_coords_offset, ZOOM_MAX); + glm::dvec2 world_bounds_max_aligned = nucleus::srs::tile_id_to_world_xy(m_tile_coords_offset + TILE_COUNTS, ZOOM_MAX); + float world_bounds_max_z = MAX_ALTITUDE / std::cos(glm::radians(BOUNDS_MAX.x)); + + m_render_shader_params_ubo = std::make_unique>(m_ctx->device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + m_render_shader_params_ubo->data.bounds_min = glm::vec4(world_bounds_min_aligned, 0.0, 0.0); + m_render_shader_params_ubo->data.bounds_max = glm::vec4(world_bounds_max_aligned, world_bounds_max_z, 0.0); + m_upscale_shader_params_ubo = std::make_unique>(m_ctx->device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + + // this represents a flattened 2d lookup table + m_cloud_tile_info_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, TILE_COUNT_TOTAL); + m_tile_infos.resize(TILE_COUNT_TOTAL); + + m_linear_sampler = std::make_unique(m_ctx->device(), + WGPUSamplerDescriptor { + .label = WGPUStringView { .data = "clouds upscale linear sampler", .length = WGPU_STRLEN }, + .addressModeU = WGPUAddressMode_ClampToEdge, + .addressModeV = WGPUAddressMode_ClampToEdge, + .magFilter = WGPUFilterMode_Linear, + .minFilter = WGPUFilterMode_Linear, + .mipmapFilter = WGPUMipmapFilterMode_Nearest, + .lodMinClamp = 0.0f, + .lodMaxClamp = 0.0f, + .compare = WGPUCompareFunction::WGPUCompareFunction_Undefined, + .maxAnisotropy = 1, + }); + + auto& reg = ctx.resource_registry(); + reg.register_shader("render_clouds", "webgpu_engine::render_clouds"); + reg.register_shader("upscale_clouds", "webgpu_engine::upscale_clouds"); + + reg.register_bind_group_layout("render_clouds", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry shader_params_entry {}; + shader_params_entry.binding = 0; + shader_params_entry.visibility = WGPUShaderStage_Compute; + shader_params_entry.buffer.type = WGPUBufferBindingType_Uniform; + shader_params_entry.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry atlas_texture_entry {}; + atlas_texture_entry.binding = 1; + atlas_texture_entry.visibility = WGPUShaderStage_Compute; + atlas_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + atlas_texture_entry.texture.viewDimension = WGPUTextureViewDimension_3D; + + WGPUBindGroupLayoutEntry atlas_texture_sampler {}; + atlas_texture_sampler.binding = 2; + atlas_texture_sampler.visibility = WGPUShaderStage_Compute; + atlas_texture_sampler.sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutEntry tile_infos_entry {}; + tile_infos_entry.binding = 3; + tile_infos_entry.visibility = WGPUShaderStage_Compute; + tile_infos_entry.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + tile_infos_entry.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry color_output_entry {}; + color_output_entry.binding = 4; + color_output_entry.visibility = WGPUShaderStage_Compute; + color_output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + color_output_entry.storageTexture.format = WGPUTextureFormat_RGBA16Float; + color_output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry depth_output_entry {}; + depth_output_entry.binding = 5; + depth_output_entry.visibility = WGPUShaderStage_Compute; + depth_output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + depth_output_entry.storageTexture.format = WGPUTextureFormat_R32Float; + depth_output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { + shader_params_entry, + atlas_texture_entry, + atlas_texture_sampler, + tile_infos_entry, + color_output_entry, + depth_output_entry, + }, + "cloud bind group"); + }); + + reg.register_bind_group_layout("upscale_clouds", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry shader_params_entry {}; + shader_params_entry.binding = 0; + shader_params_entry.visibility = WGPUShaderStage_Compute; + shader_params_entry.buffer.type = WGPUBufferBindingType_Uniform; + shader_params_entry.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry current_frame_color_texture_entry {}; + current_frame_color_texture_entry.binding = 1; + current_frame_color_texture_entry.visibility = WGPUShaderStage_Compute; + current_frame_color_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + current_frame_color_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry current_frame_depth_texture_entry {}; + current_frame_depth_texture_entry.binding = 2; + current_frame_depth_texture_entry.visibility = WGPUShaderStage_Compute; + current_frame_depth_texture_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + current_frame_depth_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry linear_sampler_entry {}; + linear_sampler_entry.binding = 3; + linear_sampler_entry.visibility = WGPUShaderStage_Compute; + linear_sampler_entry.sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutEntry accumulation_color_texture_r_entry {}; + accumulation_color_texture_r_entry.binding = 4; + accumulation_color_texture_r_entry.visibility = WGPUShaderStage_Compute; + accumulation_color_texture_r_entry.texture.sampleType = WGPUTextureSampleType_Float; + accumulation_color_texture_r_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry accumulation_color_texture_w_entry {}; + accumulation_color_texture_w_entry.binding = 5; + accumulation_color_texture_w_entry.visibility = WGPUShaderStage_Compute; + accumulation_color_texture_w_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + accumulation_color_texture_w_entry.storageTexture.format = WGPUTextureFormat_RGBA16Float; + accumulation_color_texture_w_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { + shader_params_entry, + current_frame_color_texture_entry, + current_frame_depth_texture_entry, + linear_sampler_entry, + accumulation_color_texture_r_entry, + accumulation_color_texture_w_entry, + }, + "upscale clouds bind group layout"); + }); + + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + glm::dvec2 bounds_min = nucleus::srs::lat_long_to_world(BOUNDS_MIN); + // Note: This is different from nucleus::srs::world_xy_to_tile_id because it doesn't apply the origin shift, resulting in signed coords. + // This calculation matches the shader. + double tile_size_xy = nucleus::srs::tile_width(ZOOM_MAX); + glm::ivec2 tile_coords_offset = glm::floor(bounds_min / tile_size_xy); + + std::vector constants = { + WGPUConstantEntry { .key = { .data = "tile_size_xy", .length = WGPU_STRLEN }, .value = tile_size_xy }, + WGPUConstantEntry { .key = { .data = "inv_tile_size_xy", .length = WGPU_STRLEN }, .value = 1.0 / tile_size_xy }, + WGPUConstantEntry { .key = { .data = "tile_count_x", .length = WGPU_STRLEN }, .value = TILE_COUNTS.x }, + WGPUConstantEntry { .key = { .data = "tile_count_y", .length = WGPU_STRLEN }, .value = TILE_COUNTS.y }, + WGPUConstantEntry { .key = { .data = "zoom_max", .length = WGPU_STRLEN }, .value = ZOOM_MAX }, + WGPUConstantEntry { .key = { .data = "tile_coords_offset_x", .length = WGPU_STRLEN }, .value = static_cast(tile_coords_offset.x) }, + WGPUConstantEntry { .key = { .data = "tile_coords_offset_y", .length = WGPU_STRLEN }, .value = static_cast(tile_coords_offset.y) }, + WGPUConstantEntry { .key = { .data = "atlas_bits_xy", .length = WGPU_STRLEN }, .value = ATLAS_BITS_XY }, + WGPUConstantEntry { .key = { .data = "atlas_bits_z", .length = WGPU_STRLEN }, .value = ATLAS_BITS_Z }, + }; + + WGPUComputePipelineDescriptor pipeline_desc {}; + pipeline_desc.label = WGPUStringView { .data = "cloud render pipeline", .length = WGPU_STRLEN }; + pipeline_desc.compute.module = reg.shader("render_clouds").handle(); + pipeline_desc.compute.entryPoint = WGPUStringView { .data = "computeMain", .length = WGPU_STRLEN }; + pipeline_desc.compute.constantCount = constants.size(); + pipeline_desc.compute.constants = constants.data(); + + m_render_clouds_pipeline = std::make_unique(dev, + std::vector { + ®.bind_group_layout("render_clouds"), + ®.bind_group_layout("depth_texture"), + ®.bind_group_layout("shared_config"), + }, + pipeline_desc); + }); + + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + m_upscale_clouds_pipeline = std::make_unique(dev, + reg.shader("upscale_clouds"), + std::vector { ®.bind_group_layout("upscale_clouds") }, + "upscale clouds compute pipeline"); + }); +} + +struct FrameJitterData { + glm::mat4 jittered_projection; + glm::mat4 jittered_view_proj; + glm::vec2 jitter_offset; + uint32_t frame_index; +}; + +glm::dvec2 generate_jitter_simple_4x(uint32_t frame_index, glm::uvec2 output_resolution) +{ + constexpr glm::dvec2 pattern[4] = { + glm::dvec2(-0.25, -0.25), + glm::dvec2(+0.25, -0.25), + glm::dvec2(-0.25, +0.25), + glm::dvec2(+0.25, +0.25), + }; + + uint32_t pattern_index = frame_index % 4; + glm::dvec2 jitter = pattern[pattern_index]; + + // Convert to NDC space + jitter.x /= static_cast(output_resolution.x); + jitter.y /= static_cast(output_resolution.y); + + return jitter; +} + +glm::mat4 jitter_projection_matrix(const glm::mat4& projection, glm::vec2 jitter) +{ + // Jitter is in NDC space [-0.5, 0.5] mapped to pixels + // Need to convert to NDC: multiply by 2 (since NDC is [-1, 1]) + glm::mat4 jittered = projection; + + // Modify the translation components (last column, rows 0 and 1) + // These correspond to the x and y offset in clip space + jittered[2][0] += jitter.x * 2.0f; // X offset + jittered[2][1] += jitter.y * 2.0f; // Y offset + + return jittered; +} + +inline int ceil_div(int x, int y) { return (x + y - 1) / y; } +inline unsigned ceil_div(unsigned x, unsigned y) { return (x + y - 1) / y; } + +void CloudRenderer::resize(int w, int h) +{ + constexpr float resolution_scale = 2.0f; + m_output_lo_resolution = { static_cast(w) / resolution_scale, static_cast(h) / resolution_scale }; + m_output_hi_resolution = { w, h }; + + m_upscale_shader_params_ubo->data.low_res_texel_size = 1.0f / glm::vec2(m_output_lo_resolution); + m_upscale_shader_params_ubo->data.high_res_texel_size = 1.0f / glm::vec2(m_output_hi_resolution); + m_upscale_shader_params_ubo->data.resolution_scale = glm::vec2(m_output_hi_resolution) / glm::vec2(m_output_lo_resolution); + + m_clouds_lo_color_texture = std::make_unique(m_ctx->device(), + WGPUTextureDescriptor { + .label = WGPUStringView { .data = "clouds_lo_color", .length = WGPU_STRLEN }, + .usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding, + .dimension = WGPUTextureDimension_2D, + .size = { .width = m_output_lo_resolution.x, .height = m_output_lo_resolution.y, .depthOrArrayLayers = 1 }, + .format = WGPUTextureFormat_RGBA16Float, + .mipLevelCount = 1, + .sampleCount = 1, + }); + m_clouds_lo_color_texture_view = m_clouds_lo_color_texture->create_view(); + m_clouds_lo_depth_texture = std::make_unique(m_ctx->device(), + WGPUTextureDescriptor { + .label = WGPUStringView { .data = "clouds_lo_depth", .length = WGPU_STRLEN }, + .usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding, + .dimension = WGPUTextureDimension_2D, + .size = { .width = m_output_lo_resolution.x, .height = m_output_lo_resolution.y, .depthOrArrayLayers = 1 }, + .format = WGPUTextureFormat_R32Float, + .mipLevelCount = 1, + .sampleCount = 1, + }); + m_clouds_lo_depth_texture_view = m_clouds_lo_depth_texture->create_view(); + + m_clouds_hi_color_texture_a = std::make_unique(m_ctx->device(), + WGPUTextureDescriptor { + .label = WGPUStringView { .data = "clouds_hi_color_a", .length = WGPU_STRLEN }, + .usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding, + .dimension = WGPUTextureDimension_2D, + .size = { .width = static_cast(w), .height = static_cast(h), .depthOrArrayLayers = 1 }, + .format = WGPUTextureFormat_RGBA16Float, + .mipLevelCount = 1, + .sampleCount = 1, + }); + m_clouds_hi_color_texture_view_a = m_clouds_hi_color_texture_a->create_view(); + + m_clouds_hi_color_texture_b = std::make_unique(m_ctx->device(), + WGPUTextureDescriptor { + .label = WGPUStringView { .data = "clouds_hi_color_b", .length = WGPU_STRLEN }, + .usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding, + .dimension = WGPUTextureDimension_2D, + .size = { .width = static_cast(w), .height = static_cast(h), .depthOrArrayLayers = 1 }, + .format = WGPUTextureFormat_RGBA16Float, + .mipLevelCount = 1, + .sampleCount = 1, + }); + m_clouds_hi_color_texture_view_b = m_clouds_hi_color_texture_b->create_view(); + + auto& reg = m_ctx->resource_registry(); + m_render_clouds_bind_group = std::make_unique(m_ctx->device(), + reg.bind_group_layout("render_clouds"), + std::initializer_list { + m_render_shader_params_ubo->raw_buffer().create_bind_group_entry(0), + m_cloud_atlas_view->create_bind_group_entry(1), + m_cloud_linear_sampler->create_bind_group_entry(2), + m_cloud_tile_info_buffer->create_bind_group_entry(3), + m_clouds_lo_color_texture_view->create_bind_group_entry(4), + m_clouds_lo_depth_texture_view->create_bind_group_entry(5), + }, + "render clouds bind group"); + + m_upscale_clouds_bind_group_a = std::make_unique(m_ctx->device(), + reg.bind_group_layout("upscale_clouds"), + std::initializer_list { + m_upscale_shader_params_ubo->raw_buffer().create_bind_group_entry(0), + m_clouds_lo_color_texture_view->create_bind_group_entry(1), + m_clouds_lo_depth_texture_view->create_bind_group_entry(2), + m_linear_sampler->create_bind_group_entry(3), + m_clouds_hi_color_texture_view_a->create_bind_group_entry(4), + m_clouds_hi_color_texture_view_b->create_bind_group_entry(5), + }, + "upscale clouds bind group a"); + + m_upscale_clouds_bind_group_b = std::make_unique(m_ctx->device(), + reg.bind_group_layout("upscale_clouds"), + std::initializer_list { + m_upscale_shader_params_ubo->raw_buffer().create_bind_group_entry(0), + m_clouds_lo_color_texture_view->create_bind_group_entry(1), + m_clouds_lo_depth_texture_view->create_bind_group_entry(2), + m_linear_sampler->create_bind_group_entry(3), + m_clouds_hi_color_texture_view_b->create_bind_group_entry(4), + m_clouds_hi_color_texture_view_a->create_bind_group_entry(5), + }, + "upscale clouds bind group b"); +} + +void CloudRenderer::draw(const WGPUCommandEncoder& command_encoder, + const WGPUBindGroup& depth_texture_bind_group, + const WGPUBindGroup& shared_config_bind_group, + const nucleus::camera::Definition& camera, + uint32_t frame_number) +{ + auto jitter_offset = generate_jitter_simple_4x(frame_number, m_output_lo_resolution); + glm::mat4 unjittered_projection = camera.projection_matrix(); + glm::mat4 jittered_projection = jitter_projection_matrix(unjittered_projection, jitter_offset); + glm::mat4 view_matrix = camera.local_view_matrix(); + glm::mat4 inverse_view_matrix = glm::inverse(view_matrix); + + bool stable = glm::all(glm::equal(m_upscale_shader_params_ubo->data.previous_camera.view_matrix, view_matrix)) + && glm::all(glm::equal(m_upscale_shader_params_ubo->data.previous_camera.proj_matrix, unjittered_projection)); + + if (stable) { + m_stable_frames++; + } else { + m_stable_frames = 0; + } + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "cloud render pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(command_encoder, compute_pass_desc); + + m_render_shader_params_ubo->data.camera = { + .view_matrix = view_matrix, + .proj_matrix = jittered_projection, + .inv_view_matrix = inverse_view_matrix, + .inv_proj_matrix = glm::inverse(jittered_projection), + .position = glm::vec4(camera.position(), 0.0f), + }; + m_render_shader_params_ubo->data.frame_index = frame_number; + m_render_shader_params_ubo->data.jitter = jitter_offset * glm::dvec2(m_output_hi_resolution); + m_render_shader_params_ubo->data.step_size_min = shader_params.step_size_min; + m_render_shader_params_ubo->data.step_size_distance_factor = shader_params.step_size_distance_factor; + m_render_shader_params_ubo->data.step_size_horizon_factor = shader_params.step_size_horizon_factor; + m_render_shader_params_ubo->data.extinction_coeff = shader_params.extinction_coeff; + m_render_shader_params_ubo->data.scattering_coeff = shader_params.scattering_coeff; + m_render_shader_params_ubo->data.albedo = shader_params.albedo; + m_render_shader_params_ubo->data.sun_light_scale = shader_params.sun_light_scale; + m_render_shader_params_ubo->data.ambient_light_scale = shader_params.ambient_light_scale; + m_render_shader_params_ubo->data.atm_light_scale = shader_params.atmospheric_light_scale; + m_render_shader_params_ubo->data.shadow_extinction_scale = shader_params.shadow_extinction_scale; + m_render_shader_params_ubo->data.fade_factor = shader_params.fade_factor; + m_render_shader_params_ubo->data.powder_scale = shader_params.powder_scale; + m_render_shader_params_ubo->update_gpu_data(m_ctx->queue()); + + m_cloud_tile_info_buffer->write(m_ctx->queue(), m_tile_infos.data(), m_tile_infos.size()); + + wgpuComputePassEncoderSetPipeline(compute_pass.handle(), m_render_clouds_pipeline->handle()); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, m_render_clouds_bind_group->handle(), 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 1, depth_texture_bind_group, 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 2, shared_config_bind_group, 0, nullptr); + + wgpuComputePassEncoderDispatchWorkgroups(compute_pass.handle(), ceil_div(m_output_lo_resolution.x, 8u), ceil_div(m_output_lo_resolution.y, 8u), 1); + } + + { + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "cloud upscale pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(command_encoder, compute_pass_desc); + + m_upscale_shader_params_ubo->data.previous_camera = m_upscale_shader_params_ubo->data.current_camera; + m_upscale_shader_params_ubo->data.current_camera = { + .view_matrix = view_matrix, + .proj_matrix = unjittered_projection, + .inv_view_matrix = inverse_view_matrix, + .inv_proj_matrix = glm::inverse(unjittered_projection), + .position = glm::vec4(camera.position(), 0.0f), + }; + m_upscale_shader_params_ubo->data.prev_jitter = m_upscale_shader_params_ubo->data.jitter; + m_upscale_shader_params_ubo->data.jitter = jitter_offset; + m_upscale_shader_params_ubo->update_gpu_data(m_ctx->queue()); + + wgpuComputePassEncoderSetPipeline(compute_pass.handle(), m_upscale_clouds_pipeline->handle()); + auto bind_group = frame_number % 2 == 0 ? m_upscale_clouds_bind_group_a->handle() : m_upscale_clouds_bind_group_b->handle(); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, bind_group, 0, nullptr); + + wgpuComputePassEncoderDispatchWorkgroups(compute_pass.handle(), ceil_div(m_output_hi_resolution.x, 8u), ceil_div(m_output_hi_resolution.y, 8u), 1); + } +} + +void CloudRenderer::set_tile_limit(unsigned int num_tiles) { m_loaded_cloud_textures.set_tile_limit(num_tiles); } + +void CloudRenderer::update_gpu_tiles_cloud(const std::vector& deleted_tiles, const std::vector& new_tiles) +{ + std::lock_guard lock(m_mutex); + for (const auto& id : deleted_tiles) { + if (m_loaded_cloud_textures.contains(id)) { + m_loaded_cloud_textures.remove_tile(id); + } + } + for (const auto& tile : new_tiles) { + // test for validity + assert(tile.id.zoom_level < 100); + assert(tile.texture); + + // Atlas is full + if (m_loaded_cloud_textures.n_occupied() >= m_loaded_cloud_textures.size()) { + break; + } + + // find empty spot and upload texture + const auto layer_index = m_loaded_cloud_textures.add_tile(tile.id); + + uint32_t atlas_x = layer_index & ATLAS_MASK_XY; + uint32_t atlas_y = (layer_index >> ATLAS_BITS_XY) & ATLAS_MASK_XY; + uint32_t atlas_z = (layer_index >> (2 * ATLAS_BITS_XY)) & ATLAS_MASK_Z; + assert(atlas_x < ATLAS_SCALE_XY); + assert(atlas_y < ATLAS_SCALE_XY); + assert(atlas_z < ATLAS_SCALE_Z); + // Note: z is "up" in texture space + for (int i = 0; i < tile.texture->size(); ++i) { + const auto& level = tile.texture->at(i); + glm::uvec3 atlas_offset = { atlas_x * level.width(), atlas_y * level.height(), atlas_z * level.depth() }; + m_cloud_atlas_texture->write(m_ctx->queue(), level, atlas_offset, i); + } + + // convert to coords at max zoom level + uint32_t d_z = ZOOM_MAX - tile.id.zoom_level; + int32_t x_start = static_cast(tile.id.coords.x << d_z) - static_cast(m_tile_coords_offset.x); + int32_t y_start = static_cast(tile.id.coords.y << d_z) - static_cast(m_tile_coords_offset.y); + int32_t size = 1 << d_z; + + for (int32_t dy = 0; dy < size; dy++) { + for (int32_t dx = 0; dx < size; dx++) { + int32_t x = x_start + dx; + int32_t y = y_start + dy; + if (x < 0 || x >= static_cast(TILE_COUNTS.x) || y < 0 || y >= static_cast(TILE_COUNTS.y)) + continue; + size_t info_index = y * TILE_COUNTS.x + x; + m_tile_infos[info_index] = { .index = layer_index, .zoom = tile.id.zoom_level }; + } + } + } +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/cloud/CloudRenderer.h b/webgpu/engine/cloud/CloudRenderer.h new file mode 100644 index 000000000..3018f8cfa --- /dev/null +++ b/webgpu/engine/cloud/CloudRenderer.h @@ -0,0 +1,205 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::camera { +class Definition; +} + +namespace webgpu_engine::clouds { +static constexpr uint32_t ZOOM_MAX = 10; +static constexpr glm::vec2 BOUNDS_MIN = { 46.2, 9.4 }; +static constexpr glm::vec2 BOUNDS_MAX = { 49.2, 17.4 }; +static constexpr glm::uvec2 TILE_COUNTS = { 24, 14 }; +static constexpr uint32_t TILE_COUNT_TOTAL = TILE_COUNTS.x * TILE_COUNTS.y; +static constexpr uint32_t TILE_RESOLUTION_XY = 256; +static constexpr uint32_t TILE_RESOLUTION_Z = 64; +static constexpr float MAX_ALTITUDE = 14000.0; +static constexpr uint32_t ATLAS_BITS_XY = 2; +static constexpr uint32_t ATLAS_SCALE_XY = 1 << ATLAS_BITS_XY; +static constexpr uint32_t ATLAS_MASK_XY = ATLAS_SCALE_XY - 1; +static constexpr uint32_t ATLAS_BITS_Z = 5; +static constexpr uint32_t ATLAS_SCALE_Z = 1 << ATLAS_BITS_Z; +static constexpr uint32_t ATLAS_MASK_Z = ATLAS_SCALE_Z - 1; +static constexpr uint32_t LOADED_TILE_LIMIT = ATLAS_SCALE_XY * ATLAS_SCALE_XY * ATLAS_SCALE_Z; +} // namespace webgpu_engine::clouds + +namespace webgpu_engine { + +class CloudRenderer : public QObject { + Q_OBJECT +public: + // Public shader parameters + struct ShaderParameters { + float step_size_min = 125.0f; + float step_size_distance_factor = 1.0f / 50.0f; + float step_size_horizon_factor = 1000.0f; + float scattering_coeff = 0.7f; + float extinction_coeff = 1.0f; + float albedo = 0.99f; + float sun_light_scale = 800.0f; + float ambient_light_scale = 1.0f; + float atmospheric_light_scale = 1.0f; + float shadow_extinction_scale = 0.5f; + float powder_scale = 0.9f; + float fade_factor = 0.0f; + int stable_frames_limit = 1; // originally 64, but not necessary anymore due to improvements Wendelin made + }; + + ShaderParameters shader_params = {}; + + explicit CloudRenderer(); + + void init(webgpu::Context& ctx); + + void resize(int w, int h); + + void draw(const WGPUCommandEncoder& command_encoder, + const WGPUBindGroup& depth_texture_bind_group, + const WGPUBindGroup& shared_config_bind_group, + const nucleus::camera::Definition& camera, + uint32_t frame_number); + + [[nodiscard]] bool needs_redraw() const { return m_stable_frames <= static_cast(shader_params.stable_frames_limit); } + + void set_tile_limit(unsigned new_limit); + + [[nodiscard]] webgpu::raii::TextureView* result_color_view(int frame) const + { + if (frame % 2 == 0) + return m_clouds_hi_color_texture_view_b.get(); + return m_clouds_hi_color_texture_view_a.get(); + } + + [[nodiscard]] webgpu::raii::TextureView* result_depth_view() const { return m_clouds_lo_depth_texture_view.get(); } + +signals: + void tiles_changed(); + +public slots: + void update_gpu_tiles_cloud(const std::vector& deleted_tiles, const std::vector& new_tiles); + +private: + struct alignas(16) CameraConfig { + glm::mat4 view_matrix; + glm::mat4 proj_matrix; + glm::mat4 inv_view_matrix; + glm::mat4 inv_proj_matrix; + glm::vec4 position; + }; + + struct alignas(16) ShaderParamsRender { + CameraConfig camera; + glm::vec4 bounds_min; + glm::vec4 bounds_max; + + uint32_t frame_index; + float scattering_coeff; + float extinction_coeff; + float albedo; + + float step_size_min; + float step_size_distance_factor; + float step_size_horizon_factor; + float fade_factor; + + float sun_light_scale; + float ambient_light_scale; + float atm_light_scale; + float shadow_extinction_scale; + + glm::vec2 jitter; + float powder_scale; + float _padding0; + }; + + struct alignas(16) ShaderParamsUpscale { + CameraConfig current_camera; + CameraConfig previous_camera; + glm::vec2 jitter; + glm::vec2 prev_jitter; + glm::vec2 low_res_texel_size; + glm::vec2 high_res_texel_size; + glm::vec2 resolution_scale; + glm::vec2 _padding0; + }; + + struct TileInfo { + glm::uint32 index; + glm::uint32 zoom; + }; + + // tile coordinates of the bounds min corner at max zoom level + glm::uvec2 m_tile_coords_offset = {}; + + nucleus::tile::GpuArrayHelper m_loaded_cloud_textures; + + webgpu::Context* m_ctx = nullptr; + + std::unique_ptr> m_render_shader_params_ubo; + std::unique_ptr> m_upscale_shader_params_ubo; + + std::unique_ptr> m_cloud_tile_info_buffer; + std::vector m_tile_infos; + + std::unique_ptr m_cloud_atlas_texture; + std::unique_ptr m_cloud_atlas_view; + std::unique_ptr m_cloud_linear_sampler; + + glm::uvec2 m_output_lo_resolution = {}; + glm::uvec2 m_output_hi_resolution = {}; + std::unique_ptr m_clouds_lo_color_texture; + std::unique_ptr m_clouds_lo_color_texture_view; + std::unique_ptr m_clouds_lo_depth_texture; + std::unique_ptr m_clouds_lo_depth_texture_view; + std::unique_ptr m_clouds_hi_color_texture_a; + std::unique_ptr m_clouds_hi_color_texture_view_a; + std::unique_ptr m_clouds_hi_color_texture_b; + std::unique_ptr m_clouds_hi_color_texture_view_b; + std::unique_ptr m_linear_sampler; + + std::unique_ptr m_render_clouds_bind_group; + std::unique_ptr m_upscale_clouds_bind_group_a; + std::unique_ptr m_upscale_clouds_bind_group_b; + std::unique_ptr m_camera_bind_group; + + std::unique_ptr m_render_clouds_pipeline; + std::unique_ptr m_upscale_clouds_pipeline; + + uint32_t m_stable_frames = 0; + + std::mutex m_mutex = {}; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/HeightLinesOverlay.cpp b/webgpu/engine/overlay/HeightLinesOverlay.cpp new file mode 100644 index 000000000..2e1a72185 --- /dev/null +++ b/webgpu/engine/overlay/HeightLinesOverlay.cpp @@ -0,0 +1,137 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "HeightLinesOverlay.h" + +#include "webgpu/engine/Context.h" +#include +#include +#include + +namespace webgpu_engine { + +HeightLinesOverlay::HeightLinesOverlay() + : Overlay() +{ +} + +void HeightLinesOverlay::init(Context& context) +{ + webgpu::Context& ctx = context.webgpu_ctx(); + m_ctx = &ctx; + + auto& reg = ctx.resource_registry(); + if (!reg.has_shader("height_lines_compute")) + reg.register_shader("height_lines_compute", "webgpu_engine::overlays/height_lines"); + if (!reg.has_bind_group_layout("height_lines_overlay")) + reg.register_bind_group_layout("height_lines_overlay", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry position_entry {}; + position_entry.binding = 0; + position_entry.visibility = WGPUShaderStage_Compute; + position_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + position_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry normal_entry {}; + normal_entry.binding = 1; + normal_entry.visibility = WGPUShaderStage_Compute; + normal_entry.texture.sampleType = WGPUTextureSampleType_Uint; + normal_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry settings_entry {}; + settings_entry.binding = 2; + settings_entry.visibility = WGPUShaderStage_Compute; + settings_entry.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry output_entry {}; + output_entry.binding = 3; + output_entry.visibility = WGPUShaderStage_Compute; + output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + output_entry.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry prev_output_entry {}; + prev_output_entry.binding = 4; + prev_output_entry.visibility = WGPUShaderStage_Compute; + prev_output_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + prev_output_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { position_entry, normal_entry, settings_entry, output_entry, prev_output_entry }, + "height lines overlay bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("height_lines_compute"), + std::vector { + ®.bind_group_layout("shared_config"), + ®.bind_group_layout("camera"), + ®.bind_group_layout("height_lines_overlay"), + }, + "height lines compute pipeline"); + }); + + m_settings_uniform = std::make_unique>(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + m_settings_uniform->data = settings; + m_settings_uniform->update_gpu_data(ctx.queue()); +} + +void HeightLinesOverlay::update_settings() +{ + if (!m_settings_uniform) + return; + m_settings_uniform->data = settings; + m_settings_uniform->update_gpu_data(m_ctx->queue()); +} + +void HeightLinesOverlay::draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& /*overlay_view*/, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) +{ + if (!m_pipeline) + return; + + webgpu::raii::BindGroup bind_group(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("height_lines_overlay"), + std::vector { + position_view.create_bind_group_entry(0), + normal_view.create_bind_group_entry(1), + m_settings_uniform->raw_buffer().create_bind_group_entry(2), + target_output.texture_view().create_bind_group_entry(3), + current_input.texture_view().create_bind_group_entry(4), + }, + "height lines overlay bind group"); + + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "height lines compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(command_encoder, compute_pass_desc); + + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, shared_config_bg, 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 1, camera_bg, 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 2, bind_group.handle(), 0, nullptr); + + const glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(float(output_size.x), float(output_size.y), 1.0f) / glm::vec3(16.0f, 16.0f, 1.0f)); + m_pipeline->run(compute_pass, workgroup_counts); +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/HeightLinesOverlay.h b/webgpu/engine/overlay/HeightLinesOverlay.h new file mode 100644 index 000000000..c7a3e85e6 --- /dev/null +++ b/webgpu/engine/overlay/HeightLinesOverlay.h @@ -0,0 +1,61 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Overlay.h" +#include +#include +#include +#include + +namespace webgpu_engine { + +class HeightLinesOverlay : public Overlay { +public: + struct Settings { + float primary_interval = 250.0f; // major contour interval in meters + float secondary_interval = 50.0f; // minor contour interval in meters + float base_width = 2.0f; // line base width + float minor_opacity = 0.75f; // minor line opacity relative to major + glm::vec4 line_color = glm::vec4(1.0f, 1.0f, 1.0f, 0.3f); // color of the height lines + }; + + HeightLinesOverlay(); + + void init(Context& ctx) override; + void update_settings(); + void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) override; + + Settings settings; + +private: + webgpu::Context* m_ctx = nullptr; + std::unique_ptr m_pipeline; + std::unique_ptr> m_settings_uniform; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/Overlay.h b/webgpu/engine/overlay/Overlay.h new file mode 100644 index 000000000..d867e49b8 --- /dev/null +++ b/webgpu/engine/overlay/Overlay.h @@ -0,0 +1,62 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +class Context; + +/// Abstract base class for screen-space overlays rendered by OverlayRenderer. +class Overlay { +public: + // NOTE: z_index < 0 -> pre-shading bucket, z_index >= 0 -> post-shading bucket + int z_index = 0; + + std::string name; + + virtual ~Overlay() = default; + // Initialization code for GPU Ressources goes here. Needs to be called after creation. + virtual void init(Context& ctx) = 0; + // Called once all shared GPU resources (compiled shaders, pipelines, bind group layouts) + // have been created. From here on it is safe to use them and to upload GPU data. + virtual void ready(webgpu::Context& /*ctx*/) { } + // IMPORTANT: Ping-pong contract (OverlayRenderer owns both textures, RGBA8Unorm, distinct): + // read the previous overlay state from current_input, write the composited result to target_output, + // and write EVERY pixel. Skipping a pixel leaves color in undefined state! + // Use man + virtual void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, // GBuffer slot 3 (packed tile-debug data) + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) + = 0; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/OverlayRenderer.cpp b/webgpu/engine/overlay/OverlayRenderer.cpp new file mode 100644 index 000000000..d7e8a3bae --- /dev/null +++ b/webgpu/engine/overlay/OverlayRenderer.cpp @@ -0,0 +1,185 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2025 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "OverlayRenderer.h" + +#include "webgpu/engine/Context.h" +#include +#include +#include + +namespace webgpu_engine { + +OverlayRenderer::OverlayRenderer() + : QObject { nullptr } +{ +} + +void OverlayRenderer::add_overlay(std::shared_ptr overlay) +{ + // Auto-assign the highest positive z_index + int max_z = 0; + for (const auto& o : m_overlays) + max_z = std::max(max_z, o->z_index); + overlay->z_index = max_z + 1; + + if (m_ctx) { + overlay->init(*m_ctx); + if (m_is_ready) + overlay->ready(m_ctx->webgpu_ctx()); + } + m_overlays.push_back(std::move(overlay)); + rebucket(); +} + +void OverlayRenderer::remove_overlay(size_t index) +{ + if (index < m_overlays.size()) { + m_overlays.erase(m_overlays.begin() + static_cast(index)); + rebucket(); + } +} + +const std::vector>& OverlayRenderer::overlays() const { return m_overlays; } + +void OverlayRenderer::sort_overlays() +{ + std::sort(m_overlays.begin(), m_overlays.end(), [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); + rebucket(); +} + +void OverlayRenderer::rebucket() +{ + m_pre_overlays.clear(); + m_post_overlays.clear(); + for (const auto& overlay : m_overlays) + (overlay->z_index < 0 ? m_pre_overlays : m_post_overlays).push_back(overlay.get()); +} + +void OverlayRenderer::init(Context& ctx) +{ + m_ctx = &ctx; + + for (auto& overlay : m_overlays) + overlay->init(ctx); +} + +void OverlayRenderer::ready(webgpu::Context& ctx) +{ + m_is_ready = true; + for (auto& overlay : m_overlays) + overlay->ready(ctx); +} + +std::unique_ptr OverlayRenderer::create_output_texture(int w, int h, const char* label) const +{ + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = label, .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension_2D; + texture_desc.size = { uint32_t(w), uint32_t(h), 1 }; + texture_desc.mipLevelCount = 1; + texture_desc.sampleCount = 1; + texture_desc.format = WGPUTextureFormat_RGBA8Unorm; + texture_desc.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_StorageBinding | WGPUTextureUsage_CopySrc | WGPUTextureUsage_TextureBinding; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = label, .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode_Nearest; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Nearest; + sampler_desc.minFilter = WGPUFilterMode_Nearest; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = 1.0f; + sampler_desc.compare = WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + return std::make_unique(m_ctx->webgpu_ctx().device(), texture_desc, sampler_desc); +} + +void OverlayRenderer::resize(int w, int h) +{ + if (!m_ctx) + return; + m_pre[0] = create_output_texture(w, h, "overlay pre-shading texture 0"); + m_pre[1] = create_output_texture(w, h, "overlay pre-shading texture 1"); + m_post[0] = create_output_texture(w, h, "overlay post-shading texture 0"); + m_post[1] = create_output_texture(w, h, "overlay post-shading texture 1"); +} + +void OverlayRenderer::draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg) +{ + const glm::uvec2 output_size(m_pre[0]->texture().width(), m_pre[0]->texture().height()); + draw_bucket(command_encoder, m_pre_overlays, m_pre, position_view, normal_view, overlay_view, shared_config_bg, camera_bg, output_size); + draw_bucket(command_encoder, m_post_overlays, m_post, position_view, normal_view, overlay_view, shared_config_bg, camera_bg, output_size); +} + +void OverlayRenderer::draw_bucket(const WGPUCommandEncoder& command_encoder, + const std::vector& bucket, + TexturePair& tex, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + glm::uvec2 output_size) +{ + // Start on T[N % 2] so the final write lands on index 0 + int current = static_cast(bucket.size() % 2); + + // Clear the start texture + WGPURenderPassColorAttachment clear_attachment {}; + clear_attachment.view = tex[static_cast(current)]->texture_view().handle(); + clear_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + clear_attachment.loadOp = WGPULoadOp_Clear; + clear_attachment.storeOp = WGPUStoreOp_Store; + clear_attachment.clearValue = { 0.0, 0.0, 0.0, 0.0 }; + WGPURenderPassDescriptor clear_pass_desc {}; + clear_pass_desc.colorAttachmentCount = 1; + clear_pass_desc.colorAttachments = &clear_attachment; + { + webgpu::raii::RenderPassEncoder clear_pass(command_encoder, clear_pass_desc); + } + + for (auto& overlay : bucket) { + const int target = current ^ 1; + overlay->draw(command_encoder, + position_view, + normal_view, + overlay_view, + shared_config_bg, + camera_bg, + *tex[static_cast(current)], + *tex[static_cast(target)], + output_size); + current = target; + } +} + +const webgpu::raii::TextureView* OverlayRenderer::result_pre_view() const { return m_pre[0] ? &m_pre[0]->texture_view() : nullptr; } + +const webgpu::raii::TextureView* OverlayRenderer::result_post_view() const { return m_post[0] ? &m_post[0]->texture_view() : nullptr; } + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/OverlayRenderer.h b/webgpu/engine/overlay/OverlayRenderer.h new file mode 100644 index 000000000..f51848950 --- /dev/null +++ b/webgpu/engine/overlay/OverlayRenderer.h @@ -0,0 +1,92 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Overlay.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +class Context; + +class OverlayRenderer : public QObject { + Q_OBJECT +public: + explicit OverlayRenderer(); + + void add_overlay(std::shared_ptr overlay); + void remove_overlay(size_t index); + // Re-sort m_overlays by z_index after the GUI has assigned new z_indices. + // IMPORTANT: Call after any change to the overlay set or its z_indices. + void sort_overlays(); + + [[nodiscard]] const std::vector>& overlays() const; + + void init(Context& ctx); + // Called once after all GPU resources are created (and the initial setup is done). + void ready(webgpu::Context& ctx); + void resize(int w, int h); + + void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg); + + [[nodiscard]] const webgpu::raii::TextureView* result_pre_view() const; + [[nodiscard]] const webgpu::raii::TextureView* result_post_view() const; + +private: + using TexturePair = std::array, 2>; + + std::unique_ptr create_output_texture(int w, int h, const char* label) const; + // Refill m_pre_overlays / m_post_overlays from m_overlays (z_index < 0 -> pre otherwise post), + // preserving order. + void rebucket(); + + void draw_bucket(const WGPUCommandEncoder& command_encoder, + const std::vector& bucket, + TexturePair& tex, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + glm::uvec2 output_size); + + Context* m_ctx = nullptr; + bool m_is_ready = false; + std::vector> m_overlays; // owning; sorted by z_index ascending + // Per-bucket, non-owning views into m_overlays (non-canonical, rebuilt by rebucket()), iterated directly by draw(). + std::vector m_pre_overlays; // z_index < 0 + std::vector m_post_overlays; // z_index >= 0 + // Ping-pong textures per bucket. Index 0 is the persistent result + TexturePair m_pre; + TexturePair m_post; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/ScreenSpaceSnowOverlay.cpp b/webgpu/engine/overlay/ScreenSpaceSnowOverlay.cpp new file mode 100644 index 000000000..78378c76e --- /dev/null +++ b/webgpu/engine/overlay/ScreenSpaceSnowOverlay.cpp @@ -0,0 +1,138 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "ScreenSpaceSnowOverlay.h" + +#include "webgpu/engine/Context.h" +#include +#include +#include + +namespace webgpu_engine { + +ScreenSpaceSnowOverlay::ScreenSpaceSnowOverlay() + : Overlay() +{ +} + +void ScreenSpaceSnowOverlay::init(Context& context) +{ + webgpu::Context& ctx = context.webgpu_ctx(); + m_ctx = &ctx; + + auto& reg = ctx.resource_registry(); + if (!reg.has_shader("screen_space_snow_compute")) + reg.register_shader("screen_space_snow_compute", "webgpu_engine::overlays/screen_space_snow"); + if (!reg.has_bind_group_layout("screen_space_snow_overlay")) + reg.register_bind_group_layout("screen_space_snow_overlay", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry position_entry {}; + position_entry.binding = 0; + position_entry.visibility = WGPUShaderStage_Compute; + position_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + position_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry normal_entry {}; + normal_entry.binding = 1; + normal_entry.visibility = WGPUShaderStage_Compute; + normal_entry.texture.sampleType = WGPUTextureSampleType_Uint; + normal_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry settings_entry {}; + settings_entry.binding = 2; + settings_entry.visibility = WGPUShaderStage_Compute; + settings_entry.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry output_entry {}; + output_entry.binding = 3; + output_entry.visibility = WGPUShaderStage_Compute; + output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + output_entry.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry prev_output_entry {}; + prev_output_entry.binding = 4; + prev_output_entry.visibility = WGPUShaderStage_Compute; + prev_output_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + prev_output_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { position_entry, normal_entry, settings_entry, output_entry, prev_output_entry }, + "screen space snow overlay bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("screen_space_snow_compute"), + std::vector { + ®.bind_group_layout("shared_config"), + ®.bind_group_layout("camera"), + ®.bind_group_layout("screen_space_snow_overlay"), + }, + "screen space snow compute pipeline"); + }); + + m_settings_uniform = std::make_unique>(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + m_settings_uniform->data = settings; + m_settings_uniform->update_gpu_data(ctx.queue()); +} + +void ScreenSpaceSnowOverlay::update_settings() +{ + if (!m_settings_uniform) + return; + m_settings_uniform->data = settings; + m_settings_uniform->update_gpu_data(m_ctx->queue()); +} + +void ScreenSpaceSnowOverlay::draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& /*overlay_view*/, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) +{ + if (!m_pipeline) + return; + + webgpu::raii::BindGroup bind_group(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("screen_space_snow_overlay"), + std::vector { + position_view.create_bind_group_entry(0), + normal_view.create_bind_group_entry(1), + m_settings_uniform->raw_buffer().create_bind_group_entry(2), + target_output.texture_view().create_bind_group_entry(3), + current_input.texture_view().create_bind_group_entry(4), + }, + "screen space snow overlay bind group"); + + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "screen space snow compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(command_encoder, compute_pass_desc); + + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, shared_config_bg, 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 1, camera_bg, 0, nullptr); + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 2, bind_group.handle(), 0, nullptr); + + const glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(float(output_size.x), float(output_size.y), 1.0f) / glm::vec3(16.0f, 16.0f, 1.0f)); + m_pipeline->run(compute_pass, workgroup_counts); +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/ScreenSpaceSnowOverlay.h b/webgpu/engine/overlay/ScreenSpaceSnowOverlay.h new file mode 100644 index 000000000..fe6cf14c0 --- /dev/null +++ b/webgpu/engine/overlay/ScreenSpaceSnowOverlay.h @@ -0,0 +1,65 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Overlay.h" +#include +#include +#include +#include + +namespace webgpu_engine { + +class ScreenSpaceSnowOverlay : public Overlay { +public: + struct Settings { + float angle_min = 0.0f; // steepness lower limit (deg) + float angle_max = 45.0f; // steepness upper limit (deg) + float angle_blend = 5.0f; // band smoothing (deg) + float altitude_limit = 1000.0f; // snow line altitude (m) + float altitude_variation = 200.0f; // noise variation around the snow line (m) + float altitude_blend = 200.0f; // altitude falloff (m) + float transparency = 1.0f; // overlay opacity [0, 1] + float _pad = 0.0f; + }; + + ScreenSpaceSnowOverlay(); + + void init(Context& ctx) override; + void update_settings(); + void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) override; + + Settings settings; + +private: + webgpu::Context* m_ctx = nullptr; + std::unique_ptr m_pipeline; + std::unique_ptr> m_settings_uniform; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/TextureOverlay.cpp b/webgpu/engine/overlay/TextureOverlay.cpp new file mode 100644 index 000000000..496782ceb --- /dev/null +++ b/webgpu/engine/overlay/TextureOverlay.cpp @@ -0,0 +1,232 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TextureOverlay.h" + +#include "webgpu/engine/Context.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +TextureOverlay::TextureOverlay() + : Overlay() +{ +} + +void TextureOverlay::load_image(const QString& path) +{ + assert(m_is_ready && "load_image must be called after ready()"); + m_linked_texture = nullptr; // owned source takes over + const auto image = nucleus::utils::image_loader::rgba8(path).value(); + create_texture(*m_ctx, uint32_t(image.width()), uint32_t(image.height())); + m_overlay_texture->texture().write(m_ctx->queue(), image); + if (settings.use_mipmaps) + webgpu::compute_mipmaps_for_texture(*m_ctx, &m_overlay_texture->texture()); +} + +void TextureOverlay::link_texture(const webgpu::raii::TextureWithSampler* texture) { m_linked_texture = texture; } + +void TextureOverlay::load_texture(const webgpu::raii::TextureWithSampler& source) +{ + assert(m_is_ready && "load_texture must be called after ready()"); + assert(source.texture().descriptor().format == WGPUTextureFormat_RGBA8Unorm && "load_texture requires an RGBA8Unorm source texture"); + + m_linked_texture = nullptr; // owned source takes over + create_texture(*m_ctx, uint32_t(source.texture().width()), uint32_t(source.texture().height())); + + // GPU -> GPU copy of mip 0 into our own texture + WGPUCommandEncoderDescriptor encoder_desc {}; + webgpu::raii::CommandEncoder encoder(m_ctx->device(), encoder_desc); + source.texture().copy_to_texture(encoder.handle(), 0, m_overlay_texture->texture(), 0); + WGPUCommandBufferDescriptor cmd_desc {}; + WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder.handle(), &cmd_desc); + wgpuQueueSubmit(m_ctx->queue(), 1, &command); + wgpuCommandBufferRelease(command); + + if (settings.use_mipmaps) + webgpu::compute_mipmaps_for_texture(*m_ctx, &m_overlay_texture->texture()); +} + +void TextureOverlay::init(Context& context) +{ + webgpu::Context& ctx = context.webgpu_ctx(); + m_ctx = &ctx; + + auto& reg = ctx.resource_registry(); + if (!reg.has_shader("texture_overlay_render")) + reg.register_shader("texture_overlay_render", "webgpu_engine::overlays/texture_overlay"); + if (!reg.has_bind_group_layout("texture_overlay")) + reg.register_bind_group_layout("texture_overlay", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry position_entry {}; + position_entry.binding = 0; + position_entry.visibility = WGPUShaderStage_Fragment; + position_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + position_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry settings_entry {}; + settings_entry.binding = 1; + settings_entry.visibility = WGPUShaderStage_Fragment; + settings_entry.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry overlay_texture_entry {}; + overlay_texture_entry.binding = 2; + overlay_texture_entry.visibility = WGPUShaderStage_Fragment; + overlay_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + overlay_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry overlay_sampler_entry {}; + overlay_sampler_entry.binding = 3; + overlay_sampler_entry.visibility = WGPUShaderStage_Fragment; + overlay_sampler_entry.sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutEntry background_entry {}; + background_entry.binding = 4; + background_entry.visibility = WGPUShaderStage_Fragment; + background_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + background_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { position_entry, settings_entry, overlay_texture_entry, overlay_sampler_entry, background_entry }, + "texture overlay bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + webgpu::FramebufferFormat format {}; + format.depth_format = WGPUTextureFormat_Undefined; + format.color_formats = { WGPUTextureFormat_RGBA8Unorm }; + + m_pipeline = std::make_unique(device, + reg.shader("texture_overlay_render"), + reg.shader("texture_overlay_render"), + std::vector {}, + format, + std::vector { + ®.bind_group_layout("shared_config"), + ®.bind_group_layout("camera"), + ®.bind_group_layout("texture_overlay"), + }); + }); + + m_settings_uniform = std::make_unique>(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + update_gpu_settings(); + create_texture(context.webgpu_ctx(), 1, 1); +} + +void TextureOverlay::ready(webgpu::Context& ctx) { m_is_ready = true; } + +void TextureOverlay::create_texture(webgpu::Context& ctx, uint32_t width, uint32_t height) +{ + const bool mipmaps = settings.use_mipmaps; + const auto mip_levels = mipmaps ? webgpu::raii::Texture::max_mip_level_count(glm::uvec2(width, height)) : 1u; + + const auto filter = settings.filter_mode == FilterMode::Linear ? WGPUFilterMode_Linear : WGPUFilterMode_Nearest; + const auto mip_filter = (mipmaps && settings.filter_mode == FilterMode::Linear) ? WGPUMipmapFilterMode_Linear : WGPUMipmapFilterMode_Nearest; + + WGPUTextureDescriptor texture_desc {}; + texture_desc.label = WGPUStringView { .data = "texture overlay input texture", .length = WGPU_STRLEN }; + texture_desc.dimension = WGPUTextureDimension_2D; + texture_desc.size = { width, height, 1 }; + texture_desc.mipLevelCount = mip_levels; + texture_desc.sampleCount = 1; + texture_desc.format = WGPUTextureFormat_RGBA8Unorm; + // CopyDst: write()/copy_to_texture() destination; StorageBinding: mipmap compute shader. + texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_StorageBinding; + + WGPUSamplerDescriptor sampler_desc {}; + sampler_desc.label = WGPUStringView { .data = "texture overlay sampler", .length = WGPU_STRLEN }; + sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeW = WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = filter; + sampler_desc.minFilter = filter; + sampler_desc.mipmapFilter = mip_filter; + sampler_desc.lodMinClamp = 0.0f; + sampler_desc.lodMaxClamp = float(mip_levels); + sampler_desc.compare = WGPUCompareFunction_Undefined; + sampler_desc.maxAnisotropy = 1; + + m_overlay_texture = std::make_unique(ctx.device(), texture_desc, sampler_desc); +} + +void TextureOverlay::update_gpu_settings() +{ + m_settings_uniform->data.aabb_min = glm::vec2(settings.aabb.min); + m_settings_uniform->data.aabb_size = glm::vec2(settings.aabb.size()); + m_settings_uniform->data.opacity = settings.opacity; + m_settings_uniform->data.mode = static_cast(settings.mode); + m_settings_uniform->data.float_decode_range = settings.float_decode_range; + m_settings_uniform->data.encoded_float_range = glm::vec2(nucleus::utils::geopng::ENCODED_FLOAT_RANGE_MIN, nucleus::utils::geopng::ENCODED_FLOAT_RANGE_MAX); + m_settings_uniform->update_gpu_data(m_ctx->queue()); +} + +void TextureOverlay::draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& /*normal_view*/, + const webgpu::raii::TextureView& /*overlay_view*/, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 /*output_size*/) +{ + const webgpu::raii::TextureWithSampler* tex = m_linked_texture ? m_linked_texture : m_overlay_texture.get(); + assert(tex && m_pipeline); + + webgpu::raii::BindGroup bind_group(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("texture_overlay"), + std::vector { + position_view.create_bind_group_entry(0), + m_settings_uniform->raw_buffer().create_bind_group_entry(1), + tex->texture_view().create_bind_group_entry(2), + tex->sampler().create_bind_group_entry(3), + current_input.texture_view().create_bind_group_entry(4), + }, + "texture overlay bind group"); + + // The fullscreen triangle writes every pixel, so loadOp doesnt matter (clear=fastest) + WGPURenderPassColorAttachment color_attachment {}; + color_attachment.view = target_output.texture_view().handle(); + color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + color_attachment.loadOp = WGPULoadOp_Clear; + color_attachment.storeOp = WGPUStoreOp_Store; + color_attachment.clearValue = { 0.0, 0.0, 0.0, 0.0 }; + + WGPURenderPassDescriptor render_pass_desc {}; + render_pass_desc.label = WGPUStringView { .data = "texture overlay render pass", .length = WGPU_STRLEN }; + render_pass_desc.colorAttachmentCount = 1; + render_pass_desc.colorAttachments = &color_attachment; + + webgpu::raii::RenderPassEncoder render_pass(command_encoder, render_pass_desc); + + wgpuRenderPassEncoderSetPipeline(render_pass.handle(), m_pipeline->pipeline().handle()); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 0, shared_config_bg, 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 1, camera_bg, 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 2, bind_group.handle(), 0, nullptr); + wgpuRenderPassEncoderDraw(render_pass.handle(), 3, 1, 0, 0); +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/TextureOverlay.h b/webgpu/engine/overlay/TextureOverlay.h new file mode 100644 index 000000000..6158d6fb9 --- /dev/null +++ b/webgpu/engine/overlay/TextureOverlay.h @@ -0,0 +1,94 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Overlay.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +class TextureOverlay : public Overlay { +public: + enum class FilterMode { Nearest, Linear }; + enum class Mode { AlphaBlend, EncodedFloat }; + struct Settings { + radix::geometry::Aabb<2, double> aabb = { { 0.0, 0.0 }, { 1.0, 1.0 } }; // world-space extent + float opacity = 1.0f; + Mode mode = Mode::AlphaBlend; + glm::vec2 float_decode_range = glm::vec2(0.0f, 20.0f); // [lower, upper] for EncodedFloat mode + FilterMode filter_mode = FilterMode::Linear; // filter_mode/use_mipmaps take effect on next load_image + bool use_mipmaps = true; + }; + Settings settings; + + TextureOverlay(); + + // Load an RGBA8 image from disk into the overlays own texture. + void load_image(const QString& path); + + // Copy an external GPU texture into the overlays own texture. + void load_texture(const webgpu::raii::TextureWithSampler& source); + + // Link an external GPU texture directly (non-owning). The caller must keep it alive while linked. + // pass nullptr to unlink. + void link_texture(const webgpu::raii::TextureWithSampler* texture); + [[nodiscard]] bool is_linked() const { return m_linked_texture != nullptr; } + + void init(Context& ctx) override; + void ready(webgpu::Context& ctx) override; + void update_gpu_settings(); + void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) override; + +private: + struct GpuSettings { + glm::vec2 aabb_min = glm::vec2(0.0f); + glm::vec2 aabb_size = glm::vec2(1.0f); + float opacity = 1.0f; + uint32_t mode = 0u; // (0=AlphaBlend, 1=EncodedFloat) + glm::vec2 float_decode_range = glm::vec2(0.0f, 20.0f); // user visualization range + glm::vec2 encoded_float_range; // encoding format range (defined in geopng module) + }; + + void create_texture(webgpu::Context& ctx, uint32_t width, uint32_t height); + + webgpu::Context* m_ctx = nullptr; + bool m_is_ready = false; + + std::unique_ptr m_pipeline; + std::unique_ptr> m_settings_uniform; + std::unique_ptr m_overlay_texture; // owned source (load_image/load_texture) + const webgpu::raii::TextureWithSampler* m_linked_texture = nullptr; // borrowed source (link_texture) +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/TileDebugOverlay.cpp b/webgpu/engine/overlay/TileDebugOverlay.cpp new file mode 100644 index 000000000..6d42b060b --- /dev/null +++ b/webgpu/engine/overlay/TileDebugOverlay.cpp @@ -0,0 +1,137 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TileDebugOverlay.h" + +#include "webgpu/engine/Context.h" +#include +#include +#include + +namespace webgpu_engine { + +TileDebugOverlay::TileDebugOverlay() + : Overlay() +{ +} + +TileDebugOverlay::~TileDebugOverlay() +{ + if (m_engine_ctx) + m_engine_ctx->shared_config().m_overlay_mode = 0; +} + +void TileDebugOverlay::init(Context& context) +{ + webgpu::Context& ctx = context.webgpu_ctx(); + m_ctx = &ctx; + m_engine_ctx = &context; + + auto& reg = ctx.resource_registry(); + if (!reg.has_shader("gbuffer_debug_compute")) + reg.register_shader("gbuffer_debug_compute", "webgpu_engine::overlays/gbuffer_debug"); + if (!reg.has_bind_group_layout("tile_debug_overlay")) + reg.register_bind_group_layout("tile_debug_overlay", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry overlay_entry {}; + overlay_entry.binding = 0; + overlay_entry.visibility = WGPUShaderStage_Compute; + overlay_entry.texture.sampleType = WGPUTextureSampleType_Uint; + overlay_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry settings_entry {}; + settings_entry.binding = 1; + settings_entry.visibility = WGPUShaderStage_Compute; + settings_entry.buffer.type = WGPUBufferBindingType_Uniform; + + WGPUBindGroupLayoutEntry output_entry {}; + output_entry.binding = 2; + output_entry.visibility = WGPUShaderStage_Compute; + output_entry.storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + output_entry.storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + output_entry.storageTexture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutEntry prev_output_entry {}; + prev_output_entry.binding = 3; + prev_output_entry.visibility = WGPUShaderStage_Compute; + prev_output_entry.texture.sampleType = WGPUTextureSampleType_UnfilterableFloat; + prev_output_entry.texture.viewDimension = WGPUTextureViewDimension_2D; + + return std::make_unique(device, + std::vector { overlay_entry, settings_entry, output_entry, prev_output_entry }, + "tile debug overlay bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice device, const webgpu::RenderResourceRegistry& reg) { + m_pipeline = std::make_unique(device, + reg.shader("gbuffer_debug_compute"), + std::vector { + ®.bind_group_layout("tile_debug_overlay"), + }, + "tile debug compute pipeline"); + }); + + m_settings_uniform = std::make_unique>(ctx.device(), WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform); + update_settings(); +} + +void TileDebugOverlay::update_settings() +{ + if (!m_settings_uniform) + return; + m_settings_uniform->data.strength = settings.strength; + m_settings_uniform->update_gpu_data(m_ctx->queue()); + + // NOTE: Only one TileDebugOverlay possible due to GBuffer layout + if (m_engine_ctx) + m_engine_ctx->shared_config().m_overlay_mode = static_cast(settings.mode); +} + +void TileDebugOverlay::draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& /*position_view*/, + const webgpu::raii::TextureView& /*normal_view*/, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& /*shared_config_bg*/, + const WGPUBindGroup& /*camera_bg*/, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) +{ + if (!m_pipeline) + return; + + webgpu::raii::BindGroup bind_group(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("tile_debug_overlay"), + std::vector { + overlay_view.create_bind_group_entry(0), + m_settings_uniform->raw_buffer().create_bind_group_entry(1), + target_output.texture_view().create_bind_group_entry(2), + current_input.texture_view().create_bind_group_entry(3), + }, + "tile debug overlay bind group"); + + WGPUComputePassDescriptor compute_pass_desc {}; + compute_pass_desc.label = WGPUStringView { .data = "tile debug compute pass", .length = WGPU_STRLEN }; + webgpu::raii::ComputePassEncoder compute_pass(command_encoder, compute_pass_desc); + + wgpuComputePassEncoderSetBindGroup(compute_pass.handle(), 0, bind_group.handle(), 0, nullptr); + + const glm::uvec3 workgroup_counts = glm::ceil(glm::vec3(float(output_size.x), float(output_size.y), 1.0f) / glm::vec3(16.0f, 16.0f, 1.0f)); + m_pipeline->run(compute_pass, workgroup_counts); +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/overlay/TileDebugOverlay.h b/webgpu/engine/overlay/TileDebugOverlay.h new file mode 100644 index 000000000..11afca07c --- /dev/null +++ b/webgpu/engine/overlay/TileDebugOverlay.h @@ -0,0 +1,76 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Overlay.h" +#include +#include +#include +#include + +namespace webgpu_engine { + +// Displays the per-tile debug data that render_tiles.wgsl packs into GBuffer slot 3. +class TileDebugOverlay : public Overlay { +public: + // Values must match the overlay_mode branches in render_tiles.wgsl. + enum class Mode : int { + Normals = 1, + Tiles = 2, + Zoomlevel = 3, + VertexId = 4, + }; + + struct Settings { + int mode = static_cast(Mode::Normals); // consumed CPU-side (forwarded to shared_config) + float strength = 1.0f; + }; + + TileDebugOverlay(); + ~TileDebugOverlay() override; + + void init(Context& ctx) override; + // Pushes settings to the GPU and the selected debug mode into shared_config (consumed by the tile pass). + // Call from the frontend whenever settings change. + void update_settings(); + void draw(const WGPUCommandEncoder& command_encoder, + const webgpu::raii::TextureView& position_view, + const webgpu::raii::TextureView& normal_view, + const webgpu::raii::TextureView& overlay_view, + const WGPUBindGroup& shared_config_bg, + const WGPUBindGroup& camera_bg, + const webgpu::raii::TextureWithSampler& current_input, + webgpu::raii::TextureWithSampler& target_output, + glm::uvec2 output_size) override; + + Settings settings; + +private: + struct GpuSettings { + float strength = 1.0f; + }; + + webgpu::Context* m_ctx = nullptr; + Context* m_engine_ctx = nullptr; // for shared_config access (overlay_mode) + std::unique_ptr m_pipeline; + std::unique_ptr> m_settings_uniform; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/shaders/compose_pass.wgsl b/webgpu/engine/shaders/compose_pass.wgsl new file mode 100644 index 000000000..de0974281 --- /dev/null +++ b/webgpu/engine/shaders/compose_pass.wgsl @@ -0,0 +1,237 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2022 Gerald Kimmersdorfer +* Copyright (C) 2024 Patrick Komon +* Copyright (C) 2026 Wendelin Muth +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use util/shared_config +///use util/camera_config +///use util/atmosphere +///use webgpu::encoder +///use webgpu::general +///use webgpu::tile_util + +///use screen_pass_vert + +@group(0) @binding(0) var conf: shared_config; +@group(1) @binding(0) var camera: camera_config; + +@group(2) @binding(0) var albedo_texture: texture_2d; +@group(2) @binding(1) var position_texture: texture_2d; +@group(2) @binding(2) var normal_texture: texture_2d; +@group(2) @binding(3) var atmosphere_texture: texture_2d; +@group(2) @binding(4) var overlay_texture: texture_2d; + +@group(2) @binding(5) var clouds_texture: texture_2d; +@group(2) @binding(6) var clouds_depth_texture: texture_storage_2d; +@group(2) @binding(7) var cloud_shadow_texture: texture_2d; +@group(2) @binding(8) var cloud_shadow_sampler: sampler; +@group(2) @binding(9) var depth_texture: texture_2d; +@group(2) @binding(10) var overlay_renderer_post_texture: texture_2d; +@group(2) @binding(11) var overlay_renderer_pre_texture: texture_2d; + +const CLOUD_SHADOW_AABB_MIN = vec3f(1045658.54694121, 5811660.13457852, 0.0); +const CLOUD_SHADOW_AABB_MAX = vec3f(1937220.04485951, 6309418.06277159, 14000.0); + +//Calculates the diffuse and specular illumination contribution for the given +//parameters according to the Blinn-Phong lighting model. +//All parameters must be normalized. +fn calc_blinn_phong_contribution( + toLight: vec3, + toEye: vec3, + normal: vec3, + diffFactor: vec3, + specFactor: vec3, + specShininess: f32 +) -> vec3 { + let nDotL: f32 = max(0.0, dot(normal, toLight)); //Lambertian coefficient + let h: vec3 = normalize(toLight + toEye); + let nDotH: f32 = max(0.0, dot(normal, h)); + let specPower: f32 = pow(nDotH, specShininess); + let diffuse: vec3 = diffFactor * nDotL; //Component-wise product + let specular: vec3 = specFactor * specPower; + return diffuse + specular; +} + +//Calculates the Blinn-Phong illumination for the given fragment +fn calculate_illumination( + albedo: vec3, + eyePos: vec3, + fragPos: vec3, + fragNorm: vec3, + dirLight: vec4, + ambLight: vec4, + dirDirection: vec3, + material: vec4, + ao: f32, + shadow_term: f32 +) -> vec3 { + let dirColor: vec3 = dirLight.rgb * dirLight.a; + let ambColor: vec3 = ambLight.rgb * ambLight.a; + let ambient: vec3 = material.r * albedo; + let diff: vec3 = material.g * albedo; + let spec: vec3 = vec3(material.b); + let shini: f32 = material.a; + + let ambientIllumination: vec3 = ambient * ambColor * ao; + + let toLightDirWS: vec3 = -normalize(dirDirection); + let toEyeNrmWS: vec3 = normalize(eyePos - fragPos); + let diffAndSpecIllumination: vec3 = dirColor * calc_blinn_phong_contribution(toLightDirWS, toEyeNrmWS, fragNorm, diff, spec, shini); + + return ambientIllumination + diffAndSpecIllumination * (1.0 - shadow_term); +} + +fn get_cloud_shadow_occlusion(world_pos: vec3f) -> f32 { + const SHADOW_BIAS = 0.05; + const ESM_CONSTANT = 4.0; + + //TODO: Future improvement: Implement parallax + + let uv = vec2f( + (world_pos.x - CLOUD_SHADOW_AABB_MIN.x) / (CLOUD_SHADOW_AABB_MAX.x - CLOUD_SHADOW_AABB_MIN.x), + (CLOUD_SHADOW_AABB_MAX.y - world_pos.y) / (CLOUD_SHADOW_AABB_MAX.y - CLOUD_SHADOW_AABB_MIN.y) + ); + + let shadow_map_val = textureSample(cloud_shadow_texture, cloud_shadow_sampler, uv).r; + + let height_adjusted = world_pos.z / cos(y_to_lat(world_pos.y)); + let h_receiver_norm = height_adjusted / CLOUD_SHADOW_AABB_MAX.z + SHADOW_BIAS; + let receiver_val = exp(ESM_CONSTANT * h_receiver_norm); + + //factor from 0.0 (in shadow) to 1.0 (lit) + let shadow_factor = clamp(receiver_val / shadow_map_val, 0.0, 1.0); + + if world_pos.x < CLOUD_SHADOW_AABB_MIN.x || world_pos.x > CLOUD_SHADOW_AABB_MAX.x || + world_pos.y < CLOUD_SHADOW_AABB_MIN.y || world_pos.y > CLOUD_SHADOW_AABB_MAX.y { + return 0.0; + } + + return 1.0 - shadow_factor; +} + +@fragment +fn fragmentMain(vertex_out: VertexOut) -> @location(0) vec4f { + let tci: vec2 = vec2u(vertex_out.texcoords * camera.viewport_size); + + var albedo: vec3f = unpack4x8unorm(textureLoad(albedo_texture, tci, 0).r).xyz; + let pos_dist = textureLoad(position_texture, tci, 0); + let encoded_normal = textureLoad(normal_texture, tci, 0).xy; + + let pos_cws = pos_dist.xyz; + let dist = length(pos_cws); //pos_dist.w + let tile_dist = pos_dist.w; + + let normal = octNormalDecode2u16(encoded_normal); + + var amb_occlusion = 1.0; + /* TODO : Implement ambient occlusion + if (bool(conf.ssao_enabled)) + { + amb_occlusion = texture(texin_ssao, texcoords).r; + }*/ + + let sampled_shadow_layer: i32 = -1; + + let origin = camera.position.xyz; + let pos_ws = pos_cws + origin; + + var out_Color = vec4f(0.0); + let atmospheric_color = textureLoad(atmosphere_texture, vec2u(0, tci.y), 0).rgb; + + var cloud_shadow = 0.0; + if bool(conf.clouds_enabled) { + //must be called from uniform control flow :( + let cloud_shadow_raw = get_cloud_shadow_occlusion(pos_ws); + + //Make it softer + cloud_shadow = cloud_shadow_raw * cloud_shadow_raw * cloud_shadow_raw * cloud_shadow_raw; + } + + //Don't do shading if not visible anyway and also don't for pixels where there is no geometry (depth==0.0) + if dist > 0.0 { + let ray_direction = pos_cws / dist; + var material_light_response = conf.material_light_response; + + //Apply material color by blending with albedo + albedo = mix(albedo, conf.material_color.rgb, conf.material_color.a); + + var shadow_term = cloud_shadow; + amb_occlusion *= 1.0 - cloud_shadow * 0.3; + + /*TODO : implement shadow + if (bool(conf.csm_enabled)) + { + shadow_term = csm_shadow_term(vec4(pos_cws, 1.0), normal, sampled_shadow_layer); + }*/ + + //Pre-shading overlay renderer output (applied to albedo before lighting) + let pre_overlay_color = textureLoad(overlay_renderer_pre_texture, tci, 0); + albedo = albedo * (1.0 - pre_overlay_color.a) + pre_overlay_color.rgb; + + var shaded_color = albedo; + if bool(conf.shading_enabled) { + shaded_color = calculate_illumination(shaded_color, origin, pos_ws, normal, conf.sun_light, conf.amb_light, conf.sun_light_dir.xyz, material_light_response, amb_occlusion, shadow_term); + } + if bool(conf.atmosphere_enabled) { + shaded_color = calculate_atmospheric_light(origin / 1000.0, ray_direction, dist / 1000.0, shaded_color, 10); + } + shaded_color = max(vec3(0.0), shaded_color); + if dist > 0 && bool(conf.atmosphere_enabled) { + let atmosphere_blend = calculate_falloff(dist, 300000.0, 600000.0); + shaded_color = mix(atmospheric_color, shaded_color, atmosphere_blend); + } + out_Color = vec4(shaded_color, 1.0); + } else { + if bool(conf.atmosphere_enabled) { + out_Color = vec4(atmospheric_color, 1.0); + } else { + out_Color = vec4(1.0); + } + } + + //Post-shading overlay renderer output + let post_overlay_color = textureLoad(overlay_renderer_post_texture, tci, 0); + out_Color = vec4f(out_Color.rgb * (1.0 - post_overlay_color.a) + post_overlay_color.rgb, out_Color.a); + + //Clouds + if bool(conf.clouds_enabled) { + let clouds_color = textureLoad(clouds_texture, tci, 0); + let clouds_depth = textureLoad(clouds_depth_texture, tci / 2).x; + + //convert transmittance to alpha + let raw_alpha = 1.0 - clouds_color.a; + let safe_alpha = max(raw_alpha, 0.00001); + let straight_rgb = clouds_color.rgb / safe_alpha; + var tonemapped_rgb = straight_rgb / (straight_rgb + 1.0); + + //atmosphere + if clouds_depth > 0.0 && bool(conf.atmosphere_enabled) { + let atmosphere_blend = calculate_falloff(clouds_depth, 300000.0, 600000.0); + tonemapped_rgb = mix(atmospheric_color, tonemapped_rgb, atmosphere_blend); + } + + var blend_alpha = raw_alpha; + + out_Color = vec4( + out_Color.rgb * (1.0 - blend_alpha) + tonemapped_rgb * blend_alpha, + 1.0 - (1.0 - out_Color.a) * (1.0 - blend_alpha) + ); + } + + return out_Color; +} diff --git a/webgpu/engine/shaders/overlays/gbuffer_debug.wgsl b/webgpu/engine/shaders/overlays/gbuffer_debug.wgsl new file mode 100644 index 000000000..a35a7ffc8 --- /dev/null +++ b/webgpu/engine/shaders/overlays/gbuffer_debug.wgsl @@ -0,0 +1,46 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2026 Gerald Kimmersdorfer +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +@group(0) @binding(0) var overlay_texture: texture_2d; //GBuffer slot 3 (packed RGBA via pack4x8unorm) +@group(0) @binding(1) var settings: TileDebugSettings; +@group(0) @binding(2) var output_texture: texture_storage_2d; +@group(0) @binding(3) var prev_output: texture_2d; + +struct TileDebugSettings { + strength: f32, +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3u) { + let dims = vec2u(textureDimensions(output_texture)); + if id.x >= dims.x || id.y >= dims.y { + return; + } + let tci = id.xy; + + //render_tiles.wgsl writes alpha = 1 on geometry, 0 (transparent) on background. + let packed = textureLoad(overlay_texture, tci, 0).r; + var overlay_color = unpack4x8unorm(packed); + overlay_color.a = overlay_color.a * settings.strength; + + //Blend over previous overlay in premultiplied alpha space: + let prev = textureLoad(prev_output, tci, 0); + let src_premul = vec4f(overlay_color.rgb * overlay_color.a, overlay_color.a); + let blended = src_premul + prev * (1.0 - overlay_color.a); + textureStore(output_texture, tci, blended); +} diff --git a/webgpu/engine/shaders/overlays/height_lines.wgsl b/webgpu/engine/shaders/overlays/height_lines.wgsl new file mode 100644 index 000000000..361adcbce --- /dev/null +++ b/webgpu/engine/shaders/overlays/height_lines.wgsl @@ -0,0 +1,104 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2026 Gerald Kimmersdorfer +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use util/shared_config +///use util/camera_config +///use webgpu::encoder +///use webgpu::tile_util + +@group(0) @binding(0) var conf: shared_config; +@group(1) @binding(0) var camera: camera_config; +@group(2) @binding(0) var position_texture: texture_2d; +@group(2) @binding(1) var normal_texture: texture_2d; +@group(2) @binding(2) var settings: HeightLinesSettings; +@group(2) @binding(3) var output_texture: texture_storage_2d; +@group(2) @binding(4) var prev_output: texture_2d; + +struct HeightLinesSettings { + primary_interval: f32, + secondary_interval: f32, + base_width: f32, + minor_opacity: f32, + line_color: vec4f, +} + +//Returns the blending alpha for a height line at this pixel, or 0 if no line. +fn get_height_line_alpha(pos_ws: vec3f, normal: vec3f, dist: f32, interval: f32, base_width: f32, aa_scale: f32) -> f32 { + let alpha_line = 1.0 - min(dist / 20000.0, 1.0); + if alpha_line <= 0.01 { + return 0.0; + } + + var line_width = (1.0 + dist / 5000.0) * base_width; + var aa_scaled = 0.0; + if aa_scale > 0.0 { + aa_scaled = max(dist / 2000.0 * aa_scale, 0.03); + } + + let steepness = acos(clamp(normal.z, -1.0, 1.0)); + line_width = line_width * max(0.01, steepness); + + let latitude_correction = cos(y_to_lat(pos_ws.y)); + let altitude = pos_ws.z * latitude_correction; + let fractional_part = altitude - f32(i32(altitude / interval)) * interval; + let dist_from_line = min(fractional_part, interval - fractional_part); + let dist_from_edge = (line_width / 2.0) - dist_from_line; + + if abs(dist_from_edge) < aa_scaled { + let aa_factor = smoothstep(-aa_scaled, aa_scaled, dist_from_edge); + let effective_alpha = alpha_line * aa_factor; + if effective_alpha > 0.01 { + return effective_alpha; + } + } else if dist_from_edge > 0.0 { + return alpha_line; + } + return 0.0; +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3u) { + let dims = vec2u(textureDimensions(position_texture)); + if id.x >= dims.x || id.y >= dims.y { + return; + } + let tci = id.xy; + + let pos_dist = textureLoad(position_texture, tci, 0); + let encoded_normal = textureLoad(normal_texture, tci, 0).xy; + + let pos_cws = pos_dist.xyz; + let dist = length(pos_cws); + let pos_ws = pos_cws + camera.position.xyz; + let normal = octNormalDecode2u16(encoded_normal); + + var out_color = vec4f(0.0); + + if dist > 0.0 { + let major = get_height_line_alpha(pos_ws, normal, dist, settings.primary_interval, settings.base_width, 1.0); + let minor = get_height_line_alpha(pos_ws, normal, dist, settings.secondary_interval, settings.base_width * 0.5, 1.0); + let alpha = max(major, minor * settings.minor_opacity) * settings.line_color.a; + out_color = vec4f(settings.line_color.rgb, alpha); + } + + //Blend over previous overlay in premultiplied alpha space: + let prev = textureLoad(prev_output, tci, 0); + let src_premul = vec4f(out_color.rgb * out_color.a, out_color.a); + let blended = src_premul + prev * (1.0 - out_color.a); + textureStore(output_texture, tci, blended); +} diff --git a/webgpu/engine/shaders/overlays/screen_space_snow.wgsl b/webgpu/engine/shaders/overlays/screen_space_snow.wgsl new file mode 100644 index 000000000..b33527c4c --- /dev/null +++ b/webgpu/engine/shaders/overlays/screen_space_snow.wgsl @@ -0,0 +1,76 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2026 Gerald Kimmersdorfer +* Copyright (C) 2024 Patrick Komon +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use util/shared_config +///use util/camera_config +///use webgpu::encoder +///use webgpu::tile_util +///use webgpu::snow + +@group(0) @binding(0) var conf: shared_config; +@group(1) @binding(0) var camera: camera_config; +@group(2) @binding(0) var position_texture: texture_2d; +@group(2) @binding(1) var normal_texture: texture_2d; +@group(2) @binding(2) var settings: ScreenSpaceSnowSettings; +@group(2) @binding(3) var output_texture: texture_storage_2d; +@group(2) @binding(4) var prev_output: texture_2d; + +struct ScreenSpaceSnowSettings { + angle_min: f32, + angle_max: f32, + angle_blend: f32, + altitude_limit: f32, + altitude_variation: f32, + altitude_blend: f32, + transparency: f32, + _pad: f32, +} + +@compute @workgroup_size(16, 16, 1) +fn computeMain(@builtin(global_invocation_id) id: vec3u) { + let dims = vec2u(textureDimensions(position_texture)); + if id.x >= dims.x || id.y >= dims.y { + return; + } + let tci = id.xy; + + let pos_dist = textureLoad(position_texture, tci, 0); + let encoded_normal = textureLoad(normal_texture, tci, 0).xy; + + let pos_cws = pos_dist.xyz; + let dist = length(pos_cws); + let pos_ws = pos_cws + camera.position.xyz; + let normal = octNormalDecode2u16(encoded_normal); + + var out_color = vec4f(0.0); + + if dist > 0.0 { + //Reuse the shared snow math: angle limits live in .yzw, altitude params in .xyz. + let angle = vec4f(0.0, settings.angle_min, settings.angle_max, settings.angle_blend); + let altitude = vec4f(settings.altitude_limit, settings.altitude_variation, settings.altitude_blend, 0.0); + out_color = overlay_snow(normal, pos_ws, angle, altitude); + out_color.a *= settings.transparency; + } + + //Blend over previous overlay in premultiplied alpha space: + let prev = textureLoad(prev_output, tci, 0); + let src_premul = vec4f(out_color.rgb * out_color.a, out_color.a); + let blended = src_premul + prev * (1.0 - out_color.a); + textureStore(output_texture, tci, blended); +} diff --git a/webgpu/engine/shaders/overlays/texture_overlay.wgsl b/webgpu/engine/shaders/overlays/texture_overlay.wgsl new file mode 100644 index 000000000..5a0cf866b --- /dev/null +++ b/webgpu/engine/shaders/overlays/texture_overlay.wgsl @@ -0,0 +1,81 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2026 Gerald Kimmersdorfer +* Copyright (C) 2025 Patrick Komon +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use util/shared_config +///use util/camera_config +///use webgpu::encoder +///use screen_pass_vert + +@group(0) @binding(0) var conf: shared_config; +@group(1) @binding(0) var camera: camera_config; +@group(2) @binding(0) var position_texture: texture_2d; +@group(2) @binding(1) var settings: TextureOverlaySettings; +@group(2) @binding(2) var overlay_texture: texture_2d; +@group(2) @binding(3) var overlay_sampler: sampler; +@group(2) @binding(4) var background: texture_2d; //ping-pong: previous overlay state (premultiplied) + +struct TextureOverlaySettings { + aabb_min: vec2f, + aabb_size: vec2f, + opacity: f32, + mode: u32, + float_decode_range: vec2f, + encoded_float_range: vec2f, +} + +@fragment +fn fragmentMain(in: VertexOut) -> @location(0) vec4f { + let tci = vec2i(in.position.xy); + let pos_dist = textureLoad(position_texture, tci, 0); + let pos_cws = pos_dist.xyz; + let dist = length(pos_cws); + let pos_ws = pos_cws + camera.position.xyz; + + let uv = vec2f( + (pos_ws.x - settings.aabb_min.x) / settings.aabb_size.x, + 1.0 - (pos_ws.y - settings.aabb_min.y) / settings.aabb_size.y + ); + let ddx_uv = dpdx(uv); + let ddy_uv = dpdy(uv); + + var src = vec4f(0.0); + let aabb_max = settings.aabb_min + settings.aabb_size; + if !(dist <= 0.0 || any(pos_ws.xy < settings.aabb_min) || any(pos_ws.xy > aabb_max)) { + let sample = textureSampleGrad(overlay_texture, overlay_sampler, uv, ddx_uv, ddy_uv); + if settings.mode == 1u { + //EncodedFloat: RGBA encodes a u32 via (r<<24|g<<16|b<<8|a), decoded over + //settings.encoded_float_range, then normalized to settings.float_decode_range. + let rgba_u8 = vec4u(sample * 255.0); + let packed = (rgba_u8.r << 24u) | (rgba_u8.g << 16u) | (rgba_u8.b << 8u) | rgba_u8.a; + let value = u32_to_range(packed, settings.encoded_float_range); + let t = (value - settings.float_decode_range.x) / (settings.float_decode_range.y - settings.float_decode_range.x); + if t > 0.0 && t < 1.0 { + src = vec4f(vec3f(1.0 - t, 0.0, 0.0) * settings.opacity, settings.opacity); + } + } else { + //AlphaBlend (mode == 0): premultiplied-alpha. + let eff_a = sample.a * settings.opacity; + src = vec4f(sample.rgb * eff_a, eff_a); + } + } + + //Blend over previous overlay in premultiplied alpha space: + let bg = textureLoad(background, tci, 0); + return vec4f(src.rgb + bg.rgb * (1.0 - src.a), src.a + bg.a * (1.0 - src.a)); +} diff --git a/webgpu/engine/shaders/render_atmosphere.wgsl b/webgpu/engine/shaders/render_atmosphere.wgsl new file mode 100644 index 000000000..cbcb3f247 --- /dev/null +++ b/webgpu/engine/shaders/render_atmosphere.wgsl @@ -0,0 +1,49 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use util/camera_config +///use util/atmosphere + +///use screen_pass_vert + +@group(0) @binding(0) var camera: camera_config; + +// const highp float infinity = 1.0 / 0.0; // gives a warning on webassembly (and other angle based products) +const infinity = 3.40282e+38; // https://godbolt.org/z/9o9PdbGqW + +fn unproject(normalised_device_coordinates: vec2f) -> vec3f { + let unprojected = camera.inv_proj_matrix * vec4(normalised_device_coordinates, 1.0, 1.0); + let normalised_unprojected = unprojected / unprojected.w; + return normalize((camera.inv_view_matrix * normalised_unprojected).xyz); +} + +@fragment +fn fragmentMain(vertex_out: VertexOut) -> @location(0) vec4f { + + let origin = camera.position.xyz; + let ray_direction = unproject(vertex_out.texcoords * vec2f(2.0, -2.0) + 1.0); + var ray_length = 2000.0; + let background_colour = vec3f(0.0, 0.0, 0.0); + if ray_direction.z < 0.0 { + ray_length = min(ray_length, -(origin.z * 0.001) / ray_direction.z); + } + let light_through_atmosphere = calculate_atmospheric_light(origin / 1000.0, ray_direction, ray_length, background_colour, 1000); + + return vec4f(light_through_atmosphere, 1.0); +} diff --git a/webgpu/engine/shaders/render_clouds.wgsl b/webgpu/engine/shaders/render_clouds.wgsl new file mode 100644 index 000000000..b6e959a87 --- /dev/null +++ b/webgpu/engine/shaders/render_clouds.wgsl @@ -0,0 +1,551 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use webgpu::tile_util +///use util/shared_config +///use util/atmosphere + +struct tile_info { + index: u32, + zoom: u32, +} + +struct camera_config { + view_matrix: mat4x4f, + proj_matrix: mat4x4f, + inv_view_matrix: mat4x4f, + inv_proj_matrix: mat4x4f, + position: vec4f, +} + +struct shader_params { + camera: camera_config, + bounds_min: vec4f, + bounds_max: vec4f, + frame_index: u32, + scattering_coeff: f32, + extinction_coeff: f32, + albedo: f32, + step_size_min: f32, + step_size_distance_factor: f32, + step_size_horizon_factor: f32, + fade_factor: f32, + sun_light_scale: f32, + ambient_light_scale: f32, + atm_light_scale: f32, + shadow_extinction_scale: f32, + jitter: vec2f, + powder_scale: f32, + padding: f32, +} + +struct ray_accumulator { + t: f32, + radiance: vec3f, + transmittance: f32, + depth: f32, + depth_weight: f32, + + // State machine flags + searching_for_cloud: bool, + consecutive_empty_steps: i32, + mandatory_fine_dist: f32, + step_count: i32, +} + +@group(0) @binding(0) var params: shader_params; +@group(0) @binding(1) var atlas_texture: texture_3d; +@group(0) @binding(2) var atlas_sampler_l: sampler; +@group(0) @binding(3) var tile_infos: array; +@group(0) @binding(4) var output_color: texture_storage_2d; +@group(0) @binding(5) var output_depth: texture_storage_2d; + +@group(1) @binding(0) var depth_texture: texture_2d; + +@group(2) @binding(0) var sconf: shared_config; + +// tile size at zoom level 10 +override tile_size_xy = 39135.7584820102; +override inv_tile_size_xy = 1.0 / 39135.7584820102; + +override tile_count_x = 46 / 2; +override tile_count_y = 26 / 2; + +override zoom_max = 10; + +override tile_coords_offset_x = 538; +override tile_coords_offset_y = 660; + +override atlas_bits_xy = 2u; +override atlas_bits_z = 5u; + +///define ATLAS_MASK_XY u32((1< vec3f { + let unprojected = params.camera.inv_proj_matrix * vec4(normalised_device_coordinates, 1.0); + let normalised_unprojected = unprojected / unprojected.w; + return (params.camera.inv_view_matrix * normalised_unprojected).xyz; +} + +// Box-ray intersection +fn intersect_aabb(ray_origin: vec3f, ray_dir: vec3f, box_min: vec3f, box_max: vec3f) -> vec2f { + let inv_dir = 1.0 / ray_dir; + let t0 = (box_min - ray_origin) * inv_dir; + let t1 = (box_max - ray_origin) * inv_dir; + let tmin = min(t0, t1); + let tmax = max(t0, t1); + let t_near = max(max(tmin.x, tmin.y), tmin.z); + let t_far = min(min(tmax.x, tmax.y), tmax.z); + return vec2f(t_near, t_far); +} + +fn get_tile_id_at_pos(pos_world: vec3f) -> vec2i { + // Equivalent to subtracting bounds_min, dividing and then flooring, + // but avoids floating point mismatch to tile_uv calculation. + let pos_ts = pos_world.xy * inv_tile_size_xy; + return vec2i(floor(pos_ts)) - vec2i(tile_coords_offset_x, tile_coords_offset_y); +} + +fn get_tile_info(tile_id: vec2i) -> tile_info { + if tile_id.x < 0 || tile_id.x >= tile_count_x || tile_id.y < 0 || tile_id.y >= tile_count_y { + var default_tile: tile_info; + default_tile.index = 0u; + default_tile.zoom = u32(0u); + return default_tile; + } + + let tile_index = tile_id.x + tile_id.y * tile_count_x; + return tile_infos[tile_index]; +} + +fn sample_volume(pos_world: vec3f, lod: f32, tile_id: vec2i, tile: tile_info, atlas_sampler: sampler) -> f32 { + let height_adjusted = pos_world.z * cos(y_to_lat(pos_world.y)); + if height_adjusted < 0.0 || height_adjusted > 14000.0 || tile.zoom == 0u { + return 0.0; + } + + let dz = max(u32(zoom_max) - tile.zoom, 0u); + let tile_scale = f32(1u << dz); + + let pos_ts = pos_world.xy * inv_tile_size_xy / tile_scale; + let tile_uv = fract(pos_ts); + + let atlas_pos = vec3u( + tile.index & ATLAS_MASK_XY, + (tile.index >> atlas_bits_xy) & ATLAS_MASK_XY, + (tile.index >> (atlas_bits_xy << 1u)) & ATLAS_MASK_Z + ); + + // This projects into texture space height which is 0-14000. + let height_normalized = height_adjusted * INV_HEIGHT_PER_TEXEL * INV_TILE_RESOLUTION_Z; + + let mip_scale = exp2(lod); + let texel_size = mip_scale * vec3f(INV_TILE_RESOLUTION_XY, INV_TILE_RESOLUTION_XY, INV_TILE_RESOLUTION_Z); + let safe_uvw = clamp(vec3f(tile_uv, height_normalized), texel_size, vec3f(1.0) - texel_size); + let atlas_uvw = (safe_uvw + vec3f(atlas_pos)) * ATLAS_INV_SCALE; + + let raw = textureSampleLevel(atlas_texture, atlas_sampler, atlas_uvw, lod).r; + return raw * TEXTURE_VALUE_SCALE * 0.001; +} + +fn calculate_lod(step_size: f32, tile_zoom: u32, distance: f32, view_dir: vec3f) -> f32 { + let dz = max(u32(zoom_max) - tile_zoom, 0u); + let tile_size_multiplier = f32(1u << dz); + + let texel_size_xy = (tile_size_xy * tile_size_multiplier) / TILE_RESOLUTION_XY; + let texel_size_z = HEIGHT_PER_TEXEL; + + let view_xy = length(view_dir.xy); + let view_z = abs(view_dir.z); + + // Texels traversed per step along each axis + // Small value = undersampling that axis = need fine LOD + let texels_per_step_xy = (step_size * view_xy) / texel_size_xy; + let texels_per_step_z = (step_size * view_z) / texel_size_z; + + let lod_xy = log2(max(texels_per_step_xy / 2.0, 1.0)); + let lod_z = log2(max(texels_per_step_z / 2.0, 1.0)); + + // Use the finest LOD required by either axis + return clamp(min(lod_xy, lod_z) - 0.5, 0.0, 5.0); +} + +// Saturate helper +fn saturate(x: f32) -> f32 { + return clamp(x, 0.0, 1.0); +} + +// Henyey-Greenstein phase function for anisotropic scattering +fn henyey_greenstein_phase(cos_angle: f32, g: f32) -> f32 { + let g2 = g * g; + let denom = 1.0 + g2 - 2.0 * g * cos_angle; + return (1.0 - g2) / (4.0 * 3.14159265 * pow(denom, 1.5)); +} + +// Improved phase function with better forward and back scattering +fn cloud_phase_function(cos_angle: f32) -> f32 { + let forward = henyey_greenstein_phase(cos_angle, params.scattering_coeff); + let backward = henyey_greenstein_phase(cos_angle, -params.scattering_coeff * 0.5); + let isotropic = 0.25 / 3.14159265; + return forward * 0.5 + backward * 0.2 + isotropic * 0.3; +} + +// Calculate how much light reaches a point from the sun (light transmittance) +// Uses cone-based sampling with decreasing LOD as per Nubis/Guerrilla Games approach +fn sample_light_energy(pos: vec3f, sun_dir: vec3f, extinction_coeff: f32, base_lod: f32, start_t: f32, cos_angle: f32) -> f32 { + if sun_dir.z <= 0.0 { + return 0.0; + } + + if pos.z >= params.bounds_max.z { + return 1.0; + } + + // Calculate maximum ray length to volume boundary + let max_ray_length = min((params.bounds_max.z - pos.z) / sun_dir.z, 10000.0); + // Initial step size derived from geometric series sum to exactly span max_ray_length + const GROWTH_FACTOR = 1.5; + const STEP_SIZE_CONSTANT = (GROWTH_FACTOR - 1.0) / (pow(GROWTH_FACTOR, f32(MAX_LIGHT_STEPS)) - 1.0); + var step_size = max_ray_length * STEP_SIZE_CONSTANT; + + var optical_depth = 0.0; + var t = (start_t + step_size) * 0.5; + + // March towards sun with decreasing LOD per Nubis approach + // The decreasing LOD smooths out artifacts from sparse sampling + for (var i = 0; i < MAX_LIGHT_STEPS; i++) { + let sample_pos = pos + sun_dir * t; + + // Get tile info for this sample + let tile_id = get_tile_id_at_pos(sample_pos); + let tile = get_tile_info(tile_id); + + // Calculate base LOD, then add increasing bias per step + // This decreasing detail level smooths transitions between samples + let lod_bias = max(f32(i) * 0.5 - 0.5, 0.0); // Increase LOD by 0.5 per step + let lod = base_lod + lod_bias; + + // Sample density with calculated LOD + let density = sample_volume(sample_pos, lod, tile_id, tile, atlas_sampler_l); + + // Accumulate optical depth + optical_depth += density * step_size * extinction_coeff * params.shadow_extinction_scale; + + // Early exit for deep shadows + if optical_depth > 20.0 { + return 0.0; + } + + t += step_size; + step_size *= GROWTH_FACTOR; + } + + let beer = exp(-optical_depth); + let powder = 1.0 - exp(-optical_depth * 2.0); + let powder_weight = smoothstep(0.5, -0.5, cos_angle) * params.powder_scale; + let primary = mix(beer, beer * powder * 2.0, powder_weight); + + // Secondary/MS: softer attenuation curve, but directional, + // suppress when looking toward sun, matching Horizon's approach + let secondary = exp(-optical_depth * 0.25) * 0.7; + let secondary_directional = secondary * smoothstep(-0.5, 0.7, cos_angle); + + // secondary can only brighten relative to primary, never push total above 1 + return max(primary, secondary_directional); +} + +// Dynamic step size based on distance +fn get_step_size(distance: f32, ray_direction: vec3f, ray_length: f32) -> f32 { + let horizon = 1.0 - abs(ray_direction.z); // 1.0 at horizontal, 0.0 at vertical + let horizon_bonus = horizon * horizon * params.step_size_horizon_factor; + let distance_based = max(distance * params.step_size_distance_factor, params.step_size_min); + let length_based = ray_length / 32.0; + return min(distance_based + horizon_bonus, length_based); +} + +// Convert world position to NDC depth for storage +fn world_to_depth(world_pos: vec3f) -> f32 { + let view_pos = params.camera.view_matrix * vec4f(world_pos, 1.0); + let clip_pos = params.camera.proj_matrix * view_pos; + let ndc = clip_pos.xyz / clip_pos.w; + return ndc.z; +} + +fn ign(pixel: vec2f) -> f32 { + return fract(52.9829189f * fract(dot(pixel, vec2f(0.06711056f, 0.00583715f)))); +} + +fn r1_sequence(n: u32) -> f32 { + let g = 1.6180339887498948482; + let a1 = 1.0 / g; + return fract(0.5 + a1 * f32(n)); +} + +// Combined spatial + temporal noise +fn get_ray_offset(pixel: vec2u, frame: u32) -> f32 { + let temporal = r1_sequence(frame); + let spatial = ign(vec2f(pixel)); + return fract(spatial + temporal); +} + +fn calculate_point_radiance( + pos: vec3f, + beta: f32, + sun_dir: vec3f, + lod: f32, + step_size: f32, + jitter: f32, + cloud_phase: f32, + cos_angle: f32 +) -> vec3f { + let cloud_extinction = beta * params.extinction_coeff; + let cloud_scattering = cloud_extinction * params.albedo; + + let cloud_sun_transmittance = sample_light_energy(pos, sun_dir, params.extinction_coeff, lod, step_size, cos_angle); + + let sun_radiance = sconf.sun_light.rgb * sconf.sun_light.a * params.sun_light_scale; + let cloud_sun_inscatter = sun_radiance * cloud_sun_transmittance * cloud_phase; + + // --- Ambient --- + // Ambient: only modulate by height, since we have no way to estimate + // depth-into-cloud from density alone. Cloud tops receive more sky light. + let height_factor = saturate(pos.z / params.bounds_max.z); + let ambient_occlusion = mix(0.3, 1.0, height_factor); + + let ambient_radiance = sconf.amb_light.rgb * sconf.amb_light.a * params.ambient_light_scale; + var ambient_color = ambient_radiance; + if bool(sconf.atmosphere_enabled) { + let pos_km = pos / 1000.0; + let air_density = density_at_height(pos_km.z); + let rayleigh_coeff = scattering_coefficients(); + let atm_scattering = air_density * rayleigh_coeff; + let atm_inscatter_density = atmospheric_inscatter_at_point(pos_km, sun_dir); + let atm_tint = sun_radiance * atm_inscatter_density * atm_scattering * params.atm_light_scale; + // Lerp toward tint rather than adding - controls saturation + ambient_color = mix(ambient_radiance, atm_tint, 0.2); + } + + let cloud_ambient_inscatter = ambient_color * ambient_occlusion; + + let cloud_total_inscatter = cloud_sun_inscatter + cloud_ambient_inscatter; + return cloud_total_inscatter * cloud_scattering * step_size; +} + +fn step_coarse( + ray_origin: vec3f, + ray_dir: vec3f, + fine_step_size: f32, + t_far: f32, + ray_jitter: f32, + acc: ptr +) -> bool { + let big_step = fine_step_size * 4.0; + + // Jitter within the big step + let sample_t = (*acc).t + big_step * ray_jitter; + + // Boundary check + if sample_t >= t_far { + (*acc).t += big_step; + return false; // Did not find cloud, but safe to continue + } + + let pos = ray_origin + ray_dir * sample_t; + let tile_id = get_tile_id_at_pos(pos); + let tile = get_tile_info(tile_id); + + // Calculate Coarse LOD + let base_lod = calculate_lod(fine_step_size, tile.zoom, sample_t, ray_dir); + let coarse_lod = min(base_lod + 3.0, 5.0); + + let coarse_density = sample_volume(pos, coarse_lod, tile_id, tile, atlas_sampler_l); + + if coarse_density > 0.0 { + // HIT: Switch state to Fine + (*acc).searching_for_cloud = false; + (*acc).mandatory_fine_dist = big_step; + (*acc).consecutive_empty_steps = 0; + // Do NOT advance t. We return true to tell the caller "Loop again immediately" + return true; + } + + (*acc).t += big_step; + return false; +} + +fn step_fine( + ray_origin: vec3f, + ray_dir: vec3f, + sun_dir: vec3f, + fine_step_size: f32, + t_far: f32, + fade_params: vec2f, // x: near, y: far + cloud_phase: f32, + cos_angle: f32, + ray_jitter: f32, + acc: ptr +) { + let sample_t = min((*acc).t + fine_step_size * ray_jitter, t_far); + + let pos = ray_origin + ray_dir * sample_t; + let tile_id = get_tile_id_at_pos(pos); + let tile = get_tile_info(tile_id); + + let lod = calculate_lod(fine_step_size, tile.zoom, sample_t, ray_dir); + + // Volume camera fade + let dist_cylinder = max(length(pos.xy - ray_origin.xy), (ray_origin.z - pos.z) * 0.5); + let fade_t = saturate((dist_cylinder - fade_params.x) / (fade_params.y - fade_params.x)); + let fade = fade_t * fade_t * fade_t; + + let base_beta = sample_volume(pos, lod, tile_id, tile, atlas_sampler_l); + let beta = base_beta * fade; + + if beta > 0.0 { + (*acc).consecutive_empty_steps = 0; + + let radiance_contribution = calculate_point_radiance( + pos, beta, sun_dir, lod, fine_step_size, ray_jitter, cloud_phase, cos_angle + ); + + // Accumulate Light + (*acc).radiance += radiance_contribution * (*acc).transmittance; + + // Accumulate Depth + (*acc).depth += sample_t * (*acc).transmittance; + (*acc).depth_weight += (*acc).transmittance; + + // Apply Extinction + let extinction = beta * params.extinction_coeff * fine_step_size; + (*acc).transmittance *= exp(-extinction); + } else { + (*acc).consecutive_empty_steps++; + } + + // Update Space Skipping State + (*acc).mandatory_fine_dist -= fine_step_size; + + // Switch back to coarse if we are in empty space and have cleared the mandatory zone + if (*acc).mandatory_fine_dist <= 0.0 && (*acc).consecutive_empty_steps >= 8 { + (*acc).searching_for_cloud = true; + } + + (*acc).t += fine_step_size; +} + +@compute @workgroup_size(8, 8, 1) +fn computeMain(@builtin(global_invocation_id) global_id: vec3u) { + let output_dims = textureDimensions(output_color); + let pixel_coord = global_id.xy; + + if pixel_coord.x >= output_dims.x || pixel_coord.y >= output_dims.y { + return; + } + + let pixel_center = vec2f(pixel_coord) + 0.5; + let texcoords = pixel_center / vec2f(output_dims); + + let ray_jitter = get_ray_offset(pixel_coord, params.frame_index); + + let origin = params.camera.position.xyz; + let stable_depth_coord = 2 * vec2i(pixel_coord) - vec2i(2.0 * params.jitter); + + let frag_depth = max(textureLoad(depth_texture, stable_depth_coord, 0).x, 1e-6f); + let frag_pos = unproject(vec3f(texcoords * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), frag_depth)); + let ray_direction = normalize(frag_pos); + + let fade_near = 1000.0 * params.fade_factor; + let fade_far = 50000.0 * params.fade_factor; + + // Intersect ray with volume + let intersection = intersect_aabb(origin, ray_direction, params.bounds_min.xyz, params.bounds_max.xyz); + let t_near = max(intersection.x, fade_near); + let t_far = min(intersection.y, length(frag_pos)); + + if t_near >= t_far { + textureStore(output_color, pixel_coord, vec4f(0.0, 0.0, 0.0, 1.0)); + textureStore(output_depth, pixel_coord, vec4f(0.0, 0.0, 0.0, 0.0)); + return; + } + + var acc: ray_accumulator; + acc.t = t_near; + acc.radiance = vec3f(0.0); + acc.transmittance = 1.0; + acc.depth = 0.0; + acc.depth_weight = 0.0; + acc.searching_for_cloud = true; + acc.consecutive_empty_steps = 0; + acc.mandatory_fine_dist = 0.0; + acc.step_count = 0; + + let sun_dir = normalize(-sconf.sun_light_dir.xyz); + + // Phase function (view-dependent scattering) + let cos_angle = dot(ray_direction, sun_dir); + let cloud_phase = cloud_phase_function(cos_angle); + + while acc.transmittance > 0.01 && acc.t < t_far && acc.step_count < MAX_STEPS { + // Calculate Step Size + var fine_step_size = get_step_size(acc.t, ray_direction, t_far - t_near); + fine_step_size = min(fine_step_size, t_far - acc.t); + + if fine_step_size < 0.01 { break; } + + if acc.searching_for_cloud { + // Returns true if hit, meaning we should loop again without advancing + let hit = step_coarse(origin, ray_direction, fine_step_size, t_far, ray_jitter, &acc); + if hit { continue; } + } else { + step_fine( + origin, ray_direction, sun_dir, + fine_step_size, t_far, vec2f(fade_near, fade_far), + cloud_phase, cos_angle, ray_jitter, &acc + ); + } + + acc.step_count++; + } + + // Note: accumulated radiance is already "alpha-premultiplied" in a sense + textureStore(output_color, pixel_coord, vec4f(acc.radiance, acc.transmittance)); + + // Output apparent depth (linear depth in NDC Z) + var apparent_depth = min(acc.depth / acc.depth_weight, t_far); + if acc.depth_weight == 0.0 { + apparent_depth = t_far; + } + textureStore(output_depth, pixel_coord, vec4f(apparent_depth, 0.0, 0.0, 0.0)); +} diff --git a/webgpu/engine/shaders/render_lines.wgsl b/webgpu/engine/shaders/render_lines.wgsl new file mode 100644 index 000000000..b3ef59860 --- /dev/null +++ b/webgpu/engine/shaders/render_lines.wgsl @@ -0,0 +1,78 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +///use util/shared_config +///use util/camera_config + +@group(0) @binding(0) var config: shared_config; +@group(1) @binding(0) var camera: camera_config; +@group(2) @binding(0) var depth_texture: texture_2d; +@group(3) @binding(0) var positions: array; +@group(3) @binding(1) var line_config: LineConfig; + +struct LineConfig { + color: vec4f, +} + +const behind_alpha = 0.35; + +struct VertexOut { + @builtin(position) position: vec4f, +} + +struct FragIn { + @builtin(position) position: vec4f, +} + +struct FragOut { + @location(0) color: vec4f, +} + +@vertex +fn vertexMain(@builtin(vertex_index) vertex_index: u32) -> VertexOut { + var vertex_out: VertexOut; + let pos = positions[vertex_index]; + vertex_out.position = camera.view_proj_matrix * vec4f(pos.xyz - camera.position.xyz, 1); + return vertex_out; +} + +@fragment +fn fragmentMain(frag_in: FragIn) -> FragOut { + var frag_out: FragOut; + + if config.track_render_mode == 1 { // no depth test + frag_out.color = line_config.color; + return frag_out; + } + + let depth_buffer_position = vec2u(frag_in.position.xy); + let tile_fragment_depth = textureLoad(depth_texture, depth_buffer_position, 0).x; + let line_fragment_depth = frag_in.position.z; + + if tile_fragment_depth < line_fragment_depth { + if config.track_render_mode == 2 { // depth test + discard; + } else if config.track_render_mode == 3 { // semi-transparent if depth test failed + frag_out.color = vec4f(line_config.color.xyz * behind_alpha * line_config.color.a, behind_alpha * line_config.color.a); + } + } else { + frag_out.color = line_config.color; + } + + return frag_out; +} \ No newline at end of file diff --git a/webgpu/engine/shaders/render_tiles.wgsl b/webgpu/engine/shaders/render_tiles.wgsl new file mode 100644 index 000000000..f0e894518 --- /dev/null +++ b/webgpu/engine/shaders/render_tiles.wgsl @@ -0,0 +1,249 @@ +/***************************************************************************** +* weBIGeo +* Copyright (C) 2022 Adam Celarek +* Copyright (C) 2023 Gerald Kimmersdorfer +* Copyright (C) 2024 Patrick Komon +* +* This program is free software : you can redistribute it and / or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +///use util/shared_config +///use webgpu::hashing +///use util/camera_config +///use webgpu::encoder +///use webgpu::tile_util +///use webgpu::normals_util +///use webgpu::filtering + +@group(0) @binding(0) var config: shared_config; + +@group(1) @binding(0) var camera: camera_config; + +@group(2) @binding(0) var n_edge_vertices: i32; +@group(2) @binding(1) var height_texture: texture_2d_array; +@group(2) @binding(2) var height_sampler: sampler; +@group(2) @binding(3) var ortho_texture: texture_2d_array; +@group(2) @binding(4) var ortho_sampler: sampler; + +struct VertexIn { + @location(0) bounds: vec4f, + @location(1) height_texture_layer: i32, + @location(2) ortho_texture_layer: i32, + @location(3) tileset_id: i32, + @location(4) height_zoomlevel: i32, + @location(5) tile_id: vec4, + @location(6) ortho_zoomlevel: i32, +} + +struct VertexOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + @location(1) pos_cws: vec3f, + @location(2) normal: vec3f, + @location(3) @interpolate(flat) height_texture_layer: i32, + @location(4) @interpolate(flat) ortho_texture_layer: i32, + @location(5) @interpolate(flat) color: vec3f, + @location(6) @interpolate(flat) tile_id: vec3, + @location(7) @interpolate(flat) ortho_zoomlevel: i32, +} + +struct FragOut { + @location(0) albedo: u32, + @location(1) position: vec4f, + @location(2) normal_enc: vec2u, + @location(3) overlay: u32, +} + +fn compute_vertex( + vertex_index: i32, + render_tile_id: TileId, + bounds: vec4f, + height_zoomlevel: u32, + height_texture_layer: i32, + position: ptr, + uv: ptr, + tile_id: ptr, + compute_normal: bool, + normal: ptr +) { + //get tile id of desired height tile + var height_tile_id: TileId; + { + const input_uv = vec2f(0.0); + var height_uv: vec2f; + decrease_zoom_level_until(render_tile_id, input_uv, height_zoomlevel, &height_tile_id, &height_uv); + } + + let n_quads_per_direction_int: i32 = (n_edge_vertices - 1) >> (render_tile_id.zoomlevel - height_tile_id.zoomlevel); + let n_quads_per_direction: f32 = f32(n_quads_per_direction_int); + let quad_size: f32 = (bounds.z - bounds.x) / n_quads_per_direction; + + //get row and col for specific vertex_index + var row: i32 = vertex_index / n_edge_vertices; + var col: i32 = vertex_index - (row * n_edge_vertices); + var curtain_vertex_id: i32 = vertex_index - n_edge_vertices * n_edge_vertices; + if curtain_vertex_id >= 0 { + if curtain_vertex_id < n_edge_vertices { + row = (n_edge_vertices - 1) - curtain_vertex_id; + col = (n_edge_vertices - 1); + } else if curtain_vertex_id >= n_edge_vertices && curtain_vertex_id < 2 * n_edge_vertices - 1 { + row = 0; + col = (n_edge_vertices - 1) - (curtain_vertex_id - n_edge_vertices) - 1; + } else if curtain_vertex_id >= 2 * n_edge_vertices - 1 && curtain_vertex_id < 3 * n_edge_vertices - 2 { + row = curtain_vertex_id - 2 * n_edge_vertices + 2; + col = 0; + } else { + row = (n_edge_vertices - 1); + col = curtain_vertex_id - 3 * n_edge_vertices + 3; + } + } + if row > n_quads_per_direction_int { + row = n_quads_per_direction_int; + curtain_vertex_id = 1; + } + if col > n_quads_per_direction_int { + col = n_quads_per_direction_int; + curtain_vertex_id = 1; + } + + //compute world space x and y coordinates + (*position).y = f32(n_quads_per_direction_int - row) * f32(quad_size) + bounds.y; + (*position).x = f32(col) * quad_size + bounds.x; + + //compute uv coordinates on height tile + let render_tile_uv = vec2f(f32(col) / n_quads_per_direction, f32(row) / n_quads_per_direction); + var height_tile_uv: vec2f; + { + var unused: TileId; + decrease_zoom_level_until(render_tile_id, render_tile_uv, height_zoomlevel, &unused, &height_tile_uv); + } + *uv = render_tile_uv; + + //read height texture and compute world space z coordinate + let altitude_tex = f32(bilinear_sample_u32(height_texture, height_sampler, height_tile_uv, u32(height_texture_layer))); + //Note: for higher zoom levels it would be enough to calculate the altitude_correction_factor on cpu + //for lower zoom levels we could bake it into the texture. + //there was no measurable difference despite a cos and a atan, so leaving as is for now. + let world_space_y: f32 = (*position).y + camera.position.y; + let altitude_correction_factor: f32 = 0.125 / cos(y_to_lat(world_space_y));//https://github.com/AlpineMapsOrg/renderer/issues/5 + let adjusted_altitude: f32 = altitude_tex * altitude_correction_factor; + (*position).z = adjusted_altitude - camera.position.z; + + if curtain_vertex_id >= 0 { + const curtain_height = 1000.0; + (*position).z = (*position).z - curtain_height; + } + + //TODO port this + /* if (curtain_vertex_id >= 0) + { + float curtain_height = CURTAIN_REFERENCE_HEIGHT; + if CURTAIN_HEIGHT_MODE == 1 + float dist_factor = clamp(length(position) / 100000.0, 0.2, 1.0); + curtain_height *= dist_factor; + endif + if CURTAIN_HEIGHT_MODE == 2 + float zoom_factor = 1.0 - max(0.1, float(tile_id.z) / 25.f); + curtain_height *= zoom_factor; + endif + position.z = position.z - curtain_height; + } + */ + + if compute_normal { + *normal = normal_by_finite_difference_method(height_tile_uv, quad_size, quad_size, altitude_correction_factor, height_texture_layer, height_texture); + } +} + +fn normal_by_fragment_position_interpolation(pos_cws: vec3) -> vec3 { + let dFdxPos = dpdy(pos_cws); + let dFdyPos = dpdx(pos_cws); + return normalize(cross(dFdxPos, dFdyPos)); +} + +@vertex +fn vertexMain(@builtin(vertex_index) vertex_index: u32, vertex_in: VertexIn) -> VertexOut { + let render_tile_id = TileId(vertex_in.tile_id.x, vertex_in.tile_id.y, vertex_in.tile_id.z, 4294967295u); + + var position: vec3f; + var uv: vec2f; + var height_tile_id: TileId; + var normal: vec3f; + compute_vertex(i32(vertex_index), render_tile_id, vertex_in.bounds, u32(vertex_in.height_zoomlevel), vertex_in.height_texture_layer, + &position, &uv, &height_tile_id, true, &normal); + + let clip_pos: vec4f = camera.view_proj_matrix * vec4f(position, 1.0); + + var vertex_out: VertexOut; + vertex_out.position = clip_pos; + vertex_out.uv = uv; + vertex_out.pos_cws = position; + vertex_out.normal = normal; + vertex_out.height_texture_layer = vertex_in.height_texture_layer; + vertex_out.ortho_texture_layer = vertex_in.ortho_texture_layer; + + var vertex_color = vec3f(0.0); + if config.overlay_mode == 2 { + vertex_color = color_from_id_hash(u32(vertex_in.tileset_id)); + } else if config.overlay_mode == 3 { + vertex_color = color_from_id_hash(u32(vertex_in.height_zoomlevel)); + //vertex_color = color_from_id_hash(u32(vertex_in.ortho_zoomlevel)); + } else if config.overlay_mode == 4 { + vertex_color = color_from_id_hash(u32(vertex_index)); + } + vertex_out.color = vertex_color; + vertex_out.tile_id = vertex_in.tile_id.xyz; + vertex_out.ortho_zoomlevel = vertex_in.ortho_zoomlevel; + return vertex_out; +} + +@fragment +fn fragmentMain(vertex_out: VertexOut) -> FragOut { + let tile_id = TileId(vertex_out.tile_id.x, vertex_out.tile_id.y, vertex_out.tile_id.z, 4294967295u); + + //obtain uv coordinates for desired ortho zoom level and sample + var ortho_tile_id: TileId; + var ortho_uv: vec2f; + let found_ortho = decrease_zoom_level_until(tile_id, vertex_out.uv, u32(vertex_out.ortho_zoomlevel), &ortho_tile_id, &ortho_uv); + var albedo = textureSample(ortho_texture, ortho_sampler, ortho_uv, vertex_out.ortho_texture_layer).rgb; + + var frag_out: FragOut; + + var dist = length(vertex_out.pos_cws); + var normal = vertex_out.normal; + if config.normal_mode != 0 { + if config.normal_mode == 1 { + normal = normal_by_fragment_position_interpolation(vertex_out.pos_cws); + } + + frag_out.normal_enc = octNormalEncode2u16(normal); + } + + //HANDLE DEBUG OVERLAYS THAT CAN JUST BE DONE IN THIS STAGE + var overlay_color = vec4f(0.0); + if config.overlay_mode > 0u && config.overlay_mode < 100u { + if config.overlay_mode == 1 { + overlay_color = vec4f(normal * 0.5 + 0.5, 1.0); + } else { + overlay_color = vec4f(vertex_out.color.xyz, 1); + } + //albedo = mix(albedo, overlay_color.xyz, config.overlay_strength * overlay_color.w); + } + frag_out.overlay = pack4x8unorm(overlay_color); + frag_out.albedo = pack4x8unorm(vec4f(albedo, 1.0)); + + frag_out.position = vec4f(vertex_out.pos_cws, dist); + + return frag_out; +} diff --git a/webgpu/engine/shaders/screen_pass_vert.wgsl b/webgpu/engine/shaders/screen_pass_vert.wgsl new file mode 100644 index 000000000..bbb0a8601 --- /dev/null +++ b/webgpu/engine/shaders/screen_pass_vert.wgsl @@ -0,0 +1,36 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +struct VertexOut { + @builtin(position) position: vec4f, + @location(0) texcoords: vec2f} + +@vertex +fn vertexMain(@builtin(vertex_index) vertex_index: u32) -> VertexOut { + const VERTICES = array( + vec2f(-1.0, -1.0), + vec2f(3.0, -1.0), + vec2f(-1.0, 3.0) + ); + + var vertex_out: VertexOut; + vertex_out.position = vec4(VERTICES[vertex_index], 0.0, 1.0); + vertex_out.texcoords = vec2(0.5, -0.5) * vertex_out.position.xy + vec2(0.5); + return vertex_out; +} \ No newline at end of file diff --git a/webgpu/engine/shaders/upscale_clouds.wgsl b/webgpu/engine/shaders/upscale_clouds.wgsl new file mode 100644 index 000000000..01b7538a8 --- /dev/null +++ b/webgpu/engine/shaders/upscale_clouds.wgsl @@ -0,0 +1,249 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +struct camera_config { + view_matrix: mat4x4f, + proj_matrix: mat4x4f, + inv_view_matrix: mat4x4f, + inv_proj_matrix: mat4x4f, + position: vec4f, +} + +struct accumulation_params { + curr_camera: camera_config, + prev_camera: camera_config, + jitter: vec2f, + prev_jitter: vec2f, + low_res_texel_size: vec2f, + high_res_texel_size: vec2f, + resolution_scale: vec2f, + _padding0: vec2f, +} + +@group(0) @binding(0) var params: accumulation_params; +@group(0) @binding(1) var current_color: texture_2d; +@group(0) @binding(2) var current_depth: texture_2d; +@group(0) @binding(3) var linear_sampler: sampler; +@group(0) @binding(4) var accumulation_color_r: texture_2d; +@group(0) @binding(5) var accumulation_color_w: texture_storage_2d; + +fn world_position_from_linear_depth(uv: vec2f, linear_depth: f32, inv_proj: mat4x4f, inv_view: mat4x4f) -> vec3f { + // Convert UV to NDC + let ndc_xy = uv * 2.0 - 1.0; + + let focal_x = 1.0 / inv_proj[0][0]; + let focal_y = 1.0 / inv_proj[1][1]; + + let view_pos = vec4f( + ndc_xy.x * linear_depth / focal_x, + ndc_xy.y * linear_depth / focal_y, + -linear_depth, + 1.0 + ); + + // View to world space + let world_pos = (inv_view * view_pos).xyz; + return world_pos; +} + +fn blend_transmittance(current_transmittance: f32, history_transmittance: f32, blend_factor: f32) -> f32 { + let current_optical_depth = -log(max(current_transmittance, 0.00001)); + let history_optical_depth = -log(max(history_transmittance, 0.00001)); + let blended_optical_depth = mix(current_optical_depth, history_optical_depth, blend_factor); + let blended_transmittance = exp(-blended_optical_depth); + return blended_transmittance; +} + +fn reproject_position(world_pos: vec3f, prev_view: mat4x4f, prev_proj: mat4x4f) -> vec3f { + // World to previous view space + let prev_view_pos = prev_view * vec4f(world_pos, 1.0); + + // View to clip space (unjittered projection) + var prev_clip_pos = prev_proj * prev_view_pos; + prev_clip_pos /= prev_clip_pos.w; + + // Convert to UV (unjittered) + let prev_uv = prev_clip_pos.xy * 0.5 + 0.5; + + // Return unjittered UV and ndc z (unused here) + return vec3f(prev_uv, prev_clip_pos.z); +} + +fn clip_aabb(history_color: vec3f, aabb_min: vec3f, aabb_max: vec3f) -> vec3f { + let p_clip = 0.5 * (aabb_max + aabb_min); + let e_clip = 0.5 * (aabb_max - aabb_min) + 0.00001; + + let v_clip = history_color - p_clip; + let v_unit = v_clip / e_clip; + let a_unit = abs(v_unit); + let ma_unit = max(a_unit.x, max(a_unit.y, a_unit.z)); + + if ma_unit > 1.0 { + return p_clip + v_clip / ma_unit; + } + return history_color; +} + +fn clip_scalar(history_value: f32, aabb_min: f32, aabb_max: f32) -> f32 { + return clamp(history_value, aabb_min, aabb_max); +} + +fn update_depth_stats(prev_data: vec2f, current_depth: f32, alpha: f32) -> vec2f { + let next_mean = mix(prev_data.r, current_depth, alpha); + let next_mean_sq = mix(prev_data.g, current_depth * current_depth, alpha); + return vec2f(next_mean, next_mean_sq); +} + +@compute @workgroup_size(8, 8, 1) +fn computeMain(@builtin(global_invocation_id) global_id: vec3u) { + let high_res_dims = textureDimensions(accumulation_color_w); + + if global_id.x >= high_res_dims.x || global_id.y >= high_res_dims.y { + return; + } + + let high_res_coord = vec2i(global_id.xy); + let high_res_uv = (vec2f(global_id.xy) + 0.5) / vec2f(high_res_dims); + + // Account for jitter in current sampling (assuming content shifted by -jitter, so sample at +jitter) + let jittered_uv = high_res_uv - params.jitter * vec2f(1.0, -1.0); + let jittered_coord = vec2i(jittered_uv * vec2f(textureDimensions(current_depth))); + + let current_sample = textureSampleLevel(current_color, linear_sampler, jittered_uv, 0.0); + let current_depth = textureLoad(current_depth, jittered_coord, 0).r; + + // Early exit for background/sky + if current_depth <= 0.0 { + textureStore(accumulation_color_w, high_res_coord, current_sample); + return; + } + + // Reconstruct world position from UNJITTERED position + let world_pos = world_position_from_linear_depth( + high_res_uv, // Use unjittered UV for reconstruction + current_depth, + params.curr_camera.inv_proj_matrix, + params.curr_camera.inv_view_matrix + ); + + // Reproject to previous frame (unjittered) + let prev_position = reproject_position( + world_pos, + params.prev_camera.view_matrix, + params.prev_camera.proj_matrix + ); + + // Apply previous frame's jitter to get actual texture coordinates for history + let prev_uv = prev_position.xy; + + // Validate reprojection + let is_valid = all(prev_uv >= vec2f(0.0)) && all(prev_uv <= vec2f(1.0)); + + if !is_valid { + textureStore(accumulation_color_w, high_res_coord, current_sample); + return; + } + + // Sample history (bilinear would require regular texture, but storage is point-sampled) + let history_sample = textureSampleLevel(accumulation_color_r, linear_sampler, prev_uv, 0.0); + + // Variance clipping - sample 3x3 neighborhood for scattered and transmittance + var scattered_sum = vec3f(0.0); + var scattered_squared_sum = vec3f(0.0); + var trans_sum = 0.0; + var trans_squared_sum = 0.0; + var weight_sum = 0.0; + + for (var dy = -1; dy <= 1; dy++) { + for (var dx = -1; dx <= 1; dx++) { + let offset = vec2f(f32(dx), f32(dy)) * params.low_res_texel_size; + let neighbor_uv = jittered_uv + offset; + let neighbor = textureSampleLevel(current_color, linear_sampler, neighbor_uv, 0.0); + + let opacity = max(1.0 - neighbor.a, 0.001); + let neighbor_scattered = neighbor.rgb / opacity; + + // Gaussian weight (center = 1.0, edges = ~0.6, corners = ~0.36) + let w = exp(-f32(dx * dx + dy * dy) * 0.5); + + scattered_sum += neighbor_scattered * w; + scattered_squared_sum += neighbor_scattered * neighbor_scattered * w; + trans_sum += neighbor.a * w; + trans_squared_sum += neighbor.a * neighbor.a * w; + weight_sum += w; + } + } + + let scattered_mean = scattered_sum / weight_sum; + let scattered_variance = max((scattered_squared_sum / weight_sum) - (scattered_mean * scattered_mean), vec3f(0.0)); + let scattered_stddev = sqrt(scattered_variance); + + let trans_mean = trans_sum / weight_sum; + let trans_variance = max((trans_squared_sum / weight_sum) - (trans_mean * trans_mean), 0.0); + let trans_stddev = sqrt(trans_variance); + + // AABB for variance clipping (1.0 sigma) + let gamma = 1.0; + let scattered_aabb_min = scattered_mean - gamma * scattered_stddev; + let scattered_aabb_max = scattered_mean + gamma * scattered_stddev; + let trans_aabb_min = trans_mean - gamma * trans_stddev; + let trans_aabb_max = trans_mean + gamma * trans_stddev; + + // Undo pre-multiply + let current_opacity = max(1.0 - current_sample.a, 0.001); + let history_opacity = max(1.0 - history_sample.a, 0.001); + let current_scattered = current_sample.rgb / current_opacity; + var history_scattered = history_sample.rgb / history_opacity; + var history_transmittance = history_sample.a; + + // Calculate clip penalty using unclipped history + let scattered_diff = length(history_scattered - current_scattered); + let scattered_stddev_length = length(scattered_stddev) + 0.001; + let scattered_clip_factor = 1.0 - smoothstep(scattered_stddev_length * 2.5, scattered_stddev_length * 3.5, scattered_diff); + + let trans_diff = abs(history_transmittance - current_sample.a); + let trans_stddev_length = trans_stddev + 0.001; + let trans_clip_factor = 1.0 - smoothstep(trans_stddev_length * 2.5, trans_stddev_length * 3.5, trans_diff); + + // Clip the history scattered and transmittance + history_scattered = clip_aabb(history_scattered, scattered_aabb_min, scattered_aabb_max); + history_transmittance = clip_scalar(history_transmittance, trans_aabb_min, trans_aabb_max); + + var history_weight = 0.95; + history_weight *= max(min(trans_clip_factor, scattered_clip_factor), 0.5); + + // Special case: completely empty history + if history_sample.a > 0.999 && current_sample.a < 0.95 { + history_weight = 0.0; + } + + // Blend scattered + let blended_scattered = mix(current_scattered, history_scattered, history_weight); + + // Blend transmittance + let blended_transmittance = blend_transmittance( + current_sample.a, + history_transmittance, + history_weight + ); + + let blended_color = blended_scattered * (1.0 - blended_transmittance); + + // Write output + textureStore(accumulation_color_w, high_res_coord, vec4f(blended_color, blended_transmittance)); +} \ No newline at end of file diff --git a/webgpu/engine/shaders/util/atmosphere.wgsl b/webgpu/engine/shaders/util/atmosphere.wgsl new file mode 100644 index 000000000..0948145f8 --- /dev/null +++ b/webgpu/engine/shaders/util/atmosphere.wgsl @@ -0,0 +1,122 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022 Adam Celarek + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2026 Wendelin Muth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +const atmosphere_height = 100.0; +// only an even number of steps supported (simpson) + +const wavelengths = vec3f(700.0, 560.0, 440.0); +//const wavelengths = vec3f(700, 530, 440); + +fn scattering_coefficients() -> vec3f { + return pow(400.0 / wavelengths, vec3(4.0, 4.0, 4.0)) * 0.04; +} + +fn density_at_height(height_msl: f32) -> f32 { + return exp(-height_msl * 0.13); +} + +fn an_optical_depth(origin_height: f32, h_delta: f32, distance: f32) -> f32 { + let end_height = origin_height + h_delta * distance; + // integral of density along a direction. + const w = 0.13; + let density_at_orign = density_at_height(origin_height); + let density_at_end = density_at_height(end_height); + if abs(h_delta) < 0.001 { + return distance * 0.5 * (density_at_orign + density_at_end); + } + return (1.0 / (w * h_delta)) * (density_at_orign - density_at_end); + + // highp float w = 0.13; + // if (abs(h_delta) < 0.001) + // return distance * 0.5 * (density_at_height(origin_height) + density_at_height(end_height)); + // return (1.0 / (w * h_delta)) * (exp(-origin_height * w) - exp(-end_height * w)); +} + +fn atmospheric_inscatter_at_point(pos_km: vec3f, sun_dir: vec3f) -> vec3f { + let height = pos_km.z; + + // Optical depth from this point to top of atmosphere (sunlight path) + let sun_optical_depth = an_optical_depth(height, 1.0, atmosphere_height - height); + + // Sunlight transmittance through atmosphere above this point + let sun_transmittance = exp(-sun_optical_depth * scattering_coefficients()); + + // Local air density + let air_density = density_at_height(height); + + // Scattered light (before phase function) + return air_density * sun_transmittance; +} + +fn evaluate_atmopshperic_light(h: f32, h_delta: f32, dist_from_start: f32) -> vec3f { + let sun_ray_optical_depth = an_optical_depth(h, 1.0, atmosphere_height - h); + let view_ray_optical_depth = an_optical_depth(h, -h_delta, dist_from_start); + let transmittance: vec3f = exp(-(sun_ray_optical_depth + view_ray_optical_depth) * scattering_coefficients()); + let local_density = density_at_height(h); + return local_density * transmittance; +} + +// simpson's 1/3 rule +fn integrate_atmoshpere_light(h_start: f32, h_delta: f32, ray_length: f32, n_numerical_integration_steps: i32) -> vec3f { + let h_end = h_start + h_delta * ray_length; + var in_scattered_light = vec3(0.0, 0.0, 0.0); + in_scattered_light += evaluate_atmopshperic_light(h_start, h_delta, ray_length); + in_scattered_light += evaluate_atmopshperic_light(h_end, h_delta, ray_length); + var in_scattered_light_2 = vec3(0.0, 0.0, 0.0); + var in_scattered_light_4 = vec3(0.0, 0.0, 0.0); + + for (var i = 1; i < n_numerical_integration_steps; i++) { + let perc = f32(i) / f32(n_numerical_integration_steps); + let dist_from_start = perc * ray_length; + let h = mix(h_start, h_end, perc); + if (i & 1) == 0 { + in_scattered_light_2 += evaluate_atmopshperic_light(h, h_delta, dist_from_start); // even + } else { + in_scattered_light_4 += evaluate_atmopshperic_light(h, h_delta, dist_from_start); // odd + } + } + let step_size = ray_length / f32(n_numerical_integration_steps); + return (step_size / 3.0) * (in_scattered_light + 2.0 * in_scattered_light_2 + 4.0 * in_scattered_light_4); +} + +//// rectangle rule +//highp vec3 integrate_atmoshpere_light(highp float h_start, highp float h_delta, float ray_length, int n_numerical_integration_steps) { +// highp float h = h_start; +// float step_size = ray_length / float(n_numerical_integration_steps - 1); +// highp vec3 in_scattered_light = vec3(0.0, 0.0, 0.0); +// for (int i = 0; i < n_numerical_integration_steps; i++) { +// highp float dist_from_start = step_size * i; +// in_scattered_light += evaluate_atmopshperic_light(h, h_delta, dist_from_start); +// h += h_delta * step_size; + +// } +// return (ray_length / n_numerical_integration_steps) * in_scattered_light; +//} + +fn calculate_atmospheric_light(ray_origin: vec3f, ray_direction: vec3f, ray_length: f32, original_colour: vec3f, n_numerical_integration_steps: i32) -> vec3f { + var integral = integrate_atmoshpere_light(ray_origin.z, ray_direction.z, ray_length, n_numerical_integration_steps); + let cos_sun = dot(ray_direction, vec3f(0.0, 0.0, 1.0)); + let phase_function = 0.75 * (1.0 + cos_sun * cos_sun); + integral *= scattering_coefficients() * phase_function; + let view_ray_optical_depth = an_optical_depth(ray_origin.z, ray_direction.z, ray_length); + let transmittance = exp(-(view_ray_optical_depth) * scattering_coefficients()); + + return integral + transmittance * original_colour; +} diff --git a/webgpu/engine/shaders/util/camera_config.wgsl b/webgpu/engine/shaders/util/camera_config.wgsl new file mode 100644 index 000000000..c82ffd193 --- /dev/null +++ b/webgpu/engine/shaders/util/camera_config.wgsl @@ -0,0 +1,31 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2022 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +struct camera_config { + position: vec4f, + view_matrix: mat4x4f, + proj_matrix: mat4x4f, + view_proj_matrix: mat4x4f, + inv_view_proj_matrix: mat4x4f, + inv_view_matrix: mat4x4f, + inv_proj_matrix: mat4x4f, + viewport_size: vec2f, + distance_scaling_factor: f32, + buffer2: f32, +}; diff --git a/webgpu/engine/shaders/util/shared_config.wgsl b/webgpu/engine/shaders/util/shared_config.wgsl new file mode 100644 index 000000000..09bec5e87 --- /dev/null +++ b/webgpu/engine/shaders/util/shared_config.wgsl @@ -0,0 +1,36 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * Copyright (C) 2023 Gerald Kimmersdorfer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +struct shared_config { + sun_light: vec4f, + sun_light_dir: vec4f, + amb_light: vec4f, + material_color: vec4f, + material_light_response: vec4f, + atmosphere_enabled: u32, + clouds_enabled: u32, + shading_enabled: u32, + normal_mode: u32, + overlay_mode: u32, + track_render_mode: u32, + _padding0: u32, + _padding1: u32, +} + +; diff --git a/webgpu/engine/tile_mesh/TileMeshRenderer.cpp b/webgpu/engine/tile_mesh/TileMeshRenderer.cpp new file mode 100644 index 000000000..b0a77dc25 --- /dev/null +++ b/webgpu/engine/tile_mesh/TileMeshRenderer.cpp @@ -0,0 +1,342 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Adam Celarek + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TileMeshRenderer.h" + +#include "nucleus/camera/Definition.h" +#include "nucleus/utils/terrain_mesh_index_generator.h" +#include +#include +#include +#include + +using webgpu_engine::TileMeshRenderer; + +namespace { +template +int bufferLengthInBytes(const std::vector& vec) +{ + return int(vec.size() * sizeof(T)); +} +} // namespace + +namespace webgpu_engine { + +TileMeshRenderer::TileMeshRenderer(uint32_t height_resolution, uint32_t ortho_resolution) + : QObject { nullptr } + , m_height_resolution { height_resolution } + , m_ortho_resolution { ortho_resolution } +{ +} + +void TileMeshRenderer::init(webgpu::Context& ctx) +{ + m_ctx = &ctx; + + const auto height_resolution = glm::uvec2(m_height_resolution); + const auto ortho_resolution = glm::uvec2(m_ortho_resolution); + const auto num_layers = m_loaded_height_textures.size(); + + // create index buffer + const std::vector indices = nucleus::utils::terrain_mesh_index_generator::surface_quads_with_curtains(unsigned(m_height_resolution)); + m_index_buffer = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst, indices.size()); + m_index_buffer->write(m_ctx->queue(), indices.data(), indices.size()); + m_index_buffer_size = indices.size(); + + // create buffers for bounds, tile ids, zoom level, height and ortho texture buffers + m_bounds_buffer = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_tileset_id_buffer = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_height_zoom_level_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_height_texture_layer_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_ortho_zoom_level_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_ortho_texture_layer_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + + m_tile_id_buffer + = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst, num_layers); + m_n_edge_vertices_buffer = std::make_unique>(m_ctx->device(), WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + m_n_edge_vertices_buffer->data = int(m_height_resolution); + m_n_edge_vertices_buffer->update_gpu_data(m_ctx->queue()); + + WGPUTextureDescriptor height_texture_desc {}; + height_texture_desc.label = WGPUStringView { .data = "height texture", .length = WGPU_STRLEN }; + height_texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + height_texture_desc.size = { uint32_t(height_resolution.x), uint32_t(height_resolution.y), uint32_t(num_layers) }; + height_texture_desc.mipLevelCount = 1; + height_texture_desc.sampleCount = 1; + height_texture_desc.format = WGPUTextureFormat::WGPUTextureFormat_R16Uint; + height_texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + + WGPUSamplerDescriptor height_sampler_desc {}; + height_sampler_desc.label = WGPUStringView { .data = "height sampler", .length = WGPU_STRLEN }; + height_sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + height_sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + height_sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + // height_sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + // height_sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + // height_sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + height_sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + height_sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Nearest; + height_sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Nearest; + height_sampler_desc.lodMinClamp = 0.0f; + height_sampler_desc.lodMaxClamp = 1.0f; + height_sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + height_sampler_desc.maxAnisotropy = 1; + + m_heightmap_textures = std::make_unique(m_ctx->device(), height_texture_desc, height_sampler_desc); + + // TODO mipmaps and compression + WGPUTextureDescriptor ortho_texture_desc {}; + ortho_texture_desc.label = WGPUStringView { .data = "ortho texture", .length = WGPU_STRLEN }; + ortho_texture_desc.dimension = WGPUTextureDimension::WGPUTextureDimension_2D; + // TODO: array layers might become larger than allowed by graphics API + ortho_texture_desc.size = { uint32_t(ortho_resolution.x), uint32_t(ortho_resolution.y), uint32_t(num_layers) }; + ortho_texture_desc.mipLevelCount = 1; + ortho_texture_desc.sampleCount = 1; + ortho_texture_desc.format = WGPUTextureFormat::WGPUTextureFormat_RGBA8Unorm; + ortho_texture_desc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + + WGPUSamplerDescriptor ortho_sampler_desc {}; + ortho_sampler_desc.label = WGPUStringView { .data = "ortho sampler", .length = WGPU_STRLEN }; + ortho_sampler_desc.addressModeU = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + ortho_sampler_desc.addressModeV = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + ortho_sampler_desc.addressModeW = WGPUAddressMode::WGPUAddressMode_ClampToEdge; + ortho_sampler_desc.magFilter = WGPUFilterMode::WGPUFilterMode_Linear; + ortho_sampler_desc.minFilter = WGPUFilterMode::WGPUFilterMode_Linear; + ortho_sampler_desc.mipmapFilter = WGPUMipmapFilterMode::WGPUMipmapFilterMode_Linear; + ortho_sampler_desc.lodMinClamp = 0.0f; + ortho_sampler_desc.lodMaxClamp = 1.0f; + ortho_sampler_desc.compare = WGPUCompareFunction::WGPUCompareFunction_Undefined; + ortho_sampler_desc.maxAnisotropy = 1; + + m_ortho_textures = std::make_unique(m_ctx->device(), ortho_texture_desc, ortho_sampler_desc); + + auto& reg = ctx.resource_registry(); + reg.register_shader("render_tiles", "webgpu_engine::render_tiles"); + reg.register_bind_group_layout("tile", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry n_vertices_entry {}; + n_vertices_entry.binding = 0; + n_vertices_entry.visibility = WGPUShaderStage_Vertex; + n_vertices_entry.buffer.type = WGPUBufferBindingType_Uniform; + n_vertices_entry.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry heightmap_texture_entry {}; + heightmap_texture_entry.binding = 1; + heightmap_texture_entry.visibility = WGPUShaderStage_Vertex; + heightmap_texture_entry.texture.sampleType = WGPUTextureSampleType_Uint; + heightmap_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2DArray; + + WGPUBindGroupLayoutEntry heightmap_texture_sampler {}; + heightmap_texture_sampler.binding = 2; + heightmap_texture_sampler.visibility = WGPUShaderStage_Vertex; + heightmap_texture_sampler.sampler.type = WGPUSamplerBindingType_NonFiltering; + + WGPUBindGroupLayoutEntry ortho_texture_entry {}; + ortho_texture_entry.binding = 3; + ortho_texture_entry.visibility = WGPUShaderStage_Fragment; + ortho_texture_entry.texture.sampleType = WGPUTextureSampleType_Float; + ortho_texture_entry.texture.viewDimension = WGPUTextureViewDimension_2DArray; + + WGPUBindGroupLayoutEntry ortho_texture_sampler {}; + ortho_texture_sampler.binding = 4; + ortho_texture_sampler.visibility = WGPUShaderStage_Fragment; + ortho_texture_sampler.sampler.type = WGPUSamplerBindingType_Filtering; + + return std::make_unique(device, + std::vector { + n_vertices_entry, heightmap_texture_entry, heightmap_texture_sampler, ortho_texture_entry, ortho_texture_sampler }, + "tile bind group"); + }); + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + webgpu::util::SingleVertexBufferInfo bounds_buffer_info(WGPUVertexStepMode_Instance); + bounds_buffer_info.add_attribute(0); + webgpu::util::SingleVertexBufferInfo texture_layer_buffer_info(WGPUVertexStepMode_Instance); + texture_layer_buffer_info.add_attribute(1); + webgpu::util::SingleVertexBufferInfo ortho_texture_layer_buffer_info(WGPUVertexStepMode_Instance); + ortho_texture_layer_buffer_info.add_attribute(2); + webgpu::util::SingleVertexBufferInfo tileset_id_buffer_info(WGPUVertexStepMode_Instance); + tileset_id_buffer_info.add_attribute(3); + webgpu::util::SingleVertexBufferInfo height_zoomlevel_buffer_info(WGPUVertexStepMode_Instance); + height_zoomlevel_buffer_info.add_attribute(4); + webgpu::util::SingleVertexBufferInfo tile_id_buffer_info(WGPUVertexStepMode_Instance); + tile_id_buffer_info.add_attribute(5); + webgpu::util::SingleVertexBufferInfo ortho_zoomlevel_buffer_info(WGPUVertexStepMode_Instance); + ortho_zoomlevel_buffer_info.add_attribute(6); + + webgpu::FramebufferFormat format {}; + format.depth_format = WGPUTextureFormat_Depth24Plus; + format.color_formats.emplace_back(WGPUTextureFormat_R32Uint); // albedo + format.color_formats.emplace_back(WGPUTextureFormat_RGBA32Float); // position + format.color_formats.emplace_back(WGPUTextureFormat_RG16Uint); // normal + format.color_formats.emplace_back(WGPUTextureFormat_R32Uint); // overlay + + m_pipeline = std::make_unique(dev, + reg.shader("render_tiles"), + reg.shader("render_tiles"), + std::vector { + bounds_buffer_info, + texture_layer_buffer_info, + ortho_texture_layer_buffer_info, + tileset_id_buffer_info, + height_zoomlevel_buffer_info, + tile_id_buffer_info, + ortho_zoomlevel_buffer_info, + }, + format, + std::vector { + ®.bind_group_layout("shared_config"), + ®.bind_group_layout("camera"), + ®.bind_group_layout("tile"), + }); + + m_tile_bind_group = create_bind_group(m_ortho_textures->texture_view(), m_ortho_textures->sampler()); + }); +} + +void TileMeshRenderer::draw( + WGPURenderPassEncoder render_pass, const nucleus::camera::Definition& camera, const std::vector& draw_tiles) const +{ + std::vector bounds; + bounds.reserve(draw_tiles.size()); + + std::vector tileset_id; + tileset_id.reserve(draw_tiles.size()); + + std::vector height_zoom_levels; + height_zoom_levels.reserve(draw_tiles.size()); + + std::vector height_texture_layer; + height_texture_layer.reserve(draw_tiles.size()); + + std::vector ortho_zoom_levels; + ortho_zoom_levels.reserve(draw_tiles.size()); + + std::vector ortho_texture_layers; + ortho_texture_layers.reserve(draw_tiles.size()); + + std::vector tile_ids; + tile_ids.reserve(draw_tiles.size()); + + for (const auto& id_bounds : draw_tiles) { + const auto& tile_id = id_bounds.id; + const auto& tile_bounds = id_bounds.bounds; + bounds.emplace_back(tile_bounds.min.x - camera.position().x, + tile_bounds.min.y - camera.position().y, + tile_bounds.max.x - camera.position().x, + tile_bounds.max.y - camera.position().y); + tileset_id.emplace_back(tile_id.coords[0] + tile_id.coords[1]); + + const auto height_layer_info = m_loaded_height_textures.layer(tile_id); + height_zoom_levels.emplace_back(int(height_layer_info.id.zoom_level)); + height_texture_layer.emplace_back(int(height_layer_info.index)); + + const auto ortho_layer_info = m_loaded_ortho_textures.layer(tile_id); + ortho_zoom_levels.emplace_back(int(ortho_layer_info.id.zoom_level)); + ortho_texture_layers.emplace_back(int(ortho_layer_info.index)); + + tile_ids.emplace_back(nucleus::tile::GpuTileId(id_bounds.id)); + } + + // write updated vertex buffers + m_bounds_buffer->write(m_ctx->queue(), bounds.data(), bounds.size()); + m_tileset_id_buffer->write(m_ctx->queue(), tileset_id.data(), tileset_id.size()); + m_height_zoom_level_buffer->write(m_ctx->queue(), height_zoom_levels.data(), height_zoom_levels.size()); + m_height_texture_layer_buffer->write(m_ctx->queue(), height_texture_layer.data(), height_texture_layer.size()); + m_ortho_zoom_level_buffer->write(m_ctx->queue(), ortho_zoom_levels.data(), ortho_zoom_levels.size()); + m_ortho_texture_layer_buffer->write(m_ctx->queue(), ortho_texture_layers.data(), ortho_texture_layers.size()); + m_tile_id_buffer->write(m_ctx->queue(), tile_ids.data(), tile_ids.size()); + + // set bind group for uniforms, textures and samplers + wgpuRenderPassEncoderSetBindGroup(render_pass, 2, m_tile_bind_group->handle(), 0, nullptr); + + // set index buffer and vertex buffers + wgpuRenderPassEncoderSetIndexBuffer(render_pass, m_index_buffer->handle(), WGPUIndexFormat_Uint16, 0, m_index_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 0, m_bounds_buffer->handle(), 0, m_bounds_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 1, m_height_texture_layer_buffer->handle(), 0, m_height_texture_layer_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 2, m_ortho_texture_layer_buffer->handle(), 0, m_ortho_texture_layer_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 3, m_tileset_id_buffer->handle(), 0, m_tileset_id_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 4, m_height_zoom_level_buffer->handle(), 0, m_height_zoom_level_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 5, m_tile_id_buffer->handle(), 0, m_tile_id_buffer->size_in_byte()); + wgpuRenderPassEncoderSetVertexBuffer(render_pass, 6, m_ortho_zoom_level_buffer->handle(), 0, m_ortho_zoom_level_buffer->size_in_byte()); + + // set pipeline and draw call + wgpuRenderPassEncoderSetPipeline(render_pass, m_pipeline->pipeline().handle()); + wgpuRenderPassEncoderDrawIndexed(render_pass, uint32_t(m_index_buffer_size), uint32_t(draw_tiles.size()), 0, 0, 0); +} + +void TileMeshRenderer::set_tile_limit(unsigned int num_tiles) +{ + m_loaded_height_textures.set_tile_limit(num_tiles); + m_loaded_ortho_textures.set_tile_limit(num_tiles); +} + +std::unique_ptr TileMeshRenderer::create_bind_group(const webgpu::raii::TextureView& view, const webgpu::raii::Sampler& sampler) const +{ + return std::make_unique(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("tile"), + std::initializer_list { + m_n_edge_vertices_buffer->raw_buffer().create_bind_group_entry(0), + m_heightmap_textures->texture_view().create_bind_group_entry(1), + m_heightmap_textures->sampler().create_bind_group_entry(2), + view.create_bind_group_entry(3), + sampler.create_bind_group_entry(4), + }, + "tile bind group"); +} + +const webgpu::raii::GenericRenderPipeline& TileMeshRenderer::render_tiles_pipeline() const { return *m_pipeline; } + +void TileMeshRenderer::update_gpu_tiles_height(const std::vector& deleted_tiles, const std::vector& new_tiles) +{ + for (const auto& id : deleted_tiles) { + m_loaded_height_textures.remove_tile(id); + } + + for (const auto& tile : new_tiles) { + // test for validity + assert(tile.id.zoom_level < 100); + assert(tile.surface); + + // find empty spot and upload texture + const uint32_t layer_index = m_loaded_height_textures.add_tile(tile.id); + m_heightmap_textures->texture().write(m_ctx->queue(), *tile.surface, layer_index); + } +} + +void TileMeshRenderer::update_gpu_tiles_ortho(const std::vector& deleted_tiles, const std::vector& new_tiles) +{ + for (const auto& id : deleted_tiles) { + m_loaded_ortho_textures.remove_tile(id); + } + for (const auto& tile : new_tiles) { + // test for validity + assert(tile.id.zoom_level < 100); + assert(tile.texture); + + // find empty spot and upload texture + const auto layer_index = m_loaded_ortho_textures.add_tile(tile.id); + m_ortho_textures->texture().write(m_ctx->queue(), tile.texture->front(), uint32_t(layer_index)); + } +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/tile_mesh/TileMeshRenderer.h b/webgpu/engine/tile_mesh/TileMeshRenderer.h new file mode 100644 index 000000000..a92abcc22 --- /dev/null +++ b/webgpu/engine/tile_mesh/TileMeshRenderer.h @@ -0,0 +1,92 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2023 Adam Celerek + * Copyright (C) 2023 Gerald Kimmersdorfer + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::camera { +class Definition; +} + +namespace webgpu_engine { + +class TileMeshRenderer : public QObject { + Q_OBJECT +public: + explicit TileMeshRenderer(uint32_t height_resolution, uint32_t ortho_resolution); + + void init(webgpu::Context& ctx); + + void draw(WGPURenderPassEncoder render_pass, const nucleus::camera::Definition& camera, const std::vector& draw_tiles) const; + + std::unique_ptr create_bind_group(const webgpu::raii::TextureView& view, const webgpu::raii::Sampler& sampler) const; + + [[nodiscard]] const webgpu::raii::GenericRenderPipeline& render_tiles_pipeline() const; + + size_t capacity() const; + void set_tile_limit(unsigned new_limit); + +signals: + void tiles_changed(); + +public slots: + void update_gpu_tiles_height(const std::vector& deleted_tiles, const std::vector& new_tiles); + void update_gpu_tiles_ortho(const std::vector& deleted_tiles, const std::vector& new_tiles); + +private: + uint32_t m_height_resolution; + uint32_t m_ortho_resolution; + size_t m_num_layers; + nucleus::tile::GpuArrayHelper m_loaded_height_textures; + nucleus::tile::GpuArrayHelper m_loaded_ortho_textures; + + webgpu::Context* m_ctx = nullptr; + + size_t m_index_buffer_size; + std::unique_ptr> m_index_buffer; + std::unique_ptr> m_bounds_buffer; + std::unique_ptr> m_tileset_id_buffer; + std::unique_ptr> m_height_zoom_level_buffer; + std::unique_ptr> m_height_texture_layer_buffer; + std::unique_ptr> m_ortho_zoom_level_buffer; + std::unique_ptr> m_ortho_texture_layer_buffer; + std::unique_ptr> m_n_edge_vertices_buffer; + std::unique_ptr> m_tile_id_buffer; + + std::unique_ptr m_heightmap_textures; + std::unique_ptr m_ortho_textures; + std::unique_ptr m_tile_bind_group; + std::unique_ptr m_pipeline; +}; + +} // namespace webgpu_engine diff --git a/webgpu/engine/track/TrackRenderer.cpp b/webgpu/engine/track/TrackRenderer.cpp new file mode 100644 index 000000000..3e8ab8ea9 --- /dev/null +++ b/webgpu/engine/track/TrackRenderer.cpp @@ -0,0 +1,196 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TrackRenderer.h" + +#include "nucleus/srs.h" +#include "nucleus/track/GPX.h" +#include +#include +#include +#include + +namespace webgpu_engine { + +TrackRenderer::TrackRenderer() + : QObject { nullptr } + , m_ctx { nullptr } +{ +} + +radix::geometry::Aabb3d TrackRenderer::load_track(const std::string& path) +{ + std::unique_ptr gpx_track = nucleus::track::parse(QString::fromStdString(path)); + + std::vector points; + for (const auto& segment : gpx_track->track) { + points.reserve(points.size() + segment.size()); + for (const auto& point : segment) { + points.push_back({ point.latitude, point.longitude, point.elevation }); + } + } + add_track(points); + + return nucleus::track::compute_world_aabb(*gpx_track); +} + +void TrackRenderer::init(webgpu::Context& ctx) +{ + m_ctx = &ctx; + + auto& reg = ctx.resource_registry(); + reg.register_shader("render_lines", "webgpu_engine::render_lines"); + reg.register_bind_group_layout("lines", [](WGPUDevice device) { + WGPUBindGroupLayoutEntry input_positions_entry {}; + input_positions_entry.binding = 0; + input_positions_entry.visibility = WGPUShaderStage_Vertex; + input_positions_entry.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + input_positions_entry.buffer.minBindingSize = 0; + + WGPUBindGroupLayoutEntry input_config_entry {}; + input_config_entry.binding = 1; + input_config_entry.visibility = WGPUShaderStage_Fragment; + input_config_entry.buffer.type = WGPUBufferBindingType_Uniform; + input_config_entry.buffer.minBindingSize = 0; + + return std::make_unique( + device, std::vector { input_positions_entry, input_config_entry }, "line renderer, bind group layout"); + }); + reg.register_pipeline([this](WGPUDevice dev, const webgpu::RenderResourceRegistry& reg) { + WGPUBlendState blend_state {}; + blend_state.color.operation = WGPUBlendOperation_Add; + blend_state.color.srcFactor = WGPUBlendFactor_One; + blend_state.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + blend_state.alpha.operation = WGPUBlendOperation_Add; + blend_state.alpha.srcFactor = WGPUBlendFactor_Zero; + blend_state.alpha.dstFactor = WGPUBlendFactor_One; + + WGPUColorTargetState color_target_state {}; + color_target_state.blend = &blend_state; + color_target_state.writeMask = WGPUColorWriteMask_All; + color_target_state.format = m_ctx->surface_texture_format(); + + WGPUFragmentState fragment_state {}; + fragment_state.module = reg.shader("render_lines").handle(); + fragment_state.entryPoint = WGPUStringView { .data = "fragmentMain", .length = WGPU_STRLEN }; + fragment_state.constantCount = 0; + fragment_state.constants = nullptr; + fragment_state.targetCount = 1; + fragment_state.targets = &color_target_state; + + std::vector bind_group_layout_handles { + reg.bind_group_layout("shared_config").handle(), + reg.bind_group_layout("camera").handle(), + reg.bind_group_layout("depth_texture").handle(), + reg.bind_group_layout("lines").handle(), + }; + webgpu::raii::PipelineLayout layout(dev, bind_group_layout_handles); + + WGPURenderPipelineDescriptor pipeline_desc {}; + pipeline_desc.label = WGPUStringView { .data = "line render pipeline", .length = WGPU_STRLEN }; + pipeline_desc.vertex.module = reg.shader("render_lines").handle(); + pipeline_desc.vertex.entryPoint = WGPUStringView { .data = "vertexMain", .length = WGPU_STRLEN }; + pipeline_desc.vertex.bufferCount = 0; + pipeline_desc.vertex.buffers = nullptr; + pipeline_desc.vertex.constantCount = 0; + pipeline_desc.vertex.constants = nullptr; + pipeline_desc.primitive.topology = WGPUPrimitiveTopology::WGPUPrimitiveTopology_LineStrip; + pipeline_desc.primitive.stripIndexFormat = WGPUIndexFormat::WGPUIndexFormat_Uint16; + pipeline_desc.primitive.frontFace = WGPUFrontFace::WGPUFrontFace_CCW; + pipeline_desc.primitive.cullMode = WGPUCullMode::WGPUCullMode_None; + pipeline_desc.fragment = &fragment_state; + pipeline_desc.depthStencil = nullptr; + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = ~0u; + pipeline_desc.multisample.alphaToCoverageEnabled = false; + pipeline_desc.layout = layout.handle(); + + m_pipeline = std::make_unique(dev, pipeline_desc); + }); +} + +void TrackRenderer::add_track(const Track& track, const glm::vec4& color) +{ + std::vector gpu_points; + gpu_points.reserve(track.size()); + for (const glm::dvec3& coords : track) { + gpu_points.push_back(glm::fvec4(nucleus::srs::lat_long_alt_to_world(coords), 1)); + } + add_world_positions(gpu_points, color); +} + +void TrackRenderer::add_world_positions(const std::vector& world_positions, const glm::vec4& color) +{ + assert(!world_positions.empty()); + + m_position_buffers.emplace_back(std::make_unique>( + m_ctx->device(), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst, world_positions.size(), "track renderer, storage buffer for points")); + m_position_buffers.back()->write(m_ctx->queue(), world_positions.data(), world_positions.size()); + + m_line_config_buffers.emplace_back(std::make_unique>(m_ctx->device(), WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst)); + m_line_config_buffers.back()->data.color = color; + m_line_config_buffers.back()->update_gpu_data(m_ctx->queue()); + + m_bind_groups.emplace_back(std::make_unique(m_ctx->device(), + m_ctx->resource_registry().bind_group_layout("lines"), + std::initializer_list { + m_position_buffers.back()->create_bind_group_entry(0), m_line_config_buffers.back()->raw_buffer().create_bind_group_entry(1) })); +} + +void TrackRenderer::render(WGPUCommandEncoder command_encoder, + const webgpu::raii::BindGroup& shared_config, + const webgpu::raii::BindGroup& camera_config, + const webgpu::raii::BindGroup& depth_texture, + const webgpu::raii::TextureView& color_texture) +{ + WGPURenderPassColorAttachment color_attachment {}; + color_attachment.view = color_texture.handle(); + color_attachment.resolveTarget = nullptr; + color_attachment.loadOp = WGPULoadOp::WGPULoadOp_Load; + color_attachment.storeOp = WGPUStoreOp::WGPUStoreOp_Store; + color_attachment.clearValue = WGPUColor { 0.0, 0.0, 0.0, 0.0 }; + color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + + /*WGPURenderPassDepthStencilAttachment depth_stencil_attachment {}; + depth_stencil_attachment.view = depth_texture_view.handle(); + depth_stencil_attachment.depthLoadOp = WGPULoadOp_Undefined; + depth_stencil_attachment.depthStoreOp = WGPUStoreOp_Undefined; + depth_stencil_attachment.depthReadOnly = true; + depth_stencil_attachment.stencilLoadOp = WGPULoadOp::WGPULoadOp_Undefined; + depth_stencil_attachment.stencilStoreOp = WGPUStoreOp::WGPUStoreOp_Undefined; + depth_stencil_attachment.stencilReadOnly = true;*/ + + WGPURenderPassDescriptor render_pass_descriptor {}; + render_pass_descriptor.label = WGPUStringView { .data = "line render render pass", .length = WGPU_STRLEN }; + render_pass_descriptor.colorAttachmentCount = 1; + render_pass_descriptor.colorAttachments = &color_attachment; + render_pass_descriptor.depthStencilAttachment = nullptr; + render_pass_descriptor.timestampWrites = nullptr; + + auto render_pass = webgpu::raii::RenderPassEncoder(command_encoder, render_pass_descriptor); + wgpuRenderPassEncoderSetPipeline(render_pass.handle(), m_pipeline->handle()); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 0, shared_config.handle(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 1, camera_config.handle(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 2, depth_texture.handle(), 0, nullptr); + for (size_t i = 0; i < m_bind_groups.size(); i++) { + wgpuRenderPassEncoderSetBindGroup(render_pass.handle(), 3, m_bind_groups.at(i)->handle(), 0, nullptr); + wgpuRenderPassEncoderDraw(render_pass.handle(), uint32_t(m_position_buffers.at(i)->size()), 1, 0, 0); + } +} + +} // namespace webgpu_engine diff --git a/webgpu/engine/track/TrackRenderer.h b/webgpu/engine/track/TrackRenderer.h new file mode 100644 index 000000000..dafa4351b --- /dev/null +++ b/webgpu/engine/track/TrackRenderer.h @@ -0,0 +1,75 @@ +/***************************************************************************** + * weBIGeo + * Copyright (C) 2024 Patrick Komon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "webgpu/base/raii/BindGroup.h" +#include "webgpu/base/raii/RawBuffer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace webgpu_engine { + +using Coordinates = glm::dvec3; +using Track = std::vector; + +class TrackRenderer : public QObject { + Q_OBJECT +public: + struct LineConfig { + glm::vec4 color = { 1.0f, 0.0, 0.0, 1.0f }; + }; + + // Built-in preset track, loadable from the GUI ("Open Preset ...") and the QT_DEBUG auto-load. + static constexpr const char* DEFAULT_GPX_TRACK_PATH = ":/gpx/breite_ries.gpx"; + +public: + explicit TrackRenderer(); + + void init(webgpu::Context& ctx); + + // Parses a GPX file, adds its track for rendering, and returns the track's world-space AABB. + radix::geometry::Aabb3d load_track(const std::string& path); + + void add_track(const Track& track, const glm::vec4& color = { 78.0 / 255.0f, 163.0 / 255.0f, 196.0 / 255.0f, 1.0f }); + void add_world_positions(const std::vector& world_positions, const glm::vec4& color = { 1.0f, 0.0f, 0.0f, 1.0f }); + + void render(WGPUCommandEncoder command_encoder, + const webgpu::raii::BindGroup& shared_config, + const webgpu::raii::BindGroup& camera_config, + const webgpu::raii::BindGroup& depth_texture, + const webgpu::raii::TextureView& color_texture); + +private: + webgpu::Context* m_ctx; + + std::unique_ptr m_pipeline; + + std::vector>> m_position_buffers; + std::vector>> m_line_config_buffers; + std::vector> m_bind_groups; +}; + +} // namespace webgpu_engine