[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
-6
View File
@@ -27,8 +27,6 @@ namespace MobileGL::MG_Config {
// //
// Env variables intentionally NOT mirrored here (kept as live std::getenv at their // Env variables intentionally NOT mirrored here (kept as live std::getenv at their
// call sites): // call sites):
// - MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL: the retrace harness
// mutates them at runtime via setenv, so a one-shot snapshot would go stale.
// - DISPLAY: X11 session variable, not MobileGL configuration. // - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init // - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp). // (see MG_Util/Debug/Log.cpp).
@@ -46,10 +44,6 @@ namespace MobileGL::MG_Config {
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers, // MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe. // resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false; Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_PRESENT_DUMP_PATH: directory for present frame dumps (DirectVulkan).
String PresentDumpPath;
// MOBILEGL_PRESENT_STATS: log present pixel statistics (DirectVulkan backend).
Bool PresentStats = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp). // MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false; Bool TraceSkipAutodestroy = false;
}; };
-2
View File
@@ -116,8 +116,6 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64); features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter = features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER"); QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
QueryEnvVariable("MOBILEGL_PRESENT_DUMP_PATH", features.PresentDumpPath, "");
features.PresentStats = QueryEnvFlag("MOBILEGL_PRESENT_STATS");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
} }
@@ -298,6 +298,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
{extentWidth, extentHeight, 1}, {extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size 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) { void SwapchainObject::CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice) {
@@ -21,7 +21,6 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h> #include <Config.h>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
@@ -639,24 +638,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return attributeMask; 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) { static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) {
switch (glType) { switch (glType) {
case GL_FLOAT: case GL_FLOAT:
@@ -1516,10 +1497,6 @@ void main() {
} }
} }
static Bool PresentStatsEnabled() {
return MG_Config::Features.PresentStats;
}
static Bool IsBgraVkFormat(VkFormat format) { static Bool IsBgraVkFormat(VkFormat format) {
switch (format) { switch (format) {
case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_UNORM:
@@ -1533,65 +1510,46 @@ void main() {
} }
} }
static VkExtent2D GetPresentedDumpExtent(VkExtent2D rawExtent, // Remap raw swapchain pixels (top-left origin, preTransform-rotated) into
VkSurfaceTransformFlagBitsKHR preTransform) { // 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)) { 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; return false;
} }
const Uint32 w = rawExtent.width;
const VkExtent2D presentedExtent = GetPresentedDumpExtent(rawExtent, preTransform); const Uint32 h = rawExtent.height;
const Bool presentIsBgra = IsBgraVkFormat(rawFormat); if (w == 0 || h == 0) {
std::fprintf(dump, "P6\n%u %u\n255\n", presentedExtent.width, presentedExtent.height); return false;
for (Uint32 y = 0; y < presentedExtent.height; ++y) { }
for (Uint32 x = 0; x < presentedExtent.width; ++x) { for (Uint32 outY = 0; outY < h; ++outY) {
const Uint8* p = GetPresentedDumpPixel(rawPixels, rawExtent, preTransform, x, y); const Uint32 displayY = h - 1 - outY; // GL bottom-origin -> display top-origin
const Uint8 rgb[3] = { for (Uint32 outX = 0; outX < w; ++outX) {
presentIsBgra ? p[2] : p[0], const Uint32 displayX = outX;
p[1], Uint32 rawX = displayX;
presentIsBgra ? p[0] : p[2], Uint32 rawY = displayY;
}; switch (preTransform) {
std::fwrite(rgb, 1, sizeof(rgb), dump); 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; return true;
} }
@@ -4804,16 +4762,6 @@ void main() {
const VkImageLayout srcOriginalLayout = readIsDefaultFbo const VkImageLayout srcOriginalLayout = readIsDefaultFbo
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired) ? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
: *srcBinding.trackedLayout; : *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) { if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined"); MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined");
return; return;
@@ -4890,24 +4838,24 @@ void main() {
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer"); MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
return; return;
} }
if (PresentStatsEnabled()) { const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM;
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height); if (readIsDefaultFbo) {
SizeT nonBlack = 0; const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
SizeT nonTransparent = 0; const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
for (SizeT i = 0; i < pixelCount; ++i) { if (static_cast<Uint32>(width) == swapchainExtent.width &&
const Uint8* p = mapped + i * 4; static_cast<Uint32>(height) == swapchainExtent.height) {
if (p[0] != 0 || p[1] != 0 || p[2] != 0) { Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4);
++nonBlack; if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
} remapped.data())) {
if (p[3] != 0) { PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
++nonTransparent; return;
} }
} }
std::fprintf(stderr, MGLOG_W("DirectVulkan::ReadPixels: default-FBO remap skipped (w=%d h=%d swapchain=%ux%u preTransform=%d); "
"MOBILEGL_READPIXELS_STATS nonBlack=%zu/%zu alpha=%zu/%zu\n", "falling back to raw readback",
nonBlack, pixelCount, nonTransparent, pixelCount); 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); PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
} }
@@ -5918,80 +5866,6 @@ void main() {
if (activeRenderPass) if (activeRenderPass)
VkRenderPassManager::EndRenderPass(frame.commandBuffer); 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) { if (frame.isCommandRecording) {
m_frameContext.EndCommandRecording(); m_frameContext.EndCommandRecording();
frame.hasCommandBufferRecorded = true; frame.hasCommandBufferRecorded = true;
@@ -6008,50 +5882,6 @@ void main() {
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence)); VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence));
RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false); RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false);
frame.lastSubmitIndex = m_submitCounter; 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.isCommandRecording = false;
frame.hasCommandBufferRecorded = false; frame.hasCommandBufferRecorded = false;
m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
+1
View File
@@ -46,6 +46,7 @@ Implementation notes:
- The trace APK builds independently from FCL and can be launched with `adb shell am start`. - The trace APK builds independently from FCL and can be launched with `adb shell am start`.
- The native runner validates inputs, sets `MOBILEGL_BACKEND_TYPE`, loads `libMobileGL.so`, runs apitrace GL retrace, writes `actual.png`, and writes `result.json`. - The native runner validates inputs, sets `MOBILEGL_BACKEND_TYPE`, loads `libMobileGL.so`, runs apitrace GL retrace, writes `actual.png`, and writes `result.json`.
- Snapshot capture stays entirely in the retrace layer: apitrace selects the target call, obtains drawable/read-buffer state, calls MobileGL's public `glReadPixels`, and encodes the returned pixels to PNG. MobileGL has no trace-call or output-path hooks.
- The runner uses a MobileGL-backed EGL window-system shim. GLX calls in PC traces are consumed by apitrace's GLX retrace frontend and mapped onto this EGL shim; the Android runner does not require or call a MobileGL GLX implementation. - The runner uses a MobileGL-backed EGL window-system shim. GLX calls in PC traces are consumed by apitrace's GLX retrace frontend and mapped onto this EGL shim; the Android runner does not require or call a MobileGL GLX implementation.
- `DirectGLES` and `DirectVulkan` replay on the Activity `SurfaceView` by default. DirectGLES can still use the old offscreen EGL pbuffer path by passing `use_pbuffer=true`. - `DirectGLES` and `DirectVulkan` replay on the Activity `SurfaceView` by default. DirectGLES can still use the old offscreen EGL pbuffer path by passing `use_pbuffer=true`.
- Golden comparison is implemented in native C++ with libpng RGBA decode and SSIM validation. The Java Activity only passes arguments and displays the native result, so the replay/compare core is not tied to Android UI or Bitmap APIs and can be ported to Linux. - Golden comparison is implemented in native C++ with libpng RGBA decode and SSIM validation. The Java Activity only passes arguments and displays the native result, so the replay/compare core is not tied to Android UI or Bitmap APIs and can be ported to Linux.
@@ -1,8 +1,6 @@
#include "glproc.hpp" #include "glproc.hpp"
#include <GLES3/gl3.h> #include <GLES3/gl3.h>
#include <cstdlib>
#include <cstdio>
#include <cstring> #include <cstring>
#include <dlfcn.h> #include <dlfcn.h>
@@ -56,29 +54,6 @@ void MobileGLTraceReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GL
if (gRealGlReadPixels != nullptr) { if (gRealGlReadPixels != nullptr) {
gRealGlReadPixels(x, y, width, height, format, type, pixels); gRealGlReadPixels(x, y, width, height, format, type, pixels);
} }
const char *stats = getenv("MOBILEGL_PRESENT_STATS");
if (stats == nullptr || stats[0] == '\0' || strcmp(stats, "0") == 0 ||
pixels == nullptr || width <= 0 || height <= 0 ||
format != GL_RGBA || type != GL_UNSIGNED_BYTE) {
return;
}
const auto *bytes = static_cast<const unsigned char *>(pixels);
const size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(height);
size_t nonBlack = 0;
size_t nonTransparent = 0;
for (size_t i = 0; i < pixelCount; ++i) {
const unsigned char *p = bytes + i * 4;
if (p[0] != 0 || p[1] != 0 || p[2] != 0) {
++nonBlack;
}
if (p[3] != 0) {
++nonTransparent;
}
}
fprintf(stderr,
"MOBILEGL_GLPROC_READPIXELS x=%d y=%d width=%d height=%d nonBlack=%zu/%zu alpha=%zu/%zu real=%p\n",
x, y, width, height, nonBlack, pixelCount, nonTransparent, pixelCount,
reinterpret_cast<void *>(gRealGlReadPixels));
} }
void MobileGLTraceGetIntegerv(GLenum pname, GLint *data) { void MobileGLTraceGetIntegerv(GLenum pname, GLint *data) {
@@ -183,22 +183,8 @@ public:
void swapBuffers() override { void swapBuffers() override {
if (surface != EGL_NO_SURFACE) { if (surface != EGL_NO_SURFACE) {
char callNo[32]; char callNo[32];
const char *overrideCallNo = getenv("MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE"); snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
if (overrideCallNo != nullptr && overrideCallNo[0] != '\0') {
snprintf(callNo, sizeof(callNo), "%s", overrideCallNo);
} else {
snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
}
const char *targetCall = getenv("MOBILEGL_PRESENT_DUMP_CALL");
const char *presentStats = getenv("MOBILEGL_PRESENT_STATS");
if (targetCall != nullptr && targetCall[0] != '\0' &&
presentStats != nullptr && presentStats[0] != '\0' && strcmp(presentStats, "0") != 0) {
std::cerr << "MOBILEGL_TRACE_SWAP call=" << callNo
<< " target=" << targetCall << "\n";
}
setenv("MOBILEGL_PRESENT_CURRENT_CALL", callNo, 1);
gEgl.swapBuffers(gDisplay, surface); gEgl.swapBuffers(gDisplay, surface);
unsetenv("MOBILEGL_PRESENT_CURRENT_CALL");
HoldAfterTargetPresent(callNo); HoldAfterTargetPresent(callNo);
} }
} }
@@ -3,7 +3,6 @@
#include <dlfcn.h> #include <dlfcn.h>
#include "apitrace_exit.hpp" #include "apitrace_exit.hpp"
#include "png.h" #include "png.h"
#include "trace_parser.hpp"
#include <algorithm> #include <algorithm>
#include <cerrno> #include <cerrno>
@@ -11,12 +10,10 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cctype>
#include <cstring> #include <cstring>
#include <exception> #include <exception>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <limits>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -357,106 +354,15 @@ void ForceOpaqueAlpha(RgbaImage& image) {
} }
} }
bool ReadPpmRgbAsRgba(const std::string& path, RgbaImage& image, std::string& error) { std::string SnapshotPathForCall(const Request& request) {
std::ifstream file(path, std::ios::binary);
if (!file) {
error = "failed to open PPM: " + path;
return false;
}
auto readToken = [&file]() -> std::string {
std::string token;
char ch = 0;
while (file.get(ch)) {
if (ch == '#') {
file.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
continue;
}
if (!std::isspace(static_cast<unsigned char>(ch))) {
token.push_back(ch);
break;
}
}
while (file.get(ch)) {
if (std::isspace(static_cast<unsigned char>(ch))) {
break;
}
token.push_back(ch);
}
return token;
};
const std::string magic = readToken();
const std::string widthToken = readToken();
const std::string heightToken = readToken();
const std::string maxToken = readToken();
if (magic != "P6" || widthToken.empty() || heightToken.empty() || maxToken != "255") {
error = "unsupported PPM header: " + path;
return false;
}
image.width = std::stoi(widthToken);
image.height = std::stoi(heightToken);
if (image.width <= 0 || image.height <= 0) {
error = "PPM has invalid dimensions: " + path;
return false;
}
std::vector<std::uint8_t> rgb(static_cast<std::size_t>(image.width) * image.height * 3);
file.read(reinterpret_cast<char*>(rgb.data()), static_cast<std::streamsize>(rgb.size()));
if (file.gcount() != static_cast<std::streamsize>(rgb.size())) {
error = "PPM payload is truncated: " + path;
return false;
}
image.pixels.resize(static_cast<std::size_t>(image.width) * image.height * 4);
for (std::size_t i = 0, j = 0; i < rgb.size(); i += 3, j += 4) {
image.pixels[j + 0] = rgb[i + 0];
image.pixels[j + 1] = rgb[i + 1];
image.pixels[j + 2] = rgb[i + 2];
image.pixels[j + 3] = 0xff;
}
return true;
}
std::string SnapshotPathForCall(const Request& request, bool usePresentDump) {
char call[16]; char call[16];
snprintf(call, sizeof(call), "%010lld", usePresentDump ? request.targetCall + 1 : request.targetCall); snprintf(call, sizeof(call), "%010lld", request.targetCall);
return request.outputDir + "/actual." + call + ".png"; return request.outputDir + "/actual." + call + ".png";
} }
bool TargetCallSwapsRenderTarget(const Request& request, bool& swapsRenderTarget, std::string& error) { int RunRetraceMain(const Request& request) {
trace::Parser parser;
if (!parser.open(request.tracePath.c_str())) {
error = "failed to open trace for target call inspection";
return false;
}
trace::Call* call = nullptr;
while ((call = parser.parse_call()) != nullptr) {
const long long callNo = static_cast<long long>(call->no);
if (callNo == request.targetCall) {
swapsRenderTarget = (call->flags & trace::CALL_FLAG_SWAP_RENDERTARGET) != 0;
delete call;
return true;
}
if (callNo > request.targetCall) {
delete call;
break;
}
delete call;
}
std::ostringstream message;
message << "target_call " << request.targetCall << " was not found in trace";
error = message.str();
return false;
}
int RunRetraceMain(const Request& request, bool usePresentDump) {
std::string prefix = request.outputDir + "/actual."; std::string prefix = request.outputDir + "/actual.";
const long long snapshotCall = usePresentDump ? request.targetCall + 1 : request.targetCall; std::string callSet = std::to_string(request.targetCall);
std::string callSet = std::to_string(snapshotCall);
std::string arg0 = "mobilegl-glretrace"; std::string arg0 = "mobilegl-glretrace";
std::string argBenchmark = "-b"; std::string argBenchmark = "-b";
@@ -483,12 +389,12 @@ int RunRetraceMain(const Request& request, bool usePresentDump) {
return MOBILEGL_APITRACE_RETRACE_MAIN(10, argv); return MOBILEGL_APITRACE_RETRACE_MAIN(10, argv);
} }
bool RunRetrace(const Request& request, bool usePresentDump, Result& result) { bool RunRetrace(const Request& request, Result& result) {
int status = 0; int status = 0;
ConfigureHoldEnv(request); ConfigureHoldEnv(request);
try { try {
ScopedFdRedirect redirect(request.outputDir + "/retrace.log"); ScopedFdRedirect redirect(request.outputDir + "/retrace.log");
status = RunRetraceMain(request, usePresentDump); status = RunRetraceMain(request);
} catch (const MobileGLRetraceExit& retraceExit) { } catch (const MobileGLRetraceExit& retraceExit) {
status = retraceExit.status; status = retraceExit.status;
} catch (const std::exception& exception) { } catch (const std::exception& exception) {
@@ -509,54 +415,29 @@ bool RunRetrace(const Request& request, bool usePresentDump, Result& result) {
return false; return false;
} }
std::string snapshotPath = SnapshotPathForCall(request, usePresentDump); std::string snapshotPath = SnapshotPathForCall(request);
const std::string presentPath = request.outputDir + "/present.ppm"; if (!Exists(snapshotPath)) {
const bool hasSnapshot = Exists(snapshotPath);
const bool hasPresentDump = usePresentDump && Exists(presentPath);
if (!hasSnapshot && !hasPresentDump) {
result.statusCode = STATUS_RETRACE_FAILED; result.statusCode = STATUS_RETRACE_FAILED;
result.message = "retrace completed but did not create expected snapshot: " + snapshotPath; result.message = "retrace completed but did not create expected snapshot: " + snapshotPath;
return false; return false;
} }
if (hasSnapshot && !hasPresentDump) {
RgbaImage snapshot; RgbaImage snapshot;
std::string imageError; std::string imageError;
if (!ReadPngRgba(snapshotPath, snapshot, imageError)) { if (!ReadPngRgba(snapshotPath, snapshot, imageError)) {
result.statusCode = STATUS_IO_ERROR; result.statusCode = STATUS_IO_ERROR;
result.message = imageError.empty() result.message = imageError.empty()
? "failed to decode snapshot PNG: " + snapshotPath ? "failed to decode snapshot PNG: " + snapshotPath
: imageError; : imageError;
return false; return false;
}
ForceOpaqueAlpha(snapshot);
if (!WritePngRgba(result.actualPath, snapshot, imageError)) {
result.statusCode = STATUS_IO_ERROR;
result.message = imageError.empty()
? "failed to write snapshot to actual PNG"
: imageError;
return false;
}
} }
if (usePresentDump) { ForceOpaqueAlpha(snapshot);
if (hasPresentDump) { if (!WritePngRgba(result.actualPath, snapshot, imageError)) {
RgbaImage present; result.statusCode = STATUS_IO_ERROR;
std::string imageError; result.message = imageError.empty()
if (!ReadPpmRgbAsRgba(presentPath, present, imageError) || ? "failed to write snapshot to actual PNG"
!WritePngRgba(request.outputDir + "/present.png", present, imageError)) { : imageError;
result.statusCode = STATUS_IO_ERROR; return false;
result.message = imageError.empty()
? "failed to convert DirectVulkan present dump to PNG"
: imageError;
return false;
}
if (!WritePngRgba(result.actualPath, present, imageError)) {
result.statusCode = STATUS_IO_ERROR;
result.message = imageError.empty()
? "failed to write DirectVulkan present dump to actual PNG"
: imageError;
return false;
}
}
} }
return true; return true;
} }
@@ -927,27 +808,6 @@ Result RunTraceReplay(const Request& request) {
return result; return result;
} }
bool usePresentDump = false;
if (request.backend == "DirectVulkan") {
std::string inspectError;
if (!TargetCallSwapsRenderTarget(request, usePresentDump, inspectError)) {
result.statusCode = STATUS_INVALID_ARGUMENT;
result.message = inspectError;
return result;
}
}
if (usePresentDump) {
std::string presentDumpPath = request.outputDir + "/present.ppm";
std::string presentDumpCall = std::to_string(request.targetCall);
setenv("MOBILEGL_PRESENT_DUMP_PATH", presentDumpPath.c_str(), 1);
setenv("MOBILEGL_PRESENT_DUMP_CALL", presentDumpCall.c_str(), 1);
} else {
unsetenv("MOBILEGL_PRESENT_STATS");
unsetenv("MOBILEGL_PRESENT_DUMP_PATH");
unsetenv("MOBILEGL_PRESENT_DUMP_CALL");
}
setenv("MOBILEGL_LOG_FILE_PATH", mobileGlLogPath.c_str(), 1); setenv("MOBILEGL_LOG_FILE_PATH", mobileGlLogPath.c_str(), 1);
std::string mobileGlError; std::string mobileGlError;
@@ -957,7 +817,7 @@ Result RunTraceReplay(const Request& request) {
return result; return result;
} }
if (!RunRetrace(request, usePresentDump, result)) { if (!RunRetrace(request, result)) {
HoldAfterRetrace(request); HoldAfterRetrace(request);
return result; return result;
} }
+1 -8
View File
@@ -410,15 +410,8 @@ public:
return; return;
} }
char callNo[32]; char callNo[32];
const char *overrideCallNo = getenv("MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE"); snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
if (overrideCallNo != nullptr && overrideCallNo[0] != '\0') {
snprintf(callNo, sizeof(callNo), "%s", overrideCallNo);
} else {
snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
}
setenv("MOBILEGL_PRESENT_CURRENT_CALL", callNo, 1);
gEgl.swapBuffers(gDisplay, surface); gEgl.swapBuffers(gDisplay, surface);
unsetenv("MOBILEGL_PRESENT_CURRENT_CALL");
#if defined(__APPLE__) #if defined(__APPLE__)
PumpMacOSEvents(); PumpMacOSEvents();
if (window && !windowShown) { if (window && !windowShown) {
@@ -219,8 +219,8 @@ python3 "$REPO/3rdparty/apitrace/scripts/retracediff.py" \
For MobileGL target state, inspect MobileGL logs and add temporary focused For MobileGL target state, inspect MobileGL logs and add temporary focused
logging in the relevant state sync path. Keep logs scoped to the bad call range logging in the relevant state sync path. Keep logs scoped to the bad call range
using call-number markers or a `MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE` style using retrace-side call-number markers rather than backend-specific snapshot
guard if available. hooks.
## Classify the bad call ## Classify the bad call