Files
MapLibreParticleLab/platform/glfw/glfw_webgpu_backend_impl.ipp
2025-10-16 20:35:33 +02:00

1199 lines
39 KiB
C++

#include "glfw_webgpu_backend.hpp"
#include <mbgl/util/logging.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/gfx/backend_scope.hpp>
#include <mbgl/webgpu/renderer_backend.hpp>
#include <mbgl/webgpu/renderable_resource.hpp>
#include <mbgl/webgpu/context.hpp>
#include <mbgl/webgpu/command_encoder.hpp>
#include <mbgl/gfx/renderable.hpp>
#include <GLFW/glfw3.h>
#ifdef __APPLE__
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3native.h>
#import <Cocoa/Cocoa.h>
#import <QuartzCore/CAMetalLayer.h>
#import <CoreGraphics/CoreGraphics.h>
#include <dawn/native/MetalBackend.h>
#elif defined(__linux__)
#define GLFW_EXPOSE_NATIVE_X11
#define GLFW_EXPOSE_NATIVE_WAYLAND
#include <GLFW/glfw3native.h>
#include <dawn/native/VulkanBackend.h>
#endif
#ifdef None
#undef None
#endif
#include <dawn/native/DawnNative.h>
#include <webgpu/webgpu.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cctype>
#include <condition_variable>
#include <cstdlib>
#include <optional>
#include <iostream>
#include <limits>
#include <mutex>
#include <string>
#include <thread>
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<int>(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<int>(reason)) + "] " +
(message.data ? std::string(message.data, message.length) : std::string()));
}
std::optional<WGPUBackendType> 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<WGPUTextureView>(backend.getCurrentTextureView())) {
return viewPtr;
}
return nullptr;
}
WGPUTextureView getDepthStencilTextureView() override {
if (auto* viewPtr = static_cast<WGPUTextureView>(backend.getDepthStencilView())) {
return viewPtr;
}
return nullptr;
}
private:
GLFWWebGPUBackend& backend;
};
namespace gfx {
template <>
std::unique_ptr<GLFWBackend> Backend::Create<mbgl::gfx::Backend::Type::WebGPU>(GLFWwindow* window, bool capFrameRate) {
return std::make_unique<GLFWWebGPUBackend>(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<uint32_t>(std::max(fbWidth, 0)),
static_cast<uint32_t>(std::max(fbHeight, 0))};
}(),
std::make_unique<mbgl::WebGPURenderableResource>(*this)),
window(window_) {
static_cast<void>(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<dawn::native::Instance>();
// Enumerate adapters with retry logic
std::vector<dawn::native::Adapter> 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("<unknown>")));
wgpuAdapterInfoFreeMembers(info);
}
}
std::vector<wgpu::FeatureName> 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<wgpu::FeatureName>(features.features[i]));
}
wgpuSupportedFeaturesFreeMembers(features);
}
const bool hasDepth32Stencil = std::find(supportedFeatures.begin(), supportedFeatures.end(),
wgpu::FeatureName::Depth32FloatStencil8) != supportedFeatures.end();
std::vector<wgpu::FeatureName> 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<WGPUTextureFormat>(format)) {
return format;
}
}
}
return capabilities.formatCount > 0 ? static_cast<wgpu::TextureFormat>(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<uint32_t>(width), static_cast<uint32_t>(height)};
createDepthStencilTexture(static_cast<uint32_t>(width), static_cast<uint32_t>(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<WGPUTextureFormat>(format)) {
return format;
}
}
}
return capabilities.formatCount > 0 ? static_cast<wgpu::TextureFormat>(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<uint32_t>(width), static_cast<uint32_t>(height)};
#endif
// Store WebGPU instance, device and queue in the base class
setInstance(instance->Get());
setDevice(reinterpret_cast<void*>(wgpuDevice.Get()));
setQueue(reinterpret_cast<void*>(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<SpinLock> 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<std::chrono::milliseconds>(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<SpinLock> 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<uint32_t>(width) != newSize.width ||
static_cast<uint32_t>(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<SpinLock> lock(textureStateLock);
if (currentTextureView && !framePresented) {
return reinterpret_cast<void*>(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<wgpu::SurfaceGetCurrentTextureStatus>(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<void*>(currentTextureView.Get());
}
mbgl::Size GLFWWebGPUBackend::getFramebufferSize() const {
return getSize();
}
void* GLFWWebGPUBackend::getDepthStencilView() {
std::lock_guard<SpinLock> guard(textureStateLock);
if (!depthStencilView) {
int width = 0;
int height = 0;
if (window) {
glfwGetFramebufferSize(window, &width, &height);
}
createDepthStencilTexture(static_cast<uint32_t>(std::max(width, 0)),
static_cast<uint32_t>(std::max(height, 0)));
}
return depthStencilView ? reinterpret_cast<void*>(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<SpinLock> 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<uint32_t>(width) &&
lastConfiguredSize.height == static_cast<uint32_t>(height)) {
surfaceNeedsReconfigure = false;
return;
}
size = {static_cast<uint32_t>(std::max(width, 0)), static_cast<uint32_t>(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<uint32_t>(width), static_cast<uint32_t>(height));
// Update state
surfaceConfigured = true;
surfaceNeedsReconfigure = false;
lastConfiguredSize = {static_cast<uint32_t>(width), static_cast<uint32_t>(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<std::chrono::milliseconds>(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<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);
}