[Refactor] (MG_Backend/DirectVulkan, trace-replay): extract dump-image capture out of MobileGL backend

Moves snapshot capture entirely into the apitrace retrace layer (glReadPixels + PNG encode). Drops the MOBILEGL_PRESENT_DUMP_PATH / MOBILEGL_PRESENT_STATS / MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL / MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE plumbing from Config, ConfigLoader, VulkanRenderer (GetPresentedDumpPixel/WritePresentedDumpPpm + present-stats readback), the EGL/GLX/Android ws shims, and the Android trace_replay_core PPM reader.

DirectVulkan ReadPixels on the default framebuffer now remaps raw swapchain pixels (top-left origin, preTransform-rotated) to GL orientation (bottom-left origin) so the retrace snapshot matches the golden; SwapchainObject also resizes the default-FBO stencil attachment to the swapchain extent to fix GL_INVALID_FRAMEBUFFER_OPERATION under the glReadPixels completeness check.
This commit is contained in:
2026-07-14 02:40:20 -04:00
parent 533219ede7
commit 3445ab9304
11 changed files with 105 additions and 444 deletions
@@ -298,6 +298,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
// The default FBO's stencil attachment must track the swapchain extent:
// FramebufferObject::CheckCompleteness requires every valid attachment
// to share the same dimensions, and Init.cpp leaves a 512x512 placeholder.
// Without this the retrace-layer glReadPixels snapshot fails with
// GL_INVALID_FRAMEBUFFER_OPERATION on DirectVulkan.
TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D32_SFLOAT_S8_UINT:
stencilFormat = TextureInternalFormat::Depth32FStencil8;
break;
case VK_FORMAT_D24_UNORM_S8_UINT:
stencilFormat = TextureInternalFormat::Depth24Stencil8;
break;
default:
// No stencil plane; mirror the depth format for consistency.
stencilFormat = depthFormat;
break;
}
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
stencilTex->SetInternalFormat(stencilFormat);
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
}
void SwapchainObject::CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice) {
@@ -21,7 +21,6 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vulkan/vulkan_core.h>
@@ -639,24 +638,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return attributeMask;
}
static const char* PresentDumpPath() {
const String& path = MG_Config::Features.PresentDumpPath;
return path.empty() ? nullptr : path.c_str();
}
static Bool PresentDumpMatchesTargetCall() {
// MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL stay live
// getenv on purpose: the retrace harness mutates them at runtime via
// setenv to select which eglSwapBuffers call gets dumped, so they must
// not be snapshotted into MG_Config::Features at init time.
const char* target = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
if (target == nullptr || target[0] == '\0') {
return true;
}
const char* current = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL");
return current != nullptr && std::strcmp(target, current) == 0;
}
static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) {
switch (glType) {
case GL_FLOAT:
@@ -1516,10 +1497,6 @@ void main() {
}
}
static Bool PresentStatsEnabled() {
return MG_Config::Features.PresentStats;
}
static Bool IsBgraVkFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_B8G8R8A8_UNORM:
@@ -1533,65 +1510,46 @@ void main() {
}
}
static VkExtent2D GetPresentedDumpExtent(VkExtent2D rawExtent,
VkSurfaceTransformFlagBitsKHR preTransform) {
// Remap raw swapchain pixels (top-left origin, preTransform-rotated) into
// GL-oriented pixels (bottom-left origin) for the retrace snapshot path.
// Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip
// apitrace's flipped=true Image expects. Only identity/180 share the
// swapchain extent with the default framebuffer; 90/270 swap extents and
// are not handled here.
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
VkExtent2D rawExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
Uint8* outPixels) {
if (IsQuarterTurnPreTransform(preTransform)) {
return {rawExtent.height, rawExtent.width};
}
return rawExtent;
}
static const Uint8* GetPresentedDumpPixel(const Uint8* rawPixels,
VkExtent2D rawExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
Uint32 presentedX,
Uint32 presentedY) {
Uint32 rawX = presentedX;
Uint32 rawY = presentedY;
switch (preTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
rawX = rawExtent.width - 1 - presentedY;
rawY = presentedX;
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
rawX = rawExtent.width - 1 - presentedX;
rawY = rawExtent.height - 1 - presentedY;
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
rawX = presentedY;
rawY = rawExtent.height - 1 - presentedX;
break;
default:
break;
}
return rawPixels + (static_cast<SizeT>(rawY) * rawExtent.width + rawX) * 4;
}
static Bool WritePresentedDumpPpm(const char* path,
const Uint8* rawPixels,
VkExtent2D rawExtent,
VkFormat rawFormat,
VkSurfaceTransformFlagBitsKHR preTransform) {
FILE* dump = std::fopen(path, "wb");
if (dump == nullptr) {
return false;
}
const VkExtent2D presentedExtent = GetPresentedDumpExtent(rawExtent, preTransform);
const Bool presentIsBgra = IsBgraVkFormat(rawFormat);
std::fprintf(dump, "P6\n%u %u\n255\n", presentedExtent.width, presentedExtent.height);
for (Uint32 y = 0; y < presentedExtent.height; ++y) {
for (Uint32 x = 0; x < presentedExtent.width; ++x) {
const Uint8* p = GetPresentedDumpPixel(rawPixels, rawExtent, preTransform, x, y);
const Uint8 rgb[3] = {
presentIsBgra ? p[2] : p[0],
p[1],
presentIsBgra ? p[0] : p[2],
};
std::fwrite(rgb, 1, sizeof(rgb), dump);
const Uint32 w = rawExtent.width;
const Uint32 h = rawExtent.height;
if (w == 0 || h == 0) {
return false;
}
for (Uint32 outY = 0; outY < h; ++outY) {
const Uint32 displayY = h - 1 - outY; // GL bottom-origin -> display top-origin
for (Uint32 outX = 0; outX < w; ++outX) {
const Uint32 displayX = outX;
Uint32 rawX = displayX;
Uint32 rawY = displayY;
switch (preTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
rawX = w - 1 - displayX;
rawY = h - 1 - displayY;
break;
default:
break;
}
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * 4;
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * 4;
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
}
}
std::fclose(dump);
return true;
}
@@ -4804,16 +4762,6 @@ void main() {
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
: *srcBinding.trackedLayout;
if (PresentStatsEnabled()) {
std::fprintf(stderr,
"MOBILEGL_READPIXELS_BEGIN defaultFbo=%s x=%d y=%d width=%d height=%d srcLayout=%d recording=%s recorded=%s imageIndex=%u\n",
readIsDefaultFbo ? "true" : "false",
x, y, width, height,
static_cast<Int>(srcOriginalLayout),
frame.isCommandRecording ? "true" : "false",
frame.hasCommandBufferRecorded ? "true" : "false",
m_imageIndexAcquired);
}
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined");
return;
@@ -4890,24 +4838,24 @@ void main() {
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
return;
}
if (PresentStatsEnabled()) {
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
SizeT nonBlack = 0;
SizeT nonTransparent = 0;
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* p = mapped + i * 4;
if (p[0] != 0 || p[1] != 0 || p[2] != 0) {
++nonBlack;
}
if (p[3] != 0) {
++nonTransparent;
const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM;
if (readIsDefaultFbo) {
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
if (static_cast<Uint32>(width) == swapchainExtent.width &&
static_cast<Uint32>(height) == swapchainExtent.height) {
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4);
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
remapped.data())) {
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
return;
}
}
std::fprintf(stderr,
"MOBILEGL_READPIXELS_STATS nonBlack=%zu/%zu alpha=%zu/%zu\n",
nonBlack, pixelCount, nonTransparent, pixelCount);
MGLOG_W("DirectVulkan::ReadPixels: default-FBO remap skipped (w=%d h=%d swapchain=%ux%u preTransform=%d); "
"falling back to raw readback",
width, height, swapchainExtent.width, swapchainExtent.height,
static_cast<Int>(preTransform));
}
const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM;
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
}
@@ -5918,80 +5866,6 @@ void main() {
if (activeRenderPass)
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
VkBufferObject presentStatsReadback;
VkDeviceSize presentStatsReadbackSize = 0;
const VkExtent2D presentStatsExtent = m_swapchainObject.GetExtent();
const char* presentDumpPath = PresentDumpPath();
const Bool shouldDumpPresent = presentDumpPath != nullptr && PresentDumpMatchesTargetCall();
const Bool wantPresentStats = (PresentStatsEnabled() || shouldDumpPresent) &&
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
if (wantPresentStats && !frame.isCommandRecording) {
// A mid-frame flush may have closed the frame's recording; the
// stats copy needs an open command buffer.
m_frameContext.BeginCommandRecording();
}
const Bool collectPresentStats = wantPresentStats && frame.isCommandRecording;
if (PresentStatsEnabled() && presentDumpPath != nullptr) {
// Live getenv on purpose (not MG_Config::Features): the retrace
// harness mutates these two variables at runtime via setenv.
const char* targetCall = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
const char* currentCall = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL");
std::fprintf(stderr,
"MOBILEGL_PRESENT_DUMP_GATE target=%s current=%s shouldDump=%s recording=%s size=%ux%u path=%s\n",
targetCall != nullptr ? targetCall : "",
currentCall != nullptr ? currentCall : "",
shouldDumpPresent ? "true" : "false",
frame.isCommandRecording ? "true" : "false",
presentStatsExtent.width, presentStatsExtent.height,
presentDumpPath);
}
if (collectPresentStats) {
presentStatsReadbackSize = static_cast<VkDeviceSize>(presentStatsExtent.width) *
static_cast<VkDeviceSize>(presentStatsExtent.height) * 4;
const Bool readbackCreated = presentStatsReadback.Create({
.allocator = m_allocator,
.size = presentStatsReadbackSize,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
});
MOBILEGL_ASSERT(readbackCreated, "Present stats: failed to create readback buffer");
VkImageLayout statsOriginalLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
Bool toTransferSrc = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, m_swapchainObject.GetImage(m_imageIndexAcquired),
statsOriginalLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT);
MOBILEGL_ASSERT(toTransferSrc, "Present stats: failed to transition swapchain for readback");
VkBufferImageCopy copyRegion{};
copyRegion.bufferOffset = 0;
copyRegion.bufferRowLength = 0;
copyRegion.bufferImageHeight = 0;
copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copyRegion.imageSubresource.mipLevel = 0;
copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = 1;
copyRegion.imageOffset = {0, 0, 0};
copyRegion.imageExtent = {presentStatsExtent.width, presentStatsExtent.height, 1};
vkCmdCopyImageToBuffer(frame.commandBuffer,
m_swapchainObject.GetImage(m_imageIndexAcquired),
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
presentStatsReadback.GetHandle(),
1,
&copyRegion);
Bool restoreLayout = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, m_swapchainObject.GetImage(m_imageIndexAcquired),
statsOriginalLayout, m_swapchainObject.GetImageLayout(m_imageIndexAcquired),
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT);
MOBILEGL_ASSERT(restoreLayout, "Present stats: failed to restore swapchain layout after readback");
}
if (frame.isCommandRecording) {
m_frameContext.EndCommandRecording();
frame.hasCommandBufferRecorded = true;
@@ -6008,50 +5882,6 @@ void main() {
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence));
RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false);
frame.lastSubmitIndex = m_submitCounter;
if (collectPresentStats) {
VK_VERIFY(vkWaitForFences(m_device, 1, &frame.imageInFlightFence, VK_TRUE, UINT64_MAX),
"Present stats, vkWaitForFences");
OnSubmitsCompletedUpTo(frame.lastSubmitIndex);
const auto* pixels = static_cast<const Uint8*>(presentStatsReadback.Map());
MOBILEGL_ASSERT(pixels != nullptr, "Present stats: failed to map readback buffer");
SizeT nonBlack = 0;
SizeT nonTransparent = 0;
SizeT colored = 0;
const SizeT pixelCount = static_cast<SizeT>(presentStatsExtent.width) *
static_cast<SizeT>(presentStatsExtent.height);
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* p = pixels + i * 4;
if (p[0] != 0 || p[1] != 0 || p[2] != 0) {
++nonBlack;
}
const Uint8 minRgb = std::min(p[0], std::min(p[1], p[2]));
const Uint8 maxRgb = std::max(p[0], std::max(p[1], p[2]));
if (maxRgb - minRgb > 24) {
++colored;
}
if (p[3] != 0) {
++nonTransparent;
}
}
if (PresentStatsEnabled()) {
std::fprintf(stderr,
"MOBILEGL_PRESENT_STATS nonBlack=%zu/%zu colored=%zu/%zu alpha=%zu/%zu size=%ux%u\n",
nonBlack, pixelCount, colored, pixelCount, nonTransparent, pixelCount,
presentStatsExtent.width, presentStatsExtent.height);
}
if (shouldDumpPresent) {
const VkFormat presentFormat = m_swapchainObject.GetSurfaceFormat().format;
if (WritePresentedDumpPpm(presentDumpPath, pixels, presentStatsExtent, presentFormat,
m_swapchainObject.GetPreTransform())) {
if (PresentStatsEnabled()) {
std::fprintf(stderr, "MOBILEGL_PRESENT_DUMP_WRITTEN path=%s bytes=%zu\n",
presentDumpPath, static_cast<SizeT>(pixelCount * 3));
}
} else if (PresentStatsEnabled()) {
std::fprintf(stderr, "MOBILEGL_PRESENT_DUMP_FAILED path=%s\n", presentDumpPath);
}
}
}
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);