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