diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index 59cb7ec4..a254ca9e 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -256,9 +256,9 @@ jobs: width: 854 height: 480 crop_x: 0 - crop_y: 0 - crop_width: 0 - crop_height: 0 + crop_y: 10 + crop_width: 140 + crop_height: 158 timeout_seconds: 1800 - name: minecraft-1.21.4-fabric-xaero-world-map-in-world trace_archive: tools/trace_replay/fixtures/minecraft-1.21.4-fabric-xaero-world-map-in-world.tgz diff --git a/android-plugin/trace-replay-ci.sh b/android-plugin/trace-replay-ci.sh index 66a7d656..c2af661f 100644 --- a/android-plugin/trace-replay-ci.sh +++ b/android-plugin/trace-replay-ci.sh @@ -42,6 +42,18 @@ die() { exit 2 } +host_path_for_adb() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$1" + else + printf '%s' "$1" + fi +} + +adb_device_path() { + MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "${ADB}" "$@" +} + next_arg() { if [ "$#" -lt 2 ]; then die "$1 requires a value" @@ -146,17 +158,17 @@ collect_run_diagnostics() { diagnostics_dir="$1" mkdir -p "${diagnostics_dir}" "${ADB}" logcat -d -t 2000 > "${diagnostics_dir}/logcat.txt" || true - "${ADB}" shell pidof "${package_name}" > "${diagnostics_dir}/pidof.txt" 2>&1 || true - "${ADB}" shell dumpsys activity activities > "${diagnostics_dir}/activity.txt" 2>&1 || true - "${ADB}" shell run-as "${package_name}" ls -laR "${app_dir}" > "${diagnostics_dir}/app-files.txt" 2>&1 || true - "${ADB}" exec-out run-as "${package_name}" cat "${app_dir}/output/retrace.log" > "${diagnostics_dir}/retrace.log" || true - "${ADB}" exec-out run-as "${package_name}" cat "${app_dir}/output/mobilegl.log" > "${diagnostics_dir}/mobilegl.log" || true + adb_device_path shell pidof "${package_name}" > "${diagnostics_dir}/pidof.txt" 2>&1 || true + adb_device_path shell dumpsys activity activities > "${diagnostics_dir}/activity.txt" 2>&1 || true + adb_device_path shell run-as "${package_name}" ls -laR "${app_dir}" > "${diagnostics_dir}/app-files.txt" 2>&1 || true + adb_device_path exec-out run-as "${package_name}" cat "${app_dir}/output/retrace.log" > "${diagnostics_dir}/retrace.log" || true + adb_device_path exec-out run-as "${package_name}" cat "${app_dir}/output/mobilegl.log" > "${diagnostics_dir}/mobilegl.log" || true } copy_app_artifact() { source_path="$1" destination_path="$2" - if ! "${ADB}" exec-out run-as "${package_name}" cat "${source_path}" > "${destination_path}"; then + if ! adb_device_path exec-out run-as "${package_name}" cat "${source_path}" > "${destination_path}"; then echo "trace-replay-ci.sh: warning: failed to copy ${source_path}" >&2 rm -f "${destination_path}" fi @@ -171,14 +183,14 @@ prepare_fixture() { extracted_trace="${fixture_dir}/${trace_file}" test -f "${extracted_trace}" || die "trace file not found in archive: ${trace_file}" - "${ADB}" push "${extracted_trace}" "/data/local/tmp/mobilegl-${safe_case}.trace" - "${ADB}" push "${golden_path}" "/data/local/tmp/mobilegl-${safe_case}.golden.png" + adb_device_path push "$(host_path_for_adb "${extracted_trace}")" "/data/local/tmp/mobilegl-${safe_case}.trace" + adb_device_path push "$(host_path_for_adb "${golden_path}")" "/data/local/tmp/mobilegl-${safe_case}.golden.png" if [ -n "${alternate_golden_path}" ]; then - "${ADB}" push "${alternate_golden_path}" "/data/local/tmp/mobilegl-${safe_case}.alternate-golden.png" + adb_device_path push "$(host_path_for_adb "${alternate_golden_path}")" "/data/local/tmp/mobilegl-${safe_case}.alternate-golden.png" fi - "${ADB}" shell chmod 0644 "/data/local/tmp/mobilegl-${safe_case}.trace" "/data/local/tmp/mobilegl-${safe_case}.golden.png" + adb_device_path shell chmod 0644 "/data/local/tmp/mobilegl-${safe_case}.trace" "/data/local/tmp/mobilegl-${safe_case}.golden.png" if [ -n "${alternate_golden_path}" ]; then - "${ADB}" shell chmod 0644 "/data/local/tmp/mobilegl-${safe_case}.alternate-golden.png" + adb_device_path shell chmod 0644 "/data/local/tmp/mobilegl-${safe_case}.alternate-golden.png" fi } @@ -187,12 +199,12 @@ copy_fixture_to_app() { golden_tmp="/data/local/tmp/mobilegl-${safe_case}.golden.png" alternate_golden_tmp="/data/local/tmp/mobilegl-${safe_case}.alternate-golden.png" - "${ADB}" shell run-as "${package_name}" rm -rf "${app_dir}" - "${ADB}" shell run-as "${package_name}" mkdir -p "${app_dir}/input" "${app_dir}/output" - "${ADB}" shell run-as "${package_name}" cp "${trace_tmp}" "${app_dir}/input/trace.trace" - "${ADB}" shell run-as "${package_name}" cp "${golden_tmp}" "${app_dir}/input/golden.png" + adb_device_path shell run-as "${package_name}" rm -rf "${app_dir}" + adb_device_path shell run-as "${package_name}" mkdir -p "${app_dir}/input" "${app_dir}/output" + adb_device_path shell run-as "${package_name}" cp "${trace_tmp}" "${app_dir}/input/trace.trace" + adb_device_path shell run-as "${package_name}" cp "${golden_tmp}" "${app_dir}/input/golden.png" if [ -n "${alternate_golden_path}" ]; then - "${ADB}" shell run-as "${package_name}" cp "${alternate_golden_tmp}" "${app_dir}/input/alternate-golden.png" + adb_device_path shell run-as "${package_name}" cp "${alternate_golden_tmp}" "${app_dir}/input/alternate-golden.png" fi } @@ -211,9 +223,9 @@ run_retrace() { fi mkdir -p "${result_dir}" - "${ADB}" install -r "${apk_file}" + "${ADB}" install -r "$(host_path_for_adb "${apk_file}")" copy_fixture_to_app - "${ADB}" shell am force-stop "${package_name}" + adb_device_path shell am force-stop "${package_name}" "${ADB}" logcat -c set -- am start -W -a top.mobilegl.plugin.TRACE_REPLAY \ -n "${package_name}/top.mobilegl.plugin.trace.TraceReplayActivity" \ @@ -240,14 +252,14 @@ run_retrace() { --ei crop_y "${crop_y}" \ --ei crop_width "${crop_width}" \ --ei crop_height "${crop_height}" - "${ADB}" shell "$@" + adb_device_path shell "$@" app_exited=0 for _ in $(seq 1 "${timeout_seconds}"); do - if "${ADB}" shell run-as "${package_name}" ls "${app_dir}/output/result.json" >/dev/null 2>&1; then + if adb_device_path shell run-as "${package_name}" ls "${app_dir}/output/result.json" >/dev/null 2>&1; then break fi - if ! "${ADB}" shell pidof "${package_name}" >/dev/null 2>&1; then + if ! adb_device_path shell pidof "${package_name}" >/dev/null 2>&1; then app_exited=1 break fi @@ -255,7 +267,7 @@ run_retrace() { done collect_run_diagnostics "${result_dir}" - if ! "${ADB}" shell run-as "${package_name}" ls "${app_dir}/output/result.json" >/dev/null 2>&1; then + if ! adb_device_path shell run-as "${package_name}" ls "${app_dir}/output/result.json" >/dev/null 2>&1; then if [ "${app_exited}" -eq 1 ]; then echo "trace-replay-ci.sh: app process exited before result.json was created" >&2 else @@ -275,7 +287,7 @@ run_retrace() { cat "${result_dir}/app-files.txt" >&2 || true exit 1 fi - "${ADB}" exec-out run-as "${package_name}" cat "${app_dir}/output/result.json" > "${result_dir}/result.json" + adb_device_path exec-out run-as "${package_name}" cat "${app_dir}/output/result.json" > "${result_dir}/result.json" cat "${result_dir}/result.json" copy_app_artifact "${app_dir}/output/actual.png" "${result_dir}/${safe_case}-${backend}-actual.png" copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png" diff --git a/tools/trace_replay/CMakeLists.txt b/tools/trace_replay/CMakeLists.txt index a05c0d3e..20a4bd3b 100644 --- a/tools/trace_replay/CMakeLists.txt +++ b/tools/trace_replay/CMakeLists.txt @@ -444,6 +444,10 @@ add_trace_replay_test_for_backends(minecraft-1.21.4-fabric-xaero-minimap-in-worl TARGET_CALL 2553500 WIDTH 854 HEIGHT 480 + CROP_X 0 + CROP_Y 10 + CROP_WIDTH 140 + CROP_HEIGHT 158 ) add_trace_replay_test_for_backends(minecraft-1.21.4-fabric-xaero-world-map-in-world diff --git a/tools/trace_replay/render_retrace_summary.mjs b/tools/trace_replay/render_retrace_summary.mjs index e1b7f420..d3c36cd5 100644 --- a/tools/trace_replay/render_retrace_summary.mjs +++ b/tools/trace_replay/render_retrace_summary.mjs @@ -13,6 +13,18 @@ const CASES = [ "minecraft-1.21.4-main-menu", "minecraft-1.21.4-in-world", "minecraft-1.21.4-fabric-sodium-in-world", + "minecraft-1.21.4-fabric-common-mods-in-world", + "minecraft-1.21.4-fabric-common-mods-inventory", + "minecraft-1.21.4-fabric-rei-inventory", + "minecraft-1.21.4-fabric-xaero-minimap-in-world", + "minecraft-1.21.4-fabric-xaero-world-map-in-world", + "minecraft-1.21.4-fabric-journeymap-in-world", + "minecraft-1.21.4-fabric-modernui-inventory", + "minecraft-1.21.4-fabric-rei-inventory-normal-world", + "minecraft-1.21.4-fabric-xaero-minimap-in-world-normal-world", + "minecraft-1.21.4-fabric-xaero-world-map-in-world-normal-world", + "minecraft-1.21.4-fabric-journeymap-in-world-normal-world", + "minecraft-1.21.4-fabric-modernui-inventory-normal-world", "minecraft-1.21.4-fabric-iris-bsl-in-world", "minecraft-1.21.4-fabric-iris-makeup-ultrafast-in-world", "minecraft-1.21.4-fabric-iris-super-duper-vanilla-in-world", diff --git a/tools/trace_replay/run_android_retrace_local.py b/tools/trace_replay/run_android_retrace_local.py new file mode 100644 index 00000000..8a281a69 --- /dev/null +++ b/tools/trace_replay/run_android_retrace_local.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +import argparse +import json +import shutil +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "tools" / "trace_replay" / "fixtures" +RESULT_ROOT = ROOT / ".trace-work" / "android-retrace-result" +FIXTURE_ROOT = ROOT / ".trace-work" / "android-retrace-fixture" +SUMMARY_DIR = ROOT / ".trace-work" / "android-retrace-summary" +SUMMARY_HTML = "mobilegl-android-retrace-overview.html" + +BACKENDS = { + "DirectGLES": { + "apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk", + "package": "top.mobilegl.plugin.espryt.trace", + "use_angle": False, + "use_pbuffer": False, + }, + "DirectVulkan": { + "apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "magmaTrace" / "debug" / "MobileGL-MagmaTrace-debug.apk", + "package": "top.mobilegl.plugin.magma.trace", + "use_angle": False, + "use_pbuffer": False, + }, +} + +CASES = [ + { + "name": "OpenRA", + "trace_archive": "openra.tgz", + "trace_file": "openra.trace", + "golden": "openra.0000031249.png", + "target_call": 31249, + "width": 640, + "height": 480, + "crop_x": 1, + "crop_y": 1, + "crop_width": 638, + "crop_height": 478, + "timeout_seconds": 180, + }, + {"name": "minecraft-1.21.4-startup", "golden": "minecraft-1.21.4-startup.0000092195.png", "target_call": 92195, "timeout_seconds": 180}, + {"name": "minecraft-1.21.4-main-menu", "golden": "minecraft-1.21.4-main-menu.0000481787.png", "alternate_golden": "minecraft-1.21.4-main-menu.0000481787-mali.png", "target_call": 481787, "timeout_seconds": 180}, + {"name": "minecraft-1.21.4-in-world", "golden": "minecraft-1.21.4-in-world.0000280000.png", "target_call": 280000, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-sodium-in-world", "golden": "minecraft-1.21.4-fabric-sodium-in-world.0000923340.png", "target_call": 923340, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-common-mods-in-world", "golden": "minecraft-1.21.4-fabric-common-mods-in-world.0000522084.png", "target_call": 522084, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-common-mods-inventory", "golden": "minecraft-1.21.4-fabric-common-mods-inventory.0000728558.png", "target_call": 728558, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-rei-inventory", "golden": "minecraft-1.21.4-fabric-rei-inventory.0005431826.png", "target_call": 5431826, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-xaero-minimap-in-world", "golden": "minecraft-1.21.4-fabric-xaero-minimap-in-world.0002553500.png", "target_call": 2553500, "crop_y": 10, "crop_width": 140, "crop_height": 158, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-xaero-world-map-in-world", "golden": "minecraft-1.21.4-fabric-xaero-world-map-in-world.0001598209.png", "target_call": 1598209, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-journeymap-in-world", "golden": "minecraft-1.21.4-fabric-journeymap-in-world.0002632392.png", "target_call": 2632392, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-modernui-inventory", "golden": "minecraft-1.21.4-fabric-modernui-inventory.0004907381.png", "target_call": 4907381, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-rei-inventory-normal-world", "golden": "minecraft-1.21.4-fabric-rei-inventory-normal-world.0006599207.png", "target_call": 6599207, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-xaero-minimap-in-world-normal-world", "golden": "minecraft-1.21.4-fabric-xaero-minimap-in-world-normal-world.0003893975.png", "target_call": 3893975, "crop_y": 10, "crop_width": 140, "crop_height": 158, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-xaero-world-map-in-world-normal-world", "golden": "minecraft-1.21.4-fabric-xaero-world-map-in-world-normal-world.0002099794.png", "target_call": 2099794, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-journeymap-in-world-normal-world", "golden": "minecraft-1.21.4-fabric-journeymap-in-world-normal-world.0005099482.png", "target_call": 5099482, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-modernui-inventory-normal-world", "golden": "minecraft-1.21.4-fabric-modernui-inventory-normal-world.0008585354.png", "target_call": 8585354, "timeout_seconds": 1800}, + {"name": "minecraft-1.21.4-fabric-iris-bsl-in-world", "golden": "minecraft-1.21.4-fabric-iris-bsl-in-world.0000110725.png", "alternate_golden": "minecraft-1.21.4-fabric-iris-bsl-in-world.0000110725-mali.png", "target_call": 110725, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-makeup-ultrafast-in-world", "golden": "minecraft-1.21.4-fabric-iris-makeup-ultrafast-in-world.0000095322.png", "target_call": 95322, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-super-duper-vanilla-in-world", "golden": "minecraft-1.21.4-fabric-iris-super-duper-vanilla-in-world.0000141559.png", "target_call": 141559, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-sundial-lite-in-world", "golden": "minecraft-1.21.4-fabric-iris-sundial-lite-in-world.0000150023.png", "target_call": 150023, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-complementary-reimagined-in-world", "golden": "minecraft-1.21.4-fabric-iris-complementary-reimagined-in-world.0000151297.png", "target_call": 151297, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-complementary-unbound-in-world", "golden": "minecraft-1.21.4-fabric-iris-complementary-unbound-in-world.0000146559.png", "target_call": 146559, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-mellow-in-world", "golden": "minecraft-1.21.4-fabric-iris-mellow-in-world.0000096143.png", "target_call": 96143, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-nostalgia-in-world", "golden": "minecraft-1.21.4-fabric-iris-nostalgia-in-world.0000153808-linux-mesa.png", "alternate_golden": "minecraft-1.21.4-fabric-iris-nostalgia-in-world.0000153808.png", "target_call": 153808, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-bliss-in-world", "golden": "minecraft-1.21.4-fabric-iris-bliss-in-world.0000113511.png", "target_call": 113511, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-chocapic-v6-lite-in-world", "golden": "minecraft-1.21.4-fabric-iris-chocapic-v6-lite-in-world.0000125124-linux-mesa.png", "target_call": 125124, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-iterationt-in-world", "golden": "minecraft-1.21.4-fabric-iris-iterationt-in-world.0000110538.png", "target_call": 110538, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world", "golden": "minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world.0000115019.png", "target_call": 115019, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-photon-v1.1-in-world", "golden": "minecraft-1.21.4-fabric-iris-photon-v1.1-in-world.0000159866.png", "target_call": 159866, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-photon-v1.3b-in-world", "golden": "minecraft-1.21.4-fabric-iris-photon-v1.3b-in-world.0000172128.png", "target_call": 172128, "timeout_seconds": 900}, + {"name": "minecraft-1.21.4-fabric-iris-derivative-main-d24.4.14-in-world", "golden": "minecraft-1.21.4-fabric-iris-derivative-main-d24.4.14-in-world.0000145353.png", "target_call": 145353, "timeout_seconds": 900}, +] + + +def safe_case(name): + return "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name) + + +def is_lfs_pointer(path): + return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1") + + +def bash_path(path): + path = Path(path).resolve() + drive = path.drive.rstrip(":").lower() + parts = path.parts[1:] + return "/" + drive + "/" + "/".join(parts) + + +def case_with_defaults(case): + merged = { + "trace_archive": f"{case['name']}.tgz", + "trace_file": "trace.trace", + "width": 854, + "height": 480, + "ssim_threshold": "0.99", + "crop_x": 0, + "crop_y": 0, + "crop_width": 0, + "crop_height": 0, + "timeout_seconds": 900, + } + merged.update(case) + return merged + + +def mark_skipped(case, backend, reason): + result_dir = RESULT_ROOT / f"{safe_case(case['name'])}-{backend}" + result_dir.mkdir(parents=True, exist_ok=True) + result = { + "passed": False, + "statusCode": 2, + "message": reason, + "tracePath": str(FIXTURES / case["trace_archive"]), + "goldenPath": str(FIXTURES / case["golden"]), + "alternateGoldenPaths": [], + "matchedGoldenPath": "", + "actualPath": "", + "diffPath": "", + "backend": backend, + "targetCall": case["target_call"], + "width": case["width"], + "height": case["height"], + "cropX": case["crop_x"], + "cropY": case["crop_y"], + "cropWidth": case["crop_width"], + "cropHeight": case["crop_height"], + "ssim": -1, + "ssimThreshold": float(case["ssim_threshold"]), + "mismatchPixels": -1, + } + (result_dir / "result.json").write_text(json.dumps(result, indent=2), encoding="utf-8") + + +def copy_goldens(case, backend): + result_dir = RESULT_ROOT / f"{safe_case(case['name'])}-{backend}" + result_dir.mkdir(parents=True, exist_ok=True) + for key, suffix in (("golden", "golden"), ("alternate_golden", "alternate-golden")): + value = case.get(key) + if not value: + continue + source = FIXTURES / value + if source.exists() and source.stat().st_size > 0: + shutil.copyfile(source, result_dir / f"{safe_case(case['name'])}-{backend}-{suffix}.png") + + +def render_summary(): + SUMMARY_DIR.mkdir(parents=True, exist_ok=True) + command = [ + "node", + str(ROOT / "tools" / "trace_replay" / "render_retrace_summary.mjs"), + "--input", + str(RESULT_ROOT), + "--output-dir", + str(SUMMARY_DIR), + "--title", + "MobileGL Android retrace overview", + "--group-label", + "Android Device", + "--html", + SUMMARY_HTML, + ] + subprocess.run(command, cwd=ROOT, check=True) + shutil.copyfile(SUMMARY_DIR / SUMMARY_HTML, SUMMARY_DIR / "index.html") + + +def run_case(case, backend): + backend_info = BACKENDS[backend] + trace_archive = FIXTURES / case["trace_archive"] + golden = FIXTURES / case["golden"] + alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None + if not trace_archive.exists() or is_lfs_pointer(trace_archive): + mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer") + copy_goldens(case, backend) + return 2 + if not golden.exists() or is_lfs_pointer(golden): + mark_skipped(case, backend, "SKIPPED_LFS_POINTER: golden image is missing or still an LFS pointer") + return 2 + if alternate is not None and (not alternate.exists() or is_lfs_pointer(alternate)): + alternate = None + + command = [ + "C:/Program Files/Git/bin/bash.exe", + "android-plugin/trace-replay-ci.sh", + "--apk-file", + bash_path(backend_info["apk"]), + "--package", + backend_info["package"], + "--backend", + backend, + "--result-root", + bash_path(RESULT_ROOT), + "--fixture-root", + bash_path(FIXTURE_ROOT), + "--case", + case["name"], + "--trace-archive", + bash_path(trace_archive), + "--trace-file", + case["trace_file"], + "--golden", + bash_path(golden), + "--target-call", + str(case["target_call"]), + "--width", + str(case["width"]), + "--height", + str(case["height"]), + "--ssim-threshold", + str(case["ssim_threshold"]), + "--crop-x", + str(case["crop_x"]), + "--crop-y", + str(case["crop_y"]), + "--crop-width", + str(case["crop_width"]), + "--crop-height", + str(case["crop_height"]), + "--timeout-seconds", + str(case["timeout_seconds"]), + ] + if alternate is not None: + command[command.index("--target-call"):command.index("--target-call")] = ["--alternate-golden", bash_path(alternate)] + if backend_info["use_pbuffer"]: + command.append("--use-pbuffer") + env = dict(**__import__("os").environ) + env["PYTHON"] = "python" + env["MSYS2_ARG_CONV_EXCL"] = "/data/*" + if backend_info["use_angle"]: + env["MOBILEGL_RETRACE_USE_ANGLE"] = "1" + else: + env.pop("MOBILEGL_RETRACE_USE_ANGLE", None) + result = subprocess.run(command, cwd=ROOT, env=env) + copy_goldens(case, backend) + return result.returncode + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--case", action="append", dest="cases", help="Case name to run; may be repeated.") + parser.add_argument("--backend", action="append", choices=sorted(BACKENDS), help="Backend to run; may be repeated.") + parser.add_argument("--all", action="store_true", help="Run every case in the APK workflow matrix.") + parser.add_argument("--keep-results", action="store_true", help="Do not clear the previous result root.") + return parser.parse_args() + + +def main(): + args = parse_args() + selected_backends = args.backend or list(BACKENDS) + selected_names = set(args.cases or []) + selected_cases = [case_with_defaults(case) for case in CASES if args.all or case["name"] in selected_names] + if not selected_cases: + print("No cases selected. Use --all or --case NAME.", file=sys.stderr) + return 2 + if not args.keep_results and RESULT_ROOT.exists(): + shutil.rmtree(RESULT_ROOT) + RESULT_ROOT.mkdir(parents=True, exist_ok=True) + failures = 0 + for case in selected_cases: + for backend in selected_backends: + print(f"=== Android retrace: {case['name']} / {backend} ===", flush=True) + rc = run_case(case, backend) + try: + render_summary() + except Exception as error: + print(f"failed to render summary: {error}", file=sys.stderr) + failures += 1 + if rc not in (0, 2): + failures += 1 + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main())