diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp index a65460e4..ced1528f 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp @@ -443,6 +443,59 @@ int ChannelValue(const RgbaImage& image, int x, int y, int channel) { return image.pixels[(static_cast(y) * image.width + x) * 4 + channel]; } +bool WriteDifferenceImage(const Result& result, + const RgbaImage& actual, + const RgbaImage& golden, + int x0, + int y0, + int compareWidth, + int compareHeight, + int fuzz, + std::string& error) { + if (result.diffPath.empty()) { + return true; + } + + constexpr int kDiffScale = 8; + RgbaImage diff; + diff.width = actual.width; + diff.height = actual.height; + diff.pixels.assign(static_cast(diff.width) * diff.height * 4, 0); + for (int y = 0; y < diff.height; ++y) { + for (int x = 0; x < diff.width; ++x) { + std::uint8_t* dst = diff.pixels.data() + (static_cast(y) * diff.width + x) * 4; + dst[3] = 0xff; + } + } + + for (int y = 0; y < compareHeight; ++y) { + for (int x = 0; x < compareWidth; ++x) { + int imageX = x0 + x; + int imageY = y0 + y; + int dr = std::abs(ChannelValue(actual, imageX, imageY, 0) - + ChannelValue(golden, imageX, imageY, 0)); + int dg = std::abs(ChannelValue(actual, imageX, imageY, 1) - + ChannelValue(golden, imageX, imageY, 1)); + int db = std::abs(ChannelValue(actual, imageX, imageY, 2) - + ChannelValue(golden, imageX, imageY, 2)); + bool different = dr > fuzz || dg > fuzz || db > fuzz; + std::uint8_t* dst = diff.pixels.data() + + (static_cast(imageY) * diff.width + imageX) * 4; + if (different) { + dst[0] = 0xff; + dst[1] = static_cast(std::min(255, dg * kDiffScale)); + dst[2] = static_cast(std::min(255, db * kDiffScale)); + } else { + dst[0] = static_cast(std::min(255, dr * kDiffScale)); + dst[1] = static_cast(std::min(255, dg * kDiffScale)); + dst[2] = static_cast(std::min(255, db * kDiffScale)); + } + } + } + + return WritePngRgba(result.diffPath, diff, error); +} + bool CompareWithGolden(const Request& request, Result& result) { if (request.goldenPath.empty()) { result.passed = true; @@ -511,6 +564,13 @@ bool CompareWithGolden(const Request& request, Result& result) { } } + std::string diffError; + if (!WriteDifferenceImage(result, actual, golden, x0, y0, compareWidth, compareHeight, fuzz, diffError)) { + result.statusCode = STATUS_IO_ERROR; + result.message = diffError.empty() ? "failed to write diff PNG" : diffError; + return false; + } + result.mismatchPixels = mismatch; result.passed = mismatch <= request.tolerance; result.statusCode = result.passed ? STATUS_OK : STATUS_COMPARE_FAILED; @@ -540,6 +600,7 @@ bool WriteResultJson(const Request& request, const Result& result) { file << " \"tracePath\": \"" << JsonEscape(request.tracePath) << "\",\n"; file << " \"goldenPath\": \"" << JsonEscape(request.goldenPath) << "\",\n"; file << " \"actualPath\": \"" << JsonEscape(result.actualPath) << "\",\n"; + file << " \"diffPath\": \"" << JsonEscape(result.diffPath) << "\",\n"; file << " \"backend\": \"" << JsonEscape(request.backend) << "\",\n"; file << " \"targetFrame\": " << request.targetFrame << ",\n"; file << " \"targetCall\": " << request.targetCall << ",\n"; @@ -560,6 +621,7 @@ Result RunTraceReplay(const Request& request) { Result result; result.resultPath = request.outputDir + "/result.json"; result.actualPath = request.outputDir + "/actual.png"; + result.diffPath = request.diffPath; const std::string mobileGlLogPath = request.outputDir + "/mobilegl.log"; if (!EnsureDirectory(request.outputDir)) { diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp index 2cde9dd4..de191bb6 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp @@ -18,6 +18,7 @@ struct Request { std::string tracePath; std::string goldenPath; std::string outputDir; + std::string diffPath; std::string backend; std::string mobileGlLibrary = "libMobileGL.so"; int targetFrame = -1; @@ -38,6 +39,7 @@ struct Result { std::string message; std::string resultPath; std::string actualPath; + std::string diffPath; long long mismatchPixels = -1; }; diff --git a/tools/trace_replay/CMakeLists.txt b/tools/trace_replay/CMakeLists.txt index 9e75eba4..746dbf43 100644 --- a/tools/trace_replay/CMakeLists.txt +++ b/tools/trace_replay/CMakeLists.txt @@ -365,3 +365,13 @@ add_trace_replay_test_for_backends(minecraft-1.21.4-startup HEIGHT 480 TOLERANCE 20 FUZZ_PERCENT 20) + +add_trace_replay_test_for_backends(minecraft-1.21.4-main-menu + TRACE_ARCHIVE ${MOBILEGL_TRACE_ROOT}/fixtures/minecraft-1.21.4-main-menu.tgz + TRACE_FILE trace.trace + GOLDEN ${MOBILEGL_TRACE_ROOT}/fixtures/minecraft-1.21.4-main-menu.0000481787.png + TARGET_CALL 481787 + WIDTH 854 + HEIGHT 480 + TOLERANCE 500 + FUZZ_PERCENT 20) diff --git a/tools/trace_replay/README.md b/tools/trace_replay/README.md index 6432f518..31edd057 100644 --- a/tools/trace_replay/README.md +++ b/tools/trace_replay/README.md @@ -9,6 +9,8 @@ The bundled fixtures cover: traces rejected because they rely on compatibility/fixed-function OpenGL. - `minecraft-1.21.4-startup`: captured from Minecraft 1.21.4's Mojang Studios startup screen. The fixture is trimmed to call 92195, immediately after the startup screen draw into Minecraft's offscreen framebuffer. +- `minecraft-1.21.4-main-menu`: captured from Minecraft 1.21.4's English (US) main menu. The fixture is trimmed to + call 481787, immediately after the main menu draw into Minecraft's offscreen framebuffer. Build from the MobileGL repository root: @@ -48,6 +50,10 @@ build-test/tools/trace_replay/mobilegl_trace_replay \ The Minecraft startup fixture uses `tools/trace_replay/fixtures/minecraft-1.21.4-startup.tgz`, golden image `minecraft-1.21.4-startup.0000092195.png`, `--target-call 92195`, `--width 854`, and `--height 480`. +The Minecraft main-menu fixture uses `tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.tgz`, golden image +`minecraft-1.21.4-main-menu.0000481787.png`, `--target-call 481787`, `--width 854`, `--height 480`, and +`--tolerance 500`. +Each replay that has a golden image writes `${case}-diff.png` in the backend output directory. The Linux runner loads MobileGL by path with `dlopen`. It uses an EGL pbuffer for `DirectGLES` and maps the pbuffer path to `VK_EXT_headless_surface` for `DirectVulkan`, so CI does not need an X11 or Wayland window. diff --git a/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.0000481787.png b/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.0000481787.png new file mode 100644 index 00000000..9b3c60bd Binary files /dev/null and b/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.0000481787.png differ diff --git a/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.tgz b/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.tgz new file mode 100644 index 00000000..6110a8b8 Binary files /dev/null and b/tools/trace_replay/fixtures/minecraft-1.21.4-main-menu.tgz differ diff --git a/tools/trace_replay/run_trace_case.cmake b/tools/trace_replay/run_trace_case.cmake index 80261fd7..546a55e6 100644 --- a/tools/trace_replay/run_trace_case.cmake +++ b/tools/trace_replay/run_trace_case.cmake @@ -50,6 +50,7 @@ execute_process( COMMAND "${TRACE_REPLAY_EXE}" --trace "${trace_path}" --golden "${TRACE_GOLDEN}" + --diff "${TRACE_OUTPUT_DIR}/output/${TRACE_CASE_NAME}-diff.png" --output "${TRACE_OUTPUT_DIR}/output" --backend "${TRACE_BACKEND}" --mobilegl-library "${MOBILEGL_LIBRARY}" diff --git a/tools/trace_replay/trace_replay_cli.cpp b/tools/trace_replay/trace_replay_cli.cpp index 661a3b40..af95fb72 100644 --- a/tools/trace_replay/trace_replay_cli.cpp +++ b/tools/trace_replay/trace_replay_cli.cpp @@ -14,6 +14,7 @@ void PrintUsage(const char *argv0) { << "\n" << "Options:\n" << " --golden PATH Golden PNG to compare against\n" + << " --diff PATH Difference PNG output path\n" << " --backend NAME DirectGLES or DirectVulkan (default: DirectGLES)\n" << " --mobilegl-library PATH libMobileGL.so path (default: libMobileGL.so)\n" << " --width N Replay surface width override\n" @@ -63,6 +64,8 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) { if (!ReadValue(argc, argv, i, request.tracePath)) return false; } else if (arg == "--golden") { if (!ReadValue(argc, argv, i, request.goldenPath)) return false; + } else if (arg == "--diff") { + if (!ReadValue(argc, argv, i, request.diffPath)) return false; } else if (arg == "--output") { if (!ReadValue(argc, argv, i, request.outputDir)) return false; } else if (arg == "--backend") { @@ -131,6 +134,7 @@ int main(int argc, char **argv) { std::cout << result.message << "\n" << "result: " << result.resultPath << "\n" - << "actual: " << result.actualPath << "\n"; + << "actual: " << result.actualPath << "\n" + << "diff: " << result.diffPath << "\n"; return result.passed ? 0 : result.statusCode; }