From 68459732beacdd0667acfeafce23edc4d69bc161 Mon Sep 17 00:00:00 2001 From: Birk Skyum <74932975+birkskyum@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:35:33 +0200 Subject: [PATCH] Add WebGPU Backend w/ Dawn Support (#3838) --- .github/labeler.yml | 7 + .github/scripts/install-linux-deps | 6 +- .github/workflows/macos-ci.yml | 75 +- .gitignore | 1 + BUILD.bazel | 15 + CMakeLists.txt | 118 ++ CMakePresets.json | 50 + bazel/core.bzl | 85 ++ cmake/validate-backend-options.cmake | 7 +- docs/mdbook/src/platforms/README.md | 19 +- include/mbgl/gfx/backend.hpp | 9 +- include/mbgl/gfx/dynamic_texture.hpp | 3 +- include/mbgl/gfx/renderable.hpp | 4 + include/mbgl/shaders/layer_ubo.hpp | 4 +- include/mbgl/shaders/webgpu/background.hpp | 230 ++++ include/mbgl/shaders/webgpu/circle.hpp | 211 +++ include/mbgl/shaders/webgpu/clipping_mask.hpp | 65 + include/mbgl/shaders/webgpu/collision.hpp | 240 ++++ include/mbgl/shaders/webgpu/common.hpp | 90 ++ .../mbgl/shaders/webgpu/custom_geometry.hpp | 66 + .../shaders/webgpu/custom_symbol_icon.hpp | 68 + include/mbgl/shaders/webgpu/debug.hpp | 80 ++ include/mbgl/shaders/webgpu/fill.hpp | 910 +++++++++++++ .../mbgl/shaders/webgpu/fill_extrusion.hpp | 411 ++++++ include/mbgl/shaders/webgpu/heatmap.hpp | 127 ++ .../mbgl/shaders/webgpu/heatmap_texture.hpp | 87 ++ include/mbgl/shaders/webgpu/hillshade.hpp | 122 ++ .../mbgl/shaders/webgpu/hillshade_prepare.hpp | 117 ++ include/mbgl/shaders/webgpu/line.hpp | 1027 ++++++++++++++ .../shaders/webgpu/location_indicator.hpp | 109 ++ include/mbgl/shaders/webgpu/raster.hpp | 134 ++ include/mbgl/shaders/webgpu/shader_group.hpp | 109 ++ .../mbgl/shaders/webgpu/shader_program.hpp | 169 +++ include/mbgl/shaders/webgpu/symbol.hpp | 752 +++++++++++ .../mbgl/shaders/webgpu/wgsl_preprocessor.hpp | 16 + include/mbgl/shaders/webgpu/widevector.hpp | 82 ++ include/mbgl/webgpu/buffer_resource.hpp | 65 + include/mbgl/webgpu/command_encoder.hpp | 84 ++ include/mbgl/webgpu/context.hpp | 132 ++ include/mbgl/webgpu/draw_scope_resource.hpp | 20 + include/mbgl/webgpu/drawable.hpp | 64 + include/mbgl/webgpu/drawable_builder.hpp | 29 + include/mbgl/webgpu/index_buffer_resource.hpp | 33 + include/mbgl/webgpu/layer_group.hpp | 31 + include/mbgl/webgpu/offscreen_texture.hpp | 22 + include/mbgl/webgpu/render_pass.hpp | 52 + include/mbgl/webgpu/renderable_resource.hpp | 54 + include/mbgl/webgpu/renderer_backend.hpp | 62 + include/mbgl/webgpu/texture2d.hpp | 64 + include/mbgl/webgpu/tile_layer_group.hpp | 31 + include/mbgl/webgpu/uniform_buffer.hpp | 60 + include/mbgl/webgpu/upload_pass.hpp | 70 + include/mbgl/webgpu/vertex_attribute.hpp | 52 + .../mbgl/webgpu/vertex_buffer_resource.hpp | 44 + include/mbgl/webgpu/wgpu_cpp_compat.hpp | 19 + metrics/ignores/platform-macos-webgpu.json | 8 + metrics/linux-webgpu-dawn.json | 15 + metrics/linux-webgpu-wgpu.json | 15 + metrics/macos-webgpu-dawn.json | 15 + metrics/macos-webgpu-wgpu.json | 15 + platform/darwin/darwin.cmake | 12 + .../include/mbgl/webgpu/headless_backend.hpp | 38 + platform/glfw/CMakeLists.txt | 31 + platform/glfw/glfw_view.cpp | 7 + platform/glfw/glfw_view.hpp | 3 +- platform/glfw/glfw_webgpu_backend.cpp | 4 + platform/glfw/glfw_webgpu_backend.hpp | 119 ++ platform/glfw/glfw_webgpu_backend.mm | 2 + platform/glfw/glfw_webgpu_backend_impl.ipp | 1198 +++++++++++++++++ src/mbgl/renderer/buckets/fill_bucket.hpp | 2 +- .../renderer/layers/line_layer_tweaker.cpp | 34 + .../renderer/layers/line_layer_tweaker.hpp | 9 +- .../renderer/layers/render_heatmap_layer.cpp | 20 + src/mbgl/renderer/paint_parameters.cpp | 43 +- .../renderer/sources/render_tile_source.cpp | 2 +- src/mbgl/shaders/webgpu/background.cpp | 29 + src/mbgl/shaders/webgpu/circle.cpp | 23 + src/mbgl/shaders/webgpu/clipping_mask.cpp | 15 + src/mbgl/shaders/webgpu/collision.cpp | 36 + src/mbgl/shaders/webgpu/custom_geometry.cpp | 19 + .../shaders/webgpu/custom_symbol_icon.cpp | 17 + src/mbgl/shaders/webgpu/debug.cpp | 16 + src/mbgl/shaders/webgpu/fill.cpp | 71 + src/mbgl/shaders/webgpu/fill_extrusion.cpp | 40 + src/mbgl/shaders/webgpu/heatmap.cpp | 18 + src/mbgl/shaders/webgpu/heatmap_texture.cpp | 17 + src/mbgl/shaders/webgpu/hillshade.cpp | 17 + src/mbgl/shaders/webgpu/hillshade_prepare.cpp | 17 + src/mbgl/shaders/webgpu/line.cpp | 69 + .../shaders/webgpu/location_indicator.cpp | 32 + src/mbgl/shaders/webgpu/raster.cpp | 18 + src/mbgl/shaders/webgpu/shader_program.cpp | 797 +++++++++++ src/mbgl/shaders/webgpu/symbol.cpp | 65 + src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp | 115 ++ src/mbgl/shaders/webgpu/widevector.cpp | 26 + src/mbgl/webgpu/buffer_resource.cpp | 149 ++ src/mbgl/webgpu/command_encoder.cpp | 118 ++ src/mbgl/webgpu/context.cpp | 467 +++++++ src/mbgl/webgpu/draw_scope_resource.cpp | 11 + src/mbgl/webgpu/drawable.cpp | 976 ++++++++++++++ src/mbgl/webgpu/drawable_builder.cpp | 88 ++ src/mbgl/webgpu/drawable_impl.hpp | 79 ++ src/mbgl/webgpu/headless_backend.cpp | 296 ++++ src/mbgl/webgpu/index_buffer_resource.cpp | 25 + src/mbgl/webgpu/layer_group.cpp | 83 ++ src/mbgl/webgpu/offscreen_texture.cpp | 324 +++++ src/mbgl/webgpu/render_pass.cpp | 226 ++++ src/mbgl/webgpu/renderer_backend.cpp | 191 +++ src/mbgl/webgpu/texture2d.cpp | 424 ++++++ src/mbgl/webgpu/tile_layer_group.cpp | 146 ++ src/mbgl/webgpu/uniform_buffer.cpp | 109 ++ src/mbgl/webgpu/upload_pass.cpp | 241 ++++ src/mbgl/webgpu/vertex_attribute.cpp | 43 + src/mbgl/webgpu/vertex_buffer_resource.cpp | 25 + test/BUILD.bazel | 6 +- test/CMakeLists.txt | 3 +- test/renderer/wgsl_preprocessor.test.cpp | 285 ++++ vendor/dawn.cmake | 100 ++ vendor/webgpu.cmake | 18 + 119 files changed, 14067 insertions(+), 28 deletions(-) create mode 100644 include/mbgl/shaders/webgpu/background.hpp create mode 100644 include/mbgl/shaders/webgpu/circle.hpp create mode 100644 include/mbgl/shaders/webgpu/clipping_mask.hpp create mode 100644 include/mbgl/shaders/webgpu/collision.hpp create mode 100644 include/mbgl/shaders/webgpu/common.hpp create mode 100644 include/mbgl/shaders/webgpu/custom_geometry.hpp create mode 100644 include/mbgl/shaders/webgpu/custom_symbol_icon.hpp create mode 100644 include/mbgl/shaders/webgpu/debug.hpp create mode 100644 include/mbgl/shaders/webgpu/fill.hpp create mode 100644 include/mbgl/shaders/webgpu/fill_extrusion.hpp create mode 100644 include/mbgl/shaders/webgpu/heatmap.hpp create mode 100644 include/mbgl/shaders/webgpu/heatmap_texture.hpp create mode 100644 include/mbgl/shaders/webgpu/hillshade.hpp create mode 100644 include/mbgl/shaders/webgpu/hillshade_prepare.hpp create mode 100644 include/mbgl/shaders/webgpu/line.hpp create mode 100644 include/mbgl/shaders/webgpu/location_indicator.hpp create mode 100644 include/mbgl/shaders/webgpu/raster.hpp create mode 100644 include/mbgl/shaders/webgpu/shader_group.hpp create mode 100644 include/mbgl/shaders/webgpu/shader_program.hpp create mode 100644 include/mbgl/shaders/webgpu/symbol.hpp create mode 100644 include/mbgl/shaders/webgpu/wgsl_preprocessor.hpp create mode 100644 include/mbgl/shaders/webgpu/widevector.hpp create mode 100644 include/mbgl/webgpu/buffer_resource.hpp create mode 100644 include/mbgl/webgpu/command_encoder.hpp create mode 100644 include/mbgl/webgpu/context.hpp create mode 100644 include/mbgl/webgpu/draw_scope_resource.hpp create mode 100644 include/mbgl/webgpu/drawable.hpp create mode 100644 include/mbgl/webgpu/drawable_builder.hpp create mode 100644 include/mbgl/webgpu/index_buffer_resource.hpp create mode 100644 include/mbgl/webgpu/layer_group.hpp create mode 100644 include/mbgl/webgpu/offscreen_texture.hpp create mode 100644 include/mbgl/webgpu/render_pass.hpp create mode 100644 include/mbgl/webgpu/renderable_resource.hpp create mode 100644 include/mbgl/webgpu/renderer_backend.hpp create mode 100644 include/mbgl/webgpu/texture2d.hpp create mode 100644 include/mbgl/webgpu/tile_layer_group.hpp create mode 100644 include/mbgl/webgpu/uniform_buffer.hpp create mode 100644 include/mbgl/webgpu/upload_pass.hpp create mode 100644 include/mbgl/webgpu/vertex_attribute.hpp create mode 100644 include/mbgl/webgpu/vertex_buffer_resource.hpp create mode 100644 include/mbgl/webgpu/wgpu_cpp_compat.hpp create mode 100644 metrics/ignores/platform-macos-webgpu.json create mode 100644 metrics/linux-webgpu-dawn.json create mode 100644 metrics/linux-webgpu-wgpu.json create mode 100644 metrics/macos-webgpu-dawn.json create mode 100644 metrics/macos-webgpu-wgpu.json create mode 100644 platform/default/include/mbgl/webgpu/headless_backend.hpp create mode 100644 platform/glfw/glfw_webgpu_backend.cpp create mode 100644 platform/glfw/glfw_webgpu_backend.hpp create mode 100644 platform/glfw/glfw_webgpu_backend.mm create mode 100644 platform/glfw/glfw_webgpu_backend_impl.ipp create mode 100644 src/mbgl/shaders/webgpu/background.cpp create mode 100644 src/mbgl/shaders/webgpu/circle.cpp create mode 100644 src/mbgl/shaders/webgpu/clipping_mask.cpp create mode 100644 src/mbgl/shaders/webgpu/collision.cpp create mode 100644 src/mbgl/shaders/webgpu/custom_geometry.cpp create mode 100644 src/mbgl/shaders/webgpu/custom_symbol_icon.cpp create mode 100644 src/mbgl/shaders/webgpu/debug.cpp create mode 100644 src/mbgl/shaders/webgpu/fill.cpp create mode 100644 src/mbgl/shaders/webgpu/fill_extrusion.cpp create mode 100644 src/mbgl/shaders/webgpu/heatmap.cpp create mode 100644 src/mbgl/shaders/webgpu/heatmap_texture.cpp create mode 100644 src/mbgl/shaders/webgpu/hillshade.cpp create mode 100644 src/mbgl/shaders/webgpu/hillshade_prepare.cpp create mode 100644 src/mbgl/shaders/webgpu/line.cpp create mode 100644 src/mbgl/shaders/webgpu/location_indicator.cpp create mode 100644 src/mbgl/shaders/webgpu/raster.cpp create mode 100644 src/mbgl/shaders/webgpu/shader_program.cpp create mode 100644 src/mbgl/shaders/webgpu/symbol.cpp create mode 100644 src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp create mode 100644 src/mbgl/shaders/webgpu/widevector.cpp create mode 100644 src/mbgl/webgpu/buffer_resource.cpp create mode 100644 src/mbgl/webgpu/command_encoder.cpp create mode 100644 src/mbgl/webgpu/context.cpp create mode 100644 src/mbgl/webgpu/draw_scope_resource.cpp create mode 100644 src/mbgl/webgpu/drawable.cpp create mode 100644 src/mbgl/webgpu/drawable_builder.cpp create mode 100644 src/mbgl/webgpu/drawable_impl.hpp create mode 100644 src/mbgl/webgpu/headless_backend.cpp create mode 100644 src/mbgl/webgpu/index_buffer_resource.cpp create mode 100644 src/mbgl/webgpu/layer_group.cpp create mode 100644 src/mbgl/webgpu/offscreen_texture.cpp create mode 100644 src/mbgl/webgpu/render_pass.cpp create mode 100644 src/mbgl/webgpu/renderer_backend.cpp create mode 100644 src/mbgl/webgpu/texture2d.cpp create mode 100644 src/mbgl/webgpu/tile_layer_group.cpp create mode 100644 src/mbgl/webgpu/uniform_buffer.cpp create mode 100644 src/mbgl/webgpu/upload_pass.cpp create mode 100644 src/mbgl/webgpu/vertex_attribute.cpp create mode 100644 src/mbgl/webgpu/vertex_buffer_resource.cpp create mode 100644 test/renderer/wgsl_preprocessor.test.cpp create mode 100644 vendor/dawn.cmake create mode 100644 vendor/webgpu.cmake diff --git a/.github/labeler.yml b/.github/labeler.yml index 1643249a9..2decbffef 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -56,3 +56,10 @@ metal: - any-glob-to-any-file: - '**/*metal*' - '**/*metal*/**' +WebGPU: +- changed-files: + - any-glob-to-any-file: + - '**/*webgpu*' + - '**/*webgpu*/**' + - '**/*wgpu*' + - '**/*dawn*' diff --git a/.github/scripts/install-linux-deps b/.github/scripts/install-linux-deps index c6a7e5537..3fc5afdb3 100755 --- a/.github/scripts/install-linux-deps +++ b/.github/scripts/install-linux-deps @@ -12,4 +12,8 @@ DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ mesa-vulkan-drivers \ llvm \ glslang-dev \ - libbz2-dev + libbz2-dev \ + libxinerama-dev \ + libxcursor-dev \ + libxi-dev \ + libx11-xcb-dev diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index c21f47455..82dd3eba6 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -8,12 +8,15 @@ on: paths: - 'platform/macos/**' - 'platform/darwin/**' + - 'platform/glfw/**' - '.github/workflows/macos-ci.yml' - 'bin/**' - 'include/**' - 'platform/default/**' + - 'render-test/**' - 'src/**' - 'vendor/**' + - 'metrics/**' - '.gitmodules' - '!**/*.md' - 'WORKSPACE' @@ -21,6 +24,9 @@ on: - '.bazelrc' - '.bazelversion' - 'pnpm-lock.yaml' + - 'CMakeLists.txt' + - 'CMakePresets.json' + - 'cmake/**' pull_request: branches: @@ -28,12 +34,15 @@ on: paths: - 'platform/macos/**' - 'platform/darwin/**' + - 'platform/glfw/**' - '.github/workflows/macos-ci.yml' - 'bin/**' - 'include/**' - 'platform/default/**' + - 'render-test/**' - 'src/**' - 'vendor/**' + - 'metrics/**' - '.gitmodules' - '!**/*.md' - 'WORKSPACE' @@ -41,6 +50,9 @@ on: - '.bazelrc' - '.bazelversion' - 'pnpm-lock.yaml' + - 'CMakeLists.txt' + - 'CMakePresets.json' + - 'cmake/**' concurrency: # cancel jobs on PRs only @@ -49,8 +61,8 @@ concurrency: jobs: build: - name: MacOS CI Tests - runs-on: [macos-14] + name: MacOS Metal CI Tests + runs-on: [macos-15] defaults: run: working-directory: platform/macos @@ -60,7 +72,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 with: submodules: recursive - persist-credentials: false + fetch-depth: 0 - name: Cache Bazel uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 @@ -80,3 +92,60 @@ jobs: - name: Generate Xcode project for macOS run: bazel run //platform/macos:xcodeproj --@rules_xcodeproj//xcodeproj:extra_common_flags="--//:renderer=metal" + + webgpu-cmake: + name: macOS WebGPU (${{ matrix.variant.name }}) + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + variant: + - impl: dawn + name: Dawn + # - impl: wgpu + # name: wgpu-native + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 + with: + submodules: recursive + persist-credentials: false + + - name: Install dependencies + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + run: | + brew install cmake ninja pkg-config python@3.11 go glfw libuv icu4c jpeg-turbo webp libpng ccache + brew link --overwrite python@3.11 + + - name: Configure environment + run: | + { + echo "PKG_CONFIG_PATH=$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix libuv)/lib/pkgconfig:${PKG_CONFIG_PATH}" + echo "PATH=$(brew --prefix python@3.11)/bin:$(brew --prefix go)/bin:$PATH" + echo "CC=clang" + echo "CXX=clang++" + } >> "$GITHUB_ENV" + + - name: Configure build + run: cmake --preset macos-webgpu-${{ matrix.variant.impl }} + + - name: Build targets + run: | + cmake --build build-macos-webgpu-${{ matrix.variant.impl }} --target \ + mbgl-render-test-runner + + - name: Prepare render test output directory + run: mkdir -p metrics/macos-webgpu-${{ matrix.variant.impl }} + + - name: Run WebGPU render tests + id: render_test + run: build-macos-webgpu-${{ matrix.variant.impl }}/mbgl-render-test-runner --manifestPath metrics/macos-webgpu-${{ matrix.variant.impl }}.json + + - name: Upload render report + if: always() && steps.render_test.outcome == 'failure' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: macos-webgpu-${{ matrix.variant.impl }}-render-report + path: metrics/macos-webgpu-${{ matrix.variant.impl }}.html diff --git a/.gitignore b/.gitignore index c6960e46e..b3130d0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ compile_commands.json node_modules .cxx .env +vendor/dawn/ # Node binaries. /lib diff --git a/BUILD.bazel b/BUILD.bazel index 5f368cda4..e4f53aa15 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,6 +12,8 @@ load( "MLN_DRAWABLES_MTL_HEADERS", "MLN_DRAWABLES_MTL_SOURCE", "MLN_DRAWABLES_SOURCE", + "MLN_DRAWABLES_WEBGPU_HEADERS", + "MLN_DRAWABLES_WEBGPU_SOURCE", "MLN_GENERATED_OPENGL_SHADER_HEADERS", "MLN_GENERATED_SHADER_HEADERS", "MLN_GENERATED_SHADER_SOURCE", @@ -85,6 +87,7 @@ cc_library( MLN_GENERATED_STYLE_SOURCE + select({ ":drawable_renderer": MLN_OPENGL_SOURCE + MLN_DRAWABLES_SOURCE + MLN_DRAWABLES_GL_SOURCE, ":metal_renderer": MLN_DRAWABLES_SOURCE + MLN_DRAWABLES_MTL_SOURCE, + ":webgpu_renderer": MLN_DRAWABLES_SOURCE + MLN_DRAWABLES_WEBGPU_SOURCE, "//conditions:default": [], }) + select({ "//:harfbuzz_text_shaping": MLN_SHAPING_HARFBUZZ_SRCS, @@ -93,6 +96,7 @@ cc_library( hdrs = MLN_LAYER_PLUGIN_HEADERS + MLN_CORE_HEADERS + select({ ":drawable_renderer": MLN_OPENGL_HEADERS + MLN_DRAWABLES_HEADERS + MLN_DRAWABLES_GL_HEADERS, ":metal_renderer": MLN_DRAWABLES_HEADERS + MLN_DRAWABLES_MTL_HEADERS, + ":webgpu_renderer": MLN_DRAWABLES_HEADERS + MLN_DRAWABLES_WEBGPU_HEADERS, "//conditions:default": [], }), copts = CPP_FLAGS + MAPLIBRE_FLAGS, @@ -103,6 +107,9 @@ cc_library( ":metal_renderer": [ "MLN_RENDER_BACKEND_METAL=1", ], + ":webgpu_renderer": [ + "MLN_RENDER_BACKEND_WEBGPU=1", + ], }) + select({ "@platforms//os:ios": ["GLES_SILENCE_DEPRECATION=1"], "//conditions:default": [], @@ -188,6 +195,7 @@ string_flag( values = [ "drawable", "metal", + "webgpu", ], ) @@ -205,6 +213,13 @@ config_setting( }, ) +config_setting( + name = "webgpu_renderer", + flag_values = { + ":renderer": "webgpu", + }, +) + # Selects the shaping implementation to utilize in the core string_flag( diff --git a/CMakeLists.txt b/CMakeLists.txt index f9936c86a..765de0d8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ option(MLN_WITH_OPENGL "Build with OpenGL renderer" OFF) option(MLN_WITH_EGL "Build with EGL renderer" OFF) option(MLN_WITH_VULKAN "Build with Vulkan renderer" OFF) option(MLN_WITH_METAL "Build with Metal renderer" OFF) +option(MLN_WITH_WEBGPU "Build with WebGPU renderer" OFF) option(MLN_WITH_PMTILES "Build with PMTiles support" ON) option(MLN_WITH_WERROR "Make all compilation warnings errors" ON) option(MLN_USE_UNORDERED_DENSE "Use ankerl dense containers for performance" ON) @@ -1309,6 +1310,119 @@ if(MLN_WITH_VULKAN) ) endif() +if(MLN_WITH_WEBGPU) + message(STATUS "Configuring WebGPU renderer backend") + + target_compile_definitions( + mbgl-core + PUBLIC + MLN_RENDER_BACKEND_WEBGPU=1 + ) + + list(APPEND + SRC_FILES + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/buffer_resource.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/context.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/command_encoder.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/headless_backend.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/renderer_backend.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/drawable.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/drawable_builder.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/draw_scope_resource.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/uniform_buffer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/shader_program.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/background.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/circle.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/clipping_mask.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/collision.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/custom_geometry.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/custom_symbol_icon.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/debug.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/fill.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/fill_extrusion.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/heatmap.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/heatmap_texture.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/hillshade.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/hillshade_prepare.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/line.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/location_indicator.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/raster.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/symbol.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/widevector.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/vertex_buffer_resource.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/vertex_attribute.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/texture2d.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/offscreen_texture.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/tile_layer_group.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/layer_group.cpp + ) + + list(APPEND + INCLUDE_FILES + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/buffer_resource.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/command_encoder.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/context.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/drawable.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/drawable_builder.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/index_buffer_resource.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/render_pass.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/renderer_backend.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/shader_program.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/background.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/circle.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/clipping_mask.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/collision.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/common.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/custom_geometry.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/custom_symbol_icon.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/debug.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/fill.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/fill_extrusion.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/heatmap.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/heatmap_texture.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/hillshade.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/hillshade_prepare.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/line.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/location_indicator.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/raster.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/symbol.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/shaders/webgpu/widevector.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/texture2d.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/uniform_buffer.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/upload_pass.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/webgpu/vertex_buffer_resource.hpp + ) + + list(APPEND + SRC_FILES + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/buffer_resource.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/command_encoder.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/context.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/drawable.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/drawable_builder.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/index_buffer_resource.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/render_pass.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/renderer_backend.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/shaders/webgpu/shader_program.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/texture2d.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/uniform_buffer.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/upload_pass.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/vertex_buffer_resource.cpp + ) + + # Include WebGPU vendor configuration (Dawn or wgpu) + include(${PROJECT_SOURCE_DIR}/vendor/webgpu.cmake) + + # Include Dawn integration when requested + if(MLN_WEBGPU_IMPL STREQUAL "dawn") + include(${PROJECT_SOURCE_DIR}/vendor/dawn.cmake) + if(TARGET mbgl-vendor-dawn) + target_link_libraries(mbgl-core PUBLIC mbgl-vendor-dawn) + endif() + endif() +endif() + list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/custom_drawable_layer_factory.cpp @@ -1515,6 +1629,10 @@ if(MLN_TEXT_SHAPING_HARFBUZZ) list(APPEND EXPORT_TARGETS mbgl-freetype mbgl-harfbuzz) endif() +if(TARGET mbgl-vendor-dawn) + list(APPEND EXPORT_TARGETS mbgl-vendor-dawn) +endif() + export(TARGETS ${EXPORT_TARGETS} FILE MapboxCoreTargets.cmake) if(MLN_WITH_VULKAN) diff --git a/CMakePresets.json b/CMakePresets.json index c685117d1..07059891f 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -75,6 +75,34 @@ "MLN_DARWIN_USE_LIBUV": "OFF" } }, + { + "name": "macos-webgpu-dawn", + "displayName": "macOS WebGPU (Dawn)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-macos-webgpu-dawn", + "inherits": "macos", + "cacheVariables": { + "MLN_WITH_METAL": "OFF", + "MLN_WITH_OPENGL": "OFF", + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL": "dawn", + "MLN_DARWIN_USE_LIBUV": "ON" + } + }, + { + "name": "macos-webgpu-wgpu", + "displayName": "macOS WebGPU (wgpu-native)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-macos-webgpu-wgpu", + "inherits": "macos", + "cacheVariables": { + "MLN_WITH_METAL": "OFF", + "MLN_WITH_OPENGL": "OFF", + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL": "wgpu", + "MLN_DARWIN_USE_LIBUV": "ON" + } + }, { "name": "linux", "hidden": true, @@ -113,6 +141,28 @@ "MLN_WITH_X11": "ON" } }, + { + "name": "linux-webgpu-dawn", + "binaryDir": "${sourceDir}/build-linux-webgpu-dawn", + "inherits": "linux", + "cacheVariables": { + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL": "dawn", + "MLN_WITH_WAYLAND": "OFF", + "MLN_WITH_X11": "ON" + } + }, + { + "name": "linux-webgpu-wgpu", + "binaryDir": "${sourceDir}/build-linux-webgpu-wgpu", + "inherits": "linux", + "cacheVariables": { + "MLN_WITH_WEBGPU": "ON", + "MLN_WEBGPU_IMPL": "wgpu", + "MLN_WITH_WAYLAND": "OFF", + "MLN_WITH_X11": "ON" + } + }, { "name": "linux-opengl-node", "inherits": "linux", diff --git a/bazel/core.bzl b/bazel/core.bzl index d21d74766..6271e28ad 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -1101,6 +1101,91 @@ MLN_DRAWABLES_MTL_HEADERS = [ "include/mbgl/shaders/mtl/widevector.hpp", ] +MLN_DRAWABLES_WEBGPU_SOURCE = [ + "src/mbgl/webgpu/buffer_resource.cpp", + "src/mbgl/webgpu/command_encoder.cpp", + "src/mbgl/webgpu/context.cpp", + "src/mbgl/webgpu/draw_scope_resource.cpp", + "src/mbgl/webgpu/drawable.cpp", + "src/mbgl/webgpu/drawable_builder.cpp", + "src/mbgl/webgpu/headless_backend.cpp", + "src/mbgl/webgpu/index_buffer_resource.cpp", + "src/mbgl/webgpu/layer_group.cpp", + "src/mbgl/webgpu/offscreen_texture.cpp", + "src/mbgl/webgpu/render_pass.cpp", + "src/mbgl/webgpu/renderer_backend.cpp", + "src/mbgl/webgpu/texture2d.cpp", + "src/mbgl/webgpu/tile_layer_group.cpp", + "src/mbgl/webgpu/uniform_buffer.cpp", + "src/mbgl/webgpu/upload_pass.cpp", + "src/mbgl/webgpu/vertex_attribute.cpp", + "src/mbgl/webgpu/vertex_buffer_resource.cpp", + "src/mbgl/shaders/webgpu/shader_program.cpp", + "src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp", + "src/mbgl/shaders/webgpu/background.cpp", + "src/mbgl/shaders/webgpu/circle.cpp", + "src/mbgl/shaders/webgpu/clipping_mask.cpp", + "src/mbgl/shaders/webgpu/collision.cpp", + "src/mbgl/shaders/webgpu/custom_geometry.cpp", + "src/mbgl/shaders/webgpu/custom_symbol_icon.cpp", + "src/mbgl/shaders/webgpu/debug.cpp", + "src/mbgl/shaders/webgpu/fill.cpp", + "src/mbgl/shaders/webgpu/fill_extrusion.cpp", + "src/mbgl/shaders/webgpu/heatmap.cpp", + "src/mbgl/shaders/webgpu/heatmap_texture.cpp", + "src/mbgl/shaders/webgpu/hillshade.cpp", + "src/mbgl/shaders/webgpu/hillshade_prepare.cpp", + "src/mbgl/shaders/webgpu/line.cpp", + "src/mbgl/shaders/webgpu/location_indicator.cpp", + "src/mbgl/shaders/webgpu/raster.cpp", + "src/mbgl/shaders/webgpu/symbol.cpp", + "src/mbgl/shaders/webgpu/widevector.cpp", +] + +MLN_DRAWABLES_WEBGPU_HEADERS = [ + "include/mbgl/webgpu/buffer_resource.hpp", + "include/mbgl/webgpu/command_encoder.hpp", + "include/mbgl/webgpu/context.hpp", + "include/mbgl/webgpu/draw_scope_resource.hpp", + "include/mbgl/webgpu/drawable.hpp", + "include/mbgl/webgpu/drawable_builder.hpp", + "include/mbgl/webgpu/index_buffer_resource.hpp", + "include/mbgl/webgpu/layer_group.hpp", + "include/mbgl/webgpu/offscreen_texture.hpp", + "include/mbgl/webgpu/render_pass.hpp", + "include/mbgl/webgpu/renderable_resource.hpp", + "include/mbgl/webgpu/renderer_backend.hpp", + "include/mbgl/webgpu/texture2d.hpp", + "include/mbgl/webgpu/tile_layer_group.hpp", + "include/mbgl/webgpu/uniform_buffer.hpp", + "include/mbgl/webgpu/upload_pass.hpp", + "include/mbgl/webgpu/vertex_attribute.hpp", + "include/mbgl/webgpu/vertex_buffer_resource.hpp", + "include/mbgl/webgpu/wgpu_cpp_compat.hpp", + "include/mbgl/shaders/webgpu/background.hpp", + "include/mbgl/shaders/webgpu/circle.hpp", + "include/mbgl/shaders/webgpu/clipping_mask.hpp", + "include/mbgl/shaders/webgpu/collision.hpp", + "include/mbgl/shaders/webgpu/common.hpp", + "include/mbgl/shaders/webgpu/custom_geometry.hpp", + "include/mbgl/shaders/webgpu/custom_symbol_icon.hpp", + "include/mbgl/shaders/webgpu/debug.hpp", + "include/mbgl/shaders/webgpu/fill.hpp", + "include/mbgl/shaders/webgpu/fill_extrusion.hpp", + "include/mbgl/shaders/webgpu/heatmap.hpp", + "include/mbgl/shaders/webgpu/heatmap_texture.hpp", + "include/mbgl/shaders/webgpu/hillshade.hpp", + "include/mbgl/shaders/webgpu/hillshade_prepare.hpp", + "include/mbgl/shaders/webgpu/line.hpp", + "include/mbgl/shaders/webgpu/location_indicator.hpp", + "include/mbgl/shaders/webgpu/raster.hpp", + "include/mbgl/shaders/webgpu/shader_group.hpp", + "include/mbgl/shaders/webgpu/shader_program.hpp", + "include/mbgl/shaders/webgpu/symbol.hpp", + "include/mbgl/shaders/webgpu/wgsl_preprocessor.hpp", + "include/mbgl/shaders/webgpu/widevector.hpp", +] + MLN_SHAPING_HARFBUZZ_SRCS = [ "src/mbgl/text/freetype.hpp", "src/mbgl/text/freetype.cpp", diff --git a/cmake/validate-backend-options.cmake b/cmake/validate-backend-options.cmake index a7bfcf075..d439eced8 100644 --- a/cmake/validate-backend-options.cmake +++ b/cmake/validate-backend-options.cmake @@ -16,13 +16,16 @@ endif() if (MLN_WITH_VULKAN) math(EXPR backend_count "${backend_count} + 1") endif() +if (MLN_WITH_WEBGPU) + math(EXPR backend_count "${backend_count} + 1") +endif() if (backend_count EQUAL 0) message(FATAL_ERROR "You need to set a rendering backend. " - "Set exactly one of: MLN_WITH_OPENGL, MLN_WITH_METAL, or MLN_WITH_VULKAN.") + "Set exactly one of: MLN_WITH_OPENGL, MLN_WITH_METAL, MLN_WITH_VULKAN, or MLN_WITH_WEBGPU.") elseif (backend_count GREATER 1) message(FATAL_ERROR "Multiple rendering backends selected. " - "Please enable only one of: MLN_WITH_OPENGL, MLN_WITH_METAL, or MLN_WITH_VULKAN.") + "Please enable only one of: MLN_WITH_OPENGL, MLN_WITH_METAL, MLN_WITH_VULKAN, or MLN_WITH_WEBGPU.") endif() diff --git a/docs/mdbook/src/platforms/README.md b/docs/mdbook/src/platforms/README.md index 4608c5855..278d17f08 100644 --- a/docs/mdbook/src/platforms/README.md +++ b/docs/mdbook/src/platforms/README.md @@ -28,15 +28,16 @@ Originally the project only supported OpenGL 2.0. In 2023, the [renderer was mod What platforms support which rendering backend can be found below. -| Platform | OpenGL ES 3.0 | Vulkan 1.0 | Metal | -|---|---|---|---| -| Android | ✅ | ✅ | ❌ | -| iOS | ❌ | ❌ | ✅ | -| Linux | ✅ | ✅ | ❌ | -| Windows | ✅ | ❌ | ❌ | -| macOS | ❌ | ✅ | ✅[^1] | -| Node.js | ✅ | ❌ | ✅ [^2] | -| Qt | ✅ | ❌ | ❌ | + +| Platform | OpenGL ES 3.0 | Vulkan 1.0 | Metal | WebGPU | +| -------- | -------------- | ---------- | ------ | ------- | +| Android | ✅ | ✅ | ❌ | ❌ | +| iOS | ❌ | ❌ | ✅ | ❌ | +| Linux | ✅ | ✅ | ❌ | ✅ | +| Windows | ✅ | ❌ | ❌ | ❓ | +| macOS | ❌ | ✅ | ✅[^1] | ✅ | +| Node.js | ✅ | ❌ | ✅[^2] | ❌ | +| Qt | ✅ | ❌ | ❌ | ❌ | [^1]: Requires MoltenVK. Only available when built via CMake. [^2]: Issue reported, see [#2928](https://github.com/maplibre/maplibre-native/issues/2928). diff --git a/include/mbgl/gfx/backend.hpp b/include/mbgl/gfx/backend.hpp index 28c5a4ebc..3ff2021e0 100644 --- a/include/mbgl/gfx/backend.hpp +++ b/include/mbgl/gfx/backend.hpp @@ -14,11 +14,14 @@ public: OpenGL, ///< The OpenGL API backend Metal, ///< The Metal API backend Vulkan, ///< The Vulkan API backend + WebGPU, ///< The WebGPU API backend TYPE_MAX, ///< Not a valid backend type, used to determine the number ///< of available backends (ie for array allocation). }; -#if MLN_RENDER_BACKEND_METAL +#if MLN_RENDER_BACKEND_WEBGPU + static constexpr Type DefaultType = Type::WebGPU; +#elif MLN_RENDER_BACKEND_METAL static constexpr Type DefaultType = Type::Metal; #elif MLN_RENDER_BACKEND_VULKAN static constexpr Type DefaultType = Type::Vulkan; @@ -39,7 +42,9 @@ public: template static std::unique_ptr Create(Args... args) { -#if MLN_RENDER_BACKEND_METAL +#if MLN_RENDER_BACKEND_WEBGPU + return Create(std::forward(args)...); +#elif MLN_RENDER_BACKEND_METAL return Create(std::forward(args)...); #elif MLN_RENDER_BACKEND_VULKAN return Create(std::forward(args)...); diff --git a/include/mbgl/gfx/dynamic_texture.hpp b/include/mbgl/gfx/dynamic_texture.hpp index 6b436a981..024f1c8d4 100644 --- a/include/mbgl/gfx/dynamic_texture.hpp +++ b/include/mbgl/gfx/dynamic_texture.hpp @@ -75,7 +75,8 @@ private: std::mutex mutex; }; -#define MLN_DEFER_UPLOAD_ON_RENDER_THREAD (MLN_RENDER_BACKEND_OPENGL || MLN_RENDER_BACKEND_VULKAN) +#define MLN_DEFER_UPLOAD_ON_RENDER_THREAD \ + (MLN_RENDER_BACKEND_OPENGL || MLN_RENDER_BACKEND_VULKAN || MLN_RENDER_BACKEND_WEBGPU) } // namespace gfx } // namespace mbgl diff --git a/include/mbgl/gfx/renderable.hpp b/include/mbgl/gfx/renderable.hpp index f6c707d6e..d135db8f2 100644 --- a/include/mbgl/gfx/renderable.hpp +++ b/include/mbgl/gfx/renderable.hpp @@ -42,6 +42,10 @@ public: return static_cast(*resource); } + bool hasResource() const { return resource != nullptr; } + + void setResource(std::unique_ptr resource_) { resource = std::move(resource_); } + virtual void wait() {} bool operator!=(const Renderable& other) const { return resource.get() != other.resource.get(); } diff --git a/include/mbgl/shaders/layer_ubo.hpp b/include/mbgl/shaders/layer_ubo.hpp index 1f3d97887..6477029c5 100644 --- a/include/mbgl/shaders/layer_ubo.hpp +++ b/include/mbgl/shaders/layer_ubo.hpp @@ -65,6 +65,8 @@ enum { idGlobalUBOIndex, #elif MLN_RENDER_BACKEND_VULKAN idGlobalPlatformParamsUBO, +#elif MLN_RENDER_BACKEND_WEBGPU + idGlobalUBOIndex, #endif globalUBOCount }; @@ -75,7 +77,7 @@ enum { drawableReservedUBOCount }; -#define MLN_UBO_CONSOLIDATION (MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_VULKAN) +#define MLN_UBO_CONSOLIDATION (MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_VULKAN || MLN_RENDER_BACKEND_WEBGPU) } // namespace shaders } // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/background.hpp b/include/mbgl/shaders/webgpu/background.hpp new file mode 100644 index 000000000..977648322 --- /dev/null +++ b/include/mbgl/shaders/webgpu/background.hpp @@ -0,0 +1,230 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "BackgroundShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, +}; + +struct BackgroundDrawableUBO { + matrix: mat4x4, +}; + +struct BackgroundDrawableUnionUBO { + matrix_col0: vec4, + matrix_col1: vec4, + matrix_col2: vec4, + matrix_col3: vec4, + extra0: vec4, + extra1: vec4, +}; + +struct BackgroundPropsUBO { + color: vec4, + opacity: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: BackgroundPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value]; + let matrix = mat4x4(drawable.matrix_col0, + drawable.matrix_col1, + drawable.matrix_col2, + drawable.matrix_col3); + let clip = matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + out.position = clip; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct BackgroundPropsUBO { + color: vec4, + opacity: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +@group(0) @binding(4) var props: BackgroundPropsUBO; + +@fragment +fn main() -> @location(0) vec4 { + return props.color * props.opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "BackgroundPatternShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) pos_a: vec2, + @location(1) pos_b: vec2, +}; + +struct BackgroundPatternDrawableUnionUBO { + matrix_col0: vec4, + matrix_col1: vec4, + matrix_col2: vec4, + matrix_col3: vec4, + pixel_coord_upper: vec2, + pixel_coord_lower: vec2, + tile_units_to_pixels: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +struct BackgroundPatternPropsUBO { + pattern_tl_a: vec2, + pattern_br_a: vec2, + pattern_tl_b: vec2, + pattern_br_b: vec2, + pattern_size_a: vec2, + pattern_size_b: vec2, + scale_a: f32, + scale_b: f32, + mix: f32, + opacity: f32, + pad1: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: BackgroundPatternPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + let matrix = mat4x4(drawable.matrix_col0, + drawable.matrix_col1, + drawable.matrix_col2, + drawable.matrix_col3); + + let pos = vec2(f32(in.position.x), f32(in.position.y)); + + // Use get_pattern_pos helper function for pattern positioning + out.pos_a = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + props.scale_a * props.pattern_size_a, + drawable.tile_units_to_pixels, + pos + ); + + out.pos_b = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + props.scale_b * props.pattern_size_b, + drawable.tile_units_to_pixels, + pos + ); + + let clip = matrix * vec4(pos, 0.0, 1.0); + out.position = clip; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) pos_a: vec2, + @location(1) pos_b: vec2, +}; + +struct BackgroundPatternPropsUBO { + pattern_tl_a: vec2, + pattern_br_a: vec2, + pattern_tl_b: vec2, + pattern_br_b: vec2, + pattern_size_a: vec2, + pattern_size_b: vec2, + scale_a: f32, + scale_b: f32, + mix: f32, + opacity: f32, + pad1: f32, + pad2: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + // other fields... +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(4) var props: BackgroundPatternPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var pattern_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let texsize = paintParams.pattern_atlas_texsize; + + // Sample pattern A + let imagecoord_a = gl_mod(in.pos_a, vec2(1.0)); + let pos_a = mix(props.pattern_tl_a / texsize, props.pattern_br_a / texsize, imagecoord_a); + let color_a = textureSample(pattern_texture, texture_sampler, pos_a); + + // Sample pattern B + let imagecoord_b = gl_mod(in.pos_b, vec2(1.0)); + let pos_b = mix(props.pattern_tl_b / texsize, props.pattern_br_b / texsize, imagecoord_b); + let color_b = textureSample(pattern_texture, texture_sampler, pos_b); + + // Mix patterns and apply opacity + return mix(color_a, color_b, props.mix) * props.opacity; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/circle.hpp b/include/mbgl/shaders/webgpu/circle.hpp new file mode 100644 index 000000000..8461dbcda --- /dev/null +++ b/include/mbgl/shaders/webgpu/circle.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "CircleShader"; + + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto prelude = R"( +struct CircleDrawableUBO { + matrix: mat4x4, + extrude_scale: vec2, + color_t: f32, + radius_t: f32, + blur_t: f32, + opacity_t: f32, + stroke_color_t: f32, + stroke_width_t: f32, + stroke_opacity_t: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +struct CircleEvaluatedPropsUBO { + color: vec4, + stroke_color: vec4, + radius: f32, + blur: f32, + opacity: f32, + stroke_width: f32, + stroke_opacity: f32, + scale_with_map: i32, + pitch_with_map: i32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: CircleEvaluatedPropsUBO; +)"; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(0) position: vec2, + @location(1) color: vec4, + @location(2) radius: vec2, + @location(3) blur: vec2, + @location(4) opacity: vec2, + @location(5) stroke_color: vec4, + @location(6) stroke_width: vec2, + @location(7) stroke_opacity: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) extrude: vec2, + @location(1) circle_data: vec4, + @location(2) color: vec4, + @location(3) stroke_color: vec4, + @location(4) stroke_data: vec2, +}; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + let scale_with_map = props.scale_with_map != 0; + let pitch_with_map = props.pitch_with_map != 0; + + let pos_f = vec2(f32(in.position.x), f32(in.position.y)); + let extrude = glMod2v(pos_f, vec2(2.0, 2.0)) * 2.0 - vec2(1.0, 1.0); + let scaled_extrude = extrude * drawable.extrude_scale; + let circle_center = floor(pos_f * 0.5); + + var color = props.color; +#ifndef HAS_UNIFORM_u_color + color = unpack_mix_color(in.color, drawable.color_t); +#endif + + var radius = props.radius; +#ifndef HAS_UNIFORM_u_radius + radius = unpack_mix_float(in.radius, drawable.radius_t); +#endif + + var blur = props.blur; +#ifndef HAS_UNIFORM_u_blur + blur = unpack_mix_float(in.blur, drawable.blur_t); +#endif + + var opacity = props.opacity; +#ifndef HAS_UNIFORM_u_opacity + opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + var stroke_color = props.stroke_color; +#ifndef HAS_UNIFORM_u_stroke_color + stroke_color = unpack_mix_color(in.stroke_color, drawable.stroke_color_t); +#endif + + var stroke_width = props.stroke_width; +#ifndef HAS_UNIFORM_u_stroke_width + stroke_width = unpack_mix_float(in.stroke_width, drawable.stroke_width_t); +#endif + + var stroke_opacity = props.stroke_opacity; +#ifndef HAS_UNIFORM_u_stroke_opacity + stroke_opacity = unpack_mix_float(in.stroke_opacity, drawable.stroke_opacity_t); +#endif + + let radius_with_stroke = radius + stroke_width; + + var position: vec4; + if (pitch_with_map) { + var corner_position = circle_center; + if (scale_with_map) { + corner_position += scaled_extrude * radius_with_stroke; + } else { + let projected_center = drawable.matrix * vec4(circle_center, 0.0, 1.0); + corner_position += scaled_extrude * radius_with_stroke * + (projected_center.w / paintParams.camera_to_center_distance); + } + position = drawable.matrix * vec4(corner_position, 0.0, 1.0); + } else { + position = drawable.matrix * vec4(circle_center, 0.0, 1.0); + var factor = position.w; + if (scale_with_map) { + factor = paintParams.camera_to_center_distance; + } + let delta = scaled_extrude * radius_with_stroke * factor; + position = vec4(position.x + delta.x, position.y + delta.y, position.z, position.w); + } + + let antialiasblur = 1.0 / max(paintParams.pixel_ratio * radius_with_stroke, 1e-6); + + out.position = position; + out.extrude = extrude; + out.circle_data = vec4(antialiasblur, radius, blur, opacity); + out.color = color; + out.stroke_color = stroke_color; + out.stroke_data = vec2(stroke_width, stroke_opacity); + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) extrude: vec2, + @location(1) circle_data: vec4, + @location(2) color: vec4, + @location(3) stroke_color: vec4, + @location(4) stroke_data: vec2, +}; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let extrude_length = length(in.extrude); + let antialiasblur = in.circle_data.x; + let radius = in.circle_data.y; + let blur = in.circle_data.z; + let opacity = in.circle_data.w; + let stroke_width = in.stroke_data.x; + let stroke_opacity = in.stroke_data.y; + let antialiased_blur = -max(blur, antialiasblur); + + let opacity_t = smoothstep(0.0, antialiased_blur, extrude_length - 1.0); + + var color_t: f32; + if (stroke_width < 0.01) { + color_t = 0.0; + } else { + color_t = smoothstep(antialiased_blur, 0.0, extrude_length - radius / (radius + stroke_width)); + } + + let final_color = mix(in.color * opacity, in.stroke_color * stroke_opacity, color_t); + return opacity_t * final_color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/clipping_mask.hpp b/include/mbgl/shaders/webgpu/clipping_mask.hpp new file mode 100644 index 000000000..4ba60a72d --- /dev/null +++ b/include/mbgl/shaders/webgpu/clipping_mask.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include +#include + +namespace mbgl { +namespace shaders { + +struct alignas(16) ClipUBO { + std::array matrix; + std::uint32_t stencil_ref; + float pad1; + float pad2; + float pad3; +}; +static_assert(sizeof(ClipUBO) == 5 * 16, "wrong size"); + +template <> +struct ShaderSource { + static constexpr const char* name = "ClippingMaskProgram"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(2) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, +}; + +struct ClipUBO { + matrix: mat4x4, + stencil_ref: u32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +@group(0) @binding(0) var clip: ClipUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let clip_pos = clip.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + out.position = vec4(clip_pos.x, clip_pos.y, clip_pos.z, clip_pos.w); + return out; +} +)"; + + static constexpr auto fragment = R"( +@fragment +fn main() -> @location(0) vec4 { + return vec4(0.0, 0.0, 0.0, 0.0); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/collision.hpp b/include/mbgl/shaders/webgpu/collision.hpp new file mode 100644 index 000000000..2ba6b790d --- /dev/null +++ b/include/mbgl/shaders/webgpu/collision.hpp @@ -0,0 +1,240 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "CollisionBoxShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, + @location(4) anchor_position: vec2, + @location(5) extrude: vec2, + @location(6) placed: vec2, + @location(7) shift: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) placed: f32, + @location(1) not_used: f32, +}; + +struct CollisionDrawableUBO { + matrix: mat4x4, +}; + +struct CollisionTilePropsUBO { + extrude_scale: vec2, + overscale_factor: f32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawable: CollisionDrawableUBO; +@group(0) @binding(4) var tile_props: CollisionTilePropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let projected_point = drawable.matrix * vec4(f32(in.anchor_position.x), f32(in.anchor_position.y), 0.0, 1.0); + let camera_to_anchor_distance = projected_point.w; + + let collision_perspective_ratio = clamp( + 0.5 + 0.5 * (paintParams.camera_to_center_distance / camera_to_anchor_distance), + 0.0, + 4.0); + + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + let extrude_shift = vec2(f32(in.extrude.x), f32(in.extrude.y)) + in.shift; + out.position.x += extrude_shift.x * tile_props.extrude_scale.x * out.position.w * collision_perspective_ratio; + out.position.y += extrude_shift.y * tile_props.extrude_scale.y * out.position.w * collision_perspective_ratio; + + out.placed = f32(in.placed.x); + out.not_used = f32(in.placed.y); + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) placed: f32, + @location(1) not_used: f32, +}; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let alpha = 0.5; + + // Red = collision, hide label + var color = vec4(1.0, 0.0, 0.0, 1.0) * alpha; + + // Blue = no collision, label is showing + if (in.placed > 0.5) { + color = vec4(0.0, 0.0, 1.0, 0.5) * alpha; + } + + if (in.not_used > 0.5) { + // This box not used, fade it out + color = color * 0.1; + } + + return color; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "CollisionCircleShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, + @location(4) anchor_position: vec2, + @location(5) extrude: vec2, + @location(6) placed: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) placed: f32, + @location(1) not_used: f32, + @location(2) radius: f32, + @location(3) extrude: vec2, + @location(4) extrude_scale: vec2, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct CollisionDrawableUBO { + matrix: mat4x4, +}; + +struct CollisionTilePropsUBO { + extrude_scale: vec2, + overscale_factor: f32, + pad1: f32, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawable: CollisionDrawableUBO; +@group(0) @binding(4) var tile_props: CollisionTilePropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + + + let projected_point = drawable.matrix * vec4(f32(in.anchor_position.x), f32(in.anchor_position.y), 0.0, 1.0); + let camera_to_anchor_distance = projected_point.w; + + let perspective = max(camera_to_anchor_distance, 1e-6); + let collision_perspective_ratio = clamp( + 0.5 + 0.5 * (paintParams.camera_to_center_distance / perspective), + 0.0, + 4.0); + + var position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + let padding_factor = 1.2; + let extrude_vec = vec2(f32(in.extrude.x), f32(in.extrude.y)); + position.x += extrude_vec.x * tile_props.extrude_scale.x * padding_factor * position.w * collision_perspective_ratio; + position.y += extrude_vec.y * tile_props.extrude_scale.y * padding_factor * position.w * collision_perspective_ratio; + + out.position = position; + + out.placed = f32(in.placed.x); + out.not_used = f32(in.placed.y); + out.radius = abs(f32(in.extrude.y)); + out.extrude = extrude_vec * padding_factor; + out.extrude_scale = tile_props.extrude_scale * paintParams.camera_to_center_distance * collision_perspective_ratio; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) placed: f32, + @location(1) not_used: f32, + @location(2) radius: f32, + @location(3) extrude: vec2, + @location(4) extrude_scale: vec2, +}; + +struct CollisionTilePropsUBO { + extrude_scale: vec2, + overscale_factor: f32, + pad1: f32, +}; + +@group(0) @binding(4) var tile_props: CollisionTilePropsUBO; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let alpha = 0.5; + + // Red = collision, hide label + var color = vec4(1.0, 0.0, 0.0, 1.0) * alpha; + + // Blue = no collision, label is showing + if (in.placed > 0.5) { + color = vec4(0.0, 0.0, 1.0, 0.5) * alpha; + } + + if (in.not_used > 0.5) { + color = color * 0.2; + } + + // Add circle outline effect + let extrude_scale_length = max(length(in.extrude_scale), 1e-6); + let extrude_length = length(in.extrude) * extrude_scale_length; + let overscale = max(tile_props.overscale_factor, 1.0); + let stroke_width = 15.0 * extrude_scale_length / overscale; + let radius = in.radius * extrude_scale_length; + let distance_to_edge = abs(extrude_length - radius); + let opacity_t = smoothstep(-stroke_width, 0.0, -distance_to_edge); + + return opacity_t * color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/common.hpp b/include/mbgl/shaders/webgpu/common.hpp new file mode 100644 index 000000000..4f1843642 --- /dev/null +++ b/include/mbgl/shaders/webgpu/common.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace shaders { + +// The enum values are already defined in layer_ubo.hpp +// WebGPU uses the same indices as Metal + +template <> +struct ShaderSource { + static constexpr const char* name = "Prelude"; + + // Common WGSL code that can be included in other shaders + static constexpr auto prelude = R"( +// Constants +const PI: f32 = 3.14159265358979323846; +const LINE_NORMAL_SCALE: f32 = 1.0 / 63.0; // 1.0 / (127 / 2) +const MAX_LINE_DISTANCE: f32 = 32767.0; +const SDF_PX: f32 = 8.0; + +// Helper functions for unpacking data +fn unpack_float(packedValue: f32) -> vec2 { + let packedIntValue = i32(packedValue); + let v0 = packedIntValue / 256; + return vec2(f32(v0), f32(packedIntValue - v0 * 256)); +} + +fn unpack_opacity(packedOpacity: f32) -> vec2 { + let intOpacity = i32(packedOpacity) / 2; + return vec2(f32(intOpacity) / 127.0, packedOpacity % 2.0); +} + +// Decode a color that has been packed into two floats +fn decode_color(encoded: vec2) -> vec4 { + let e0 = unpack_float(encoded[0]) / 255.0; + let e1 = unpack_float(encoded[1]) / 255.0; + return vec4(e0.x, e0.y, e1.x, e1.y); +} + +// Unpack and interpolate between two float values +fn unpack_mix_float(packedValue: vec2, t: f32) -> f32 { + return mix(packedValue[0], packedValue[1], t); +} + +// Unpack and interpolate between two colors +fn unpack_mix_color(packedColors: vec4, t: f32) -> vec4 { + let minColor = decode_color(vec2(packedColors[0], packedColors[1])); + let maxColor = decode_color(vec2(packedColors[2], packedColors[3])); + return mix(minColor, maxColor, t); +} + +// Get pattern position for texture atlas lookups +fn get_pattern_pos(pixel_coord_upper: vec2, pixel_coord_lower: vec2, + pattern_size: vec2, tile_units_to_pixels: f32, pos: vec2) -> vec2 { + let offset_a = (pixel_coord_upper % pattern_size) * 256.0; + let offset_b = (offset_a % pattern_size) * 256.0 + pixel_coord_lower; + let offset = offset_b % pattern_size; + return (tile_units_to_pixels * pos + offset) / pattern_size; +} + +// Radians conversion +fn radians(degrees: f32) -> f32 { + return PI * degrees / 180.0; +} + +// WebGPU equivalent of GLSL mod function +fn glMod(x: f32, y: f32) -> f32 { + return x - y * floor(x / y); +} + +fn glMod2(x: vec2, y: f32) -> vec2 { + return x - y * floor(x / y); +} + +fn glMod2v(x: vec2, y: vec2) -> vec2 { + return x - y * floor(x / y); +} + +// GLSL compatibility helpers +fn gl_mod(x: vec2, y: vec2) -> vec2 { + return glMod2v(x, y); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/custom_geometry.hpp b/include/mbgl/shaders/webgpu/custom_geometry.hpp new file mode 100644 index 000000000..6752dd132 --- /dev/null +++ b/include/mbgl/shaders/webgpu/custom_geometry.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "CustomGeometryShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec3, + @location(4) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +struct CustomDrawableUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var drawable: CustomDrawableUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.uv = in.texcoord; + out.position = drawable.matrix * vec4(in.position, 1.0); + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) uv: vec2, +}; + +struct CustomDrawableUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var drawable: CustomDrawableUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var texture_image: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + return textureSample(texture_image, texture_sampler, in.uv) * drawable.color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/custom_symbol_icon.hpp b/include/mbgl/shaders/webgpu/custom_symbol_icon.hpp new file mode 100644 index 000000000..d8656ccb0 --- /dev/null +++ b/include/mbgl/shaders/webgpu/custom_symbol_icon.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "CustomSymbolIconShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(4) position: vec3, + @location(5) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +struct SymbolUBO { + matrix: mat4x4, + opacity: f32, +}; + +@group(0) @binding(0) var symbol_ubo: SymbolUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = symbol_ubo.matrix * vec4(in.position, 1.0); + out.uv = in.texcoord; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) uv: vec2, +}; + +struct SymbolUBO { + matrix: mat4x4, + opacity: f32, +}; + +@group(0) @binding(0) var symbol_ubo: SymbolUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var texture_image: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + var color = textureSample(texture_image, texture_sampler, in.uv); + color.a = color.a * symbol_ubo.opacity; + return color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/debug.hpp b/include/mbgl/shaders/webgpu/debug.hpp new file mode 100644 index 000000000..77c0b6596 --- /dev/null +++ b/include/mbgl/shaders/webgpu/debug.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "DebugShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +struct DebugUBO { + matrix: mat4x4, + color: vec4, + overlay_scale: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +@group(0) @binding(0) var debug: DebugUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let scaled_pos = vec2(f32(in.position.x), f32(in.position.y)) * debug.overlay_scale; + out.position = debug.matrix * vec4(scaled_pos, 0.0, 1.0); + + // This vertex shader expects a EXTENT x EXTENT quad, + // The UV co-ordinates for the overlay texture can be calculated using that knowledge + out.uv = vec2(f32(in.position.x), f32(in.position.y)) / 8192.0; + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) uv: vec2, +}; + +struct DebugUBO { + matrix: mat4x4, + color: vec4, + overlay_scale: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +@group(0) @binding(0) var debug: DebugUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var overlay_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let overlay_color = textureSample(overlay_texture, texture_sampler, in.uv); + return mix(debug.color, overlay_color, overlay_color.a); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/fill.hpp b/include/mbgl/shaders/webgpu/fill.hpp new file mode 100644 index 000000000..428ae01ae --- /dev/null +++ b/include/mbgl/shaders/webgpu/fill.hpp @@ -0,0 +1,910 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "FillShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(0) position: vec2, +#ifndef HAS_UNIFORM_u_color + @location(1) color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(2) opacity: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) opacity: f32, +}; + +struct FillDrawableUBO { + matrix: mat4x4, + color_t: f32, + opacity_t: f32, + pad1: f32, + pad2: f32, +}; + +struct FillDrawableUnionUBO { + fill: FillDrawableUBO, + padding: vec4, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + // Transform position using the matrix + let drawable = drawableVector[globalIndex.value].fill; + let clip = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + out.position = clip; + + var color: vec4; +#ifdef HAS_UNIFORM_u_color + color = props.color; +#else + color = unpack_mix_color(in.color, drawable.color_t); +#endif + + var opacity: f32; +#ifdef HAS_UNIFORM_u_opacity + opacity = props.opacity; +#else + opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + out.color = color; + out.opacity = opacity; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; + +struct FragmentInput { + @location(0) color: vec4, + @location(1) opacity: f32, +}; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + var color: vec4; +#ifdef HAS_UNIFORM_u_color + color = props.color; +#else + color = in.color; +#endif + + var opacity: f32; +#ifdef HAS_UNIFORM_u_opacity + opacity = props.opacity; +#else + opacity = in.opacity; +#endif + + return color * opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "FillOutlineShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(0) position: vec2, +#ifndef HAS_UNIFORM_u_outline_color + @location(1) outline_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(2) opacity: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) opacity: f32, + @location(2) pos: vec2, +}; + +struct FillOutlineDrawableUBO { + matrix: mat4x4, + outline_color_t: f32, + opacity_t: f32, + pad1: f32, + pad2: f32, +}; + +struct FillOutlineDrawableUnionUBO { + fill: FillOutlineDrawableUBO, + padding: vec4, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct FillOutlineEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(5) var props: FillOutlineEvaluatedPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + // Transform position using the matrix + let drawable = drawableVector[globalIndex.value].fill; + let clip = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + let invW = 1.0 / clip.w; + let ndcXY = clip.xy * invW; + out.position = clip; + out.pos = (ndcXY + vec2(1.0)) * 0.5 * paintParams.world_size; + + var color: vec4; +#ifdef HAS_UNIFORM_u_outline_color + color = props.outline_color; +#else + color = unpack_mix_color(in.outline_color, drawable.outline_color_t); +#endif + + var opacity: f32; +#ifdef HAS_UNIFORM_u_opacity + opacity = props.opacity; +#else + opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + out.color = color; + out.opacity = opacity; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FillOutlineEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +@group(0) @binding(5) var props: FillOutlineEvaluatedPropsUBO; + +struct FragmentInput { + @location(0) color: vec4, + @location(1) opacity: f32, + @location(2) pos: vec2, +}; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + var color: vec4; +#ifdef HAS_UNIFORM_u_outline_color + color = props.outline_color; +#else + color = in.color; +#endif + + var opacity: f32; +#ifdef HAS_UNIFORM_u_opacity + opacity = props.opacity; +#else + opacity = in.opacity; +#endif + + return color * opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "FillPatternShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) position: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(5) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(6) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(7) opacity: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_pos_a: vec2, + @location(1) v_pos_b: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(2) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(3) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(4) opacity: f32, +#endif +}; + +struct FillPatternDrawableUBO { + matrix: mat4x4, + pixel_coord_upper: vec2, + pixel_coord_lower: vec2, + tile_ratio: f32, + pattern_from_t: f32, + pattern_to_t: f32, + opacity_t: f32, +}; + +struct FillPatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let index = globalIndex.value; + let drawable = drawableVector[index]; + let tileProps = tilePropsVector[index]; + + var pattern_from: vec4; +#ifdef HAS_UNIFORM_u_pattern_from + pattern_from = tileProps.pattern_from; +#else + pattern_from = vec4(in.pattern_from); +#endif + + var pattern_to: vec4; +#ifdef HAS_UNIFORM_u_pattern_to + pattern_to = tileProps.pattern_to; +#else + pattern_to = vec4(in.pattern_to); +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + let pixelRatio = paintParams.pixel_ratio; + let tileZoomRatio = drawable.tile_ratio; + let fromScale = props.from_scale; + let toScale = props.to_scale; + + let display_size_a = vec2( + (pattern_br_a.x - pattern_tl_a.x) / pixelRatio, + (pattern_br_a.y - pattern_tl_a.y) / pixelRatio + ); + let display_size_b = vec2( + (pattern_br_b.x - pattern_tl_b.x) / pixelRatio, + (pattern_br_b.y - pattern_tl_b.y) / pixelRatio + ); + + let pos = vec2(f32(in.position.x), f32(in.position.y)); + let clip = drawable.matrix * vec4(pos, 0.0, 1.0); + out.position = clip; + out.v_pos_a = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + fromScale * display_size_a, + tileZoomRatio, + pos + ); + out.v_pos_b = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + toScale * display_size_b, + tileZoomRatio, + pos + ); +#ifndef HAS_UNIFORM_u_pattern_from + out.pattern_from = pattern_from; +#endif +#ifndef HAS_UNIFORM_u_pattern_to + out.pattern_to = pattern_to; +#endif +#ifndef HAS_UNIFORM_u_opacity + out.opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_pos_a: vec2, + @location(1) v_pos_b: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(2) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(3) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(4) opacity: f32, +#endif +}; + +struct FillPatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var pattern_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; + + let pattern_from = +#ifdef HAS_UNIFORM_u_pattern_from + tileProps.pattern_from; +#else + in.pattern_from; +#endif + + let pattern_to = +#ifdef HAS_UNIFORM_u_pattern_to + tileProps.pattern_to; +#else + in.pattern_to; +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + // Sample pattern A + let imagecoord_a = gl_mod(in.v_pos_a, vec2(1.0)); + let pos_a = mix(pattern_tl_a / tileProps.texsize, pattern_br_a / tileProps.texsize, imagecoord_a); + let color_a = textureSample(pattern_texture, texture_sampler, pos_a); + + // Sample pattern B + let imagecoord_b = gl_mod(in.v_pos_b, vec2(1.0)); + let pos_b = mix(pattern_tl_b / tileProps.texsize, pattern_br_b / tileProps.texsize, imagecoord_b); + let color_b = textureSample(pattern_texture, texture_sampler, pos_b); + + // Mix patterns and apply opacity + let opacity = +#ifdef HAS_UNIFORM_u_opacity + props.opacity; +#else + in.opacity; +#endif + + return mix(color_a, color_b, props.fade) * opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "FillOutlinePatternShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) position: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(5) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(6) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(7) opacity: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_pos_a: vec2, + @location(1) v_pos_b: vec2, + @location(2) v_pos: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(3) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(4) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(5) opacity: f32, +#endif +}; + +struct FillOutlinePatternDrawableUBO { + matrix: mat4x4, + pixel_coord_upper: vec2, + pixel_coord_lower: vec2, + tile_ratio: f32, + pattern_from_t: f32, + pattern_to_t: f32, + opacity_t: f32, +}; + +struct FillOutlinePatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let index = globalIndex.value; + let drawable = drawableVector[index]; + let tileProps = tilePropsVector[index]; + + var pattern_from: vec4; +#ifdef HAS_UNIFORM_u_pattern_from + pattern_from = tileProps.pattern_from; +#else + pattern_from = vec4(in.pattern_from); +#endif + + var pattern_to: vec4; +#ifdef HAS_UNIFORM_u_pattern_to + pattern_to = tileProps.pattern_to; +#else + pattern_to = vec4(in.pattern_to); +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + let pixelRatio = paintParams.pixel_ratio; + let tileZoomRatio = drawable.tile_ratio; + let fromScale = props.from_scale; + let toScale = props.to_scale; + + let display_size_a = vec2( + (pattern_br_a.x - pattern_tl_a.x) / pixelRatio, + (pattern_br_a.y - pattern_tl_a.y) / pixelRatio + ); + let display_size_b = vec2( + (pattern_br_b.x - pattern_tl_b.x) / pixelRatio, + (pattern_br_b.y - pattern_tl_b.y) / pixelRatio + ); + + let pos = vec2(f32(in.position.x), f32(in.position.y)); + let clip = drawable.matrix * vec4(pos, 0.0, 1.0); + let invW = 1.0 / clip.w; + let ndcXY = clip.xy * invW; + + out.position = clip; + out.v_pos_a = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + fromScale * display_size_a, + tileZoomRatio, + pos + ); + out.v_pos_b = get_pattern_pos( + drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + toScale * display_size_b, + tileZoomRatio, + pos + ); + out.v_pos = (ndcXY + vec2(1.0, 1.0)) * 0.5 * paintParams.world_size; +#ifndef HAS_UNIFORM_u_pattern_from + out.pattern_from = pattern_from; +#endif +#ifndef HAS_UNIFORM_u_pattern_to + out.pattern_to = pattern_to; +#endif +#ifndef HAS_UNIFORM_u_opacity + out.opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_pos_a: vec2, + @location(1) v_pos_b: vec2, + @location(2) v_pos: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(3) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(4) pattern_to: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(5) opacity: f32, +#endif +}; + +struct FillOutlinePatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var pattern_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; + + let pattern_from = +#ifdef HAS_UNIFORM_u_pattern_from + tileProps.pattern_from; +#else + in.pattern_from; +#endif + + let pattern_to = +#ifdef HAS_UNIFORM_u_pattern_to + tileProps.pattern_to; +#else + in.pattern_to; +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + // Sample pattern A + let imagecoord_a = gl_mod(in.v_pos_a, vec2(1.0)); + let pos_a = mix(pattern_tl_a / tileProps.texsize, pattern_br_a / tileProps.texsize, imagecoord_a); + let color_a = textureSample(pattern_texture, texture_sampler, pos_a); + + // Sample pattern B + let imagecoord_b = gl_mod(in.v_pos_b, vec2(1.0)); + let pos_b = mix(pattern_tl_b / tileProps.texsize, pattern_br_b / tileProps.texsize, imagecoord_b); + let color_b = textureSample(pattern_texture, texture_sampler, pos_b); + + // Mix patterns and apply opacity + let opacity = +#ifdef HAS_UNIFORM_u_opacity + props.opacity; +#else + in.opacity; +#endif + + return mix(color_a, color_b, props.fade) * opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "FillOutlineTriangulatedShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +const SCALE: f32 = 0.015873016; + +struct VertexInput { + @location(0) pos_normal: vec2, + @location(1) data: vec4, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) normal: vec2, + @location(1) width: f32, + @location(2) gamma_scale: f32, +}; + +struct FillOutlineTriangulatedDrawableUnionUBO { + matrix_col0: vec4, + matrix_col1: vec4, + matrix_col2: vec4, + matrix_col3: vec4, + params: vec4, + padding: vec4, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + let matrix = mat4x4(drawable.matrix_col0, + drawable.matrix_col1, + drawable.matrix_col2, + drawable.matrix_col3); + let ratio = max(drawable.params.x, 1e-6); + + let pos_normal = vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)); + let pos = floor(pos_normal * 0.5); + var normal = pos_normal - pos * 2.0; + normal.y = normal.y * 2.0 - 1.0; + + let data = vec4(in.data); + let extrude = data.xy - vec2(128.0, 128.0); + + let half_width = 0.5; + let antialiasing = 0.5 / paintParams.pixel_ratio; + let outset = half_width + antialiasing; + + let dist = extrude * outset * SCALE; + let extrude_vec = dist / ratio; + + let projected_extrude = matrix * vec4(extrude_vec, 0.0, 0.0); + let base = matrix * vec4(pos, 0.0, 1.0); + let clip = base + projected_extrude; + out.position = clip; + + let extrude_length_without_perspective = length(dist); + let extrude_length_with_perspective = length((projected_extrude.xy / clip.w) * paintParams.units_to_pixels); + let gamma_denom = max(extrude_length_with_perspective, 1e-6); + + out.normal = normal; + out.width = outset; + out.gamma_scale = extrude_length_without_perspective / gamma_denom; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) normal: vec2, + @location(1) width: f32, + @location(2) gamma_scale: f32, +}; + +struct FillEvaluatedPropsUBO { + color: vec4, + outline_color: vec4, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(5) var props: FillEvaluatedPropsUBO; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let dist = length(in.normal) * in.width; + let blur2 = (1.0 / paintParams.pixel_ratio) * in.gamma_scale; + let denom = max(blur2, 1e-6); + let alpha = clamp(min(dist + blur2, in.width - dist) / denom, 0.0, 1.0); + return props.outline_color * (alpha * props.opacity); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/fill_extrusion.hpp b/include/mbgl/shaders/webgpu/fill_extrusion.hpp new file mode 100644 index 000000000..e3cddf5fc --- /dev/null +++ b/include/mbgl/shaders/webgpu/fill_extrusion.hpp @@ -0,0 +1,411 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "FillExtrusionShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, + @location(4) normal_ed: vec4, +#ifndef HAS_UNIFORM_u_color + @location(5) color: vec4, +#endif +#ifndef HAS_UNIFORM_u_base + @location(6) base: vec2, +#endif +#ifndef HAS_UNIFORM_u_height + @location(7) height: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, +}; + +struct FillExtrusionDrawableUBO { + matrix: mat4x4, + pixel_coord_upper: vec2, + pixel_coord_lower: vec2, + height_factor: f32, + tile_ratio: f32, + base_t: f32, + height_t: f32, + color_t: f32, + pattern_from_t: f32, + pattern_to_t: f32, + pad1: f32, +}; + +struct FillExtrusionPropsUBO { + color: vec4, + light_color_pad: vec4, + light_position_base: vec4, + height: f32, + light_intensity: f32, + vertical_gradient: f32, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(5) var props: FillExtrusionPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value]; + +#ifndef HAS_UNIFORM_u_base + let baseValue = max(unpack_mix_float(in.base, drawable.base_t), 0.0); +#else + let baseValue = max(props.light_position_base.w, 0.0); +#endif + +#ifndef HAS_UNIFORM_u_height + let heightValue = max(unpack_mix_float(in.height, drawable.height_t), 0.0); +#else + let heightValue = max(props.height, 0.0); +#endif + + let normal_i = vec3(f32(in.normal_ed.x), f32(in.normal_ed.y), f32(in.normal_ed.z)); + let t = glMod(normal_i.x, 2.0); + let z = select(baseValue, heightValue, t != 0.0); + + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), z, 1.0); + +#ifdef OVERDRAW_INSPECTOR + out.color = vec4(1.0, 1.0, 1.0, 1.0); + return out; +#endif + +#ifndef HAS_UNIFORM_u_color + var color = unpack_mix_color(in.color, drawable.color_t); +#else + var color = props.color; +#endif + color = color + min(vec4(0.03, 0.03, 0.03, 1.0), vec4(1.0)); + + let luminance = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); + let unitNormal = normal_i / 16384.0; + let directionalFraction = clamp(dot(unitNormal, props.light_position_base.xyz), 0.0, 1.0); + let minDirectional = 1.0 - props.light_intensity; + let maxDirectional = max(1.0 - luminance + props.light_intensity, 1.0); + var directional = mix(minDirectional, maxDirectional, directionalFraction); + + if (normal_i.y != 0.0) { + let gradientMin = mix(0.7, 0.98, 1.0 - props.light_intensity); + let factor = clamp((t + baseValue) * pow(heightValue / 150.0, 0.5), gradientMin, 1.0); + directional *= (1.0 - props.vertical_gradient) + props.vertical_gradient * factor; + } + + let lightColor = props.light_color_pad.xyz; + let minLight = mix(vec3(0.0), vec3(0.3), 1.0 - lightColor); + let lit = clamp(color.rgb * directional * lightColor, minLight, vec3(1.0)); + + var vcolor = vec4(0.0, 0.0, 0.0, 1.0); + vcolor += vec4(lit, 0.0); + + out.color = vcolor * props.opacity; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) color: vec4, +}; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + return in.color; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "FillExtrusionPatternShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(3) position: vec2, + @location(4) normal_ed: vec4, +#ifndef HAS_UNIFORM_u_base + @location(5) base: vec2, +#endif +#ifndef HAS_UNIFORM_u_height + @location(6) height: vec2, +#endif +#ifndef HAS_UNIFORM_u_pattern_from + @location(7) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(8) pattern_to: vec4, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) lighting: vec4, + @location(1) pos_a: vec2, + @location(2) pos_b: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(3) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(4) pattern_to: vec4, +#endif +}; + +struct FillExtrusionPatternDrawableUBO { + matrix: mat4x4, + pixel_coord_upper: vec2, + pixel_coord_lower: vec2, + height_factor: f32, + tile_ratio: f32, + base_t: f32, + height_t: f32, + pattern_from_t: f32, + pattern_to_t: f32, + pad1: f32, + pad2: f32, +}; + +struct FillExtrusionPatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillExtrusionPropsUBO { + color: vec4, + light_color_pad: vec4, + light_position_base: vec4, + height: f32, + light_intensity: f32, + vertical_gradient: f32, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, + pad2: f32, +}; + +struct GlobalPaintParamsUBO { + pixel_ratio: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillExtrusionPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let index = globalIndex.value; + let drawable = drawableVector[index]; + +#ifndef HAS_UNIFORM_u_base + let baseValue = max(unpack_mix_float(in.base, drawable.base_t), 0.0); +#else + let baseValue = max(props.light_position_base.w, 0.0); +#endif + +#ifndef HAS_UNIFORM_u_height + let heightValue = max(unpack_mix_float(in.height, drawable.height_t), 0.0); +#else + let heightValue = max(props.height, 0.0); +#endif + + let normal_i = vec3(f32(in.normal_ed.x), f32(in.normal_ed.y), f32(in.normal_ed.z)); + let edgedistance = f32(in.normal_ed.w); + let t = glMod(normal_i.x, 2.0); + let z = select(baseValue, heightValue, t != 0.0); + + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), z, 1.0); + + var patternPos: vec2; + if (normal_i.x == 1.0 && normal_i.y == 0.0 && normal_i.z == 16384.0) { + patternPos = vec2(f32(in.position.x), f32(in.position.y)); + } else { + patternPos = vec2(edgedistance, z * drawable.height_factor); + } + + let pixelRatio = paintParams.pixel_ratio; + let tileZoomRatio = drawable.tile_ratio; + let fromScale = props.from_scale; + let toScale = props.to_scale; + +#ifdef HAS_UNIFORM_u_pattern_from + let patternFrom = tilePropsVector[index].pattern_from; +#else + let patternFrom = vec4(in.pattern_from); + out.pattern_from = patternFrom; +#endif + +#ifdef HAS_UNIFORM_u_pattern_to + let patternTo = tilePropsVector[index].pattern_to; +#else + let patternTo = vec4(in.pattern_to); + out.pattern_to = patternTo; +#endif + + let pattern_tl_a = patternFrom.xy; + let pattern_br_a = patternFrom.zw; + let pattern_tl_b = patternTo.xy; + let pattern_br_b = patternTo.zw; + + let display_size_a = vec2((pattern_br_a.x - pattern_tl_a.x) / pixelRatio, + (pattern_br_a.y - pattern_tl_a.y) / pixelRatio); + let display_size_b = vec2((pattern_br_b.x - pattern_tl_b.x) / pixelRatio, + (pattern_br_b.y - pattern_tl_b.y) / pixelRatio); + + var lighting = vec4(0.0, 0.0, 0.0, 1.0); + var directional = clamp(dot(normal_i / 16383.0, props.light_position_base.xyz), 0.0, 1.0); + directional = mix(1.0 - props.light_intensity, max(0.5 + props.light_intensity, 1.0), directional); + + if (normal_i.y != 0.0) { + let gradientMin = mix(0.7, 0.98, 1.0 - props.light_intensity); + let factor = clamp((t + baseValue) * pow(heightValue / 150.0, 0.5), gradientMin, 1.0); + directional *= (1.0 - props.vertical_gradient) + props.vertical_gradient * factor; + } + + let lightColor = props.light_color_pad.xyz; + let lit = clamp(directional * lightColor, + mix(vec3(0.0), vec3(0.3), 1.0 - lightColor), + vec3(1.0)); + lighting = vec4(lighting.rgb + lit, lighting.a); + lighting *= props.opacity; + + out.lighting = lighting; + out.pos_a = get_pattern_pos(drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + fromScale * display_size_a, + tileZoomRatio, + patternPos); + out.pos_b = get_pattern_pos(drawable.pixel_coord_upper, + drawable.pixel_coord_lower, + toScale * display_size_b, + tileZoomRatio, + patternPos); + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) lighting: vec4, + @location(1) pos_a: vec2, + @location(2) pos_b: vec2, +#ifndef HAS_UNIFORM_u_pattern_from + @location(3) pattern_from: vec4, +#endif +#ifndef HAS_UNIFORM_u_pattern_to + @location(4) pattern_to: vec4, +#endif +}; + +struct FillExtrusionPatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + texsize: vec2, + pad1: f32, + pad2: f32, +}; + +struct FillExtrusionPropsUBO { + color: vec4, + light_color_pad: vec4, + light_position_base: vec4, + height: f32, + light_intensity: f32, + vertical_gradient: f32, + opacity: f32, + fade: f32, + from_scale: f32, + to_scale: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(4) var tilePropsVector: array; +@group(0) @binding(5) var props: FillExtrusionPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var pattern_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; + +#ifdef HAS_UNIFORM_u_pattern_from + let pattern_from = tileProps.pattern_from; +#else + let pattern_from = in.pattern_from; +#endif + +#ifdef HAS_UNIFORM_u_pattern_to + let pattern_to = tileProps.pattern_to; +#else + let pattern_to = in.pattern_to; +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + let imagecoord_a = gl_mod(in.pos_a, vec2(1.0)); + let pos_a = mix(pattern_tl_a / tileProps.texsize, pattern_br_a / tileProps.texsize, imagecoord_a); + let color_a = textureSample(pattern_texture, texture_sampler, pos_a); + + let imagecoord_b = gl_mod(in.pos_b, vec2(1.0)); + let pos_b = mix(pattern_tl_b / tileProps.texsize, pattern_br_b / tileProps.texsize, imagecoord_b); + let color_b = textureSample(pattern_texture, texture_sampler, pos_b); + + let pattern_color = mix(color_a, color_b, props.fade); + return pattern_color * in.lighting; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/heatmap.hpp b/include/mbgl/shaders/webgpu/heatmap.hpp new file mode 100644 index 000000000..8d095befe --- /dev/null +++ b/include/mbgl/shaders/webgpu/heatmap.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "HeatmapShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(0) position: vec2, +#ifndef HAS_UNIFORM_u_weight + @location(1) weight: vec2, +#endif +#ifndef HAS_UNIFORM_u_radius + @location(2) radius: vec2, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) weight: f32, + @location(1) extrude: vec2, +}; + +struct HeatmapDrawableUBO { + matrix: mat4x4, + extrude_scale: f32, + weight_t: f32, + radius_t: f32, + pad1: f32, +}; + +struct HeatmapEvaluatedPropsUBO { + weight: f32, + radius: f32, + intensity: f32, + padding: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad: vec3, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: HeatmapEvaluatedPropsUBO; + +const ZERO: f32 = 1.0 / 255.0 / 16.0; +const GAUSS_COEF: f32 = 0.3989422804014327; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + +#ifdef HAS_UNIFORM_u_weight + let weight = props.weight; +#else + let weight = unpack_mix_float(in.weight, drawable.weight_t); +#endif + +#ifdef HAS_UNIFORM_u_radius + let radius = props.radius; +#else + let radius = unpack_mix_float(in.radius, drawable.radius_t); +#endif + + let pos = vec2(f32(in.position.x), f32(in.position.y)); + let unscaled_extrude = fma(vec2(f32(in.position.x & 1), f32(in.position.y & 1)), vec2(2.0, 2.0), + vec2(-1.0, -1.0)); + + let S = sqrt(-2.0 * log(ZERO / (max(weight, ZERO) * max(props.intensity, ZERO) * GAUSS_COEF))) / 3.0; + let extrude = S * unscaled_extrude; + let scaled_extrude = extrude * radius * drawable.extrude_scale; + + let base = floor(pos * 0.5); + out.position = drawable.matrix * vec4(base + scaled_extrude, 0.0, 1.0); + out.weight = weight; + out.extrude = extrude; + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) weight: f32, + @location(1) extrude: vec2, +}; + +struct HeatmapEvaluatedPropsUBO { + weight: f32, + radius: f32, + intensity: f32, + padding: f32, +}; + +const GAUSS_COEF: f32 = 0.3989422804014327; + +@group(0) @binding(4) var props: HeatmapEvaluatedPropsUBO; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { +#ifdef OVERDRAW_INSPECTOR + return vec4(1.0, 1.0, 1.0, 1.0); +#else + let d = -0.5 * 3.0 * 3.0 * dot(in.extrude, in.extrude); + let val = in.weight * props.intensity * GAUSS_COEF * exp(d); + return vec4(val, 1.0, 1.0, 1.0); +#endif +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/heatmap_texture.hpp b/include/mbgl/shaders/webgpu/heatmap_texture.hpp new file mode 100644 index 000000000..b225718e3 --- /dev/null +++ b/include/mbgl/shaders/webgpu/heatmap_texture.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "HeatmapTextureShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex_coord: vec2, +}; + +struct HeatmapTextureUBO { + matrix: mat4x4, + opacity: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + // remaining fields unused +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(4) var ubo: HeatmapTextureUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let quad_pos = vec2(f32(in.position.x), f32(in.position.y)); + let scaled = quad_pos * paintParams.world_size; + + out.position = ubo.matrix * vec4(scaled, 0.0, 1.0); + out.tex_coord = quad_pos; + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex_coord: vec2, +}; + +struct HeatmapTextureUBO { + matrix: mat4x4, + opacity: f32, + pad1: f32, + pad2: f32, + pad3: f32, +}; + +@group(0) @binding(4) var ubo: HeatmapTextureUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var heatmap_texture: texture_2d; +@group(1) @binding(2) var color_ramp_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let t = textureSample(heatmap_texture, texture_sampler, in.tex_coord).r; + let color = textureSample(color_ramp_texture, texture_sampler, vec2(t, 0.5)); + return color * ubo.opacity; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/hillshade.hpp b/include/mbgl/shaders/webgpu/hillshade.hpp new file mode 100644 index 000000000..340dee529 --- /dev/null +++ b/include/mbgl/shaders/webgpu/hillshade.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "HillshadeShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, + @location(6) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex_coord: vec2, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct HillshadeDrawableUBO { + matrix: mat4x4, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value]; + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + + var tex = vec2(f32(in.texcoord.x), f32(in.texcoord.y)) / 8192.0; + tex.y = 1.0 - tex.y; + out.tex_coord = tex; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex_coord: vec2, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct HillshadeTilePropsUBO { + latrange: vec2, + light: vec2, +}; + +struct HillshadeEvaluatedPropsUBO { + highlight: vec4, + shadow: vec4, + accent: vec4, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(4) var props: HillshadeEvaluatedPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var hillshade_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { +#ifdef OVERDRAW_INSPECTOR + return vec4(1.0, 1.0, 1.0, 1.0); +#endif + + let tileProps = tilePropsVector[globalIndex.value]; + let pixel = textureSample(hillshade_texture, texture_sampler, in.tex_coord); + + let deriv = pixel.rg * 2.0 - vec2(1.0, 1.0); + + let latRange = tileProps.latrange; + let latitude = (latRange.x - latRange.y) * in.tex_coord.y + latRange.y; + let scaleFactor = cos(radians(latitude)); + let slope = atan(1.25 * length(deriv) / scaleFactor); + let aspectDefault = 0.5 * PI * select(-1.0, 1.0, deriv.y > 0.0); + let aspect = select(aspectDefault, atan2(deriv.y, -deriv.x), deriv.x != 0.0); + + let intensity = tileProps.light.x; + let azimuth = tileProps.light.y + PI; + + let base = 1.875 - intensity * 1.75; + let maxValue = 0.5 * PI; + let denom = pow(base, maxValue) - 1.0; + let useLinear = abs(intensity - 0.5) < 1e-6; + let scaledSlope = select(((pow(base, slope) - 1.0) / denom) * maxValue, + slope, + useLinear); + + let accent = cos(scaledSlope); + let accentColor = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); + + let shade = abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); + let shadeColor = mix(props.shadow, props.highlight, shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); + + let color = accentColor * (1.0 - shadeColor.a) + shadeColor; + return color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/hillshade_prepare.hpp b/include/mbgl/shaders/webgpu/hillshade_prepare.hpp new file mode 100644 index 000000000..a9916b887 --- /dev/null +++ b/include/mbgl/shaders/webgpu/hillshade_prepare.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "HillshadePrepareShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, + @location(6) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex_coord: vec2, +}; + +struct HillshadePrepareDrawableUBO { + matrix: mat4x4, +}; + +struct HillshadePrepareTilePropsUBO { + unpack: vec4, + dimension: vec2, + zoom: f32, + maxzoom: f32, +}; + +@group(0) @binding(2) var drawable: HillshadePrepareDrawableUBO; +@group(0) @binding(4) var tileProps: HillshadePrepareTilePropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + + let epsilon = vec2(1.0, 1.0) / tileProps.dimension; + let scale = (tileProps.dimension.x - 2.0) / tileProps.dimension.x; + out.tex_coord = vec2(f32(in.texcoord.x), f32(in.texcoord.y)) / 8192.0 * scale + epsilon; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex_coord: vec2, +}; + +struct HillshadePrepareTilePropsUBO { + unpack: vec4, + dimension: vec2, + zoom: f32, + maxzoom: f32, +}; + +@group(0) @binding(4) var tileProps: HillshadePrepareTilePropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var dem_texture: texture_2d; + +fn getElevation(coord: vec2) -> f32 { + var data = textureSample(dem_texture, texture_sampler, coord) * 255.0; + data.a = -1.0; + return dot(data, tileProps.unpack) / 4.0; +} + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { +#ifdef OVERDRAW_INSPECTOR + return vec4(1.0, 1.0, 1.0, 1.0); +#endif + + let epsilon = vec2(1.0, 1.0) / tileProps.dimension; + + let a = getElevation(in.tex_coord + vec2(-epsilon.x, -epsilon.y)); + let b = getElevation(in.tex_coord + vec2(0.0, -epsilon.y)); + let c = getElevation(in.tex_coord + vec2(epsilon.x, -epsilon.y)); + let d = getElevation(in.tex_coord + vec2(-epsilon.x, 0.0)); + let f = getElevation(in.tex_coord + vec2(epsilon.x, 0.0)); + let g = getElevation(in.tex_coord + vec2(-epsilon.x, epsilon.y)); + let h = getElevation(in.tex_coord + vec2(0.0, epsilon.y)); + let i = getElevation(in.tex_coord + vec2(epsilon.x, epsilon.y)); + + var exaggeration = 0.3; + if (tileProps.zoom < 4.5) { + exaggeration = 0.35; + } + if (tileProps.zoom < 2.0) { + exaggeration = 0.4; + } + + let denom = pow(2.0, (tileProps.zoom - tileProps.maxzoom) * exaggeration + 19.2562 - tileProps.zoom); + let deriv = vec2((c + f + f + i) - (a + d + d + g), + (g + h + h + i) - (a + b + b + c)) / denom; + + let color = clamp(vec4(deriv.x / 2.0 + 0.5, + deriv.y / 2.0 + 0.5, + 1.0, + 1.0), + vec4(0.0, 0.0, 0.0, 0.0), + vec4(1.0, 1.0, 1.0, 1.0)); + return color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/line.hpp b/include/mbgl/shaders/webgpu/line.hpp new file mode 100644 index 000000000..dbb6115f9 --- /dev/null +++ b/include/mbgl/shaders/webgpu/line.hpp @@ -0,0 +1,1027 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "LineShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) pos_normal: vec2, // packed position and normal + @location(5) data: vec4, // extrude, direction, linesofar + @location(6) color: vec4, + @location(7) blur: vec2, + @location(8) opacity: vec2, + @location(9) gapwidth: vec2, + @location(10) offset: vec2, + @location(11) width: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_color: vec4, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, +}; + +struct LineDrawableUBO { + matrix: mat4x4, + ratio: f32, + color_t: f32, + blur_t: f32, + opacity_t: f32, + gapwidth_t: f32, + offset_t: f32, + width_t: f32, + pad1: f32, +}; + +struct LineEvaluatedPropsUBO { + color: vec4, + blur: f32, + opacity: f32, + gapwidth: f32, + offset: f32, + width: f32, + floorwidth: f32, + expressionMask: u32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LineDrawableEntry { + data: LineDrawableUBO, + pad0: vec4, + pad1: vec4, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: LineEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value].data; + let ratio = max(drawable.ratio, 1e-6); + let pixel_ratio = paintParams.pixel_ratio; + let antialiasing = (1.0 / pixel_ratio) * 0.5; + + // Unpack vertex data + let a_extrude = vec2(f32(in.data.x), f32(in.data.y)) - vec2(128.0, 128.0); + let a_direction = f32(in.data.z & 3u) - 1.0; + let raw_pos = vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)); + let pos = floor(raw_pos * 0.5); + var normal = raw_pos - pos * 2.0; + normal.y = normal.y * 2.0 - 1.0; + + var color: vec4; + color = props.color; +#ifndef HAS_UNIFORM_u_color + color = unpack_mix_color(in.color, drawable.color_t); +#endif + + var blur: f32; + blur = props.blur; +#ifndef HAS_UNIFORM_u_blur + blur = unpack_mix_float(in.blur, drawable.blur_t); +#endif + + var opacity: f32; + opacity = props.opacity; +#ifndef HAS_UNIFORM_u_opacity + opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + var gapwidth: f32; + gapwidth = props.gapwidth; +#ifndef HAS_UNIFORM_u_gapwidth + gapwidth = unpack_mix_float(in.gapwidth, drawable.gapwidth_t); +#endif + gapwidth = gapwidth / 2.0; + + var offset: f32; + offset = props.offset; +#ifndef HAS_UNIFORM_u_offset + offset = unpack_mix_float(in.offset, drawable.offset_t); +#endif + offset = -offset; + + var width: f32; + width = props.width; +#ifndef HAS_UNIFORM_u_width + width = unpack_mix_float(in.width, drawable.width_t); +#endif + + let halfwidth = width * 0.5; + let inset = gapwidth + select(0.0, antialiasing, gapwidth > 0.0); + let outset = gapwidth + halfwidth * select(1.0, 2.0, gapwidth > 0.0) + + select(0.0, antialiasing, halfwidth != 0.0); + + // Scale the extrusion vector down to a normal and then up by the line width of this vertex + let dist = outset * a_extrude * LINE_NORMAL_SCALE; + + // Calculate the offset when drawing a line that is to the side of the actual line + let u = 0.5 * a_direction; + let t = 1.0 - abs(u); + let offset2 = offset * a_extrude * LINE_NORMAL_SCALE * normal.y * mat2x2(t, -u, u, t); + + let projected_extrude = drawable.matrix * vec4(dist / ratio, 0.0, 0.0); + let base = drawable.matrix * vec4(pos + offset2 / ratio, 0.0, 1.0); + let clip = base + projected_extrude; + + let inv_w = 1.0 / clip.w; + + out.position = clip; + + let extrude_length_without_perspective = length(dist); + let extrude_length_with_perspective = + length((projected_extrude.xy / clip.w) * paintParams.units_to_pixels); + let gamma_denom = max(extrude_length_with_perspective, 1e-6); + + out.v_width2 = vec2(outset, inset); + out.v_normal = normal; + out.v_gamma_scale = extrude_length_without_perspective / gamma_denom; + out.v_color = color; + out.v_blur = blur; + out.v_opacity = opacity; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_color: vec4, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + // Calculate the distance of the pixel from the line in pixels + let dist = length(in.v_normal) * in.v_width2.x; + + // Calculate the antialiasing fade factor + let antialiasing = 0.5 / paintParams.pixel_ratio; + let blur2 = (in.v_blur + (1.0 / paintParams.pixel_ratio)) * in.v_gamma_scale; + let denom = max(blur2, 1e-6); + let alpha = clamp(min(dist - (in.v_width2.y - blur2), in.v_width2.x - dist) / denom, 0.0, 1.0); + + return in.v_color * (alpha * in.v_opacity); +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "LineGradientShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) pos_normal: vec2, + @location(5) data: vec4, + @location(6) blur: vec2, + @location(7) opacity: vec2, + @location(8) gapwidth: vec2, + @location(9) offset: vec2, + @location(10) width: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_lineprogress: f32, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, +}; + +struct LineGradientDrawableUBO { + matrix: mat4x4, + ratio: f32, + blur_t: f32, + opacity_t: f32, + gapwidth_t: f32, + offset_t: f32, + width_t: f32, + pad1: f32, + pad2: f32, +}; + +const LINE_EXPRESSION_COLOR: u32 = 1u << 0u; +const LINE_EXPRESSION_OPACITY: u32 = 1u << 1u; +const LINE_EXPRESSION_BLUR: u32 = 1u << 2u; +const LINE_EXPRESSION_WIDTH: u32 = 1u << 3u; +const LINE_EXPRESSION_GAPWIDTH: u32 = 1u << 4u; +const LINE_EXPRESSION_FLOORWIDTH: u32 = 1u << 5u; +const LINE_EXPRESSION_OFFSET: u32 = 1u << 6u; + +struct LineEvaluatedPropsUBO { + color: vec4, + blur: f32, + opacity: f32, + gapwidth: f32, + offset: f32, + width: f32, + floorwidth: f32, + expressionMask: u32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LineGradientDrawableEntry { + data: LineGradientDrawableUBO, + pad0: vec4, + pad1: vec4, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: LineEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value].data; + let mask = props.expressionMask; + + // Constants + let pixel_ratio = max(paintParams.pixel_ratio, 1e-6); + let antialiasing = 0.5 / pixel_ratio; + let MAX_LINE_DISTANCE = 32767.0; + + // Unpack vertex data + let a_extrude = vec2(f32(in.data.x), f32(in.data.y)) - 128.0; + let a_direction = f32(in.data.z % 4u) - 1.0; + let v_lineprogress = (f32(in.data.z / 4u) + f32(in.data.w) * 64.0) * 2.0 / MAX_LINE_DISTANCE; + + // Unpack position and normal + let pos = floor(vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)) * 0.5); + let normal = vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)) - 2.0 * pos; + let v_normal = vec2(normal.x, normal.y * 2.0 - 1.0); + + // Unpack attributes using helper functions + var blur = unpack_mix_float(in.blur, drawable.blur_t); + if ((mask & LINE_EXPRESSION_BLUR) != 0u) { + blur = props.blur; + } + + var opacity = unpack_mix_float(in.opacity, drawable.opacity_t); + if ((mask & LINE_EXPRESSION_OPACITY) != 0u) { + opacity = props.opacity; + } + + var gapwidth = unpack_mix_float(in.gapwidth, drawable.gapwidth_t); + if ((mask & LINE_EXPRESSION_GAPWIDTH) != 0u) { + gapwidth = props.gapwidth; + } + gapwidth = gapwidth / 2.0; + + var offset = unpack_mix_float(in.offset, drawable.offset_t); + if ((mask & LINE_EXPRESSION_OFFSET) != 0u) { + offset = props.offset; + } + offset = -offset; + + var width = unpack_mix_float(in.width, drawable.width_t); + if ((mask & LINE_EXPRESSION_WIDTH) != 0u) { + width = props.width; + } + + let halfwidth = width / 2.0; + let inset = gapwidth + select(0.0, antialiasing, gapwidth > 0.0); + let outset = gapwidth + halfwidth * select(1.0, 2.0, gapwidth > 0.0) + + select(0.0, antialiasing, halfwidth != 0.0); + + // Scale the extrusion vector down to a normal and then up by the line width + let dist = outset * a_extrude * LINE_NORMAL_SCALE; + + // Calculate the offset + let u = 0.5 * a_direction; + let t = 1.0 - abs(u); + let offset2 = offset * a_extrude * LINE_NORMAL_SCALE * v_normal.y * mat2x2(t, -u, u, t); + + let projected_extrude = drawable.matrix * vec4(dist / drawable.ratio, 0.0, 0.0); + let position = drawable.matrix * vec4(pos + offset2 / drawable.ratio, 0.0, 1.0) + projected_extrude; + + // Calculate gamma scale + let extrude_length_without_perspective = length(dist); + let extrude_length_with_perspective = length(projected_extrude.xy / position.w * paintParams.units_to_pixels); + + out.position = position; + out.v_width2 = vec2(outset, inset); + out.v_normal = v_normal; + out.v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; + out.v_lineprogress = v_lineprogress; + out.v_blur = blur; + out.v_opacity = opacity; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_lineprogress: f32, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(1) @binding(0) var gradient_sampler: sampler; +@group(1) @binding(1) var gradient_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + // Calculate the distance of the pixel from the line + let dist = length(in.v_normal) * in.v_width2.x; + + // Calculate the antialiasing fade factor + let pixel_ratio = max(paintParams.pixel_ratio, 1e-6); + let blur2 = (in.v_blur + 1.0 / pixel_ratio) * in.v_gamma_scale; + let alpha = clamp(min(dist - (in.v_width2.y - blur2), in.v_width2.x - dist) / blur2, 0.0, 1.0); + + // Sample gradient texture + let color = textureSample(gradient_texture, gradient_sampler, vec2(in.v_lineprogress, 0.5)); + + return color * (alpha * in.v_opacity); +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "LinePatternShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) pos_normal: vec2, + @location(5) data: vec4, + @location(6) blur: vec2, + @location(7) opacity: vec2, + @location(8) gapwidth: vec2, + @location(9) offset: vec2, + @location(10) width: vec2, + @location(11) pattern_from: vec4, + @location(12) pattern_to: vec4, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_linesofar: f32, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, + @location(6) v_pattern_from: vec4, + @location(7) v_pattern_to: vec4, +}; + +struct LinePatternDrawableUBO { + matrix: mat4x4, + ratio: f32, + blur_t: f32, + opacity_t: f32, + gapwidth_t: f32, + offset_t: f32, + width_t: f32, + pattern_from_t: f32, + pattern_to_t: f32, +}; + +struct LinePatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + scale: vec4, + texsize: vec2, + fade: f32, + pad2: f32, +}; + +struct LineEvaluatedPropsUBO { + color: vec4, + blur: f32, + opacity: f32, + gapwidth: f32, + offset: f32, + width: f32, + floorwidth: f32, + expressionMask: u32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LinePatternDrawableEntry { + data: LinePatternDrawableUBO, + pad0: vec4, + pad1: vec4, +}; + +struct LinePatternTilePropsEntry { + data: LinePatternTilePropsUBO, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(4) var props: LineEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let index = globalIndex.value; + let drawable = drawableVector[index].data; + let tileProps = tilePropsVector[index].data; + + let ratio = max(drawable.ratio, 1e-6); + let antialiasing = 0.5 / paintParams.pixel_ratio; + let lineDistanceScale = 2.0; + + let a_extrude = vec2(f32(in.data.x), f32(in.data.y)) - 128.0; + let a_direction = f32(in.data.z & 3u) - 1.0; + let v_linesofar = (floor(f32(in.data.z) * 0.25) + f32(in.data.w) * 64.0) * lineDistanceScale; + + let raw_pos = vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)); + let pos = floor(raw_pos * 0.5); + let normal = raw_pos - 2.0 * pos; + let v_normal = vec2(normal.x, normal.y * 2.0 - 1.0); + + var blur = props.blur; +#ifndef HAS_UNIFORM_u_blur + blur = unpack_mix_float(in.blur, drawable.blur_t); +#endif + + var opacity = props.opacity; +#ifndef HAS_UNIFORM_u_opacity + opacity = unpack_mix_float(in.opacity, drawable.opacity_t); +#endif + + var gapwidth = props.gapwidth; +#ifndef HAS_UNIFORM_u_gapwidth + gapwidth = unpack_mix_float(in.gapwidth, drawable.gapwidth_t); +#endif + gapwidth = gapwidth * 0.5; + + var offset = props.offset; +#ifndef HAS_UNIFORM_u_offset + offset = unpack_mix_float(in.offset, drawable.offset_t); +#endif + offset = -offset; + + var width = props.width; +#ifndef HAS_UNIFORM_u_width + width = unpack_mix_float(in.width, drawable.width_t); +#endif + + var pattern_from = tileProps.pattern_from; +#ifndef HAS_UNIFORM_u_pattern_from + pattern_from = vec4(in.pattern_from); +#endif + var pattern_to = tileProps.pattern_to; +#ifndef HAS_UNIFORM_u_pattern_to + pattern_to = vec4(in.pattern_to); +#endif + + let halfwidth = width * 0.5; + let inset = gapwidth + select(0.0, antialiasing, gapwidth > 0.0); + let outset = gapwidth + halfwidth * select(1.0, 2.0, gapwidth > 0.0) + select(0.0, antialiasing, halfwidth != 0.0); + + let dist = outset * a_extrude * LINE_NORMAL_SCALE; + + let u = 0.5 * a_direction; + let t = 1.0 - abs(u); + let offset2 = offset * a_extrude * LINE_NORMAL_SCALE * v_normal.y * mat2x2(t, -u, u, t); + + let projected_extrude = drawable.matrix * vec4(dist / ratio, 0.0, 0.0); + let position = drawable.matrix * vec4(pos + offset2 / ratio, 0.0, 1.0) + projected_extrude; + + let extrude_length_without_perspective = length(dist); + let extrude_length_with_perspective = length(projected_extrude.xy / position.w * paintParams.units_to_pixels); + let gamma_scale = extrude_length_without_perspective / max(extrude_length_with_perspective, 1e-6); + + out.position = position; + out.v_width2 = vec2(outset, inset); + out.v_normal = v_normal; + out.v_gamma_scale = gamma_scale; + out.v_linesofar = v_linesofar; + out.v_blur = blur; + out.v_opacity = opacity; + out.v_pattern_from = pattern_from; + out.v_pattern_to = pattern_to; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_linesofar: f32, + @location(4) v_blur: f32, + @location(5) v_opacity: f32, + @location(6) v_pattern_from: vec4, + @location(7) v_pattern_to: vec4, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct LinePatternTilePropsUBO { + pattern_from: vec4, + pattern_to: vec4, + scale: vec4, + texsize: vec2, + fade: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LinePatternTilePropsEntry { + data: LinePatternTilePropsUBO, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(1) @binding(0) var pattern_sampler: sampler; +@group(1) @binding(1) var pattern_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value].data; + + var pattern_from = in.v_pattern_from; +#ifdef HAS_UNIFORM_u_pattern_from + pattern_from = tileProps.pattern_from; +#endif + var pattern_to = in.v_pattern_to; +#ifdef HAS_UNIFORM_u_pattern_to + pattern_to = tileProps.pattern_to; +#endif + + let pattern_tl_a = pattern_from.xy; + let pattern_br_a = pattern_from.zw; + let pattern_tl_b = pattern_to.xy; + let pattern_br_b = pattern_to.zw; + + let pixelRatio = tileProps.scale.x; + let tileZoomRatio = max(tileProps.scale.y, 1e-6); + let fromScale = tileProps.scale.z; + let toScale = tileProps.scale.w; + + let display_size_a = vec2((pattern_br_a.x - pattern_tl_a.x) / pixelRatio, + (pattern_br_a.y - pattern_tl_a.y) / pixelRatio); + let display_size_b = vec2((pattern_br_b.x - pattern_tl_b.x) / pixelRatio, + (pattern_br_b.y - pattern_tl_b.y) / pixelRatio); + + let pattern_size_a = vec2(display_size_a.x * fromScale / tileZoomRatio, display_size_a.y); + let pattern_size_b = vec2(display_size_b.x * toScale / tileZoomRatio, display_size_b.y); + + let dist = length(in.v_normal) * in.v_width2.x; + let blur = in.v_blur; + let opacity = in.v_opacity; + let invPixelRatio = 1.0 / max(paintParams.pixel_ratio, 1e-6); + let blur2 = (blur + invPixelRatio) * in.v_gamma_scale; + let alpha = clamp(min(dist - (in.v_width2.y - blur2), in.v_width2.x - dist) / blur2, 0.0, 1.0); + + let patternSizeAX = max(pattern_size_a.x, 1e-6); + let patternSizeBX = max(pattern_size_b.x, 1e-6); + let patternSizeAY = max(pattern_size_a.y, 1e-6); + let patternSizeBY = max(pattern_size_b.y, 1e-6); + + let x_a = glMod(in.v_linesofar / patternSizeAX, 1.0); + let x_b = glMod(in.v_linesofar / patternSizeBX, 1.0); + + let clampedA = clamp(in.v_width2.x, 0.0, (pattern_size_a.y + 2.0) * 0.5); + let clampedB = clamp(in.v_width2.x, 0.0, (pattern_size_b.y + 2.0) * 0.5); + + let y_a = clamp(0.5 + (in.v_normal.y * clampedA / patternSizeAY), 0.0, 1.0); + let y_b = clamp(0.5 + (in.v_normal.y * clampedB / patternSizeBY), 0.0, 1.0); + + let pos_a = mix(pattern_tl_a / tileProps.texsize, pattern_br_a / tileProps.texsize, vec2(x_a, y_a)); + let pos_b = mix(pattern_tl_b / tileProps.texsize, pattern_br_b / tileProps.texsize, vec2(x_b, y_b)); + + let color_a = textureSample(pattern_texture, pattern_sampler, pos_a); + let color_b = textureSample(pattern_texture, pattern_sampler, pos_b); + + let pattern_color = mix(color_a, color_b, tileProps.fade); + + return pattern_color * (alpha * opacity); +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "LineSDFShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr const char* vertex = R"( +struct VertexInput { + @location(4) pos_normal: vec2, + @location(5) data: vec4, + @location(6) color: vec4, + @location(7) blur: vec2, + @location(8) opacity: vec2, + @location(9) gapwidth: vec2, + @location(10) offset: vec2, + @location(11) width: vec2, + @location(12) floorwidth: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_tex_a: vec2, + @location(4) v_tex_b: vec2, + @location(5) v_color: vec4, + @location(6) v_blur: f32, + @location(7) v_opacity: f32, + @location(8) v_floorwidth: f32, +}; + +struct LineSDFDrawableUBO { + matrix: mat4x4, + patternscale_a: vec2, + patternscale_b: vec2, + tex_y_a: f32, + tex_y_b: f32, + ratio: f32, + color_t: f32, + blur_t: f32, + opacity_t: f32, + gapwidth_t: f32, + offset_t: f32, + width_t: f32, + floorwidth_t: f32, + pad1: f32, + pad2: f32, +}; + +const LINE_EXPRESSION_COLOR: u32 = 1u << 0u; +const LINE_EXPRESSION_OPACITY: u32 = 1u << 1u; +const LINE_EXPRESSION_BLUR: u32 = 1u << 2u; +const LINE_EXPRESSION_WIDTH: u32 = 1u << 3u; +const LINE_EXPRESSION_GAPWIDTH: u32 = 1u << 4u; +const LINE_EXPRESSION_FLOORWIDTH: u32 = 1u << 5u; +const LINE_EXPRESSION_OFFSET: u32 = 1u << 6u; + +struct LineSDFTilePropsUBO { + sdfgamma: f32, + mix: f32, + pad1: f32, + pad2: f32, +}; + +struct LineEvaluatedPropsUBO { + color: vec4, + blur: f32, + opacity: f32, + gapwidth: f32, + offset: f32, + width: f32, + floorwidth: f32, + expressionMask: u32, + pad1: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LineSDFDrawableEntry { + data: LineSDFDrawableUBO, +}; + +struct LineSDFTilePropsEntry { + data: LineSDFTilePropsUBO, + pad0: vec4, + pad1: vec4, + pad2: vec4, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(4) var props: LineEvaluatedPropsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let index = globalIndex.value; + let drawable = drawableVector[index].data; + let tileProps = tilePropsVector[index].data; + let mask = props.expressionMask; + + // Constants + let LINE_DISTANCE_SCALE = 2.0; + + // Unpack vertex data + let a_extrude = vec2(f32(in.data.x), f32(in.data.y)) - 128.0; + let a_direction = f32(in.data.z % 4u) - 1.0; + let v_linesofar = (floor(f32(in.data.z) * 0.25) + f32(in.data.w) * 64.0) * LINE_DISTANCE_SCALE; + + // Unpack position and normal + let pos = floor(vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)) * 0.5); + let normal = vec2(f32(in.pos_normal.x), f32(in.pos_normal.y)) - 2.0 * pos; + let v_normal = vec2(normal.x, normal.y * 2.0 - 1.0); + + // Unpack attributes using helper functions + var color = unpack_mix_color(in.color, drawable.color_t); + if ((mask & LINE_EXPRESSION_COLOR) != 0u) { + color = props.color; + } + + var blur = unpack_mix_float(in.blur, drawable.blur_t); + if ((mask & LINE_EXPRESSION_BLUR) != 0u) { + blur = props.blur; + } + + var opacity = unpack_mix_float(in.opacity, drawable.opacity_t); + if ((mask & LINE_EXPRESSION_OPACITY) != 0u) { + opacity = props.opacity; + } + + var gapwidth = unpack_mix_float(in.gapwidth, drawable.gapwidth_t); + if ((mask & LINE_EXPRESSION_GAPWIDTH) != 0u) { + gapwidth = props.gapwidth; + } + gapwidth = gapwidth / 2.0; + + var offset = unpack_mix_float(in.offset, drawable.offset_t); + if ((mask & LINE_EXPRESSION_OFFSET) != 0u) { + offset = props.offset; + } + offset = -offset; + + var width = unpack_mix_float(in.width, drawable.width_t); + if ((mask & LINE_EXPRESSION_WIDTH) != 0u) { + width = props.width; + } + + var floorwidth = unpack_mix_float(in.floorwidth, drawable.floorwidth_t); + if ((mask & LINE_EXPRESSION_FLOORWIDTH) != 0u) { + floorwidth = props.floorwidth; + } + + let pixel_ratio = max(paintParams.pixel_ratio, 1e-6); + let antialiasing = 0.5 / pixel_ratio; + + let halfwidth = width / 2.0; + let inset = gapwidth + select(0.0, antialiasing, gapwidth > 0.0); + let outset = gapwidth + halfwidth * select(1.0, 2.0, gapwidth > 0.0) + + select(0.0, antialiasing, halfwidth != 0.0); + + // Scale the extrusion vector + let dist = outset * a_extrude * LINE_NORMAL_SCALE; + + // Calculate the offset + let u = 0.5 * a_direction; + let t = 1.0 - abs(u); + let offset2 = offset * a_extrude * LINE_NORMAL_SCALE * v_normal.y * mat2x2(t, -u, u, t); + + let projected_extrude = drawable.matrix * vec4(dist / drawable.ratio, 0.0, 0.0); + let position = drawable.matrix * vec4(pos + offset2 / drawable.ratio, 0.0, 1.0) + projected_extrude; + + // Calculate gamma scale + let extrude_length_without_perspective = length(dist); + let extrude_length_with_perspective = length(projected_extrude.xy / position.w * paintParams.units_to_pixels); + + // Calculate texture coordinates + let tex_a = vec2( + v_linesofar * drawable.patternscale_a.x / floorwidth, + v_normal.y * drawable.patternscale_a.y + drawable.tex_y_a + ); + let tex_b = vec2( + v_linesofar * drawable.patternscale_b.x / floorwidth, + v_normal.y * drawable.patternscale_b.y + drawable.tex_y_b + ); + + out.position = position; + out.v_width2 = vec2(outset, inset); + out.v_normal = v_normal; + out.v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; + out.v_tex_a = tex_a; + out.v_tex_b = tex_b; + out.v_color = color; + out.v_blur = blur; + out.v_opacity = opacity; + out.v_floorwidth = floorwidth; + + return out; +} +)"; + + static constexpr const char* fragment = R"( +struct FragmentInput { + @location(0) v_width2: vec2, + @location(1) v_normal: vec2, + @location(2) v_gamma_scale: f32, + @location(3) v_tex_a: vec2, + @location(4) v_tex_b: vec2, + @location(5) v_color: vec4, + @location(6) v_blur: f32, + @location(7) v_opacity: f32, + @location(8) v_floorwidth: f32, +}; + +struct LineSDFTilePropsUBO { + sdfgamma: f32, + mix: f32, + pad1: f32, + pad2: f32, +}; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +struct LineSDFTilePropsEntry { + data: LineSDFTilePropsUBO, + pad0: vec4, + pad1: vec4, + pad2: vec4, +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(1) @binding(0) var sdf_sampler: sampler; +@group(1) @binding(1) var sdf_texture: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value].data; + + // Calculate the distance of the pixel from the line + let dist = length(in.v_normal) * in.v_width2.x; + + // Calculate the antialiasing fade factor + let pixel_ratio = max(paintParams.pixel_ratio, 1e-6); + let blur2 = (in.v_blur + 1.0 / pixel_ratio) * in.v_gamma_scale; + let alpha = clamp(min(dist - (in.v_width2.y - blur2), in.v_width2.x - dist) / blur2, 0.0, 1.0); + + // Sample SDF texture + let dist_a = textureSample(sdf_texture, sdf_sampler, in.v_tex_a).r; + let dist_b = textureSample(sdf_texture, sdf_sampler, in.v_tex_b).r; + let sdfdist = mix(dist_a, dist_b, tileProps.mix); + + // Calculate SDF alpha + let sdf_alpha = smoothstep(0.5 - tileProps.sdfgamma / in.v_floorwidth, 0.5 + tileProps.sdfgamma / in.v_floorwidth, sdfdist); + + return in.v_color * (alpha * in.v_opacity * sdf_alpha); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/location_indicator.hpp b/include/mbgl/shaders/webgpu/location_indicator.hpp new file mode 100644 index 000000000..09786ef3d --- /dev/null +++ b/include/mbgl/shaders/webgpu/location_indicator.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "LocationIndicatorShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, +}; + +struct LocationIndicatorUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var ubo: LocationIndicatorUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = ubo.matrix * vec4(in.position, 0.0, 1.0); + return out; +} +)"; + + static constexpr auto fragment = R"( +struct LocationIndicatorUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var ubo: LocationIndicatorUBO; + +@fragment +fn main(@builtin(position) position: vec4) -> @location(0) vec4 { + return ubo.color; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "LocationIndicatorTexturedShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec2, + @location(6) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +struct LocationIndicatorUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var ubo: LocationIndicatorUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = ubo.matrix * vec4(in.position, 0.0, 1.0); + out.uv = in.uv; + return out; +} +)"; + + static constexpr auto fragment = R"( +struct LocationIndicatorUBO { + matrix: mat4x4, + color: vec4, +}; + +@group(0) @binding(0) var ubo: LocationIndicatorUBO; +@group(0) @binding(1) var colorTexture: texture_2d; +@group(0) @binding(2) var colorSampler: sampler; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let textureColor = textureSample(colorTexture, colorSampler, uv); + return textureColor * ubo.color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/raster.hpp b/include/mbgl/shaders/webgpu/raster.hpp new file mode 100644 index 000000000..1e1d653d8 --- /dev/null +++ b/include/mbgl/shaders/webgpu/raster.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "RasterShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + + static constexpr auto vertex = R"( +struct VertexInput { + @location(0) position: vec2, + @location(1) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) pos0: vec2, + @location(1) pos1: vec2, +}; + +struct RasterDrawableUBO { + matrix: mat4x4, +}; + +struct RasterEvaluatedPropsUBO { + spin_weights: vec4, + tl_parent: vec2, + scale_parent: f32, + buffer_scale: f32, + fade_t: f32, + opacity: f32, + brightness_low: f32, + brightness_high: f32, + saturation_factor: f32, + contrast_factor: f32, + pad1: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: RasterEvaluatedPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let drawable = drawableVector[globalIndex.value]; + let clip = drawable.matrix * vec4(f32(in.position.x), f32(in.position.y), 0.0, 1.0); + out.position = clip; + + let tex = vec2(f32(in.texcoord.x), f32(in.texcoord.y)); + let pos0 = (((tex / 8192.0) - vec2(0.5, 0.5)) / props.buffer_scale) + vec2(0.5, 0.5); + out.pos0 = pos0; + out.pos1 = pos0 * props.scale_parent + props.tl_parent; + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) pos0: vec2, + @location(1) pos1: vec2, +}; + +struct RasterEvaluatedPropsUBO { + spin_weights: vec4, + tl_parent: vec2, + scale_parent: f32, + buffer_scale: f32, + fade_t: f32, + opacity: f32, + brightness_low: f32, + brightness_high: f32, + saturation_factor: f32, + contrast_factor: f32, + pad1: f32, + pad2: f32, +}; + +@group(0) @binding(4) var props: RasterEvaluatedPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var image0: texture_2d; +@group(1) @binding(2) var image1: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + var color0 = textureSample(image0, texture_sampler, in.pos0); + var color1 = textureSample(image1, texture_sampler, in.pos1); + + if (color0.a > 0.0) { + color0 = vec4(color0.rgb / color0.a, color0.a); + } + if (color1.a > 0.0) { + color1 = vec4(color1.rgb / color1.a, color1.a); + } + + var color = mix(color0, color1, props.fade_t); + color.a = color.a * props.opacity; + var rgb = color.rgb; + + let spin = props.spin_weights; + rgb = vec3(dot(rgb, spin.xyz), + dot(rgb, spin.zxy), + dot(rgb, spin.yzx)); + + let average = (color.r + color.g + color.b) / 3.0; + rgb = rgb + (average - rgb) * props.saturation_factor; + rgb = (rgb - vec3(0.5, 0.5, 0.5)) * props.contrast_factor + vec3(0.5, 0.5, 0.5); + + let high_vec = vec3(props.brightness_low, props.brightness_low, props.brightness_low); + let low_vec = vec3(props.brightness_high, props.brightness_high, props.brightness_high); + + let final_rgb = mix(high_vec, low_vec, rgb) * color.a; + return vec4(final_rgb, color.a); +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/shader_group.hpp b/include/mbgl/shaders/webgpu/shader_group.hpp new file mode 100644 index 000000000..1ac121871 --- /dev/null +++ b/include/mbgl/shaders/webgpu/shader_group.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class ShaderGroupBase : public gfx::ShaderGroup { +protected: + ShaderGroupBase(const ProgramParameters& parameters_) + : programParameters(parameters_) {} + + using DefinesMap = mbgl::unordered_map; + void addAdditionalDefines(const StringIDSetsPair& propertiesAsUniforms, DefinesMap& additionalDefines) { + additionalDefines.reserve(propertiesAsUniforms.first.size()); + for (const auto name : propertiesAsUniforms.first) { + // We expect the names to be prefixed by "a_", but we need just the base here. + const auto* base = (name[0] == 'a' && name[1] == '_') ? &name[2] : name.data(); + additionalDefines.insert(std::make_pair(std::string(uniformPrefix) + base, std::string())); + } + } + + ProgramParameters programParameters; + +private: + static constexpr auto uniformPrefix = "HAS_UNIFORM_u_"; +}; + +template +class ShaderGroup final : public ShaderGroupBase { +public: + ShaderGroup(const ProgramParameters& programParameters_) + : ShaderGroupBase(programParameters_) {} + ~ShaderGroup() noexcept override = default; + + gfx::ShaderPtr getOrCreateShader(gfx::Context& gfxContext, + const StringIDSetsPair& propertiesAsUniforms, + std::string_view /*firstAttribName*/) override { + using ShaderSource = shaders::ShaderSource; + constexpr auto& name = ShaderSource::name; + constexpr auto& vert = ShaderSource::vertex; + constexpr auto& frag = ShaderSource::fragment; + + std::size_t seed = 0; + mbgl::util::hash_combine(seed, propertyHash(propertiesAsUniforms)); + mbgl::util::hash_combine(seed, programParameters.getDefinesHash()); + const std::string shaderName = getShaderName(name, seed); + + auto shader = get(shaderName); + if (!shader) { + DefinesMap additionalDefines; + addAdditionalDefines(propertiesAsUniforms, additionalDefines); + + std::string vertexSource; + std::string fragmentSource; + + if constexpr (requires { ShaderSource::prelude; }) { + vertexSource = std::string(ShaderSource::prelude) + vert; + fragmentSource = std::string(ShaderSource::prelude) + frag; + } else { + vertexSource = vert; + fragmentSource = frag; + } + + std::unordered_map defineSet; + const auto& programDefines = programParameters.getDefines(); + for (const auto& entry : programDefines) { + defineSet.emplace(entry.first, true); + } + for (const auto& entry : additionalDefines) { + defineSet.emplace(entry.first, true); + } + + vertexSource = detail::preprocessWGSL(vertexSource, defineSet); + fragmentSource = detail::preprocessWGSL(fragmentSource, defineSet); + + auto& context = static_cast(gfxContext); + shader = std::make_shared(context, + vertexSource, + fragmentSource, + ShaderSource::attributes, + ShaderSource::textures, + std::string(name)); + + assert(shader); + if (!shader || !registerShader(shader, shaderName)) { + assert(false); + throw std::runtime_error("Failed to register " + shaderName + " with shader group!"); + } + } + + return shader; + } +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/shader_program.hpp b/include/mbgl/shaders/webgpu/shader_program.hpp new file mode 100644 index 000000000..839f3843c --- /dev/null +++ b/include/mbgl/shaders/webgpu/shader_program.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace shaders { +struct UniformBlockInfo { + constexpr UniformBlockInfo(bool vertex_, bool fragment_, std::size_t size_, std::size_t id_) + : index(id_), + vertex(vertex_), + fragment(fragment_), + size(size_), + id(id_) {} + + std::size_t index; + bool vertex; + bool fragment; + std::size_t size; + std::size_t id; +}; + +struct AttributeInfo { + constexpr AttributeInfo(std::size_t index_, gfx::AttributeDataType dataType_, std::size_t id_) + : index(index_), + dataType(dataType_), + id(id_) {} + + std::size_t index; + gfx::AttributeDataType dataType; + std::size_t id; +}; + +struct TextureInfo { + constexpr TextureInfo(std::size_t index_, std::size_t id_) + : index(index_), + id(id_) {} + + std::size_t index; + std::size_t id; +}; + +} // namespace shaders + +namespace webgpu { + +class Context; +class RendererBackend; + +class ShaderProgram final : public gfx::ShaderProgramBase { +public: + // Static name member required by is_shader_v trait + static constexpr std::string_view Name = "WebGPUShader"; + + enum class BindingType { + UniformBuffer, + ReadOnlyStorageBuffer, + StorageBuffer, + Sampler, + Texture + }; + + struct BindingInfo { + uint32_t group = 0; + uint32_t binding = 0; + BindingType type = BindingType::UniformBuffer; + WGPUShaderStage visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + }; + + ShaderProgram(std::string name, + RendererBackend& backend, + WGPUShaderModule vertexModule, + WGPUShaderModule fragmentModule); + + ShaderProgram(Context& context, const std::string& vertexSource, const std::string& fragmentSource); + + template + ShaderProgram(Context& context, + const std::string& vertexSource, + const std::string& fragmentSource, + const std::array& attrs, + const std::array& texts, + const std::string& name = "ShaderProgram") + : ShaderProgram(context, vertexSource, fragmentSource) { + if (name != "ShaderProgram") { + shaderName = name; + } + + for (const auto& attr : attrs) { + initAttribute(attr); + } + + for (const auto& tex : texts) { + initTexture(tex); + } + } + + ~ShaderProgram() override; + + WGPURenderPipeline getRenderPipeline(const gfx::Renderable& renderable, + const WGPUVertexBufferLayout* vertexLayouts, + uint32_t vertexLayoutCount, + const gfx::ColorMode& colorMode, + const gfx::DepthMode& depthMode, + const gfx::StencilMode& stencilMode, + gfx::DrawModeType drawModeType, + const std::optional reuseHash = std::nullopt); + + const std::vector& getBindingInfos() const { return bindingInfos; } + const std::vector& getBindingInfosForGroup(uint32_t group) const; + WGPUBindGroupLayout getBindGroupLayout(uint32_t group) const; + const std::vector& getBindGroupLayouts() const { return bindGroupLayouts; } + const std::vector& getBindGroupOrder() const { return bindGroupOrder; } + + const std::string_view typeName() const noexcept override { return Name; } + std::optional getSamplerLocation(const size_t id) const override; + const gfx::VertexAttributeArray& getVertexAttributes() const override { return vertexAttributes; } + const gfx::VertexAttributeArray& getInstanceAttributes() const override { return instanceAttributes; } + + void initAttribute(const shaders::AttributeInfo& info); + void initInstanceAttribute(const shaders::AttributeInfo& info); + void initTexture(const shaders::TextureInfo& info); + +protected: + void createPipelineLayout(const std::string& vertexSource, const std::string& fragmentSource); + WGPURenderPipeline createPipeline(const WGPUVertexBufferLayout* vertexLayouts, + uint32_t vertexLayoutCount, + const gfx::ColorMode& colorMode, + const gfx::DepthMode& depthMode, + const gfx::StencilMode& stencilMode, + gfx::DrawModeType drawModeType); + + void analyzeShaderBindings(const std::string& source, WGPUShaderStage stage); + void rebuildBindGroupLayouts(); + + std::string shaderName; + RendererBackend& backend; + + WGPUShaderModule vertexShaderModule = nullptr; + WGPUShaderModule fragmentShaderModule = nullptr; + std::vector bindGroupLayouts; + std::vector bindGroupOrder; + WGPUPipelineLayout pipelineLayout = nullptr; + + mutable std::unordered_map renderPipelineCache; + + gfx::VertexAttributeArray vertexAttributes; + gfx::VertexAttributeArray instanceAttributes; + std::array, shaders::maxTextureCountPerShader> textureBindings; + + std::vector bindingInfos; + std::vector> bindingsPerGroup; + + bool hasVertexEntryPoint = true; + bool hasFragmentEntryPoint = true; + bool loggedMissingEntryPoint = false; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/symbol.hpp b/include/mbgl/shaders/webgpu/symbol.hpp new file mode 100644 index 000000000..557f22b66 --- /dev/null +++ b/include/mbgl/shaders/webgpu/symbol.hpp @@ -0,0 +1,752 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +constexpr auto symbolShaderPrelude = R"( +// Symbol shader UBO indices +const idGlobalPaintParamsUBO = 0u; +const idGlobalUBOIndex = 1u; +const idSymbolDrawableUBO = 2u; +const idSymbolTilePropsUBO = 3u; +const idSymbolEvaluatedPropsUBO = 4u; +const symbolUBOCount = 5u; + +struct GlobalPaintParamsUBO { + pattern_atlas_texsize: vec2, + units_to_pixels: vec2, + world_size: vec2, + camera_to_center_distance: f32, + symbol_fade_change: f32, + aspect_ratio: f32, + pixel_ratio: f32, + map_zoom: f32, + pad1: f32, +}; + +struct SymbolDrawableUBO { + matrix: mat4x4, + label_plane_matrix: mat4x4, + coord_matrix: mat4x4, + + texsize: vec2, + texsize_icon: vec2, + + is_text_prop: u32, + rotate_symbol: u32, + pitch_with_map: u32, + is_size_zoom_constant: u32, + is_size_feature_constant: u32, + + size_t: f32, + size: f32, + + // Interpolations + fill_color_t: f32, + halo_color_t: f32, + opacity_t: f32, + halo_width_t: f32, + halo_blur_t: f32, +}; + +struct SymbolTilePropsUBO { + is_text: u32, + is_halo: u32, + gamma_scale: f32, + pad1: f32, +}; + +struct SymbolEvaluatedPropsUBO { + text_fill_color: vec4, + text_halo_color: vec4, + text_opacity: f32, + text_halo_width: f32, + text_halo_blur: f32, + pad1: f32, + icon_fill_color: vec4, + icon_halo_color: vec4, + icon_opacity: f32, + icon_halo_width: f32, + icon_halo_blur: f32, + pad2: f32, +}; + +struct GlobalIndexUBO { + value: u32, + pad0: vec3, +}; + +const c_offscreen_degenerate_triangle_location: f32 = -2.0; +)"; + +template <> +struct ShaderSource { + static constexpr const char* name = "SymbolIconShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr auto prelude = symbolShaderPrelude; + + static constexpr auto vertex = R"()" + R"( +struct VertexInput { + @location(3) pos_offset: vec4, + @location(4) data: vec4, + @location(5) pixeloffset: vec4, + @location(6) projected_pos: vec3, + @location(7) fade_opacity: f32, +#ifndef HAS_UNIFORM_u_opacity + @location(8) opacity: f32, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex: vec2, + @location(1) fade_opacity: f32, +#ifndef HAS_UNIFORM_u_opacity + @location(2) opacity: f32, +#endif +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + + let raw_fade_opacity = unpack_opacity(in.fade_opacity); + let fade_change = select(-paintParams.symbol_fade_change, paintParams.symbol_fade_change, raw_fade_opacity.y > 0.5); + let fade_opacity = max(0.0, min(1.0, raw_fade_opacity.x + fade_change)); + +#ifdef HAS_UNIFORM_u_opacity + let final_opacity = fade_opacity; +#else + let paint_opacity = unpack_mix_float(vec2(in.opacity, in.opacity), drawable.opacity_t); + let final_opacity = paint_opacity * fade_opacity; +#endif + + // Cull vertices with zero opacity + if (final_opacity == 0.0) { + out.position = vec4(c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, 1.0); + return out; + } + + let a_pos = vec2(f32(in.pos_offset.x), f32(in.pos_offset.y)); + let a_offset = vec2(f32(in.pos_offset.z), f32(in.pos_offset.w)); + + let a_tex = vec2(f32(in.data.x), f32(in.data.y)); + let a_size = vec2(f32(in.data.z), f32(in.data.w)); + + let a_size_min = floor(a_size.x * 0.5); + let a_pxoffset = vec2(f32(in.pixeloffset.x), f32(in.pixeloffset.y)); + let a_minFontScale = vec2(f32(in.pixeloffset.z), f32(in.pixeloffset.w)) / 256.0; + + let segment_angle = -in.projected_pos.z; + + let size_zoom_constant = drawable.is_size_zoom_constant != 0u; + let size_feature_constant = drawable.is_size_feature_constant != 0u; + var size: f32; + if (!size_zoom_constant && !size_feature_constant) { + size = mix(a_size_min, a_size.y, drawable.size_t) / 128.0; + } else if (size_zoom_constant && !size_feature_constant) { + size = a_size_min / 128.0; + } else { + size = drawable.size; + } + + let projectedPoint = drawable.matrix * vec4(a_pos, 0.0, 1.0); + let camera_to_anchor_distance = projectedPoint.w; + let pitch_with_map = drawable.pitch_with_map != 0u; + let distance_ratio = select( + paintParams.camera_to_center_distance / camera_to_anchor_distance, + camera_to_anchor_distance / paintParams.camera_to_center_distance, + pitch_with_map + ); + let perspective_ratio = clamp(0.5 + 0.5 * distance_ratio, 0.0, 4.0); + + size *= perspective_ratio; + + let is_text = drawable.is_text_prop != 0u; + let fontScale = select(size, size / 24.0, is_text); + + var symbol_rotation = 0.0; + if (drawable.rotate_symbol != 0u) { + let offsetProjectedPoint = drawable.matrix * vec4(a_pos + vec2(1.0, 0.0), 0.0, 1.0); + let a = projectedPoint.xy / projectedPoint.w; + let b = offsetProjectedPoint.xy / offsetProjectedPoint.w; + symbol_rotation = atan2((b.y - a.y) / paintParams.aspect_ratio, b.x - a.x); + } + + let angle_sin = sin(segment_angle + symbol_rotation); + let angle_cos = cos(segment_angle + symbol_rotation); + let rotation_matrix = mat2x2(angle_cos, -angle_sin, angle_sin, angle_cos); + + let projected_pos = drawable.label_plane_matrix * vec4(in.projected_pos.xy, 0.0, 1.0); + let pos0 = projected_pos.xy / projected_pos.w; + let posOffset = a_offset * max(a_minFontScale, vec2(fontScale)) / 32.0 + a_pxoffset / 16.0; + let position = drawable.coord_matrix * vec4(pos0 + rotation_matrix * posOffset, 0.0, 1.0); + + out.position = position; + out.tex = a_tex / drawable.texsize; + out.fade_opacity = fade_opacity; +#ifndef HAS_UNIFORM_u_opacity + out.opacity = final_opacity; +#endif + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex: vec2, + @location(1) fade_opacity: f32, +#ifndef HAS_UNIFORM_u_opacity + @location(2) opacity: f32, +#endif +}; + +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(4) var props: SymbolEvaluatedPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var image: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; +#ifdef HAS_UNIFORM_u_opacity + let base_opacity = select(props.icon_opacity, props.text_opacity, tileProps.is_text != 0u); + let opacity = base_opacity * in.fade_opacity; +#else + let opacity = select(props.icon_opacity, props.text_opacity, tileProps.is_text != 0u) * in.opacity; +#endif + return textureSample(image, texture_sampler, in.tex) * opacity; +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "SymbolSDFShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr auto prelude = symbolShaderPrelude; + + static constexpr auto vertex = R"()" + R"( +struct VertexInput { + @location(3) pos_offset: vec4, + @location(4) data: vec4, + @location(5) pixeloffset: vec4, + @location(6) projected_pos: vec3, + @location(7) fade_opacity: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(8) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(9) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(10) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(11) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(12) halo_blur: f32, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex: vec2, + @location(1) fade_opacity: f32, + @location(2) gamma_scale: f32, + @location(3) fontScale: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(4) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(5) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(6) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(7) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(8) halo_blur: f32, +#endif +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; +@group(0) @binding(4) var props: SymbolEvaluatedPropsUBO; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + + let fade_opacity = unpack_opacity(in.fade_opacity); + let fade_change = select(-paintParams.symbol_fade_change, paintParams.symbol_fade_change, fade_opacity.y > 0.5); + let fo = max(0.0, min(1.0, fade_opacity.x + fade_change)); + + // Cull vertices with zero opacity + if (fo == 0.0) { + out.position = vec4(c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, 1.0); + return out; + } + + let a_pos = vec2(f32(in.pos_offset.x), f32(in.pos_offset.y)); + let a_offset = vec2(f32(in.pos_offset.z), f32(in.pos_offset.w)); + + let a_tex = vec2(f32(in.data.x), f32(in.data.y)); + let a_size = vec2(f32(in.data.z), f32(in.data.w)); + + let a_size_min = floor(a_size.x * 0.5); + let a_pxoffset = vec2(f32(in.pixeloffset.x), f32(in.pixeloffset.y)); + + let segment_angle = -in.projected_pos.z; + + var size: f32; + let size_zoom_constant = drawable.is_size_zoom_constant != 0u; + let size_feature_constant = drawable.is_size_feature_constant != 0u; + if (!size_zoom_constant && !size_feature_constant) { + size = mix(a_size_min, a_size.y, drawable.size_t) / 128.0; + } else if (size_zoom_constant && !size_feature_constant) { + size = a_size_min / 128.0; + } else { + size = drawable.size; + } + + let projectedPoint = drawable.matrix * vec4(a_pos, 0.0, 1.0); + let camera_to_anchor_distance = projectedPoint.w; + let pitch_with_map = drawable.pitch_with_map != 0u; + let distance_ratio = select( + paintParams.camera_to_center_distance / camera_to_anchor_distance, + camera_to_anchor_distance / paintParams.camera_to_center_distance, + pitch_with_map + ); + let perspective_ratio = clamp(0.5 + 0.5 * distance_ratio, 0.0, 4.0); + + size *= perspective_ratio; + + let is_text = drawable.is_text_prop != 0u; + let fontScale = select(size, size / 24.0, is_text); + + var symbol_rotation = 0.0; + if (drawable.rotate_symbol != 0u) { + let offsetProjectedPoint = drawable.matrix * vec4(a_pos + vec2(1.0, 0.0), 0.0, 1.0); + let a = projectedPoint.xy / projectedPoint.w; + let b = offsetProjectedPoint.xy / offsetProjectedPoint.w; + symbol_rotation = atan2((b.y - a.y) / paintParams.aspect_ratio, b.x - a.x); + } + + let angle_sin = sin(segment_angle + symbol_rotation); + let angle_cos = cos(segment_angle + symbol_rotation); + let rotation_matrix = mat2x2(angle_cos, -angle_sin, angle_sin, angle_cos); + + let projected_pos = drawable.label_plane_matrix * vec4(in.projected_pos.xy, 0.0, 1.0); + let pos_rot = a_offset / 32.0 * fontScale + a_pxoffset; + let pos0 = projected_pos.xy / projected_pos.w + rotation_matrix * pos_rot; + let position = drawable.coord_matrix * vec4(pos0, 0.0, 1.0); + + out.position = position; + out.tex = a_tex / drawable.texsize; + out.gamma_scale = position.w; + out.fontScale = fontScale; + out.fade_opacity = fo; +#ifndef HAS_UNIFORM_u_fill_color + out.fill_color = unpack_mix_color(in.fill_color, drawable.fill_color_t); +#endif +#ifndef HAS_UNIFORM_u_halo_color + out.halo_color = unpack_mix_color(in.halo_color, drawable.halo_color_t); +#endif +#ifndef HAS_UNIFORM_u_halo_width + out.halo_width = unpack_mix_float(vec2(in.halo_width, in.halo_width), drawable.halo_width_t); +#endif +#ifndef HAS_UNIFORM_u_halo_blur + out.halo_blur = unpack_mix_float(vec2(in.halo_blur, in.halo_blur), drawable.halo_blur_t); +#endif +#ifndef HAS_UNIFORM_u_opacity + out.opacity = unpack_mix_float(vec2(in.opacity, in.opacity), drawable.opacity_t); +#endif + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex: vec2, + @location(1) fade_opacity: f32, + @location(2) gamma_scale: f32, + @location(3) fontScale: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(4) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(5) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(6) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(7) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(8) halo_blur: f32, +#endif +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(4) var props: SymbolEvaluatedPropsUBO; +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var glyph_atlas: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; +#ifndef HAS_UNIFORM_u_fill_color + let fill_color = in.fill_color; +#else + let fill_color = select(props.icon_fill_color, props.text_fill_color, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_color + let halo_color = in.halo_color; +#else + let halo_color = select(props.icon_halo_color, props.text_halo_color, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_opacity + let opacity = in.opacity; +#else + let opacity = select(props.icon_opacity, props.text_opacity, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_width + let halo_width = in.halo_width; +#else + let halo_width = select(props.icon_halo_width, props.text_halo_width, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_blur + let halo_blur = in.halo_blur; +#else + let halo_blur = select(props.icon_halo_blur, props.text_halo_blur, tileProps.is_text != 0u); +#endif + + let EDGE_GAMMA = 0.105 / paintParams.pixel_ratio; + let fontGamma = in.fontScale * tileProps.gamma_scale; + let color = select(fill_color, halo_color, tileProps.is_halo != 0u); + let gamma = (select(0.0, halo_blur * 1.19 / SDF_PX, tileProps.is_halo != 0u) + EDGE_GAMMA) / fontGamma; + let gamma_scaled = gamma * in.gamma_scale; + + // For halos, offset the inner edge outward to create the ring + var inner_edge = (256.0 - 64.0) / 256.0; + if (tileProps.is_halo != 0u) { + inner_edge = inner_edge + gamma_scaled; + } + + let sample = textureSample(glyph_atlas, texture_sampler, in.tex); + let dist = select(sample.a, sample.r, tileProps.is_text != 0u); + var alpha = smoothstep(inner_edge - gamma_scaled, inner_edge + gamma_scaled, dist); + + // When drawing halos, make the inside transparent so the fill can show through + if (tileProps.is_halo != 0u) { + let halo_edge = (6.0 - halo_width / in.fontScale) / SDF_PX; + alpha = min(smoothstep(halo_edge - gamma_scaled, halo_edge + gamma_scaled, dist), 1.0 - alpha); + } + + let coverage = alpha * opacity * in.fade_opacity; + let outAlpha = color.a * coverage; + + return vec4(color.rgb * coverage, outAlpha); +} +)"; +}; + +template <> +struct ShaderSource { + static constexpr const char* name = "SymbolTextAndIconShader"; + static const std::array attributes; + static constexpr std::array instanceAttributes{}; + static const std::array textures; + static constexpr auto prelude = symbolShaderPrelude; + + static constexpr auto vertex = R"()" + R"( +struct VertexInput { + @location(3) pos_offset: vec4, + @location(4) data: vec4, + @location(5) projected_pos: vec3, + @location(6) fade_opacity: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(7) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(8) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(9) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(10) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(11) halo_blur: f32, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex: vec2, + @location(1) fade_opacity: f32, + @location(2) gamma_scale: f32, + @location(3) fontScale: f32, + @location(4) is_icon: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(5) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(6) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(7) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(8) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(9) halo_blur: f32, +#endif +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(2) var drawableVector: array; + +const SDF = 1.0; +const ICON = 0.0; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let drawable = drawableVector[globalIndex.value]; + + let fade_opacity = unpack_opacity(in.fade_opacity); + let fade_change = select(-paintParams.symbol_fade_change, paintParams.symbol_fade_change, fade_opacity.y > 0.5); + let fo = max(0.0, min(1.0, fade_opacity.x + fade_change)); + + // Cull vertices with zero opacity + if (fo == 0.0) { + out.position = vec4(c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, + c_offscreen_degenerate_triangle_location, 1.0); + return out; + } + + let a_pos = vec2(f32(in.pos_offset.x), f32(in.pos_offset.y)); + let a_offset = vec2(f32(in.pos_offset.z), f32(in.pos_offset.w)); + + let a_tex = vec2(f32(in.data.x), f32(in.data.y)); + let a_size = vec2(f32(in.data.z), f32(in.data.w)); + + let a_size_min = floor(a_size.x * 0.5); + let is_sdf = a_size.x - 2.0 * a_size_min; + + let segment_angle = -in.projected_pos.z; + + let size_zoom_constant = drawable.is_size_zoom_constant != 0u; + let size_feature_constant = drawable.is_size_feature_constant != 0u; + var size: f32; + if (!size_zoom_constant && !size_feature_constant) { + size = mix(a_size_min, a_size.y, drawable.size_t) / 128.0; + } else if (size_zoom_constant && !size_feature_constant) { + size = a_size_min / 128.0; + } else { + size = drawable.size; + } + + let projectedPoint = drawable.matrix * vec4(a_pos, 0.0, 1.0); + let camera_to_anchor_distance = projectedPoint.w; + let pitch_with_map = drawable.pitch_with_map != 0u; + let distance_ratio = select( + paintParams.camera_to_center_distance / camera_to_anchor_distance, + camera_to_anchor_distance / paintParams.camera_to_center_distance, + pitch_with_map + ); + let perspective_ratio = clamp(0.5 + 0.5 * distance_ratio, 0.0, 4.0); + + size *= perspective_ratio; + + let fontScale = size / 24.0; + + var symbol_rotation = 0.0; + if (drawable.rotate_symbol != 0u) { + let offsetProjectedPoint = drawable.matrix * vec4(a_pos + vec2(1.0, 0.0), 0.0, 1.0); + let a = projectedPoint.xy / projectedPoint.w; + let b = offsetProjectedPoint.xy / offsetProjectedPoint.w; + symbol_rotation = atan2((b.y - a.y) / paintParams.aspect_ratio, b.x - a.x); + } + + let angle_sin = sin(segment_angle + symbol_rotation); + let angle_cos = cos(segment_angle + symbol_rotation); + let rotation_matrix = mat2x2(angle_cos, -angle_sin, angle_sin, angle_cos); + + let projected_pos = drawable.label_plane_matrix * vec4(in.projected_pos.xy, 0.0, 1.0); + let pos_rot = a_offset / 32.0 * fontScale; + let pos0 = projected_pos.xy / projected_pos.w + rotation_matrix * pos_rot; + let position = drawable.coord_matrix * vec4(pos0, 0.0, 1.0); + let gamma_scale = position.w; + let is_icon = select(1.0, 0.0, is_sdf == SDF); + + out.position = position; + + let inv_tex = 1.0 / select(drawable.texsize, drawable.texsize_icon, is_icon != 0.0); + out.tex = a_tex * inv_tex; + out.gamma_scale = gamma_scale; + out.fontScale = fontScale; + out.fade_opacity = fo; + out.is_icon = is_icon; + +#ifndef HAS_UNIFORM_u_fill_color + out.fill_color = unpack_mix_color(in.fill_color, drawable.fill_color_t); +#endif +#ifndef HAS_UNIFORM_u_halo_color + out.halo_color = unpack_mix_color(in.halo_color, drawable.halo_color_t); +#endif +#ifndef HAS_UNIFORM_u_opacity + out.opacity = unpack_mix_float(vec2(in.opacity, in.opacity), drawable.opacity_t); +#endif +#ifndef HAS_UNIFORM_u_halo_width + out.halo_width = unpack_mix_float(vec2(in.halo_width, in.halo_width), drawable.halo_width_t); +#endif +#ifndef HAS_UNIFORM_u_halo_blur + out.halo_blur = unpack_mix_float(vec2(in.halo_blur, in.halo_blur), drawable.halo_blur_t); +#endif + + return out; +} +)"; + + static constexpr auto fragment = R"( +struct FragmentInput { + @location(0) tex: vec2, + @location(1) fade_opacity: f32, + @location(2) gamma_scale: f32, + @location(3) fontScale: f32, + @location(4) is_icon: f32, +#ifndef HAS_UNIFORM_u_fill_color + @location(5) fill_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_halo_color + @location(6) halo_color: vec4, +#endif +#ifndef HAS_UNIFORM_u_opacity + @location(7) opacity: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_width + @location(8) halo_width: f32, +#endif +#ifndef HAS_UNIFORM_u_halo_blur + @location(9) halo_blur: f32, +#endif +}; + +@group(0) @binding(0) var paintParams: GlobalPaintParamsUBO; +@group(0) @binding(3) var tilePropsVector: array; +@group(0) @binding(1) var globalIndex: GlobalIndexUBO; +@group(0) @binding(4) var props: SymbolEvaluatedPropsUBO; +@group(1) @binding(0) var glyph_sampler: sampler; +@group(1) @binding(1) var glyph_image: texture_2d; +@group(1) @binding(2) var icon_sampler: sampler; +@group(1) @binding(3) var icon_image: texture_2d; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4 { + let tileProps = tilePropsVector[globalIndex.value]; +#ifndef HAS_UNIFORM_u_fill_color + let fill_color = in.fill_color; +#else + let fill_color = select(props.icon_fill_color, props.text_fill_color, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_color + let halo_color = in.halo_color; +#else + let halo_color = select(props.icon_halo_color, props.text_halo_color, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_opacity + let opacity = in.opacity; +#else + let opacity = select(props.icon_opacity, props.text_opacity, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_width + let halo_width = in.halo_width; +#else + let halo_width = select(props.icon_halo_width, props.text_halo_width, tileProps.is_text != 0u); +#endif +#ifndef HAS_UNIFORM_u_halo_blur + let halo_blur = in.halo_blur; +#else + let halo_blur = select(props.icon_halo_blur, props.text_halo_blur, tileProps.is_text != 0u); +#endif + + if (in.is_icon != 0.0) { + let alpha = opacity * in.fade_opacity; + let iconSample = textureSampleLevel(icon_image, icon_sampler, in.tex, 0.0); + return iconSample * alpha; + } + + let EDGE_GAMMA = 0.105 / paintParams.pixel_ratio; + let color = select(fill_color, halo_color, tileProps.is_halo != 0u); + let fontGamma = in.fontScale * tileProps.gamma_scale; + let gamma = (select(0.0, halo_blur * 1.19 / SDF_PX, tileProps.is_halo != 0u) + EDGE_GAMMA) / fontGamma; + let gamma_scaled = gamma * in.gamma_scale; + + // For halos, offset the inner edge outward to create the ring + var inner_edge = (256.0 - 64.0) / 256.0; + if (tileProps.is_halo != 0u) { + inner_edge = inner_edge + gamma_scaled; + } + + let sample = textureSampleLevel(glyph_image, glyph_sampler, in.tex, 0.0); + let dist = select(sample.a, sample.r, tileProps.is_text != 0u); + var alpha = smoothstep(inner_edge - gamma_scaled, inner_edge + gamma_scaled, dist); + + // When drawing halos, make the inside transparent so the fill can show through + if (tileProps.is_halo != 0u) { + let halo_edge = (6.0 - halo_width / in.fontScale) / SDF_PX; + alpha = min(smoothstep(halo_edge - gamma_scaled, halo_edge + gamma_scaled, dist), 1.0 - alpha); + } + + let coverage = alpha * opacity * in.fade_opacity; + let outAlpha = color.a * coverage; + + return vec4(color.rgb * coverage, outAlpha); + +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/wgsl_preprocessor.hpp b/include/mbgl/shaders/webgpu/wgsl_preprocessor.hpp new file mode 100644 index 000000000..aae915e1c --- /dev/null +++ b/include/mbgl/shaders/webgpu/wgsl_preprocessor.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace webgpu { +namespace detail { + +bool isDirective(const std::string& line, const std::string& directive); +std::string getDirectiveArgument(const std::string& line); +std::string preprocessWGSL(const std::string& source, const std::unordered_map& defines); + +} // namespace detail +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/shaders/webgpu/widevector.hpp b/include/mbgl/shaders/webgpu/widevector.hpp new file mode 100644 index 000000000..99a768ea7 --- /dev/null +++ b/include/mbgl/shaders/webgpu/widevector.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace shaders { + +template <> +struct ShaderSource { + static constexpr const char* name = "WideVectorShader"; + static const std::array attributes; + static const std::array instanceAttributes; + static const std::array textures; + + // Simplified wide vector shader for WebGPU + static constexpr auto vertex = R"( +struct VertexInput { + @location(5) position: vec3, + @location(6) normal: vec2, + @location(7) texCoord: vec2, +}; + +struct InstanceInput { + @location(8) instanceCenter: vec3, + @location(9) instanceColor: vec4, + @location(10) instancePrev: i32, + @location(11) instanceNext: i32, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) texCoord: vec2, +}; + +struct WideVectorUBO { + mvpMatrix: mat4x4, + mvpMatrixDiff: mat4x4, + mvMatrix: mat4x4, + mvMatrixDiff: mat4x4, + pMatrix: mat4x4, + pMatrixDiff: mat4x4, + frameSize: vec2, + color: vec4, + width: f32, + offset: f32, + edge: f32, + texRepeat: f32, + texOffset: vec2, +}; + +@group(0) @binding(0) var ubo: WideVectorUBO; + +@vertex +fn main(in: VertexInput, instance: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + // Calculate position with offset based on normal + let worldPos = instance.instanceCenter + vec3(in.normal * ubo.width, 0.0); + out.position = ubo.mvpMatrix * vec4(worldPos, 1.0); + + // Pass through color and texture coordinates + out.color = instance.instanceColor * ubo.color; + out.texCoord = in.texCoord * ubo.texRepeat + ubo.texOffset; + + return out; +} +)"; + + static constexpr auto fragment = R"( +@fragment +fn main(@location(0) color: vec4, @location(1) texCoord: vec2) -> @location(0) vec4 { + // Simple color output with alpha blending support + return color; +} +)"; +}; + +} // namespace shaders +} // namespace mbgl diff --git a/include/mbgl/webgpu/buffer_resource.hpp b/include/mbgl/webgpu/buffer_resource.hpp new file mode 100644 index 000000000..3281e3cde --- /dev/null +++ b/include/mbgl/webgpu/buffer_resource.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class Context; + +class BufferResource { +public: + BufferResource() noexcept = delete; + BufferResource(Context& context, + const void* data, + std::size_t size, + uint32_t usage, + bool isIndexBuffer = false, + bool persistent = false); + BufferResource(BufferResource&&) noexcept; + virtual ~BufferResource() noexcept; + + BufferResource& operator=(BufferResource&&) noexcept; + + BufferResource clone() const; + + void update(const void* data, std::size_t size, std::size_t offset) noexcept; + + std::size_t getSizeInBytes() const noexcept { return size; } + const void* contents() const noexcept { return raw.empty() ? nullptr : raw.data(); } + + Context& getContext() const noexcept { return context; } + WGPUBuffer getBuffer() const noexcept { return buffer; } + + bool isValid() const noexcept { return buffer || !raw.empty(); } + operator bool() const noexcept { return isValid(); } + bool operator!() const noexcept { return !isValid(); } + + using VersionType = std::uint16_t; + + /// Used to detect whether buffer contents have changed + VersionType getVersion() const noexcept { return version; } + + /// Indicates whether this buffer needs to be re-bound from a previous binding at the given version + bool needReBind(VersionType version) const noexcept; + + uint32_t getUsage() const { return usage; } + bool isPersistent() const { return persistent; } + +protected: + Context& context; + WGPUBuffer buffer = nullptr; + std::vector raw; + std::size_t size = 0; + uint32_t usage = 0; + std::uint16_t version = 0; + bool isIndexBuffer = false; + bool persistent = false; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/command_encoder.hpp b/include/mbgl/webgpu/command_encoder.hpp new file mode 100644 index 000000000..125bc0dbf --- /dev/null +++ b/include/mbgl/webgpu/command_encoder.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mbgl { +namespace gfx { +class Renderable; +} // namespace gfx + +namespace webgpu { + +class Context; +class RenderPass; +class UploadPass; + +class CommandEncoder final : public gfx::CommandEncoder { +public: + explicit CommandEncoder(Context& context_); + ~CommandEncoder() override; + + webgpu::Context& getContext() { return context; } + const webgpu::Context& getContext() const { return context; } + WGPUCommandEncoder getEncoder() const { return encoder; } + + std::unique_ptr createUploadPass(const char* name, gfx::Renderable&) override; + std::unique_ptr createRenderPass(const char* name, const gfx::RenderPassDescriptor&) override; + + void present(gfx::Renderable&) override; + + // WebGPU-specific: submit command buffer directly + void submitCommandBuffer(); + + template + void visitDebugGroups(TFunc f) { + for (const auto& item : debugGroupNames) { + f(item.name); + } + } + +private: + friend class RenderPass; + friend class UploadPass; + + // Track a RenderPass, propagating debug groups to it + void trackRenderPass(RenderPass*); + // Stop tracking a RenderPass + void forgetRenderPass(RenderPass*); + + // Track an UploadPass, propagating debug groups to it + void trackUploadPass(UploadPass*); + // Stop tracking a RenderPass + void forgetUploadPass(UploadPass*); + + void pushDebugGroup(const char* name) override; + void popDebugGroup() override; + +protected: + struct GroupEntry { + std::string name; + std::size_t renderGroups; + std::size_t uploadGroups; + }; + +protected: + webgpu::Context& context; + WGPUCommandEncoder encoder = nullptr; + std::vector debugGroupNames; + std::vector> renderDebugGroups; + std::vector> uploadDebugGroups; + mbgl::unordered_set renderPasses; + mbgl::unordered_set uploadPasses; + UploadPass* currentUploadPass = nullptr; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/context.hpp b/include/mbgl/webgpu/context.hpp new file mode 100644 index 000000000..6aa8853f9 --- /dev/null +++ b/include/mbgl/webgpu/context.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +class RenderStaticData; +namespace gfx { +// Forward declaration +class AttributeBinding; +using AttributeBindingArray = std::vector>; +class RenderPass; +class Renderable; +class ShaderProgramBase; +} // namespace gfx + +namespace shaders { +struct ClipUBO; +} // namespace shaders + +namespace webgpu { + +// Forward declaration +class VertexBufferResource; + +class Context : public gfx::Context { +public: + explicit Context(RendererBackend& backend); + ~Context() override; + + RendererBackend& getBackend() { return backend; } + const RendererBackend& getBackend() const { return backend; } + + // Frame management + void beginFrame() override; + void endFrame() override; + void performCleanup() override; + void reduceMemoryUsage() override; + + // Resource creation + std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool); + std::unique_ptr createOffscreenTexture(Size, gfx::TextureChannelDataType) override; + std::unique_ptr createCommandEncoder() override; + gfx::VertexAttributeArrayPtr createVertexAttributeArray() const override; + gfx::UniqueDrawableBuilder createDrawableBuilder(std::string name) override; + gfx::UniformBufferPtr createUniformBuffer(const void* data, + std::size_t size, + bool persistent = false, + bool ssbo = false) override; + gfx::UniqueUniformBufferArray createLayerUniformBufferArray() override; + gfx::ShaderProgramBasePtr getGenericShader(gfx::ShaderRegistry&, const std::string& name) override; + TileLayerGroupPtr createTileLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) override; + LayerGroupPtr createLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) override; + gfx::Texture2DPtr createTexture2D() override; + RenderTargetPtr createRenderTarget(const Size size, const gfx::TextureChannelDataType type) override; + + // State management + void resetState(gfx::DepthMode depthMode, gfx::ColorMode colorMode) override; + void setDirtyState() override; + void clearStencilBuffer(int32_t) override; + + // Uniform buffer management + bool emplaceOrUpdateUniformBuffer(gfx::UniformBufferPtr&, + const void* data, + std::size_t size, + bool persistent = false) override; + const gfx::UniformBufferArray& getGlobalUniformBuffers() const override; + gfx::UniformBufferArray& mutableGlobalUniformBuffers() override; + void bindGlobalUniformBuffers(gfx::RenderPass&) const noexcept override; + void unbindGlobalUniformBuffers(gfx::RenderPass&) const noexcept override; + +#if !defined(NDEBUG) + void visualizeStencilBuffer() override; + void visualizeDepthBuffer(float depthRangeSize) override; +#endif + + // Buffer creation (aligned with Metal) + BufferResource createBuffer( + const void* data, std::size_t size, uint32_t usage, bool isIndexBuffer, bool persistent) const; + + // Get reusable buffers (aligned with Metal) + const BufferResource& getTileVertexBuffer(); + const BufferResource& getTileIndexBuffer(); + + // Tile clipping mask rendering + bool renderTileClippingMasks(gfx::RenderPass& renderPass, + RenderStaticData& staticData, + const std::vector& tileUBOs); + + // Command encoder tracking + class CommandEncoder* getCurrentCommandEncoder() const { return currentCommandEncoder; } + void setCurrentCommandEncoder(class CommandEncoder* encoder) { currentCommandEncoder = encoder; } + + // Vertex binding creation - needed for drawable geometry + gfx::AttributeBindingArray getOrCreateVertexBindings(gfx::Context&, + const gfx::AttributeDataType vertexType, + const size_t vertexAttributeIndex, + const std::vector& vertexData, + const gfx::VertexAttributeArray& defaults, + const gfx::VertexAttributeArray& overrides, + gfx::BufferUsageType, + const std::optional>&, + std::shared_ptr&) noexcept; + +protected: + std::unique_ptr createRenderbufferResource(gfx::RenderbufferPixelType, Size) override; + std::unique_ptr createDrawScopeResource() override; + +private: + RendererBackend& backend; + std::unique_ptr globalUniformBuffers; + class CommandEncoder* currentCommandEncoder = nullptr; + + // Cached buffers (aligned with Metal) + std::optional tileVertexBuffer; + std::optional tileIndexBuffer; + + // Cached clipping resources + gfx::ShaderProgramBasePtr clipMaskShader; + const gfx::Renderable* clipMaskRenderable = nullptr; + std::optional clipMaskPipelineHash; + std::vector clipMaskUniformBuffers; + std::vector clipMaskActiveBindGroups; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/draw_scope_resource.hpp b/include/mbgl/webgpu/draw_scope_resource.hpp new file mode 100644 index 000000000..cf0a6662d --- /dev/null +++ b/include/mbgl/webgpu/draw_scope_resource.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace mbgl { +namespace webgpu { + +class Context; + +class DrawScopeResource : public gfx::DrawScopeResource { +public: + explicit DrawScopeResource(Context& context); + ~DrawScopeResource() override = default; + +private: + Context& context; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/drawable.hpp b/include/mbgl/webgpu/drawable.hpp new file mode 100644 index 000000000..c8a522a51 --- /dev/null +++ b/include/mbgl/webgpu/drawable.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Forward declare WebGPU types +typedef struct WGPUBindGroupLayoutImpl* WGPUBindGroupLayout; + +namespace mbgl { +namespace gfx { +class UploadPass; +class DepthMode; +class StencilMode; +} // namespace gfx + +namespace webgpu { + +class CommandEncoder; +class UploadPass; +class RenderPipeline; + +class Drawable : public gfx::Drawable { +public: + Drawable(std::string name); + ~Drawable() override; + + void upload(gfx::UploadPass&); + void draw(PaintParameters&) const override; + + void setIndexData(gfx::IndexVectorBasePtr, std::vector segments) override; + void setVertices(std::vector&&, std::size_t, gfx::AttributeDataType) override; + + const gfx::UniformBufferArray& getUniformBuffers() const override; + gfx::UniformBufferArray& mutableUniformBuffers() override; + + void setColorMode(const gfx::ColorMode&) override; + + void setShader(gfx::ShaderProgramBasePtr) override; + + void setEnableStencil(bool) override; + void setEnableDepth(bool) override; + void setSubLayerIndex(int32_t) override; + void setDepthType(gfx::DepthMaskType) override; + void setDepthModeFor3D(const gfx::DepthMode&); + void setStencilModeFor3D(const gfx::StencilMode&); + + void setVertexAttrId(const size_t); + + void updateVertexAttributes(gfx::VertexAttributeArrayPtr, + std::size_t vertexCount, + gfx::DrawMode, + gfx::IndexVectorBasePtr, + const SegmentBase* segments, + std::size_t segmentCount) override; + + class Impl; + const std::unique_ptr impl; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/drawable_builder.hpp b/include/mbgl/webgpu/drawable_builder.hpp new file mode 100644 index 000000000..415767a3f --- /dev/null +++ b/include/mbgl/webgpu/drawable_builder.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class Context; + +class DrawableBuilder : public gfx::DrawableBuilder { +public: + explicit DrawableBuilder(std::string name); + ~DrawableBuilder() override; + +protected: + std::unique_ptr createDrawable() const override; + std::unique_ptr createSegment(gfx::DrawMode, SegmentBase&&) override; + void init() override; + +private: + // WebGPU-specific drawable configuration is handled in the drawable implementation + // Pipeline, buffers, and textures are managed by the Context and Drawable classes +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/index_buffer_resource.hpp b/include/mbgl/webgpu/index_buffer_resource.hpp new file mode 100644 index 000000000..58d604d4c --- /dev/null +++ b/include/mbgl/webgpu/index_buffer_resource.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace webgpu { + +class IndexBufferResource : public gfx::IndexBufferResource { +public: + IndexBufferResource(BufferResource&&) noexcept; + IndexBufferResource(IndexBufferResource&& other) noexcept + : buffer(std::move(other.buffer)) {} + ~IndexBufferResource() noexcept override; + + std::size_t getSizeInBytes() const noexcept { return buffer.getSizeInBytes(); } + const void* contents() const noexcept { return buffer.contents(); } + + BufferResource& get() noexcept { return buffer; } + const BufferResource& get() const noexcept { return buffer; } + + // Compatibility with existing code + const BufferResource& getBuffer() const { return buffer; } + BufferResource& getBuffer() { return buffer; } + + void update(const void* data, std::size_t size) { buffer.update(data, size, 0); } + +protected: + BufferResource buffer; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/layer_group.hpp b/include/mbgl/webgpu/layer_group.hpp new file mode 100644 index 000000000..d67083d62 --- /dev/null +++ b/include/mbgl/webgpu/layer_group.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace mbgl { + +class PaintParameters; + +namespace webgpu { + +/** + * A layer group for non-tile-based drawables + */ +class LayerGroup : public mbgl::LayerGroup { +public: + LayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name); + ~LayerGroup() override {} + + void upload(gfx::UploadPass&) override; + void render(RenderOrchestrator&, PaintParameters&) override; + + const gfx::UniformBufferArray& getUniformBuffers() const override { return uniformBuffers; } + gfx::UniformBufferArray& mutableUniformBuffers() override { return uniformBuffers; } + +protected: + webgpu::UniformBufferArray uniformBuffers; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/offscreen_texture.hpp b/include/mbgl/webgpu/offscreen_texture.hpp new file mode 100644 index 000000000..04bfc95f2 --- /dev/null +++ b/include/mbgl/webgpu/offscreen_texture.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace webgpu { + +class Context; +class Texture2D; + +class OffscreenTexture final : public gfx::OffscreenTexture { +public: + OffscreenTexture(Context&, Size size, gfx::TextureChannelDataType type, bool depth, bool stencil); + + bool isRenderable() override; + PremultipliedImage readStillImage() override; + const gfx::Texture2DPtr& getTexture() override; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/render_pass.hpp b/include/mbgl/webgpu/render_pass.hpp new file mode 100644 index 000000000..b78a3aaef --- /dev/null +++ b/include/mbgl/webgpu/render_pass.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +// Forward declare WebGPU types +typedef struct WGPURenderPassEncoderImpl* WGPURenderPassEncoder; + +namespace mbgl { +namespace gfx { +class CommandEncoder; +class UniformBufferArray; +} // namespace gfx + +namespace webgpu { + +class CommandEncoder; + +class RenderPass final : public gfx::RenderPass { +public: + RenderPass(CommandEncoder& commandEncoder, const char* name, const gfx::RenderPassDescriptor& descriptor); + ~RenderPass() override; + + // Match Metal's interface + CommandEncoder& getCommandEncoder() { return commandEncoder; } + const CommandEncoder& getCommandEncoder() const { return commandEncoder; } + + // Get the WebGPU render pass encoder (similar to Metal's getMetalEncoder) + WGPURenderPassEncoder getEncoder() const; + const gfx::RenderPassDescriptor& getDescriptor() const { return descriptor; } + + // Set/get global uniform buffers for drawables to access + void setGlobalUniformBuffers(const gfx::UniformBufferArray* buffers); + const gfx::UniformBufferArray* getGlobalUniformBuffers() const; + +private: + void pushDebugGroup(const char* name) override; + void popDebugGroup() override; + void addDebugSignpost(const char* name) override; + +private: + gfx::RenderPassDescriptor descriptor; + CommandEncoder& commandEncoder; + const gfx::DebugGroup debugGroup; + + // Implementation details + class Impl; + std::unique_ptr impl; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/renderable_resource.hpp b/include/mbgl/webgpu/renderable_resource.hpp new file mode 100644 index 000000000..5d355e13a --- /dev/null +++ b/include/mbgl/webgpu/renderable_resource.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include + +namespace mbgl { +namespace webgpu { + +class RendererBackend; + +class RenderableResource : public gfx::RenderableResource { +protected: + explicit RenderableResource() = default; + +public: + void bind() override { + // WebGPU binding happens at the command encoder level + // This is a no-op for now + } + + virtual void swap() { + // Renderable resources that require a swap function to be called + // explicitly can override this method. + } + + virtual const mbgl::webgpu::RendererBackend& getBackend() const = 0; + virtual const WGPUCommandEncoder& getCommandEncoder() const = 0; + virtual WGPURenderPassEncoder getRenderPassEncoder() const = 0; + + /** + * Obtain a texture view that should be bound as the color attachment for + * the render pass targeting this resource. Implementations should return a + * valid `WGPUTextureView` or `nullptr` if the resource relies on the + * backend to supply the swapchain view (e.g. default framebuffer). + */ + virtual WGPUTextureView getColorTextureView() { return nullptr; } + + virtual std::optional getColorTextureFormat() const { return std::nullopt; } + + /** + * Obtain a depth/stencil texture view for the render pass if the resource + * manages one. Returning `nullptr` indicates that the backend-provided + * depth/stencil view (if any) should be used instead. + */ + virtual WGPUTextureView getDepthStencilTextureView() { return nullptr; } + + virtual std::optional getDepthStencilTextureFormat() const { return std::nullopt; } +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/renderer_backend.hpp b/include/mbgl/webgpu/renderer_backend.hpp new file mode 100644 index 000000000..ace9750a0 --- /dev/null +++ b/include/mbgl/webgpu/renderer_backend.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { + +class ProgramParameters; + +namespace gfx { +class ShaderRegistry; +} + +namespace webgpu { + +class Context; + +class RendererBackend : public gfx::RendererBackend { +public: + explicit RendererBackend(gfx::ContextMode); + ~RendererBackend() override; + + // getDefaultRenderable() is pure virtual - must be implemented by platform backends + // Platform backends typically inherit from gfx::Renderable and return *this + void initShaders(gfx::ShaderRegistry&, const ProgramParameters&) override; + + // Platform-specific surface creation + void setSurface(void* nativeWindow); + + // WebGPU-specific methods + void setInstance(void* instance); + void setDevice(void* device); + void setQueue(void* queue); + void* getInstance() const; + void* getDevice() const; + void* getQueue() const; + void* getSurface() const; + + // Surface texture access - can be overridden by platform backends + virtual void* getCurrentTextureView(); + virtual void* getDepthStencilView(); + virtual mbgl::Size getFramebufferSize() const; + + void setDepthStencilFormat(wgpu::TextureFormat); + wgpu::TextureFormat getDepthStencilFormat() const; + void setColorFormat(wgpu::TextureFormat); + wgpu::TextureFormat getColorFormat() const; + +protected: + std::unique_ptr createContext() override; + void activate() override; + void deactivate() override; + +private: + class Impl; + std::unique_ptr impl; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/texture2d.hpp b/include/mbgl/webgpu/texture2d.hpp new file mode 100644 index 000000000..ed3f39e34 --- /dev/null +++ b/include/mbgl/webgpu/texture2d.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace webgpu { + +class Context; +class RenderPass; + +class Texture2D : public gfx::Texture2D { +public: + Texture2D(Context& context_); + ~Texture2D() override; + + gfx::Texture2D& setSamplerConfiguration(const SamplerState&) noexcept override; + gfx::Texture2D& setFormat(gfx::TexturePixelType, gfx::TextureChannelDataType) noexcept override; + gfx::Texture2D& setSize(Size size_) noexcept override; + gfx::Texture2D& setImage(std::shared_ptr) noexcept override; + Texture2D& setUsage(uint32_t usageFlags_) noexcept; + void setSizeChangedCallback(std::function) noexcept; + WGPUTextureFormat getNativeFormat() const noexcept { return nativeFormat; } + + gfx::TexturePixelType getFormat() const noexcept override { return pixelFormat; } + Size getSize() const noexcept override { return size; } + size_t getDataSize() const noexcept override; + size_t getPixelStride() const noexcept override; + size_t numChannels() const noexcept override; + + void create() noexcept override; + void upload(const void* pixelData, const Size& size) noexcept override; + void upload() noexcept override; + void uploadSubRegion(const void* pixelData, const Size& size, uint16_t xOffset, uint16_t yOffset) noexcept override; + bool needsUpload() const noexcept override { return dirty; } + + WGPUTexture getTexture() const { return texture; } + WGPUTextureView getTextureView() const { return textureView; } + WGPUSampler getSampler() const { return sampler; } + const SamplerState& getSamplerState() const noexcept { return samplerState; } + +protected: + Context& context; + WGPUTexture texture = nullptr; + WGPUTextureView textureView = nullptr; + WGPUSampler sampler = nullptr; + uint32_t usageFlags = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc; + WGPUTextureFormat nativeFormat = WGPUTextureFormat_Undefined; + + Size size{0, 0}; + gfx::TexturePixelType pixelFormat = gfx::TexturePixelType::RGBA; + gfx::TextureChannelDataType channelType = gfx::TextureChannelDataType::UnsignedByte; + SamplerState samplerState; + std::shared_ptr image; + bool dirty = false; + std::function sizeChangedCallback; +}; + +} // namespace webgpu +} // namespace mbgl +#include diff --git a/include/mbgl/webgpu/tile_layer_group.hpp b/include/mbgl/webgpu/tile_layer_group.hpp new file mode 100644 index 000000000..10f9e45e4 --- /dev/null +++ b/include/mbgl/webgpu/tile_layer_group.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace mbgl { + +class PaintParameters; + +namespace webgpu { + +/** + * A layer group for tile-based drawables + */ +class TileLayerGroup : public mbgl::TileLayerGroup { +public: + TileLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name); + ~TileLayerGroup() override {} + + void upload(gfx::UploadPass&) override; + void render(RenderOrchestrator&, PaintParameters&) override; + + const gfx::UniformBufferArray& getUniformBuffers() const override { return uniformBuffers; } + gfx::UniformBufferArray& mutableUniformBuffers() override { return uniformBuffers; } + +protected: + webgpu::UniformBufferArray uniformBuffers; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/uniform_buffer.hpp b/include/mbgl/webgpu/uniform_buffer.hpp new file mode 100644 index 000000000..7cba1e691 --- /dev/null +++ b/include/mbgl/webgpu/uniform_buffer.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace webgpu { + +class Context; +class RenderPass; + +class UniformBuffer final : public gfx::UniformBuffer { +public: + UniformBuffer(Context& context, const void* data, std::size_t size); + ~UniformBuffer() override; + + UniformBuffer(const UniformBuffer&) = delete; + UniformBuffer(UniformBuffer&& other) noexcept; + UniformBuffer& operator=(const UniformBuffer&) = delete; + UniformBuffer& operator=(UniformBuffer&&) = delete; + + UniformBuffer clone() const; + + void update(const void* data, std::size_t dataSize) override; + + WGPUBuffer getBuffer() const { return buffer; } + +private: + Context& context; + WGPUBuffer buffer = nullptr; +}; + +/// Stores a collection of uniform buffers by name +class UniformBufferArray final : public gfx::UniformBufferArray { +public: + UniformBufferArray() = default; + UniformBufferArray(UniformBufferArray&& other) + : gfx::UniformBufferArray(std::move(other)) {} + UniformBufferArray(const UniformBufferArray&) = delete; + + UniformBufferArray& operator=(UniformBufferArray&& other) { + gfx::UniformBufferArray::operator=(std::move(other)); + return *this; + } + UniformBufferArray& operator=(const UniformBufferArray& other) { + gfx::UniformBufferArray::operator=(other); + return *this; + } + + void bindWebgpu(RenderPass&) const noexcept; + void bind(gfx::RenderPass& renderPass) override; + +private: + gfx::UniqueUniformBuffer copy(const gfx::UniformBuffer& buffer) override { + return std::make_unique(static_cast(buffer).clone()); + } +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/upload_pass.hpp b/include/mbgl/webgpu/upload_pass.hpp new file mode 100644 index 000000000..edfad074d --- /dev/null +++ b/include/mbgl/webgpu/upload_pass.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include +#include + +namespace mbgl { +namespace gfx { +class CommandEncoder; +class Renderable; +} // namespace gfx + +namespace webgpu { + +class CommandEncoder; + +class RenderbufferResource : public gfx::RenderbufferResource { +public: + RenderbufferResource() = default; +}; + +class UploadPass final : public gfx::UploadPass { +public: + UploadPass(gfx::Renderable& renderable, CommandEncoder& commandEncoder, const char* name); + ~UploadPass() override; + +private: + void pushDebugGroup(const char* name) override; + void popDebugGroup() override; + +public: + gfx::Context& getContext() override; + const gfx::Context& getContext() const override; + + std::unique_ptr createVertexBufferResource(const void* data, + std::size_t size, + gfx::BufferUsageType, + bool persistent) override; + void updateVertexBufferResource(gfx::VertexBufferResource&, const void* data, std::size_t size) override; + + std::unique_ptr createIndexBufferResource(const void* data, + std::size_t size, + gfx::BufferUsageType, + bool persistent) override; + void updateIndexBufferResource(gfx::IndexBufferResource&, const void* data, std::size_t size) override; + + gfx::AttributeBindingArray buildAttributeBindings( + const std::size_t vertexCount, + const gfx::AttributeDataType vertexType, + const std::size_t vertexAttributeIndex, + const std::vector& vertexData, + const gfx::VertexAttributeArray& defaults, + const gfx::VertexAttributeArray& overrides, + gfx::BufferUsageType, + const std::optional> lastUpdate, + /*out*/ std::vector>& outBuffers) override; + + const gfx::UniqueVertexBufferResource& getBuffer(const gfx::VertexVectorBasePtr&, + gfx::BufferUsageType, + bool forceUpdate); + +private: + CommandEncoder& commandEncoder; + std::vector> debugGroups; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/vertex_attribute.hpp b/include/mbgl/webgpu/vertex_attribute.hpp new file mode 100644 index 000000000..d77e472b1 --- /dev/null +++ b/include/mbgl/webgpu/vertex_attribute.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace gfx { +class VertexBufferResource; +} // namespace gfx +namespace webgpu { + +class UploadPass; + +class VertexAttribute final : public gfx::VertexAttribute { +public: + VertexAttribute(int index_, gfx::AttributeDataType dataType_, std::size_t count_) + : gfx::VertexAttribute(index_, dataType_, count_) {} + VertexAttribute(const VertexAttribute& other) = delete; + VertexAttribute(VertexAttribute&& other) + : gfx::VertexAttribute(std::move(other)) {} + ~VertexAttribute() override = default; + + static const gfx::UniqueVertexBufferResource& getBuffer(gfx::VertexAttribute&, + UploadPass&, + const gfx::BufferUsageType, + bool forceUpdate); +}; + +/// Stores a collection of vertex attributes by name +class VertexAttributeArray final : public gfx::VertexAttributeArray { +public: + VertexAttributeArray() = default; + VertexAttributeArray(VertexAttributeArray&& other) + : gfx::VertexAttributeArray(std::move(other)) {} + + VertexAttributeArray& operator=(VertexAttributeArray&& other) { + gfx::VertexAttributeArray::operator=(std::move(other)); + return *this; + } + VertexAttributeArray& operator=(const VertexAttributeArray& other) = delete; + + /// Indicates whether any values have changed + bool isModifiedAfter(std::chrono::duration time) const; + +private: + gfx::UniqueVertexAttribute create(int index, gfx::AttributeDataType dataType, std::size_t count) const override { + return std::make_unique(index, dataType, count); + } +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/vertex_buffer_resource.hpp b/include/mbgl/webgpu/vertex_buffer_resource.hpp new file mode 100644 index 000000000..046bcaa05 --- /dev/null +++ b/include/mbgl/webgpu/vertex_buffer_resource.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace webgpu { + +class Context; + +class VertexBufferResource : public gfx::VertexBufferResource { +public: + VertexBufferResource(BufferResource&&) noexcept; + VertexBufferResource(VertexBufferResource&& other) noexcept + : buffer(std::move(other.buffer)) {} + ~VertexBufferResource() noexcept override; + + std::size_t getSizeInBytes() const noexcept { return buffer.getSizeInBytes(); } + const void* contents() const noexcept { return buffer.contents(); } + + BufferResource& get() noexcept { return buffer; } + const BufferResource& get() const noexcept { return buffer; } + + // Compatibility with existing code + const BufferResource& getBuffer() const { return buffer; } + BufferResource& getBuffer() { return buffer; } + + std::chrono::duration getLastUpdated() const { return lastUpdated; } + void setLastUpdated(std::chrono::duration time) { lastUpdated = time; } + + void update(const void* data, std::size_t size) { buffer.update(data, size, 0); } + +protected: + BufferResource buffer; + std::chrono::duration lastUpdated; +}; + +using UniqueVertexBufferResource = std::unique_ptr; + +} // namespace webgpu +} // namespace mbgl diff --git a/include/mbgl/webgpu/wgpu_cpp_compat.hpp b/include/mbgl/webgpu/wgpu_cpp_compat.hpp new file mode 100644 index 000000000..d249b2796 --- /dev/null +++ b/include/mbgl/webgpu/wgpu_cpp_compat.hpp @@ -0,0 +1,19 @@ +#pragma once + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" +#pragma clang diagnostic ignored "-Wstrict-aliasing" +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif + +#include + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif diff --git a/metrics/ignores/platform-macos-webgpu.json b/metrics/ignores/platform-macos-webgpu.json new file mode 100644 index 000000000..3e9bbfe89 --- /dev/null +++ b/metrics/ignores/platform-macos-webgpu.json @@ -0,0 +1,8 @@ +{ + "render-tests/icon-text-fit/enlargen-both": "Needs to be investigated and fixed.", + "render-tests/regressions/mapbox-gl-js#5642": "Failing with mbgl-render-test", + "render-tests/regressions/mapbox-gl-js#7066": "Failing with mbgl-render-test", + "render-tests/fill-outline-color/zoom-and-property-function": "It will be fixed after triangulated lines will support data driven properties", + "render-tests/fill-outline-color/property-function": "It will be fixed after triangulated lines will support data driven properties", + "render-tests/fill-extrusion-color/function": "Layer Z Fighting: https://github.com/maplibre/maplibre-native/issues/1847" +} diff --git a/metrics/linux-webgpu-dawn.json b/metrics/linux-webgpu-dawn.json new file mode 100644 index 000000000..012f1b557 --- /dev/null +++ b/metrics/linux-webgpu-dawn.json @@ -0,0 +1,15 @@ +{ + "base_test_path": "integration", + "cache_path": "cache-style.db", + "expectation_paths": [ + "expectations/platform-linux" + ], + "ignore_paths": [ + "ignores/platform-all.json", + "ignores/platform-linux-webgpu.json" + ], + "metric_path": "linux-webgpu-dawn", + "probes": [ + "probeNetwork" + ] +} diff --git a/metrics/linux-webgpu-wgpu.json b/metrics/linux-webgpu-wgpu.json new file mode 100644 index 000000000..388cf9daa --- /dev/null +++ b/metrics/linux-webgpu-wgpu.json @@ -0,0 +1,15 @@ +{ + "base_test_path": "integration", + "cache_path": "cache-style.db", + "expectation_paths": [ + "expectations/platform-linux" + ], + "ignore_paths": [ + "ignores/platform-all.json", + "ignores/platform-linux-webgpu.json" + ], + "metric_path": "linux-webgpu-wgpu", + "probes": [ + "probeNetwork" + ] +} diff --git a/metrics/macos-webgpu-dawn.json b/metrics/macos-webgpu-dawn.json new file mode 100644 index 000000000..64e0be1c3 --- /dev/null +++ b/metrics/macos-webgpu-dawn.json @@ -0,0 +1,15 @@ +{ + "base_test_path": "integration", + "cache_path": "cache-style.db", + "expectation_paths": [ + "expectations/platform-macos" + ], + "ignore_paths": [ + "ignores/platform-all.json", + "ignores/platform-macos-webgpu.json" + ], + "metric_path": "macos-webgpu-dawn", + "probes": [ + "probeNetwork" + ] +} diff --git a/metrics/macos-webgpu-wgpu.json b/metrics/macos-webgpu-wgpu.json new file mode 100644 index 000000000..edabdf64b --- /dev/null +++ b/metrics/macos-webgpu-wgpu.json @@ -0,0 +1,15 @@ +{ + "base_test_path": "integration", + "cache_path": "cache-style.db", + "expectation_paths": [ + "expectations/platform-macos" + ], + "ignore_paths": [ + "ignores/platform-all.json", + "ignores/platform-macos-webgpu.json" + ], + "metric_path": "macos-webgpu-wgpu", + "probes": [ + "probeNetwork" + ] +} diff --git a/platform/darwin/darwin.cmake b/platform/darwin/darwin.cmake index 7f25019eb..1bdab18ae 100644 --- a/platform/darwin/darwin.cmake +++ b/platform/darwin/darwin.cmake @@ -12,6 +12,18 @@ target_link_libraries( z ) +if(TARGET mbgl-vendor-dawn) + target_link_libraries( + mbgl-vendor-dawn + INTERFACE + "-framework Metal" + "-framework QuartzCore" + "-framework IOKit" + "-framework IOSurface" + "-framework CoreGraphics" + ) +endif() + if(MLN_DARWIN_USE_LIBUV) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBUV REQUIRED IMPORTED_TARGET libuv) diff --git a/platform/default/include/mbgl/webgpu/headless_backend.hpp b/platform/default/include/mbgl/webgpu/headless_backend.hpp new file mode 100644 index 000000000..1a3774817 --- /dev/null +++ b/platform/default/include/mbgl/webgpu/headless_backend.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class HeadlessBackend final : public webgpu::RendererBackend, public gfx::HeadlessBackend { +public: + HeadlessBackend(Size = {256, 256}, + SwapBehaviour = SwapBehaviour::NoFlush, + gfx::ContextMode = gfx::ContextMode::Unique); + ~HeadlessBackend() override; + + gfx::Renderable& getDefaultRenderable() override; + PremultipliedImage readStillImage() override; + RendererBackend* getRendererBackend() override; + + // Override WebGPU-specific methods + void* getCurrentTextureView() override; + void* getDepthStencilView() override; + mbgl::Size getFramebufferSize() const override; + +private: + void activate() override; + void deactivate() override; + void createOffscreenTextures(); + +private: + class Impl; + std::unique_ptr impl; + bool active = false; +}; + +} // namespace webgpu +} // namespace mbgl diff --git a/platform/glfw/CMakeLists.txt b/platform/glfw/CMakeLists.txt index b9a0234d8..82d2ebf3b 100644 --- a/platform/glfw/CMakeLists.txt +++ b/platform/glfw/CMakeLists.txt @@ -69,6 +69,37 @@ if(MLN_WITH_METAL) ) endif() +if(MLN_WITH_WEBGPU) + if(APPLE) + target_sources( + mbgl-glfw + PRIVATE ${PROJECT_SOURCE_DIR}/platform/glfw/glfw_webgpu_backend.mm + ) + else() + target_sources( + mbgl-glfw + PRIVATE ${PROJECT_SOURCE_DIR}/platform/glfw/glfw_webgpu_backend.cpp + ) + endif() + if(TARGET mbgl-vendor-dawn) + target_link_libraries(mbgl-glfw PRIVATE mbgl-vendor-dawn) + endif() +endif() + +if(DEFINED MLN_WITH_X11 AND MLN_WITH_X11) + target_compile_definitions( + mbgl-glfw + PRIVATE MLN_WITH_X11=1 + ) +endif() + +if(DEFINED MLN_WITH_WAYLAND AND MLN_WITH_WAYLAND) + target_compile_definitions( + mbgl-glfw + PRIVATE MLN_WITH_WAYLAND=1 + ) +endif() + target_include_directories( mbgl-glfw PRIVATE diff --git a/platform/glfw/glfw_view.cpp b/platform/glfw/glfw_view.cpp index 3485ef5b4..c33d42e2b 100644 --- a/platform/glfw/glfw_view.cpp +++ b/platform/glfw/glfw_view.cpp @@ -222,6 +222,13 @@ GLFWView::GLFWView(bool fullscreen_, glfwSetErrorCallback(glfwError); std::srand(static_cast(std::time(nullptr))); +#if defined(MLN_RENDER_BACKEND_WEBGPU) +#ifdef __linux__ + // Force X11 platform for WebGPU compatibility (Dawn doesn't support Wayland yet) + // For now, always use X11 when WebGPU might be used + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); +#endif +#endif if (!glfwInit()) { mbgl::Log::Error(mbgl::Event::OpenGL, "failed to initialize glfw"); diff --git a/platform/glfw/glfw_view.hpp b/platform/glfw/glfw_view.hpp index ffea1020a..51fc5b33d 100644 --- a/platform/glfw/glfw_view.hpp +++ b/platform/glfw/glfw_view.hpp @@ -9,7 +9,8 @@ #include #include -#if (defined(MLN_RENDER_BACKEND_OPENGL) || defined(MLN_RENDER_BACKEND_VULKAN)) && \ +#if (defined(MLN_RENDER_BACKEND_OPENGL) || defined(MLN_RENDER_BACKEND_VULKAN) || \ + defined(MLN_RENDER_BACKEND_WEBGPU)) && \ !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) #define ENABLE_LOCATION_INDICATOR #endif diff --git a/platform/glfw/glfw_webgpu_backend.cpp b/platform/glfw/glfw_webgpu_backend.cpp new file mode 100644 index 000000000..2a47f7ce2 --- /dev/null +++ b/platform/glfw/glfw_webgpu_backend.cpp @@ -0,0 +1,4 @@ +#ifndef __APPLE__ +#define GLFW_WEBGPU_BACKEND_IMPLEMENTATION +#include "glfw_webgpu_backend_impl.ipp" +#endif diff --git a/platform/glfw/glfw_webgpu_backend.hpp b/platform/glfw/glfw_webgpu_backend.hpp new file mode 100644 index 000000000..43106bd2d --- /dev/null +++ b/platform/glfw/glfw_webgpu_backend.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include "glfw_backend.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GLFWwindow; +struct WGPUDeviceImpl; +struct WGPUSurfaceImpl; + +namespace dawn::native { +class Instance; +class Adapter; +} // namespace dawn::native + +// Multiple inheritance: GLFWBackend for window management, +// webgpu::RendererBackend for rendering, gfx::Renderable for framebuffer +class GLFWWebGPUBackend final : public GLFWBackend, public mbgl::webgpu::RendererBackend, public mbgl::gfx::Renderable { +public: + GLFWWebGPUBackend(GLFWwindow* window, bool capFrameRate); + ~GLFWWebGPUBackend() override; + + void swap(); + + // GLFWBackend implementation +public: + mbgl::gfx::RendererBackend& getRendererBackend() override; + mbgl::Size getSize() const override; + void setSize(mbgl::Size) override; + + // mbgl::gfx::RendererBackend implementation +public: + mbgl::gfx::Renderable& getDefaultRenderable() override; + +protected: + void activate() override; + void deactivate() override; + + // WebGPU-specific methods +public: + // Override virtual methods from RendererBackend + void* getCurrentTextureView() override; + void* getDepthStencilView() override; + mbgl::Size getFramebufferSize() const override; + +private: + class SpinLock { + public: + void lock(); + void unlock(); + + private: + std::atomic_flag flag = ATOMIC_FLAG_INIT; + }; + + mutable SpinLock textureStateLock; + + GLFWwindow* window; + std::unique_ptr instance; + wgpu::Device wgpuDevice; // This owns the device + wgpu::Queue queue; + wgpu::Surface wgpuSurface; + wgpu::TextureFormat swapChainFormat = wgpu::TextureFormat::BGRA8Unorm; + wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined; + std::array configuredViewFormats{wgpu::TextureFormat::BGRA8Unorm}; + + // Protect texture view access from multiple threads + wgpu::TextureView currentTextureView; // Keep the current texture view alive + wgpu::Texture currentTexture; // Keep the texture alive as well + + // Depth/stencil texture and view + wgpu::Texture depthStencilTexture; + wgpu::TextureView depthStencilView; + + // Frame synchronization + std::atomic frameInProgress{false}; + // Track if we have presented the current frame + std::atomic framePresented{true}; + + // Track if we're shutting down to prevent mutex usage during destruction + std::atomic isShuttingDown{false}; + + // Previous frame resources - keep alive until next swap + wgpu::TextureView previousTextureView; + wgpu::Texture previousTexture; + + // Surface state tracking + std::atomic surfaceConfigured{false}; + std::atomic surfaceNeedsReconfigure{false}; + mbgl::Size lastConfiguredSize{0, 0}; + + // Error recovery + std::atomic consecutiveErrors{0}; + static constexpr int maxConsecutiveErrors = 3; + + // Helper methods + void reconfigureSurface(); + void createDepthStencilTexture(uint32_t width, uint32_t height); + bool waitForFrame(std::chrono::milliseconds timeout = std::chrono::milliseconds(100)); + void signalFrameComplete(); + void periodicMaintenance(); + void processEvents(); + + // Debug triangle utilities + void ensureDebugTriangleResources(); + void drawDebugTriangle(const wgpu::TextureView& targetView); + + bool debugTriangleInitialized = false; + wgpu::RenderPipeline debugTrianglePipeline; + wgpu::Buffer debugTriangleVertexBuffer; + uint64_t debugTriangleVertexBufferSize = 0; +}; diff --git a/platform/glfw/glfw_webgpu_backend.mm b/platform/glfw/glfw_webgpu_backend.mm new file mode 100644 index 000000000..989faa59d --- /dev/null +++ b/platform/glfw/glfw_webgpu_backend.mm @@ -0,0 +1,2 @@ +#define GLFW_WEBGPU_BACKEND_IMPLEMENTATION +#include "glfw_webgpu_backend_impl.ipp" diff --git a/platform/glfw/glfw_webgpu_backend_impl.ipp b/platform/glfw/glfw_webgpu_backend_impl.ipp new file mode 100644 index 000000000..28a03157a --- /dev/null +++ b/platform/glfw/glfw_webgpu_backend_impl.ipp @@ -0,0 +1,1198 @@ +#include "glfw_webgpu_backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef __APPLE__ +#define GLFW_EXPOSE_NATIVE_COCOA +#include +#import +#import +#import +#include +#elif defined(__linux__) +#define GLFW_EXPOSE_NATIVE_X11 +#define GLFW_EXPOSE_NATIVE_WAYLAND +#include +#include +#endif + +#ifdef None +#undef None +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void logUncapturedError(const wgpu::Device&, wgpu::ErrorType type, wgpu::StringView message) { + mbgl::Log::Error(mbgl::Event::Render, + std::string("Dawn validation error [") + + std::to_string(static_cast(type)) + "] " + + (message.data ? std::string(message.data, message.length) : std::string())); +} + +void logDeviceLost(const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) { + mbgl::Log::Error(mbgl::Event::Render, + std::string("Dawn device lost [") + + std::to_string(static_cast(reason)) + "] " + + (message.data ? std::string(message.data, message.length) : std::string())); +} + +std::optional desiredBackendFromEnv() { + if (const char* env = std::getenv("MLN_WEBGPU_BACKEND_TARGET")) { + std::string value(env); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return std::tolower(c); }); + if (value == "vulkan") { + return WGPUBackendType_Vulkan; + } else if (value == "opengl" || value == "gl" || value == "opengles" || value == "gles") { + return WGPUBackendType_OpenGL; + } else if (value == "metal") { + return WGPUBackendType_Metal; + } else if (value == "d3d11") { + return WGPUBackendType_D3D11; + } else if (value == "d3d12") { + return WGPUBackendType_D3D12; + } + } + return std::nullopt; +} + +const char* backendTypeToString(WGPUBackendType type) { + switch (type) { + case WGPUBackendType_Null: + return "Null"; + case WGPUBackendType_WebGPU: + return "WebGPU"; + case WGPUBackendType_D3D11: + return "D3D11"; + case WGPUBackendType_D3D12: + return "D3D12"; + case WGPUBackendType_Metal: + return "Metal"; + case WGPUBackendType_Vulkan: + return "Vulkan"; + case WGPUBackendType_OpenGL: + return "OpenGL"; + case WGPUBackendType_OpenGLES: + return "OpenGLES"; + default: + return "Unknown"; + } +} + +bool backendMatches(WGPUBackendType candidate, WGPUBackendType desired) { + if (desired == WGPUBackendType_OpenGL) { + return candidate == WGPUBackendType_OpenGL || candidate == WGPUBackendType_OpenGLES; + } + return candidate == desired; +} + +#ifdef __APPLE__ +MTLPixelFormat toMetalPixelFormat(wgpu::TextureFormat format) { + switch (format) { + case wgpu::TextureFormat::BGRA8UnormSrgb: + return MTLPixelFormatBGRA8Unorm_sRGB; + case wgpu::TextureFormat::RGBA16Float: + return MTLPixelFormatRGBA16Float; + default: + return MTLPixelFormatBGRA8Unorm; + } +} +#endif + +} // namespace + +// Forward declaration +class GLFWWebGPUBackend; + +namespace mbgl { + +// WebGPU-specific RenderableResource implementation (similar to Metal's) +class WebGPURenderableResource final : public webgpu::RenderableResource { +public: + WebGPURenderableResource(GLFWWebGPUBackend& backend_) + : backend(backend_) {} + + void bind() override { + // Similar to Metal - prepare for rendering + // Nothing to do here for WebGPU as command encoder is managed by CommandEncoder class + } + + void swap() override { + // Similar to Metal - submit command buffer and present surface + // The command buffer submission happens in CommandEncoder::present() + // Here we just present the surface + backend.swap(); + } + + const mbgl::webgpu::RendererBackend& getBackend() const override { + return backend; + } + + const WGPUCommandEncoder& getCommandEncoder() const override { + // WebGPU's command encoder is managed by the CommandEncoder class + static WGPUCommandEncoder dummy = nullptr; + return dummy; + } + + WGPURenderPassEncoder getRenderPassEncoder() const override { + // Render pass encoder is managed by RenderPass class + return nullptr; + } + + WGPUTextureView getColorTextureView() override { + if (auto* viewPtr = static_cast(backend.getCurrentTextureView())) { + return viewPtr; + } + return nullptr; + } + + WGPUTextureView getDepthStencilTextureView() override { + if (auto* viewPtr = static_cast(backend.getDepthStencilView())) { + return viewPtr; + } + return nullptr; + } + +private: + GLFWWebGPUBackend& backend; +}; + +namespace gfx { +template <> +std::unique_ptr Backend::Create(GLFWwindow* window, bool capFrameRate) { + return std::make_unique(window, capFrameRate); +} +} // namespace gfx +} // namespace mbgl + +void GLFWWebGPUBackend::SpinLock::lock() { + while (flag.test_and_set(std::memory_order_acquire)) { + std::this_thread::yield(); + } +} + +void GLFWWebGPUBackend::SpinLock::unlock() { + flag.clear(std::memory_order_release); +} + +GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate) + : GLFWBackend(), + mbgl::webgpu::RendererBackend(mbgl::gfx::ContextMode::Unique), + mbgl::gfx::Renderable([window_] { + int fbWidth = 0; + int fbHeight = 0; + if (window_) { + glfwGetFramebufferSize(window_, &fbWidth, &fbHeight); + } + return mbgl::Size{static_cast(std::max(fbWidth, 0)), + static_cast(std::max(fbHeight, 0))}; + }(), + std::make_unique(*this)), + window(window_) { + static_cast(capFrameRate); + + // Add small delay to let previous resources clean up + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Check window visibility + if (window) { + int visible = glfwGetWindowAttrib(window, GLFW_VISIBLE); + + if (!visible) { + glfwShowWindow(window); + } + } + + + // Create Dawn instance + instance = std::make_unique(); + + // Enumerate adapters with retry logic + std::vector adapters; + for (int attempt = 0; attempt < 3; ++attempt) { + adapters = instance->EnumerateAdapters(); + if (!adapters.empty()) { + // Format the count as a string to avoid logging API issues + std::string adapterMsg = "Found " + std::to_string(adapters.size()) + " Dawn adapters"; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + if (adapters.empty()) { + throw std::runtime_error("No WebGPU adapters found after retries"); + } + + + std::size_t selectedIndex = 0; + if (auto targettedBackend = desiredBackendFromEnv()) { + bool matched = false; + for (std::size_t i = 0; i < adapters.size(); ++i) { + WGPUAdapterInfo info = WGPU_ADAPTER_INFO_INIT; + if (wgpuAdapterGetInfo(adapters[i].Get(), &info) != WGPUStatus_Success) { + continue; + } + const auto candidate = info.backendType; + const bool isMatch = backendMatches(candidate, *targettedBackend); + wgpuAdapterInfoFreeMembers(info); + if (isMatch) { + selectedIndex = i; + matched = true; + mbgl::Log::Info(mbgl::Event::Render, + std::string("WebGPU: Forcing adapter backend to ") + + backendTypeToString(candidate)); + break; + } + } + if (!matched) { + mbgl::Log::Warning(mbgl::Event::Render, + "WebGPU: Requested backend not available, using default adapter"); + } + } + + dawn::native::Adapter& selectedAdapter = adapters[selectedIndex]; + + { + WGPUAdapterInfo info = WGPU_ADAPTER_INFO_INIT; + if (wgpuAdapterGetInfo(selectedAdapter.Get(), &info) == WGPUStatus_Success) { + mbgl::Log::Info(mbgl::Event::Render, + std::string("WebGPU: Selected adapter backend = ") + + backendTypeToString(info.backendType) + + ", name = " + + (info.device.data ? std::string(info.device.data, info.device.length) + : std::string(""))); + wgpuAdapterInfoFreeMembers(info); + } + } + + std::vector supportedFeatures; + WGPUSupportedFeatures features = WGPU_SUPPORTED_FEATURES_INIT; + wgpuAdapterGetFeatures(selectedAdapter.Get(), &features); + if (features.featureCount > 0 && features.features) { + supportedFeatures.reserve(features.featureCount); + for (size_t i = 0; i < features.featureCount; ++i) { + supportedFeatures.push_back(static_cast(features.features[i])); + } + wgpuSupportedFeaturesFreeMembers(features); + } + const bool hasDepth32Stencil = std::find(supportedFeatures.begin(), supportedFeatures.end(), + wgpu::FeatureName::Depth32FloatStencil8) != supportedFeatures.end(); + + std::vector requiredFeatures; + if (hasDepth32Stencil) { + requiredFeatures.push_back(wgpu::FeatureName::Depth32FloatStencil8); + depthStencilFormat = wgpu::TextureFormat::Depth32FloatStencil8; + } else { + depthStencilFormat = wgpu::TextureFormat::Depth24PlusStencil8; + } + + + wgpu::DeviceDescriptor deviceDesc = {}; + deviceDesc.label = "MapLibre WebGPU Device"; + deviceDesc.requiredFeatures = requiredFeatures.data(); + deviceDesc.requiredFeatureCount = requiredFeatures.size(); + deviceDesc.SetUncapturedErrorCallback(logUncapturedError); + deviceDesc.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous, logDeviceLost); + + // Create device with descriptor + WGPUDevice rawDevice = selectedAdapter.CreateDevice(&deviceDesc); + if (!rawDevice) { + // Retry once after delay + mbgl::Log::Warning(mbgl::Event::Render, "Failed to create device, retrying..."); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + rawDevice = selectedAdapter.CreateDevice(&deviceDesc); + if (!rawDevice) { + throw std::runtime_error("Failed to create WebGPU device after retry"); + } + } + + wgpuDevice = wgpu::Device::Acquire(rawDevice); + + setDepthStencilFormat(depthStencilFormat); + + // Process device events to ensure it's ready + processEvents(); + + // Note: Dawn's error callback API is different from wgpu-native + // Errors will be logged by Dawn's internal validation for now + +#ifdef __APPLE__ + // Setup Metal surface on macOS + NSWindow* nsWindow = glfwGetCocoaWindow(window); + NSView* view = [nsWindow contentView]; + [view setWantsLayer:YES]; + + // Create CAMetalLayer + CAMetalLayer* layer = [CAMetalLayer layer]; + layer.device = dawn::native::metal::GetMTLDevice(wgpuDevice.Get()); + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + if (colorSpace) { + layer.colorspace = colorSpace; + CGColorSpaceRelease(colorSpace); + } + CGFloat scale = [nsWindow backingScaleFactor]; + if (scale <= 0.0) { + scale = [[NSScreen mainScreen] backingScaleFactor]; + } + layer.contentsScale = scale; + + // Get window size + int width, height; + glfwGetFramebufferSize(window, &width, &height); + layer.drawableSize = CGSizeMake(width, height); + + [view setLayer:layer]; + + // Create surface from metal layer + wgpu::SurfaceSourceMetalLayer metalDesc; + metalDesc.layer = (__bridge void*)layer; + + wgpu::SurfaceDescriptor surfaceDesc; + surfaceDesc.nextInChain = &metalDesc; + + // Create surface using the instance + wgpu::Instance wgpuInstance(instance->Get()); + wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc); + + if (wgpuSurface) { + WGPUSurfaceCapabilities capabilities = WGPU_SURFACE_CAPABILITIES_INIT; + if (wgpuSurfaceGetCapabilities(wgpuSurface.Get(), selectedAdapter.Get(), &capabilities) == WGPUStatus_Success) { + const auto pickFormat = [&]() { + const wgpu::TextureFormat preferredFormats[] = { + wgpu::TextureFormat::BGRA8Unorm, + wgpu::TextureFormat::BGRA8UnormSrgb, + wgpu::TextureFormat::RGBA8Unorm, + wgpu::TextureFormat::RGBA8UnormSrgb + }; + for (const auto format : preferredFormats) { + for (size_t i = 0; i < capabilities.formatCount; ++i) { + if (capabilities.formats[i] == static_cast(format)) { + return format; + } + } + } + return capabilities.formatCount > 0 ? static_cast(capabilities.formats[0]) + : swapChainFormat; + }; + swapChainFormat = pickFormat(); + } + wgpuSurfaceCapabilitiesFreeMembers(capabilities); + } + + setColorFormat(swapChainFormat); + + layer.pixelFormat = toMetalPixelFormat(swapChainFormat); + + // Get the queue + queue = wgpuDevice.GetQueue(); + + // Configure surface + wgpu::SurfaceConfiguration config = {}; + config.device = wgpuDevice; + config.format = swapChainFormat; + config.usage = wgpu::TextureUsage::RenderAttachment; + config.alphaMode = wgpu::CompositeAlphaMode::Auto; + config.width = width; + config.height = height; + config.presentMode = wgpu::PresentMode::Fifo; + configuredViewFormats[0] = swapChainFormat; + config.viewFormatCount = 1; + config.viewFormats = configuredViewFormats.data(); + + wgpuSurface.Configure(&config); + surfaceConfigured = true; + lastConfiguredSize = {static_cast(width), static_cast(height)}; + + createDepthStencilTexture(static_cast(width), static_cast(height)); +#elif defined(__linux__) + // Get window size + int width, height; + glfwGetFramebufferSize(window, &width, &height); + std::string windowSizeMsg = "Window size: " + std::to_string(width) + "x" + std::to_string(height); + + // Create surface for WebGPU + wgpu::Instance wgpuInstance(instance->Get()); + + bool surfaceCreated = false; + +#if defined(MLN_WITH_X11) + if (!surfaceCreated) { + Display* x11Display = glfwGetX11Display(); + Window x11Window = glfwGetX11Window(window); + + if (x11Display && x11Window) { + wgpu::SurfaceDescriptorFromXlibWindow x11Desc = {}; + x11Desc.display = x11Display; + x11Desc.window = x11Window; + + wgpu::SurfaceDescriptor surfaceDesc = {}; + surfaceDesc.nextInChain = &x11Desc; + + wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc); + surfaceCreated = true; + } + } +#endif + +#if defined(MLN_WITH_WAYLAND) + if (!surfaceCreated) { + struct wl_display* waylandDisplay = glfwGetWaylandDisplay(); + struct wl_surface* waylandSurface = glfwGetWaylandWindow(window); + + if (waylandDisplay && waylandSurface) { + mbgl::Log::Warning(mbgl::Event::Render, "Attempting Wayland surface (experimental)"); + + wgpu::SurfaceDescriptorFromWaylandSurface waylandDesc = {}; + waylandDesc.display = waylandDisplay; + waylandDesc.surface = waylandSurface; + + wgpu::SurfaceDescriptor surfaceDesc = {}; + surfaceDesc.nextInChain = &waylandDesc; + + wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc); + surfaceCreated = true; + } + } +#endif + + if (!surfaceCreated) { +#if defined(MLN_WITH_X11) && defined(MLN_WITH_WAYLAND) + throw std::runtime_error("Failed to get X11 or Wayland window surface from GLFW"); +#elif defined(MLN_WITH_X11) + throw std::runtime_error("Failed to get X11 window surface from GLFW"); +#elif defined(MLN_WITH_WAYLAND) + throw std::runtime_error("Failed to get Wayland window surface from GLFW"); +#else + throw std::runtime_error("WebGPU backend built without window system support"); +#endif + } + + if (wgpuSurface) { + WGPUSurfaceCapabilities capabilities = WGPU_SURFACE_CAPABILITIES_INIT; + if (wgpuSurfaceGetCapabilities(wgpuSurface.Get(), selectedAdapter.Get(), &capabilities) == WGPUStatus_Success) { + const auto pickFormat = [&]() { + const wgpu::TextureFormat preferredFormats[] = { + wgpu::TextureFormat::BGRA8Unorm, + wgpu::TextureFormat::BGRA8UnormSrgb, + wgpu::TextureFormat::RGBA8Unorm, + wgpu::TextureFormat::RGBA8UnormSrgb + }; + for (const auto format : preferredFormats) { + for (size_t i = 0; i < capabilities.formatCount; ++i) { + if (capabilities.formats[i] == static_cast(format)) { + return format; + } + } + } + return capabilities.formatCount > 0 ? static_cast(capabilities.formats[0]) + : swapChainFormat; + }; + swapChainFormat = pickFormat(); + } + wgpuSurfaceCapabilitiesFreeMembers(capabilities); + } + + setColorFormat(swapChainFormat); + + // Get the queue + queue = wgpuDevice.GetQueue(); + + // Configure surface + wgpu::SurfaceConfiguration config = {}; + config.device = wgpuDevice; + config.format = swapChainFormat; + config.usage = wgpu::TextureUsage::RenderAttachment; + config.alphaMode = wgpu::CompositeAlphaMode::Auto; + config.width = width; + config.height = height; + config.presentMode = wgpu::PresentMode::Fifo; + configuredViewFormats[0] = swapChainFormat; + config.viewFormatCount = 1; + config.viewFormats = configuredViewFormats.data(); + + wgpuSurface.Configure(&config); + surfaceConfigured = true; + lastConfiguredSize = {static_cast(width), static_cast(height)}; +#endif + + // Store WebGPU instance, device and queue in the base class + setInstance(instance->Get()); + setDevice(reinterpret_cast(wgpuDevice.Get())); + setQueue(reinterpret_cast(queue.Get())); + + // Make sure the window is visible and focused + glfwShowWindow(window); + glfwFocusWindow(window); + + // Check if window is actually visible + if (glfwGetWindowAttrib(window, GLFW_VISIBLE) != GLFW_TRUE) { + } + + // Make sure window shouldn't close + if (glfwWindowShouldClose(window)) { + } + + // Final delay to ensure everything is ready + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +GLFWWebGPUBackend::~GLFWWebGPUBackend() { + // Set shutdown flag atomically with memory barrier + isShuttingDown.store(true, std::memory_order_release); + + // Mark any in-flight frame as completed so tear-down logic can continue. + frameInProgress.store(false, std::memory_order_release); + + { + std::lock_guard guard(textureStateLock); + + // Release texture views first (they reference textures) + currentTextureView = nullptr; + previousTextureView = nullptr; + depthStencilView = nullptr; + + // Then release textures + currentTexture = nullptr; + previousTexture = nullptr; + depthStencilTexture = nullptr; + } + + // Ensure all GPU work is complete + if (queue) { + // Pump outstanding callbacks so pending GPU work can complete. + processEvents(); + } + + // Unconfigure surface before releasing device + if (wgpuSurface && surfaceConfigured) { + wgpuSurface.Unconfigure(); + surfaceConfigured = false; + } + + // Release WebGPU resources in reverse order of creation + wgpuSurface = nullptr; // Surface depends on device + queue = nullptr; // Queue is owned by device + wgpuDevice = nullptr; // Device depends on instance + + // Release Dawn instance last (it owns the backend) + instance.reset(); +} + +mbgl::gfx::RendererBackend& GLFWWebGPUBackend::getRendererBackend() { + // We inherit from mbgl::webgpu::RendererBackend which is a mbgl::gfx::RendererBackend + return *this; +} + +mbgl::gfx::Renderable& GLFWWebGPUBackend::getDefaultRenderable() { + // We directly inherit from mbgl::gfx::Renderable + return *this; +} + + + +void GLFWWebGPUBackend::swap() { +#if defined(__APPLE__) + @autoreleasepool { +#endif + // Don't do anything if we're shutting down + if (isShuttingDown) { + return; + } + + // Keep track of swap calls for debugging + static int swapCount = 0; + static auto lastSwapTime = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + auto timeSinceLastSwap = std::chrono::duration_cast(now - lastSwapTime).count(); + + // If it's been too long since last swap, tick the device to keep it alive + if (timeSinceLastSwap > 1000) { // 1 second + // Keep Dawn device alive during idle periods + processEvents(); + // Only reconfigure if really needed (> 10 seconds) + if (timeSinceLastSwap > 10000 && surfaceConfigured) { + surfaceNeedsReconfigure = true; + } + } + lastSwapTime = now; + swapCount++; + + // Process any pending device work + processEvents(); + + // Run periodic maintenance + periodicMaintenance(); + + // Note: Command buffer is already submitted in CommandEncoder::present() + // before swap() is called, so we don't need to submit it again here. + + // Wait for any previous frame to complete with longer timeout + // if (!waitForFrame(std::chrono::milliseconds(500))) { + // // Timeout - force frame completion + // signalFrameComplete(); + // } + + // Present the current frame + if (wgpuSurface && surfaceConfigured) { + // Check if surface needs reconfiguration + if (surfaceNeedsReconfigure) { + reconfigureSurface(); + } + + // Only present if we have a valid texture view and haven't presented yet + { + std::lock_guard guard(textureStateLock); + + if (currentTextureView && !framePresented) { + wgpuSurface.Present(); + framePresented = true; + + // Move current resources to previous (keep them alive) + previousTextureView = std::move(currentTextureView); + previousTexture = std::move(currentTexture); + + // Clear current references + currentTextureView = nullptr; + currentTexture = nullptr; + } + } + + // Reset error counter on successful present + consecutiveErrors = 0; + } + + // Signal frame completion + signalFrameComplete(); + + // Poll for window events to keep the window responsive + glfwPollEvents(); +#if defined(__APPLE__) + } +#endif +} + +void GLFWWebGPUBackend::activate() { + // WebGPU doesn't need explicit context activation like OpenGL +} + +void GLFWWebGPUBackend::deactivate() { + // WebGPU doesn't need explicit context deactivation like OpenGL +} + +mbgl::Size GLFWWebGPUBackend::getSize() const { + return size; +} + +void GLFWWebGPUBackend::setSize(mbgl::Size newSize) { + // Update swap chain size if needed + if (size != newSize) { +#ifdef __APPLE__ + int width = 0; + int height = 0; + glfwGetFramebufferSize(window, &width, &height); + if (static_cast(width) != newSize.width || + static_cast(height) != newSize.height) { + NSWindow* nsWindow = glfwGetCocoaWindow(window); + if (nsWindow) { + CAMetalLayer* layer = (CAMetalLayer*)[[nsWindow contentView] layer]; + if (layer) { + layer.drawableSize = CGSizeMake(newSize.width, newSize.height); + } + } + } +#endif + size = newSize; + surfaceNeedsReconfigure = true; + } +} + +void* GLFWWebGPUBackend::getCurrentTextureView() { +#if defined(__APPLE__) + @autoreleasepool { +#endif + // Don't do anything if we're shutting down + if (isShuttingDown) { + return nullptr; + } + + if (!wgpuSurface || !surfaceConfigured) { + return nullptr; + } + + // Process any pending device work before acquiring texture + processEvents(); + + // Run periodic maintenance + periodicMaintenance(); + + // Check error threshold + if (consecutiveErrors >= maxConsecutiveErrors) { + // Too many errors, need to reconfigure + surfaceNeedsReconfigure = true; + reconfigureSurface(); + consecutiveErrors = 0; + } + + std::unique_lock lock(textureStateLock); + + if (currentTextureView && !framePresented) { + return reinterpret_cast(currentTextureView.Get()); + } + + if (framePresented) { + currentTextureView = nullptr; + currentTexture = nullptr; + } + + bool expected = false; + if (!frameInProgress.compare_exchange_strong(expected, true)) { + return nullptr; + } + + framePresented = false; + + lock.unlock(); + + wgpu::SurfaceTexture surfaceTexture; + + try { + wgpuSurface.GetCurrentTexture(&surfaceTexture); + } catch (...) { + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + if (!surfaceTexture.texture) { + surfaceNeedsReconfigure = true; + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + auto status = static_cast(surfaceTexture.status); + if (status == wgpu::SurfaceGetCurrentTextureStatus::Outdated || + status == wgpu::SurfaceGetCurrentTextureStatus::Lost) { + surfaceNeedsReconfigure = true; + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + if (status == wgpu::SurfaceGetCurrentTextureStatus::Timeout || + (status != wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal && + status != wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal)) { + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + lock.lock(); + + // Create texture view with explicit descriptor + wgpu::TextureViewDescriptor viewDesc = {}; + viewDesc.format = swapChainFormat; + viewDesc.dimension = wgpu::TextureViewDimension::e2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = wgpu::TextureAspect::All; + viewDesc.label = "SwapChain TextureView"; + + // Store the texture to keep it alive + currentTexture = surfaceTexture.texture; + + // Create the texture view with validation + wgpu::TextureView newView; + try { + newView = currentTexture.CreateView(&viewDesc); + } catch (...) { + // Handle any exceptions from Dawn + currentTexture = nullptr; + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + // Validate the new view before storing + if (!newView) { + currentTexture = nullptr; + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + // Store the validated view + currentTextureView = newView; + + // Return the created texture view + if (!currentTextureView) { + currentTexture = nullptr; + consecutiveErrors++; + frameInProgress = false; + return nullptr; + } + + // Success - reset error counter + consecutiveErrors = 0; + +#if defined(__APPLE__) + } +#endif + + return reinterpret_cast(currentTextureView.Get()); +} + +mbgl::Size GLFWWebGPUBackend::getFramebufferSize() const { + return getSize(); +} + +void* GLFWWebGPUBackend::getDepthStencilView() { + std::lock_guard guard(textureStateLock); + if (!depthStencilView) { + int width = 0; + int height = 0; + if (window) { + glfwGetFramebufferSize(window, &width, &height); + } + createDepthStencilTexture(static_cast(std::max(width, 0)), + static_cast(std::max(height, 0))); + } + return depthStencilView ? reinterpret_cast(depthStencilView.Get()) : nullptr; +} + +void GLFWWebGPUBackend::reconfigureSurface() { + if (!wgpuSurface || !wgpuDevice || isShuttingDown) { + return; + } + + // Wait for any in-progress frame + waitForFrame(); + + // Don't proceed if we're shutting down after waiting + if (isShuttingDown) { + return; + } + + { + std::lock_guard guard(textureStateLock); + + // Clear all texture state + currentTextureView = nullptr; + currentTexture = nullptr; + previousTextureView = nullptr; + previousTexture = nullptr; + } + + // Get current size + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + // Skip if size hasn't changed and we're already configured + if (surfaceConfigured && + lastConfiguredSize.width == static_cast(width) && + lastConfiguredSize.height == static_cast(height)) { + surfaceNeedsReconfigure = false; + return; + } + + size = {static_cast(std::max(width, 0)), static_cast(std::max(height, 0))}; + + // Configure surface + wgpu::SurfaceConfiguration config = {}; + config.device = wgpuDevice; + config.format = swapChainFormat; + config.usage = wgpu::TextureUsage::RenderAttachment; + config.alphaMode = wgpu::CompositeAlphaMode::Auto; + config.width = width; + config.height = height; + config.presentMode = wgpu::PresentMode::Fifo; + + wgpuSurface.Configure(&config); + + createDepthStencilTexture(static_cast(width), static_cast(height)); + + // Update state + surfaceConfigured = true; + surfaceNeedsReconfigure = false; + lastConfiguredSize = {static_cast(width), static_cast(height)}; + consecutiveErrors = 0; +} + +void GLFWWebGPUBackend::processEvents() { + // TODO(webgpu): hook Dawn event processing once the backend relies on it for + // resource lifecycle management. For now, MapLibre doesn't depend on these + // callbacks and invoking them triggers instability in the native backend. +} + +void GLFWWebGPUBackend::createDepthStencilTexture(uint32_t width, uint32_t height) { + depthStencilView = nullptr; + depthStencilTexture = nullptr; + + if (!wgpuDevice || width == 0 || height == 0 || depthStencilFormat == wgpu::TextureFormat::Undefined) { + return; + } + + wgpu::TextureDescriptor depthDesc = {}; + depthDesc.label = "DepthStencilTexture"; + depthDesc.usage = wgpu::TextureUsage::RenderAttachment; + depthDesc.dimension = wgpu::TextureDimension::e2D; + depthDesc.size = {width, height, 1}; + depthDesc.format = depthStencilFormat; + depthDesc.mipLevelCount = 1; + depthDesc.sampleCount = 1; + + depthStencilTexture = wgpuDevice.CreateTexture(&depthDesc); + if (!depthStencilTexture) { + mbgl::Log::Warning(mbgl::Event::Render, "WebGPU: Failed to create depth/stencil texture"); + depthStencilFormat = wgpu::TextureFormat::Undefined; + setDepthStencilFormat(depthStencilFormat); + return; + } + + wgpu::TextureViewDescriptor viewDesc = {}; + viewDesc.label = "DepthStencilTextureView"; + viewDesc.format = depthDesc.format; + viewDesc.dimension = wgpu::TextureViewDimension::e2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = wgpu::TextureAspect::All; + + depthStencilView = depthStencilTexture.CreateView(&viewDesc); + if (!depthStencilView) { + mbgl::Log::Warning(mbgl::Event::Render, "WebGPU: Failed to create depth/stencil view"); + depthStencilTexture = nullptr; + depthStencilFormat = wgpu::TextureFormat::Undefined; + setDepthStencilFormat(depthStencilFormat); + } +} + +bool GLFWWebGPUBackend::waitForFrame(std::chrono::milliseconds timeout) { + const auto start = std::chrono::steady_clock::now(); + + while (frameInProgress.load(std::memory_order_acquire)) { + if (isShuttingDown.load(std::memory_order_acquire)) { + return true; + } + + processEvents(); + + if (std::chrono::steady_clock::now() - start >= timeout) { + return false; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + return true; +} + +void GLFWWebGPUBackend::signalFrameComplete() { + frameInProgress.store(false, std::memory_order_release); +} + +void GLFWWebGPUBackend::periodicMaintenance() { + // Check shutdown with proper memory ordering + if (isShuttingDown.load(std::memory_order_acquire)) { + return; + } + + static auto lastMaintenanceTime = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + auto timeSinceLastMaintenance = std::chrono::duration_cast(now - lastMaintenanceTime).count(); + + // Run maintenance every 100ms to keep Dawn alive during idle periods + if (timeSinceLastMaintenance > 100) { + // Pump pending callbacks; we intentionally ignore errors here because + // ProcessEvents returns false on device loss and we let the main loop + // handle it on the next swap. + processEvents(); + + lastMaintenanceTime = now; + } +} + +void GLFWWebGPUBackend::ensureDebugTriangleResources() { + if (debugTriangleInitialized || !wgpuDevice || !queue) { + return; + } + + static constexpr char shaderSource[] = R"( +struct VertexOut { + @builtin(position) position : vec4, + @location(0) color : vec3, +}; + +@vertex +fn vs_main(@location(0) position : vec2, + @location(1) color : vec3) -> VertexOut { + var out : VertexOut; + out.position = vec4(position, 0.0, 1.0); + out.color = color; + return out; +} + +@fragment +fn fs_main(input : VertexOut) -> @location(0) vec4 { + return vec4(input.color, 1.0); +} +)"; + + wgpu::ShaderSourceWGSL wgslDesc{}; + wgslDesc.code = shaderSource; + + wgpu::ShaderModuleDescriptor shaderDesc{}; + shaderDesc.nextInChain = &wgslDesc; + shaderDesc.label = "DebugTriangleShader"; + + auto shaderModule = wgpuDevice.CreateShaderModule(&shaderDesc); + if (!shaderModule) { + return; + } + + wgpu::VertexAttribute attributes[2]; + attributes[0].format = wgpu::VertexFormat::Float32x2; + attributes[0].offset = 0; + attributes[0].shaderLocation = 0; + attributes[1].format = wgpu::VertexFormat::Float32x3; + attributes[1].offset = sizeof(float) * 2; + attributes[1].shaderLocation = 1; + + wgpu::VertexBufferLayout vertexLayout{}; + vertexLayout.arrayStride = sizeof(float) * 5; + vertexLayout.attributeCount = 2; + vertexLayout.attributes = attributes; + vertexLayout.stepMode = wgpu::VertexStepMode::Vertex; + + wgpu::VertexState vertexState{}; + vertexState.module = shaderModule; + vertexState.entryPoint = "vs_main"; + vertexState.bufferCount = 1; + vertexState.buffers = &vertexLayout; + + wgpu::ColorTargetState colorTarget{}; + colorTarget.format = swapChainFormat; + colorTarget.writeMask = wgpu::ColorWriteMask::All; + + wgpu::FragmentState fragmentState{}; + fragmentState.module = shaderModule; + fragmentState.entryPoint = "fs_main"; + fragmentState.targetCount = 1; + fragmentState.targets = &colorTarget; + + wgpu::PrimitiveState primitiveState{}; + primitiveState.topology = wgpu::PrimitiveTopology::TriangleList; + primitiveState.stripIndexFormat = wgpu::IndexFormat::Undefined; + primitiveState.frontFace = wgpu::FrontFace::CCW; + primitiveState.cullMode = wgpu::CullMode::None; + + wgpu::MultisampleState multisample{}; + multisample.count = 1; + multisample.mask = ~0u; + multisample.alphaToCoverageEnabled = false; + + wgpu::RenderPipelineDescriptor pipelineDesc{}; + pipelineDesc.label = "DebugTrianglePipeline"; + pipelineDesc.vertex = vertexState; + pipelineDesc.fragment = &fragmentState; + pipelineDesc.primitive = primitiveState; + pipelineDesc.multisample = multisample; + + debugTrianglePipeline = wgpuDevice.CreateRenderPipeline(&pipelineDesc); + if (!debugTrianglePipeline) { + return; + } + + const float vertices[] = { + 0.0f, 0.8f, 1.0f, 0.0f, 0.0f, + -0.8f, -0.8f, 0.0f, 1.0f, 0.0f, + 0.8f, -0.8f, 0.0f, 0.0f, 1.0f, + }; + + debugTriangleVertexBufferSize = sizeof(vertices); + + wgpu::BufferDescriptor bufferDesc{}; + bufferDesc.label = "DebugTriangleVertexBuffer"; + bufferDesc.size = debugTriangleVertexBufferSize; + bufferDesc.usage = wgpu::BufferUsage::Vertex | wgpu::BufferUsage::CopyDst; + + debugTriangleVertexBuffer = wgpuDevice.CreateBuffer(&bufferDesc); + if (!debugTriangleVertexBuffer) { + debugTrianglePipeline = nullptr; + debugTriangleVertexBufferSize = 0; + return; + } + + queue.WriteBuffer(debugTriangleVertexBuffer, 0, vertices, debugTriangleVertexBufferSize); + + debugTriangleInitialized = true; +} + +void GLFWWebGPUBackend::drawDebugTriangle(const wgpu::TextureView& targetView) { + if (!targetView) { + return; + } + + ensureDebugTriangleResources(); + if (!debugTriangleInitialized || !debugTrianglePipeline || !debugTriangleVertexBuffer) { + return; + } + + wgpu::CommandEncoderDescriptor encoderDesc{}; + encoderDesc.label = "DebugTriangleEncoder"; + auto encoder = wgpuDevice.CreateCommandEncoder(&encoderDesc); + if (!encoder) { + return; + } + + wgpu::RenderPassColorAttachment colorAttachment{}; + colorAttachment.view = targetView; + colorAttachment.loadOp = wgpu::LoadOp::Load; + colorAttachment.storeOp = wgpu::StoreOp::Store; + + wgpu::RenderPassDescriptor passDesc{}; + passDesc.label = "DebugTrianglePass"; + passDesc.colorAttachmentCount = 1; + passDesc.colorAttachments = &colorAttachment; + + auto pass = encoder.BeginRenderPass(&passDesc); + pass.SetViewport(0.0f, 0.0f, static_cast(lastConfiguredSize.width), static_cast(lastConfiguredSize.height), 0.0f, 1.0f); + pass.SetScissorRect(0, 0, lastConfiguredSize.width, lastConfiguredSize.height); + pass.SetPipeline(debugTrianglePipeline); + pass.SetVertexBuffer(0, debugTriangleVertexBuffer, 0, debugTriangleVertexBufferSize); + pass.Draw(3); + pass.End(); + + auto commandBuffer = encoder.Finish(); + + queue.Submit(1, &commandBuffer); +} diff --git a/src/mbgl/renderer/buckets/fill_bucket.hpp b/src/mbgl/renderer/buckets/fill_bucket.hpp index 2477eb046..973470d48 100644 --- a/src/mbgl/renderer/buckets/fill_bucket.hpp +++ b/src/mbgl/renderer/buckets/fill_bucket.hpp @@ -13,7 +13,7 @@ MLN_TRIANGULATE_FILL_OUTLINES = 0 : Simple line primitives will be generated. Draw using gfx::Lines MLN_TRIANGULATE_FILL_OUTLINES = 1 : Generate triangulated lines. Draw using gfx::Triangles and a Line shader. */ -#define MLN_TRIANGULATE_FILL_OUTLINES (MLN_RENDER_BACKEND_METAL) +#define MLN_TRIANGULATE_FILL_OUTLINES (MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_WEBGPU) #if MLN_TRIANGULATE_FILL_OUTLINES #include diff --git a/src/mbgl/renderer/layers/line_layer_tweaker.cpp b/src/mbgl/renderer/layers/line_layer_tweaker.cpp index 4e998abd2..55de8eb1e 100644 --- a/src/mbgl/renderer/layers/line_layer_tweaker.cpp +++ b/src/mbgl/renderer/layers/line_layer_tweaker.cpp @@ -138,6 +138,40 @@ void LineLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters : evaluate(parameters), .expressionMask = expressionMask, .pad1 = 0}; +#elif MLN_RENDER_BACKEND_WEBGPU + expressionMask = LineExpressionMask::None; + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::Color; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::Blur; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::Opacity; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::GapWidth; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::Offset; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::Width; + } + if (evaluated.get().isConstant()) { + expressionMask |= LineExpressionMask::FloorWidth; + } + + const LineEvaluatedPropsUBO propsUBO{ + .color = evaluated.get().constantOr(LineColor::defaultValue()), + .blur = evaluated.get().constantOr(LineBlur::defaultValue()), + .opacity = evaluated.get().constantOr(LineOpacity::defaultValue()), + .gapwidth = evaluated.get().constantOr(LineGapWidth::defaultValue()), + .offset = evaluated.get().constantOr(LineOffset::defaultValue()), + .width = evaluated.get().constantOr(LineWidth::defaultValue()), + .floorwidth = evaluated.get().constantOr(LineFloorWidth::defaultValue()), + .expressionMask = expressionMask, + .pad1 = 0}; #else const LineEvaluatedPropsUBO propsUBO{/*color =*/evaluate(parameters), /*blur =*/evaluate(parameters), diff --git a/src/mbgl/renderer/layers/line_layer_tweaker.hpp b/src/mbgl/renderer/layers/line_layer_tweaker.hpp index 511f18ac7..6bdba2e94 100644 --- a/src/mbgl/renderer/layers/line_layer_tweaker.hpp +++ b/src/mbgl/renderer/layers/line_layer_tweaker.hpp @@ -3,9 +3,9 @@ #include #include -#if MLN_RENDER_BACKEND_METAL +#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_WEBGPU #include -#endif // MLN_RENDER_BACKEND_METAL +#endif // MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_WEBGPU #include @@ -68,9 +68,12 @@ protected: #if MLN_RENDER_BACKEND_METAL gfx::UniformBufferPtr expressionUniformBuffer; Unevaluated::GPUExpressions gpuExpressions; - shaders::LineExpressionMask expressionMask = shaders::LineExpressionMask::None; bool gpuExpressionsUpdated = true; #endif // MLN_RENDER_BACKEND_METAL + +#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_WEBGPU + shaders::LineExpressionMask expressionMask = shaders::LineExpressionMask::None; +#endif }; } // namespace mbgl diff --git a/src/mbgl/renderer/layers/render_heatmap_layer.cpp b/src/mbgl/renderer/layers/render_heatmap_layer.cpp index 5f4416415..17d18e0e6 100644 --- a/src/mbgl/renderer/layers/render_heatmap_layer.cpp +++ b/src/mbgl/renderer/layers/render_heatmap_layer.cpp @@ -253,9 +253,29 @@ void RenderHeatmapLayer::update(gfx::ShaderRegistry& shaders, } return true; }; +#if MLN_RENDER_BACKEND_WEBGPU + bool anyUpdated = false; + bool skippedDrawables = false; + tileLayerGroup->visitDrawables(renderPass, tileID, [&](gfx::Drawable& drawable) { + if (updateExisting(drawable)) { + anyUpdated = true; + } else { + skippedDrawables = true; + } + }); + + if (!anyUpdated && skippedDrawables) { + removeTile(renderPass, tileID); + } + + if (anyUpdated) { + continue; + } +#else if (updateTile(renderPass, tileID, std::move(updateExisting))) { continue; } +#endif if (!propertiesAsUniforms) { propertiesAsUniforms.emplace(); diff --git a/src/mbgl/renderer/paint_parameters.cpp b/src/mbgl/renderer/paint_parameters.cpp index 4f29d2932..432bb052f 100644 --- a/src/mbgl/renderer/paint_parameters.cpp +++ b/src/mbgl/renderer/paint_parameters.cpp @@ -15,6 +15,11 @@ #include #endif +#if MLN_RENDER_BACKEND_WEBGPU +#include +#include +#endif + #if MLN_RENDER_BACKEND_METAL #include #include @@ -155,6 +160,9 @@ void PaintParameters::clearStencil() { const auto& vulkanRenderPass = static_cast(*renderPass); vulkanRenderPass.clearStencil(); + context.renderingStats().stencilClears++; +#elif MLN_RENDER_BACKEND_WEBGPU + // WebGPU clears stencil through render pass descriptor context.renderingStats().stencilClears++; #elif MLN_RENDER_BACKEND_OPENGL context.clearStencilBuffer(0b00000000); @@ -176,7 +184,40 @@ void PaintParameters::renderTileClippingMasks(const RenderTiles& renderTiles) { clearStencil(); } -#if MLN_RENDER_BACKEND_METAL +#if MLN_RENDER_BACKEND_WEBGPU + std::vector tileUBOs; + for (const auto& tileRef : *renderTiles) { + const auto& tileID = tileRef.get().id; + + const int32_t stencilID = nextStencilID; + const auto result = tileClippingMaskIDs.insert(std::make_pair(tileID, stencilID)); + if (result.second) { + nextStencilID++; + } else { + continue; + } + + if (tileUBOs.empty()) { + tileUBOs.reserve(count); + } + + tileUBOs.emplace_back(shaders::ClipUBO{/* .matrix = */ util::cast(matrixForTile(tileID)), + /* .stencil_ref = */ static_cast(stencilID), + /* .pad1 = */ 0, + /* .pad2 = */ 0, + /* .pad3 = */ 0}); + } + + if (!tileUBOs.empty()) { +#if !defined(NDEBUG) + const auto debugGroup = renderPass->createDebugGroup("tile-clip-masks"); +#endif + + auto& webgpuContext = static_cast(context); + webgpuContext.renderTileClippingMasks(*renderPass, staticData, tileUBOs); + webgpuContext.renderingStats().stencilUpdates++; + } +#elif MLN_RENDER_BACKEND_METAL // Assign a stencil ID and build a UBO for each tile in the set std::vector tileUBOs; for (const auto& tileRef : *renderTiles) { diff --git a/src/mbgl/renderer/sources/render_tile_source.cpp b/src/mbgl/renderer/sources/render_tile_source.cpp index 408bcf220..adebf834d 100644 --- a/src/mbgl/renderer/sources/render_tile_source.cpp +++ b/src/mbgl/renderer/sources/render_tile_source.cpp @@ -26,7 +26,7 @@ #include #include -#if MLN_RENDER_BACKEND_METAL || (MLN_RENDER_BACKEND_VULKAN && defined(__ANDROID__)) +#if MLN_RENDER_BACKEND_METAL || MLN_RENDER_BACKEND_WEBGPU || (MLN_RENDER_BACKEND_VULKAN && defined(__ANDROID__)) #define MLN_ENABLE_POLYLINE_DRAWABLES 1 #else #define MLN_ENABLE_POLYLINE_DRAWABLES 0 diff --git a/src/mbgl/shaders/webgpu/background.cpp b/src/mbgl/shaders/webgpu/background.cpp new file mode 100644 index 000000000..e0badbc73 --- /dev/null +++ b/src/mbgl/shaders/webgpu/background.cpp @@ -0,0 +1,29 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// +// Background + +using BackgroundShaderSource = ShaderSource; + +const std::array BackgroundShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idBackgroundPosVertexAttribute}, +}; +const std::array BackgroundShaderSource::textures = {}; + +// +// Background pattern + +using BackgroundPatternShaderSource = ShaderSource; + +const std::array BackgroundPatternShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idBackgroundPosVertexAttribute}, +}; +const std::array BackgroundPatternShaderSource::textures = {TextureInfo{0, idBackgroundImageTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/circle.cpp b/src/mbgl/shaders/webgpu/circle.cpp new file mode 100644 index 000000000..c2b38e93f --- /dev/null +++ b/src/mbgl/shaders/webgpu/circle.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using CircleShaderSource = ShaderSource; + +const std::array CircleShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idCirclePosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Float4, idCircleColorVertexAttribute}, + AttributeInfo{2, gfx::AttributeDataType::Float2, idCircleRadiusVertexAttribute}, + AttributeInfo{3, gfx::AttributeDataType::Float2, idCircleBlurVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Float2, idCircleOpacityVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Float4, idCircleStrokeColorVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idCircleStrokeWidthVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idCircleStrokeOpacityVertexAttribute}, +}; +const std::array CircleShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/clipping_mask.cpp b/src/mbgl/shaders/webgpu/clipping_mask.cpp new file mode 100644 index 000000000..7d110e800 --- /dev/null +++ b/src/mbgl/shaders/webgpu/clipping_mask.cpp @@ -0,0 +1,15 @@ +#include +#include + +namespace mbgl { +namespace shaders { + +using ClippingMaskShaderSource = ShaderSource; + +const std::array ClippingMaskShaderSource::attributes = { + AttributeInfo{2, gfx::AttributeDataType::Short2, idClippingMaskPosVertexAttribute}, +}; +const std::array ClippingMaskShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/collision.cpp b/src/mbgl/shaders/webgpu/collision.cpp new file mode 100644 index 000000000..284789aa7 --- /dev/null +++ b/src/mbgl/shaders/webgpu/collision.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// +// Collision box + +using CollisionBoxShaderSource = ShaderSource; + +const std::array CollisionBoxShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idCollisionPosVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Short2, idCollisionAnchorPosVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Short2, idCollisionExtrudeVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::UShort2, idCollisionPlacedVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idCollisionShiftVertexAttribute}, +}; +const std::array CollisionBoxShaderSource::textures = {}; + +// +// Collision circle + +using CollisionCircleShaderSource = ShaderSource; + +const std::array CollisionCircleShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idCollisionPosVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Short2, idCollisionAnchorPosVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Short2, idCollisionExtrudeVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::UShort2, idCollisionPlacedVertexAttribute}, +}; +const std::array CollisionCircleShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/custom_geometry.cpp b/src/mbgl/shaders/webgpu/custom_geometry.cpp new file mode 100644 index 000000000..c4aa476a0 --- /dev/null +++ b/src/mbgl/shaders/webgpu/custom_geometry.cpp @@ -0,0 +1,19 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using CustomGeometryShaderSource = ShaderSource; + +const std::array CustomGeometryShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Float3, idCustomGeometryPosVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Float2, idCustomGeometryTexVertexAttribute}, +}; +const std::array CustomGeometryShaderSource::textures = { + TextureInfo{0, idCustomGeometryTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/custom_symbol_icon.cpp b/src/mbgl/shaders/webgpu/custom_symbol_icon.cpp new file mode 100644 index 000000000..4d63ebb4c --- /dev/null +++ b/src/mbgl/shaders/webgpu/custom_symbol_icon.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using CustomSymbolIconShaderSource = ShaderSource; + +const std::array CustomSymbolIconShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Float3, idCustomSymbolPosVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Float2, idCustomSymbolTexVertexAttribute}, +}; +const std::array CustomSymbolIconShaderSource::textures = {TextureInfo{0, idCustomSymbolImageTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/debug.cpp b/src/mbgl/shaders/webgpu/debug.cpp new file mode 100644 index 000000000..433cd53a5 --- /dev/null +++ b/src/mbgl/shaders/webgpu/debug.cpp @@ -0,0 +1,16 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using DebugShaderSource = ShaderSource; + +const std::array DebugShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Short2, idDebugPosVertexAttribute}, +}; +const std::array DebugShaderSource::textures = {TextureInfo{0, idDebugOverlayTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/fill.cpp b/src/mbgl/shaders/webgpu/fill.cpp new file mode 100644 index 000000000..4de790bbc --- /dev/null +++ b/src/mbgl/shaders/webgpu/fill.cpp @@ -0,0 +1,71 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// +// Fill + +using FillShaderSource = ShaderSource; + +const std::array FillShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idFillPosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Float4, idFillColorVertexAttribute}, + AttributeInfo{2, gfx::AttributeDataType::Float2, idFillOpacityVertexAttribute}, +}; +const std::array FillShaderSource::textures = {}; + +// +// Fill outline + +using FillOutlineShaderSource = ShaderSource; + +const std::array FillOutlineShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idFillPosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Float4, idFillOutlineColorVertexAttribute}, + AttributeInfo{2, gfx::AttributeDataType::Float2, idFillOpacityVertexAttribute}, +}; +const std::array FillOutlineShaderSource::textures = {}; + +// +// Fill pattern + +using FillPatternShaderSource = ShaderSource; + +const std::array FillPatternShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idFillPosVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UShort4, idFillPatternFromVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::UShort4, idFillPatternToVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idFillOpacityVertexAttribute}, +}; +const std::array FillPatternShaderSource::textures = {TextureInfo{0, idFillImageTexture}}; + +// +// Fill outline pattern + +using FillOutlinePatternShaderSource = ShaderSource; + +const std::array FillOutlinePatternShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idFillPosVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UShort4, idFillPatternFromVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::UShort4, idFillPatternToVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idFillOpacityVertexAttribute}, +}; +const std::array FillOutlinePatternShaderSource::textures = {TextureInfo{0, idFillImageTexture}}; + +// +// Fill outline triangulated + +using FillOutlineTriangulatedShaderSource = + ShaderSource; + +const std::array FillOutlineTriangulatedShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idLinePosNormalVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::UByte4, idLineDataVertexAttribute}, +}; +const std::array FillOutlineTriangulatedShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/fill_extrusion.cpp b/src/mbgl/shaders/webgpu/fill_extrusion.cpp new file mode 100644 index 000000000..7b156a6bc --- /dev/null +++ b/src/mbgl/shaders/webgpu/fill_extrusion.cpp @@ -0,0 +1,40 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// +// Fill extrusion + +using FillExtrusionShaderSource = ShaderSource; + +const std::array FillExtrusionShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idFillExtrusionPosVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Short4, idFillExtrusionNormalEdVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Float4, idFillExtrusionColorVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idFillExtrusionBaseVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idFillExtrusionHeightVertexAttribute}, +}; +const std::array FillExtrusionShaderSource::textures = {}; + +// +// Fill extrusion pattern + +using FillExtrusionPatternShaderSource = ShaderSource; + +const std::array FillExtrusionPatternShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short2, idFillExtrusionPosVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::Short4, idFillExtrusionNormalEdVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Float2, idFillExtrusionBaseVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idFillExtrusionHeightVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::UShort4, idFillExtrusionPatternFromVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::UShort4, idFillExtrusionPatternToVertexAttribute}, +}; +const std::array FillExtrusionPatternShaderSource::textures = { + TextureInfo{0, idFillExtrusionImageTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/heatmap.cpp b/src/mbgl/shaders/webgpu/heatmap.cpp new file mode 100644 index 000000000..ae716902a --- /dev/null +++ b/src/mbgl/shaders/webgpu/heatmap.cpp @@ -0,0 +1,18 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using HeatmapShaderSource = ShaderSource; + +const std::array HeatmapShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idHeatmapPosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Float2, idHeatmapWeightVertexAttribute}, + AttributeInfo{2, gfx::AttributeDataType::Float2, idHeatmapRadiusVertexAttribute}, +}; +const std::array HeatmapShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/heatmap_texture.cpp b/src/mbgl/shaders/webgpu/heatmap_texture.cpp new file mode 100644 index 000000000..1a066d546 --- /dev/null +++ b/src/mbgl/shaders/webgpu/heatmap_texture.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using HeatmapTextureShaderSource = ShaderSource; + +const std::array HeatmapTextureShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Short2, idHeatmapPosVertexAttribute}, +}; +const std::array HeatmapTextureShaderSource::textures = {TextureInfo{0, idHeatmapImageTexture}, + TextureInfo{1, idHeatmapColorRampTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/hillshade.cpp b/src/mbgl/shaders/webgpu/hillshade.cpp new file mode 100644 index 000000000..ea2de8b9b --- /dev/null +++ b/src/mbgl/shaders/webgpu/hillshade.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using HillshadeShaderSource = ShaderSource; + +const std::array HillshadeShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Short2, idHillshadePosVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Short2, idHillshadeTexturePosVertexAttribute}, +}; +const std::array HillshadeShaderSource::textures = {TextureInfo{0, idHillshadeImageTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/hillshade_prepare.cpp b/src/mbgl/shaders/webgpu/hillshade_prepare.cpp new file mode 100644 index 000000000..f392f06b5 --- /dev/null +++ b/src/mbgl/shaders/webgpu/hillshade_prepare.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using HillshadePrepareShaderSource = ShaderSource; + +const std::array HillshadePrepareShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Short2, idHillshadePosVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Short2, idHillshadeTexturePosVertexAttribute}, +}; +const std::array HillshadePrepareShaderSource::textures = {TextureInfo{0, idHillshadeImageTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/line.cpp b/src/mbgl/shaders/webgpu/line.cpp new file mode 100644 index 000000000..76d3a7936 --- /dev/null +++ b/src/mbgl/shaders/webgpu/line.cpp @@ -0,0 +1,69 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using LineShaderSource = ShaderSource; + +const std::array LineShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idLinePosNormalVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UByte4, idLineDataVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float4, idLineColorVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idLineBlurVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float2, idLineOpacityVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float2, idLineGapWidthVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float2, idLineOffsetVertexAttribute}, + AttributeInfo{11, gfx::AttributeDataType::Float2, idLineWidthVertexAttribute}, +}; +const std::array LineShaderSource::textures = {}; + +// Line Gradient +using LineGradientShaderSource = ShaderSource; + +const std::array LineGradientShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idLinePosNormalVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UByte4, idLineDataVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idLineBlurVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idLineOpacityVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float2, idLineGapWidthVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float2, idLineOffsetVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float2, idLineWidthVertexAttribute}, +}; +const std::array LineGradientShaderSource::textures = {TextureInfo{0, idLineImageTexture}}; + +// Line Pattern +using LinePatternShaderSource = ShaderSource; + +const std::array LinePatternShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idLinePosNormalVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UByte4, idLineDataVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idLineBlurVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idLineOpacityVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float2, idLineGapWidthVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float2, idLineOffsetVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float2, idLineWidthVertexAttribute}, + AttributeInfo{11, gfx::AttributeDataType::UShort4, idLinePatternFromVertexAttribute}, + AttributeInfo{12, gfx::AttributeDataType::UShort4, idLinePatternToVertexAttribute}, +}; +const std::array LinePatternShaderSource::textures = {TextureInfo{0, idLineImageTexture}}; + +// Line SDF +using LineSDFShaderSource = ShaderSource; + +const std::array LineSDFShaderSource::attributes = { + AttributeInfo{4, gfx::AttributeDataType::Short2, idLinePosNormalVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::UByte4, idLineDataVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float4, idLineColorVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float2, idLineBlurVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float2, idLineOpacityVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float2, idLineGapWidthVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float2, idLineOffsetVertexAttribute}, + AttributeInfo{11, gfx::AttributeDataType::Float2, idLineWidthVertexAttribute}, + AttributeInfo{12, gfx::AttributeDataType::Float2, idLineFloorWidthVertexAttribute}, +}; +const std::array LineSDFShaderSource::textures = {TextureInfo{0, idLineImageTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/location_indicator.cpp b/src/mbgl/shaders/webgpu/location_indicator.cpp new file mode 100644 index 000000000..f171a4096 --- /dev/null +++ b/src/mbgl/shaders/webgpu/location_indicator.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// +// Location indicator + +using LocationIndicatorShaderSource = ShaderSource; + +const std::array LocationIndicatorShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Float2, idLocationIndicatorPosVertexAttribute}, +}; +const std::array LocationIndicatorShaderSource::textures = {}; + +// +// Location indicator textured + +using LocationIndicatorTexturedShaderSource = + ShaderSource; + +const std::array LocationIndicatorTexturedShaderSource::attributes = { + AttributeInfo{5, gfx::AttributeDataType::Float2, idLocationIndicatorPosVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float2, idLocationIndicatorTexVertexAttribute}, +}; +const std::array LocationIndicatorTexturedShaderSource::textures = { + TextureInfo{0, idLocationIndicatorTexture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/raster.cpp b/src/mbgl/shaders/webgpu/raster.cpp new file mode 100644 index 000000000..423cb487e --- /dev/null +++ b/src/mbgl/shaders/webgpu/raster.cpp @@ -0,0 +1,18 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using RasterShaderSource = ShaderSource; + +const std::array RasterShaderSource::attributes = { + AttributeInfo{0, gfx::AttributeDataType::Short2, idRasterPosVertexAttribute}, + AttributeInfo{1, gfx::AttributeDataType::Short2, idRasterTexturePosVertexAttribute}, +}; +const std::array RasterShaderSource::textures = {TextureInfo{0, idRasterImage0Texture}, + TextureInfo{1, idRasterImage1Texture}}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/shader_program.cpp b/src/mbgl/shaders/webgpu/shader_program.cpp new file mode 100644 index 000000000..fda5861c4 --- /dev/null +++ b/src/mbgl/shaders/webgpu/shader_program.cpp @@ -0,0 +1,797 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +namespace { +std::string trimString(std::string_view value) { + auto begin = value.begin(); + auto end = value.end(); + while (begin != end && std::isspace(static_cast(*begin))) { + ++begin; + } + while (begin != end) { + auto prev = end - 1; + if (!std::isspace(static_cast(*prev))) { + break; + } + end = prev; + } + return std::string(begin, end); +} + +WGPUBlendOperation webgpuBlendOperation(const gfx::ColorBlendEquationType& equation) { + switch (equation) { + case gfx::ColorBlendEquationType::Add: + return WGPUBlendOperation_Add; + case gfx::ColorBlendEquationType::Subtract: + return WGPUBlendOperation_Subtract; + case gfx::ColorBlendEquationType::ReverseSubtract: + return WGPUBlendOperation_ReverseSubtract; + default: + return WGPUBlendOperation_Add; + } +} + +WGPUBlendFactor webgpuBlendFactor(const gfx::ColorBlendFactorType& factor) { + switch (factor) { + case gfx::ColorBlendFactorType::Zero: + return WGPUBlendFactor_Zero; + case gfx::ColorBlendFactorType::One: + return WGPUBlendFactor_One; + case gfx::ColorBlendFactorType::SrcColor: + return WGPUBlendFactor_Src; + case gfx::ColorBlendFactorType::OneMinusSrcColor: + return WGPUBlendFactor_OneMinusSrc; + case gfx::ColorBlendFactorType::SrcAlpha: + return WGPUBlendFactor_SrcAlpha; + case gfx::ColorBlendFactorType::OneMinusSrcAlpha: + return WGPUBlendFactor_OneMinusSrcAlpha; + case gfx::ColorBlendFactorType::DstAlpha: + return WGPUBlendFactor_DstAlpha; + case gfx::ColorBlendFactorType::OneMinusDstAlpha: + return WGPUBlendFactor_OneMinusDstAlpha; + case gfx::ColorBlendFactorType::DstColor: + return WGPUBlendFactor_Dst; + case gfx::ColorBlendFactorType::OneMinusDstColor: + return WGPUBlendFactor_OneMinusDst; + case gfx::ColorBlendFactorType::SrcAlphaSaturate: + return WGPUBlendFactor_SrcAlphaSaturated; + case gfx::ColorBlendFactorType::ConstantColor: + return WGPUBlendFactor_Constant; + case gfx::ColorBlendFactorType::OneMinusConstantColor: + return WGPUBlendFactor_OneMinusConstant; + case gfx::ColorBlendFactorType::ConstantAlpha: + return WGPUBlendFactor_Constant; + case gfx::ColorBlendFactorType::OneMinusConstantAlpha: + return WGPUBlendFactor_OneMinusConstant; + default: + return WGPUBlendFactor_One; + } +} + +WGPUPrimitiveTopology toPrimitiveTopology(gfx::DrawModeType drawModeType) { + switch (drawModeType) { + case gfx::DrawModeType::Points: + return WGPUPrimitiveTopology_PointList; + case gfx::DrawModeType::Lines: + case gfx::DrawModeType::LineLoop: + return WGPUPrimitiveTopology_LineList; + case gfx::DrawModeType::LineStrip: + return WGPUPrimitiveTopology_LineStrip; + case gfx::DrawModeType::TriangleStrip: + return WGPUPrimitiveTopology_TriangleStrip; + case gfx::DrawModeType::TriangleFan: + // WebGPU does not support triangle fan directly; approximate with triangle list + return WGPUPrimitiveTopology_TriangleList; + case gfx::DrawModeType::Triangles: + default: + return WGPUPrimitiveTopology_TriangleList; + } +} + +WGPUCompareFunction toCompareFunction(const gfx::DepthFunctionType func) { + switch (func) { + case gfx::DepthFunctionType::Never: + return WGPUCompareFunction_Never; + case gfx::DepthFunctionType::Less: + return WGPUCompareFunction_Less; + case gfx::DepthFunctionType::Equal: + return WGPUCompareFunction_Equal; + case gfx::DepthFunctionType::LessEqual: + return WGPUCompareFunction_LessEqual; + case gfx::DepthFunctionType::Greater: + return WGPUCompareFunction_Greater; + case gfx::DepthFunctionType::NotEqual: + return WGPUCompareFunction_NotEqual; + case gfx::DepthFunctionType::GreaterEqual: + return WGPUCompareFunction_GreaterEqual; + case gfx::DepthFunctionType::Always: + default: + return WGPUCompareFunction_Always; + } +} + +WGPUCompareFunction toCompareFunction(const gfx::StencilFunctionType func) { + switch (func) { + case gfx::StencilFunctionType::Never: + return WGPUCompareFunction_Never; + case gfx::StencilFunctionType::Less: + return WGPUCompareFunction_Less; + case gfx::StencilFunctionType::Equal: + return WGPUCompareFunction_Equal; + case gfx::StencilFunctionType::LessEqual: + return WGPUCompareFunction_LessEqual; + case gfx::StencilFunctionType::Greater: + return WGPUCompareFunction_Greater; + case gfx::StencilFunctionType::NotEqual: + return WGPUCompareFunction_NotEqual; + case gfx::StencilFunctionType::GreaterEqual: + return WGPUCompareFunction_GreaterEqual; + case gfx::StencilFunctionType::Always: + default: + return WGPUCompareFunction_Always; + } +} + +WGPUStencilOperation toStencilOp(const gfx::StencilOpType op) { + switch (op) { + case gfx::StencilOpType::Zero: + return WGPUStencilOperation_Zero; + case gfx::StencilOpType::Keep: + return WGPUStencilOperation_Keep; + case gfx::StencilOpType::Replace: + return WGPUStencilOperation_Replace; + case gfx::StencilOpType::Increment: + return WGPUStencilOperation_IncrementClamp; + case gfx::StencilOpType::Decrement: + return WGPUStencilOperation_DecrementClamp; + case gfx::StencilOpType::Invert: + return WGPUStencilOperation_Invert; + case gfx::StencilOpType::IncrementWrap: + return WGPUStencilOperation_IncrementWrap; + case gfx::StencilOpType::DecrementWrap: + return WGPUStencilOperation_DecrementWrap; + default: + return WGPUStencilOperation_Keep; + } +} +} // namespace + +ShaderProgram::ShaderProgram(std::string name, + RendererBackend& backend_, + WGPUShaderModule vertexModule, + WGPUShaderModule fragmentModule) + : ShaderProgramBase(), + shaderName(std::move(name)), + backend(backend_), + vertexShaderModule(vertexModule), + fragmentShaderModule(fragmentModule) { + createPipelineLayout({}, {}); +} + +// Minimal constructor for Context::createShader compatibility +ShaderProgram::ShaderProgram(Context& context, const std::string& vertexSource, const std::string& fragmentSource) + : ShaderProgramBase(), + shaderName("ContextShader"), + backend(static_cast(context.getBackend())) { + WGPUDevice device = static_cast(backend.getDevice()); + if (!device) return; + + std::string trimmedVertex = trimString(vertexSource); + std::string trimmedFragment = trimString(fragmentSource); + hasVertexEntryPoint = !trimmedVertex.empty(); + hasFragmentEntryPoint = !trimmedFragment.empty(); + + // Get the prelude from common.hpp + using PreludeShader = shaders::ShaderSource; + + std::string vertexWithPrelude; + if (hasVertexEntryPoint) { + vertexWithPrelude = std::string(PreludeShader::prelude) + "\n" + vertexSource; + WGPUShaderSourceWGSL wgslDesc = {}; + wgslDesc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgslDesc.chain.next = nullptr; + WGPUStringView vertexCode = {vertexWithPrelude.c_str(), vertexWithPrelude.length()}; + wgslDesc.code = vertexCode; + + WGPUShaderModuleDescriptor vertexShaderDesc = {}; + WGPUStringView vertexLabel = {"Vertex Shader Module", strlen("Vertex Shader Module")}; + vertexShaderDesc.label = vertexLabel; + vertexShaderDesc.nextInChain = (WGPUChainedStruct*)&wgslDesc; + + vertexShaderModule = wgpuDeviceCreateShaderModule(device, &vertexShaderDesc); + if (!vertexShaderModule) { + Log::Error(Event::Render, "Failed to create vertex shader module for " + shaderName); + Log::Error(Event::Render, "Vertex shader source length: " + std::to_string(vertexWithPrelude.length())); + if (shaderName.find("CircleShader") != std::string::npos) { + std::ofstream dump("/tmp/circle_vertex.wgsl", std::ios::trunc); + dump << vertexSource; + } + } + } + + std::string fragmentWithPrelude; + if (hasFragmentEntryPoint) { + fragmentWithPrelude = std::string(PreludeShader::prelude) + "\n" + fragmentSource; + + WGPUShaderSourceWGSL wgslDesc = {}; + wgslDesc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgslDesc.chain.next = nullptr; + WGPUStringView fragmentCode = {fragmentWithPrelude.c_str(), fragmentWithPrelude.length()}; + wgslDesc.code = fragmentCode; + + WGPUShaderModuleDescriptor fragmentShaderDesc = {}; + WGPUStringView fragmentLabel = {"Fragment Shader Module", strlen("Fragment Shader Module")}; + fragmentShaderDesc.label = fragmentLabel; + fragmentShaderDesc.nextInChain = (WGPUChainedStruct*)&wgslDesc; + + fragmentShaderModule = wgpuDeviceCreateShaderModule(device, &fragmentShaderDesc); + if (!fragmentShaderModule) { + Log::Error(Event::Render, "Failed to create fragment shader module for " + shaderName); + Log::Error(Event::Render, "Fragment shader source length: " + std::to_string(fragmentWithPrelude.length())); + if (shaderName.find("CircleShader") != std::string::npos) { + std::ofstream dump("/tmp/circle_fragment.wgsl", std::ios::trunc); + dump << fragmentSource; + } + } + } + + createPipelineLayout(vertexWithPrelude, fragmentWithPrelude); +} + +ShaderProgram::~ShaderProgram() { + // Release cached pipelines + for (auto& pair : renderPipelineCache) { + if (pair.second) { + wgpuRenderPipelineRelease(pair.second); + } + } + renderPipelineCache.clear(); + + // Release resources + if (pipelineLayout) { + wgpuPipelineLayoutRelease(pipelineLayout); + pipelineLayout = nullptr; + } + + for (auto layout : bindGroupLayouts) { + if (layout) { + wgpuBindGroupLayoutRelease(layout); + } + } + bindGroupLayouts.clear(); + bindGroupOrder.clear(); + bindingsPerGroup.clear(); + + // Release shader modules if we created them + if (vertexShaderModule) { + wgpuShaderModuleRelease(vertexShaderModule); + vertexShaderModule = nullptr; + } + + if (fragmentShaderModule) { + wgpuShaderModuleRelease(fragmentShaderModule); + fragmentShaderModule = nullptr; + } +} + +WGPURenderPipeline ShaderProgram::getRenderPipeline(const gfx::Renderable& renderable, + const WGPUVertexBufferLayout* vertexLayouts, + uint32_t vertexLayoutCount, + const gfx::ColorMode& colorMode, + const gfx::DepthMode& depthMode, + const gfx::StencilMode& stencilMode, + gfx::DrawModeType drawModeType, + const std::optional reuseHash) { + (void)renderable; + // Check cache first + if (reuseHash.has_value()) { + auto it = renderPipelineCache.find(reuseHash.value()); + if (it != renderPipelineCache.end()) { + return it->second; + } + } + + // Create new pipeline + WGPURenderPipeline pipeline = createPipeline( + vertexLayouts, vertexLayoutCount, colorMode, depthMode, stencilMode, drawModeType); + + // Cache the pipeline if we have a reuse hash + if (reuseHash.has_value() && pipeline) { + renderPipelineCache[reuseHash.value()] = pipeline; + } + + return pipeline; +} + +std::optional ShaderProgram::getSamplerLocation(const size_t id) const { + return (id < textureBindings.size()) ? textureBindings[id] : std::nullopt; +} + +void ShaderProgram::initAttribute(const shaders::AttributeInfo& info) { + const auto index = static_cast(info.index); +#if !defined(NDEBUG) + // Indexes must be unique, if there's a conflict check the `attributes` array in the shader + vertexAttributes.visitAttributes([&](const gfx::VertexAttribute& attrib) { assert(attrib.getIndex() != index); }); + instanceAttributes.visitAttributes([&](const gfx::VertexAttribute& attrib) { assert(attrib.getIndex() != index); }); +#endif + vertexAttributes.set(info.id, index, info.dataType, 1); +} + +void ShaderProgram::initInstanceAttribute(const shaders::AttributeInfo& info) { + // Index is the block index of the instance attribute + const auto index = static_cast(info.index); +#if !defined(NDEBUG) + // Indexes must not be reused by regular attributes or uniform blocks + // More than one instance attribute can have the same index, if they share the block + vertexAttributes.visitAttributes([&](const gfx::VertexAttribute& attrib) { assert(attrib.getIndex() != index); }); +#endif + instanceAttributes.set(info.id, index, info.dataType, 1); +} + +void ShaderProgram::initTexture(const shaders::TextureInfo& info) { + assert(info.id < textureBindings.size()); + if (info.id >= textureBindings.size()) { + return; + } + textureBindings[info.id] = info.index; +} + +const std::vector& ShaderProgram::getBindingInfosForGroup(uint32_t group) const { + for (size_t i = 0; i < bindGroupOrder.size(); ++i) { + if (bindGroupOrder[i] == group) { + return bindingsPerGroup[i]; + } + } + static const std::vector empty; + return empty; +} + +WGPUBindGroupLayout ShaderProgram::getBindGroupLayout(uint32_t group) const { + for (size_t i = 0; i < bindGroupOrder.size(); ++i) { + if (bindGroupOrder[i] == group) { + return bindGroupLayouts[i]; + } + } + return nullptr; +} + +void ShaderProgram::createPipelineLayout(const std::string& vertexSource, const std::string& fragmentSource) { + WGPUDevice device = static_cast(backend.getDevice()); + + if (!device) { + return; + } + + if (!vertexSource.empty() || !fragmentSource.empty()) { + bindingInfos.clear(); + if (!vertexSource.empty()) { + analyzeShaderBindings(vertexSource, WGPUShaderStage_Vertex); + } + if (!fragmentSource.empty()) { + analyzeShaderBindings(fragmentSource, WGPUShaderStage_Fragment); + } + } + + rebuildBindGroupLayouts(); + + if (pipelineLayout) { + wgpuPipelineLayoutRelease(pipelineLayout); + pipelineLayout = nullptr; + } + + std::vector layoutHandles; + layoutHandles.reserve(bindGroupLayouts.size()); + for (const auto& layout : bindGroupLayouts) { + layoutHandles.push_back(layout); + } + + WGPUPipelineLayoutDescriptor pipelineLayoutDesc = {}; + WGPUStringView pipelineLayoutLabel = {"Pipeline Layout", strlen("Pipeline Layout")}; + pipelineLayoutDesc.label = pipelineLayoutLabel; + pipelineLayoutDesc.bindGroupLayoutCount = layoutHandles.size(); + pipelineLayoutDesc.bindGroupLayouts = layoutHandles.empty() ? nullptr : layoutHandles.data(); + + pipelineLayout = wgpuDeviceCreatePipelineLayout(device, &pipelineLayoutDesc); +} + +void ShaderProgram::analyzeShaderBindings(const std::string& source, WGPUShaderStage stage) { + size_t searchPos = 0; + while (true) { + size_t groupToken = source.find("@group", searchPos); + if (groupToken == std::string::npos) { + break; + } + size_t groupParen = source.find('(', groupToken); + if (groupParen == std::string::npos) { + break; + } + size_t groupStart = groupParen + 1; + while (groupStart < source.size() && std::isspace(static_cast(source[groupStart]))) { + ++groupStart; + } + size_t groupEnd = groupStart; + while (groupEnd < source.size() && std::isdigit(static_cast(source[groupEnd]))) { + ++groupEnd; + } + if (groupEnd == groupStart) { + searchPos = groupEnd; + continue; + } + uint32_t group = static_cast( + std::strtoul(source.substr(groupStart, groupEnd - groupStart).c_str(), nullptr, 10)); + + size_t bindingToken = source.find("@binding", groupEnd); + if (bindingToken == std::string::npos) { + break; + } + size_t bindingParen = source.find('(', bindingToken); + if (bindingParen == std::string::npos) { + break; + } + size_t bindingStart = bindingParen + 1; + while (bindingStart < source.size() && std::isspace(static_cast(source[bindingStart]))) { + ++bindingStart; + } + size_t bindingEnd = bindingStart; + while (bindingEnd < source.size() && std::isdigit(static_cast(source[bindingEnd]))) { + ++bindingEnd; + } + if (bindingEnd == bindingStart) { + searchPos = bindingEnd; + continue; + } + uint32_t binding = static_cast( + std::strtoul(source.substr(bindingStart, bindingEnd - bindingStart).c_str(), nullptr, 10)); + + size_t varPos = source.find("var", bindingEnd); + if (varPos == std::string::npos) { + break; + } + if (varPos > bindingEnd && std::isalpha(static_cast(source[varPos - 1]))) { + searchPos = varPos + 3; + continue; + } + + size_t afterVar = varPos + 3; + while (afterVar < source.size() && std::isspace(static_cast(source[afterVar]))) { + ++afterVar; + } + + BindingType type = BindingType::UniformBuffer; + if (afterVar < source.size() && source[afterVar] == '<') { + size_t qualifierEnd = source.find('>', afterVar); + if (qualifierEnd == std::string::npos) { + break; + } + const auto qualifier = trimString( + std::string_view(source.data() + afterVar + 1, qualifierEnd - afterVar - 1)); + if (qualifier.find("storage") != std::string::npos) { + if (qualifier.find("read_write") != std::string::npos) { + type = BindingType::StorageBuffer; + } else { + type = BindingType::ReadOnlyStorageBuffer; + } + } else { + type = BindingType::UniformBuffer; + } + afterVar = qualifierEnd + 1; + } + + size_t colonPos = source.find(':', afterVar); + if (colonPos == std::string::npos) { + break; + } + size_t typeStart = colonPos + 1; + size_t typeEnd = typeStart; + while (typeEnd < source.size()) { + char c = source[typeEnd]; + if (c == ';' || c == '\n' || c == '\r') { + break; + } + ++typeEnd; + } + const auto typeName = trimString(std::string_view(source.data() + typeStart, typeEnd - typeStart)); + + if (type == BindingType::UniformBuffer) { + if (typeName.find("sampler") != std::string::npos) { + type = BindingType::Sampler; + } else if (typeName.find("texture") != std::string::npos) { + type = BindingType::Texture; + } + } + + auto existing = std::find_if(bindingInfos.begin(), bindingInfos.end(), [&](const BindingInfo& info) { + return info.group == group && info.binding == binding; + }); + + if (existing == bindingInfos.end()) { + bindingInfos.push_back({group, binding, type, stage}); + } else { + existing->visibility |= stage; + if (existing->type == BindingType::UniformBuffer && type != BindingType::UniformBuffer) { + existing->type = type; + } + } + + searchPos = typeEnd; + } +} + +void ShaderProgram::rebuildBindGroupLayouts() { + WGPUDevice device = static_cast(backend.getDevice()); + if (!device) { + return; + } + + for (auto layout : bindGroupLayouts) { + if (layout) { + wgpuBindGroupLayoutRelease(layout); + } + } + bindGroupLayouts.clear(); + bindGroupOrder.clear(); + bindingsPerGroup.clear(); + + if (bindingInfos.empty()) { + bindingInfos.push_back({0u, 0u, BindingType::UniformBuffer, WGPUShaderStage_Vertex | WGPUShaderStage_Fragment}); + } + + std::map> grouped; + for (const auto& info : bindingInfos) { + grouped[info.group].push_back(info); + } + + for (auto& entry : grouped) { + auto& infos = entry.second; + std::sort(infos.begin(), infos.end(), [](const BindingInfo& lhs, const BindingInfo& rhs) { + return lhs.binding < rhs.binding; + }); + + std::vector layoutEntries; + layoutEntries.reserve(infos.size()); + + for (const auto& info : infos) { + WGPUBindGroupLayoutEntry layoutEntry = {}; + layoutEntry.binding = info.binding; + layoutEntry.visibility = info.visibility; + + switch (info.type) { + case BindingType::UniformBuffer: + layoutEntry.buffer.type = WGPUBufferBindingType_Uniform; + layoutEntry.buffer.hasDynamicOffset = 0; + layoutEntry.buffer.minBindingSize = 0; + break; + case BindingType::ReadOnlyStorageBuffer: + layoutEntry.buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + layoutEntry.buffer.hasDynamicOffset = 0; + layoutEntry.buffer.minBindingSize = 0; + break; + case BindingType::StorageBuffer: + layoutEntry.buffer.type = WGPUBufferBindingType_Storage; + layoutEntry.buffer.hasDynamicOffset = 0; + layoutEntry.buffer.minBindingSize = 0; + break; + case BindingType::Sampler: + layoutEntry.sampler.type = WGPUSamplerBindingType_Filtering; + break; + case BindingType::Texture: + layoutEntry.texture.sampleType = WGPUTextureSampleType_Float; + layoutEntry.texture.viewDimension = WGPUTextureViewDimension_2D; + layoutEntry.texture.multisampled = false; + break; + } + + layoutEntries.push_back(layoutEntry); + } + + WGPUBindGroupLayoutDescriptor desc = {}; + const std::string labelStr = "Shader Bind Group " + std::to_string(entry.first); + WGPUStringView labelView = {labelStr.c_str(), labelStr.length()}; + desc.label = labelView; + desc.entryCount = layoutEntries.size(); + desc.entries = layoutEntries.data(); + + WGPUBindGroupLayout layout = wgpuDeviceCreateBindGroupLayout(device, &desc); + if (layout) { + bindGroupOrder.push_back(entry.first); + bindingsPerGroup.push_back(infos); + bindGroupLayouts.push_back(layout); + } + } +} + +WGPURenderPipeline ShaderProgram::createPipeline(const WGPUVertexBufferLayout* vertexLayouts, + uint32_t vertexLayoutCount, + const gfx::ColorMode& colorMode, + const gfx::DepthMode& depthMode, + const gfx::StencilMode& stencilMode, + gfx::DrawModeType drawModeType) { + WGPUDevice device = static_cast(backend.getDevice()); + + if (!hasVertexEntryPoint || !hasFragmentEntryPoint) { + if (!loggedMissingEntryPoint) { + Log::Warning( + Event::Render, + "WebGPU: shader '" + shaderName + "' is missing WGSL entry points; skipping pipeline creation"); + loggedMissingEntryPoint = true; + } + return nullptr; + } + + if (!device || !vertexShaderModule || !fragmentShaderModule || !pipelineLayout) { + Log::Error(Event::Render, + "createPipeline missing requirement: device=" + std::to_string(device != nullptr) + + ", vertexModule=" + std::to_string(vertexShaderModule != nullptr) + + ", fragmentModule=" + std::to_string(fragmentShaderModule != nullptr) + + ", pipelineLayout=" + std::to_string(pipelineLayout != nullptr)); + return nullptr; + } + + // Set up vertex state + WGPUVertexState vertexState = {}; + vertexState.module = vertexShaderModule; + WGPUStringView vertexEntryPoint = {"main", strlen("main")}; + vertexState.entryPoint = vertexEntryPoint; + vertexState.bufferCount = vertexLayoutCount; + vertexState.buffers = vertexLayouts; + + // Set up fragment state with color mode blending + WGPUBlendComponent alphaBlend = {}; + alphaBlend.operation = webgpuBlendOperation(gfx::ColorBlendEquationType::Add); + alphaBlend.srcFactor = webgpuBlendFactor(gfx::ColorBlendFactorType::SrcAlpha); + alphaBlend.dstFactor = webgpuBlendFactor(gfx::ColorBlendFactorType::OneMinusSrcAlpha); + + WGPUBlendComponent colorBlend = {}; + colorBlend.operation = webgpuBlendOperation(gfx::ColorBlendEquationType::Add); + colorBlend.srcFactor = webgpuBlendFactor(gfx::ColorBlendFactorType::SrcAlpha); + colorBlend.dstFactor = webgpuBlendFactor(gfx::ColorBlendFactorType::OneMinusSrcAlpha); + + // Apply color mode blending if not replace + if (!colorMode.blendFunction.is()) { + colorMode.blendFunction.match([&](const auto& blendFunction) { + colorBlend.operation = webgpuBlendOperation(gfx::ColorBlendEquationType(blendFunction.equation)); + colorBlend.srcFactor = webgpuBlendFactor(gfx::ColorBlendFactorType(blendFunction.srcFactor)); + colorBlend.dstFactor = webgpuBlendFactor(gfx::ColorBlendFactorType(blendFunction.dstFactor)); + alphaBlend = colorBlend; + }); + } + + WGPUBlendState blendState = {}; + blendState.color = colorBlend; + blendState.alpha = alphaBlend; + + WGPUColorTargetState colorTarget = {}; + auto colorFormat = backend.getColorFormat(); + if (colorFormat == wgpu::TextureFormat::Undefined) { + colorFormat = wgpu::TextureFormat::BGRA8Unorm; + } + colorTarget.format = static_cast(colorFormat); + colorTarget.blend = &blendState; + colorTarget.writeMask = (colorMode.mask.r ? WGPUColorWriteMask_Red : WGPUColorWriteMask_None) | + (colorMode.mask.g ? WGPUColorWriteMask_Green : WGPUColorWriteMask_None) | + (colorMode.mask.b ? WGPUColorWriteMask_Blue : WGPUColorWriteMask_None) | + (colorMode.mask.a ? WGPUColorWriteMask_Alpha : WGPUColorWriteMask_None); + + WGPUFragmentState fragmentState = {}; + fragmentState.module = fragmentShaderModule; + WGPUStringView fragmentEntryPoint = {"main", strlen("main")}; + fragmentState.entryPoint = fragmentEntryPoint; + fragmentState.targetCount = 1; + fragmentState.targets = &colorTarget; + + // Set up primitive state + WGPUPrimitiveState primitiveState = {}; + primitiveState.topology = toPrimitiveTopology(drawModeType); + primitiveState.stripIndexFormat = (drawModeType == gfx::DrawModeType::LineStrip || + drawModeType == gfx::DrawModeType::TriangleStrip) + ? WGPUIndexFormat_Uint16 + : WGPUIndexFormat_Undefined; + primitiveState.frontFace = WGPUFrontFace_CCW; + primitiveState.cullMode = WGPUCullMode_None; + + // Set up depth stencil state + WGPUDepthStencilState depthStencilState = {}; + const auto depthFormat = backend.getDepthStencilFormat(); + if (depthFormat != wgpu::TextureFormat::Undefined) { + depthStencilState.format = static_cast(depthFormat); + depthStencilState.depthWriteEnabled = depthMode.mask == gfx::DepthMaskType::ReadWrite ? WGPUOptionalBool_True + : WGPUOptionalBool_False; + depthStencilState.depthCompare = toCompareFunction(depthMode.func); + + depthStencilState.stencilFront.compare = WGPUCompareFunction_Always; + depthStencilState.stencilFront.failOp = WGPUStencilOperation_Keep; + depthStencilState.stencilFront.depthFailOp = WGPUStencilOperation_Keep; + depthStencilState.stencilFront.passOp = WGPUStencilOperation_Keep; + depthStencilState.stencilBack = depthStencilState.stencilFront; + depthStencilState.stencilReadMask = 0; + depthStencilState.stencilWriteMask = 0; + + bool stencilEnabled = true; + if (stencilMode.test.is() && stencilMode.mask == 0 && + stencilMode.fail == gfx::StencilOpType::Keep && stencilMode.depthFail == gfx::StencilOpType::Keep && + stencilMode.pass == gfx::StencilOpType::Keep) { + stencilEnabled = false; + } + + if (stencilEnabled) { + gfx::StencilFunctionType function = gfx::StencilFunctionType::Always; + uint32_t readMask = 0xFF; + mapbox::util::apply_visitor( + [&](const auto& test) { + using TestType = std::decay_t; + function = TestType::func; + if constexpr (requires { test.mask; }) { + readMask = test.mask; + } else { + readMask = 0xFF; + } + }, + stencilMode.test); + + depthStencilState.stencilFront.compare = toCompareFunction(function); + depthStencilState.stencilBack = depthStencilState.stencilFront; + depthStencilState.stencilFront.failOp = toStencilOp(stencilMode.fail); + depthStencilState.stencilFront.depthFailOp = toStencilOp(stencilMode.depthFail); + depthStencilState.stencilFront.passOp = toStencilOp(stencilMode.pass); + depthStencilState.stencilBack = depthStencilState.stencilFront; + depthStencilState.stencilReadMask = readMask; + depthStencilState.stencilWriteMask = stencilMode.mask; + } + + if (shaderName == "ClippingMaskProgram") { + depthStencilState.stencilFront.passOp = WGPUStencilOperation_Replace; + depthStencilState.stencilBack = depthStencilState.stencilFront; + if (depthStencilState.stencilWriteMask == 0) { + depthStencilState.stencilWriteMask = 0xFF; + } + if (depthStencilState.stencilReadMask == 0) { + depthStencilState.stencilReadMask = 0xFF; + } + depthStencilState.stencilFront.compare = WGPUCompareFunction_Always; + depthStencilState.stencilBack = depthStencilState.stencilFront; + } + } + + // Create render pipeline + WGPURenderPipelineDescriptor pipelineDesc = {}; + WGPUStringView pipelineLabel = {shaderName.c_str(), shaderName.length()}; + pipelineDesc.label = pipelineLabel; + pipelineDesc.layout = pipelineLayout; + pipelineDesc.vertex = vertexState; + pipelineDesc.fragment = &fragmentState; + pipelineDesc.primitive = primitiveState; + pipelineDesc.depthStencil = depthFormat != wgpu::TextureFormat::Undefined ? &depthStencilState : nullptr; + pipelineDesc.multisample.count = 1; + pipelineDesc.multisample.mask = 0xFFFFFFFF; + pipelineDesc.multisample.alphaToCoverageEnabled = 0; + + WGPURenderPipeline pipeline = wgpuDeviceCreateRenderPipeline(device, &pipelineDesc); + if (!pipeline) { + Log::Error(Event::Render, shaderName + " pipeline creation failed"); + } + return pipeline; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/symbol.cpp b/src/mbgl/shaders/webgpu/symbol.cpp new file mode 100644 index 000000000..2cf1e8538 --- /dev/null +++ b/src/mbgl/shaders/webgpu/symbol.cpp @@ -0,0 +1,65 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +// Symbol Icon +using SymbolIconShaderSource = ShaderSource; + +const std::array SymbolIconShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short4, idSymbolPosOffsetVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::UShort4, idSymbolDataVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Short4, idSymbolPixelOffsetVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float3, idSymbolProjectedPosVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float, idSymbolFadeOpacityVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float, idSymbolOpacityVertexAttribute}, +}; + +const std::array SymbolIconShaderSource::textures = { + TextureInfo{0, idSymbolImageTexture}, +}; + +// Symbol SDF +using SymbolSDFShaderSource = ShaderSource; + +const std::array SymbolSDFShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short4, idSymbolPosOffsetVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::UShort4, idSymbolDataVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Short4, idSymbolPixelOffsetVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float3, idSymbolProjectedPosVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float, idSymbolFadeOpacityVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float4, idSymbolColorVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float4, idSymbolHaloColorVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float, idSymbolOpacityVertexAttribute}, + AttributeInfo{11, gfx::AttributeDataType::Float, idSymbolHaloWidthVertexAttribute}, + AttributeInfo{12, gfx::AttributeDataType::Float, idSymbolHaloBlurVertexAttribute}, +}; + +const std::array SymbolSDFShaderSource::textures = { + TextureInfo{0, idSymbolImageTexture}, +}; + +// Symbol Text and Icon +using SymbolTextAndIconShaderSource = ShaderSource; + +const std::array SymbolTextAndIconShaderSource::attributes = { + AttributeInfo{3, gfx::AttributeDataType::Short4, idSymbolPosOffsetVertexAttribute}, + AttributeInfo{4, gfx::AttributeDataType::UShort4, idSymbolDataVertexAttribute}, + AttributeInfo{5, gfx::AttributeDataType::Float3, idSymbolProjectedPosVertexAttribute}, + AttributeInfo{6, gfx::AttributeDataType::Float, idSymbolFadeOpacityVertexAttribute}, + AttributeInfo{7, gfx::AttributeDataType::Float4, idSymbolColorVertexAttribute}, + AttributeInfo{8, gfx::AttributeDataType::Float4, idSymbolHaloColorVertexAttribute}, + AttributeInfo{9, gfx::AttributeDataType::Float, idSymbolOpacityVertexAttribute}, + AttributeInfo{10, gfx::AttributeDataType::Float, idSymbolHaloWidthVertexAttribute}, + AttributeInfo{11, gfx::AttributeDataType::Float, idSymbolHaloBlurVertexAttribute}, +}; + +const std::array SymbolTextAndIconShaderSource::textures = { + TextureInfo{0, idSymbolImageTexture}, + TextureInfo{2, idSymbolImageIconTexture}, +}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp b/src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp new file mode 100644 index 000000000..29e06002c --- /dev/null +++ b/src/mbgl/shaders/webgpu/wgsl_preprocessor.cpp @@ -0,0 +1,115 @@ +#include + +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { +namespace detail { + +bool isDirective(const std::string& line, const std::string& directive) { + std::size_t pos = 0; + while (pos < line.size() && std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (pos >= line.size() || line[pos] != '#') { + return false; + } + ++pos; + while (pos < line.size() && std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (line.compare(pos, directive.size(), directive) != 0) { + return false; + } + pos += directive.size(); + return pos == line.size() || std::isspace(static_cast(line[pos])); +} + +std::string getDirectiveArgument(const std::string& line) { + auto hash = line.find('#'); + if (hash == std::string::npos) { + return {}; + } + auto pos = line.find_first_not_of(" \t", hash + 1); + pos = line.find_first_of(" \t", pos); + if (pos == std::string::npos) { + return {}; + } + pos = line.find_first_not_of(" \t", pos); + if (pos == std::string::npos) { + return {}; + } + const auto end = line.find_first_of(" \t\r\n", pos); + return line.substr(pos, end == std::string::npos ? std::string::npos : end - pos); +} + +std::string preprocessWGSL(const std::string& source, const std::unordered_map& defines) { + std::stringstream input(source); + std::string line; + std::string output; + output.reserve(source.size()); + + struct ConditionalFrame { + bool parentActive; + bool condition; + bool elseSeen; + }; + + std::stack stack; + bool currentActive = true; + + while (std::getline(input, line)) { + if (isDirective(line, "ifdef")) { + const auto symbol = getDirectiveArgument(line); + const bool condition = defines.contains(symbol); + stack.push(ConditionalFrame{currentActive, condition, false}); + currentActive = currentActive && condition; + continue; + } + + if (isDirective(line, "ifndef")) { + const auto symbol = getDirectiveArgument(line); + const bool condition = !defines.contains(symbol); + stack.push(ConditionalFrame{currentActive, condition, false}); + currentActive = currentActive && condition; + continue; + } + + if (isDirective(line, "else")) { + if (stack.empty()) { + continue; + } + auto& frame = stack.top(); + if (!frame.elseSeen) { + frame.elseSeen = true; + currentActive = frame.parentActive && !frame.condition; + } else { + currentActive = false; + } + continue; + } + + if (isDirective(line, "endif")) { + if (stack.empty()) { + continue; + } + currentActive = stack.top().parentActive; + stack.pop(); + continue; + } + + if (currentActive) { + output.append(line); + output.push_back('\n'); + } + } + + return output; +} + +} // namespace detail +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/shaders/webgpu/widevector.cpp b/src/mbgl/shaders/webgpu/widevector.cpp new file mode 100644 index 000000000..750cf056d --- /dev/null +++ b/src/mbgl/shaders/webgpu/widevector.cpp @@ -0,0 +1,26 @@ +#include +#include +#include + +namespace mbgl { +namespace shaders { + +using WideVectorShaderSource = ShaderSource; + +const std::array WideVectorShaderSource::attributes = { + AttributeInfo{wideVectorUBOCount + 0, gfx::AttributeDataType::Float3, idWideVectorScreenPos}, + AttributeInfo{wideVectorUBOCount + 1, gfx::AttributeDataType::Float4, idWideVectorColor}, + AttributeInfo{wideVectorUBOCount + 2, gfx::AttributeDataType::Int, idWideVectorIndex}, +}; + +const std::array WideVectorShaderSource::instanceAttributes = { + AttributeInfo{wideVectorUBOCount + 3, gfx::AttributeDataType::Float3, idWideVectorInstanceCenter}, + AttributeInfo{wideVectorUBOCount + 4, gfx::AttributeDataType::Float4, idWideVectorInstanceColor}, + AttributeInfo{wideVectorUBOCount + 5, gfx::AttributeDataType::Int, idWideVectorInstancePrevious}, + AttributeInfo{wideVectorUBOCount + 6, gfx::AttributeDataType::Int, idWideVectorInstanceNext}, +}; + +const std::array WideVectorShaderSource::textures = {}; + +} // namespace shaders +} // namespace mbgl diff --git a/src/mbgl/webgpu/buffer_resource.cpp b/src/mbgl/webgpu/buffer_resource.cpp new file mode 100644 index 000000000..22964118e --- /dev/null +++ b/src/mbgl/webgpu/buffer_resource.cpp @@ -0,0 +1,149 @@ +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace webgpu { + +BufferResource::BufferResource( + Context& context_, const void* data, std::size_t size_, uint32_t usage_, bool isIndexBuffer_, bool persistent_) + : context(context_), + size(size_), + usage(usage_), + isIndexBuffer(isIndexBuffer_), + persistent(persistent_) { + if (size == 0) { + return; + } + + // Store raw data if provided + if (data) { + raw.resize(size); + std::memcpy(raw.data(), data, size); + } + + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + + if (!device) { + return; + } + + // Create buffer descriptor + WGPUBufferDescriptor bufferDesc{}; + WGPUStringView label = {"Buffer Resource", strlen("Buffer Resource")}; + bufferDesc.label = label; + const std::size_t paddedSize = (size + 3u) & ~std::size_t(3); + + bufferDesc.usage = usage | WGPUBufferUsage_CopyDst; + bufferDesc.size = paddedSize; + bufferDesc.mappedAtCreation = 0; + + // Create buffer + buffer = wgpuDeviceCreateBuffer(device, &bufferDesc); + + if (buffer && data) { + // Upload initial data via queue write + WGPUQueue queue = static_cast(backend.getQueue()); + if (queue) { + if (paddedSize == size) { + wgpuQueueWriteBuffer(queue, buffer, 0, data, size); + } else { + std::vector padded(paddedSize, 0); + std::memcpy(padded.data(), data, size); + wgpuQueueWriteBuffer(queue, buffer, 0, padded.data(), paddedSize); + } + } else { + // Fallback to mapping when queue is unavailable (should be rare) + void* mapped = wgpuBufferGetMappedRange(buffer, 0, paddedSize); + if (mapped) { + std::memcpy(mapped, data, size); + if (paddedSize > size) { + std::memset(static_cast(mapped) + size, 0, paddedSize - size); + } + wgpuBufferUnmap(buffer); + } + } + } +} + +BufferResource::BufferResource(BufferResource&& other) noexcept + : context(other.context), + buffer(other.buffer), + raw(std::move(other.raw)), + size(other.size), + usage(other.usage), + version(other.version), + isIndexBuffer(other.isIndexBuffer), + persistent(other.persistent) { + other.buffer = nullptr; + other.size = 0; +} + +BufferResource::~BufferResource() noexcept { + if (buffer) { + wgpuBufferRelease(buffer); + buffer = nullptr; + } +} + +BufferResource& BufferResource::operator=(BufferResource&& other) noexcept { + if (this != &other) { + // Can't reassign context reference, so check they match + assert(&context == &other.context); + + if (buffer) { + wgpuBufferRelease(buffer); + } + buffer = other.buffer; + raw = std::move(other.raw); + size = other.size; + usage = other.usage; + version = other.version; + isIndexBuffer = other.isIndexBuffer; + persistent = other.persistent; + + other.buffer = nullptr; + other.size = 0; + } + return *this; +} + +BufferResource BufferResource::clone() const { + return BufferResource(context, raw.empty() ? nullptr : raw.data(), size, usage, isIndexBuffer, persistent); +} + +void BufferResource::update(const void* data, std::size_t updateSize, std::size_t offset) noexcept { + if (!data || updateSize == 0) { + return; + } + + // Update raw data if we have it + if (!raw.empty() && offset + updateSize <= raw.size()) { + std::memcpy(raw.data() + offset, data, updateSize); + } + + // Update GPU buffer if we have one + if (buffer) { + // Get the queue from backend + auto& backend = static_cast(context.getBackend()); + WGPUQueue queue = static_cast(backend.getQueue()); + if (queue) { + // Write data to buffer using queue + wgpuQueueWriteBuffer(queue, buffer, offset, data, updateSize); + } + } + + // Increment version to indicate change + ++version; +} + +bool BufferResource::needReBind(VersionType otherVersion) const noexcept { + return version != otherVersion; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/command_encoder.cpp b/src/mbgl/webgpu/command_encoder.cpp new file mode 100644 index 000000000..cf3f9d89d --- /dev/null +++ b/src/mbgl/webgpu/command_encoder.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include // for strlen + +// Platform-specific includes for swap +#ifdef __APPLE__ +#include "../../platform/glfw/glfw_webgpu_backend.hpp" +#endif + +namespace mbgl { +namespace webgpu { + +CommandEncoder::CommandEncoder(Context& ctx) + : context(ctx) { + // Initialize WebGPU command encoder + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + + if (device) { + WGPUCommandEncoderDescriptor desc = {}; + WGPUStringView label = {"MapLibre Command Encoder", strlen("MapLibre Command Encoder")}; + desc.label = label; + encoder = wgpuDeviceCreateCommandEncoder(device, &desc); + } +} + +CommandEncoder::~CommandEncoder() { + if (encoder) { + wgpuCommandEncoderRelease(encoder); + } +} + +std::unique_ptr CommandEncoder::createRenderPass(const char* name, + const gfx::RenderPassDescriptor& descriptor) { + return std::make_unique(*this, name, descriptor); +} + +std::unique_ptr CommandEncoder::createUploadPass(const char* name, gfx::Renderable& renderable) { + return std::make_unique(renderable, *this, name); +} + +void CommandEncoder::present(gfx::Renderable& renderable) { + // Submit the command buffer first + submitCommandBuffer(); + + // Then call swap on the RenderableResource to present the surface + // This matches Metal's pattern where swap handles surface presentation + try { + renderable.getResource().swap(); + } catch (...) { + // No RenderableResource available - surface presentation handled elsewhere + } +} + +void CommandEncoder::submitCommandBuffer() { + // Submit command buffer directly (for cases without RenderableResource) + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + WGPUQueue queue = static_cast(backend.getQueue()); + + if (!device || !queue || !encoder) { + return; + } + + // Finish and submit the command buffer + WGPUCommandBufferDescriptor cmdBufferDesc = {}; + WGPUStringView label = {"Command Buffer", strlen("Command Buffer")}; + cmdBufferDesc.label = label; + WGPUCommandBuffer cmdBuffer = wgpuCommandEncoderFinish(encoder, &cmdBufferDesc); + + if (cmdBuffer) { + wgpuQueueSubmit(queue, 1, &cmdBuffer); + wgpuCommandBufferRelease(cmdBuffer); + } else { + Log::Error(Event::Render, "WebGPU: Failed to finish command encoder"); + } + + // Create a new command encoder so subsequent passes in the same frame can continue recording + WGPUCommandEncoderDescriptor desc = {}; + WGPUStringView encoderLabel = {"MapLibre Command Encoder", strlen("MapLibre Command Encoder")}; + desc.label = encoderLabel; + encoder = wgpuDeviceCreateCommandEncoder(device, &desc); + + if (encoder) { + context.setCurrentCommandEncoder(this); + } else { + context.setCurrentCommandEncoder(nullptr); + } +} + +void CommandEncoder::pushDebugGroup(const char*) { + // Debug markers are currently handled at the render pass level. +} + +void CommandEncoder::popDebugGroup() { + // Debug markers are currently handled at the render pass level. +} + +void CommandEncoder::trackUploadPass(UploadPass* uploadPass) { + // Track the upload pass for debug group management + currentUploadPass = uploadPass; +} + +void CommandEncoder::forgetUploadPass(UploadPass* uploadPass) { + // Forget the upload pass when it's destroyed + if (currentUploadPass == uploadPass) { + currentUploadPass = nullptr; + } +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/context.cpp b/src/mbgl/webgpu/context.cpp new file mode 100644 index 000000000..ad246ba27 --- /dev/null +++ b/src/mbgl/webgpu/context.cpp @@ -0,0 +1,467 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +namespace { +const auto clipMaskStencilMode = gfx::StencilMode{ + /*.test=*/gfx::StencilMode::Always(), + /*.ref=*/0, + /*.mask=*/0xFF, + /*.fail=*/gfx::StencilOpType::Keep, + /*.depthFail=*/gfx::StencilOpType::Keep, + /*.pass=*/gfx::StencilOpType::Replace, +}; +const auto clipMaskDepthMode = gfx::DepthMode{.func = gfx::DepthFunctionType::Always, + .mask = gfx::DepthMaskType::ReadOnly}; +} // namespace + +Context::Context(RendererBackend& backend_) + : gfx::Context(gfx::Context::minimumRequiredVertexBindingCount), + backend(backend_), + globalUniformBuffers(std::make_unique()) {} + +Context::~Context() = default; + +void Context::beginFrame() { + // Begin a new frame - WebGPU command recording starts here +} + +void Context::endFrame() { + // End the frame - submit WebGPU commands +} + +void Context::performCleanup() { + // Clean up unused resources +} + +void Context::reduceMemoryUsage() { + // Free cached resources to reduce memory +} + +std::unique_ptr Context::createOffscreenTexture(Size size, + gfx::TextureChannelDataType type, + bool depth, + bool stencil) { + return std::make_unique(*this, size, type, depth, stencil); +} + +std::unique_ptr Context::createOffscreenTexture(Size size, gfx::TextureChannelDataType type) { + return std::make_unique(*this, size, type, true, false); +} + +std::unique_ptr Context::createCommandEncoder() { + auto encoder = std::make_unique(*this); + currentCommandEncoder = encoder.get(); + return encoder; +} + +gfx::VertexAttributeArrayPtr Context::createVertexAttributeArray() const { + return std::make_shared(); +} + +gfx::UniqueDrawableBuilder Context::createDrawableBuilder(std::string name) { + return std::make_unique(std::move(name)); +} + +gfx::UniformBufferPtr Context::createUniformBuffer(const void* data, std::size_t size, bool persistent, bool) { + (void)persistent; + return std::make_shared(*this, data, size); +} + +gfx::UniqueUniformBufferArray Context::createLayerUniformBufferArray() { + return std::make_unique(); +} + +gfx::ShaderProgramBasePtr Context::getGenericShader(gfx::ShaderRegistry& registry, const std::string& name) { + // Align with Metal - just get shader from registry without caching + const auto shaderGroup = registry.getShaderGroup(name); + auto shader = shaderGroup ? shaderGroup->getOrCreateShader(*this, {}) : gfx::ShaderProgramBasePtr{}; + return std::static_pointer_cast(std::move(shader)); +} + +TileLayerGroupPtr Context::createTileLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) { + auto tileLayerGroup = std::make_shared(layerIndex, initialCapacity, std::move(name)); + return tileLayerGroup; +} + +LayerGroupPtr Context::createLayerGroup(int32_t layerIndex, std::size_t initialCapacity, std::string name) { + auto layerGroup = std::make_shared(layerIndex, initialCapacity, std::move(name)); + return layerGroup; +} + +gfx::Texture2DPtr Context::createTexture2D() { + return std::make_shared(*this); +} + +RenderTargetPtr Context::createRenderTarget(const Size size, const gfx::TextureChannelDataType type) { + return std::make_shared(*this, size, type); +} + +void Context::resetState(gfx::DepthMode, gfx::ColorMode) { + // Reset WebGPU render state +} + +void Context::setDirtyState() { + // Mark state as needing update +} + +void Context::clearStencilBuffer(int32_t value) { + // Clear stencil buffer to specified value + (void)value; +} + +bool Context::emplaceOrUpdateUniformBuffer(gfx::UniformBufferPtr& ptr, + const void* data, + std::size_t size, + bool persistent) { + if (!ptr) { + ptr = createUniformBuffer(data, size, persistent); + return true; + } + + // Update existing buffer + auto* buffer = static_cast(ptr.get()); + buffer->update(data, size); + return false; +} + +const gfx::UniformBufferArray& Context::getGlobalUniformBuffers() const { + return *globalUniformBuffers; +} + +gfx::UniformBufferArray& Context::mutableGlobalUniformBuffers() { + return *globalUniformBuffers; +} + +void Context::bindGlobalUniformBuffers(gfx::RenderPass& renderPass) const noexcept { + // In WebGPU, we can't bind buffers globally to the render pass like Metal does. + // Instead, we need to ensure these buffers are available for drawables to include + // in their bind groups. The actual binding happens per-drawable. + + // Store a reference to global buffers in the render pass for drawables to access + auto& webgpuRenderPass = static_cast(renderPass); + webgpuRenderPass.setGlobalUniformBuffers(globalUniformBuffers.get()); +} + +void Context::unbindGlobalUniformBuffers(gfx::RenderPass& renderPass) const noexcept { + // Clear the global buffer reference from the render pass + auto& webgpuRenderPass = static_cast(renderPass); + webgpuRenderPass.setGlobalUniformBuffers(nullptr); +} + +#if !defined(NDEBUG) +void Context::visualizeStencilBuffer() { + // Debug visualization of stencil buffer +} + +void Context::visualizeDepthBuffer([[maybe_unused]] float depthRangeSize) { + // Debug visualization of depth buffer +} +#endif + +std::unique_ptr Context::createRenderbufferResource(gfx::RenderbufferPixelType type, + Size size) { + (void)type; + (void)size; + return std::make_unique(); +} + +std::unique_ptr Context::createDrawScopeResource() { + return std::make_unique(*this); +} + +// Buffer creation (aligned with Metal) +BufferResource Context::createBuffer( + const void* data, std::size_t size, uint32_t usage, bool isIndexBuffer, bool persistent) const { + return BufferResource(const_cast(*this), data, size, usage, isIndexBuffer, persistent); +} + +// Get reusable tile vertex buffer (aligned with Metal) +const BufferResource& Context::getTileVertexBuffer() { + if (!tileVertexBuffer) { + const auto vertices = RenderStaticData::tileVertices(); + const uint32_t usage = WGPUBufferUsage_Vertex; + tileVertexBuffer = createBuffer(vertices.data(), vertices.bytes(), usage, false, true); + } + return *tileVertexBuffer; +} + +// Get reusable tile index buffer (aligned with Metal) +const BufferResource& Context::getTileIndexBuffer() { + if (!tileIndexBuffer) { + const auto indices = RenderStaticData::quadTriangleIndices(); + const uint32_t usage = WGPUBufferUsage_Index; + tileIndexBuffer = createBuffer(indices.data(), indices.bytes(), usage, true, true); + } + return *tileIndexBuffer; +} + +bool Context::renderTileClippingMasks(gfx::RenderPass& renderPass, + RenderStaticData& staticData, + const std::vector& tileUBOs) { + using ShaderClass = shaders::ShaderSource; + + if (tileUBOs.empty()) { + return false; + } + + if (!clipMaskShader) { + if (auto group = staticData.shaders->getShaderGroup(ShaderClass::name)) { + clipMaskShader = std::static_pointer_cast(group->getOrCreateShader(*this, {})); + } + } + if (!clipMaskShader) { + Log::Error(Event::Render, "WebGPU: Failed to acquire clipping mask shader"); + return false; + } + + auto& shader = static_cast(*clipMaskShader); + auto& webgpuRenderPass = static_cast(renderPass); + const auto encoder = webgpuRenderPass.getEncoder(); + if (!encoder) { + Log::Error(Event::Render, "WebGPU: render pass encoder unavailable for clipping masks"); + return false; + } + + const auto& renderable = webgpuRenderPass.getDescriptor().renderable; + if (clipMaskRenderable != &renderable) { + clipMaskPipelineHash.reset(); + clipMaskRenderable = &renderable; + } + + const auto& vertexResource = getTileVertexBuffer(); + const auto& indexResource = getTileIndexBuffer(); + if (!vertexResource.getBuffer() || !indexResource.getBuffer()) { + Log::Error(Event::Render, "WebGPU: Missing tile geometry buffers for clipping masks"); + return false; + } + + const gfx::ColorMode colorMode = gfx::ColorMode::disabled(); + + WGPUVertexAttribute vertexAttribute{}; + vertexAttribute.format = WGPUVertexFormat_Sint16x2; + vertexAttribute.offset = 0; + vertexAttribute.shaderLocation = static_cast(ShaderClass::attributes[0].index); + + WGPUVertexBufferLayout vertexLayout{}; + vertexLayout.arrayStride = sizeof(gfx::Vertex); + vertexLayout.attributeCount = 1; + vertexLayout.attributes = &vertexAttribute; + vertexLayout.stepMode = WGPUVertexStepMode_Vertex; + + if (!clipMaskPipelineHash) { + clipMaskPipelineHash = util::hash(colorMode.hash(), + vertexLayout.arrayStride, + static_cast(vertexAttribute.format), + vertexAttribute.shaderLocation); + } + + const auto pipeline = shader.getRenderPipeline(renderable, + &vertexLayout, + 1, + colorMode, + clipMaskDepthMode, + clipMaskStencilMode, + gfx::DrawModeType::Triangles, + clipMaskPipelineHash); + if (!pipeline) { + Log::Error(Event::Render, "WebGPU: Failed to create clipping mask pipeline"); + return false; + } + + wgpuRenderPassEncoderSetPipeline(encoder, pipeline); + + wgpuRenderPassEncoderSetVertexBuffer(encoder, + /*slot=*/0, + vertexResource.getBuffer(), + /*offset=*/0, + vertexResource.getSizeInBytes()); + wgpuRenderPassEncoderSetIndexBuffer(encoder, + indexResource.getBuffer(), + WGPUIndexFormat_Uint16, + /*offset=*/0, + indexResource.getSizeInBytes()); + + const uint64_t uboSize = sizeof(shaders::ClipUBO); + + clipMaskUniformBuffers.clear(); + clipMaskUniformBuffers.reserve(tileUBOs.size()); + for (const auto& ubo : tileUBOs) { + auto buffer = createBuffer(&ubo, uboSize, WGPUBufferUsage_Uniform, false, false); + if (!buffer.getBuffer()) { + Log::Error(Event::Render, "WebGPU: Failed to allocate uniform buffer for clipping mask tile"); + return false; + } + clipMaskUniformBuffers.emplace_back(std::move(buffer)); + } + + auto& rendererBackend = static_cast(getBackend()); + WGPUDevice device = static_cast(rendererBackend.getDevice()); + if (!device) { + Log::Error(Event::Render, "WebGPU: Device unavailable for clipping masks"); + return false; + } + + const auto& bindGroupOrder = shader.getBindGroupOrder(); + if (bindGroupOrder.empty()) { + Log::Error(Event::Render, "WebGPU: No bind group layouts available for clipping mask shader"); + return false; + } + const uint32_t bindGroupIndex = bindGroupOrder.front(); + const auto layout = shader.getBindGroupLayout(bindGroupIndex); + if (!layout) { + Log::Error(Event::Render, "WebGPU: Bind group layout missing for clipping mask shader"); + return false; + } + + const auto& bindingInfos = shader.getBindingInfosForGroup(bindGroupIndex); + uint32_t uniformBinding = 0; + bool bindingFound = false; + for (const auto& info : bindingInfos) { + if (info.type == webgpu::ShaderProgram::BindingType::UniformBuffer) { + uniformBinding = info.binding; + bindingFound = true; + break; + } + } + if (!bindingFound) { + Log::Error(Event::Render, "WebGPU: Uniform binding not found for clipping mask shader"); + return false; + } + + clipMaskActiveBindGroups.clear(); + clipMaskActiveBindGroups.reserve(tileUBOs.size()); + + for (std::size_t i = 0; i < tileUBOs.size(); ++i) { + WGPUBindGroupEntry entry{}; + entry.binding = uniformBinding; + const auto& uboBuffer = clipMaskUniformBuffers[i]; + entry.buffer = uboBuffer.getBuffer(); + entry.offset = 0; + entry.size = uboSize; + + WGPUBindGroupDescriptor descriptor{}; + descriptor.layout = layout; + descriptor.entryCount = 1; + descriptor.entries = &entry; + + const auto bindGroup = wgpuDeviceCreateBindGroup(device, &descriptor); + if (!bindGroup) { + Log::Error(Event::Render, "WebGPU: Failed to create bind group for clipping mask draw"); + continue; + } + + clipMaskActiveBindGroups.push_back(bindGroup); + + wgpuRenderPassEncoderSetBindGroup(encoder, bindGroupIndex, bindGroup, 0, nullptr); + wgpuRenderPassEncoderSetStencilReference(encoder, tileUBOs[i].stencil_ref); + + wgpuRenderPassEncoderDrawIndexed(encoder, + /*indexCount=*/6, + /*instanceCount=*/1, + /*firstIndex=*/0, + /*baseVertex=*/0, + /*firstInstance=*/0); + } + + for (auto bindGroup : clipMaskActiveBindGroups) { + if (bindGroup) { + wgpuBindGroupRelease(bindGroup); + } + } + clipMaskActiveBindGroups.clear(); + + auto& renderStats = renderingStats(); + renderStats.numDrawCalls += tileUBOs.size(); + renderStats.totalDrawCalls += tileUBOs.size(); + + return true; +} + +gfx::AttributeBindingArray Context::getOrCreateVertexBindings(gfx::Context&, + const gfx::AttributeDataType vertexType, + const size_t vertexAttributeIndex, + const std::vector& vertexData, + const gfx::VertexAttributeArray& defaults, + const gfx::VertexAttributeArray& overrides, + gfx::BufferUsageType, + const std::optional>&, + std::shared_ptr&) noexcept { + static_cast(vertexType); + static_cast(vertexAttributeIndex); + static_cast(vertexData); + + gfx::AttributeBindingArray result; + result.resize(overrides.allocatedSize()); + + // Process each vertex attribute + overrides.visitAttributes([&](const gfx::VertexAttribute& attr) { + const auto index = static_cast(attr.getIndex()); + if (index >= result.size()) return; + + // Get the shared raw data if available + gfx::VertexVectorBasePtr sharedRaw = attr.getSharedRawData(); + if (!sharedRaw) { + // Try default attributes by ID + defaults.visitAttributes([&](const gfx::VertexAttribute& defaultAttr) { + if (!sharedRaw && defaultAttr.getIndex() == attr.getIndex()) { + sharedRaw = defaultAttr.getSharedRawData(); + } + }); + } + + if (sharedRaw) { + // Create vertex buffer from the shared data + const auto dataSize = sharedRaw->getRawSize() * sharedRaw->getRawCount(); + if (dataSize > 0) { + // Create GPU buffer for this vertex attribute + uint32_t usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; + auto bufferResource = std::make_shared( + createBuffer(sharedRaw->getRawData(), dataSize, usage, false, false)); + + // Create attribute binding + result[index] = {/*.attribute = */ {attr.getDataType(), /*offset=*/0}, + /*.vertexStride = */ static_cast(attr.getStride()), + /*.vertexBufferResource = */ bufferResource.get(), + /*.vertexOffset = */ 0}; + } + } + }); + + return result; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/draw_scope_resource.cpp b/src/mbgl/webgpu/draw_scope_resource.cpp new file mode 100644 index 000000000..f03f46d63 --- /dev/null +++ b/src/mbgl/webgpu/draw_scope_resource.cpp @@ -0,0 +1,11 @@ +#include +#include + +namespace mbgl { +namespace webgpu { + +DrawScopeResource::DrawScopeResource(Context& context_) + : context(context_) {} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/drawable.cpp b/src/mbgl/webgpu/drawable.cpp new file mode 100644 index 000000000..bf6d7aae1 --- /dev/null +++ b/src/mbgl/webgpu/drawable.cpp @@ -0,0 +1,976 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#if !defined(NDEBUG) +#include +#endif + +namespace mbgl { +namespace webgpu { + +namespace { +#if !defined(NDEBUG) +std::string debugLabel(const Drawable& drawable) { + std::ostringstream ss; + ss << drawable.getName() << "#" << drawable.getID(); + return ss.str(); +} +#endif + +struct alignas(16) GlobalUBOIndexData { + uint32_t value = 0; + uint32_t pad[3] = {0, 0, 0}; +}; + +std::size_t hashStencilMode(const gfx::StencilMode& mode) { + std::size_t hash = util::hash(static_cast(mode.ref), + static_cast(mode.mask), + static_cast(mode.fail), + static_cast(mode.depthFail), + static_cast(mode.pass)); + mapbox::util::apply_visitor( + [&](const auto& test) { + using TestType = std::decay_t; + hash = util::hash(hash, static_cast(TestType::func)); + if constexpr (requires { test.mask; }) { + hash = util::hash(hash, static_cast(test.mask)); + } + }, + mode.test); + return hash; +} + +WGPUVertexFormat wgpuVertexFormatOf(gfx::AttributeDataType type) { + switch (type) { + case gfx::AttributeDataType::Byte: + return WGPUVertexFormat_Sint8; + case gfx::AttributeDataType::Byte2: + return WGPUVertexFormat_Sint8x2; + case gfx::AttributeDataType::Byte4: + return WGPUVertexFormat_Sint8x4; + case gfx::AttributeDataType::UByte: + return WGPUVertexFormat_Uint8; + case gfx::AttributeDataType::UByte2: + return WGPUVertexFormat_Uint8x2; + case gfx::AttributeDataType::UByte4: + return WGPUVertexFormat_Uint8x4; + case gfx::AttributeDataType::Short: + return WGPUVertexFormat_Sint16; + case gfx::AttributeDataType::Short2: + return WGPUVertexFormat_Sint16x2; + case gfx::AttributeDataType::Short4: + return WGPUVertexFormat_Sint16x4; + case gfx::AttributeDataType::UShort: + return WGPUVertexFormat_Uint16; + case gfx::AttributeDataType::UShort2: + return WGPUVertexFormat_Uint16x2; + case gfx::AttributeDataType::UShort4: + return WGPUVertexFormat_Uint16x4; + case gfx::AttributeDataType::Int: + return WGPUVertexFormat_Sint32; + case gfx::AttributeDataType::Int2: + return WGPUVertexFormat_Sint32x2; + case gfx::AttributeDataType::Int3: + return WGPUVertexFormat_Sint32x3; + case gfx::AttributeDataType::Int4: + return WGPUVertexFormat_Sint32x4; + case gfx::AttributeDataType::UInt: + return WGPUVertexFormat_Uint32; + case gfx::AttributeDataType::UInt2: + return WGPUVertexFormat_Uint32x2; + case gfx::AttributeDataType::UInt3: + return WGPUVertexFormat_Uint32x3; + case gfx::AttributeDataType::UInt4: + return WGPUVertexFormat_Uint32x4; + case gfx::AttributeDataType::Float: + return WGPUVertexFormat_Float32; + case gfx::AttributeDataType::Float2: + return WGPUVertexFormat_Float32x2; + case gfx::AttributeDataType::Float3: + return WGPUVertexFormat_Float32x3; + case gfx::AttributeDataType::Float4: + return WGPUVertexFormat_Float32x4; + default: + assert(!"Unsupported vertex attribute format"); + return WGPUVertexFormat_Float32x2; // Default fallback + } +} +} // namespace + +// Simple wrapper to adapt gfx::IndexBuffer to IndexBufferBase +struct IndexBuffer : public gfx::IndexBufferBase { + IndexBuffer(std::unique_ptr&& buffer_) + : buffer(std::move(buffer_)) {} + ~IndexBuffer() override = default; + + std::unique_ptr buffer; +}; + +Drawable::Drawable(std::string name_) + : gfx::Drawable(std::move(name_)), + impl(std::make_unique()) {} + +Drawable::~Drawable() { + // Clean up WebGPU resources safely + // Note: We don't own the pipeline - it's owned by the shader program + // So we should NOT release it here + impl->pipelineState = nullptr; + + for (auto& record : impl->bindGroups) { + if (record.handle) { + wgpuBindGroupRelease(record.handle); + } + } + impl->bindGroups.clear(); + + // Uniform buffers are managed by UniformBufferArray + // Vertex and index buffers are now managed through attributeBindings and indexes +} + +namespace { + +[[maybe_unused]] WGPUPrimitiveTopology getPrimitiveTopology(const gfx::DrawModeType type) noexcept { + switch (type) { + default: + assert(false); + [[fallthrough]]; + case gfx::DrawModeType::Points: + return WGPUPrimitiveTopology_PointList; + case gfx::DrawModeType::Lines: + return WGPUPrimitiveTopology_LineList; + case gfx::DrawModeType::LineStrip: + return WGPUPrimitiveTopology_LineStrip; + case gfx::DrawModeType::Triangles: + return WGPUPrimitiveTopology_TriangleList; + } +} + +#if !defined(NDEBUG) +[[maybe_unused]] std::string debugLabel(const gfx::Drawable& drawable) { + std::ostringstream oss; + oss << drawable.getID().id() << "/" << drawable.getName() << "/tile="; + if (const auto& tileID = drawable.getTileID()) { + oss << util::toString(*tileID); + } else { + oss << "(null)"; + } + return oss.str(); +} +#endif // !defined(NDEBUG) + +} // namespace + +void Drawable::setColorMode(const gfx::ColorMode& value) { + gfx::Drawable::setColorMode(value); + impl->pipelineState = nullptr; // Rebuild pipeline to honour new color mode +} + +void Drawable::setEnableStencil(bool value) { + if (getEnableStencil() == value) { + return; + } + gfx::Drawable::setEnableStencil(value); + impl->pipelineState = nullptr; // Reset pipeline to force recreation with new stencil state +} + +void Drawable::setEnableDepth(bool value) { + if (getEnableDepth() == value) { + return; + } + gfx::Drawable::setEnableDepth(value); + impl->pipelineState = nullptr; // Reset pipeline to force recreation with new depth state +} + +void Drawable::setSubLayerIndex(int32_t value) { + if (getSubLayerIndex() == value) { + return; + } + gfx::Drawable::setSubLayerIndex(value); +} + +void Drawable::setDepthType(gfx::DepthMaskType value) { + if (getDepthType() == value) { + return; + } + gfx::Drawable::setDepthType(value); + impl->pipelineState = nullptr; // Reset pipeline to force recreation with new depth mask +} + +void Drawable::setDepthModeFor3D(const gfx::DepthMode& value) { + if (impl->depthMode.func != value.func || impl->depthMode.mask != value.mask) { + impl->pipelineState = nullptr; + } + impl->depthMode = value; +} + +void Drawable::setStencilModeFor3D(const gfx::StencilMode& value) { + const auto newHash = hashStencilMode(value); + if (!impl->stencilModeHashValid || impl->stencilModeHash != newHash || impl->stencilMode.ref != value.ref || + impl->stencilMode.mask != value.mask || impl->stencilMode.fail != value.fail || + impl->stencilMode.depthFail != value.depthFail || impl->stencilMode.pass != value.pass) { + impl->pipelineState = nullptr; + impl->stencilModeHashValid = false; + } + impl->stencilMode = value; + impl->stencilModeHash = newHash; +} + +void Drawable::setShader(gfx::ShaderProgramBasePtr value) { + if (shader == value) { + return; + } + shader = std::move(value); + impl->pipelineState = nullptr; // Reset pipeline when shader changes +} + +void Drawable::upload(gfx::UploadPass& uploadPass) { + if (isCustom) { + return; + } + if (!shader) { + Log::Warning(Event::General, "Missing shader for drawable " + util::toString(getID()) + "/" + getName()); + assert(false); + return; + } + + auto& webgpuUploadPass = static_cast(uploadPass); + auto& gfxContext = webgpuUploadPass.getContext(); + [[maybe_unused]] auto& context = static_cast(gfxContext); + constexpr auto usage = gfx::BufferUsageType::StaticDraw; + + // We need either raw index data or a buffer already created from them. + // We can have a buffer and no indexes, but only if it's not marked dirty. + if (!impl->indexes || (impl->indexes->empty() && (!impl->indexes->getBuffer() || impl->indexes->getDirty()))) { + assert(!"Missing index data"); + return; + } + + if (!impl->indexes->getBuffer() || impl->indexes->getDirty()) { + // Create or update a buffer for the index data. We don't update any + // existing buffer because it may still be in use by the previous frame. + auto indexBufferResource = webgpuUploadPass.createIndexBufferResource(impl->indexes->data(), + impl->indexes->bytes(), + usage, + /*persistent=*/false); + auto gfxIndexBuffer = std::make_unique(impl->indexes->elements(), + std::move(indexBufferResource)); + auto buffer = std::make_unique(std::move(gfxIndexBuffer)); + + impl->indexes->setBuffer(std::move(buffer)); + impl->indexes->setDirty(false); + } + + // Upload vertex attributes (like Metal does) + const bool buildAttribs = !vertexAttributes || !attributeUpdateTime || + vertexAttributes->isModifiedAfter(*attributeUpdateTime); + + if (buildAttribs) { +#if !defined(NDEBUG) + const auto debugGroup = webgpuUploadPass.createDebugGroup(debugLabel(*this)); +#endif + + if (!vertexAttributes) { + vertexAttributes = std::make_shared(); + } + + // Apply drawable values to shader defaults (matching Metal's approach) + std::vector> vertexBuffers; + auto attributeBindings_ = webgpuUploadPass.buildAttributeBindings(impl->vertexCount, + impl->vertexType, + /*vertexAttributeIndex=*/-1, + /*vertexData=*/{}, + shader->getVertexAttributes(), + *vertexAttributes, + usage, + attributeUpdateTime, + vertexBuffers); + + if (impl->attributeBindings != attributeBindings_) { + impl->attributeBindings = std::move(attributeBindings_); + impl->vertexDescHash = 0; // Reset hash when bindings change + impl->pipelineState = nullptr; // Reset pipeline state when attribute bindings change + // Store the dummy vertex buffers to keep them alive + impl->dummyVertexBuffers = std::move(vertexBuffers); + } + } + + // Build instance buffer (like Metal does) + const bool buildInstanceBuffer = + (instanceAttributes && (!attributeUpdateTime || instanceAttributes->isModifiedAfter(*attributeUpdateTime))); + + if (buildInstanceBuffer) { + // Apply instance values to shader defaults (matching Metal's approach) + std::vector> instanceBuffers; + auto instanceBindings_ = webgpuUploadPass.buildAttributeBindings(impl->vertexCount, + impl->vertexType, + /*vertexAttributeIndex=*/-1, + /*vertexData=*/{}, + shader->getInstanceAttributes(), + *instanceAttributes, + usage, + attributeUpdateTime, + instanceBuffers); + + if (impl->instanceBindings != instanceBindings_) { + impl->instanceBindings = std::move(instanceBindings_); + impl->pipelineState = nullptr; // Reset pipeline state when instance bindings change + // Store the dummy instance buffers to keep them alive + if (!instanceBuffers.empty()) { + impl->dummyVertexBuffers.insert(impl->dummyVertexBuffers.end(), + std::make_move_iterator(instanceBuffers.begin()), + std::make_move_iterator(instanceBuffers.end())); + } + } + } + + // Upload uniform buffers to ensure they're ready for the GPU + // This is critical for making drawables visible + for (size_t i = 0; i < impl->uniformBuffers.allocatedSize(); ++i) { + auto& uniformBuffer = impl->uniformBuffers.get(i); + if (uniformBuffer) { + // Make sure the uniform buffer has its data uploaded + // The uniform buffer should handle its own upload internally + // but we need to ensure it's marked as not dirty + auto* webgpuUniformBuffer = static_cast(uniformBuffer.get()); + if (webgpuUniformBuffer) { + // The uniform buffer's data should already be uploaded when it was created/updated + // Just ensure we have a valid buffer handle + if (!webgpuUniformBuffer->getBuffer()) { + Log::Error(Event::Render, "Uniform buffer " + std::to_string(i) + " has no GPU buffer!"); + } + } + } + } + + // Upload textures if needed + const bool texturesNeedUpload = std::any_of( + textures.begin(), textures.end(), [](const auto& texture) { return texture && texture->needsUpload(); }); + + if (texturesNeedUpload) { + for (auto& texture : textures) { + if (texture && texture->needsUpload()) { + texture->upload(); + } + } + } + + attributeUpdateTime = util::MonotonicTimer::now(); +} + +void Drawable::draw(PaintParameters& parameters) const { + static int drawCallCount = 0; + drawCallCount++; + + if (isCustom) { + return; + } + + // Get WebGPU context and render pass (following Metal's pattern) + auto& context = static_cast(parameters.context); + auto& webgpuRenderPass = static_cast(*parameters.renderPass); + WGPURenderPassEncoder renderPassEncoder = webgpuRenderPass.getEncoder(); + if (!renderPassEncoder) { + Log::Error(Event::Render, "No render pass encoder available"); + assert(false); + return; + } + + if (!shader) { + Log::Warning(Event::General, "Missing shader for drawable " + util::toString(getID()) + "/" + getName()); + assert(false); + return; + } + + if (!getEnabled()) { + return; + } + + // Get WebGPU backend and device + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + WGPUQueue queue = static_cast(backend.getQueue()); + + if (!device || !queue) { + return; + } + +#if !defined(NDEBUG) + // Debug group creation would go here (Metal has this but we need to implement it for WebGPU) + // const auto debugGroup = parameters.encoder->createDebugGroup(getName()); +#endif + + // Handle uboIndex like Metal does + // In WebGPU, we'll need to handle this differently since we use bind groups + // Metal uses setVertexBytes/setFragmentBytes for the uboIndex + // For now, we'll store it for use in bind group creation + + // Bind uniform buffers (like Metal does) + impl->uniformBuffers.bind(webgpuRenderPass); + + // Check index buffer is valid (like Metal does) + if (impl->indexes && (!impl->indexes->getBuffer() || impl->indexes->getDirty())) { + assert(!"Index buffer not uploaded"); + return; + } + + // Build bind groups based on shader metadata + if (shader) { + auto webgpuShader = std::static_pointer_cast(shader); + if (webgpuShader) { + WGPUDevice deviceHandle = static_cast(backend.getDevice()); + + for (auto& record : impl->bindGroups) { + if (record.handle) { + wgpuBindGroupRelease(record.handle); + } + } + impl->bindGroups.clear(); + + if (deviceHandle) { + const auto& groupOrder = webgpuShader->getBindGroupOrder(); + static bool loggedBindGroupOrder = false; + if (!loggedBindGroupOrder && getName().find("fill-outline") != std::string::npos) { + std::string orderStr; + for (size_t idx = 0; idx < groupOrder.size(); ++idx) { + orderStr += std::to_string(groupOrder[idx]); + if (idx + 1 < groupOrder.size()) orderStr += ","; + } + loggedBindGroupOrder = true; + } + const auto* globalBuffers = webgpuRenderPass.getGlobalUniformBuffers(); + + const auto findTextureForBinding = [&](uint32_t binding, bool samplerBinding) -> Texture2D* { + for (size_t texIndex = 0; texIndex < textures.size(); ++texIndex) { + if (!textures[texIndex]) { + continue; + } + if (const auto location = webgpuShader->getSamplerLocation(texIndex)) { + if ((samplerBinding && *location == binding) || + (!samplerBinding && (*location == binding || *location + 1 == binding))) { + return static_cast(textures[texIndex].get()); + } + } + } + return nullptr; + }; + + for (size_t slot = 0; slot < groupOrder.size(); ++slot) { + const uint32_t group = groupOrder[slot]; + auto layout = webgpuShader->getBindGroupLayout(group); + if (!layout) { + const std::string shaderName = webgpuShader ? std::string(webgpuShader->typeName()) : "unknown"; + Log::Warning(Event::Render, + "WebGPU: missing bind group layout for group " + std::to_string(group) + + " in shader '" + shaderName + "'"); + continue; + } + + const auto& bindingInfos = webgpuShader->getBindingInfosForGroup(group); + if (getName().find("fill-outline") != std::string::npos) { + std::string infoStr; + for (const auto& info : bindingInfos) { + infoStr += "(binding=" + std::to_string(info.binding) + + ", type=" + std::to_string(static_cast(info.type)) + ")"; + } + } + std::vector entries; + entries.reserve(bindingInfos.size()); + bool validBindings = true; + + for (const auto& bindingInfo : bindingInfos) { + WGPUBindGroupEntry entry = {}; + entry.binding = bindingInfo.binding; + + switch (bindingInfo.type) { + case ShaderProgram::BindingType::UniformBuffer: + case ShaderProgram::BindingType::ReadOnlyStorageBuffer: + case ShaderProgram::BindingType::StorageBuffer: { + std::shared_ptr buffer = impl->uniformBuffers.get( + bindingInfo.binding); + if (bindingInfo.binding == shaders::idGlobalUBOIndex) { + GlobalUBOIndexData indexData; + indexData.value = getUBOIndex(); + context.emplaceOrUpdateUniformBuffer( + impl->uboIndexUniform, &indexData, sizeof(indexData), false); + if (impl->uboIndexUniform) { + impl->uniformBuffers.set(bindingInfo.binding, impl->uboIndexUniform); + buffer = impl->uboIndexUniform; + } + } + + if (!buffer && globalBuffers) { + buffer = globalBuffers->get(bindingInfo.binding); + } + + if (!buffer) { + if (drawCallCount <= 200) { + Log::Warning(Event::Render, + "No buffer found for binding " + + std::to_string(bindingInfo.binding) + + " type=" + std::to_string(static_cast(bindingInfo.type)) + + " group=" + std::to_string(group)); + } + validBindings = false; + break; + } + + const auto* webgpuUniform = static_cast(buffer.get()); + if (!webgpuUniform || !webgpuUniform->getBuffer()) { + if (drawCallCount <= 200) { + Log::Warning(Event::Render, + "Uniform buffer missing GPU handle for binding " + + std::to_string(bindingInfo.binding) + + " group=" + std::to_string(group)); + } + validBindings = false; + break; + } + + entry.buffer = webgpuUniform->getBuffer(); + entry.offset = 0; + entry.size = buffer->getSize(); + break; + } + case ShaderProgram::BindingType::Sampler: { + Texture2D* texture = findTextureForBinding(bindingInfo.binding, true); + if (!texture) { + validBindings = false; + break; + } + if (!texture->getSampler()) { + texture->setSamplerConfiguration(texture->getSamplerState()); + } + entry.sampler = texture->getSampler(); + break; + } + case ShaderProgram::BindingType::Texture: { + Texture2D* texture = findTextureForBinding(bindingInfo.binding, false); + if (!texture) { + validBindings = false; + break; + } + if (!texture->getTexture() || !texture->getTextureView()) { + texture->create(); + } + if (!texture->getTextureView()) { + validBindings = false; + break; + } + entry.textureView = texture->getTextureView(); + break; + } + } + + if (!validBindings) { + break; + } + + entries.push_back(entry); + } + + if (!validBindings || entries.empty()) { + Log::Warning(Event::Render, + "WebGPU: invalid bindings for drawable '" + getName() + + "' (group=" + std::to_string(group) + ", slot=" + std::to_string(slot) + ")"); + continue; + } + + const std::string label = getName() + " bind-group " + std::to_string(group); + WGPUStringView labelView = {label.c_str(), label.length()}; + + WGPUBindGroupDescriptor descriptor = {}; + descriptor.label = labelView; + descriptor.layout = layout; + descriptor.entryCount = entries.size(); + descriptor.entries = entries.data(); + + WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(deviceHandle, &descriptor); + impl->bindGroups.push_back({static_cast(slot), group, bindGroup}); + } + } + } + } + + const auto drawableTileID = getTileID(); + + gfx::DepthMode depthMode = gfx::DepthMode::disabled(); + gfx::StencilMode stencilMode = gfx::StencilMode::disabled(); + + if (is3D) { + depthMode = impl->depthMode; + stencilMode = impl->stencilMode; + } else { + if (getEnableDepth()) { + depthMode = parameters.depthModeForSublayer(getSubLayerIndex(), getDepthType()); + } + if (getEnableStencil() && drawableTileID) { + stencilMode = parameters.stencilModeForClipping(drawableTileID->toUnwrapped()); + } + } + + const std::size_t newStencilHash = hashStencilMode(stencilMode); + if (!impl->stencilModeHashValid || impl->stencilModeHash != newStencilHash || + impl->depthMode.func != depthMode.func || impl->depthMode.mask != depthMode.mask) { + impl->pipelineState = nullptr; + impl->stencilModeHashValid = true; + } + + impl->depthMode = depthMode; + impl->stencilMode = stencilMode; + impl->stencilModeHash = newStencilHash; + impl->previousStencilMode = stencilMode; + + // Get pipeline from shader if we don't have it yet (like Metal) + struct UniqueBinding { + const VertexBufferResource* vertexResource = nullptr; + const BufferResource* buffer = nullptr; + uint32_t stride = 0; + uint64_t baseOffset = 0; + WGPUVertexStepMode stepMode = WGPUVertexStepMode_Vertex; + std::vector attributes; + std::vector attributeByteOffsets; + }; + + auto accumulateBindings = [](const gfx::AttributeBindingArray& source, + const WGPUVertexStepMode stepMode, + std::vector& out) { + for (size_t attributeIndex = 0; attributeIndex < source.size(); ++attributeIndex) { + const auto& binding = source[attributeIndex]; + if (!binding.has_value() || !binding->vertexBufferResource) { + continue; + } + + const auto* vertexBufferRes = static_cast(binding->vertexBufferResource); + if (!vertexBufferRes) { + continue; + } + + const auto& bufferResource = vertexBufferRes->getBuffer(); + if (!bufferResource.getBuffer()) { + continue; + } + + const uint64_t bindingByteOffset = static_cast(binding->vertexOffset) * binding->vertexStride; + + auto it = std::find_if(out.begin(), out.end(), [&](const UniqueBinding& entry) { + return entry.vertexResource == vertexBufferRes && entry.stride == binding->vertexStride && + entry.stepMode == stepMode; + }); + + if (it == out.end()) { + UniqueBinding entry; + entry.vertexResource = vertexBufferRes; + entry.buffer = &bufferResource; + entry.stride = binding->vertexStride; + entry.stepMode = stepMode; + entry.baseOffset = bindingByteOffset; + entry.attributes.reserve(4); + entry.attributeByteOffsets.reserve(4); + out.push_back(std::move(entry)); + it = out.end() - 1; + } else { + it->baseOffset = std::min(it->baseOffset, bindingByteOffset); + } + + WGPUVertexAttribute attr = {}; + attr.format = wgpuVertexFormatOf(binding->attribute.dataType); + attr.shaderLocation = static_cast(attributeIndex); + attr.offset = binding->attribute.offset; + + it->attributes.push_back(attr); + it->attributeByteOffsets.push_back(bindingByteOffset); + } + }; + + std::vector uniqueBindings; + uniqueBindings.reserve(impl->attributeBindings.size() + impl->instanceBindings.size()); + accumulateBindings(impl->attributeBindings, WGPUVertexStepMode_Vertex, uniqueBindings); + accumulateBindings(impl->instanceBindings, WGPUVertexStepMode_Instance, uniqueBindings); + + constexpr uint32_t maxVertexBufferSlots = 8; + if (uniqueBindings.size() > maxVertexBufferSlots) { + Log::Warning(Event::Render, + "WebGPU: reducing vertex buffer bindings from " + std::to_string(uniqueBindings.size()) + " to " + + std::to_string(maxVertexBufferSlots)); + uniqueBindings.resize(maxVertexBufferSlots); + } + + for (auto& entry : uniqueBindings) { + for (size_t j = 0; j < entry.attributes.size(); ++j) { + if (entry.attributeByteOffsets[j] < entry.baseOffset) { + continue; + } + const auto delta = entry.attributeByteOffsets[j] - entry.baseOffset; + entry.attributes[j].offset += static_cast(delta); + } + } + + auto& shaderWebGPU = static_cast(*shader); + + if (!impl->pipelineState) { + // Create vertex layout from attribute bindings + std::vector vertexLayouts; + vertexLayouts.reserve(uniqueBindings.size()); + + for (auto& binding : uniqueBindings) { + WGPUVertexBufferLayout layout = {}; + layout.arrayStride = binding.stride; + layout.stepMode = binding.stepMode; + layout.attributeCount = binding.attributes.size(); + layout.attributes = binding.attributes.data(); + vertexLayouts.push_back(layout); + } + + // Get render pipeline similar to Metal's getRenderPipelineState + // Use the actual color mode from the drawable (like Metal does) + const auto& colorMode = getColorMode(); + + // Get the renderable from render pass descriptor like Metal does + const auto& renderable = webgpuRenderPass.getDescriptor().renderable; + + gfx::DrawModeType drawModeType = gfx::DrawModeType::Triangles; + if (!impl->segments.empty()) { + drawModeType = impl->segments.front()->getMode().type; + } + + impl->pipelineState = shaderWebGPU.getRenderPipeline(renderable, + vertexLayouts.empty() ? nullptr : vertexLayouts.data(), + vertexLayouts.size(), + colorMode, + depthMode, + stencilMode, + drawModeType, + std::nullopt); + if (!impl->pipelineState) { + Log::Warning(Event::Render, + "WebGPU: skipping pipeline creation for drawable '" + getName() + "' (shader='" + + shaderWebGPU.typeName().data() + "')"); + } + } + + if (impl->pipelineState) { + if (!renderPassEncoder) { + Log::Error(Event::Render, "renderPassEncoder is null!"); + return; + } + wgpuRenderPassEncoderSetPipeline(renderPassEncoder, impl->pipelineState); + if (getEnableStencil()) { + wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, static_cast(stencilMode.ref)); + } + } else { + Log::Warning(Event::Render, "WebGPU: no pipeline available for drawable '" + getName() + "'; draw skipped"); + return; + } + + uint32_t bufferSlot = 0; + for (const auto& binding : uniqueBindings) { + if (!binding.buffer || !binding.buffer->getBuffer()) { + continue; + } + + const uint64_t bufferSize = binding.buffer->getSizeInBytes(); + if (binding.baseOffset >= bufferSize) { + Log::Warning(Event::Render, + "Skipping vertex buffer slot " + std::to_string(bufferSlot) + " due to base offset " + + std::to_string(binding.baseOffset) + " >= size " + std::to_string(bufferSize)); + continue; + } + + wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, + bufferSlot, + binding.buffer->getBuffer(), + binding.baseOffset, + bufferSize - binding.baseOffset); + bufferSlot++; + } + + // Bind index buffer if present + if (impl->indexes && impl->indexes->elements() > 0 && !impl->indexes->getDirty()) { + // Get the buffer from indexes + if (const auto* indexBufferBase = impl->indexes->getBuffer()) { + if (const auto* webgpuIndexBuffer = static_cast(indexBufferBase)) { + if (webgpuIndexBuffer->buffer) { + const auto& indexBufferRes = webgpuIndexBuffer->buffer->getResource(); + const auto& bufferResource = indexBufferRes.getBuffer(); + if (bufferResource.getBuffer()) { + // Always use Uint16 format for now + WGPUIndexFormat indexFormat = WGPUIndexFormat_Uint16; + wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder, + bufferResource.getBuffer(), + indexFormat, + 0, + bufferResource.getSizeInBytes()); + } + } + } + } + } + + // Set bind group + for (const auto& record : impl->bindGroups) { + if (record.handle) { + wgpuRenderPassEncoderSetBindGroup(renderPassEncoder, record.slot, record.handle, 0, nullptr); + } + } + + // Handle depth and stencil states (like Metal does) + // For 3D mode, stenciling is handled by the layer group + if (!is3D) { + if (enableStencil) { + if (tileID) { + const auto expectedStencil = parameters.stencilModeForClipping(tileID->toUnwrapped()); +#if !defined(NDEBUG) + assert(hashStencilMode(expectedStencil) == hashStencilMode(impl->stencilMode)); +#else + if (hashStencilMode(expectedStencil) != hashStencilMode(impl->stencilMode)) { + Log::Warning( + Event::Render, + "WebGPU drawable stencil mismatch for '" + getName() + "' tile=" + util::toString(*tileID)); + } +#endif + wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, static_cast(expectedStencil.ref)); + } + } else { + // Reset the reference so subsequent drawables that re-enable stencilling start from a known value. + wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, 0u); + } + + if (getEnableDepth()) { +#if !defined(NDEBUG) + const auto expectedDepth = parameters.depthModeForSublayer(getSubLayerIndex(), getDepthType()); + assert(expectedDepth.func == impl->depthMode.func); + assert(expectedDepth.mask == impl->depthMode.mask); +#endif + } + } + + // Draw indexed geometry - loop through segments (exactly like Metal does) + // int segmentCount = 0; + for (const auto& seg_ : impl->segments) { + const auto& segment = static_cast(*seg_); + const auto& mlSegment = segment.getSegment(); + if (mlSegment.indexLength > 0) { + const uint32_t instanceCount = instanceAttributes ? instanceAttributes->getMaxCount() : 1; + const uint32_t indexOffset = mlSegment.indexOffset; + const int32_t baseVertex = static_cast(mlSegment.vertexOffset); + const uint32_t baseInstance = 0; + + // Check if encoder is valid before drawing + if (!renderPassEncoder) { + Log::Error(Event::Render, "Render pass encoder became null before draw!"); + return; + } + + // Make sure we have valid indices + if (mlSegment.indexLength == 0) { + Log::Warning(Event::Render, "Skipping draw with 0 indices"); + continue; + } + + wgpuRenderPassEncoderDrawIndexed(renderPassEncoder, + mlSegment.indexLength, // indexCount + instanceCount, // instanceCount + indexOffset, // firstIndex + baseVertex, // baseVertex + baseInstance); // firstInstance + + context.renderingStats().numDrawCalls++; + } + } +} + +void Drawable::setIndexData(gfx::IndexVectorBasePtr indices, std::vector segments) { + impl->indexes = std::move(indices); + impl->segments = std::move(segments); +} + +void Drawable::setVertices(std::vector&& data, std::size_t count, gfx::AttributeDataType dataType) { + impl->vertexCount = count; + impl->vertexType = dataType; + + if (count && dataType != gfx::AttributeDataType::Invalid && !data.empty()) { + if (!vertexAttributes) { + vertexAttributes = std::make_shared(); + } + if (auto& attrib = vertexAttributes->set(impl->vertexAttrId, /*index=*/-1, dataType)) { + attrib->setRawData(std::move(data)); + attrib->setStride(gfx::VertexAttribute::getStrideOf(dataType)); + } else { + using namespace std::string_literals; + Log::Warning(Event::General, + "Vertex attribute type mismatch: "s + name + " / " + util::toString(impl->vertexAttrId)); + assert(false); + } + } +} + +const gfx::UniformBufferArray& Drawable::getUniformBuffers() const { + return impl->uniformBuffers; +} + +gfx::UniformBufferArray& Drawable::mutableUniformBuffers() { + return impl->uniformBuffers; +} + +void Drawable::setVertexAttrId(const size_t id) { + impl->vertexAttrId = id; +} + +void Drawable::updateVertexAttributes(gfx::VertexAttributeArrayPtr vertices, + std::size_t vertexCount, + gfx::DrawMode mode, + gfx::IndexVectorBasePtr indexes, + const SegmentBase* segments, + std::size_t segmentCount) { + // Store the vertex attributes (base class method) + gfx::Drawable::setVertexAttributes(std::move(vertices)); + + // Store vertex count + impl->vertexCount = vertexCount; + + // Handle segments - check for nullptr + std::vector drawSegs; + if (segments && segmentCount > 0) { + drawSegs.reserve(segmentCount); + for (std::size_t i = 0; i < segmentCount; ++i) { + const auto& seg = segments[i]; + drawSegs.push_back(std::make_unique( + mode, SegmentBase{seg.vertexOffset, seg.indexOffset, seg.vertexLength, seg.indexLength, seg.sortKey})); + } + } + + // Set the index data with segments + setIndexData(std::move(indexes), std::move(drawSegs)); +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/drawable_builder.cpp b/src/mbgl/webgpu/drawable_builder.cpp new file mode 100644 index 000000000..fa7b1d1ee --- /dev/null +++ b/src/mbgl/webgpu/drawable_builder.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +DrawableBuilder::DrawableBuilder(std::string name_) + : gfx::DrawableBuilder(std::move(name_)) {} + +DrawableBuilder::~DrawableBuilder() = default; + +std::unique_ptr DrawableBuilder::createDrawable() const { + return std::make_unique(name); +} + +std::unique_ptr DrawableBuilder::createSegment(gfx::DrawMode drawMode, + SegmentBase&& segment) { + return std::make_unique(drawMode, std::move(segment)); +} + +void DrawableBuilder::init() { + if (!currentDrawable) { + mbgl::Log::Warning(mbgl::Event::Render, "DrawableBuilder::init called with no current drawable"); + return; + } + + auto& drawable = static_cast(*currentDrawable); + + // Set the vertex attribute ID so the drawable knows which shared vertex buffer to use + drawable.setVertexAttrId(vertexAttrId); + + // Handle special case where rawVerticesCount is set but rawVertices is empty (from fills) + // In this case, the vertex data is in the vertex attributes + if (impl->rawVerticesCount && impl->rawVertices.empty()) { + // The vertex data is in the vertex attributes, use updateVertexAttributes + // Get the vertex attributes from the drawable (already set by base class flush()) + if (drawable.getVertexAttributes() && !impl->segments.empty()) { + // Get the draw mode from the first segment + gfx::DrawMode drawMode = impl->segments[0]->getMode(); + // The segments in the drawable will be set by setIndexData below + // We just need to trigger the vertex attribute extraction + // Pass empty segments for now since setIndexData will set them properly + drawable.updateVertexAttributes( + drawable.getVertexAttributes(), impl->rawVerticesCount, drawMode, impl->sharedIndexes, nullptr, 0); + + } else { + } + } else if (impl->rawVerticesCount && !impl->rawVertices.empty()) { + auto raw = std::move(impl->rawVertices); + drawable.setVertices(std::move(raw), impl->rawVerticesCount, impl->rawVerticesType); + } else if (!impl->vertices.empty()) { + const auto& verts = impl->vertices.vector(); + + if (!verts.empty()) { + constexpr auto vertSize = sizeof(std::remove_reference::type::value_type); + std::vector raw(verts.size() * vertSize); + std::memcpy(raw.data(), verts.data(), raw.size()); + drawable.setVertices(std::move(raw), verts.size(), gfx::AttributeDataType::Short2); + } + } else { + } + + // Set index data if available + if (!impl->sharedIndexes && !impl->buildIndexes.empty()) { + impl->sharedIndexes = std::make_shared(std::move(impl->buildIndexes)); + } + + if (impl->sharedIndexes && impl->sharedIndexes->elements()) { + drawable.setIndexData(impl->sharedIndexes, std::move(impl->segments)); + } else { + mbgl::Log::Warning(mbgl::Event::Render, " No index data to set!"); + } + + // Flush texture bindings so the shared builder doesn't leak the previous tile's + // Texture2D handles into the next draw call. Other backends reset textures while + // binding state; WebGPU relies on the builder being fresh per pass. + for (auto& textureSlot : textures) { + textureSlot.reset(); + } +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/drawable_impl.hpp b/src/mbgl/webgpu/drawable_impl.hpp new file mode 100644 index 000000000..b38ca63b7 --- /dev/null +++ b/src/mbgl/webgpu/drawable_impl.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class Drawable::Impl final { +public: + Impl() = default; + ~Impl() = default; + + std::vector segments; + + // Vertex description (similar to Metal's vertexDesc/vertexDescHash) + std::size_t vertexDescHash = 0; + + // Index data (matching Metal's indexes) + gfx::IndexVectorBasePtr indexes; + std::size_t vertexCount = 0; + gfx::AttributeDataType vertexType = gfx::AttributeDataType::Invalid; + + gfx::AttributeBindingArray attributeBindings; + gfx::AttributeBindingArray instanceBindings; + + // Store dummy vertex buffers to keep them alive + std::vector> dummyVertexBuffers; + + UniformBufferArray uniformBuffers; + gfx::UniformBufferPtr uboIndexUniform; + + // Render state (matching Metal) + gfx::DepthMode depthMode = gfx::DepthMode::disabled(); + gfx::StencilMode stencilMode; + gfx::CullFaceMode cullFaceMode; + std::size_t vertexAttrId = 0; + std::size_t stencilModeHash = 0; + bool stencilModeHashValid = false; + + // Pipeline state (WebGPU equivalent of Metal's pipelineState) + WGPURenderPipeline pipelineState = nullptr; + + std::optional renderPassDescriptor; + + // WebGPU-specific state (similar to Metal's depthStencilState) + WGPUDepthStencilState depthStencilState = {}; + gfx::StencilMode previousStencilMode; + + // WebGPU-specific resources needed for rendering + struct BindGroupRecord { + uint32_t slot = 0; + uint32_t group = 0; + WGPUBindGroup handle = nullptr; + }; + std::vector bindGroups; +}; + +// WebGPU-specific DrawSegment inheriting from the base gfx::Drawable::DrawSegment +using DrawSegment = gfx::Drawable::DrawSegment; + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/headless_backend.cpp b/src/mbgl/webgpu/headless_backend.cpp new file mode 100644 index 000000000..64bfc5048 --- /dev/null +++ b/src/mbgl/webgpu/headless_backend.cpp @@ -0,0 +1,296 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +class HeadlessRenderableResource final : public webgpu::RenderableResource { +public: + HeadlessRenderableResource(HeadlessBackend&, webgpu::Context& context_, Size size) + : context(context_), + size(size) { + // Create offscreen texture using the proper WebGPU OffscreenTexture class + offscreenTexture = context.createOffscreenTexture(size, gfx::TextureChannelDataType::UnsignedByte, true, true); + } + + ~HeadlessRenderableResource() noexcept override = default; + + void bind() override { + // Bind the offscreen texture + if (offscreenTexture) { + offscreenTexture->getResource().bind(); + } + } + + void swap() override { + // For headless rendering, we don't need to swap + } + + const mbgl::webgpu::RendererBackend& getBackend() const override { return context.getBackend(); } + + const WGPUCommandEncoder& getCommandEncoder() const override { + // This will be set by the command encoder when needed + static WGPUCommandEncoder dummy = nullptr; + return dummy; + } + + WGPURenderPassEncoder getRenderPassEncoder() const override { + // This will be set by the render pass when needed + return nullptr; + } + + WGPUTextureView getColorTextureView() override { + if (offscreenTexture) { + return offscreenTexture->getResource().getColorTextureView(); + } + return nullptr; + } + + std::optional getColorTextureFormat() const override { + if (offscreenTexture) { + return offscreenTexture->getResource().getColorTextureFormat(); + } + return std::nullopt; + } + + WGPUTextureView getDepthStencilTextureView() override { + if (offscreenTexture) { + return offscreenTexture->getResource().getDepthStencilTextureView(); + } + return nullptr; + } + + std::optional getDepthStencilTextureFormat() const override { + if (offscreenTexture) { + return offscreenTexture->getResource().getDepthStencilTextureFormat(); + } + return std::nullopt; + } + + PremultipliedImage readStillImage() { + if (offscreenTexture) { + return offscreenTexture->readStillImage(); + } + return PremultipliedImage(size); + } + +private: + webgpu::Context& context; + Size size; + std::unique_ptr offscreenTexture; +}; + +class HeadlessBackend::Impl { +public: + std::unique_ptr instance; + wgpu::Device device; + wgpu::Queue queue; + wgpu::Texture offscreenTexture; + wgpu::TextureView offscreenTextureView; + wgpu::Texture depthTexture; + wgpu::TextureView depthTextureView; + Size framebufferSize; +}; + +HeadlessBackend::HeadlessBackend(Size size_, SwapBehaviour swapBehaviour_, gfx::ContextMode mode) + : webgpu::RendererBackend(mode), + gfx::HeadlessBackend(size_), + impl(std::make_unique()) { + static_cast(swapBehaviour_); + + impl->framebufferSize = size_; + + // Initialize Dawn instance with TimedWaitAny feature enabled + wgpu::InstanceFeatureName timedWaitAnyFeature = wgpu::InstanceFeatureName::TimedWaitAny; + wgpu::InstanceDescriptor instanceDesc = {}; + instanceDesc.requiredFeatureCount = 1; + instanceDesc.requiredFeatures = &timedWaitAnyFeature; + + impl->instance = std::make_unique(&instanceDesc); + + // Enumerate adapters using the correct API + std::vector adapters = impl->instance->EnumerateAdapters(); + + if (adapters.empty()) { + Log::Error(Event::Render, "WebGPU HeadlessBackend: No adapters found"); + return; + } + + // Use the first adapter (typically Vulkan on Linux) + dawn::native::Adapter& selectedAdapter = adapters[0]; + + // Create device descriptor + wgpu::DeviceDescriptor deviceDesc = {}; + deviceDesc.label = "MapLibre Headless WebGPU Device"; + + // Create device + WGPUDevice rawDevice = selectedAdapter.CreateDevice(&deviceDesc); + if (!rawDevice) { + Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create device"); + return; + } + + impl->device = wgpu::Device::Acquire(rawDevice); + impl->queue = impl->device.GetQueue(); + + // Store device and queue in base class + setDevice(impl->device.Get()); + setQueue(impl->queue.Get()); + setInstance(impl->instance->Get()); + + // Create offscreen render texture + createOffscreenTextures(); +} + +HeadlessBackend::~HeadlessBackend() { + // Explicitly reset the renderable resource + setResource(nullptr); + + if (impl) { + // Clean up textures + impl->offscreenTextureView = nullptr; + impl->offscreenTexture = nullptr; + impl->depthTextureView = nullptr; + impl->depthTexture = nullptr; + + // Clean up device and queue + impl->queue = nullptr; + impl->device = nullptr; + impl->instance = nullptr; + } +} + +void HeadlessBackend::createOffscreenTextures() { + if (!impl->device) return; + + // Release old textures if they exist + impl->offscreenTextureView = nullptr; + impl->offscreenTexture = nullptr; + impl->depthTextureView = nullptr; + impl->depthTexture = nullptr; + + // Create offscreen color texture + wgpu::TextureDescriptor colorDesc = {}; + colorDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; + colorDesc.dimension = wgpu::TextureDimension::e2D; + colorDesc.size = { + static_cast(impl->framebufferSize.width), static_cast(impl->framebufferSize.height), 1}; + colorDesc.format = wgpu::TextureFormat::RGBA8Unorm; + colorDesc.mipLevelCount = 1; + colorDesc.sampleCount = 1; + colorDesc.label = "Headless Color Texture"; + + impl->offscreenTexture = impl->device.CreateTexture(&colorDesc); + + if (impl->offscreenTexture) { + wgpu::TextureViewDescriptor viewDesc = {}; + viewDesc.format = wgpu::TextureFormat::RGBA8Unorm; + viewDesc.dimension = wgpu::TextureViewDimension::e2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = wgpu::TextureAspect::All; + viewDesc.label = "Headless Color TextureView"; + + impl->offscreenTextureView = impl->offscreenTexture.CreateView(&viewDesc); + } + + // Create depth/stencil texture + wgpu::TextureDescriptor depthDesc = {}; + depthDesc.usage = wgpu::TextureUsage::RenderAttachment; + depthDesc.dimension = wgpu::TextureDimension::e2D; + depthDesc.size = { + static_cast(impl->framebufferSize.width), static_cast(impl->framebufferSize.height), 1}; + depthDesc.format = wgpu::TextureFormat::Depth24PlusStencil8; + depthDesc.mipLevelCount = 1; + depthDesc.sampleCount = 1; + depthDesc.label = "Headless Depth/Stencil Texture"; + + impl->depthTexture = impl->device.CreateTexture(&depthDesc); + + if (impl->depthTexture) { + wgpu::TextureViewDescriptor viewDesc = {}; + viewDesc.format = wgpu::TextureFormat::Depth24PlusStencil8; + viewDesc.dimension = wgpu::TextureViewDimension::e2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = wgpu::TextureAspect::All; + viewDesc.label = "Headless Depth/Stencil TextureView"; + + impl->depthTextureView = impl->depthTexture.CreateView(&viewDesc); + } +} + +gfx::Renderable& HeadlessBackend::getDefaultRenderable() { + if (!hasResource()) { + setResource( + std::make_unique(*this, static_cast(getContext()), size)); + } + return *this; +} + +PremultipliedImage HeadlessBackend::readStillImage() { + if (hasResource()) { + return getResource().readStillImage(); + } + return PremultipliedImage(getSize()); +} + +RendererBackend* HeadlessBackend::getRendererBackend() { + return this; +} + +void HeadlessBackend::activate() { + active = true; + // Ensure textures are created if size changed + if (impl && impl->framebufferSize != getSize()) { + impl->framebufferSize = getSize(); + createOffscreenTextures(); + } +} + +void HeadlessBackend::deactivate() { + active = false; +} + +void* HeadlessBackend::getCurrentTextureView() { + if (hasResource()) { + return getResource().getColorTextureView(); + } + return nullptr; +} + +void* HeadlessBackend::getDepthStencilView() { + if (hasResource()) { + return getResource().getDepthStencilTextureView(); + } + return nullptr; +} + +mbgl::Size HeadlessBackend::getFramebufferSize() const { + if (impl) { + return impl->framebufferSize; + } + return getSize(); +} + +} // namespace webgpu + +namespace gfx { + +template <> +std::unique_ptr Backend::Create( + Size size, gfx::Renderable::SwapBehaviour swapBehaviour, gfx::ContextMode contextMode) { + return std::make_unique(size, swapBehaviour, contextMode); +} + +} // namespace gfx +} // namespace mbgl diff --git a/src/mbgl/webgpu/index_buffer_resource.cpp b/src/mbgl/webgpu/index_buffer_resource.cpp new file mode 100644 index 000000000..abda5863f --- /dev/null +++ b/src/mbgl/webgpu/index_buffer_resource.cpp @@ -0,0 +1,25 @@ +#include +#include + +namespace mbgl { +namespace webgpu { + +IndexBufferResource::IndexBufferResource(BufferResource&& buffer_) noexcept + : buffer(std::move(buffer_)) { + if (buffer.getBuffer()) { + auto& stats = buffer.getContext().renderingStats(); + stats.numIndexBuffers++; + stats.memIndexBuffers += buffer.getSizeInBytes(); + } +} + +IndexBufferResource::~IndexBufferResource() noexcept { + if (buffer.getBuffer()) { + auto& stats = buffer.getContext().renderingStats(); + stats.numIndexBuffers--; + stats.memIndexBuffers -= buffer.getSizeInBytes(); + } +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/layer_group.cpp b/src/mbgl/webgpu/layer_group.cpp new file mode 100644 index 000000000..f96e189f1 --- /dev/null +++ b/src/mbgl/webgpu/layer_group.cpp @@ -0,0 +1,83 @@ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace mbgl { +namespace webgpu { + +LayerGroup::LayerGroup(int32_t layerIndex_, std::size_t initialCapacity, std::string name_) + : mbgl::LayerGroup(layerIndex_, initialCapacity, std::move(name_)) {} + +void LayerGroup::upload(gfx::UploadPass& uploadPass) { + if (!enabled) { + return; + } + +#if !defined(NDEBUG) + const auto debugGroup = uploadPass.createDebugGroup(getName() + "-upload"); +#endif + + visitDrawables([&](gfx::Drawable& drawable) { + if (drawable.getEnabled()) { + auto& drawableWebGPU = static_cast(drawable); + drawableWebGPU.upload(uploadPass); + } + }); +} + +void LayerGroup::render(RenderOrchestrator&, PaintParameters& parameters) { + if (!enabled || !getDrawableCount() || !parameters.renderPass) { + return; + } + +#if !defined(NDEBUG) + const auto debugGroup = parameters.encoder->createDebugGroup(getName() + "-render"); +#endif + + auto& renderPass = static_cast(*parameters.renderPass); + + bool bindUBOs = false; + visitDrawables([&](gfx::Drawable& drawable) { + if (!drawable.getEnabled() || !drawable.hasRenderPass(parameters.pass)) { + return; + } + + if (!bindUBOs) { + // In WebGPU, binding happens per-drawable through bind groups, + // but we still call bindWebgpu once to prepare any shared state + static_cast(uniformBuffers).bindWebgpu(renderPass); + bindUBOs = true; + } + + // Copy uniform buffers from layer group to drawable for WebGPU's bind groups + auto& drawableWebGPU = static_cast(drawable); + for (size_t i = 0; i < uniformBuffers.allocatedSize(); ++i) { + const auto& uniformBuffer = uniformBuffers.get(i); + if (uniformBuffer) { + drawableWebGPU.mutableUniformBuffers().set(i, uniformBuffer); + } + } + + for (const auto& tweaker : drawable.getTweakers()) { + tweaker->execute(drawable, parameters); + } + + drawable.draw(parameters); + }); +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/offscreen_texture.cpp b/src/mbgl/webgpu/offscreen_texture.cpp new file mode 100644 index 000000000..830f6dbcc --- /dev/null +++ b/src/mbgl/webgpu/offscreen_texture.cpp @@ -0,0 +1,324 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr uint32_t bytesPerRowAlignment = 256u; +} + +namespace mbgl { +namespace webgpu { + +class OffscreenTextureResource final : public webgpu::RenderableResource { +public: + OffscreenTextureResource( + Context& context_, const Size size_, const gfx::TextureChannelDataType type_, bool depth, bool stencil) + : context(context_), + size(size_), + type(type_) { + assert(!size.isEmpty()); + + // Create color texture + colorTexture = context.createTexture2D(); + colorTexture->setSize(size); + colorTexture->setFormat(gfx::TexturePixelType::RGBA, type); + if (auto* webgpuTexture = static_cast(colorTexture.get())) { + webgpuTexture->setUsage(WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | + WGPUTextureUsage_CopySrc | WGPUTextureUsage_RenderAttachment); + webgpuTexture->setSizeChangedCallback([this](const Size& newSize) { handleResize(newSize); }); + } + colorTexture->setSamplerConfiguration( + {gfx::TextureFilterType::Linear, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); + + // Create depth/stencil texture if needed + // WebGPU requires a combined depth-stencil texture when both are needed + if (depth || stencil) { + depthTexture = context.createTexture2D(); + depthTexture->setSize(size); + + // Use combined depth-stencil format when both are needed + if (depth && stencil) { + depthTexture->setFormat(gfx::TexturePixelType::Stencil, gfx::TextureChannelDataType::UnsignedByte); + } else if (depth) { + depthTexture->setFormat(gfx::TexturePixelType::Depth, gfx::TextureChannelDataType::Float); + } else { + depthTexture->setFormat(gfx::TexturePixelType::Stencil, gfx::TextureChannelDataType::UnsignedByte); + } + + if (auto* webgpuDepth = static_cast(depthTexture.get())) { + webgpuDepth->setUsage(WGPUTextureUsage_RenderAttachment); + } + depthTexture->setSamplerConfiguration( + {gfx::TextureFilterType::Linear, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); + } + } + + ~OffscreenTextureResource() noexcept override { context.renderingStats().numFrameBuffers--; } + + void bind() override { + // Create textures if not already created + colorTexture->create(); + + if (depthTexture) { + depthTexture->create(); + } + } + + void swap() override { + // Offscreen targets do not present to a surface. + } + + const mbgl::webgpu::RendererBackend& getBackend() const override { + return static_cast(context.getBackend()); + } + + const WGPUCommandEncoder& getCommandEncoder() const override { + static WGPUCommandEncoder dummyEncoder = nullptr; + return dummyEncoder; + } + + WGPURenderPassEncoder getRenderPassEncoder() const override { return nullptr; } + + WGPUTextureView getColorTextureView() override { + if (!colorTexture) { + return nullptr; + } + + auto& texture = static_cast(*colorTexture); + texture.create(); + return texture.getTextureView(); + } + + std::optional getColorTextureFormat() const override { + if (!colorTexture) { + return std::nullopt; + } + const auto* texture = static_cast(colorTexture.get()); + return static_cast(texture->getNativeFormat()); + } + + WGPUTextureView getDepthStencilTextureView() override { + if (depthTexture) { + auto& texture = static_cast(*depthTexture); + texture.create(); + return texture.getTextureView(); + } + return nullptr; + } + + std::optional getDepthStencilTextureFormat() const override { + if (depthTexture) { + const auto* texture = static_cast(depthTexture.get()); + return static_cast(texture->getNativeFormat()); + } + return std::nullopt; + } + + PremultipliedImage readStillImage() { + // Read back the color texture data + constexpr uint32_t bytesPerPixel = 4; // RGBA8 output + const uint32_t rowStride = static_cast(size.width) * bytesPerPixel; + const uint32_t alignedRowStride = ((rowStride + bytesPerRowAlignment - 1) / bytesPerRowAlignment) * + bytesPerRowAlignment; + const uint64_t alignedDataSize = static_cast(alignedRowStride) * size.height; + + auto data = std::make_unique(static_cast(rowStride) * size.height); + + // WebGPU texture readback requires: + // 1. Ensure all rendering commands are submitted and completed + // 2. Create a buffer with MAP_READ usage + // 3. Copy texture to buffer using command encoder + // 4. Map the buffer and read the data + // This is an async operation in WebGPU, but we need sync behavior here + + auto& backend = static_cast(context.getBackend()); + auto device = static_cast(backend.getDevice()); + auto queue = static_cast(backend.getQueue()); + if (!device || !queue) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: No device or queue available for readback"); + return {size, std::move(data)}; + } + + // Process all pending device operations to ensure rendering is complete + wgpuDeviceTick(device); + + // Create staging buffer for readback + WGPUBufferDescriptor bufferDesc = {}; + WGPUStringView bufferLabel = {"Readback Buffer", strlen("Readback Buffer")}; + bufferDesc.label = bufferLabel; + bufferDesc.size = alignedDataSize; + bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; + + WGPUBuffer stagingBuffer = wgpuDeviceCreateBuffer(device, &bufferDesc); + if (!stagingBuffer) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to create staging buffer"); + return {size, std::move(data)}; + } + + // Copy texture to buffer + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr); + if (encoder) { + auto& webgpuTexture = static_cast(*colorTexture); + WGPUTexture texture = webgpuTexture.getTexture(); + + if (texture) { + WGPUTexelCopyTextureInfo src = {}; + src.texture = texture; + src.mipLevel = 0; + src.origin = {0, 0, 0}; + src.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyBufferInfo dst = {}; + dst.buffer = stagingBuffer; + dst.layout.offset = 0; + dst.layout.bytesPerRow = alignedRowStride; + dst.layout.rowsPerImage = size.height; + + WGPUExtent3D copySize = {static_cast(size.width), static_cast(size.height), 1}; + + wgpuCommandEncoderCopyTextureToBuffer(encoder, &src, &dst, ©Size); + + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); + wgpuQueueSubmit(queue, 1, &commands); + wgpuCommandBufferRelease(commands); + } else { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Color texture is null"); + } + wgpuCommandEncoderRelease(encoder); + } else { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to create command encoder"); + } + + // Map buffer and read data (blocking) + // Use WaitAny for proper synchronous blocking instead of polling + struct MapContext { + WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error; + }; + MapContext mapContext; + + WGPUBufferMapCallbackInfo callbackInfo = {}; + callbackInfo.mode = WGPUCallbackMode_WaitAnyOnly; + callbackInfo.callback = + [](WGPUMapAsyncStatus status, WGPUStringView message, void* userdata1, void* userdata2) { + auto* ctx = static_cast(userdata1); + ctx->status = status; + (void)message; + (void)userdata2; + }; + callbackInfo.userdata1 = &mapContext; + callbackInfo.userdata2 = nullptr; + + WGPUFuture future = wgpuBufferMapAsync(stagingBuffer, WGPUMapMode_Read, 0, alignedDataSize, callbackInfo); + + // Use WaitAny to block until mapping completes + auto instance = static_cast(backend.getInstance()); + if (instance) { + WGPUFutureWaitInfo waitInfo = {}; + waitInfo.future = future; + waitInfo.completed = false; + + // Wait indefinitely for the mapping to complete + WGPUWaitStatus waitStatus = wgpuInstanceWaitAny(instance, 1, &waitInfo, UINT64_MAX); + + if (waitStatus != WGPUWaitStatus_Success || !waitInfo.completed) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Buffer mapping wait failed"); + } + } + + // Check if mapping was successful and read the data + if (mapContext.status == WGPUMapAsyncStatus_Success) { + const auto* mappedData = static_cast( + wgpuBufferGetConstMappedRange(stagingBuffer, 0, alignedDataSize)); + if (mappedData) { + if (alignedRowStride == rowStride) { + std::memcpy(data.get(), mappedData, static_cast(alignedDataSize)); + } else { + auto* dstPtr = data.get(); + for (uint32_t row = 0; row < size.height; ++row) { + std::memcpy(dstPtr + static_cast(row) * rowStride, + mappedData + static_cast(row) * alignedRowStride, + rowStride); + } + } + } else { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to get mapped range"); + } + } else { + mbgl::Log::Error(mbgl::Event::Render, + "WebGPU: Buffer mapping failed with status: " + std::to_string(mapContext.status)); + } + + wgpuBufferUnmap(stagingBuffer); + wgpuBufferRelease(stagingBuffer); + + return {size, std::move(data)}; + } + + gfx::Texture2DPtr& getTexture() { + assert(colorTexture); + return colorTexture; + } + + void setSizeListener(std::function listener) { sizeListener = std::move(listener); } + +private: + void handleResize(const Size& newSize) { + if (newSize == size || newSize.isEmpty()) { + return; + } + size = newSize; + + if (depthTexture) { + static_cast(*depthTexture).setSize(newSize); + static_cast(*depthTexture).create(); + } + + if (auto* color = static_cast(colorTexture.get())) { + color->create(); + } + + if (sizeListener) { + sizeListener(size); + } + } + + Context& context; + Size size; + const gfx::TextureChannelDataType type; + gfx::Texture2DPtr colorTexture; + gfx::Texture2DPtr depthTexture; + std::function sizeListener; +}; + +OffscreenTexture::OffscreenTexture( + Context& context, const Size size_, const gfx::TextureChannelDataType type, bool depth, bool stencil) + : gfx::OffscreenTexture(size_, std::make_unique(context, size_, type, depth, stencil)) { + auto& offscreenResource = getResource(); + offscreenResource.setSizeListener([this](const Size& newSize) { size = newSize; }); +} + +bool OffscreenTexture::isRenderable() { + // Check if the texture resource is valid and can be rendered to + try { + auto& res = getResource(); + return res.getTexture() != nullptr; + } catch (...) { + return false; + } +} + +PremultipliedImage OffscreenTexture::readStillImage() { + return getResource().readStillImage(); +} + +const gfx::Texture2DPtr& OffscreenTexture::getTexture() { + return getResource().getTexture(); +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/render_pass.cpp b/src/mbgl/webgpu/render_pass.cpp new file mode 100644 index 000000000..e42aa47a4 --- /dev/null +++ b/src/mbgl/webgpu/render_pass.cpp @@ -0,0 +1,226 @@ +// Include Dawn headers first to define WEBGPU_H_ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include // for strlen +#include // for std::getenv + +namespace mbgl { +namespace webgpu { + +class RenderPass::Impl { +public: + WGPURenderPassEncoder encoder = nullptr; + WGPUCommandEncoder commandEncoder = nullptr; + const gfx::UniformBufferArray* globalUniformBuffers = nullptr; + wgpu::TextureView colorView; + wgpu::TextureView depthStencilView; + wgpu::TextureFormat previousColorFormat = wgpu::TextureFormat::Undefined; + wgpu::TextureFormat previousDepthStencilFormat = wgpu::TextureFormat::Undefined; + bool colorFormatUpdated = false; + bool depthFormatUpdated = false; +}; + +RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const gfx::RenderPassDescriptor& descriptor_) + : descriptor(descriptor_), + commandEncoder(commandEncoder_), + debugGroup(commandEncoder_.createDebugGroup(name)), + impl(std::make_unique()) { + // Get the WebGPU command encoder from our CommandEncoder + impl->commandEncoder = commandEncoder_.getEncoder(); + + if (!impl->commandEncoder) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Command encoder unavailable"); + return; + } + + // Access the renderable resource associated with this pass + // Check if resource exists before accessing it + if (!descriptor.renderable.hasResource()) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: No renderable resource available"); + return; + } + + auto& renderableResource = descriptor.renderable.getResource(); + renderableResource.bind(); + + // Get the backend to access device and surface fallbacks + auto& context = commandEncoder_.getContext(); + auto& backend = static_cast(context.getBackend()); + impl->previousColorFormat = backend.getColorFormat(); + impl->previousDepthStencilFormat = backend.getDepthStencilFormat(); + + // Resolve the color attachment view, preferring the renderable's texture view + WGPUTextureView colorViewHandle = renderableResource.getColorTextureView(); + if (!colorViewHandle) { + if (void* textureViewPtr = backend.getCurrentTextureView()) { + colorViewHandle = reinterpret_cast(textureViewPtr); + } + } + + if (!colorViewHandle) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to acquire color attachment view"); + return; + } + + wgpuTextureViewAddRef(colorViewHandle); + impl->colorView = wgpu::TextureView::Acquire(colorViewHandle); + + if (auto colorFormat = renderableResource.getColorTextureFormat()) { + backend.setColorFormat(*colorFormat); + impl->colorFormatUpdated = true; + } + + WGPURenderPassColorAttachment colorAttachment = {}; + colorAttachment.view = impl->colorView.Get(); + colorAttachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + colorAttachment.resolveTarget = nullptr; + colorAttachment.storeOp = WGPUStoreOp_Store; + + if (descriptor.clearColor) { + const auto& clearColor = descriptor.clearColor.value(); + const WGPUColor value{static_cast(clearColor.r), + static_cast(clearColor.g), + static_cast(clearColor.b), + static_cast(clearColor.a)}; + colorAttachment.loadOp = WGPULoadOp_Clear; + colorAttachment.clearValue = value; + } else { + colorAttachment.loadOp = WGPULoadOp_Load; + } + + WGPURenderPassDepthStencilAttachment depthAttachment = {}; + depthAttachment.depthLoadOp = descriptor.clearDepth ? WGPULoadOp_Clear : WGPULoadOp_Load; + depthAttachment.depthStoreOp = WGPUStoreOp_Store; + depthAttachment.depthClearValue = descriptor.clearDepth ? descriptor.clearDepth.value() : 1.0f; + depthAttachment.depthReadOnly = WGPU_FALSE; + if (descriptor.clearStencil) { + depthAttachment.stencilLoadOp = WGPULoadOp_Clear; + depthAttachment.stencilStoreOp = WGPUStoreOp_Store; + depthAttachment.stencilClearValue = static_cast(descriptor.clearStencil.value()); + } else { + depthAttachment.stencilLoadOp = WGPULoadOp_Undefined; + depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined; + depthAttachment.stencilClearValue = 0u; + } + depthAttachment.stencilReadOnly = WGPU_FALSE; + depthAttachment.view = nullptr; + + WGPURenderPassDepthStencilAttachment* depthAttachmentPtr = nullptr; + + WGPUTextureView depthViewHandle = renderableResource.getDepthStencilTextureView(); + if (!depthViewHandle) { + if (void* depthStencilViewPtr = backend.getDepthStencilView()) { + depthViewHandle = reinterpret_cast(depthStencilViewPtr); + } + } + + if (depthViewHandle) { + wgpuTextureViewAddRef(depthViewHandle); + impl->depthStencilView = wgpu::TextureView::Acquire(depthViewHandle); + if (impl->depthStencilView) { + depthAttachment.view = impl->depthStencilView.Get(); + depthAttachmentPtr = &depthAttachment; + } + } + + if (depthAttachmentPtr) { + if (auto depthFormat = renderableResource.getDepthStencilTextureFormat()) { + backend.setDepthStencilFormat(*depthFormat); + impl->depthFormatUpdated = true; + } + } + + WGPURenderPassDescriptor renderPassDesc = {}; + const WGPUStringView passLabel{name, name ? strlen(name) : 0}; + renderPassDesc.label = passLabel; + renderPassDesc.colorAttachmentCount = 1; + renderPassDesc.colorAttachments = &colorAttachment; + renderPassDesc.depthStencilAttachment = depthAttachmentPtr; + + impl->encoder = wgpuCommandEncoderBeginRenderPass(impl->commandEncoder, &renderPassDesc); + + if (impl->encoder) { + auto size = descriptor.renderable.getSize(); + wgpuRenderPassEncoderSetViewport( + impl->encoder, 0.0f, 0.0f, static_cast(size.width), static_cast(size.height), 0.0f, 1.0f); + wgpuRenderPassEncoderSetScissorRect(impl->encoder, 0, 0, size.width, size.height); + } else { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to begin render pass"); + impl->colorView = nullptr; + impl->depthStencilView = nullptr; + } +} + +RenderPass::~RenderPass() { + if (impl->encoder) { + // End the render pass + try { + wgpuRenderPassEncoderEnd(impl->encoder); + } catch (...) { + mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to end render pass"); + } + wgpuRenderPassEncoderRelease(impl->encoder); + } + + auto& backend = static_cast(commandEncoder.getContext().getBackend()); + if (impl->colorFormatUpdated) { + backend.setColorFormat(impl->previousColorFormat); + } + if (impl->depthFormatUpdated) { + backend.setDepthStencilFormat(impl->previousDepthStencilFormat); + } +} + +void RenderPass::pushDebugGroup(const char* name) { + if (impl->encoder) { + // Debug groups on render pass encoders can cause issues in some Dawn versions + // Enable with environment variable if needed for debugging + static bool enableDebugGroups = std::getenv("WEBGPU_ENABLE_DEBUG_GROUPS") != nullptr; + if (enableDebugGroups) { + WGPUStringView label = {name, name ? strlen(name) : 0}; + wgpuRenderPassEncoderPushDebugGroup(impl->encoder, label); + } + } +} + +void RenderPass::popDebugGroup() { + if (impl->encoder) { + // Debug groups on render pass encoders can cause issues in some Dawn versions + // Enable with environment variable if needed for debugging + static bool enableDebugGroups = std::getenv("WEBGPU_ENABLE_DEBUG_GROUPS") != nullptr; + if (enableDebugGroups) { + wgpuRenderPassEncoderPopDebugGroup(impl->encoder); + } + } +} + +void RenderPass::addDebugSignpost(const char* name) { + if (impl->encoder) { + WGPUStringView label = {name, name ? strlen(name) : 0}; + wgpuRenderPassEncoderInsertDebugMarker(impl->encoder, label); + } +} + +WGPURenderPassEncoder RenderPass::getEncoder() const { + return impl->encoder; +} + +void RenderPass::setGlobalUniformBuffers(const gfx::UniformBufferArray* buffers) { + impl->globalUniformBuffers = buffers; +} + +const gfx::UniformBufferArray* RenderPass::getGlobalUniformBuffers() const { + return impl->globalUniformBuffers; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/renderer_backend.cpp b/src/mbgl/webgpu/renderer_backend.cpp new file mode 100644 index 000000000..7c6f4b985 --- /dev/null +++ b/src/mbgl/webgpu/renderer_backend.cpp @@ -0,0 +1,191 @@ +#include +#include +#include +#include +#include +#include +#include + +// Include shader group and individual shader headers +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +// Forward declare and define the Impl class +class RendererBackend::Impl { +public: + void* instance = nullptr; + void* device = nullptr; + void* queue = nullptr; + void* surface = nullptr; + wgpu::TextureFormat depthStencilFormat = wgpu::TextureFormat::Undefined; + wgpu::TextureFormat colorFormat = wgpu::TextureFormat::Undefined; +}; + +RendererBackend::RendererBackend(const gfx::ContextMode contextMode_) + : gfx::RendererBackend(contextMode_), + impl(std::make_unique()) {} + +RendererBackend::~RendererBackend() = default; + +void RendererBackend::activate() { + // Activation logic if needed +} + +void RendererBackend::deactivate() { + // Deactivation logic if needed +} + +std::unique_ptr RendererBackend::createContext() { + auto ctx = std::make_unique(*this); + return ctx; +} + +// getDefaultRenderable() is pure virtual and must be implemented by platform-specific backends +// The platform backend (e.g., GLFWWebGPUBackend) typically inherits from gfx::Renderable itself +// and returns *this from getDefaultRenderable() + +namespace { + +template +void registerTypes(gfx::ShaderRegistry& registry, const ProgramParameters& programParameters) { + using namespace std::string_literals; + + // Register each shader type using fold expression + ( + [&]() { + using ShaderClass = shaders::ShaderSource; + auto group = std::make_shared>(programParameters); + if (!registry.registerShaderGroup(std::move(group), ShaderClass::name)) { + assert(!"duplicate shader group"); + throw std::runtime_error("Failed to register "s + ShaderClass::name + " with shader registry!"); + } + }(), + ...); +} + +} // namespace + +void RendererBackend::initShaders(gfx::ShaderRegistry& registry, const ProgramParameters& parameters) { + // Register all shader types - even if headers don't exist yet, fallback implementation will be used + // As WebGPU shader headers are created, they will automatically be picked up + registerTypes(registry, parameters); +} + +void RendererBackend::setSurface(void* nativeWindow) { + // Platform-specific surface creation will be handled by subclasses + // For now, just store the native window handle + // The actual surface creation depends on the platform (X11, Wayland, etc.) + (void)nativeWindow; +} + +void RendererBackend::setInstance(void* instance) { + impl->instance = instance; +} + +void RendererBackend::setDevice(void* device) { + impl->device = device; +} + +void RendererBackend::setQueue(void* queue) { + impl->queue = queue; +} + +void* RendererBackend::getInstance() const { + return impl->instance; +} + +void* RendererBackend::getDevice() const { + return impl->device; +} + +void* RendererBackend::getQueue() const { + return impl->queue; +} + +void* RendererBackend::getSurface() const { + return impl->surface; +} + +void* RendererBackend::getCurrentTextureView() { + // Default implementation - platform backends can override if needed + return nullptr; +} + +void* RendererBackend::getDepthStencilView() { + // Default implementation - platform backends can override if needed + return nullptr; +} + +mbgl::Size RendererBackend::getFramebufferSize() const { + // Default implementation - platform backends should override + return {0, 0}; +} + +void RendererBackend::setDepthStencilFormat(wgpu::TextureFormat format) { + impl->depthStencilFormat = format; +} + +wgpu::TextureFormat RendererBackend::getDepthStencilFormat() const { + return impl->depthStencilFormat; +} + +void RendererBackend::setColorFormat(wgpu::TextureFormat format) { + impl->colorFormat = format; +} + +wgpu::TextureFormat RendererBackend::getColorFormat() const { + return impl->colorFormat; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/texture2d.cpp b/src/mbgl/webgpu/texture2d.cpp new file mode 100644 index 000000000..4788a212b --- /dev/null +++ b/src/mbgl/webgpu/texture2d.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr uint32_t bytesPerRowAlignment = 256u; + +struct UploadCopyData { + const void* dataPtr = nullptr; + size_t dataSize = 0u; + uint32_t bytesPerRow = 0u; + std::vector staging; +}; + +UploadCopyData prepareCopyData(const void* pixelData, const mbgl::Size& regionSize, size_t pixelStride) { + UploadCopyData copyData; + + if (!pixelData || regionSize.width == 0 || regionSize.height == 0) { + return copyData; + } + + const size_t rowStride = static_cast(regionSize.width) * pixelStride; + copyData.bytesPerRow = static_cast(rowStride); + copyData.dataSize = rowStride * regionSize.height; + copyData.dataPtr = pixelData; + + if (regionSize.height > 1 && (rowStride % bytesPerRowAlignment) != 0) { + const size_t alignedRowStride = ((rowStride + bytesPerRowAlignment - 1) / bytesPerRowAlignment) * + bytesPerRowAlignment; + copyData.bytesPerRow = static_cast(alignedRowStride); + copyData.staging.resize(alignedRowStride * regionSize.height, 0u); + + auto* dst = copyData.staging.data(); + const auto* src = static_cast(pixelData); + for (uint32_t row = 0; row < regionSize.height; ++row) { + std::memcpy(dst + static_cast(row) * alignedRowStride, + src + static_cast(row) * rowStride, + rowStride); + } + + copyData.dataPtr = copyData.staging.data(); + copyData.dataSize = copyData.staging.size(); + } + + return copyData; +} +} // namespace + +namespace mbgl { +namespace webgpu { + +Texture2D::Texture2D(Context& context_) + : context(context_) {} + +Texture2D::~Texture2D() { + if (textureView) { + wgpuTextureViewRelease(textureView); + textureView = nullptr; + } + if (texture) { + wgpuTextureRelease(texture); + texture = nullptr; + } + if (sampler) { + wgpuSamplerRelease(sampler); + sampler = nullptr; + } +} + +gfx::Texture2D& Texture2D::setSamplerConfiguration(const gfx::Texture2D::SamplerState& samplerState_) noexcept { + gfx::Texture2D::SamplerState safeState = samplerState_; + if (safeState.maxAnisotropy < 1) { + safeState.maxAnisotropy = 1; + } + samplerState = safeState; + + // Release old sampler if it exists + if (sampler) { + wgpuSamplerRelease(sampler); + sampler = nullptr; + } + + // Create new sampler with updated configuration + WGPUSamplerDescriptor samplerDesc = {}; + WGPUStringView samplerLabel = {"Texture Sampler", strlen("Texture Sampler")}; + samplerDesc.label = samplerLabel; + + // Map filter modes + samplerDesc.minFilter = (samplerState.filter == gfx::TextureFilterType::Linear) ? WGPUFilterMode_Linear + : WGPUFilterMode_Nearest; + samplerDesc.magFilter = (samplerState.filter == gfx::TextureFilterType::Linear) ? WGPUFilterMode_Linear + : WGPUFilterMode_Nearest; + samplerDesc.mipmapFilter = samplerState.mipmapped ? WGPUMipmapFilterMode_Linear : WGPUMipmapFilterMode_Nearest; + + // Map wrap modes + auto mapWrapMode = [](gfx::TextureWrapType wrap) -> WGPUAddressMode { + switch (wrap) { + case gfx::TextureWrapType::Repeat: + return WGPUAddressMode_Repeat; + case gfx::TextureWrapType::Clamp: + return WGPUAddressMode_ClampToEdge; + default: + return WGPUAddressMode_ClampToEdge; + } + }; + + samplerDesc.addressModeU = mapWrapMode(samplerState.wrapU); + samplerDesc.addressModeV = mapWrapMode(samplerState.wrapV); + samplerDesc.addressModeW = WGPUAddressMode_ClampToEdge; + samplerDesc.maxAnisotropy = static_cast(samplerState.maxAnisotropy); + + // Create the sampler + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + if (device) { + sampler = wgpuDeviceCreateSampler(device, &samplerDesc); + } + + return *this; +} + +gfx::Texture2D& Texture2D::setFormat(gfx::TexturePixelType pixelFormat_, + gfx::TextureChannelDataType channelType_) noexcept { + pixelFormat = pixelFormat_; + channelType = channelType_; + return *this; +} + +gfx::Texture2D& Texture2D::setSize(Size size_) noexcept { + if (size == size_) { + return *this; + } + size = size_; + dirty = true; + if (sizeChangedCallback) { + sizeChangedCallback(size); + } + return *this; +} + +gfx::Texture2D& Texture2D::setImage(std::shared_ptr image_) noexcept { + image = image_; + if (image) { + size = image->size; + dirty = true; + } + return *this; +} + +Texture2D& Texture2D::setUsage(uint32_t usageFlags_) noexcept { + usageFlags = usageFlags_; + return *this; +} + +size_t Texture2D::getDataSize() const noexcept { + return size.width * size.height * getPixelStride(); +} + +size_t Texture2D::getPixelStride() const noexcept { + switch (pixelFormat) { + case gfx::TexturePixelType::Alpha: + case gfx::TexturePixelType::Luminance: + switch (channelType) { + case gfx::TextureChannelDataType::UnsignedByte: + return 1; + case gfx::TextureChannelDataType::HalfFloat: + return 2; + case gfx::TextureChannelDataType::Float: + return 4; + } + return 1; + case gfx::TexturePixelType::RGBA: + switch (channelType) { + case gfx::TextureChannelDataType::UnsignedByte: + return 4; + case gfx::TextureChannelDataType::HalfFloat: + return 8; + case gfx::TextureChannelDataType::Float: + return 16; + } + return 4; + default: + return 4; + } +} + +size_t Texture2D::numChannels() const noexcept { + switch (pixelFormat) { + case gfx::TexturePixelType::Alpha: + return 1; + case gfx::TexturePixelType::Luminance: + return 1; + case gfx::TexturePixelType::RGBA: + return 4; + default: + return 4; + } +} + +void Texture2D::create() noexcept { + // If texture already exists, don't recreate it + if (texture && textureView) { + return; + } + + // Release old resources if they exist + if (textureView) { + wgpuTextureViewRelease(textureView); + textureView = nullptr; + } + if (texture) { + wgpuTextureRelease(texture); + texture = nullptr; + } + + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + if (!device || size.width == 0 || size.height == 0) { + return; + } + + // Determine WebGPU texture format based on pixel format + WGPUTextureFormat format = WGPUTextureFormat_Undefined; + uint32_t usage = usageFlags; + switch (pixelFormat) { + case gfx::TexturePixelType::Alpha: + switch (channelType) { + case gfx::TextureChannelDataType::UnsignedByte: + format = WGPUTextureFormat_R8Unorm; + break; + case gfx::TextureChannelDataType::HalfFloat: + format = WGPUTextureFormat_R16Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + case gfx::TextureChannelDataType::Float: + format = WGPUTextureFormat_R32Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + } + break; + case gfx::TexturePixelType::Luminance: + switch (channelType) { + case gfx::TextureChannelDataType::UnsignedByte: + format = WGPUTextureFormat_R8Unorm; + break; + case gfx::TextureChannelDataType::HalfFloat: + format = WGPUTextureFormat_R16Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + case gfx::TextureChannelDataType::Float: + format = WGPUTextureFormat_R32Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + } + break; + case gfx::TexturePixelType::Depth: + format = WGPUTextureFormat_Depth32Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + case gfx::TexturePixelType::Stencil: +#ifdef WGPUTextureFormat_Stencil8 + format = WGPUTextureFormat_Stencil8; +#else + format = WGPUTextureFormat_Depth24PlusStencil8; +#endif + usage |= WGPUTextureUsage_RenderAttachment; + break; + case gfx::TexturePixelType::RGBA: + default: + switch (channelType) { + case gfx::TextureChannelDataType::UnsignedByte: + format = WGPUTextureFormat_RGBA8Unorm; + break; + case gfx::TextureChannelDataType::HalfFloat: + format = WGPUTextureFormat_RGBA16Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + case gfx::TextureChannelDataType::Float: + format = WGPUTextureFormat_RGBA32Float; + usage |= WGPUTextureUsage_RenderAttachment; + break; + } + break; + } + + if (format == WGPUTextureFormat_Undefined) { + format = WGPUTextureFormat_RGBA8Unorm; + } + + nativeFormat = format; + + // Create texture descriptor + WGPUTextureDescriptor textureDesc = {}; + WGPUStringView textureLabel = {"Texture2D", strlen("Texture2D")}; + textureDesc.label = textureLabel; + textureDesc.size = {static_cast(size.width), static_cast(size.height), 1}; + textureDesc.mipLevelCount = 1; + textureDesc.sampleCount = 1; + textureDesc.dimension = WGPUTextureDimension_2D; + textureDesc.format = format; + // Ensure color targets are renderable when requested via usage flags + if (pixelFormat == gfx::TexturePixelType::RGBA) { + usage |= WGPUTextureUsage_RenderAttachment; + } + textureDesc.usage = usage; + + // Create the texture + texture = wgpuDeviceCreateTexture(device, &textureDesc); + + // Create texture view + if (texture) { + WGPUTextureViewDescriptor viewDesc = {}; + WGPUStringView viewLabel = {"Texture2D View", strlen("Texture2D View")}; + viewDesc.label = viewLabel; + viewDesc.format = format; + viewDesc.dimension = WGPUTextureViewDimension_2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + viewDesc.aspect = WGPUTextureAspect_All; + + textureView = wgpuTextureCreateView(texture, &viewDesc); + } + + // Create default sampler if not already created + if (!sampler) { + setSamplerConfiguration(samplerState); + } +} + +void Texture2D::setSizeChangedCallback(std::function callback) noexcept { + sizeChangedCallback = std::move(callback); +} + +void Texture2D::upload(const void* pixelData, const Size& size_) noexcept { + size = size_; + + // Create texture if it doesn't exist + if (!texture) { + create(); + } + + if (!texture || !pixelData) { + return; + } + + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + WGPUQueue queue = static_cast(backend.getQueue()); + if (!device || !queue) { + return; + } + + const auto copyData = prepareCopyData(pixelData, size, getPixelStride()); + if (!copyData.dataPtr) { + return; + } + + WGPUTexelCopyTextureInfo destination = {}; + destination.texture = texture; + destination.mipLevel = 0; + destination.origin = {0, 0, 0}; + destination.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyBufferLayout dataLayout = {}; + dataLayout.offset = 0; + dataLayout.bytesPerRow = copyData.bytesPerRow; + dataLayout.rowsPerImage = static_cast(size.height); + + WGPUExtent3D writeSize = {static_cast(size.width), static_cast(size.height), 1}; + + wgpuQueueWriteTexture(queue, &destination, copyData.dataPtr, copyData.dataSize, &dataLayout, &writeSize); +} + +void Texture2D::upload() noexcept { + if (image) { + upload(image->data.get(), image->size); + dirty = false; + } +} + +void Texture2D::uploadSubRegion(const void* pixelData, + const Size& regionSize, + uint16_t xOffset, + uint16_t yOffset) noexcept { + if (!texture || !pixelData) { + return; + } + + auto& backend = static_cast(context.getBackend()); + WGPUQueue queue = static_cast(backend.getQueue()); + if (!queue) { + return; + } + + const auto copyData = prepareCopyData(pixelData, regionSize, getPixelStride()); + if (!copyData.dataPtr) { + return; + } + + WGPUTexelCopyTextureInfo destination = {}; + destination.texture = texture; + destination.mipLevel = 0; + destination.origin = {xOffset, yOffset, 0}; + destination.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyBufferLayout dataLayout = {}; + dataLayout.offset = 0; + dataLayout.bytesPerRow = copyData.bytesPerRow; + dataLayout.rowsPerImage = static_cast(regionSize.height); + + WGPUExtent3D writeSize = {static_cast(regionSize.width), static_cast(regionSize.height), 1}; + + wgpuQueueWriteTexture(queue, &destination, copyData.dataPtr, copyData.dataSize, &dataLayout, &writeSize); +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/tile_layer_group.cpp b/src/mbgl/webgpu/tile_layer_group.cpp new file mode 100644 index 000000000..bf0d7e901 --- /dev/null +++ b/src/mbgl/webgpu/tile_layer_group.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace webgpu { + +TileLayerGroup::TileLayerGroup(int32_t layerIndex_, std::size_t initialCapacity, std::string name_) + : mbgl::TileLayerGroup(layerIndex_, initialCapacity, std::move(name_)) {} + +void TileLayerGroup::upload(gfx::UploadPass& uploadPass) { + if (!enabled || !getDrawableCount()) { + return; + } + +#if !defined(NDEBUG) + const auto debugGroup = uploadPass.createDebugGroup(getName() + "-upload"); +#endif + + visitDrawables([&](gfx::Drawable& drawable) { + if (drawable.getEnabled()) { + auto& drawableWebGPU = static_cast(drawable); + drawableWebGPU.upload(uploadPass); + } + }); +} + +void TileLayerGroup::render(RenderOrchestrator&, PaintParameters& parameters) { + if (!enabled || !getDrawableCount() || !parameters.renderPass) { + return; + } + + auto& renderPass = static_cast(*parameters.renderPass); + + // `stencilModeFor3D` increments a shared mask value each call, so we cache the results + // when a layer contains 3D drawables to ensure consistent state across drawables. + bool features3d = false; + bool stencil3d = false; + std::optional depthMode3d; + std::optional stencilMode3d; + + if (stencilTiles && !stencilTiles->empty()) { + visitDrawables([&](const gfx::Drawable& drawable) { + if (drawable.getEnabled() && drawable.getIs3D() && drawable.hasRenderPass(parameters.pass)) { + features3d = true; + if (drawable.getEnableStencil()) { + stencil3d = true; + } + } + }); + } + +#if !defined(NDEBUG) + const auto debugGroup = parameters.encoder->createDebugGroup(getName() + "-render"); +#endif + + if (features3d) { + depthMode3d = parameters.depthModeFor3D(); + if (stencil3d) { + stencilMode3d = parameters.stencilModeFor3D(); + } + } else if (stencilTiles && !stencilTiles->empty()) { + // Handle stencil tiles if present (2D stenciling) + parameters.renderTileClippingMasks(stencilTiles); + } + + // Rely on drawables to provide their shaders; no layer-level override needed. + bool bindUBOs = false; + int visitCount = 0; + int drawCount = 0; + const auto initialDrawCalls = parameters.context.renderingStats().numDrawCalls; + visitDrawables([&](gfx::Drawable& drawable) { + visitCount++; + + if (!drawable.getEnabled() || !drawable.hasRenderPass(parameters.pass)) { + return; + } + drawCount++; + + if (!drawable.getShader()) { + mbgl::Log::Warning(mbgl::Event::Render, + "Drawable " + drawable.getName() + " in " + getName() + " missing shader; skipping"); + return; + } + + if (!bindUBOs) { + // Bind uniform buffers once for all drawables + static_cast(uniformBuffers).bindWebgpu(renderPass); + bindUBOs = true; + } + + // Copy shared uniform buffers from layer group to drawable before tweakers + // These are typically global paint params and evaluated props + auto& drawableWebGPU = static_cast(drawable); + + // Copy any layer-provided uniform buffers so drawables can build bind groups from them. + // This includes consolidated per-drawable data (e.g. idFillDrawableUBO) which lives in + // the layer UBO array and is selected via the UBO index each drawable writes later. + auto& drawableUniforms = drawableWebGPU.mutableUniformBuffers(); + for (size_t i = 0; i < uniformBuffers.allocatedSize(); ++i) { + const auto& uniformBuffer = uniformBuffers.get(i); + if (uniformBuffer) { + drawableUniforms.set(i, uniformBuffer); + } + } + + // Execute tweakers - they create drawable-specific uniform buffers like FillDrawableUBO + for (const auto& tweaker : drawable.getTweakers()) { + tweaker->execute(drawable, parameters); + } + + if (features3d) { + const auto depth = (drawable.getEnableDepth() && depthMode3d) ? *depthMode3d : gfx::DepthMode::disabled(); + const auto stencil = (drawable.getEnableStencil() && stencilMode3d) ? *stencilMode3d + : gfx::StencilMode::disabled(); + drawableWebGPU.setDepthModeFor3D(depth); + drawableWebGPU.setStencilModeFor3D(stencil); + } + + drawable.draw(parameters); + }); + + const auto finalDrawCalls = parameters.context.renderingStats().numDrawCalls; + if (drawCount > 0 && finalDrawCalls == initialDrawCalls) { + mbgl::Log::Warning( + mbgl::Event::Render, + getName() + " visited " + std::to_string(visitCount) + " drawables but produced no draw calls!"); + } +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/uniform_buffer.cpp b/src/mbgl/webgpu/uniform_buffer.cpp new file mode 100644 index 000000000..33a705ccd --- /dev/null +++ b/src/mbgl/webgpu/uniform_buffer.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +namespace { +constexpr std::size_t kUniformBufferAlignment = 256u; + +constexpr std::size_t alignedUniformSize(std::size_t size) { + if (size == 0u) { + return 0u; + } + return ((size + (kUniformBufferAlignment - 1u)) / kUniformBufferAlignment) * kUniformBufferAlignment; +} +} // namespace + +UniformBuffer::UniformBuffer(Context& context_, const void* data, std::size_t size_) + : gfx::UniformBuffer(alignedUniformSize(size_)), + context(context_) { + const std::size_t alignedSize = getSize(); + + auto& backend = static_cast(context.getBackend()); + WGPUDevice device = static_cast(backend.getDevice()); + + if (device && alignedSize > 0) { + WGPUBufferDescriptor bufferDesc = {}; + WGPUStringView label = {"Uniform Buffer", strlen("Uniform Buffer")}; + bufferDesc.label = label; + bufferDesc.size = alignedSize; + bufferDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; + bufferDesc.mappedAtCreation = data ? 1 : 0; + + buffer = wgpuDeviceCreateBuffer(device, &bufferDesc); + + if (buffer && data) { + void* mappedData = wgpuBufferGetMappedRange(buffer, 0, alignedSize); + if (mappedData) { + std::memcpy(mappedData, data, size_); + if (alignedSize > size_) { + std::memset(static_cast(mappedData) + size_, 0, alignedSize - size_); + } + wgpuBufferUnmap(buffer); + } + } + } +} + +UniformBuffer UniformBuffer::clone() const { + UniformBuffer newBuffer(context, nullptr, getSize()); + + // Note: We can't directly copy buffer contents in WebGPU without a command encoder + // The data will need to be copied when updated + + return newBuffer; +} + +UniformBuffer::UniformBuffer(UniformBuffer&& other) noexcept + : gfx::UniformBuffer(std::move(other)), + context(other.context), + buffer(other.buffer) { + other.buffer = nullptr; +} + +UniformBuffer::~UniformBuffer() { + if (buffer) { + wgpuBufferRelease(buffer); + buffer = nullptr; + } +} + +void UniformBuffer::update(const void* data, std::size_t dataSize) { + if (data && dataSize > 0 && buffer) { + auto& backend = static_cast(context.getBackend()); + WGPUQueue queue = static_cast(backend.getQueue()); + if (queue) { + wgpuQueueWriteBuffer(queue, buffer, 0, data, dataSize); + } + } +} + +// UniformBufferArray implementation + +void UniformBufferArray::bind(gfx::RenderPass& renderPass) { + // Note: In WebGPU, uniform buffers are bound through bind groups + // which are created at the drawable level with the pipeline. + // This method primarily ensures buffers are ready for use. + // The actual binding happens in Drawable::draw() via bind groups. + bindWebgpu(static_cast(renderPass)); +} + +// The copy() method is now implemented inline in the header file to match Metal + +void UniformBufferArray::bindWebgpu(RenderPass& renderPass) const noexcept { + static_cast(renderPass); + // In WebGPU, uniform buffers are bound through bind groups per drawable, + // not globally to the render pass like in Metal. + // The actual binding happens in Drawable::draw when creating the bind group. + // This method is here for API compatibility with Metal. +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/upload_pass.cpp b/src/mbgl/webgpu/upload_pass.cpp new file mode 100644 index 000000000..b9bf8987c --- /dev/null +++ b/src/mbgl/webgpu/upload_pass.cpp @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace webgpu { + +UploadPass::UploadPass(gfx::Renderable& renderable, CommandEncoder& commandEncoder_, const char* name) + : commandEncoder(commandEncoder_) { + // Push debug groups (Metal pattern) + commandEncoder.visitDebugGroups([this](const auto& group) { + debugGroups.emplace_back(gfx::DebugGroup{*this, group.c_str()}); + }); + + // Push the group for the name provided + debugGroups.emplace_back(gfx::DebugGroup{*this, name}); + + // Let the encoder pass along any groups pushed to it after this + commandEncoder.trackUploadPass(this); +} + +UploadPass::~UploadPass() { + commandEncoder.forgetUploadPass(this); + debugGroups.clear(); +} + +void UploadPass::pushDebugGroup(const char* /*name*/) { + // WebGPU debug groups are typically set on command encoders + // This would require access to the WebGPU command encoder + // For now, this is a no-op +} + +void UploadPass::popDebugGroup() { + // WebGPU debug groups are typically set on command encoders + // This would require access to the WebGPU command encoder + // For now, this is a no-op +} + +gfx::Context& UploadPass::getContext() { + return commandEncoder.getContext(); +} + +const gfx::Context& UploadPass::getContext() const { + return commandEncoder.getContext(); +} + +std::unique_ptr UploadPass::createVertexBufferResource(const void* data, + std::size_t size, + gfx::BufferUsageType /*usage*/, + bool persistent) { + auto& context = static_cast(getContext()); + BufferResource buffer(context, data, size, WGPUBufferUsage_Vertex, /*isIndexBuffer=*/false, persistent); + + if (!buffer.getBuffer()) { + mbgl::Log::Error(mbgl::Event::Render, " Failed to create vertex buffer!"); + } + + return std::make_unique(std::move(buffer)); +} + +void UploadPass::updateVertexBufferResource(gfx::VertexBufferResource& resource, const void* data, std::size_t size) { + auto& buffer = static_cast(resource); + buffer.update(data, size); +} + +std::unique_ptr UploadPass::createIndexBufferResource(const void* data, + std::size_t size, + gfx::BufferUsageType /*usage*/, + bool persistent) { + auto& context = static_cast(getContext()); + BufferResource buffer(context, data, size, WGPUBufferUsage_Index, /*isIndexBuffer=*/true, persistent); + return std::make_unique(std::move(buffer)); +} + +void UploadPass::updateIndexBufferResource(gfx::IndexBufferResource& resource, const void* data, std::size_t size) { + auto& buffer = static_cast(resource); + buffer.update(data, size); +} + +struct VertexBuffer : public gfx::VertexBufferBase { + ~VertexBuffer() override = default; + + std::unique_ptr resource; +}; + +namespace { +const std::unique_ptr noBuffer; +} + +const gfx::UniqueVertexBufferResource& UploadPass::getBuffer(const gfx::VertexVectorBasePtr& vec, + const gfx::BufferUsageType usage, + bool forceUpdate) { + if (vec) { + const auto* rawBufPtr = vec->getRawData(); + const auto rawBufSize = vec->getRawCount() * vec->getRawSize(); + + if (auto* rawData = static_cast(vec->getBuffer()); rawData && rawData->resource) { + auto& resource = static_cast(*rawData->resource); + + if (rawBufSize <= resource.getSizeInBytes()) { + if (forceUpdate || vec->isModifiedAfter(resource.getLastUpdated())) { + updateVertexBufferResource(resource, rawBufPtr, rawBufSize); + resource.setLastUpdated(vec->getLastModified()); + } + return rawData->resource; + } + } + if (rawBufSize > 0) { + auto buffer_ = std::make_unique(); + buffer_->resource = createVertexBufferResource(rawBufPtr, rawBufSize, usage, /*persistent=*/false); + vec->setBuffer(std::move(buffer_)); + return static_cast(vec->getBuffer())->resource; + } + } + return noBuffer; +} + +gfx::AttributeBindingArray UploadPass::buildAttributeBindings( + [[maybe_unused]] const std::size_t vertexCount, + [[maybe_unused]] const gfx::AttributeDataType vertexType, + [[maybe_unused]] const std::size_t vertexAttributeIndex, + [[maybe_unused]] const std::vector& vertexData, + const gfx::VertexAttributeArray& defaults, + const gfx::VertexAttributeArray& overrides, + const gfx::BufferUsageType usage, + const std::optional> lastUpdate, + /*out*/ std::vector>& vertexBuffers) { + gfx::AttributeBindingArray bindings; + bindings.resize(defaults.allocatedSize()); + + const auto resolveAttr = [&]([[maybe_unused]] const size_t id, auto& default_, auto& override_) -> void { + auto& effectiveAttr = override_ ? *override_ : default_; + const auto& defaultAttr = static_cast(default_); + const auto index = static_cast(defaultAttr.getIndex()); + + bindings.resize(std::max(bindings.size(), index + 1)); + + if (!override_) { + // No attribute was provided. WebGPU requires all vertex attributes to have buffers bound, + // so we create a dummy buffer with default values. + const auto stride = VertexAttribute::getStrideOf(defaultAttr.getDataType()); + const auto bufferSize = stride * vertexCount; + + // Create a buffer with appropriate default values + std::vector dummyData(bufferSize); + + // Fill with appropriate default values based on the attribute type + const auto dataType = defaultAttr.getDataType(); + if (dataType == gfx::AttributeDataType::Float4) { + // For color attribute - default to white (1,1,1,1) + float* floatData = reinterpret_cast(dummyData.data()); + for (size_t i = 0; i < vertexCount; ++i) { + floatData[i * 4 + 0] = 1.0f; // R + floatData[i * 4 + 1] = 1.0f; // G + floatData[i * 4 + 2] = 1.0f; // B + floatData[i * 4 + 3] = 1.0f; // A + } + } else if (dataType == gfx::AttributeDataType::Float2) { + // For opacity attribute - default to full opacity (1,1) + float* floatData = reinterpret_cast(dummyData.data()); + for (size_t i = 0; i < vertexCount; ++i) { + floatData[i * 2 + 0] = 1.0f; + floatData[i * 2 + 1] = 1.0f; + } + } else { + // For other types, fill with zeros + std::fill(dummyData.begin(), dummyData.end(), 0); + } + + // Create vertex buffer resource for the dummy data + auto dummyBuffer = createVertexBufferResource(dummyData.data(), + bufferSize, + usage, + /*persistent=*/false); + + bindings[index] = { + /*.attribute = */ {defaultAttr.getDataType(), /*offset=*/0}, + /*.vertexStride = */ static_cast(stride), + /*.vertexBufferResource = */ dummyBuffer.get(), + /*.vertexOffset = */ 0, + }; + + // Store the buffer so it doesn't get destroyed + vertexBuffers.push_back(std::move(dummyBuffer)); + + return; + } + + // If the attribute references data shared with a bucket, get the corresponding buffer. + if (const auto& buffer_ = getBuffer(effectiveAttr.getSharedRawData(), usage, !lastUpdate)) { + assert(effectiveAttr.getSharedStride() * effectiveAttr.getSharedVertexOffset() < + effectiveAttr.getSharedRawData()->getRawSize() * effectiveAttr.getSharedRawData()->getRawCount()); + + bindings[index] = { + /*.attribute = */ {effectiveAttr.getSharedType(), effectiveAttr.getSharedOffset()}, + /*.vertexStride = */ effectiveAttr.getSharedStride(), + /*.vertexBufferResource = */ buffer_.get(), + /*.vertexOffset = */ effectiveAttr.getSharedVertexOffset(), + }; + return; + } + + assert(effectiveAttr.getStride() > 0); + + // Otherwise, turn the data managed by the attribute into a buffer. + if (const auto& buffer_ = VertexAttribute::getBuffer( + effectiveAttr, *this, gfx::BufferUsageType::StaticDraw, !lastUpdate)) { + bindings[index] = { + /*.attribute = */ {effectiveAttr.getDataType(), /*offset=*/0}, + /*.vertexStride = */ static_cast(effectiveAttr.getStride()), + /*.vertexBufferResource = */ buffer_.get(), + /*.vertexOffset = */ 0, + }; + return; + } + + Log::Error(Event::Render, "Failed to create buffer for attribute"); + assert(false); + }; + + // This version is called when the attribute is available, but isn't being used by the shader + const auto missingAttr = [&](const size_t, auto& attr) -> void { + attr->setDirty(false); + }; + + defaults.resolve(overrides, resolveAttr, missingAttr); + + return bindings; +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/vertex_attribute.cpp b/src/mbgl/webgpu/vertex_attribute.cpp new file mode 100644 index 000000000..f6cba8f51 --- /dev/null +++ b/src/mbgl/webgpu/vertex_attribute.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace webgpu { + +const gfx::UniqueVertexBufferResource& VertexAttribute::getBuffer(gfx::VertexAttribute& attrib_, + UploadPass& uploadPass, + const gfx::BufferUsageType usage, + bool forceUpdate) { + if (!attrib_.getBuffer()) { + auto& attrib = static_cast(attrib_); + if (attrib.sharedRawData) { + return uploadPass.getBuffer(attrib.sharedRawData, usage, forceUpdate); + } else { + if (!attrib.rawData.empty()) { + auto buffer = uploadPass.createVertexBufferResource( + attrib.rawData.data(), attrib.rawData.size(), usage, false); + attrib.setBuffer(std::move(buffer)); + attrib.setRawData({}); + attrib_.setDirty(false); + } else { + assert(false); + } + } + } + return attrib_.getBuffer(); +} + +bool VertexAttributeArray::isModifiedAfter(std::chrono::duration time) const { + // Use the base class implementation + return gfx::VertexAttributeArray::isModifiedAfter(time); +} + +} // namespace webgpu +} // namespace mbgl diff --git a/src/mbgl/webgpu/vertex_buffer_resource.cpp b/src/mbgl/webgpu/vertex_buffer_resource.cpp new file mode 100644 index 000000000..ba1f1cb8d --- /dev/null +++ b/src/mbgl/webgpu/vertex_buffer_resource.cpp @@ -0,0 +1,25 @@ +#include +#include + +namespace mbgl { +namespace webgpu { + +VertexBufferResource::VertexBufferResource(BufferResource&& buffer_) noexcept + : buffer(std::move(buffer_)) { + if (buffer.getBuffer()) { + auto& stats = buffer.getContext().renderingStats(); + stats.numVertexBuffers++; + stats.memVertexBuffers += buffer.getSizeInBytes(); + } +} + +VertexBufferResource::~VertexBufferResource() noexcept { + if (buffer.getBuffer()) { + auto& stats = buffer.getContext().renderingStats(); + stats.numVertexBuffers--; + stats.memVertexBuffers -= buffer.getSizeInBytes(); + } +} + +} // namespace webgpu +} // namespace mbgl diff --git a/test/BUILD.bazel b/test/BUILD.bazel index 6f34e0e7f..56a1c2cbc 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -50,7 +50,11 @@ cc_library( [ "**/*.test.cpp", ], - ), + exclude = ["renderer/wgsl_preprocessor.test.cpp"], + ) + select({ + "//:webgpu_renderer": ["renderer/wgsl_preprocessor.test.cpp"], + "//conditions:default": [], + }), copts = CPP_FLAGS + MAPLIBRE_FLAGS, visibility = [ "//platform/ios/test/core:__pkg__", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 05189deee..0517fbdd6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,6 +22,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/renderer/image_manager.test.cpp ${PROJECT_SOURCE_DIR}/test/renderer/pattern_atlas.test.cpp ${PROJECT_SOURCE_DIR}/test/renderer/shader_registry.test.cpp + $<$:${PROJECT_SOURCE_DIR}/test/renderer/wgsl_preprocessor.test.cpp> ${PROJECT_SOURCE_DIR}/test/sprite/sprite_loader.test.cpp ${PROJECT_SOURCE_DIR}/test/sprite/sprite_parser.test.cpp ${PROJECT_SOURCE_DIR}/test/src/mbgl/test/fixture_log_observer.cpp @@ -253,7 +254,7 @@ if (MLN_WITH_CLANG_TIDY) set_target_properties(mbgl-test PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") endif() -if (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL Linux) +if (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL Linux OR MLN_WITH_WEBGPU) target_compile_definitions( mbgl-test PRIVATE USE_CPP_TEST_SERVER diff --git a/test/renderer/wgsl_preprocessor.test.cpp b/test/renderer/wgsl_preprocessor.test.cpp new file mode 100644 index 000000000..5524c06b0 --- /dev/null +++ b/test/renderer/wgsl_preprocessor.test.cpp @@ -0,0 +1,285 @@ +#include +#include + +using namespace mbgl; +using namespace mbgl::webgpu::detail; + +TEST(ShaderGroupPreprocessor, isDirective_ValidDirectives) { + EXPECT_TRUE(isDirective("#ifdef FEATURE", "ifdef")); + EXPECT_TRUE(isDirective("#ifndef FEATURE", "ifndef")); + EXPECT_TRUE(isDirective("#else", "else")); + EXPECT_TRUE(isDirective("#endif", "endif")); + + // With leading whitespace + EXPECT_TRUE(isDirective(" #ifdef FEATURE", "ifdef")); + EXPECT_TRUE(isDirective("\t#ifdef FEATURE", "ifdef")); + EXPECT_TRUE(isDirective(" \t #ifdef FEATURE", "ifdef")); + + // With whitespace after # + EXPECT_TRUE(isDirective("# ifdef FEATURE", "ifdef")); + EXPECT_TRUE(isDirective("#\tifdef FEATURE", "ifdef")); + EXPECT_TRUE(isDirective("# ifdef FEATURE", "ifdef")); +} + +TEST(ShaderGroupPreprocessor, isDirective_InvalidDirectives) { + EXPECT_FALSE(isDirective("ifdef FEATURE", "ifdef")); // Missing # + EXPECT_FALSE(isDirective("#ifdef FEATURE", "ifndef")); // Wrong directive + EXPECT_FALSE(isDirective("#ifdefine FEATURE", "ifdef")); // Similar but not exact + EXPECT_FALSE(isDirective("// #ifdef FEATURE", "ifdef")); // Commented out + EXPECT_FALSE(isDirective("", "ifdef")); // Empty line + EXPECT_FALSE(isDirective(" ", "ifdef")); // Whitespace only +} + +TEST(ShaderGroupPreprocessor, getDirectiveArgument_ValidArguments) { + EXPECT_EQ(getDirectiveArgument("#ifdef FEATURE"), "FEATURE"); + EXPECT_EQ(getDirectiveArgument("#ifndef DEBUG_MODE"), "DEBUG_MODE"); + EXPECT_EQ(getDirectiveArgument("#ifdef FEATURE"), "FEATURE"); // Extra spaces + EXPECT_EQ(getDirectiveArgument(" #ifdef FEATURE"), "FEATURE"); // Leading whitespace + EXPECT_EQ(getDirectiveArgument("#ifdef\tFEATURE"), "FEATURE"); // Tab separator +} + +TEST(ShaderGroupPreprocessor, getDirectiveArgument_InvalidOrEmpty) { + EXPECT_EQ(getDirectiveArgument(""), ""); // Empty line + EXPECT_EQ(getDirectiveArgument("#ifdef"), ""); // No argument + EXPECT_EQ(getDirectiveArgument("#else"), ""); // No argument expected + EXPECT_EQ(getDirectiveArgument("#endif"), ""); // No argument expected + EXPECT_EQ(getDirectiveArgument("no hash symbol"), ""); // Invalid format +} + +TEST(ShaderGroupPreprocessor, getDirectiveArgument_MultipleWords) { + // Should only return the first word + EXPECT_EQ(getDirectiveArgument("#ifdef FEATURE_A FEATURE_B"), "FEATURE_A"); + EXPECT_EQ(getDirectiveArgument("#ifdef FEATURE\nmore text"), "FEATURE"); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_SimpleIfdef) { + std::unordered_map defines; + defines["ENABLED"] = true; + + std::string source = R"( +line1 +#ifdef ENABLED +line2 +#endif +line3 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); + EXPECT_NE(result.find("line3"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_IfdefDisabled) { + std::unordered_map defines; + // ENABLED is not defined + + std::string source = R"( +line1 +#ifdef ENABLED +line2 +#endif +line3 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_EQ(result.find("line2"), std::string::npos); // Should be excluded + EXPECT_NE(result.find("line3"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_Ifndef) { + std::unordered_map defines; + // DISABLED is not defined + + std::string source = R"( +line1 +#ifndef DISABLED +line2 +#endif +line3 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); // Should be included + EXPECT_NE(result.find("line3"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_IfndefWithDefine) { + std::unordered_map defines; + defines["DISABLED"] = true; + + std::string source = R"( +line1 +#ifndef DISABLED +line2 +#endif +line3 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_EQ(result.find("line2"), std::string::npos); // Should be excluded + EXPECT_NE(result.find("line3"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_Else) { + std::unordered_map defines; + defines["ENABLED"] = true; + + std::string source = R"( +#ifdef ENABLED +line1 +#else +line2 +#endif +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_EQ(result.find("line2"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_ElseInverted) { + std::unordered_map defines; + // ENABLED is not defined + + std::string source = R"( +#ifdef ENABLED +line1 +#else +line2 +#endif +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_EQ(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_NestedConditionals) { + std::unordered_map defines; + defines["OUTER"] = true; + defines["INNER"] = true; + + std::string source = R"( +line1 +#ifdef OUTER +line2 +#ifdef INNER +line3 +#endif +line4 +#endif +line5 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); + EXPECT_NE(result.find("line3"), std::string::npos); + EXPECT_NE(result.find("line4"), std::string::npos); + EXPECT_NE(result.find("line5"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_NestedConditionalsPartiallyEnabled) { + std::unordered_map defines; + defines["OUTER"] = true; + // INNER is not defined + + std::string source = R"( +line1 +#ifdef OUTER +line2 +#ifdef INNER +line3 +#endif +line4 +#endif +line5 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); + EXPECT_EQ(result.find("line3"), std::string::npos); // Inner block excluded + EXPECT_NE(result.find("line4"), std::string::npos); + EXPECT_NE(result.find("line5"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_ComplexNesting) { + std::unordered_map defines; + defines["A"] = true; + defines["B"] = true; + + std::string source = R"( +#ifdef A +insideA1 +#ifdef B +insideB +#else +notB +#endif +insideA2 +#endif +outside +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("insideA1"), std::string::npos); + EXPECT_NE(result.find("insideB"), std::string::npos); + EXPECT_EQ(result.find("notB"), std::string::npos); + EXPECT_NE(result.find("insideA2"), std::string::npos); + EXPECT_NE(result.find("outside"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_EmptySource) { + std::unordered_map defines; + + std::string source = ""; + std::string result = preprocessWGSL(source, defines); + EXPECT_EQ(result, ""); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_NoConditionals) { + std::unordered_map defines; + + std::string source = R"( +line1 +line2 +line3 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); + EXPECT_NE(result.find("line3"), std::string::npos); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_PreservesNewlines) { + std::unordered_map defines; + defines["ENABLED"] = true; + + std::string source = "line1\n#ifdef ENABLED\nline2\n#endif\nline3"; + std::string result = preprocessWGSL(source, defines); + + // Count newlines - should have 3 (after line1, line2, line3) + size_t newlineCount = std::count(result.begin(), result.end(), '\n'); + EXPECT_EQ(newlineCount, 3); +} + +TEST(ShaderGroupPreprocessor, preprocessWGSL_MismatchedEndif) { + std::unordered_map defines; + + // Extra endif should be ignored gracefully + std::string source = R"( +line1 +#endif +line2 +)"; + + std::string result = preprocessWGSL(source, defines); + EXPECT_NE(result.find("line1"), std::string::npos); + EXPECT_NE(result.find("line2"), std::string::npos); +} diff --git a/vendor/dawn.cmake b/vendor/dawn.cmake new file mode 100644 index 000000000..5f80cb736 --- /dev/null +++ b/vendor/dawn.cmake @@ -0,0 +1,100 @@ +# Dawn WebGPU Implementation Configuration + +if(TARGET mbgl-vendor-dawn) + return() +endif() + +if(NOT MLN_WITH_WEBGPU) + return() +endif() + +if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) +endif() + +set(MLN_DAWN_GIT_VERSION "v20251014.163906" CACHE STRING "Git ref (branch, tag, or commit) used when fetching Dawn") + +message(STATUS "Configuring Dawn WebGPU implementation (${MLN_DAWN_GIT_VERSION})") + +set(_mln_dawn_source_dir "${PROJECT_SOURCE_DIR}/vendor/dawn") +set(_mln_dawn_binary_dir "${CMAKE_BINARY_DIR}/vendor/dawn/build") + + +if(EXISTS "${_mln_dawn_source_dir}/CMakeLists.txt") + set(FETCHCONTENT_SOURCE_DIR_MAPLIBRE_DAWN ${_mln_dawn_source_dir}) +endif() + +include(FetchContent) + +FetchContent_Declare(maplibre_dawn + GIT_REPOSITORY https://github.com/google/dawn.git + GIT_TAG ${MLN_DAWN_GIT_VERSION} + GIT_SUBMODULES "" +) + +FetchContent_GetProperties(maplibre_dawn) +if(NOT maplibre_dawn_POPULATED) + message(STATUS "Fetching Dawn sources into ${_mln_dawn_source_dir}") + FetchContent_Populate(maplibre_dawn) + + if(NOT EXISTS "${_mln_dawn_source_dir}/CMakeLists.txt") + file(REMOVE_RECURSE "${_mln_dawn_source_dir}") + file(RENAME "${maplibre_dawn_SOURCE_DIR}" "${_mln_dawn_source_dir}") + # Make future configure runs reuse the relocated checkout. + set(FETCHCONTENT_SOURCE_DIR_MAPLIBRE_DAWN "${_mln_dawn_source_dir}" CACHE PATH "" FORCE) + endif() +endif() + +set(maplibre_dawn_SOURCE_DIR "${_mln_dawn_source_dir}") + +set(DAWN_DIR "${_mln_dawn_source_dir}") +set(DAWN_BUILD_DIR "${_mln_dawn_binary_dir}") + +# Configure Dawn the same way as the quickstart recommends. +set(DAWN_FETCH_DEPENDENCIES ON CACHE BOOL "Use Dawn's dependency bootstrapper" FORCE) +set(DAWN_ENABLE_INSTALL ON CACHE BOOL "Generate install targets for Dawn" FORCE) +set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "Disable Dawn samples" FORCE) +set(DAWN_BUILD_TESTS OFF CACHE BOOL "Disable Dawn tests" FORCE) +set(DAWN_BUILD_BENCHMARKS OFF CACHE BOOL "Disable Dawn benchmarks" FORCE) +set(DAWN_BUILD_NODE_BINDINGS OFF CACHE BOOL "Disable Dawn Node bindings" FORCE) +set(DAWN_WERROR OFF CACHE BOOL "Disable -Werror in Dawn" FORCE) + +if(NOT TARGET dawn::webgpu_dawn) + if(APPLE) + enable_language(OBJC OBJCXX) + endif() + add_subdirectory(${DAWN_DIR} ${DAWN_BUILD_DIR}) +endif() + +add_library(mbgl-vendor-dawn INTERFACE) + +target_link_libraries(mbgl-vendor-dawn INTERFACE dawn::webgpu_dawn) + +target_include_directories(mbgl-vendor-dawn + SYSTEM INTERFACE + ${DAWN_DIR}/include + ${DAWN_BUILD_DIR}/gen/include +) + +target_compile_definitions(mbgl-vendor-dawn + INTERFACE + MLN_WITH_DAWN=1 + WEBGPU_BACKEND_DAWN=1 +) + +if(APPLE) + target_compile_definitions(mbgl-vendor-dawn INTERFACE DAWN_ENABLE_BACKEND_METAL=1) +else() + target_compile_definitions(mbgl-vendor-dawn INTERFACE DAWN_ENABLE_BACKEND_VULKAN=1) +endif() + +target_compile_options(mbgl-vendor-dawn INTERFACE -Wno-strict-aliasing -Wno-error=strict-aliasing) + +set_target_properties( + mbgl-vendor-dawn + PROPERTIES + INTERFACE_MAPLIBRE_NAME "dawn" + INTERFACE_MAPLIBRE_URL "https://dawn.googlesource.com/dawn" + INTERFACE_MAPLIBRE_AUTHOR "Chromium Dawn Team" + INTERFACE_MAPLIBRE_LICENSE "${PROJECT_SOURCE_DIR}/vendor/dawn/LICENSE" +) diff --git a/vendor/webgpu.cmake b/vendor/webgpu.cmake new file mode 100644 index 000000000..a08c596e6 --- /dev/null +++ b/vendor/webgpu.cmake @@ -0,0 +1,18 @@ +# WebGPU implementation configuration +# This file handles the integration of either Dawn or wgpu + +set(MLN_WEBGPU_IMPL "dawn" CACHE STRING "WebGPU backend implementation (dawn or wgpu)") +set_property(CACHE MLN_WEBGPU_IMPL PROPERTY STRINGS dawn wgpu stub) + +if(MLN_WITH_WEBGPU) + if(MLN_WEBGPU_IMPL STREQUAL "dawn") + message(STATUS "Using Dawn as WebGPU backend") + + elseif(MLN_WEBGPU_IMPL STREQUAL "wgpu") + message(FATAL_ERROR "Using wgpu as WebGPU backend is not implemented yet") + + else() + message(FATAL_ERROR "MLN_WEBGPU_IMPL can take values \"dawn\" or \"wgpu\"") + + endif() +endif()