#include #include #include #include #include #include #include #include namespace { constexpr uint32_t bytesPerRowAlignment = 256u; } namespace mbgl { namespace webgpu { class OffscreenTextureResource final : public webgpu::RenderableResource { public: OffscreenTextureResource( Context& context_, const Size size_, const gfx::TextureChannelDataType type_, bool depth, bool stencil) : context(context_), size(size_), type(type_) { assert(!size.isEmpty()); // Create color texture colorTexture = context.createTexture2D(); colorTexture->setSize(size); colorTexture->setFormat(gfx::TexturePixelType::RGBA, type); if (auto* webgpuTexture = static_cast(colorTexture.get())) { webgpuTexture->setUsage(WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc | WGPUTextureUsage_RenderAttachment); webgpuTexture->setSizeChangedCallback([this](const Size& newSize) { handleResize(newSize); }); } colorTexture->setSamplerConfiguration( {gfx::TextureFilterType::Linear, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); // Create depth/stencil texture if needed // WebGPU requires a combined depth-stencil texture when both are needed if (depth || stencil) { depthTexture = context.createTexture2D(); depthTexture->setSize(size); // Use combined depth-stencil format when both are needed if (depth && stencil) { depthTexture->setFormat(gfx::TexturePixelType::Stencil, gfx::TextureChannelDataType::UnsignedByte); } else if (depth) { depthTexture->setFormat(gfx::TexturePixelType::Depth, gfx::TextureChannelDataType::Float); } else { depthTexture->setFormat(gfx::TexturePixelType::Stencil, gfx::TextureChannelDataType::UnsignedByte); } if (auto* webgpuDepth = static_cast(depthTexture.get())) { webgpuDepth->setUsage(WGPUTextureUsage_RenderAttachment); } depthTexture->setSamplerConfiguration( {gfx::TextureFilterType::Linear, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); } } ~OffscreenTextureResource() noexcept override { context.renderingStats().numFrameBuffers--; } void bind() override { // Create textures if not already created colorTexture->create(); if (depthTexture) { depthTexture->create(); } } void swap() override { // Offscreen targets do not present to a surface. } const mbgl::webgpu::RendererBackend& getBackend() const override { return static_cast(context.getBackend()); } const WGPUCommandEncoder& getCommandEncoder() const override { static WGPUCommandEncoder dummyEncoder = nullptr; return dummyEncoder; } WGPURenderPassEncoder getRenderPassEncoder() const override { return nullptr; } WGPUTextureView getColorTextureView() override { if (!colorTexture) { return nullptr; } auto& texture = static_cast(*colorTexture); texture.create(); return texture.getTextureView(); } std::optional getColorTextureFormat() const override { if (!colorTexture) { return std::nullopt; } const auto* texture = static_cast(colorTexture.get()); return static_cast(texture->getNativeFormat()); } WGPUTextureView getDepthStencilTextureView() override { if (depthTexture) { auto& texture = static_cast(*depthTexture); texture.create(); return texture.getTextureView(); } return nullptr; } std::optional getDepthStencilTextureFormat() const override { if (depthTexture) { const auto* texture = static_cast(depthTexture.get()); return static_cast(texture->getNativeFormat()); } return std::nullopt; } PremultipliedImage readStillImage() { // Read back the color texture data constexpr uint32_t bytesPerPixel = 4; // RGBA8 output const uint32_t rowStride = static_cast(size.width) * bytesPerPixel; const uint32_t alignedRowStride = ((rowStride + bytesPerRowAlignment - 1) / bytesPerRowAlignment) * bytesPerRowAlignment; const uint64_t alignedDataSize = static_cast(alignedRowStride) * size.height; auto data = std::make_unique(static_cast(rowStride) * size.height); // WebGPU texture readback requires: // 1. Ensure all rendering commands are submitted and completed // 2. Create a buffer with MAP_READ usage // 3. Copy texture to buffer using command encoder // 4. Map the buffer and read the data // This is an async operation in WebGPU, but we need sync behavior here auto& backend = static_cast(context.getBackend()); auto device = static_cast(backend.getDevice()); auto queue = static_cast(backend.getQueue()); if (!device || !queue) { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: No device or queue available for readback"); return {size, std::move(data)}; } // Process all pending device operations to ensure rendering is complete wgpuDeviceTick(device); // Create staging buffer for readback WGPUBufferDescriptor bufferDesc = {}; WGPUStringView bufferLabel = {"Readback Buffer", strlen("Readback Buffer")}; bufferDesc.label = bufferLabel; bufferDesc.size = alignedDataSize; bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; WGPUBuffer stagingBuffer = wgpuDeviceCreateBuffer(device, &bufferDesc); if (!stagingBuffer) { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to create staging buffer"); return {size, std::move(data)}; } // Copy texture to buffer WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr); if (encoder) { auto& webgpuTexture = static_cast(*colorTexture); WGPUTexture texture = webgpuTexture.getTexture(); if (texture) { WGPUTexelCopyTextureInfo src = {}; src.texture = texture; src.mipLevel = 0; src.origin = {0, 0, 0}; src.aspect = WGPUTextureAspect_All; WGPUTexelCopyBufferInfo dst = {}; dst.buffer = stagingBuffer; dst.layout.offset = 0; dst.layout.bytesPerRow = alignedRowStride; dst.layout.rowsPerImage = size.height; WGPUExtent3D copySize = {static_cast(size.width), static_cast(size.height), 1}; wgpuCommandEncoderCopyTextureToBuffer(encoder, &src, &dst, ©Size); WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); wgpuQueueSubmit(queue, 1, &commands); wgpuCommandBufferRelease(commands); } else { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Color texture is null"); } wgpuCommandEncoderRelease(encoder); } else { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to create command encoder"); } // Map buffer and read data (blocking) // Use WaitAny for proper synchronous blocking instead of polling struct MapContext { WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error; }; MapContext mapContext; WGPUBufferMapCallbackInfo callbackInfo = {}; callbackInfo.mode = WGPUCallbackMode_WaitAnyOnly; callbackInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView message, void* userdata1, void* userdata2) { auto* ctx = static_cast(userdata1); ctx->status = status; (void)message; (void)userdata2; }; callbackInfo.userdata1 = &mapContext; callbackInfo.userdata2 = nullptr; WGPUFuture future = wgpuBufferMapAsync(stagingBuffer, WGPUMapMode_Read, 0, alignedDataSize, callbackInfo); // Use WaitAny to block until mapping completes auto instance = static_cast(backend.getInstance()); if (instance) { WGPUFutureWaitInfo waitInfo = {}; waitInfo.future = future; waitInfo.completed = false; // Wait indefinitely for the mapping to complete WGPUWaitStatus waitStatus = wgpuInstanceWaitAny(instance, 1, &waitInfo, UINT64_MAX); if (waitStatus != WGPUWaitStatus_Success || !waitInfo.completed) { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Buffer mapping wait failed"); } } // Check if mapping was successful and read the data if (mapContext.status == WGPUMapAsyncStatus_Success) { const auto* mappedData = static_cast( wgpuBufferGetConstMappedRange(stagingBuffer, 0, alignedDataSize)); if (mappedData) { if (alignedRowStride == rowStride) { std::memcpy(data.get(), mappedData, static_cast(alignedDataSize)); } else { auto* dstPtr = data.get(); for (uint32_t row = 0; row < size.height; ++row) { std::memcpy(dstPtr + static_cast(row) * rowStride, mappedData + static_cast(row) * alignedRowStride, rowStride); } } } else { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Failed to get mapped range"); } } else { mbgl::Log::Error(mbgl::Event::Render, "WebGPU: Buffer mapping failed with status: " + std::to_string(mapContext.status)); } wgpuBufferUnmap(stagingBuffer); wgpuBufferRelease(stagingBuffer); return {size, std::move(data)}; } gfx::Texture2DPtr& getTexture() { assert(colorTexture); return colorTexture; } void setSizeListener(std::function listener) { sizeListener = std::move(listener); } private: void handleResize(const Size& newSize) { if (newSize == size || newSize.isEmpty()) { return; } size = newSize; if (depthTexture) { static_cast(*depthTexture).setSize(newSize); static_cast(*depthTexture).create(); } if (auto* color = static_cast(colorTexture.get())) { color->create(); } if (sizeListener) { sizeListener(size); } } Context& context; Size size; const gfx::TextureChannelDataType type; gfx::Texture2DPtr colorTexture; gfx::Texture2DPtr depthTexture; std::function sizeListener; }; OffscreenTexture::OffscreenTexture( Context& context, const Size size_, const gfx::TextureChannelDataType type, bool depth, bool stencil) : gfx::OffscreenTexture(size_, std::make_unique(context, size_, type, depth, stencil)) { auto& offscreenResource = getResource(); offscreenResource.setSizeListener([this](const Size& newSize) { size = newSize; }); } bool OffscreenTexture::isRenderable() { // Check if the texture resource is valid and can be rendered to try { auto& res = getResource(); return res.getTexture() != nullptr; } catch (...) { return false; } } PremultipliedImage OffscreenTexture::readStillImage() { return getResource().readStillImage(); } const gfx::Texture2DPtr& OffscreenTexture::getTexture() { return getResource().getTexture(); } } // namespace webgpu } // namespace mbgl