[Feat] (trace-replay): add Minecraft main menu trace

This commit is contained in:
2026-06-16 13:00:01 +08:00
parent 0dc3a0916e
commit b33ae1c481
8 changed files with 86 additions and 1 deletions
@@ -443,6 +443,59 @@ int ChannelValue(const RgbaImage& image, int x, int y, int channel) {
return image.pixels[(static_cast<std::size_t>(y) * image.width + x) * 4 + channel]; return image.pixels[(static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(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<std::size_t>(imageY) * diff.width + imageX) * 4;
if (different) {
dst[0] = 0xff;
dst[1] = static_cast<std::uint8_t>(std::min(255, dg * kDiffScale));
dst[2] = static_cast<std::uint8_t>(std::min(255, db * kDiffScale));
} else {
dst[0] = static_cast<std::uint8_t>(std::min(255, dr * kDiffScale));
dst[1] = static_cast<std::uint8_t>(std::min(255, dg * kDiffScale));
dst[2] = static_cast<std::uint8_t>(std::min(255, db * kDiffScale));
}
}
}
return WritePngRgba(result.diffPath, diff, error);
}
bool CompareWithGolden(const Request& request, Result& result) { bool CompareWithGolden(const Request& request, Result& result) {
if (request.goldenPath.empty()) { if (request.goldenPath.empty()) {
result.passed = true; 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.mismatchPixels = mismatch;
result.passed = mismatch <= request.tolerance; result.passed = mismatch <= request.tolerance;
result.statusCode = result.passed ? STATUS_OK : STATUS_COMPARE_FAILED; 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 << " \"tracePath\": \"" << JsonEscape(request.tracePath) << "\",\n";
file << " \"goldenPath\": \"" << JsonEscape(request.goldenPath) << "\",\n"; file << " \"goldenPath\": \"" << JsonEscape(request.goldenPath) << "\",\n";
file << " \"actualPath\": \"" << JsonEscape(result.actualPath) << "\",\n"; file << " \"actualPath\": \"" << JsonEscape(result.actualPath) << "\",\n";
file << " \"diffPath\": \"" << JsonEscape(result.diffPath) << "\",\n";
file << " \"backend\": \"" << JsonEscape(request.backend) << "\",\n"; file << " \"backend\": \"" << JsonEscape(request.backend) << "\",\n";
file << " \"targetFrame\": " << request.targetFrame << ",\n"; file << " \"targetFrame\": " << request.targetFrame << ",\n";
file << " \"targetCall\": " << request.targetCall << ",\n"; file << " \"targetCall\": " << request.targetCall << ",\n";
@@ -560,6 +621,7 @@ Result RunTraceReplay(const Request& request) {
Result result; Result result;
result.resultPath = request.outputDir + "/result.json"; result.resultPath = request.outputDir + "/result.json";
result.actualPath = request.outputDir + "/actual.png"; result.actualPath = request.outputDir + "/actual.png";
result.diffPath = request.diffPath;
const std::string mobileGlLogPath = request.outputDir + "/mobilegl.log"; const std::string mobileGlLogPath = request.outputDir + "/mobilegl.log";
if (!EnsureDirectory(request.outputDir)) { if (!EnsureDirectory(request.outputDir)) {
@@ -18,6 +18,7 @@ struct Request {
std::string tracePath; std::string tracePath;
std::string goldenPath; std::string goldenPath;
std::string outputDir; std::string outputDir;
std::string diffPath;
std::string backend; std::string backend;
std::string mobileGlLibrary = "libMobileGL.so"; std::string mobileGlLibrary = "libMobileGL.so";
int targetFrame = -1; int targetFrame = -1;
@@ -38,6 +39,7 @@ struct Result {
std::string message; std::string message;
std::string resultPath; std::string resultPath;
std::string actualPath; std::string actualPath;
std::string diffPath;
long long mismatchPixels = -1; long long mismatchPixels = -1;
}; };
+10
View File
@@ -365,3 +365,13 @@ add_trace_replay_test_for_backends(minecraft-1.21.4-startup
HEIGHT 480 HEIGHT 480
TOLERANCE 20 TOLERANCE 20
FUZZ_PERCENT 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)
+6
View File
@@ -9,6 +9,8 @@ The bundled fixtures cover:
traces rejected because they rely on compatibility/fixed-function OpenGL. 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 - `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. 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: 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 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`. `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 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. path to `VK_EXT_headless_surface` for `DirectVulkan`, so CI does not need an X11 or Wayland window.
Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

+1
View File
@@ -50,6 +50,7 @@ execute_process(
COMMAND "${TRACE_REPLAY_EXE}" COMMAND "${TRACE_REPLAY_EXE}"
--trace "${trace_path}" --trace "${trace_path}"
--golden "${TRACE_GOLDEN}" --golden "${TRACE_GOLDEN}"
--diff "${TRACE_OUTPUT_DIR}/output/${TRACE_CASE_NAME}-diff.png"
--output "${TRACE_OUTPUT_DIR}/output" --output "${TRACE_OUTPUT_DIR}/output"
--backend "${TRACE_BACKEND}" --backend "${TRACE_BACKEND}"
--mobilegl-library "${MOBILEGL_LIBRARY}" --mobilegl-library "${MOBILEGL_LIBRARY}"
+5 -1
View File
@@ -14,6 +14,7 @@ void PrintUsage(const char *argv0) {
<< "\n" << "\n"
<< "Options:\n" << "Options:\n"
<< " --golden PATH Golden PNG to compare against\n" << " --golden PATH Golden PNG to compare against\n"
<< " --diff PATH Difference PNG output path\n"
<< " --backend NAME DirectGLES or DirectVulkan (default: DirectGLES)\n" << " --backend NAME DirectGLES or DirectVulkan (default: DirectGLES)\n"
<< " --mobilegl-library PATH libMobileGL.so path (default: libMobileGL.so)\n" << " --mobilegl-library PATH libMobileGL.so path (default: libMobileGL.so)\n"
<< " --width N Replay surface width override\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; if (!ReadValue(argc, argv, i, request.tracePath)) return false;
} else if (arg == "--golden") { } else if (arg == "--golden") {
if (!ReadValue(argc, argv, i, request.goldenPath)) return false; 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") { } else if (arg == "--output") {
if (!ReadValue(argc, argv, i, request.outputDir)) return false; if (!ReadValue(argc, argv, i, request.outputDir)) return false;
} else if (arg == "--backend") { } else if (arg == "--backend") {
@@ -131,6 +134,7 @@ int main(int argc, char **argv) {
std::cout << result.message << "\n" std::cout << result.message << "\n"
<< "result: " << result.resultPath << "\n" << "result: " << result.resultPath << "\n"
<< "actual: " << result.actualPath << "\n"; << "actual: " << result.actualPath << "\n"
<< "diff: " << result.diffPath << "\n";
return result.passed ? 0 : result.statusCode; return result.passed ? 0 : result.statusCode;
} }