[Docs] (trace-replay): refresh fixture-authoring skill from the Create fixture work

Registration now documents the trace_cases.json registry (the CMakeLists /
apk.yml instructions were stale). Adds the field-tested guidance from
authoring the Create fixtures: in-tree apitrace fork requirements (frametrim
DSA/multi-bind, persistent-map shadowing) and the Windows wgltrace wrapper,
frozen-world + unfocused-window capture discipline, late-frame selection,
trim verification, brotli repack (with the stale-archive trap), golden
content verification, Android signing/stale-package/emulator-flake and
stale-result pitfalls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 14:26:52 +00:00
co-authored by Claude Fable 5
parent effdaabab3
commit 4e558ee142
@@ -34,19 +34,30 @@ Install:
## Build apitrace
Build the in-tree fork, not an upstream release: it carries the frametrim
handlers for DSA and ARB_multi_bind call streams and shadow-based tracking of
persistent-mapped buffers, all of which modern Minecraft mod stacks need.
```sh
cmake -S "$REPO/3rdparty/apitrace" -B "$WORK/build-apitrace" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_GUI=OFF
cmake --build "$WORK/build-apitrace" --target apitrace glretrace --parallel
cmake --build "$WORK/build-apitrace" --target apitrace glretrace gltrim --parallel
export APITRACE="$(find "$WORK/build-apitrace" -type f -name apitrace -perm -111 | head -n 1)"
export GLRETRACE="$(find "$WORK/build-apitrace" -type f -name glretrace -perm -111 | head -n 1)"
export GLTRIM="$(find "$WORK/build-apitrace" -type f -name gltrim -perm -111 | head -n 1)"
test -n "$APITRACE"
test -n "$GLRETRACE"
test -n "$GLTRIM"
```
On Windows, set `APITRACE` and `GLRETRACE` to the corresponding `.exe` files.
On Windows, set `APITRACE` and `GLRETRACE` to the corresponding `.exe` files
and also build the `wgltrace` target: `apitrace trace` fails with "failed to
find opengl32.dll wrapper" unless `wrappers/opengl32.dll` was built. `gltrim`
may be built and run on a Linux/WSL checkout instead; traces are portable, and
trimming a multi-hundred-MB trace is much faster from a native filesystem copy
than through `/mnt/c`.
## Prepare the capture
@@ -58,6 +69,28 @@ On Windows, set `APITRACE` and `GLRETRACE` to the corresponding `.exe` files.
- For Minecraft, document version, mod loader, mods, shader pack, language,
world, time, and camera setup.
Minecraft specifics that keep the capture deterministic and small:
- Freeze the world in `level.dat`: `doDaylightCycle`, `doWeatherCycle`,
`doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player
`Rotation` that frames the intended subject. The camera snaps to the saved
rotation on world join, so composition is edited in the save, not in-game.
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), and a
small `renderDistance` (3). Frame rate and render distance are the two main
levers on trace size; a ~35 s in-world session at 10 fps lands well under
the archive budget after repack.
- Enter the world non-interactively with `--quickPlaySingleplayer <world>` so
every capture takes the same path from boot to gameplay.
- Keep the game window UNFOCUSED for the whole capture (focus the desktop
right after launch, and again before closing). A focused Minecraft window
grabs the mouse, and any physical mouse motion rotates the camera - the
resulting goldens show a drifted view that is easy to misread as a
rendering bug.
- On Windows with JDK 21+, pass
`-Djdk.net.unixdomain.tmpdir=<short-path-without-spaces>`: NIO selectors
create AF_UNIX sockets under `%TEMP%`, which fails on some hosts and kills
the game at boot with "Unable to establish loopback connection".
## Capture
```sh
@@ -75,8 +108,21 @@ For Java:
-- "$JAVA_EXE" <jvm-args> <main-class-or-jar> <game-args>
```
An `@argfile` with the full JVM+game command line keeps the invocation
reproducible across recaptures.
Keep `full.trace` until both backends are validated.
Persistent-mapped buffers: apps may legally write a `GL_MAP_PERSISTENT_BIT`
mapping and let the GPU read it without an explicit flush (Flywheel's indirect
backend writes its compute scatter descriptors this way). Stock apitrace never
records those writes, so the trimmed fixture silently loses the content that
depends on them - the symptom is geometry that renders live but disappears in
replay. The in-tree fork shadow-tracks persistent mappings unconditionally;
if a replay of `full.trace` is already missing content that the live run
showed, fix capture (wrapper) first - no amount of trimming will bring the
data back, and the case must be recaptured.
## Select target frame
Fixture selection must be frame-based. Do not trim the fixture from a full trace
@@ -88,8 +134,17 @@ frame, then trim with `gltrim -f`.
> "$WORK/$CASE/frames.txt"
```
Inspect `frames.txt`, identify the frame that contains the intended visual
state, and set:
A quick way to bound the choice is the total frame count from a benchmark
replay:
```sh
"$GLRETRACE" -b "$WORK/$CASE/full.trace" # "Rendered N frames in ..."
```
Pick a LATE frame (roughly `N - 20`): early frames still contain loading
screens, chunk pop-in, and animation warm-up, while the last few frames may
overlap the window-close path. Inspect `frames.txt` or snapshot the candidate
frame to confirm it contains the intended visual state, then set:
```sh
export TARGET_FRAME=<chosen-frame-number>
@@ -98,12 +153,22 @@ export TARGET_FRAME=<chosen-frame-number>
## Trim and package
```sh
"$APITRACE" gltrim \
"$GLTRIM" \
-f "$TARGET_FRAME" \
--output "$WORK/$CASE/trace.trace" \
"$WORK/$CASE/full.trace"
```
Then VERIFY the trim before doing anything else: replay `trace.trace`,
snapshot its final swap, and compare the content against the same frame of
`full.trace`. gltrim bugs fail silently - the classic symptom is a trimmed
trace whose static world renders fine while everything driven by less common
call patterns (DSA texture binds, `glBindBuffersRange` multi-bind setup,
compute-written buffers) is missing or garbled, often with "invalid buffer
name"-style retrace warnings. If content is missing from the trimmed trace
but present in the full trace, the fix belongs in `3rdparty/apitrace`'s
frametrim, not in the fixture.
## Generate golden
Generate frame snapshots from the trimmed trace, then choose the snapshot that
@@ -118,6 +183,16 @@ mkdir -p "$WORK/$CASE/golden"
"$WORK/$CASE/trace.trace"
```
For a single-frame trim the target is simply the trimmed trace's final swap;
finding it and snapshotting just that call is much faster than dumping every
frame:
```sh
"$APITRACE" dump "$WORK/$CASE/trace.trace" | grep SwapBuffers | tail -n 1
"$GLRETRACE" -s "$WORK/$CASE/golden/$CASE." -S <final-swap-call> \
"$WORK/$CASE/trace.trace"
```
Set `TARGET_CALL` to the call number in the chosen trimmed-trace snapshot
filename:
@@ -147,20 +222,34 @@ Or copy directly:
cp "$GOLDEN_SRC" "$WORK/$CASE/$CASE.$(printf '%010d' "$TARGET_CALL").png"
```
Package:
Verify the golden CONTENT against a reference (a screenshot of the live run,
or the same scene on a known-good backend), not just that a file exists. The
subject must be present, correctly shaped, and framed as intended - a golden
captured through a drifted camera or a half-loaded scene will happily pass
authoring and then permanently enshrine the wrong image.
Package. Compress the trace itself with `repack --brotli` first - it shrinks
a gzip-resistant trace by an order of magnitude (a ~70 MB single-frame
Minecraft trim lands around 7 MB) and glretrace reads it directly:
```sh
"$APITRACE" repack --brotli "$WORK/$CASE/trace.trace" "$WORK/$CASE/trace-brotli.trace"
mkdir -p "$WORK/$CASE/archive"
cp "$WORK/$CASE/trace.trace" "$WORK/$CASE/archive/trace.trace"
cp "$WORK/$CASE/trace-brotli.trace" "$WORK/$CASE/archive/trace.trace"
tar -czf "$REPO/tools/trace_replay/fixtures/$CASE.tgz" \
-C "$WORK/$CASE/archive" trace.trace
cp "$WORK/$CASE/$CASE.$(printf '%010d' "$TARGET_CALL").png" \
"$REPO/tools/trace_replay/fixtures/"
```
If the case is ever re-trimmed, REDO the repack and the archive: a `.tgz`
whose repack predates the latest trim silently packages the stale trace, and
the mismatch only surfaces later as "did not create expected snapshot" when
the registered target call no longer exists.
Check the final archive size. The committed fixture archive should be less than
20 MiB, and should preferably be less than 10 MiB. If it is larger, recapture
with a shorter run or trim a smaller frame-only fixture instead of adding
with a shorter run or a lower frame rate / render distance instead of adding
call-based filtering.
```sh
@@ -178,43 +267,29 @@ git add .gitattributes tools/trace_replay/fixtures/$CASE.tgz \
git lfs status
```
## Register Linux replay
## Register the case
Edit `tools/trace_replay/CMakeLists.txt`:
Both Linux ctest and the Android CI matrix are generated from the single
registry `tools/trace_replay/trace_cases.json` (via
`tools/trace_replay/trace_cases.py`); there is nothing to edit in
`CMakeLists.txt` or `apk.yml`. Append one object to `cases`:
```cmake
add_trace_replay_test_for_backends(case-name
TRACE_ARCHIVE ${MOBILEGL_TRACE_ROOT}/fixtures/case-name.tgz
TRACE_FILE trace.trace
GOLDEN ${MOBILEGL_TRACE_ROOT}/fixtures/case-name.0000000000.png
TARGET_CALL 0
WIDTH 854
HEIGHT 480
SSIM_THRESHOLD 0.99)
```json
{
"name": "case-name",
"trace_archive": "case-name.tgz",
"golden": "case-name.0000000000.png",
"target_call": 0,
"timeout_seconds": 900
}
```
Optional crop:
```cmake
CROP_X 1
CROP_Y 1
CROP_WIDTH 852
CROP_HEIGHT 478
```
## Register Android replay
Edit `.github/workflows/apk.yml` `TRACE_REPLAY_CASES`:
```text
case-name|tools/trace_replay/fixtures/case-name.tgz|trace.trace|tools/trace_replay/fixtures/case-name.0000000000.png|0|854|480|0.99|0|0|0|0|900
```
Field order:
```text
case|trace archive|trace file|golden|target call|width|height|ssim threshold|crop x|crop y|crop width|crop height|timeout seconds
```
Values matching the `defaults` block (854x480, `trace.trace`, ssim 0.99, zero
crop, 900 s timeout) may be omitted. Available per-case keys: `name`,
`trace_archive`, `trace_file`, `golden`, `alternate_golden`, `target_call`,
`width`, `height`, `ssim_threshold`, `crop_x/y/width/height`,
`timeout_seconds`, `ci`. Long single-frame replays of heavy in-world scenes
need a raised `timeout_seconds` (the Create fixtures use 1800).
Update `tools/trace_replay/README.md` with one fixture sentence and one golden
image link.
@@ -291,6 +366,32 @@ gradle --no-daemon -p "$REPO/android-plugin" \
--parallel
```
Release APKs are only signed when `SIGNING_STORE_PASSWORD`,
`SIGNING_KEY_ALIAS`, and `SIGNING_KEY_PASSWORD` are set and
`android-plugin/keystore.jks` exists - an unsigned build still "succeeds" but
installs fail later with `INSTALL_PARSE_FAILED_NO_CERTIFICATES`. If the
device or emulator has a trace package from a different keystore, uninstall
`top.mobilegl.plugin.espryt.trace` / `top.mobilegl.plugin.magma.trace` first
or the install fails with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
Match the CI environment (`.github/workflows/apk.yml` matrix): the emulator
boots with `--gpu software` + `MOBILEGL_RETRACE_USE_ANGLE=1` for `DirectGLES`
and `--gpu lavapipe` + `MOBILEGL_VULKAN_R11G11B10F_FALLBACK=1` for
`DirectVulkan`. The emulator's ANGLE-on-Vulkan GLES stack exercises genuinely
different driver semantics than physical devices (e.g. indirect-draw
`gl_InstanceID` handling), so treat AVD-only image mismatches as real signal,
not emulator noise.
Known emulator flake: the FIRST DirectVulkan replay after a fresh lavapipe
AVD boot segfaults intermittently (~50%), for any trace; subsequent runs in
the same boot are stable. Burn a warm-up replay and discard its result before
the measured runs.
Result directories keep `result.json` from previous runs - delete the case's
result directory before each run, or an earlier failure/success can masquerade
as the current one (identical-to-the-last-digit ssim across "different" runs
is the tell).
Run DirectGLES:
```sh
@@ -331,10 +432,14 @@ Inspect:
## Checklist
- Golden content is verified against a live-run reference (subject present,
correct shapes, intended camera framing).
- Golden matches the committed trace and target call.
- Archive contains only `trace.trace`.
- Both Linux backends pass before Linux CI registration.
- Both Android backends pass before APK CI registration.
- Archive contains only `trace.trace`, and that file is the brotli repack of
the CURRENT trim.
- Both Linux backends pass before registration in `trace_cases.json`.
- Both Android backends pass on a CI-equivalent AVD (software/ANGLE +
lavapipe) before relying on APK CI.
- `actual.png` and `case-diff.png` are inspected.
- Fixture `.tgz` and `.png` files are tracked by Git LFS.
- No build output, extracted trace directory, temporary report, or debug text is