From 9fa80b37d2022deee257e9c70590627e104bb78b Mon Sep 17 00:00:00 2001 From: Adrian Cojocaru Date: Sat, 6 Jun 2026 02:06:40 +0300 Subject: [PATCH] Implement Vulkan surface snapshot (#4314) --- include/mbgl/vulkan/renderable_resource.hpp | 7 +- include/mbgl/vulkan/texture2d.hpp | 1 + .../src/cpp/android_renderer_backend.hpp | 1 + .../cpp/android_vulkan_renderer_backend.cpp | 7 +- .../cpp/android_vulkan_renderer_backend.hpp | 1 + .../MapLibreAndroid/src/cpp/map_renderer.cpp | 6 + platform/glfw/glfw_view.cpp | 2 +- src/mbgl/vulkan/renderable_resource.cpp | 103 +++++++++++++++++- src/mbgl/vulkan/texture2d.cpp | 27 +++++ 9 files changed, 147 insertions(+), 8 deletions(-) diff --git a/include/mbgl/vulkan/renderable_resource.hpp b/include/mbgl/vulkan/renderable_resource.hpp index ca675627f..9c45f4440 100644 --- a/include/mbgl/vulkan/renderable_resource.hpp +++ b/include/mbgl/vulkan/renderable_resource.hpp @@ -55,7 +55,8 @@ public: const vk::UniqueSurfaceKHR& getPlatformSurface() const { return surface; } const vk::UniqueSwapchainKHR& getSwapchain() const { return swapchain; } - const vk::UniqueFramebuffer& getFramebuffer() const override; + const vk::UniqueFramebuffer& getFramebuffer() const override { return swapchainFramebuffers[acquiredImageIndex]; } + vk::Format getColorFormat() const { return colorFormat; } uint32_t getImageCount() const { return static_cast(swapchainFramebuffers.size()); }; uint32_t getAcquiredImageIndex() const { return acquiredImageIndex; }; @@ -76,6 +77,9 @@ public: void init(uint32_t w, uint32_t h); void recreateSwapchain(); + void enableSurfaceRead(bool value = true); + std::shared_ptr readImage(); + protected: vk::UniqueSurfaceKHR surface; vk::UniqueSwapchainKHR swapchain; @@ -99,6 +103,7 @@ protected: vk::Format depthFormat{vk::Format::eUndefined}; int32_t surfaceTransformPollingInterval{-1}; + bool surfaceRead{false}; }; class Renderable : public gfx::Renderable { diff --git a/include/mbgl/vulkan/texture2d.hpp b/include/mbgl/vulkan/texture2d.hpp index d30a9c2a8..2de4fe6ec 100644 --- a/include/mbgl/vulkan/texture2d.hpp +++ b/include/mbgl/vulkan/texture2d.hpp @@ -87,6 +87,7 @@ public: const vk::Sampler& getVulkanSampler(); void copyImage(vk::Image image, Size imageSize, uint16_t xOffset = 0, uint16_t yOffset = 0); + void blitImage(vk::Image image, Size imageSize, uint16_t xOffset = 0, uint16_t yOffset = 0); std::shared_ptr readImage(); private: diff --git a/platform/android/MapLibreAndroid/src/cpp/android_renderer_backend.hpp b/platform/android/MapLibreAndroid/src/cpp/android_renderer_backend.hpp index 64fac445f..f6925967f 100644 --- a/platform/android/MapLibreAndroid/src/cpp/android_renderer_backend.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/android_renderer_backend.hpp @@ -31,6 +31,7 @@ public: virtual void destroySurface() {} virtual void resizeFramebuffer(int width, int height); + virtual void enableFramebufferRead(bool value) {} virtual PremultipliedImage readFramebuffer(); gfx::Renderable::SwapBehaviour getSwapBehavior() const { return swapBehaviour; } diff --git a/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.cpp b/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.cpp index 949cc862d..01c6599d7 100644 --- a/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.cpp @@ -83,9 +83,12 @@ void AndroidVulkanRendererBackend::resizeFramebuffer(int, int) { } } +void AndroidVulkanRendererBackend::enableFramebufferRead(bool value) { + getResource().enableSurfaceRead(); +} + PremultipliedImage AndroidVulkanRendererBackend::readFramebuffer() { - // TODO not implemented - return PremultipliedImage(Size(2, 2)); + return std::move(*getResource().readImage()); } } // namespace android diff --git a/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.hpp b/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.hpp index d18ec0fa0..0a4791c9d 100644 --- a/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/android_vulkan_renderer_backend.hpp @@ -24,6 +24,7 @@ public: std::vector getInstanceExtensions() override; void resizeFramebuffer(int width, int height) override; + void enableFramebufferRead(bool value) override; PremultipliedImage readFramebuffer() override; // mbgl::gfx::RendererBackend implementation diff --git a/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp b/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp index 2fbdb4343..e5887b4f1 100644 --- a/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp @@ -207,6 +207,12 @@ void MapRenderer::resetRenderer() { void MapRenderer::scheduleSnapshot(std::unique_ptr callback) { snapshotCallback = std::move(callback); + + if (backend) { + gfx::BackendScope backendGuard{backend->getImpl()}; + backend->enableFramebufferRead(true); + } + requestRender(); } diff --git a/platform/glfw/glfw_view.cpp b/platform/glfw/glfw_view.cpp index e7880722d..1701ae9b0 100644 --- a/platform/glfw/glfw_view.cpp +++ b/platform/glfw/glfw_view.cpp @@ -977,7 +977,7 @@ void GLFWView::makeSnapshot(bool withOverlay) { oss << "Made snapshot './snapshot.png' with size w:" << image.size.width << "px h:" << image.size.height << "px"; mbgl::Log::Info(mbgl::Event::General, oss.str()); - std::ofstream file("./snapshot.png"); + std::ofstream file("./snapshot.png", std::ios::out | std::ios::binary); file << mbgl::encodePNG(image); } else { mbgl::Log::Error(mbgl::Event::General, "Failed to make a snapshot!"); diff --git a/src/mbgl/vulkan/renderable_resource.cpp b/src/mbgl/vulkan/renderable_resource.cpp index b4861a1f0..07f1f66ec 100644 --- a/src/mbgl/vulkan/renderable_resource.cpp +++ b/src/mbgl/vulkan/renderable_resource.cpp @@ -92,9 +92,17 @@ void SurfaceRenderableResource::initSwapchain(uint32_t w, uint32_t h) { } } - // pick surface size + vk::ImageUsageFlags imageUsage = vk::ImageUsageFlagBits::eColorAttachment; + capabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.get(), dispatcher); + // when reading from the swapchain disable pre-rotation and enable source transfer usage + if (surfaceRead) { + capabilities.currentTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity; + imageUsage |= vk::ImageUsageFlagBits::eTransferSrc; + } + + // pick surface size if (capabilities.currentExtent.width != std::numeric_limits::max()) { extent = capabilities.currentExtent; } else { @@ -124,7 +132,7 @@ void SurfaceRenderableResource::initSwapchain(uint32_t w, uint32_t h) { .setPresentMode(presentMode) .setImageExtent(extent) .setImageArrayLayers(1) - .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment); + .setImageUsage(imageUsage); int32_t graphicsQueueIndex = backend.getGraphicsQueueIndex(); int32_t presentQueueIndex = backend.getPresentQueueIndex(); @@ -449,8 +457,95 @@ void SurfaceRenderableResource::recreateSwapchain() { init(extent.width, extent.height); } -const vk::UniqueFramebuffer& SurfaceRenderableResource::getFramebuffer() const { - return swapchainFramebuffers[acquiredImageIndex]; +void SurfaceRenderableResource::enableSurfaceRead(bool value) { + if (surfaceRead == value) { + return; + } + + surfaceRead = value; + backend.getContext().requestSurfaceUpdate(false); +} + +std::shared_ptr SurfaceRenderableResource::readImage() { + auto& contextImpl = backend.getContext(); + const auto& dispatcher = backend.getDispatcher(); + const auto& physicalDevice = backend.getPhysicalDevice(); + + auto texture = std::make_unique(contextImpl); + texture->setFormat(gfx::TexturePixelType::RGBA, gfx::TextureChannelDataType::UnsignedByte); + texture->setUsage(Texture2DUsage::Read); + texture->setSize({extent.width, extent.height}); + + contextImpl.waitFrame(); + + const auto swapchainImage = getAcquiredImage(); + + if (surface) { + contextImpl.submitOneTimeCommand([&](const vk::UniqueCommandBuffer& buffer) { + const auto barrier = vk::ImageMemoryBarrier() + .setImage(swapchainImage) + .setOldLayout(vk::ImageLayout::ePresentSrcKHR) + .setNewLayout(vk::ImageLayout::eTransferSrcOptimal) + .setSrcAccessMask({}) + .setDstAccessMask(vk::AccessFlagBits::eTransferWrite) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + + buffer->pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTransfer, + {}, + nullptr, + nullptr, + barrier, + dispatcher); + }); + } + + bool useBlit = surface && + (physicalDevice.getFormatProperties(colorFormat, dispatcher).optimalTilingFeatures & + vk::FormatFeatureFlagBits::eBlitSrc && + physicalDevice.getFormatProperties(texture->getVulkanFormat(), dispatcher).linearTilingFeatures & + vk::FormatFeatureFlagBits::eBlitDst); + + if (useBlit) { + texture->blitImage(swapchainImage, texture->getSize()); + } else { + texture->copyImage(swapchainImage, texture->getSize()); + } + + if (surface) { + contextImpl.submitOneTimeCommand([&](const vk::UniqueCommandBuffer& buffer) { + const auto barrier = vk::ImageMemoryBarrier() + .setImage(swapchainImage) + .setOldLayout(vk::ImageLayout::eTransferSrcOptimal) + .setNewLayout(vk::ImageLayout::ePresentSrcKHR) + .setSrcAccessMask(vk::AccessFlagBits::eMemoryRead) + .setDstAccessMask(vk::AccessFlagBits::eTransferWrite) + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); + + buffer->pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTransfer, + {}, + nullptr, + nullptr, + barrier, + dispatcher); + }); + } + + const auto image = texture->readImage(); + + // swizzle pixels to RGBA + if (!useBlit && colorFormat == vk::Format::eB8G8R8A8Unorm) { + for (size_t i = 0, size = image->size.area(); i < size; ++i) { + std::swap(image->data[i * image->channels + 0], image->data[i * image->channels + 2]); + } + } + + return image; } } // namespace vulkan diff --git a/src/mbgl/vulkan/texture2d.cpp b/src/mbgl/vulkan/texture2d.cpp index bc5c5d4c6..01da8c151 100644 --- a/src/mbgl/vulkan/texture2d.cpp +++ b/src/mbgl/vulkan/texture2d.cpp @@ -580,6 +580,33 @@ void Texture2D::copyImage(vk::Image image, Size imageSize, uint16_t xOffset, uin }); } +void Texture2D::blitImage(vk::Image image, Size imageSize, uint16_t xOffset, uint16_t yOffset) { + if (!image) return; + + create(); + + context.submitOneTimeCommand([&](const vk::UniqueCommandBuffer& commandBuffer) { + const std::array offsets = { + vk::Offset3D{xOffset, yOffset, 0}, + {static_cast(imageSize.width), static_cast(imageSize.height), 1}}; + const auto blitInfo = vk::ImageBlit() + .setSrcOffsets(offsets) + .setDstOffsets(offsets) + .setSrcSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1}) + .setDstSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1}); + + transitionToTransferWriteLayout(commandBuffer); + commandBuffer->blitImage(image, + vk::ImageLayout::eTransferSrcOptimal, + imageAllocation->image, + imageLayout, + blitInfo, + vk::Filter::eNearest, + context.getBackend().getDispatcher()); + transitionToGeneralLayout(commandBuffer); + }); +} + std::shared_ptr Texture2D::readImage() { if (!imageData) { imageData = std::make_shared();