Files
MapLibreParticleLab/src/mbgl/webgpu/drawable.cpp

1043 lines
45 KiB
C++

#include <mbgl/webgpu/drawable.hpp>
#include <mbgl/gfx/color_mode.hpp>
#include <mbgl/gfx/cull_face_mode.hpp>
#include <mbgl/gfx/depth_mode.hpp>
#include <mbgl/gfx/stencil_mode.hpp>
#include <mbgl/gfx/vertex_attribute.hpp>
#include <mbgl/gfx/vertex_vector.hpp>
#include <mbgl/renderer/paint_parameters.hpp>
#include <mbgl/shaders/segment.hpp>
#include <mbgl/shaders/shader_defines.hpp>
#include <mbgl/shaders/webgpu/shader_program.hpp>
#include <mbgl/webgpu/command_encoder.hpp>
#include <mbgl/webgpu/context.hpp>
#include <mbgl/webgpu/drawable_impl.hpp>
#include <algorithm>
#include <sstream>
#include <mbgl/webgpu/index_buffer_resource.hpp>
#include <mbgl/webgpu/render_pass.hpp>
#include <mbgl/webgpu/renderer_backend.hpp>
#include <mbgl/webgpu/texture2d.hpp>
#include <mbgl/webgpu/uniform_buffer.hpp>
#include <mbgl/webgpu/upload_pass.hpp>
#include <mbgl/webgpu/vertex_buffer_resource.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/variant.hpp>
#include <mbgl/util/hash.hpp>
#include <mbgl/util/convert.hpp>
#include <mbgl/util/monotonic_timer.hpp>
#include <cassert>
#include <memory>
#if !defined(NDEBUG)
#include <sstream>
#endif
namespace mbgl {
namespace webgpu {
namespace {
#if !defined(NDEBUG)
std::string debugLabel(const Drawable& drawable) {
std::ostringstream ss;
ss << drawable.getName() << "#" << drawable.getID();
return ss.str();
}
#endif
struct alignas(16) GlobalUBOIndexData {
uint32_t value = 0;
uint32_t pad[3] = {0, 0, 0};
};
std::size_t hashStencilMode(const gfx::StencilMode& mode) {
std::size_t hash = util::hash(static_cast<int>(mode.ref),
static_cast<uint32_t>(mode.mask),
static_cast<int>(mode.fail),
static_cast<int>(mode.depthFail),
static_cast<int>(mode.pass));
mapbox::util::apply_visitor(
[&](const auto& test) {
using TestType = std::decay_t<decltype(test)>;
hash = util::hash(hash, static_cast<int>(TestType::func));
if constexpr (requires { test.mask; }) {
hash = util::hash(hash, static_cast<uint32_t>(test.mask));
}
},
mode.test);
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:
return WGPUVertexFormat_Sint8;
case gfx::AttributeDataType::Byte2:
return WGPUVertexFormat_Sint8x2;
case gfx::AttributeDataType::Byte4:
return WGPUVertexFormat_Sint8x4;
case gfx::AttributeDataType::UByte:
return WGPUVertexFormat_Uint8;
case gfx::AttributeDataType::UByte2:
return WGPUVertexFormat_Uint8x2;
case gfx::AttributeDataType::UByte4:
return WGPUVertexFormat_Uint8x4;
case gfx::AttributeDataType::Short:
return WGPUVertexFormat_Sint16;
case gfx::AttributeDataType::Short2:
return WGPUVertexFormat_Sint16x2;
case gfx::AttributeDataType::Short4:
return WGPUVertexFormat_Sint16x4;
case gfx::AttributeDataType::UShort:
return WGPUVertexFormat_Uint16;
case gfx::AttributeDataType::UShort2:
return WGPUVertexFormat_Uint16x2;
case gfx::AttributeDataType::UShort4:
return WGPUVertexFormat_Uint16x4;
case gfx::AttributeDataType::Int:
return WGPUVertexFormat_Sint32;
case gfx::AttributeDataType::Int2:
return WGPUVertexFormat_Sint32x2;
case gfx::AttributeDataType::Int3:
return WGPUVertexFormat_Sint32x3;
case gfx::AttributeDataType::Int4:
return WGPUVertexFormat_Sint32x4;
case gfx::AttributeDataType::UInt:
return WGPUVertexFormat_Uint32;
case gfx::AttributeDataType::UInt2:
return WGPUVertexFormat_Uint32x2;
case gfx::AttributeDataType::UInt3:
return WGPUVertexFormat_Uint32x3;
case gfx::AttributeDataType::UInt4:
return WGPUVertexFormat_Uint32x4;
case gfx::AttributeDataType::Float:
return WGPUVertexFormat_Float32;
case gfx::AttributeDataType::Float2:
return WGPUVertexFormat_Float32x2;
case gfx::AttributeDataType::Float3:
return WGPUVertexFormat_Float32x3;
case gfx::AttributeDataType::Float4:
return WGPUVertexFormat_Float32x4;
default:
assert(!"Unsupported vertex attribute format");
return WGPUVertexFormat_Float32x2; // Default fallback
}
}
} // namespace
// Simple wrapper to adapt gfx::IndexBuffer to IndexBufferBase
struct IndexBuffer : public gfx::IndexBufferBase {
IndexBuffer(std::unique_ptr<gfx::IndexBuffer>&& buffer_)
: buffer(std::move(buffer_)) {}
~IndexBuffer() override = default;
std::unique_ptr<mbgl::gfx::IndexBuffer> buffer;
};
Drawable::Drawable(std::string name_)
: gfx::Drawable(std::move(name_)),
impl(std::make_unique<Impl>()) {}
Drawable::~Drawable() {
// Clean up WebGPU resources safely
// Note: We don't own the pipeline - it's owned by the shader program
// So we should NOT release it here
impl->pipelineState = nullptr;
for (auto& record : impl->bindGroups) {
if (record.handle) {
wgpuBindGroupRelease(record.handle);
}
}
impl->bindGroups.clear();
// Uniform buffers are managed by UniformBufferArray
// Vertex and index buffers are now managed through attributeBindings and indexes
}
namespace {
[[maybe_unused]] WGPUPrimitiveTopology getPrimitiveTopology(const gfx::DrawModeType type) noexcept {
switch (type) {
default:
assert(false);
[[fallthrough]];
case gfx::DrawModeType::Points:
return WGPUPrimitiveTopology_PointList;
case gfx::DrawModeType::Lines:
return WGPUPrimitiveTopology_LineList;
case gfx::DrawModeType::LineStrip:
return WGPUPrimitiveTopology_LineStrip;
case gfx::DrawModeType::Triangles:
return WGPUPrimitiveTopology_TriangleList;
}
}
#if !defined(NDEBUG)
[[maybe_unused]] std::string debugLabel(const gfx::Drawable& drawable) {
std::ostringstream oss;
oss << drawable.getID().id() << "/" << drawable.getName() << "/tile=";
if (const auto& tileID = drawable.getTileID()) {
oss << util::toString(*tileID);
} else {
oss << "(null)";
}
return oss.str();
}
#endif // !defined(NDEBUG)
} // namespace
void Drawable::setColorMode(const gfx::ColorMode& value) {
gfx::Drawable::setColorMode(value);
impl->pipelineState = nullptr; // Rebuild pipeline to honour new color mode
}
void Drawable::setEnableStencil(bool value) {
if (getEnableStencil() == value) {
return;
}
gfx::Drawable::setEnableStencil(value);
impl->pipelineState = nullptr; // Reset pipeline to force recreation with new stencil state
}
void Drawable::setEnableDepth(bool value) {
if (getEnableDepth() == value) {
return;
}
gfx::Drawable::setEnableDepth(value);
impl->pipelineState = nullptr; // Reset pipeline to force recreation with new depth state
}
void Drawable::setSubLayerIndex(int32_t value) {
if (getSubLayerIndex() == value) {
return;
}
gfx::Drawable::setSubLayerIndex(value);
}
void Drawable::setDepthType(gfx::DepthMaskType value) {
if (getDepthType() == value) {
return;
}
gfx::Drawable::setDepthType(value);
impl->pipelineState = nullptr; // Reset pipeline to force recreation with new depth mask
}
void Drawable::setDepthModeFor3D(const gfx::DepthMode& value) {
if (impl->depthMode.func != value.func || impl->depthMode.mask != value.mask) {
impl->pipelineState = nullptr;
}
impl->depthMode = value;
}
void Drawable::setStencilModeFor3D(const gfx::StencilMode& value) {
const auto newHash = hashStencilMode(value);
if (!impl->stencilModeHashValid || impl->stencilModeHash != newHash || impl->stencilMode.ref != value.ref ||
impl->stencilMode.mask != value.mask || impl->stencilMode.fail != value.fail ||
impl->stencilMode.depthFail != value.depthFail || impl->stencilMode.pass != value.pass) {
impl->pipelineState = nullptr;
impl->stencilModeHashValid = false;
}
impl->stencilMode = value;
impl->stencilModeHash = newHash;
}
void Drawable::setShader(gfx::ShaderProgramBasePtr value) {
if (shader == value) {
return;
}
shader = std::move(value);
impl->pipelineState = nullptr; // Reset pipeline when shader changes
}
void Drawable::upload(gfx::UploadPass& uploadPass) {
if (isCustom) {
return;
}
if (!shader) {
Log::Warning(Event::General, "Missing shader for drawable " + util::toString(getID()) + "/" + getName());
assert(false);
return;
}
auto& webgpuUploadPass = static_cast<webgpu::UploadPass&>(uploadPass);
auto& gfxContext = webgpuUploadPass.getContext();
[[maybe_unused]] auto& context = static_cast<webgpu::Context&>(gfxContext);
constexpr auto usage = gfx::BufferUsageType::StaticDraw;
// We need either raw index data or a buffer already created from them.
// We can have a buffer and no indexes, but only if it's not marked dirty.
if (!impl->indexes || (impl->indexes->empty() && (!impl->indexes->getBuffer() || impl->indexes->getDirty()))) {
assert(!"Missing index data");
return;
}
if (!impl->indexes->getBuffer() || impl->indexes->getDirty()) {
// Create or update a buffer for the index data. We don't update any
// existing buffer because it may still be in use by the previous frame.
auto indexBufferResource = webgpuUploadPass.createIndexBufferResource(impl->indexes->data(),
impl->indexes->bytes(),
usage,
/*persistent=*/false);
auto gfxIndexBuffer = std::make_unique<gfx::IndexBuffer>(impl->indexes->elements(),
std::move(indexBufferResource));
auto buffer = std::make_unique<IndexBuffer>(std::move(gfxIndexBuffer));
impl->indexes->setBuffer(std::move(buffer));
impl->indexes->setDirty(false);
}
// Upload vertex attributes (like Metal does)
const bool buildAttribs = !vertexAttributes || !attributeUpdateTime ||
vertexAttributes->isModifiedAfter(*attributeUpdateTime);
if (buildAttribs) {
#if !defined(NDEBUG)
const auto debugGroup = webgpuUploadPass.createDebugGroup(debugLabel(*this));
#endif
if (!vertexAttributes) {
vertexAttributes = std::make_shared<gfx::VertexAttributeArray>();
}
// Apply drawable values to shader defaults (matching Metal's approach)
std::vector<std::unique_ptr<gfx::VertexBufferResource>> vertexBuffers;
auto attributeBindings_ = webgpuUploadPass.buildAttributeBindings(impl->vertexCount,
impl->vertexType,
/*vertexAttributeIndex=*/-1,
/*vertexData=*/{},
shader->getVertexAttributes(),
*vertexAttributes,
usage,
attributeUpdateTime,
vertexBuffers);
if (impl->attributeBindings != attributeBindings_) {
impl->attributeBindings = std::move(attributeBindings_);
impl->vertexDescHash = 0; // Reset hash when bindings change
impl->pipelineState = nullptr; // Reset pipeline state when attribute bindings change
// Store the dummy vertex buffers to keep them alive
impl->dummyVertexBuffers = std::move(vertexBuffers);
}
}
// Build instance buffer (like Metal does)
const bool buildInstanceBuffer =
(instanceAttributes && (!attributeUpdateTime || instanceAttributes->isModifiedAfter(*attributeUpdateTime)));
if (buildInstanceBuffer) {
// Apply instance values to shader defaults (matching Metal's approach)
std::vector<std::unique_ptr<gfx::VertexBufferResource>> instanceBuffers;
auto instanceBindings_ = webgpuUploadPass.buildAttributeBindings(impl->vertexCount,
impl->vertexType,
/*vertexAttributeIndex=*/-1,
/*vertexData=*/{},
shader->getInstanceAttributes(),
*instanceAttributes,
usage,
attributeUpdateTime,
instanceBuffers);
if (impl->instanceBindings != instanceBindings_) {
impl->instanceBindings = std::move(instanceBindings_);
impl->pipelineState = nullptr; // Reset pipeline state when instance bindings change
// Store the dummy instance buffers to keep them alive
if (!instanceBuffers.empty()) {
impl->dummyVertexBuffers.insert(impl->dummyVertexBuffers.end(),
std::make_move_iterator(instanceBuffers.begin()),
std::make_move_iterator(instanceBuffers.end()));
}
}
}
// Upload uniform buffers to ensure they're ready for the GPU
// This is critical for making drawables visible
for (size_t i = 0; i < impl->uniformBuffers.allocatedSize(); ++i) {
auto& uniformBuffer = impl->uniformBuffers.get(i);
if (uniformBuffer) {
// Make sure the uniform buffer has its data uploaded
// The uniform buffer should handle its own upload internally
// but we need to ensure it's marked as not dirty
auto* webgpuUniformBuffer = static_cast<webgpu::UniformBuffer*>(uniformBuffer.get());
if (webgpuUniformBuffer) {
// The uniform buffer's data should already be uploaded when it was created/updated
// Just ensure we have a valid buffer handle
if (!webgpuUniformBuffer->getBuffer()) {
Log::Error(Event::Render, "Uniform buffer " + std::to_string(i) + " has no GPU buffer!");
}
}
}
}
// Upload textures if needed
const bool texturesNeedUpload = std::any_of(
textures.begin(), textures.end(), [](const auto& texture) { return texture && texture->needsUpload(); });
if (texturesNeedUpload) {
for (auto& texture : textures) {
if (texture && texture->needsUpload()) {
texture->upload();
}
}
}
attributeUpdateTime = util::MonotonicTimer::now();
}
void Drawable::draw(PaintParameters& parameters) const {
static int drawCallCount = 0;
drawCallCount++;
if (isCustom) {
return;
}
// Get WebGPU context and render pass (following Metal's pattern)
auto& context = static_cast<webgpu::Context&>(parameters.context);
auto& webgpuRenderPass = static_cast<webgpu::RenderPass&>(*parameters.renderPass);
WGPURenderPassEncoder renderPassEncoder = webgpuRenderPass.getEncoder();
if (!renderPassEncoder) {
Log::Error(Event::Render, "No render pass encoder available");
assert(false);
return;
}
if (!shader) {
Log::Warning(Event::General, "Missing shader for drawable " + util::toString(getID()) + "/" + getName());
assert(false);
return;
}
if (!getEnabled()) {
return;
}
// Get WebGPU backend and device
auto& backend = static_cast<webgpu::RendererBackend&>(context.getBackend());
WGPUDevice device = static_cast<WGPUDevice>(backend.getDevice());
WGPUQueue queue = static_cast<WGPUQueue>(backend.getQueue());
if (!device || !queue) {
return;
}
#if !defined(NDEBUG)
// Debug group creation would go here (Metal has this but we need to implement it for WebGPU)
// const auto debugGroup = parameters.encoder->createDebugGroup(getName());
#endif
// Handle uboIndex like Metal does
// In WebGPU, we'll need to handle this differently since we use bind groups
// Metal uses setVertexBytes/setFragmentBytes for the uboIndex
// For now, we'll store it for use in bind group creation
// Bind uniform buffers (like Metal does)
impl->uniformBuffers.bind(webgpuRenderPass);
// Check index buffer is valid (like Metal does)
if (impl->indexes && (!impl->indexes->getBuffer() || impl->indexes->getDirty())) {
assert(!"Index buffer not uploaded");
return;
}
// Build bind groups based on shader metadata
if (shader) {
auto webgpuShader = std::static_pointer_cast<mbgl::webgpu::ShaderProgram>(shader);
if (webgpuShader) {
WGPUDevice deviceHandle = static_cast<WGPUDevice>(backend.getDevice());
for (auto& record : impl->bindGroups) {
if (record.handle) {
wgpuBindGroupRelease(record.handle);
}
}
impl->bindGroups.clear();
if (deviceHandle) {
const auto& groupOrder = webgpuShader->getBindGroupOrder();
static bool loggedBindGroupOrder = false;
if (!loggedBindGroupOrder && getName().find("fill-outline") != std::string::npos) {
std::string orderStr;
for (size_t idx = 0; idx < groupOrder.size(); ++idx) {
orderStr += std::to_string(groupOrder[idx]);
if (idx + 1 < groupOrder.size()) orderStr += ",";
}
loggedBindGroupOrder = true;
}
const auto* globalBuffers = webgpuRenderPass.getGlobalUniformBuffers();
const auto findTextureForBinding = [&](uint32_t binding, bool samplerBinding) -> Texture2D* {
for (size_t texIndex = 0; texIndex < textures.size(); ++texIndex) {
if (!textures[texIndex]) {
continue;
}
if (const auto location = webgpuShader->getSamplerLocation(texIndex)) {
if ((samplerBinding && *location == binding) ||
(!samplerBinding && (*location == binding || *location + 1 == binding))) {
return static_cast<Texture2D*>(textures[texIndex].get());
}
}
}
return nullptr;
};
for (size_t slot = 0; slot < groupOrder.size(); ++slot) {
const uint32_t group = groupOrder[slot];
auto layout = webgpuShader->getBindGroupLayout(group);
if (!layout) {
const std::string shaderName = webgpuShader ? std::string(webgpuShader->typeName()) : "unknown";
Log::Warning(Event::Render,
"WebGPU: missing bind group layout for group " + std::to_string(group) +
" in shader '" + shaderName + "'");
continue;
}
const auto& bindingInfos = webgpuShader->getBindingInfosForGroup(group);
if (getName().find("fill-outline") != std::string::npos) {
std::string infoStr;
for (const auto& info : bindingInfos) {
infoStr += "(binding=" + std::to_string(info.binding) +
", type=" + std::to_string(static_cast<int>(info.type)) + ")";
}
}
std::vector<WGPUBindGroupEntry> entries;
entries.reserve(bindingInfos.size());
bool validBindings = true;
for (const auto& bindingInfo : bindingInfos) {
WGPUBindGroupEntry entry = {};
entry.binding = bindingInfo.binding;
switch (bindingInfo.type) {
case ShaderProgram::BindingType::UniformBuffer:
case ShaderProgram::BindingType::ReadOnlyStorageBuffer:
case ShaderProgram::BindingType::StorageBuffer: {
std::shared_ptr<gfx::UniformBuffer> buffer = impl->uniformBuffers.get(
bindingInfo.binding);
if (bindingInfo.binding == shaders::idGlobalUBOIndex) {
GlobalUBOIndexData indexData;
indexData.value = getUBOIndex();
context.emplaceOrUpdateUniformBuffer(
impl->uboIndexUniform, &indexData, sizeof(indexData), false);
if (impl->uboIndexUniform) {
impl->uniformBuffers.set(bindingInfo.binding, impl->uboIndexUniform);
buffer = impl->uboIndexUniform;
}
}
if (!buffer && globalBuffers) {
buffer = globalBuffers->get(bindingInfo.binding);
}
if (!buffer) {
if (drawCallCount <= 200) {
Log::Warning(Event::Render,
"No buffer found for binding " +
std::to_string(bindingInfo.binding) +
" type=" + std::to_string(static_cast<int>(bindingInfo.type)) +
" group=" + std::to_string(group));
}
validBindings = false;
break;
}
const auto* webgpuUniform = static_cast<const webgpu::UniformBuffer*>(buffer.get());
if (!webgpuUniform || !webgpuUniform->getBuffer()) {
if (drawCallCount <= 200) {
Log::Warning(Event::Render,
"Uniform buffer missing GPU handle for binding " +
std::to_string(bindingInfo.binding) +
" group=" + std::to_string(group));
}
validBindings = false;
break;
}
entry.buffer = webgpuUniform->getBuffer();
entry.offset = 0;
entry.size = buffer->getSize();
break;
}
case ShaderProgram::BindingType::Sampler: {
Texture2D* texture = findTextureForBinding(bindingInfo.binding, true);
if (!texture) {
validBindings = false;
break;
}
if (!texture->getSampler()) {
texture->setSamplerConfiguration(texture->getSamplerState());
}
entry.sampler = texture->getSampler();
break;
}
case ShaderProgram::BindingType::Texture:
case ShaderProgram::BindingType::UnfilterableTexture: {
Texture2D* texture = findTextureForBinding(bindingInfo.binding, false);
if (!texture) {
validBindings = false;
break;
}
if (!texture->getTexture() || !texture->getTextureView()) {
texture->create();
}
if (!texture->getTextureView()) {
validBindings = false;
break;
}
entry.textureView = texture->getTextureView();
break;
}
}
if (!validBindings) {
break;
}
entries.push_back(entry);
}
if (!validBindings || entries.empty()) {
Log::Warning(Event::Render,
"WebGPU: invalid bindings for drawable '" + getName() +
"' (group=" + std::to_string(group) + ", slot=" + std::to_string(slot) + ")");
continue;
}
const std::string label = getName() + " bind-group " + std::to_string(group);
WGPUStringView labelView = {label.c_str(), label.length()};
WGPUBindGroupDescriptor descriptor = {};
descriptor.label = labelView;
descriptor.layout = layout;
descriptor.entryCount = entries.size();
descriptor.entries = entries.data();
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(deviceHandle, &descriptor);
impl->bindGroups.push_back({static_cast<uint32_t>(slot), group, bindGroup});
}
}
}
}
const auto drawableTileID = getTileID();
gfx::DepthMode depthMode = gfx::DepthMode::disabled();
gfx::StencilMode stencilMode = gfx::StencilMode::disabled();
if (is3D) {
depthMode = impl->depthMode;
stencilMode = impl->stencilMode;
} else {
if (getEnableDepth()) {
depthMode = parameters.depthModeForSublayer(getSubLayerIndex(), getDepthType());
}
if (getEnableStencil() && drawableTileID) {
stencilMode = parameters.stencilModeForClipping(drawableTileID->toUnwrapped());
}
}
const std::size_t newStencilHash = hashStencilMode(stencilMode);
if (!impl->stencilModeHashValid || impl->stencilModeHash != newStencilHash ||
impl->depthMode.func != depthMode.func || impl->depthMode.mask != depthMode.mask) {
impl->pipelineState = nullptr;
impl->stencilModeHashValid = true;
}
impl->depthMode = depthMode;
impl->stencilMode = stencilMode;
impl->stencilModeHash = newStencilHash;
impl->previousStencilMode = stencilMode;
// Get pipeline from shader if we don't have it yet (like Metal)
struct UniqueBinding {
const VertexBufferResource* vertexResource = nullptr;
const BufferResource* buffer = nullptr;
uint32_t stride = 0;
uint64_t baseOffset = 0;
WGPUVertexStepMode stepMode = WGPUVertexStepMode_Vertex;
std::vector<WGPUVertexAttribute> attributes;
std::vector<uint64_t> attributeByteOffsets;
};
auto accumulateBindings = [](const gfx::AttributeBindingArray& source,
const WGPUVertexStepMode stepMode,
std::vector<UniqueBinding>& out) {
for (size_t attributeIndex = 0; attributeIndex < source.size(); ++attributeIndex) {
const auto& binding = source[attributeIndex];
if (!binding.has_value() || !binding->vertexBufferResource) {
continue;
}
const auto* vertexBufferRes = static_cast<const VertexBufferResource*>(binding->vertexBufferResource);
if (!vertexBufferRes) {
continue;
}
const auto& bufferResource = vertexBufferRes->getBuffer();
if (!bufferResource.getBuffer()) {
continue;
}
const uint64_t bindingByteOffset = static_cast<uint64_t>(binding->vertexOffset) * binding->vertexStride;
auto it = std::find_if(out.begin(), out.end(), [&](const UniqueBinding& entry) {
return entry.vertexResource == vertexBufferRes && entry.stride == binding->vertexStride &&
entry.stepMode == stepMode;
});
if (it == out.end()) {
UniqueBinding entry;
entry.vertexResource = vertexBufferRes;
entry.buffer = &bufferResource;
entry.stride = binding->vertexStride;
entry.stepMode = stepMode;
entry.baseOffset = bindingByteOffset;
entry.attributes.reserve(4);
entry.attributeByteOffsets.reserve(4);
out.push_back(std::move(entry));
it = out.end() - 1;
} else {
it->baseOffset = std::min(it->baseOffset, bindingByteOffset);
}
WGPUVertexAttribute attr = {};
attr.format = wgpuVertexFormatOf(binding->attribute.dataType);
attr.shaderLocation = static_cast<uint32_t>(attributeIndex);
attr.offset = binding->attribute.offset;
it->attributes.push_back(attr);
it->attributeByteOffsets.push_back(bindingByteOffset);
}
};
std::vector<UniqueBinding> uniqueBindings;
uniqueBindings.reserve(impl->attributeBindings.size() + impl->instanceBindings.size());
accumulateBindings(impl->attributeBindings, WGPUVertexStepMode_Vertex, uniqueBindings);
accumulateBindings(impl->instanceBindings, WGPUVertexStepMode_Instance, uniqueBindings);
constexpr uint32_t maxVertexBufferSlots = 8;
if (uniqueBindings.size() > maxVertexBufferSlots) {
Log::Warning(Event::Render,
"WebGPU: reducing vertex buffer bindings from " + std::to_string(uniqueBindings.size()) + " to " +
std::to_string(maxVertexBufferSlots));
uniqueBindings.resize(maxVertexBufferSlots);
}
for (auto& entry : uniqueBindings) {
for (size_t j = 0; j < entry.attributes.size(); ++j) {
if (entry.attributeByteOffsets[j] < entry.baseOffset) {
continue;
}
const auto delta = entry.attributeByteOffsets[j] - entry.baseOffset;
entry.attributes[j].offset += static_cast<uint32_t>(delta);
}
}
auto& shaderWebGPU = static_cast<mbgl::webgpu::ShaderProgram&>(*shader);
if (!impl->pipelineState) {
// Create vertex layout from attribute bindings
std::vector<WGPUVertexBufferLayout> vertexLayouts;
vertexLayouts.reserve(uniqueBindings.size());
for (auto& binding : uniqueBindings) {
WGPUVertexBufferLayout layout = {};
layout.arrayStride = binding.stride;
layout.stepMode = binding.stepMode;
layout.attributeCount = binding.attributes.size();
layout.attributes = binding.attributes.data();
vertexLayouts.push_back(layout);
}
// Get render pipeline similar to Metal's getRenderPipelineState
// Use the actual color mode from the drawable (like Metal does)
const auto& colorMode = getColorMode();
// Get the renderable from render pass descriptor like Metal does
const auto& renderable = webgpuRenderPass.getDescriptor().renderable;
gfx::DrawModeType drawModeType = gfx::DrawModeType::Triangles;
if (!impl->segments.empty()) {
drawModeType = impl->segments.front()->getMode().type;
}
// 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(),
static_cast<uint32_t>(vertexLayouts.size()),
colorMode,
depthMode,
stencilMode,
drawModeType,
pipelineHash);
if (!impl->pipelineState) {
Log::Warning(Event::Render,
"WebGPU: skipping pipeline creation for drawable '" + getName() + "' (shader='" +
shaderWebGPU.typeName().data() + "')");
}
}
if (impl->pipelineState) {
if (!renderPassEncoder) {
Log::Error(Event::Render, "renderPassEncoder is null!");
return;
}
wgpuRenderPassEncoderSetPipeline(renderPassEncoder, impl->pipelineState);
if (getEnableStencil()) {
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, static_cast<uint32_t>(stencilMode.ref));
}
} else {
Log::Warning(Event::Render, "WebGPU: no pipeline available for drawable '" + getName() + "'; draw skipped");
return;
}
// Clamp scissor to the current render pass size (not the default renderable,
// which may be different during offscreen passes like hillshade prepare).
{
const auto& webgpuRP = static_cast<const webgpu::RenderPass&>(*parameters.renderPass);
const auto rtSize = webgpuRP.getDescriptor().renderable.getSize();
const uint32_t sx = static_cast<uint32_t>(parameters.scissorRect.x);
const uint32_t sy = static_cast<uint32_t>(parameters.scissorRect.y);
const uint32_t sw = (sx < rtSize.width) ? std::min(parameters.scissorRect.width, rtSize.width - sx) : 0;
const uint32_t sh = (sy < rtSize.height) ? std::min(parameters.scissorRect.height, rtSize.height - sy) : 0;
if (sw > 0 && sh > 0) {
wgpuRenderPassEncoderSetScissorRect(renderPassEncoder, sx, sy, sw, sh);
}
}
uint32_t bufferSlot = 0;
for (const auto& binding : uniqueBindings) {
if (!binding.buffer || !binding.buffer->getBuffer()) {
continue;
}
const uint64_t bufferSize = binding.buffer->getSizeInBytes();
if (binding.baseOffset >= bufferSize) {
Log::Warning(Event::Render,
"Skipping vertex buffer slot " + std::to_string(bufferSlot) + " due to base offset " +
std::to_string(binding.baseOffset) + " >= size " + std::to_string(bufferSize));
continue;
}
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder,
bufferSlot,
binding.buffer->getBuffer(),
binding.baseOffset,
bufferSize - binding.baseOffset);
bufferSlot++;
}
// Bind index buffer if present
if (impl->indexes && impl->indexes->elements() > 0 && !impl->indexes->getDirty()) {
// Get the buffer from indexes
if (const auto* indexBufferBase = impl->indexes->getBuffer()) {
if (const auto* webgpuIndexBuffer = static_cast<const IndexBuffer*>(indexBufferBase)) {
if (webgpuIndexBuffer->buffer) {
const auto& indexBufferRes = webgpuIndexBuffer->buffer->getResource<IndexBufferResource>();
const auto& bufferResource = indexBufferRes.getBuffer();
if (bufferResource.getBuffer()) {
// Always use Uint16 format for now
WGPUIndexFormat indexFormat = WGPUIndexFormat_Uint16;
wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder,
bufferResource.getBuffer(),
indexFormat,
0,
bufferResource.getSizeInBytes());
}
}
}
}
}
// Set bind group
for (const auto& record : impl->bindGroups) {
if (record.handle) {
wgpuRenderPassEncoderSetBindGroup(renderPassEncoder, record.slot, record.handle, 0, nullptr);
}
}
// Handle depth and stencil states (like Metal does)
// For 3D mode, stenciling is handled by the layer group
if (!is3D) {
if (enableStencil) {
if (tileID) {
const auto expectedStencil = parameters.stencilModeForClipping(tileID->toUnwrapped());
#if !defined(NDEBUG)
assert(hashStencilMode(expectedStencil) == hashStencilMode(impl->stencilMode));
#else
if (hashStencilMode(expectedStencil) != hashStencilMode(impl->stencilMode)) {
Log::Warning(
Event::Render,
"WebGPU drawable stencil mismatch for '" + getName() + "' tile=" + util::toString(*tileID));
}
#endif
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, static_cast<uint32_t>(expectedStencil.ref));
}
} else {
// Reset the reference so subsequent drawables that re-enable stencilling start from a known value.
wgpuRenderPassEncoderSetStencilReference(renderPassEncoder, 0u);
}
if (getEnableDepth()) {
#if !defined(NDEBUG)
const auto expectedDepth = parameters.depthModeForSublayer(getSubLayerIndex(), getDepthType());
assert(expectedDepth.func == impl->depthMode.func);
assert(expectedDepth.mask == impl->depthMode.mask);
#endif
}
}
// Draw indexed geometry - loop through segments (exactly like Metal does)
// int segmentCount = 0;
for (const auto& seg_ : impl->segments) {
const auto& segment = static_cast<DrawSegment&>(*seg_);
const auto& mlSegment = segment.getSegment();
if (mlSegment.indexLength > 0) {
const uint32_t instanceCount = instanceAttributes ? static_cast<uint32_t>(instanceAttributes->getMaxCount())
: 1;
const uint32_t indexOffset = static_cast<uint32_t>(mlSegment.indexOffset);
const int32_t baseVertex = static_cast<int32_t>(mlSegment.vertexOffset);
const uint32_t baseInstance = 0;
// Check if encoder is valid before drawing
if (!renderPassEncoder) {
Log::Error(Event::Render, "Render pass encoder became null before draw!");
return;
}
// Make sure we have valid indices
if (mlSegment.indexLength == 0) {
Log::Warning(Event::Render, "Skipping draw with 0 indices");
continue;
}
wgpuRenderPassEncoderDrawIndexed(renderPassEncoder,
static_cast<uint32_t>(mlSegment.indexLength), // indexCount
instanceCount, // instanceCount
indexOffset, // firstIndex
baseVertex, // baseVertex
baseInstance); // firstInstance
context.renderingStats().numDrawCalls++;
}
}
}
void Drawable::setIndexData(gfx::IndexVectorBasePtr indices, std::vector<UniqueDrawSegment> segments) {
impl->indexes = std::move(indices);
impl->segments = std::move(segments);
}
void Drawable::setVertices(std::vector<uint8_t>&& data, std::size_t count, gfx::AttributeDataType dataType) {
impl->vertexCount = count;
impl->vertexType = dataType;
if (count && dataType != gfx::AttributeDataType::Invalid && !data.empty()) {
if (!vertexAttributes) {
vertexAttributes = std::make_shared<gfx::VertexAttributeArray>();
}
if (auto& attrib = vertexAttributes->set(impl->vertexAttrId, /*index=*/-1, dataType)) {
attrib->setRawData(std::move(data));
attrib->setStride(gfx::VertexAttribute::getStrideOf(dataType));
} else {
using namespace std::string_literals;
Log::Warning(Event::General,
"Vertex attribute type mismatch: "s + name + " / " + util::toString(impl->vertexAttrId));
assert(false);
}
}
}
const gfx::UniformBufferArray& Drawable::getUniformBuffers() const {
return impl->uniformBuffers;
}
gfx::UniformBufferArray& Drawable::mutableUniformBuffers() {
return impl->uniformBuffers;
}
void Drawable::setVertexAttrId(const size_t id) {
impl->vertexAttrId = id;
}
void Drawable::updateVertexAttributes(gfx::VertexAttributeArrayPtr vertices,
std::size_t vertexCount,
gfx::DrawMode mode,
gfx::IndexVectorBasePtr indexes,
const SegmentBase* segments,
std::size_t segmentCount) {
// Store the vertex attributes (base class method)
gfx::Drawable::setVertexAttributes(std::move(vertices));
// Store vertex count
impl->vertexCount = vertexCount;
// Handle segments - check for nullptr
std::vector<UniqueDrawSegment> drawSegs;
if (segments && segmentCount > 0) {
drawSegs.reserve(segmentCount);
for (std::size_t i = 0; i < segmentCount; ++i) {
const auto& seg = segments[i];
drawSegs.push_back(std::make_unique<DrawSegment>(
mode, SegmentBase{seg.vertexOffset, seg.indexOffset, seg.vertexLength, seg.indexLength, seg.sortKey}));
}
}
// Set the index data with segments
setIndexData(std::move(indexes), std::move(drawSegs));
}
} // namespace webgpu
} // namespace mbgl