Files
MobileGL/tools/trace_replay/README.md
T

632 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MobileGL trace replay
This directory builds a Linux command line replay runner for [apitrace](https://github.com/apitrace/apitrace) files. It
is an integration testing infrastructure of MobileGL.
The bundled fixtures cover:
- OpenRA: sourced from GL4ES' apitrace corpus.
![OpenRA golden](fixtures/openra.0000031249.png)
- minecraft-1.21.4-startup: captured from Minecraft 1.21.4's startup screen.
![Minecraft 1.21.4 startup golden](fixtures/minecraft-1.21.4-startup.0000092195.png)
- minecraft-1.21.4-main-menu: captured from Minecraft 1.21.4's main menu.
![Minecraft 1.21.4 main menu golden](fixtures/minecraft-1.21.4-main-menu.0000481787.png)
- minecraft-1.21.11-main-menu: captured from Minecraft 1.21.11's main menu on a Pixel 8 Pro through FCL MobileGL.
![Minecraft 1.21.11 main menu golden](fixtures/minecraft-1.21.11-main-menu.0000205347.png)
- minecraft-1.17-main-menu-854: captured from Minecraft 1.17's 854x480 main menu through FCL MobileGL capture.
![Minecraft 1.17 854x480 main menu golden](fixtures/minecraft-1.17-main-menu-854.0000117757.png)
- minecraft-1.21.4-in-world: captured from Minecraft 1.21.4 after entering a singleplayer world.
![Minecraft 1.21.4 in-world golden](fixtures/minecraft-1.21.4-in-world.0000280000.png)
- minecraft-1.21.4-rd12-odinlite-in-world: captured on an Android device (FCL MobileGL Magma capture, 854x480) from
vanilla Minecraft 1.21.4 at render distance 12, drifting down a river valley in a boat. Unlike the other in-world
fixtures this one is a 251-frame window (gltrim `-f 1094-1343`) rather than a single frame, so it doubles as the
campaign's benchmark scene: benchmark mode replays the whole window and the tail frames measure steady-state
in-world frame time. The golden is still the final frame, so it works as an ordinary correctness case too.
![Minecraft 1.21.4 render distance 12 in-world golden](fixtures/minecraft-1.21.4-rd12-odinlite-in-world.0004660351.png)
- minecraft-1.21.4-fabric-sodium-in-world: captured from Minecraft 1.21.4 Fabric with Sodium after entering a
singleplayer world with Fancy graphics.
![Minecraft 1.21.4 Fabric Sodium in-world golden](fixtures/minecraft-1.21.4-fabric-sodium-in-world.0000923340.png)
- improved-transparency-minecraft-26.3: captured from the Minecraft 26.3 improved-transparency scene.
![Minecraft 26.3 improved-transparency golden](fixtures/improved-transparency-minecraft-26.3.0002667619.png)
- minecraft-1.21.4-fabric-common-mods-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, REI,
Xaero's Minimap, Xaero's World Map, JourneyMap, and Modern UI, with shader packs disabled.
![Minecraft 1.21.4 Fabric common mods in-world golden](fixtures/minecraft-1.21.4-fabric-common-mods-in-world.0000522084.png)
- minecraft-1.21.4-fabric-common-mods-inventory: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, REI,
Xaero's Minimap, Xaero's World Map, JourneyMap, and Modern UI with the creative inventory and REI item list open.
![Minecraft 1.21.4 Fabric common mods inventory golden](fixtures/minecraft-1.21.4-fabric-common-mods-inventory.0000728558.png)
- minecraft-1.21.4-fabric-rei-inventory: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and REI, with
shader packs disabled and the creative inventory and REI item list open.
![Minecraft 1.21.4 Fabric REI inventory golden](fixtures/minecraft-1.21.4-fabric-rei-inventory.0005431826.png)
- minecraft-1.21.4-fabric-xaero-minimap-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Xaero's Minimap after entering a singleplayer world with shader packs disabled.
![Minecraft 1.21.4 Fabric Xaero's Minimap in-world golden](fixtures/minecraft-1.21.4-fabric-xaero-minimap-in-world.0002553500.png)
- minecraft-1.21.4-fabric-xaero-world-map-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Xaero's World Map, with shader packs disabled and the world map screen open.
![Minecraft 1.21.4 Fabric Xaero's World Map in-world golden](fixtures/minecraft-1.21.4-fabric-xaero-world-map-in-world.0001598209.png)
- minecraft-1.21.4-fabric-journeymap-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
JourneyMap after entering a singleplayer world with shader packs disabled.
![Minecraft 1.21.4 Fabric JourneyMap in-world golden](fixtures/minecraft-1.21.4-fabric-journeymap-in-world.0002632392.png)
- minecraft-1.21.4-fabric-modernui-inventory: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and Modern UI,
with shader packs disabled and the creative inventory open.
![Minecraft 1.21.4 Fabric Modern UI inventory golden](fixtures/minecraft-1.21.4-fabric-modernui-inventory.0004907381.png)
- minecraft-1.21.4-fabric-rei-inventory-normal-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
REI in a normal singleplayer world, with shader packs disabled and the creative inventory and REI item list open.
![Minecraft 1.21.4 Fabric REI inventory normal-world golden](fixtures/minecraft-1.21.4-fabric-rei-inventory-normal-world.0000734465.png)
- minecraft-1.21.4-fabric-xaero-minimap-in-world-normal-world: captured from Minecraft 1.21.4 Fabric with Sodium,
Iris, and Xaero's Minimap after entering a normal singleplayer world with shader packs disabled.
![Minecraft 1.21.4 Fabric Xaero's Minimap normal-world golden](fixtures/minecraft-1.21.4-fabric-xaero-minimap-in-world-normal-world.0000457190.png)
- minecraft-1.21.4-fabric-xaero-world-map-in-world-normal-world: captured from Minecraft 1.21.4 Fabric with Sodium,
Iris, and Xaero's World Map in a normal singleplayer world, with shader packs disabled and the world map screen open.
![Minecraft 1.21.4 Fabric Xaero's World Map normal-world golden](fixtures/minecraft-1.21.4-fabric-xaero-world-map-in-world-normal-world.0000573061.png)
- minecraft-1.21.4-fabric-journeymap-in-world-normal-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris,
and JourneyMap after entering a normal singleplayer world with shader packs disabled.
![Minecraft 1.21.4 Fabric JourneyMap normal-world golden](fixtures/minecraft-1.21.4-fabric-journeymap-in-world-normal-world.0000641975.png)
- minecraft-1.21.4-fabric-modernui-inventory-normal-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris,
and Modern UI in a normal singleplayer world, with shader packs disabled and the creative inventory open.
![Minecraft 1.21.4 Fabric Modern UI inventory normal-world golden](fixtures/minecraft-1.21.4-fabric-modernui-inventory-normal-world.0000727926.png)
- minecraft-1.21.4-fabric-iris-bsl-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and BSL
Shaders after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris BSL in-world golden](fixtures/minecraft-1.21.4-fabric-iris-bsl-in-world.0000110725.png)
- minecraft-1.21.4-fabric-iris-bsl-esc-menu-854: captured on an Android device (Mali-G77, FCL MobileGL capture) from
Minecraft 1.21.4 Fabric with Sodium, Iris, and BSL Shaders, at the pause menu over a BSL-blurred world. The frame
pins glyph rendering: every menu label, the menu title and the tutorial toast must be present. Regressions in the
DirectGLES per-draw texture memo have made the whole text path disappear here while sprites kept rendering, so a
failure that leaves the buttons but empties them is the signature to look for in the diff.
![Minecraft 1.21.4 Fabric Iris BSL ESC menu 854x480 golden](fixtures/minecraft-1.21.4-fabric-iris-bsl-esc-menu-854.0001303534.png)
- minecraft-1.21.4-fabric-iris-makeup-ultrafast-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
MakeUP UltraFast after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris MakeUP UltraFast in-world golden](fixtures/minecraft-1.21.4-fabric-iris-makeup-ultrafast-in-world.0000095322.png)
- minecraft-1.21.4-fabric-iris-super-duper-vanilla-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris,
and Super Duper Vanilla after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Super Duper Vanilla in-world golden](fixtures/minecraft-1.21.4-fabric-iris-super-duper-vanilla-in-world.0000141559.png)
- minecraft-1.21.4-fabric-iris-complementary-reimagined-in-world: captured from Minecraft 1.21.4 Fabric with Sodium,
Iris, and Complementary Reimagined after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Complementary Reimagined in-world golden](fixtures/minecraft-1.21.4-fabric-iris-complementary-reimagined-in-world.0000151297.png)
- minecraft-1.21.4-fabric-iris-complementary-unbound-in-world: captured from Minecraft 1.21.4 Fabric with Sodium,
Iris, and Complementary Unbound after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Complementary Unbound in-world golden](fixtures/minecraft-1.21.4-fabric-iris-complementary-unbound-in-world.0000146559.png)
- minecraft-1.21.4-fabric-iris-mellow-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and Mellow
after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Mellow in-world golden](fixtures/minecraft-1.21.4-fabric-iris-mellow-in-world.0000096143.png)
- minecraft-1.21.4-fabric-iris-nostalgia-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Nostalgia after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Nostalgia in-world golden](fixtures/minecraft-1.21.4-fabric-iris-nostalgia-in-world.0000153808-linux-mesa.png)
- minecraft-1.21.4-fabric-iris-bliss-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and Bliss
after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Bliss in-world golden](fixtures/minecraft-1.21.4-fabric-iris-bliss-in-world.0000113511.png)
- minecraft-1.21.4-fabric-iris-chocapic-v6-lite-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Chocapic V6 Lite after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Chocapic V6 Lite in-world golden](fixtures/minecraft-1.21.4-fabric-iris-chocapic-v6-lite-in-world.0000125124-linux-mesa.png)
- minecraft-1.21.4-fabric-iris-iterationt-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
iterationT after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris iterationT in-world golden](fixtures/minecraft-1.21.4-fabric-iris-iterationt-in-world.0000110538.png)
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
![Minecraft 1.21.4 Fabric Iris iterationT no-DSA in-world golden](fixtures/minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world.0000115019.png)
- minecraft-1.21.4-fabric-iris-iterationrp-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
iterationRP after entering a singleplayer world, framing the iterationRP name overlay over a lake with far-shore
tree reflections. iterationRP's temporal auto-exposure makes a single-frame trim overexpose and drop the overlay,
so the fixture is a prefix trace (all calls up to the target frame) that replays the temporal state. The pack also
gates an NVIDIA-only shadow path (`subgroupPartitionNV`, `GL_NV_shader_subgroup_partitioned`) on the GL vendor
string, so the capture reports a masked vendor and the trace carries the portable `subgroupShuffleXor` path that
non-NVIDIA GPUs take.
The trace archive and golden are not committed yet (the repository's Git LFS quota rejects new objects with
`GH009`); the case stays registered and its fixture files are hydrated from the trace fixture mirror.
- minecraft-1.21.4-fabric-iris-photon-v1.1-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Photon v1.1 after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Photon v1.1 in-world golden](fixtures/minecraft-1.21.4-fabric-iris-photon-v1.1-in-world.0000159866.png)
- minecraft-1.21.4-fabric-iris-photon-v1.3b-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Photon v1.3b after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Photon v1.3b in-world golden](fixtures/minecraft-1.21.4-fabric-iris-photon-v1.3b-in-world.0000172128.png)
- minecraft-1.21.4-fabric-iris-derivative-main-d24.4.14-in-world: captured from Minecraft 1.21.4 Fabric with Sodium,
Iris, and Derivative Main d24.4.14 after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Derivative Main d24.4.14 in-world golden](fixtures/minecraft-1.21.4-fabric-iris-derivative-main-d24.4.14-in-world.0000145353.png)
- minecraft-1.21.4-fabric-iris-sundial-lite-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Sundial Lite after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Sundial Lite in-world golden](fixtures/minecraft-1.21.4-fabric-iris-sundial-lite-in-world.0000150023.png)
- minecraft-1.21.1-neoforge-create-indirect-in-world-align1024: **not an original capture** - derived from the retired
`minecraft-1.21.1-neoforge-create-indirect-in-world` capture (Minecraft 1.21.1 NeoForge with Create, Sodium, and
Iris (no shader pack) in a world facing Create water wheels and a large cogwheel, with Flywheel's
`flywheel:indirect` backend: compute-shader culling, glMultiDrawElementsIndirect, persistent-mapped staging). That
capture came from a desktop driver whose `GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT` is 16, so Flywheel bound its
scatter lists at 16-byte offsets, and a driver that requires more (Adreno 830: 64) rejects every one of those binds
with `GL_INVALID_VALUE`. [`fixture_tools/realign_buffer_ranges.cpp`](fixture_tools/realign_buffer_ranges.cpp)
`--align 1024` rewrote it: each of the 1,270 range binds off a 1024-byte boundary now binds a 1024-aligned slot of
an added buffer that is filled with the same bytes right before the dispatch that reads them. The 3,812 inserted
calls renumber the trace (target call 504631 -> 508443); the golden is the original one (the same frame), and the
host replay matches it with the original's SSIM to 9 digits on both backends.
![Minecraft 1.21.1 NeoForge Create indirect (align 1024) in-world golden](fixtures/minecraft-1.21.1-neoforge-create-indirect-in-world-align1024.0000508443.png)
- minecraft-1.21.1-neoforge-create-instancing-in-world: same world and camera as the indirect case, with Flywheel's
`flywheel:instancing` backend (texture-buffer instance data, glDrawElementsInstancedBaseVertex).
![Minecraft 1.21.1 NeoForge Create instancing in-world golden](fixtures/minecraft-1.21.1-neoforge-create-instancing-in-world.0000530333.png)
Build from the MobileGL repository root:
```sh
cmake -S . -B build-test -G Ninja \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_TRACE_REPLAY=ON
cmake --build build-test
```
Run the fixture tests:
```sh
ctest --test-dir build-test -V -R 'MobileGLTraceReplay\.'
```
Run the CLI directly:
```sh
build-test/tools/trace_replay/mobilegl_trace_replay \
--trace openra.trace \
--golden openra.0000031249.png \
--output out/openra \
--backend DirectGLES \
--mobilegl-library build-test/libMobileGL.so \
--target-call 31249 \
--width 640 \
--height 480 \
--crop-x 1 \
--crop-y 1 \
--crop-width 638 \
--crop-height 478 \
--ssim-threshold 0.99
```
## Dumping framebuffer attachments mid-frame
`--target-call` snapshots one framebuffer. To see *inside* a frame - which
intermediate render target a pass actually produced - pass
`--dump-fbo-attachments CALL:DIR[:FBO,FBO,...]`, repeatably:
```sh
build-test/tools/trace_replay/mobilegl_trace_replay \
--trace trace.trace --golden golden.png --output out --target-call 2667619 \
--dump-fbo-attachments 2666231:out/fbos-before \
--dump-fbo-attachments 2666232:out/fbos-after
```
At each call boundary it walks every live framebuffer object (or only the named
ones), reads back every colour attachment and the depth attachment, and writes
`fbo<N>-att<M>.png` / `fbo<N>-depth.png` plus a `manifest.txt` line per
attachment recording the attached object, size, internal format, component type
and per-channel min/max/mean and a content hash. Attachments are read as floats
whatever their storage, so HDR accumulation buffers stay legible in the
statistics even though the PNG has to clamp.
The manifest is the useful part when comparing two drivers: dump the same call
on both stacks and `diff`/`paste` the two manifests, and the first attachment
whose hash differs names the pass that diverged. Read-side and pixel-pack state
is saved and restored, so the replay continues unperturbed; without the flag
nothing is installed and the replay is byte-for-byte what it was.
Run the macOS native-window DirectVulkan retrace matrix and render the same
HTML overview shape as CI:
```sh
cmake -S . -B cmake-build-macos-trace-arm64 -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DMOBILEGL_BUILD_TEST=OFF \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_TRACE_REPLAY=ON
cmake --build cmake-build-macos-trace-arm64 --target MobileGL mobilegl_trace_replay
python3 tools/trace_replay/run_macos_window_retrace_local.py --ci --all
open .trace-work/macos-window-retrace-summary/mobilegl-macos-window-vulkan-retrace-overview.html
```
The macOS runner hydrates missing fixtures from the trace fixture mirror with
parallel downloads before falling back to Git LFS. It reuses the
`cmake-build-macos-trace-arm64` harness by default on Apple Silicon, passes
`--window-surface`, and defaults to DirectVulkan only. If a native-window replay
hits a fatal assertion, the runner writes a failure result and stops before
launching later cases; use `--continue-after-fatal` to collect the full matrix,
or `--skip-case NAME` for known fatal cases.
## Android device replay
Build and install the generic trace APK from the repository root. Both
`DirectGLES` and `DirectVulkan` use the same APK and package; select the
backend with the intent's `backend` extra.
```sh
gradle --no-daemon -p android-plugin :app:assembleTraceDebug
TRACE_APK=$(find android-plugin/app/build/outputs/apk/trace/debug -maxdepth 1 -name '*.apk' -print -quit)
adb install -r "$TRACE_APK"
```
Prepare a fixture and copy it into the app-private directory:
```sh
mkdir -p /tmp/mobilegl-openra
tar -xzf tools/trace_replay/fixtures/openra.tgz -C /tmp/mobilegl-openra
adb push /tmp/mobilegl-openra/openra.trace /data/local/tmp/mobilegl-openra.trace
adb push tools/trace_replay/fixtures/openra.0000031249.png /data/local/tmp/mobilegl-openra.golden.png
PKG=top.mobilegl.plugin.trace
APP_DIR=/data/user/0/$PKG/files/trace-replay
adb shell run-as $PKG rm -rf files/trace-replay
adb shell run-as $PKG mkdir -p files/trace-replay/input files/trace-replay/output
adb shell run-as $PKG cp /data/local/tmp/mobilegl-openra.trace files/trace-replay/input/openra.trace
adb shell run-as $PKG cp /data/local/tmp/mobilegl-openra.golden.png files/trace-replay/input/openra.golden.png
```
Launch the standalone trace runner Activity:
```sh
adb shell am force-stop $PKG
adb shell am start -W -a top.mobilegl.plugin.TRACE_REPLAY \
-n $PKG/top.mobilegl.plugin.trace.TraceReplayActivity \
--es trace_path $APP_DIR/input/openra.trace \
--es golden_path $APP_DIR/input/openra.golden.png \
--es output_dir $APP_DIR/output \
--es diff_path $APP_DIR/output/openra-diff.png \
--es backend DirectGLES \
--el target_call 31249 \
--ei width 640 \
--ei height 480 \
--ei crop_x 1 \
--ei crop_y 1 \
--ei crop_width 638 \
--ei crop_height 478 \
--es ssim_threshold 0.99
```
Read back the result and images:
```sh
adb shell run-as $PKG cat files/trace-replay/output/result.json
adb exec-out run-as $PKG cat files/trace-replay/output/actual.png > openra-actual.png
adb exec-out run-as $PKG cat files/trace-replay/output/openra-diff.png > openra-diff.png
```
For Vulkan replay, keep the same APK and `$PKG`, then pass
`--es backend DirectVulkan`. DirectGLES also renders to the Activity surface by
default; pass `--ez use_pbuffer true` to use the offscreen pbuffer path. Always
`adb shell am force-stop $PKG` before another replay: apitrace snapshot state is
process-local. For cases registered with `coherent_as_flush` (Flywheel-style
unflushed persistent maps, e.g. the Create fixtures), pass
`--ez coherent_as_flush true` so the replay runs with
`MOBILEGL_COHERENT_AS_FLUSH=1`. For cases registered with
`avoid_angle_llvmpipe_explicit_lod_bias` (DirectGLES on ANGLE llvmpipe, e.g. the
sundial-lite fixture), pass `--ez avoid_angle_llvmpipe_explicit_lod_bias true` so
the replay runs with `MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS=1`.
### Repeats, the CI split subset, and keeping every repeat's evidence
`run_android_retrace_local.py` drives the above through `trace-replay-ci.sh`. Three
options exist for the P7-7 device diagnostics, where the question is not "did this
case pass" but "which cases diverge, and is the divergence reproducible":
| option | what it does |
| --- | --- |
| `--matrix` | run the **CI split subset** (the set `retrace-split` runs; 39 cases today) instead of the whole manifest. `--all` additionally includes the non-CI workloads such as rd12, which ID-P7-4 excludes from the exit-gate-3 denominator. Each case runs only the backends its own `ci_backends` names - and a case NAMED with `--case` that does not run the requested backend is an error, not a silent skip. |
| `--repeat N` | replay each case/backend N times. Repeats after the first pass `--reuse-fixture`, so they measure the replay and not the push. |
| `--archive-dir DIR` | copy each repeat's `result.json`, actual PNG, both role logs, `transport-proof.json` and `logcat.txt` to `DIR/<case>-<backend>/repeat-NN/` **before the next run overwrites them**, plus `DIR/run.json` recording the APK SHA-256, the arm and the environment. |
The archive exists because `.trace-work/android-retrace-result/<case>-<backend>/` is
keyed by case and backend and by nothing else: without `--archive-dir` the only
artefact that survives a three-repeat run is the last one, and "were the three
repeats the same picture?" becomes unanswerable after the fact.
```sh
python tools/trace_replay/run_android_retrace_local.py \
--matrix --backend DirectVulkan --use-pbuffer --transport inproc --repeat 3 \
--archive-dir .trace-work/p7w1/E0-inproc
python3 tools/trace_replay/compare_actuals.py summary .trace-work/p7w1/E0-inproc \
--json E0-inproc-summary.json
```
`compare_actuals.py` computes the pair `result.json` cannot: SSIM between two actual
PNGs. It is the retrace gate's OWN SSIM - a transcription of `ComputeChannelSsim` /
`ComputeRgbSsim` (`android-plugin/app/src/trace/cpp/trace_replay_core.cpp:630` and
`:671`), one global window per channel, C1 = (0.01*255)², C2 = (0.03*255)², alpha
ignored - and the crop comes from each repeat's own `result.json`, so the
`ssim_vs_first` column describes the same rectangle as the recorded `ssim_vs_golden`.
`summary` prints `case × repeat × ssim_vs_golden × ssim_vs_first × bit-identical`;
`compare A.png B.png` scores one pair; `--self-test` checks the transcription against
closed-form values. Unit tests: `python3 tools/trace_replay/test_compare_actuals.py`.
## Benchmark mode (frame timing)
Benchmark mode reuses the same fixtures as a performance harness instead of a
correctness one: it replays the trace from the first call to the last, takes a
wall-clock timestamp at every frame boundary, and takes no snapshot and runs no
SSIM comparison. `passed` then only means the replay reached the end of the trace
without an error.
Frame times include GPU completion by default, because a swap boundary on a tiled
mobile GPU returns long before the tiler is done and would otherwise time CPU
submission alone. That is what `benchmark_finish` / `--benchmark-finish` controls:
on (the default) it issues a `glFinish` through the replayed context at every
frame boundary, which serializes CPU/GPU overlap - pessimistic against a real
running game, but deterministic and comparable between backends and revisions.
Turn it off to measure CPU-side submission only.
The mean/median/p95 are computed over the last `benchmark_tail_frames` frames
(default 200, clamped to the frames actually recorded); the head of a trace is
dominated by shader compiles and first-use uploads. The full per-frame array is in
the timing JSON, next to the headline numbers, which also appear in `result.json`.
Whole device runs, one line per run plus the best of the repeats:
```sh
python tools/trace_replay/run_android_retrace_local.py --benchmark \
--case minecraft-1.21.4-fabric-iris-photon-in-world --backend DirectVulkan \
--benchmark-repeats 3
```
`--benchmark-tail-frames N`, `--benchmark-no-finish` and
`--benchmark-timeout-seconds N` are available; only the first repeat installs the
APK and pushes the trace. Each run's `benchmark.json` is kept next to the case
result as `benchmark-run<N>.json`.
The Activity takes the same settings directly:
```sh
adb shell am start -a top.mobilegl.plugin.TRACE_REPLAY \
-n $PKG/top.mobilegl.plugin.trace.TraceReplayActivity \
--es trace_path $APP_DIR/input/openra.trace \
--es output_dir $APP_DIR/output \
--es backend DirectGLES \
--ei width 640 --ei height 480 \
--ez benchmark true \
--ei benchmark_tail_frames 200 \
--ez benchmark_finish true \
--es benchmark_result_path $APP_DIR/output/benchmark.json
adb exec-out run-as $PKG cat files/trace-replay/output/benchmark.json > benchmark.json
```
`golden_path` and `target_call` are not needed in benchmark mode. The Linux CLI
takes the same options:
```sh
./mobilegl_trace_replay --trace openra.trace --output out --backend DirectGLES \
--benchmark --benchmark-tail-frames=200 --benchmark-finish=1 \
--benchmark-result=out/benchmark.json
```
Two caveats when reading the numbers. Frame times are taken at `eglSwapBuffers`,
so a trace that ends frames with `glFrameTerminatorGREMEDY` instead is not timed.
And on a window surface the swap can block on the compositor, which pins frame
times to the display refresh; replay against the pbuffer surface
(`--ez use_pbuffer true`) to measure the renderer rather than the presentation
path.
## Reproducing the Android DirectGLES lane on Linux (ANGLE on lavapipe)
The APK workflow's DirectGLES lane is not the same stack as the Linux one, which
is why a case can be green here and red there:
| lane | stack |
| --- | --- |
| Linux `Test` retrace, DirectGLES | Espryt -> Mesa GLES -> llvmpipe |
| Android `APK` retrace, DirectGLES | Espryt -> **ANGLE** -> Mesa Vulkan (lavapipe) |
| Android `APK` retrace, DirectVulkan | Magma -> lavapipe (no ANGLE) |
Only the Android DirectGLES lane puts ANGLE in the middle, so an ANGLE
translation difference shows up in exactly one of the six combinations. That
stack can be reproduced on Linux without an emulator, which is far faster to
iterate on than a CI round trip. The Android emulator SDK ships a glibc ANGLE:
```sh
ANGLE=$ANDROID_SDK_ROOT/emulator/lib64/gles_angle
mkdir -p ~/angle-farm && cd ~/angle-farm
# MobileGL dlopens these two names; ANGLE's own libEGL then dlopens the
# unsuffixed libGLESv2.so from the same directory - without that symlink it
# loads a truncated entry-point table and dies on a missing EGL function.
ln -sf $ANGLE/libEGL.so libEGL_angle.so
ln -sf $ANGLE/libGLESv2.so libGLESv2_angle.so
ln -sf $ANGLE/libEGL.so libEGL.so
ln -sf $ANGLE/libGLESv2.so libGLESv2.so
ln -sf $ANGLE/libvulkan.so.1 libvulkan.so.1 # else eglInitialize fails
MOBILEGL_ESPRYT_USE_ANGLE=1 \
LD_LIBRARY_PATH=~/angle-farm:/path/to/build/ \
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json \
ANGLE_DEFAULT_PLATFORM=vulkan \
./mobilegl_trace_replay --trace trace.trace --golden golden.png \
--target-call N --width 854 --height 480 --backend DirectGLES \
--output outdir --pbuffer-surface
```
`ANGLE_DEFAULT_PLATFORM=vulkan` is required: ANGLE otherwise picks its OpenGL
backend and you get `ANGLE (Mesa, llvmpipe ..., OpenGL 4.6 (Core Profile))`
instead of the CI-shaped `ANGLE (Mesa, Vulkan 1.x (llvmpipe ...))`. Check
`MOBILEGL_TRACE_GL_RENDERER` in `outdir/retrace.log` before trusting a result.
Run the binary directly rather than through `ctest`, whose `ENVIRONMENT`
property overrides these variables. Build with clang, not gcc: gcc rejects
`GLXImpl.cpp` under `-Wchanges-meaning`.
One more caveat before attributing anything: the emulator SDK's ANGLE is not
the ANGLE the Android lane runs. The CI lane uses a pinned build
(`MOBILEGL_TRACE_ANGLE_VARIANT`, default `ec889e6ea831`) whose version and
extension set differ from the SDK copy (`GL_EXT_texture_buffer` support, ES 3.2
entry points). Compare `GL_RENDERER` and the relevant extension lists on both
stacks before treating a local result as a statement about CI.
## TCP matrix driver (Linux / WSL)
`run_tcp_matrix.py` uses a CTest JSON catalog plus the **current** `trace_cases.json`.
It does not build, install an APK, or launch the remote server. For the formal
P6.5 run-ahead matrix, use `--require-run-ahead`. It forces `MOBILEGL_IPC_RUN_AHEAD=1`
and `MOBILEGL_IPC_VERB_BARRIER=1`, then requires `run-ahead ARMED` in each private
client log and rejects any `running lockstep` / `DISARMED` marker. The checkpoint
records `requested_arm` and `actual_arm`; strict resume cannot reuse an earlier
lockstep image pass. Start the intended server first, then run serially:
```bash
ctest --test-dir /path/to/retrace-build --show-only=json-v1 > /tmp/trace-catalog.json
python3 tools/trace_replay/run_tcp_matrix.py \
--catalog /tmp/trace-catalog.json \
--runner /path/to/mobilegl_trace_replay --library /path/to/libMobileGL.so \
--endpoint tcp://192.168.21.181:40613 --token devtoken \
--backend DirectGLES --credit 2 --require-run-ahead --out /tmp/tcp-matrix
# Continue the same frozen library/runner/peer:
python3 tools/trace_replay/run_tcp_matrix.py \
--catalog /tmp/trace-catalog.json \
--runner /path/to/mobilegl_trace_replay --library /path/to/libMobileGL.so \
--endpoint tcp://192.168.21.181:40613 --token devtoken \
--backend DirectGLES --credit 2 --require-run-ahead --out /tmp/tcp-matrix --resume
```
`--build-dir` can replace `--catalog` to obtain the JSON without building.
`--runner` overrides the catalog runner; fixture paths default to the catalog's
fixture directory and can be overridden with `--fixtures`. Repeat `--case` or
`--backend` to select a subset. The default is the manifest's CI split subset
(currently 39 cases), honoring each case's backend list. Explicit `--case
minecraft-1.21.4-rd12-odinlite-in-world` permits that non-CI workload separately;
an explicit `split:false` always remains excluded.
### Both backends, one driver
`--backend DirectVulkan` runs P7 exit gate 3's device matrix with the same
run-ahead proof, resume and watchdog the DirectGLES matrix uses. Three things are
per backend and the rest is deliberately shared:
- **selection** - a case is planned for a backend only if its own `ci_backends`
names it, so `minecraft-1.21.4-fabric-iris-iterationrp-in-world` appears in a
DirectVulkan sweep (39 cases) and not in a DirectGLES one (38). A `--case` named
explicitly that does not run a requested backend is a refusal that names the
case, not one plan fewer and no message;
- **golden and threshold** - from `backend_overrides.<backend>` in
`trace_cases.json` when the case declares one. A per-backend `golden` REPLACES
the shared `golden`/`alternate_golden` pair for that backend rather than adding
to it, and a run that matched an image another backend declares is refused by
name. (`alternate_golden` alone cannot express this: it is an OR across
backends, so a DirectVulkan run matching the DirectGLES picture passes.) The
block may restate only `golden`, `alternate_golden` and `ssim_threshold` -
restating `target_call` or a crop would stop the two backends retracing the same
frame through the same window, which is what makes the two numbers comparable;
- **result naming and checkpoint key** - `<backend>/credit-<n>/<case>`, plus the
golden, alternate golden and threshold recorded in each `results.json` row.
A case with no `backend_overrides` is resolved to itself, so DirectGLES plans,
identities and checkpoint keys are unchanged by this option's existence - and the
key is dropped from the fingerprint even for the backend that overrides nothing,
so giving DirectVulkan its own golden does not invalidate the DirectGLES
checkpoints. **The CTest catalog emitter and the APK matrix refuse a case that
declares `backend_overrides`**: both carry one golden path for both backends, so
emitting such a case through them would register one backend's arm against the
other's image. Teach them the key in the same commit that adds an override.
The watchdog defaults to **300 seconds without log progress** and a separate
**7200-second absolute ceiling** (`--idle-seconds`, `--max-seconds`). It observes
runner output and trace/client/server log changes. It deliberately ignores the
old local CTest/manifest `timeout_seconds`: TCP main-menu kept progressing beyond
its old local 180-second budget. A growing log does not bypass the explicit absolute ceiling.
On timeout the driver terminates and reaps the entire case process group,
including a child that ignores TERM after CMake exits, and stops the matrix to
avoid cascading Busy failures. Check the peer before resuming. Ctrl-C/TERM/HUP
also clean up the group and save a cancelled checkpoint.
Each attempt has a new directory. `checkpoint.json` is written atomically, and
only a zero exit plus current-attempt successful result, matching backend/call/
golden, adequate SSIM, an actual image, and the requested TCP arm marker count
as passed. Resume also checks request/artifact identity and stored evidence
hashes. Failed, timed-out, interrupted, changed, or missing evidence is rerun;
old scratch `results.json` files are retained but never silently promoted into
successful checkpoints. Replacing the remote deployment requires a new output
root unless it is the same frozen peer. `results.json` summarizes the currently
selected runs and marks reused evidence with `resumed:true`.
`--wake-adb-serial 2f7cbe2e` explicitly opts into `adb shell input keyevent
KEYCODE_WAKEUP` every 15 seconds during a case. It does not install anything or
change global power/idle settings. Its own adb log is excluded from the progress
watchdog, so wakeup messages cannot conceal a stalled trace.
Tool regression tests use short process stubs, never a GPU/device/trace. They run
in CI beside the other executable negative controls (`test.yml`, the R-16 step):
```bash
python3 tools/trace_replay/test_run_tcp_matrix.py
```
## Device servers: offscreen or on the phone's screen
`tcp_device_server.py` starts one of the trace APK's two TCP servers; only one is active
at a time (whichever starts last stops the other). `stop` force-stops the package.
```bash
# Offscreen (default): MobileGLServerService exec's the supervisor, one child per session.
python3 tools/trace_replay/tcp_device_server.py start --serial 2f7cbe2e \
--listen tcp://127.0.0.1:40613 --token devtoken-of-16-bytes --forward
# On-screen: MobileGLDisplayActivity (process :mglwin) serves in its own process and a
# client's window surface renders on its SurfaceView. The screen must be on and unlocked.
python3 tools/trace_replay/tcp_device_server.py start --surface window [--backend DirectVulkan] \
--serial 2f7cbe2e --listen tcp://127.0.0.1:40613 --token devtoken-of-16-bytes --forward
```
A headless client replays onto the phone's screen with `--window-surface` and
`MOBILEGL_IPC_SURFACE=server` (plus the usual `MOBILEGL_TRANSPORT=spawn`,
`MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:40613`, `MOBILEGL_IPC_DATA=stream`,
`MOBILEGL_IPC_TOKEN`). The server sizes its window to the trace's EGL_WIDTH/EGL_HEIGHT
(letterboxed on screen) and the client's `eglQuerySurface` answers that size. Without
`--backend` the on-screen server pins the first session's backend for its process; restart
it (`stop`, then `start`) to switch. The same knob against the offscreen service is refused
by name (`NoServerDisplay`) and that service goes on serving.
## TCP credit and Stage measurements
For a paired frame-time breakdown, enable `MOBILEGL_PIPE_STATS=1` in both roles
and keep their logs. The following reducer matches client and server present
serials, drops the first (unpaired) server interval and one warmup frame, and
reports avg/p50/p99 in milliseconds plus percentages of client present time:
```bash
python3 tools/trace_replay/frame_stats.py \
--client-log mobilegl.client.log --server-log mobilegl.server.log \
--warmup-frames 1 --output frame-stats.json
```
`transport_wait_ms` measures wall time in the client producer's transport park
(flush, spin and sleep). It includes waiting for replies, present credit and
ring space; it is not socket latency. Client and server CPU can overlap with
each other and with a transport wait, so each percentage uses the client
present interval as its denominator and the three percentages need not sum to
100%. A missing frame is rejected. Percentiles use nearest rank over per-frame
samples.
`benchmark_tcp_credits.py` runs the complete OpenRA and rd12 traces at credits
1, 2, and 3, then selects matching client/server tail frame IDs (100 and 200
frames respectively). Extract the unchanged trace files as
`inputs/OpenRA/openra.trace` and `inputs/rd12/trace.trace`. Keep the server and
library frozen, enable server `MOBILEGL_PIPE_STATS=1` and
`MOBILEGL_PIPE_STATS_PERIOD=1`, and leave the device connection free:
```bash
python3 tools/trace_replay/benchmark_tcp_credits.py \
--runner /path/to/mobilegl_trace_replay --library /path/to/libMobileGL.so \
--inputs /path/to/inputs --out /tmp/tcp-credits \
--endpoint tcp://192.168.21.181:40613 --token devtoken --serial 2f7cbe2e
python3 tools/trace_replay/measure_tcp_stage_burst.py \
--library /path/to/libMobileGL.so --out /tmp/tcp-stage-burst \
--endpoint tcp://192.168.21.181:40613 --token devtoken
```
The credit runner serializes all six runs, sends the same explicit awake key as
the matrix driver, and preserves complete benchmark and role logs. It requires
the requested credit and continuous run-ahead ARMED proof in the client log,
matching complete tail frames, and positive per-thread CPU observations.
`--case` and `--credit` select a subset; `--resume` reuses only matching identity
and intact evidence. FPS, `kWaitReply` counts, bucketed RTT distribution, Stage
bytes, and the two role CPU clocks come from the selected steady window.
Completing this benchmark does not perform or replace a golden image comparison.
The Stage tool uploads exactly 64 MiB after one warmup frame, consumes the dirty
buffer in a draw, and waits for apply. It requires actual TCP run-ahead and at
least 64 MiB of observed Stage bytes in frame 2, preserves artifact hashes, and
refuses to overwrite an existing output directory. Its throughput includes
client copies and backend work; it is not raw Wi-Fi bandwidth. Divide steady
frame Stage traffic by this separately measured throughput only with that
interpretation. Device matrices, benchmarks, and the burst must run one at a time.
Offline evidence controls (no device):
```bash
python3 tools/trace_replay/test_benchmark_tcp_credits.py
```