WebGPU - Add support for wgpu (#3899)
This commit is contained in:
16
.github/workflows/macos-ci.yml
vendored
16
.github/workflows/macos-ci.yml
vendored
@@ -102,8 +102,8 @@ jobs:
|
||||
variant:
|
||||
- impl: dawn
|
||||
name: Dawn
|
||||
# - impl: wgpu
|
||||
# name: wgpu-native
|
||||
- impl: wgpu
|
||||
name: wgpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4
|
||||
@@ -134,7 +134,17 @@ jobs:
|
||||
- name: Build targets
|
||||
run: |
|
||||
cmake --build build-macos-webgpu-${{ matrix.variant.impl }} --target \
|
||||
mbgl-render-test-runner
|
||||
mbgl-render-test-runner \
|
||||
mbgl-expression-test \
|
||||
mbgl-test-runner
|
||||
|
||||
- name: Run unit tests
|
||||
id: unit_test
|
||||
run: build-macos-webgpu-${{ matrix.variant.impl }}/mbgl-test-runner --gtest_filter=-Map.StyleNetworkErrorRetry:LocalGlyphRasterizer.PingFang:LocalGlyphRasterizer.PingFangWithBoldInStyle:LocalGlyphRasterizer.PingFangSemibold:Map.Offline
|
||||
|
||||
- name: Run expression tests
|
||||
id: expression_test
|
||||
run: build-macos-webgpu-${{ matrix.variant.impl }}/expression-test/mbgl-expression-test
|
||||
|
||||
- name: Prepare render test output directory
|
||||
run: mkdir -p metrics/macos-webgpu-${{ matrix.variant.impl }}
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -103,3 +103,9 @@
|
||||
[submodule "vendor/kdbush.hpp"]
|
||||
path = vendor/kdbush.hpp
|
||||
url = https://github.com/mourner/kdbush.hpp.git
|
||||
[submodule "vendor/webgpu-cpp"]
|
||||
path = vendor/webgpu-cpp
|
||||
url = https://github.com/eliemichel/WebGPU-Cpp.git
|
||||
[submodule "vendor/wgpu-native"]
|
||||
path = vendor/wgpu-native
|
||||
url = https://github.com/gfx-rs/wgpu-native.git
|
||||
|
||||
@@ -1415,10 +1415,18 @@ if(MLN_WITH_WEBGPU)
|
||||
include(${PROJECT_SOURCE_DIR}/vendor/webgpu.cmake)
|
||||
|
||||
# Include Dawn integration when requested
|
||||
if(MLN_WEBGPU_IMPL STREQUAL "dawn")
|
||||
if(MLN_WEBGPU_IMPL_DAWN)
|
||||
include(${PROJECT_SOURCE_DIR}/vendor/dawn.cmake)
|
||||
if(TARGET mbgl-vendor-dawn)
|
||||
target_link_libraries(mbgl-core PUBLIC mbgl-vendor-dawn)
|
||||
target_link_libraries(mbgl-core PRIVATE mbgl-vendor-dawn)
|
||||
endif()
|
||||
elseif(MLN_WEBGPU_IMPL_WGPU)
|
||||
# Include wgpu-native integration
|
||||
include(${PROJECT_SOURCE_DIR}/vendor/wgpu.cmake)
|
||||
if(TARGET mbgl-vendor-wgpu)
|
||||
target_link_libraries(mbgl-core PRIVATE mbgl-vendor-wgpu)
|
||||
# Add WebGPU-Cpp implementation file (required for wgpu-native backend)
|
||||
list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/webgpu/webgpu_cpp_impl.cpp)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -1633,6 +1641,10 @@ if(TARGET mbgl-vendor-dawn)
|
||||
list(APPEND EXPORT_TARGETS mbgl-vendor-dawn)
|
||||
endif()
|
||||
|
||||
if(TARGET mbgl-vendor-wgpu)
|
||||
list(APPEND EXPORT_TARGETS mbgl-vendor-wgpu)
|
||||
endif()
|
||||
|
||||
export(TARGETS ${EXPORT_TARGETS} FILE MapboxCoreTargets.cmake)
|
||||
|
||||
if(MLN_WITH_VULKAN)
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"inherits": "macos",
|
||||
"cacheVariables": {
|
||||
"MLN_WITH_WEBGPU": "ON",
|
||||
"MLN_WEBGPU_IMPL": "dawn",
|
||||
"MLN_WEBGPU_IMPL_DAWN": "ON",
|
||||
"MLN_DARWIN_USE_LIBUV": "ON"
|
||||
}
|
||||
},
|
||||
@@ -101,7 +101,7 @@
|
||||
"inherits": "macos",
|
||||
"cacheVariables": {
|
||||
"MLN_WITH_WEBGPU": "ON",
|
||||
"MLN_WEBGPU_IMPL": "wgpu",
|
||||
"MLN_WEBGPU_IMPL_WGPU": "ON",
|
||||
"MLN_DARWIN_USE_LIBUV": "ON"
|
||||
}
|
||||
},
|
||||
@@ -149,7 +149,7 @@
|
||||
"inherits": "linux",
|
||||
"cacheVariables": {
|
||||
"MLN_WITH_WEBGPU": "ON",
|
||||
"MLN_WEBGPU_IMPL": "dawn",
|
||||
"MLN_WEBGPU_IMPL_DAWN": "ON",
|
||||
"MLN_WITH_WAYLAND": "OFF",
|
||||
"MLN_WITH_X11": "ON"
|
||||
}
|
||||
@@ -160,7 +160,7 @@
|
||||
"inherits": "linux",
|
||||
"cacheVariables": {
|
||||
"MLN_WITH_WEBGPU": "ON",
|
||||
"MLN_WEBGPU_IMPL": "wgpu",
|
||||
"MLN_WEBGPU_IMPL_WGPU": "ON",
|
||||
"MLN_WITH_WAYLAND": "OFF",
|
||||
"MLN_WITH_X11": "ON"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public:
|
||||
~DrawScopeResource() override = default;
|
||||
|
||||
private:
|
||||
Context& context;
|
||||
[[maybe_unused]] Context& context;
|
||||
};
|
||||
|
||||
} // namespace webgpu
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
#include <mbgl/gfx/renderer_backend.hpp>
|
||||
#include <mbgl/util/size.hpp>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <webgpu/webgpu.h>
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
#include <webgpu.h>
|
||||
#endif
|
||||
|
||||
#include <mbgl/webgpu/wgpu_cpp_compat.hpp>
|
||||
#include <memory>
|
||||
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
|
||||
#endif
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// For Dawn backend, use Dawn's C++ wrapper
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// For wgpu-native backend, use the WebGPU-Cpp wrapper
|
||||
#include <webgpu.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@@ -84,6 +84,9 @@ if(MLN_WITH_WEBGPU)
|
||||
if(TARGET mbgl-vendor-dawn)
|
||||
target_link_libraries(mbgl-glfw PRIVATE mbgl-vendor-dawn)
|
||||
endif()
|
||||
if(TARGET mbgl-vendor-wgpu)
|
||||
target_link_libraries(mbgl-glfw PRIVATE mbgl-vendor-wgpu)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(DEFINED MLN_WITH_X11 AND MLN_WITH_X11)
|
||||
|
||||
@@ -295,7 +295,10 @@ GLFWView::GLFWView(bool fullscreen_,
|
||||
|
||||
// "... applications will typically want to set the swap interval to one"
|
||||
// https://www.glfw.org/docs/latest/quick.html#quick_swap_buffers
|
||||
|
||||
#if defined(MLN_RENDER_BACKEND_OPENGL)
|
||||
glfwSwapInterval(1);
|
||||
#endif
|
||||
|
||||
glfwGetWindowSize(window, &width, &height);
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ struct GLFWwindow;
|
||||
struct WGPUDeviceImpl;
|
||||
struct WGPUSurfaceImpl;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
namespace dawn::native {
|
||||
class Instance;
|
||||
class Adapter;
|
||||
} // namespace dawn::native
|
||||
#endif
|
||||
|
||||
// Multiple inheritance: GLFWBackend for window management,
|
||||
// webgpu::RendererBackend for rendering, gfx::Renderable for framebuffer
|
||||
@@ -63,7 +65,12 @@ private:
|
||||
mutable SpinLock textureStateLock;
|
||||
|
||||
GLFWwindow* window;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
std::unique_ptr<dawn::native::Instance> instance;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpu::Instance wgpuInstance;
|
||||
#endif
|
||||
wgpu::Device wgpuDevice; // This owns the device
|
||||
wgpu::Queue queue;
|
||||
wgpu::Surface wgpuSurface;
|
||||
@@ -100,6 +107,9 @@ private:
|
||||
std::atomic<int> consecutiveErrors{0};
|
||||
static constexpr int maxConsecutiveErrors = 3;
|
||||
|
||||
// VSync control
|
||||
bool enableVSync = true;
|
||||
|
||||
// Helper methods
|
||||
void reconfigureSurface();
|
||||
void createDepthStencilTexture(uint32_t width, uint32_t height);
|
||||
@@ -107,13 +117,4 @@ private:
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -17,22 +17,39 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <Metal/Metal.h>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <dawn/native/MetalBackend.h>
|
||||
#endif
|
||||
|
||||
#elif defined(__linux__)
|
||||
#define GLFW_EXPOSE_NATIVE_X11
|
||||
#define GLFW_EXPOSE_NATIVE_WAYLAND
|
||||
#include <GLFW/glfw3native.h>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <dawn/native/VulkanBackend.h>
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef None
|
||||
#undef None
|
||||
#endif
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <dawn/native/DawnNative.h>
|
||||
#endif
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <webgpu/webgpu_glfw.h>
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
#include <webgpu/wgpu.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
@@ -47,8 +64,24 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
// Helper macro for label assignment (wgpu-native needs WGPUStringView)
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Dawn uses simple const char* labels - just assign directly inline
|
||||
#define WGPU_LABEL(desc, labelStr) \
|
||||
(desc.label = labelStr)
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
#define WGPU_LABEL(desc, labelStr) \
|
||||
do { \
|
||||
static constexpr char label_text[] = labelStr; \
|
||||
WGPUStringView label_view = {label_text, sizeof(label_text) - 1}; \
|
||||
desc.label = label_view; \
|
||||
} while(0)
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
void logUncapturedError(const wgpu::Device&, wgpu::ErrorType type, wgpu::StringView message) {
|
||||
mbgl::Log::Error(mbgl::Event::Render,
|
||||
std::string("Dawn validation error [") +
|
||||
@@ -82,6 +115,14 @@ std::optional<WGPUBackendType> desiredBackendFromEnv() {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool backendMatches(WGPUBackendType candidate, WGPUBackendType desired) {
|
||||
if (desired == WGPUBackendType_OpenGL) {
|
||||
return candidate == WGPUBackendType_OpenGL || candidate == WGPUBackendType_OpenGLES;
|
||||
}
|
||||
return candidate == desired;
|
||||
}
|
||||
#endif // MLN_WEBGPU_IMPL_DAWN
|
||||
|
||||
const char* backendTypeToString(WGPUBackendType type) {
|
||||
switch (type) {
|
||||
case WGPUBackendType_Null:
|
||||
@@ -105,13 +146,6 @@ const char* backendTypeToString(WGPUBackendType type) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -214,8 +248,8 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
static_cast<uint32_t>(std::max(fbHeight, 0))};
|
||||
}(),
|
||||
std::make_unique<mbgl::WebGPURenderableResource>(*this)),
|
||||
window(window_) {
|
||||
static_cast<void>(capFrameRate);
|
||||
window(window_),
|
||||
enableVSync(capFrameRate) {
|
||||
|
||||
// Add small delay to let previous resources clean up
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
@@ -230,6 +264,9 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
|
||||
// Create Dawn instance
|
||||
instance = std::make_unique<dawn::native::Instance>();
|
||||
|
||||
@@ -249,7 +286,27 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
throw std::runtime_error("No WebGPU adapters found after retries");
|
||||
}
|
||||
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// Create wgpu-native instance
|
||||
// Try without extras first to see if the instance can be created at all
|
||||
wgpuInstance = wgpu::createInstance();
|
||||
if (!wgpuInstance) {
|
||||
throw std::runtime_error("Failed to create WebGPU instance");
|
||||
}
|
||||
|
||||
// Request adapter
|
||||
wgpu::RequestAdapterOptions adapterOpts = {};
|
||||
adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance;
|
||||
|
||||
wgpu::Adapter selectedAdapter = wgpuInstance.requestAdapter(adapterOpts);
|
||||
if (!selectedAdapter) {
|
||||
throw std::runtime_error("No WebGPU adapter found");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
std::size_t selectedIndex = 0;
|
||||
if (auto targettedBackend = desiredBackendFromEnv()) {
|
||||
bool matched = false;
|
||||
@@ -291,9 +348,32 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
}
|
||||
}
|
||||
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// Log adapter info for wgpu-native
|
||||
{
|
||||
WGPUAdapterInfo info = {};
|
||||
if (wgpuAdapterGetInfo(static_cast<WGPUAdapter>(selectedAdapter), &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("<unknown>")));
|
||||
wgpuAdapterInfoFreeMembers(info);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<wgpu::FeatureName> supportedFeatures;
|
||||
WGPUSupportedFeatures features = WGPU_SUPPORTED_FEATURES_INIT;
|
||||
|
||||
WGPUSupportedFeatures features = {};
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuAdapterGetFeatures(selectedAdapter.Get(), &features);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuAdapterGetFeatures(static_cast<WGPUAdapter>(selectedAdapter), &features);
|
||||
#endif
|
||||
|
||||
if (features.featureCount > 0 && features.features) {
|
||||
supportedFeatures.reserve(features.featureCount);
|
||||
for (size_t i = 0; i < features.featureCount; ++i) {
|
||||
@@ -314,12 +394,24 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
|
||||
|
||||
wgpu::DeviceDescriptor deviceDesc = {};
|
||||
deviceDesc.label = "MapLibre WebGPU Device";
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
deviceDesc.requiredFeatures = requiredFeatures.data();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
deviceDesc.requiredFeatures = reinterpret_cast<const WGPUFeatureName*>(requiredFeatures.data());
|
||||
#endif
|
||||
deviceDesc.requiredFeatureCount = requiredFeatures.size();
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Dawn supports error callbacks at device creation time
|
||||
deviceDesc.label = "MapLibre WebGPU Device";
|
||||
deviceDesc.SetUncapturedErrorCallback(logUncapturedError);
|
||||
deviceDesc.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous, logDeviceLost);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// wgpu-native: Use label through descriptor, callbacks are set after device creation
|
||||
WGPU_LABEL(deviceDesc, "MapLibre WebGPU Device");
|
||||
#endif
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Create device with descriptor
|
||||
WGPUDevice rawDevice = selectedAdapter.CreateDevice(&deviceDesc);
|
||||
if (!rawDevice) {
|
||||
@@ -333,6 +425,13 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
}
|
||||
|
||||
wgpuDevice = wgpu::Device::Acquire(rawDevice);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// Create device with wgpu-native
|
||||
wgpuDevice = selectedAdapter.requestDevice(deviceDesc);
|
||||
if (!wgpuDevice) {
|
||||
throw std::runtime_error("Failed to create WebGPU device");
|
||||
}
|
||||
#endif
|
||||
|
||||
setDepthStencilFormat(depthStencilFormat);
|
||||
|
||||
@@ -350,7 +449,14 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
|
||||
// Create CAMetalLayer
|
||||
CAMetalLayer* layer = [CAMetalLayer layer];
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// For Dawn, use Dawn's native API to get Metal device
|
||||
layer.device = dawn::native::metal::GetMTLDevice(wgpuDevice.Get());
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// For wgpu-native, don't set the device - the surface configuration will handle it
|
||||
// Setting it to a different device could cause performance issues
|
||||
#endif
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
if (colorSpace) {
|
||||
layer.colorspace = colorSpace;
|
||||
@@ -369,20 +475,40 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
|
||||
[view setLayer:layer];
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Create surface from metal layer
|
||||
wgpu::SurfaceSourceMetalLayer metalDesc;
|
||||
metalDesc.layer = (__bridge void*)layer;
|
||||
|
||||
wgpu::SurfaceDescriptor surfaceDesc;
|
||||
surfaceDesc.nextInChain = &metalDesc;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpu::SurfaceSourceMetalLayer metalDesc = {};
|
||||
metalDesc.chain.sType = wgpu::SType::SurfaceSourceMetalLayer;
|
||||
metalDesc.chain.next = nullptr;
|
||||
metalDesc.layer = (__bridge void*)layer;
|
||||
|
||||
wgpu::SurfaceDescriptor surfaceDesc = {};
|
||||
surfaceDesc.nextInChain = reinterpret_cast<const WGPUChainedStruct*>(&metalDesc);
|
||||
#endif
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Create surface using the instance
|
||||
wgpu::Instance wgpuInstance(instance->Get());
|
||||
wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface = wgpuInstance.createSurface(surfaceDesc);
|
||||
#endif
|
||||
|
||||
if (wgpuSurface) {
|
||||
WGPUSurfaceCapabilities capabilities = WGPU_SURFACE_CAPABILITIES_INIT;
|
||||
|
||||
WGPUSurfaceCapabilities capabilities = {};
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
if (wgpuSurfaceGetCapabilities(wgpuSurface.Get(), selectedAdapter.Get(), &capabilities) == WGPUStatus_Success) {
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
if (wgpuSurfaceGetCapabilities(static_cast<WGPUSurface>(wgpuSurface), static_cast<WGPUAdapter>(selectedAdapter), &capabilities) == WGPUStatus_Success) {
|
||||
#endif
|
||||
const auto pickFormat = [&]() {
|
||||
const wgpu::TextureFormat preferredFormats[] = {
|
||||
wgpu::TextureFormat::BGRA8Unorm,
|
||||
@@ -410,7 +536,11 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
layer.pixelFormat = toMetalPixelFormat(swapChainFormat);
|
||||
|
||||
// Get the queue
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
queue = wgpuDevice.GetQueue();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
queue = wgpuDevice.getQueue();
|
||||
#endif
|
||||
|
||||
// Configure surface
|
||||
wgpu::SurfaceConfiguration config = {};
|
||||
@@ -420,12 +550,20 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
config.alphaMode = wgpu::CompositeAlphaMode::Auto;
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
config.presentMode = wgpu::PresentMode::Fifo;
|
||||
// Use Immediate mode (no vsync) in benchmark mode, Fifo (vsync) otherwise
|
||||
config.presentMode = enableVSync ? wgpu::PresentMode::Fifo : wgpu::PresentMode::Immediate;
|
||||
configuredViewFormats[0] = swapChainFormat;
|
||||
config.viewFormatCount = 1;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
config.viewFormats = configuredViewFormats.data();
|
||||
|
||||
wgpuSurface.Configure(&config);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
config.viewFormats = reinterpret_cast<const WGPUTextureFormat*>(configuredViewFormats.data());
|
||||
wgpuSurface.configure(config);
|
||||
#endif
|
||||
|
||||
surfaceConfigured = true;
|
||||
lastConfiguredSize = {static_cast<uint32_t>(width), static_cast<uint32_t>(height)};
|
||||
|
||||
@@ -437,7 +575,9 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
std::string windowSizeMsg = "Window size: " + std::to_string(width) + "x" + std::to_string(height);
|
||||
|
||||
// Create surface for WebGPU
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpu::Instance wgpuInstance(instance->Get());
|
||||
#endif
|
||||
|
||||
bool surfaceCreated = false;
|
||||
|
||||
@@ -447,7 +587,9 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
Window x11Window = glfwGetX11Window(window);
|
||||
|
||||
if (x11Display && x11Window) {
|
||||
wgpu::SurfaceDescriptorFromXlibWindow x11Desc = {};
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Dawn uses a different structure without explicit chain member
|
||||
wgpu::SurfaceSourceXlibWindow x11Desc = {};
|
||||
x11Desc.display = x11Display;
|
||||
x11Desc.window = x11Window;
|
||||
|
||||
@@ -455,6 +597,19 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
surfaceDesc.nextInChain = &x11Desc;
|
||||
|
||||
wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// wgpu-native uses explicit chain members
|
||||
wgpu::SurfaceSourceXlibWindow x11Desc = {};
|
||||
x11Desc.chain.sType = wgpu::SType::SurfaceSourceXlibWindow;
|
||||
x11Desc.chain.next = nullptr;
|
||||
x11Desc.display = x11Display;
|
||||
x11Desc.window = x11Window;
|
||||
|
||||
wgpu::SurfaceDescriptor surfaceDesc = {};
|
||||
surfaceDesc.nextInChain = reinterpret_cast<const WGPUChainedStruct*>(&x11Desc);
|
||||
|
||||
wgpuSurface = wgpuInstance.createSurface(surfaceDesc);
|
||||
#endif
|
||||
surfaceCreated = true;
|
||||
}
|
||||
}
|
||||
@@ -475,7 +630,11 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
wgpu::SurfaceDescriptor surfaceDesc = {};
|
||||
surfaceDesc.nextInChain = &waylandDesc;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuSurface = wgpuInstance.CreateSurface(&surfaceDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface = wgpuInstance.createSurface(surfaceDesc);
|
||||
#endif
|
||||
surfaceCreated = true;
|
||||
}
|
||||
}
|
||||
@@ -494,8 +653,13 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
}
|
||||
|
||||
if (wgpuSurface) {
|
||||
WGPUSurfaceCapabilities capabilities = WGPU_SURFACE_CAPABILITIES_INIT;
|
||||
|
||||
WGPUSurfaceCapabilities capabilities = {};
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
if (wgpuSurfaceGetCapabilities(wgpuSurface.Get(), selectedAdapter.Get(), &capabilities) == WGPUStatus_Success) {
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
if (wgpuSurfaceGetCapabilities(static_cast<WGPUSurface>(wgpuSurface), static_cast<WGPUAdapter>(selectedAdapter), &capabilities) == WGPUStatus_Success) {
|
||||
#endif
|
||||
const auto pickFormat = [&]() {
|
||||
const wgpu::TextureFormat preferredFormats[] = {
|
||||
wgpu::TextureFormat::BGRA8Unorm,
|
||||
@@ -521,7 +685,11 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
setColorFormat(swapChainFormat);
|
||||
|
||||
// Get the queue
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
queue = wgpuDevice.GetQueue();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
queue = wgpuDevice.getQueue();
|
||||
#endif
|
||||
|
||||
// Configure surface
|
||||
wgpu::SurfaceConfiguration config = {};
|
||||
@@ -531,20 +699,34 @@ GLFWWebGPUBackend::GLFWWebGPUBackend(GLFWwindow* window_, bool capFrameRate)
|
||||
config.alphaMode = wgpu::CompositeAlphaMode::Auto;
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
config.presentMode = wgpu::PresentMode::Fifo;
|
||||
// Use Immediate mode (no vsync) in benchmark mode, Fifo (vsync) otherwise
|
||||
config.presentMode = enableVSync ? wgpu::PresentMode::Fifo : wgpu::PresentMode::Immediate;
|
||||
configuredViewFormats[0] = swapChainFormat;
|
||||
config.viewFormatCount = 1;
|
||||
config.viewFormats = configuredViewFormats.data();
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
config.viewFormats = configuredViewFormats.data();
|
||||
wgpuSurface.Configure(&config);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
config.viewFormats = reinterpret_cast<const WGPUTextureFormat*>(configuredViewFormats.data());
|
||||
wgpuSurface.configure(config);
|
||||
#endif
|
||||
|
||||
surfaceConfigured = true;
|
||||
lastConfiguredSize = {static_cast<uint32_t>(width), static_cast<uint32_t>(height)};
|
||||
#endif
|
||||
|
||||
// Store WebGPU instance, device and queue in the base class
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
setInstance(instance->Get());
|
||||
setDevice(reinterpret_cast<void*>(wgpuDevice.Get()));
|
||||
setQueue(reinterpret_cast<void*>(queue.Get()));
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
setInstance(static_cast<WGPUInstance>(wgpuInstance));
|
||||
setDevice(static_cast<WGPUDevice>(wgpuDevice));
|
||||
setQueue(static_cast<WGPUQueue>(queue));
|
||||
#endif
|
||||
|
||||
// Make sure the window is visible and focused
|
||||
glfwShowWindow(window);
|
||||
@@ -591,7 +773,12 @@ GLFWWebGPUBackend::~GLFWWebGPUBackend() {
|
||||
|
||||
// Unconfigure surface before releasing device
|
||||
if (wgpuSurface && surfaceConfigured) {
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuSurface.Unconfigure();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface.unconfigure();
|
||||
#endif
|
||||
surfaceConfigured = false;
|
||||
}
|
||||
|
||||
@@ -600,8 +787,13 @@ GLFWWebGPUBackend::~GLFWWebGPUBackend() {
|
||||
queue = nullptr; // Queue is owned by device
|
||||
wgpuDevice = nullptr; // Device depends on instance
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Release Dawn instance last (it owns the backend)
|
||||
instance.reset();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuInstance = nullptr;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
mbgl::gfx::RendererBackend& GLFWWebGPUBackend::getRendererBackend() {
|
||||
@@ -626,7 +818,7 @@ void GLFWWebGPUBackend::swap() {
|
||||
}
|
||||
|
||||
// Keep track of swap calls for debugging
|
||||
static int swapCount = 0;
|
||||
[[maybe_unused]] 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<std::chrono::milliseconds>(now - lastSwapTime).count();
|
||||
@@ -643,12 +835,6 @@ void GLFWWebGPUBackend::swap() {
|
||||
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.
|
||||
|
||||
@@ -670,16 +856,19 @@ void GLFWWebGPUBackend::swap() {
|
||||
std::lock_guard<SpinLock> guard(textureStateLock);
|
||||
|
||||
if (currentTextureView && !framePresented) {
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuSurface.Present();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface.present();
|
||||
#endif
|
||||
framePresented = true;
|
||||
|
||||
// Move current resources to previous (keep them alive)
|
||||
previousTextureView = std::move(currentTextureView);
|
||||
previousTexture = std::move(currentTexture);
|
||||
|
||||
// Clear current references
|
||||
// Release resources immediately (wgpu-native performs better with immediate release)
|
||||
currentTextureView = nullptr;
|
||||
currentTexture = nullptr;
|
||||
previousTextureView = nullptr;
|
||||
previousTexture = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,12 +934,6 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
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
|
||||
@@ -762,18 +945,18 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
std::unique_lock<SpinLock> lock(textureStateLock);
|
||||
|
||||
if (currentTextureView && !framePresented) {
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
return reinterpret_cast<void*>(currentTextureView.Get());
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
return reinterpret_cast<void*>(static_cast<WGPUTextureView>(currentTextureView));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
if (framePresented) {
|
||||
// Always release textures before acquiring new ones to avoid "already acquired" error
|
||||
currentTextureView = nullptr;
|
||||
currentTexture = nullptr;
|
||||
}
|
||||
|
||||
bool expected = false;
|
||||
if (!frameInProgress.compare_exchange_strong(expected, true)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
framePresented = false;
|
||||
|
||||
@@ -782,17 +965,20 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
wgpu::SurfaceTexture surfaceTexture;
|
||||
|
||||
try {
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuSurface.GetCurrentTexture(&surfaceTexture);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface.getCurrentTexture(&surfaceTexture);
|
||||
#endif
|
||||
|
||||
} catch (...) {
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!surfaceTexture.texture) {
|
||||
surfaceNeedsReconfigure = true;
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -801,7 +987,6 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
status == wgpu::SurfaceGetCurrentTextureStatus::Lost) {
|
||||
surfaceNeedsReconfigure = true;
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -809,7 +994,6 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
(status != wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal &&
|
||||
status != wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal)) {
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -818,13 +1002,18 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
// Create texture view with explicit descriptor
|
||||
wgpu::TextureViewDescriptor viewDesc = {};
|
||||
viewDesc.format = swapChainFormat;
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
|
||||
#endif
|
||||
|
||||
viewDesc.baseMipLevel = 0;
|
||||
viewDesc.mipLevelCount = 1;
|
||||
viewDesc.baseArrayLayer = 0;
|
||||
viewDesc.arrayLayerCount = 1;
|
||||
viewDesc.aspect = wgpu::TextureAspect::All;
|
||||
viewDesc.label = "SwapChain TextureView";
|
||||
WGPU_LABEL(viewDesc, "SwapChain TextureView");
|
||||
|
||||
// Store the texture to keep it alive
|
||||
currentTexture = surfaceTexture.texture;
|
||||
@@ -832,12 +1021,15 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
// Create the texture view with validation
|
||||
wgpu::TextureView newView;
|
||||
try {
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
newView = currentTexture.CreateView(&viewDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
newView = currentTexture.createView(viewDesc);
|
||||
#endif
|
||||
} catch (...) {
|
||||
// Handle any exceptions from Dawn
|
||||
currentTexture = nullptr;
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -845,7 +1037,6 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
if (!newView) {
|
||||
currentTexture = nullptr;
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -856,7 +1047,6 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
if (!currentTextureView) {
|
||||
currentTexture = nullptr;
|
||||
consecutiveErrors++;
|
||||
frameInProgress = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -867,7 +1057,11 @@ void* GLFWWebGPUBackend::getCurrentTextureView() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
return reinterpret_cast<void*>(currentTextureView.Get());
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
return reinterpret_cast<void*>(static_cast<WGPUTextureView>(currentTextureView));
|
||||
#endif
|
||||
}
|
||||
|
||||
mbgl::Size GLFWWebGPUBackend::getFramebufferSize() const {
|
||||
@@ -885,7 +1079,14 @@ void* GLFWWebGPUBackend::getDepthStencilView() {
|
||||
createDepthStencilTexture(static_cast<uint32_t>(std::max(width, 0)),
|
||||
static_cast<uint32_t>(std::max(height, 0)));
|
||||
}
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
return depthStencilView ? reinterpret_cast<void*>(depthStencilView.Get()) : nullptr;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
return depthStencilView ? reinterpret_cast<void*>(static_cast<WGPUTextureView>(depthStencilView)) : nullptr;
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
void GLFWWebGPUBackend::reconfigureSurface() {
|
||||
@@ -933,9 +1134,14 @@ void GLFWWebGPUBackend::reconfigureSurface() {
|
||||
config.alphaMode = wgpu::CompositeAlphaMode::Auto;
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
config.presentMode = wgpu::PresentMode::Fifo;
|
||||
// Use Immediate mode (no vsync) in benchmark mode, Fifo (vsync) otherwise
|
||||
config.presentMode = enableVSync ? wgpu::PresentMode::Fifo : wgpu::PresentMode::Immediate;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuSurface.Configure(&config);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpuSurface.configure(config);
|
||||
#endif
|
||||
|
||||
createDepthStencilTexture(static_cast<uint32_t>(width), static_cast<uint32_t>(height));
|
||||
|
||||
@@ -947,9 +1153,22 @@ void GLFWWebGPUBackend::reconfigureSurface() {
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Dawn: Process device events for resource lifecycle management
|
||||
// This ticks the Dawn device to process pending callbacks and resource cleanup
|
||||
if (wgpuDevice) {
|
||||
wgpuDeviceTick(wgpuDevice.Get());
|
||||
}
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// wgpu-native: Use wgpuDevicePoll to process GPU work
|
||||
// This is the lightweight equivalent of Dawn's wgpuDeviceTick
|
||||
// wgpuDevicePoll with wait=false just processes pending GPU work without blocking
|
||||
if (wgpuDevice) {
|
||||
wgpuDevicePoll(static_cast<WGPUDevice>(wgpuDevice), false, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void GLFWWebGPUBackend::createDepthStencilTexture(uint32_t width, uint32_t height) {
|
||||
@@ -961,15 +1180,27 @@ void GLFWWebGPUBackend::createDepthStencilTexture(uint32_t width, uint32_t heigh
|
||||
}
|
||||
|
||||
wgpu::TextureDescriptor depthDesc = {};
|
||||
depthDesc.label = "DepthStencilTexture";
|
||||
WGPU_LABEL(depthDesc, "DepthStencilTexture");
|
||||
depthDesc.usage = wgpu::TextureUsage::RenderAttachment;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthDesc.dimension = wgpu::TextureDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
depthDesc.dimension = wgpu::TextureDimension::_2D;
|
||||
#endif
|
||||
|
||||
|
||||
depthDesc.size = {width, height, 1};
|
||||
depthDesc.format = depthStencilFormat;
|
||||
depthDesc.mipLevelCount = 1;
|
||||
depthDesc.sampleCount = 1;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthStencilTexture = wgpuDevice.CreateTexture(&depthDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
depthStencilTexture = wgpuDevice.createTexture(depthDesc);
|
||||
#endif
|
||||
|
||||
if (!depthStencilTexture) {
|
||||
mbgl::Log::Warning(mbgl::Event::Render, "WebGPU: Failed to create depth/stencil texture");
|
||||
depthStencilFormat = wgpu::TextureFormat::Undefined;
|
||||
@@ -978,16 +1209,27 @@ void GLFWWebGPUBackend::createDepthStencilTexture(uint32_t width, uint32_t heigh
|
||||
}
|
||||
|
||||
wgpu::TextureViewDescriptor viewDesc = {};
|
||||
viewDesc.label = "DepthStencilTextureView";
|
||||
WGPU_LABEL(viewDesc, "DepthStencilTextureView");
|
||||
viewDesc.format = depthDesc.format;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
|
||||
#endif
|
||||
|
||||
viewDesc.baseMipLevel = 0;
|
||||
viewDesc.mipLevelCount = 1;
|
||||
viewDesc.baseArrayLayer = 0;
|
||||
viewDesc.arrayLayerCount = 1;
|
||||
viewDesc.aspect = wgpu::TextureAspect::All;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthStencilView = depthStencilTexture.CreateView(&viewDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
depthStencilView = depthStencilTexture.createView(viewDesc);
|
||||
#endif
|
||||
|
||||
if (!depthStencilView) {
|
||||
mbgl::Log::Warning(mbgl::Event::Render, "WebGPU: Failed to create depth/stencil view");
|
||||
depthStencilTexture = nullptr;
|
||||
@@ -1004,8 +1246,6 @@ bool GLFWWebGPUBackend::waitForFrame(std::chrono::milliseconds timeout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
processEvents();
|
||||
|
||||
if (std::chrono::steady_clock::now() - start >= timeout) {
|
||||
return false;
|
||||
}
|
||||
@@ -1040,159 +1280,3 @@ void GLFWWebGPUBackend::periodicMaintenance() {
|
||||
lastMaintenanceTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
void GLFWWebGPUBackend::ensureDebugTriangleResources() {
|
||||
if (debugTriangleInitialized || !wgpuDevice || !queue) {
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr char shaderSource[] = R"(
|
||||
struct VertexOut {
|
||||
@builtin(position) position : vec4<f32>,
|
||||
@location(0) color : vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(@location(0) position : vec2<f32>,
|
||||
@location(1) color : vec3<f32>) -> VertexOut {
|
||||
var out : VertexOut;
|
||||
out.position = vec4<f32>(position, 0.0, 1.0);
|
||||
out.color = color;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input : VertexOut) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(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<float>(lastConfiguredSize.width), static_cast<float>(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);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,52 @@ std::size_t hashStencilMode(const gfx::StencilMode& mode) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::size_t hashPipelineState(const std::vector<WGPUVertexBufferLayout>& vertexLayouts,
|
||||
const gfx::ColorMode& colorMode,
|
||||
const gfx::DepthMode& depthMode,
|
||||
const gfx::StencilMode& stencilMode,
|
||||
gfx::DrawModeType drawModeType) {
|
||||
std::size_t hash = 0;
|
||||
|
||||
// Hash vertex layouts
|
||||
for (const auto& layout : vertexLayouts) {
|
||||
hash = util::hash(hash, static_cast<uint32_t>(layout.arrayStride));
|
||||
hash = util::hash(hash, static_cast<uint32_t>(layout.stepMode));
|
||||
hash = util::hash(hash, static_cast<uint32_t>(layout.attributeCount));
|
||||
for (uint32_t i = 0; i < layout.attributeCount && i < 16; ++i) {
|
||||
hash = util::hash(hash,
|
||||
static_cast<uint32_t>(layout.attributes[i].format),
|
||||
static_cast<uint32_t>(layout.attributes[i].shaderLocation),
|
||||
static_cast<uint32_t>(layout.attributes[i].offset));
|
||||
}
|
||||
}
|
||||
|
||||
// Hash color mode
|
||||
hash = util::hash(hash, colorMode.mask.r, colorMode.mask.g, colorMode.mask.b, colorMode.mask.a);
|
||||
colorMode.blendFunction.match([&](const auto& blendFunc) {
|
||||
using T = std::decay_t<decltype(blendFunc)>;
|
||||
if constexpr (std::is_same_v<T, gfx::ColorMode::Replace>) {
|
||||
hash = util::hash(hash, 0);
|
||||
} else {
|
||||
hash = util::hash(hash,
|
||||
static_cast<int>(blendFunc.equation),
|
||||
static_cast<int>(blendFunc.srcFactor),
|
||||
static_cast<int>(blendFunc.dstFactor));
|
||||
}
|
||||
});
|
||||
|
||||
// Hash depth mode
|
||||
hash = util::hash(hash, static_cast<int>(depthMode.func), static_cast<int>(depthMode.mask));
|
||||
|
||||
// Hash stencil mode
|
||||
hash = util::hash(hash, hashStencilMode(stencilMode));
|
||||
|
||||
// Hash draw mode
|
||||
hash = util::hash(hash, static_cast<int>(drawModeType));
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
WGPUVertexFormat wgpuVertexFormatOf(gfx::AttributeDataType type) {
|
||||
switch (type) {
|
||||
case gfx::AttributeDataType::Byte:
|
||||
@@ -762,6 +808,10 @@ void Drawable::draw(PaintParameters& parameters) const {
|
||||
drawModeType = impl->segments.front()->getMode().type;
|
||||
}
|
||||
|
||||
// Compute pipeline state hash for caching
|
||||
const std::size_t pipelineHash = hashPipelineState(
|
||||
vertexLayouts, colorMode, depthMode, stencilMode, drawModeType);
|
||||
|
||||
impl->pipelineState = shaderWebGPU.getRenderPipeline(renderable,
|
||||
vertexLayouts.empty() ? nullptr : vertexLayouts.data(),
|
||||
vertexLayouts.size(),
|
||||
@@ -769,7 +819,7 @@ void Drawable::draw(PaintParameters& parameters) const {
|
||||
depthMode,
|
||||
stencilMode,
|
||||
drawModeType,
|
||||
std::nullopt);
|
||||
pipelineHash);
|
||||
if (!impl->pipelineState) {
|
||||
Log::Warning(Event::Render,
|
||||
"WebGPU: skipping pipeline creation for drawable '" + getName() + "' (shader='" +
|
||||
|
||||
@@ -15,7 +15,7 @@ DrawableBuilder::DrawableBuilder(std::string name_)
|
||||
DrawableBuilder::~DrawableBuilder() = default;
|
||||
|
||||
std::unique_ptr<gfx::Drawable> DrawableBuilder::createDrawable() const {
|
||||
return std::make_unique<Drawable>(name);
|
||||
return std::make_unique<Drawable>(drawableName);
|
||||
}
|
||||
|
||||
std::unique_ptr<gfx::Drawable::DrawSegment> DrawableBuilder::createSegment(gfx::DrawMode drawMode,
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
#include <mbgl/webgpu/offscreen_texture.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
#include <mbgl/webgpu/wgpu_cpp_compat.hpp>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <dawn/native/DawnNative.h>
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
#include <wgpu.h>
|
||||
#endif
|
||||
|
||||
namespace mbgl {
|
||||
namespace webgpu {
|
||||
@@ -87,7 +92,12 @@ private:
|
||||
|
||||
class HeadlessBackend::Impl {
|
||||
public:
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
std::unique_ptr<dawn::native::Instance> instance;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
wgpu::Instance instance;
|
||||
wgpu::Adapter adapter;
|
||||
#endif
|
||||
wgpu::Device device;
|
||||
wgpu::Queue queue;
|
||||
wgpu::Texture offscreenTexture;
|
||||
@@ -105,6 +115,7 @@ HeadlessBackend::HeadlessBackend(Size size_, SwapBehaviour swapBehaviour_, gfx::
|
||||
|
||||
impl->framebufferSize = size_;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Initialize Dawn instance with TimedWaitAny feature enabled
|
||||
wgpu::InstanceFeatureName timedWaitAnyFeature = wgpu::InstanceFeatureName::TimedWaitAny;
|
||||
wgpu::InstanceDescriptor instanceDesc = {};
|
||||
@@ -142,6 +153,44 @@ HeadlessBackend::HeadlessBackend(Size size_, SwapBehaviour swapBehaviour_, gfx::
|
||||
setDevice(impl->device.Get());
|
||||
setQueue(impl->queue.Get());
|
||||
setInstance(impl->instance->Get());
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
// wgpu-native backend initialization
|
||||
wgpu::InstanceDescriptor instanceDesc = {};
|
||||
impl->instance = wgpu::createInstance(instanceDesc);
|
||||
|
||||
if (!impl->instance) {
|
||||
Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create instance");
|
||||
return;
|
||||
}
|
||||
|
||||
// Request adapter using simpler blocking API
|
||||
wgpu::RequestAdapterOptions adapterOpts = {};
|
||||
adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance;
|
||||
|
||||
impl->adapter = impl->instance.requestAdapter(adapterOpts);
|
||||
|
||||
if (!impl->adapter) {
|
||||
Log::Error(Event::Render, "WebGPU HeadlessBackend: No adapter found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Request device using simpler blocking API
|
||||
wgpu::DeviceDescriptor deviceDesc = {};
|
||||
|
||||
impl->device = impl->adapter.requestDevice(deviceDesc);
|
||||
|
||||
if (!impl->device) {
|
||||
Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create device");
|
||||
return;
|
||||
}
|
||||
|
||||
impl->queue = impl->device.getQueue();
|
||||
|
||||
// Store device, queue, and instance in base class
|
||||
setDevice(static_cast<WGPUDevice>(impl->device));
|
||||
setQueue(static_cast<WGPUQueue>(impl->queue));
|
||||
setInstance(static_cast<WGPUInstance>(impl->instance));
|
||||
#endif
|
||||
|
||||
// Create offscreen render texture
|
||||
createOffscreenTextures();
|
||||
@@ -177,55 +226,94 @@ void HeadlessBackend::createOffscreenTextures() {
|
||||
// Create offscreen color texture
|
||||
wgpu::TextureDescriptor colorDesc = {};
|
||||
colorDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
colorDesc.dimension = wgpu::TextureDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
colorDesc.dimension = wgpu::TextureDimension::_2D;
|
||||
#endif
|
||||
colorDesc.size = {
|
||||
static_cast<uint32_t>(impl->framebufferSize.width), static_cast<uint32_t>(impl->framebufferSize.height), 1};
|
||||
colorDesc.format = wgpu::TextureFormat::RGBA8Unorm;
|
||||
colorDesc.mipLevelCount = 1;
|
||||
colorDesc.sampleCount = 1;
|
||||
colorDesc.label = "Headless Color Texture";
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
colorDesc.label = "Headless Color Texture";
|
||||
impl->offscreenTexture = impl->device.CreateTexture(&colorDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->offscreenTexture = impl->device.createTexture(colorDesc);
|
||||
|
||||
#endif
|
||||
|
||||
if (impl->offscreenTexture) {
|
||||
wgpu::TextureViewDescriptor viewDesc = {};
|
||||
viewDesc.format = wgpu::TextureFormat::RGBA8Unorm;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
|
||||
#endif
|
||||
viewDesc.baseMipLevel = 0;
|
||||
viewDesc.mipLevelCount = 1;
|
||||
viewDesc.baseArrayLayer = 0;
|
||||
viewDesc.arrayLayerCount = 1;
|
||||
viewDesc.aspect = wgpu::TextureAspect::All;
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.label = "Headless Color TextureView";
|
||||
|
||||
impl->offscreenTextureView = impl->offscreenTexture.CreateView(&viewDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->offscreenTextureView = impl->offscreenTexture.createView(viewDesc);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Create depth/stencil texture
|
||||
wgpu::TextureDescriptor depthDesc = {};
|
||||
depthDesc.usage = wgpu::TextureUsage::RenderAttachment;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthDesc.dimension = wgpu::TextureDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
depthDesc.dimension = wgpu::TextureDimension::_2D;
|
||||
#endif
|
||||
depthDesc.size = {
|
||||
static_cast<uint32_t>(impl->framebufferSize.width), static_cast<uint32_t>(impl->framebufferSize.height), 1};
|
||||
depthDesc.format = wgpu::TextureFormat::Depth24PlusStencil8;
|
||||
depthDesc.mipLevelCount = 1;
|
||||
depthDesc.sampleCount = 1;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthDesc.label = "Headless Depth/Stencil Texture";
|
||||
|
||||
impl->depthTexture = impl->device.CreateTexture(&depthDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->depthTexture = impl->device.createTexture(depthDesc);
|
||||
#endif
|
||||
|
||||
if (impl->depthTexture) {
|
||||
wgpu::TextureViewDescriptor viewDesc = {};
|
||||
viewDesc.format = wgpu::TextureFormat::Depth24PlusStencil8;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
|
||||
#endif
|
||||
viewDesc.baseMipLevel = 0;
|
||||
viewDesc.mipLevelCount = 1;
|
||||
viewDesc.baseArrayLayer = 0;
|
||||
viewDesc.arrayLayerCount = 1;
|
||||
viewDesc.aspect = wgpu::TextureAspect::All;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
viewDesc.label = "Headless Depth/Stencil TextureView";
|
||||
|
||||
impl->depthTextureView = impl->depthTexture.CreateView(&viewDesc);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->depthTextureView = impl->depthTexture.createView(viewDesc);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
#if MLN_WEBGPU_IMPL_WGPU
|
||||
#include <webgpu/wgpu.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t bytesPerRowAlignment = 256u;
|
||||
}
|
||||
@@ -145,7 +149,9 @@ public:
|
||||
}
|
||||
|
||||
// Process all pending device operations to ensure rendering is complete
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
wgpuDeviceTick(device);
|
||||
#endif
|
||||
|
||||
// Create staging buffer for readback
|
||||
WGPUBufferDescriptor bufferDesc = {};
|
||||
@@ -198,17 +204,19 @@ public:
|
||||
// Use WaitAny for proper synchronous blocking instead of polling
|
||||
struct MapContext {
|
||||
WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error;
|
||||
bool completed = false;
|
||||
};
|
||||
MapContext mapContext;
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
// Dawn: Use WaitAny for proper synchronous blocking
|
||||
WGPUBufferMapCallbackInfo callbackInfo = {};
|
||||
callbackInfo.mode = WGPUCallbackMode_WaitAnyOnly;
|
||||
callbackInfo.callback =
|
||||
[](WGPUMapAsyncStatus status, WGPUStringView message, void* userdata1, void* userdata2) {
|
||||
[](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void* userdata1, void* /*userdata2*/) {
|
||||
auto* ctx = static_cast<MapContext*>(userdata1);
|
||||
ctx->status = status;
|
||||
(void)message;
|
||||
(void)userdata2;
|
||||
ctx->completed = true;
|
||||
};
|
||||
callbackInfo.userdata1 = &mapContext;
|
||||
callbackInfo.userdata2 = nullptr;
|
||||
@@ -229,6 +237,38 @@ public:
|
||||
mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Buffer mapping wait failed");
|
||||
}
|
||||
}
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
|
||||
// wgpu-native: Use polling approach (WaitAny not implemented yet)
|
||||
// Use AllowProcessEvents callback mode for synchronous polling
|
||||
WGPUBufferMapCallbackInfo callbackInfo = {};
|
||||
callbackInfo.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||
callbackInfo.callback =
|
||||
[](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void* userdata1, void* /*userdata2*/) {
|
||||
auto* ctx = static_cast<MapContext*>(userdata1);
|
||||
ctx->status = status;
|
||||
ctx->completed = true;
|
||||
};
|
||||
callbackInfo.userdata1 = &mapContext;
|
||||
callbackInfo.userdata2 = nullptr;
|
||||
|
||||
wgpuBufferMapAsync(stagingBuffer, WGPUMapMode_Read, 0, alignedDataSize, callbackInfo);
|
||||
|
||||
// Poll device until mapping completes
|
||||
// wgpu-native processes callbacks during wgpuDevicePoll
|
||||
constexpr int maxIterations = 1000;
|
||||
for (int i = 0; i < maxIterations && !mapContext.completed; ++i) {
|
||||
wgpuDevicePoll(device, true, nullptr);
|
||||
if (!mapContext.completed) {
|
||||
// Small sleep to avoid busy-waiting
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (!mapContext.completed) {
|
||||
mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Buffer mapping timeout after polling");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check if mapping was successful and read the data
|
||||
if (mapContext.status == WGPUMapAsyncStatus_Success) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Include Dawn headers first to define WEBGPU_H_
|
||||
// Include WebGPU headers first to define WEBGPU_H_
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
#include <webgpu/webgpu.h>
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
#include <webgpu.h>
|
||||
#endif
|
||||
#include <mbgl/webgpu/wgpu_cpp_compat.hpp>
|
||||
|
||||
#include <mbgl/webgpu/render_pass.hpp>
|
||||
@@ -72,7 +76,12 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
|
||||
}
|
||||
|
||||
wgpuTextureViewAddRef(colorViewHandle);
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
impl->colorView = wgpu::TextureView::Acquire(colorViewHandle);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->colorView = wgpu::TextureView(colorViewHandle);
|
||||
#endif
|
||||
|
||||
if (auto colorFormat = renderableResource.getColorTextureFormat()) {
|
||||
backend.setColorFormat(*colorFormat);
|
||||
@@ -80,7 +89,13 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
|
||||
}
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = {};
|
||||
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
colorAttachment.view = impl->colorView.Get();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
colorAttachment.view = static_cast<WGPUTextureView>(impl->colorView);
|
||||
|
||||
#endif
|
||||
colorAttachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
colorAttachment.resolveTarget = nullptr;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
@@ -101,7 +116,7 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
|
||||
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;
|
||||
depthAttachment.depthReadOnly = false;
|
||||
if (descriptor.clearStencil) {
|
||||
depthAttachment.stencilLoadOp = WGPULoadOp_Clear;
|
||||
depthAttachment.stencilStoreOp = WGPUStoreOp_Store;
|
||||
@@ -111,7 +126,7 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
|
||||
depthAttachment.stencilStoreOp = WGPUStoreOp_Undefined;
|
||||
depthAttachment.stencilClearValue = 0u;
|
||||
}
|
||||
depthAttachment.stencilReadOnly = WGPU_FALSE;
|
||||
depthAttachment.stencilReadOnly = false;
|
||||
depthAttachment.view = nullptr;
|
||||
|
||||
WGPURenderPassDepthStencilAttachment* depthAttachmentPtr = nullptr;
|
||||
@@ -125,9 +140,17 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
|
||||
|
||||
if (depthViewHandle) {
|
||||
wgpuTextureViewAddRef(depthViewHandle);
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
impl->depthStencilView = wgpu::TextureView::Acquire(depthViewHandle);
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
impl->depthStencilView = wgpu::TextureView(depthViewHandle);
|
||||
#endif
|
||||
if (impl->depthStencilView) {
|
||||
#if MLN_WEBGPU_IMPL_DAWN
|
||||
depthAttachment.view = impl->depthStencilView.Get();
|
||||
#elif MLN_WEBGPU_IMPL_WGPU
|
||||
depthAttachment.view = static_cast<WGPUTextureView>(impl->depthStencilView);
|
||||
#endif
|
||||
depthAttachmentPtr = &depthAttachment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
namespace mbgl {
|
||||
namespace webgpu {
|
||||
|
||||
UploadPass::UploadPass(gfx::Renderable& renderable, CommandEncoder& commandEncoder_, const char* name)
|
||||
UploadPass::UploadPass([[maybe_unused]] gfx::Renderable& renderable, CommandEncoder& commandEncoder_, const char* name)
|
||||
: commandEncoder(commandEncoder_) {
|
||||
// Push debug groups (Metal pattern)
|
||||
commandEncoder.visitDebugGroups([this](const auto& group) {
|
||||
|
||||
6
src/mbgl/webgpu/webgpu_cpp_impl.cpp
Normal file
6
src/mbgl/webgpu/webgpu_cpp_impl.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
// Implementation file for WebGPU-Cpp wrapper
|
||||
// This file must be compiled exactly once to provide implementations
|
||||
// for the WebGPU C++ wrapper functions
|
||||
|
||||
#define WEBGPU_CPP_IMPLEMENTATION
|
||||
#include <webgpu.hpp>
|
||||
6
vendor/dawn.cmake
vendored
6
vendor/dawn.cmake
vendored
@@ -76,12 +76,6 @@ target_include_directories(mbgl-vendor-dawn
|
||||
${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()
|
||||
|
||||
1
vendor/webgpu-cpp
vendored
Submodule
1
vendor/webgpu-cpp
vendored
Submodule
Submodule vendor/webgpu-cpp added at b9507d9753
14
vendor/webgpu.cmake
vendored
14
vendor/webgpu.cmake
vendored
@@ -1,18 +1,20 @@
|
||||
# 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)
|
||||
set(MLN_WEBGPU_IMPL_DAWN OFF CACHE STRING "WebGPU with Dawn implementation")
|
||||
set(MLN_WEBGPU_IMPL_WGPU OFF CACHE STRING "WebGPU with wgpu implementation")
|
||||
|
||||
if(MLN_WITH_WEBGPU)
|
||||
if(MLN_WEBGPU_IMPL STREQUAL "dawn")
|
||||
if(MLN_WEBGPU_IMPL_DAWN)
|
||||
message(STATUS "Using Dawn as WebGPU backend")
|
||||
add_compile_definitions(MLN_WEBGPU_IMPL_DAWN)
|
||||
|
||||
elseif(MLN_WEBGPU_IMPL STREQUAL "wgpu")
|
||||
message(FATAL_ERROR "Using wgpu as WebGPU backend is not implemented yet")
|
||||
elseif(MLN_WEBGPU_IMPL_WGPU)
|
||||
message(STATUS "Using wgpu as WebGPU backend")
|
||||
add_compile_definitions(MLN_WEBGPU_IMPL_WGPU)
|
||||
|
||||
else()
|
||||
message(FATAL_ERROR "MLN_WEBGPU_IMPL can take values \"dawn\" or \"wgpu\"")
|
||||
message(FATAL_ERROR "MLN_WEBGPU_IMPL_DAWN or MLN_WEBGPU_IMPL_WGPU must be set")
|
||||
|
||||
endif()
|
||||
endif()
|
||||
|
||||
1
vendor/wgpu-native
vendored
Submodule
1
vendor/wgpu-native
vendored
Submodule
Submodule vendor/wgpu-native added at 74f8c24c90
262
vendor/wgpu.cmake
vendored
Normal file
262
vendor/wgpu.cmake
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
# wgpu-native WebGPU Implementation Configuration
|
||||
|
||||
if(TARGET mbgl-vendor-wgpu)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT MLN_WITH_WEBGPU)
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Configuring wgpu-native WebGPU implementation")
|
||||
|
||||
set(_mln_wgpu_source_dir "${PROJECT_SOURCE_DIR}/vendor/wgpu-native")
|
||||
|
||||
# Check if wgpu-native exists
|
||||
if(NOT EXISTS "${_mln_wgpu_source_dir}/ffi/webgpu-headers/webgpu.h")
|
||||
message(FATAL_ERROR
|
||||
"wgpu-native not found at ${_mln_wgpu_source_dir}. "
|
||||
"Please ensure the wgpu-native submodule is initialized: "
|
||||
"git submodule update --init --recursive vendor/wgpu-native")
|
||||
endif()
|
||||
|
||||
# wgpu-native provides pre-built binaries or can be built from source
|
||||
# We'll look for prebuilt binaries first, then fall back to building from source
|
||||
|
||||
# Platform-specific library names and paths
|
||||
if(APPLE)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
|
||||
set(_wgpu_lib_arch "aarch64-apple-darwin")
|
||||
else()
|
||||
set(_wgpu_lib_arch "x86_64-apple-darwin")
|
||||
endif()
|
||||
set(_wgpu_lib_name "libwgpu_native.a")
|
||||
set(_wgpu_lib_suffix ".dylib")
|
||||
elseif(WIN32)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_wgpu_lib_arch "x86_64-pc-windows-msvc")
|
||||
else()
|
||||
set(_wgpu_lib_arch "i686-pc-windows-msvc")
|
||||
endif()
|
||||
set(_wgpu_lib_name "wgpu_native.lib")
|
||||
set(_wgpu_lib_suffix ".dll")
|
||||
else() # Linux
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
|
||||
set(_wgpu_lib_arch "aarch64-unknown-linux-gnu")
|
||||
else()
|
||||
set(_wgpu_lib_arch "x86_64-unknown-linux-gnu")
|
||||
endif()
|
||||
set(_wgpu_lib_name "libwgpu_native.a")
|
||||
set(_wgpu_lib_suffix ".so")
|
||||
endif()
|
||||
|
||||
# Look for prebuilt wgpu-native library
|
||||
set(_wgpu_lib_search_paths
|
||||
"${_mln_wgpu_source_dir}/target/release"
|
||||
"${_mln_wgpu_source_dir}/target/${_wgpu_lib_arch}/release"
|
||||
"${_mln_wgpu_source_dir}/lib"
|
||||
"${_mln_wgpu_source_dir}/build/release"
|
||||
)
|
||||
|
||||
find_library(WGPU_LIBRARY
|
||||
NAMES wgpu_native libwgpu_native
|
||||
PATHS ${_wgpu_lib_search_paths}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
# If not found, try to build it using cargo
|
||||
if(NOT WGPU_LIBRARY)
|
||||
message(STATUS "Pre-built wgpu-native library not found, attempting to build from source...")
|
||||
|
||||
# Check if cargo is available
|
||||
find_program(CARGO_EXECUTABLE cargo)
|
||||
|
||||
if(NOT CARGO_EXECUTABLE)
|
||||
message(FATAL_ERROR
|
||||
"wgpu-native library not found and cargo is not available to build it. "
|
||||
"Please either:\n"
|
||||
" 1. Install Rust and cargo (https://rustup.rs/), or\n"
|
||||
" 2. Download pre-built wgpu-native binaries from https://github.com/gfx-rs/wgpu-native/releases\n"
|
||||
" and place them in ${_mln_wgpu_source_dir}/target/release/")
|
||||
endif()
|
||||
|
||||
# Build wgpu-native using cargo
|
||||
message(STATUS "Building wgpu-native with cargo (this may take a few minutes)...")
|
||||
|
||||
execute_process(
|
||||
COMMAND ${CARGO_EXECUTABLE} build --release
|
||||
WORKING_DIRECTORY ${_mln_wgpu_source_dir}
|
||||
RESULT_VARIABLE _cargo_result
|
||||
OUTPUT_VARIABLE _cargo_output
|
||||
ERROR_VARIABLE _cargo_error
|
||||
)
|
||||
|
||||
if(NOT _cargo_result EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"Failed to build wgpu-native:\n${_cargo_error}\n"
|
||||
"You can try building manually:\n"
|
||||
" cd ${_mln_wgpu_source_dir} && cargo build --release")
|
||||
endif()
|
||||
|
||||
# Try to find the library again
|
||||
find_library(WGPU_LIBRARY
|
||||
NAMES wgpu_native libwgpu_native
|
||||
PATHS ${_wgpu_lib_search_paths}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
if(NOT WGPU_LIBRARY)
|
||||
message(FATAL_ERROR "Failed to locate wgpu-native library after building")
|
||||
endif()
|
||||
|
||||
message(STATUS "Successfully built wgpu-native: ${WGPU_LIBRARY}")
|
||||
else()
|
||||
message(STATUS "Found wgpu-native library: ${WGPU_LIBRARY}")
|
||||
endif()
|
||||
|
||||
# Generate WebGPU-Cpp wrapper if needed
|
||||
set(_webgpu_cpp_dir "${PROJECT_SOURCE_DIR}/vendor/webgpu-cpp")
|
||||
set(_webgpu_cpp_header "${_webgpu_cpp_dir}/webgpu.hpp")
|
||||
|
||||
if(NOT EXISTS "${_webgpu_cpp_header}")
|
||||
message(STATUS "Generating WebGPU-Cpp wrapper...")
|
||||
|
||||
# Check if Python 3 is available
|
||||
find_program(PYTHON_EXECUTABLE python3 python)
|
||||
if(NOT PYTHON_EXECUTABLE)
|
||||
message(FATAL_ERROR
|
||||
"Python is required to generate WebGPU-Cpp wrapper. "
|
||||
"Please install Python 3.")
|
||||
endif()
|
||||
|
||||
# Run the generator script using the local webgpu.h from wgpu-native
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} generate.py
|
||||
--use-init-macros
|
||||
--header ${_mln_wgpu_source_dir}/ffi/webgpu-headers/webgpu.h
|
||||
WORKING_DIRECTORY ${_webgpu_cpp_dir}
|
||||
RESULT_VARIABLE _gen_result
|
||||
OUTPUT_VARIABLE _gen_output
|
||||
ERROR_VARIABLE _gen_error
|
||||
)
|
||||
|
||||
if(NOT _gen_result EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"Failed to generate WebGPU-Cpp wrapper:\n${_gen_error}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Successfully generated WebGPU-Cpp wrapper")
|
||||
endif()
|
||||
|
||||
# Create compatibility shims for Dawn header paths if they don't exist
|
||||
set(_compat_shim_dir "${_webgpu_cpp_dir}/wgpu-native/webgpu")
|
||||
set(_compat_shim_header "${_compat_shim_dir}/webgpu_cpp.h")
|
||||
set(_compat_shim_webgpu_h "${_compat_shim_dir}/webgpu.h")
|
||||
|
||||
if(NOT EXISTS "${_compat_shim_header}")
|
||||
message(STATUS "Creating WebGPU-Cpp compatibility shim...")
|
||||
|
||||
file(MAKE_DIRECTORY "${_compat_shim_dir}")
|
||||
file(WRITE "${_compat_shim_header}"
|
||||
"#pragma once
|
||||
// Compatibility shim for Dawn header paths
|
||||
// This file allows MapLibre code to use #include <webgpu/webgpu_cpp.h>
|
||||
// for both Dawn and wgpu-native backends
|
||||
|
||||
// Include the WebGPU-Cpp wrapper
|
||||
#include \"../../webgpu.hpp\"
|
||||
")
|
||||
|
||||
message(STATUS "Successfully created WebGPU-Cpp compatibility shim")
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_compat_shim_webgpu_h}")
|
||||
message(STATUS "Creating webgpu.h compatibility shim...")
|
||||
|
||||
file(WRITE "${_compat_shim_webgpu_h}"
|
||||
"#pragma once
|
||||
// Compatibility shim for Dawn header paths
|
||||
// This file allows MapLibre code to use #include <webgpu/webgpu.h>
|
||||
// for both Dawn and wgpu-native backends
|
||||
|
||||
// Include the actual webgpu.h header
|
||||
#include <webgpu.h>
|
||||
")
|
||||
|
||||
message(STATUS "Successfully created webgpu.h compatibility shim")
|
||||
endif()
|
||||
|
||||
# Also create wgpu.h shim for wgpu-native specific includes
|
||||
set(_compat_shim_wgpu_h "${_compat_shim_dir}/wgpu.h")
|
||||
if(NOT EXISTS "${_compat_shim_wgpu_h}")
|
||||
message(STATUS "Creating wgpu.h compatibility shim...")
|
||||
|
||||
file(WRITE "${_compat_shim_wgpu_h}"
|
||||
"#pragma once
|
||||
// Compatibility shim for wgpu-native specific header
|
||||
// This file allows MapLibre code to use #include <webgpu/wgpu.h>
|
||||
// for both Dawn and wgpu-native backends
|
||||
|
||||
// Include the actual wgpu.h header
|
||||
#include <wgpu.h>
|
||||
")
|
||||
|
||||
message(STATUS "Successfully created wgpu.h compatibility shim")
|
||||
endif()
|
||||
|
||||
# Create interface library
|
||||
add_library(mbgl-vendor-wgpu INTERFACE)
|
||||
|
||||
target_link_libraries(mbgl-vendor-wgpu INTERFACE ${WGPU_LIBRARY})
|
||||
|
||||
# Add include directories for webgpu.h, wgpu.h, and C++ wrapper
|
||||
target_include_directories(mbgl-vendor-wgpu
|
||||
SYSTEM INTERFACE
|
||||
${_webgpu_cpp_dir} # For generated webgpu.hpp
|
||||
${_webgpu_cpp_dir}/wgpu-native # For webgpu/webgpu_cpp.h compatibility shim
|
||||
${_mln_wgpu_source_dir}/ffi/webgpu-headers
|
||||
${_mln_wgpu_source_dir}/ffi
|
||||
)
|
||||
|
||||
# Platform-specific system libraries that wgpu-native needs
|
||||
if(APPLE)
|
||||
target_link_libraries(mbgl-vendor-wgpu INTERFACE
|
||||
"-framework Foundation"
|
||||
"-framework QuartzCore"
|
||||
"-framework Metal"
|
||||
)
|
||||
elseif(WIN32)
|
||||
target_link_libraries(mbgl-vendor-wgpu INTERFACE
|
||||
d3d12
|
||||
dxgi
|
||||
d3dcompiler
|
||||
ws2_32
|
||||
userenv
|
||||
bcrypt
|
||||
ntdll
|
||||
)
|
||||
else() # Linux
|
||||
# wgpu-native on Linux might need these depending on the build configuration
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mbgl-vendor-wgpu INTERFACE
|
||||
Threads::Threads
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
endif()
|
||||
|
||||
# Suppress warnings from wgpu headers
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
||||
target_compile_options(mbgl-vendor-wgpu INTERFACE
|
||||
-Wno-strict-aliasing
|
||||
-Wno-error=strict-aliasing
|
||||
)
|
||||
endif()
|
||||
|
||||
set_target_properties(
|
||||
mbgl-vendor-wgpu
|
||||
PROPERTIES
|
||||
INTERFACE_MAPLIBRE_NAME "wgpu-native"
|
||||
INTERFACE_MAPLIBRE_URL "https://github.com/gfx-rs/wgpu-native"
|
||||
INTERFACE_MAPLIBRE_AUTHOR "gfx-rs developers"
|
||||
INTERFACE_MAPLIBRE_LICENSE "${PROJECT_SOURCE_DIR}/vendor/wgpu-native/LICENSE.MIT"
|
||||
)
|
||||
Reference in New Issue
Block a user