[Refactor] (TraceReplay): reorganize trace skills into uniform packages with bundled scripts

This commit is contained in:
2026-07-19 07:00:56 -04:00
parent f3def150e7
commit 1963b427db
18 changed files with 849 additions and 9 deletions
@@ -1,4 +0,0 @@
interface:
display_name: "Capture RenderDoc Trace Frame"
short_description: "Capture exact Android Vulkan/GLES trace frames"
default_prompt: "Use $renderdoc-capture-trace-frame to capture and validate an exact Android retrace frame."
+20
View File
@@ -0,0 +1,20 @@
# MobileGL trace-replay skills
Task-focused skills for capturing, replaying, debugging, and authoring MobileGL
apitrace fixtures. Each skill is a self-contained package:
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
directory name equals the frontmatter `name`.
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
`short_description`, `default_prompt`).
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
skill has them.
## Skills
| Skill | What it does |
| --- | --- |
| [trace-fixture-authoring-on-android-fcl](trace-fixture-authoring-on-android-fcl/SKILL.md) | Capture an on-device Android apitrace from FCL's MobileGL renderers (DirectGLES / Magma / SimpleFPEWrapper), mark the defect frame, and pull `full.trace`. |
| [renderdoc-debug-on-trace-replay](renderdoc-debug-on-trace-replay/SKILL.md) | Capture and validate an exact frame from a MobileGL retrace on a connected Android device with RenderDoc / rdc-cli. |
| [mismatch-retrace-debugging](mismatch-retrace-debugging/SKILL.md) | Localize the first divergent render pass and draw call when a fixture replays correctly in a golden environment but renders differently on a target backend. |
| [trace-fixture-authoring](trace-fixture-authoring/SKILL.md) | Author a deterministic trace-replay fixture — trim, golden, package under the size budget, register in `trace_cases.json`, and validate on Linux and Android. |
@@ -1,3 +1,8 @@
---
name: mismatch-retrace-debugging
description: Localize the first divergent render pass and draw call when a MobileGL apitrace fixture replays correctly in a golden environment but renders differently under mobilegl_trace_replay, Android trace replay, or another backend. Use to binary-search pass/draw endpoints, diff GL state around the first bad call, and classify the fault as a vertex/VS, fragment/FS, or framebuffer/composition mismatch.
---
# Mismatch retrace debugging
Use this when an apitrace fixture replays correctly on one environment but
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL Mismatch Retrace Debugging"
short_description: "Localize the first divergent draw in a mismatching MobileGL retrace"
default_prompt: "Use $mismatch-retrace-debugging to find the first divergent render pass and draw call in a MobileGL retrace mismatch."
@@ -1,5 +1,5 @@
---
name: renderdoc-capture-trace-frame
name: renderdoc-debug-on-trace-replay
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
---
@@ -19,20 +19,20 @@ adb -s SERIAL shell pm path top.mobilegl.plugin.trace
rdc doctor
```
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/capture_android_retrace.py`.
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py`.
## Capture
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
```powershell
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
```
Change only the backend and output for GLES:
```powershell
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
```
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
@@ -0,0 +1,4 @@
interface:
display_name: "RenderDoc Debug on Trace Replay"
short_description: "Capture and debug an exact MobileGL trace-replay frame in RenderDoc"
default_prompt: "Use $renderdoc-debug-on-trace-replay to capture and validate an exact Android trace-replay frame in RenderDoc."
@@ -2,7 +2,7 @@
## TargetControl timing
- Start `tools/trace_replay/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
- Start `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
@@ -0,0 +1,137 @@
---
name: trace-fixture-authoring-on-android-fcl
description: Capture an on-device Android apitrace from FCL's MobileGL renderers. Use when preparing a reproducible MobileGL DirectGLES, Magma (DirectVulkan), or SimpleFPEWrapper rendering trace, marking the frame of a visual defect, pulling the resulting full.trace, or turning a device capture into a replay fixture.
---
# MobileGL Android trace capture
## Overview
Use FCL's Android `egltrace.so` wrapper, not Perfetto. When enabled before
launch, it records the complete EGL/GL call stream to `full.trace`. The game's
**MobileGL Trace → Capture** control marks the next swap frame in
`capture-result.json`; it does not start or stop recording and does not produce
a one-frame trace by itself.
Run commands from the FoldCraftLauncher repository root:
```sh
export REPO="$PWD"
export CAPTURE="$REPO/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts"
export SERIAL=<adb-device-serial> # omit --serial only if exactly one device is attached
```
The capture scripts are bundled inside this skill under `scripts/`; they
auto-detect the FoldCraftLauncher repository root from their own location, so
`--repo` only needs to be passed for a non-standard checkout layout.
## Prerequisites
- Use an FCL build containing `MobileGLTraceCapture` and the in-game Capture
menu entry.
- Select one of these renderers: MobileGL (DirectGLES), MobileGL Magma
(DirectVulkan), or SimpleFPEWrapper. MobileGlues is not supported by this
capture wrapper.
- Install `adb` and make it available on `PATH`; authorize USB debugging.
- Build the wrapper with Android NDK, CMake, Ninja, Python 3, and the checked
out in-tree `MobileGL/3rdparty/apitrace` submodule.
Confirm the attached device and ABI before building. The wrapper ABI must match
the device process ABI.
```sh
adb devices -l
adb -s "$SERIAL" shell getprop ro.product.cpu.abi
```
Use `arm64-v8a` for the usual `arm64-v8a` result; use the matching NDK ABI for
other devices.
## Build and install the wrapper
Build once per ABI or after changing apitrace/wrapper sources:
```sh
python3 "$CAPTURE/build_android_egltrace.py" --abi arm64-v8a
```
This generates `egltrace.so` under the skill's `scripts/out/` directory. Push
it and write FCL's enable sentinel before launching the game:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" install-wrapper
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" enable
```
The device-side control directory is `/sdcard/FCL/mobilegl-trace`. FCL copies
the shared `egltrace.so` into its private files directory at launch, replaces
the renderer's EGL library with it, and forwards to the real MobileGL library.
## Capture a reproduction
1. Start FCL after the wrapper and enable sentinel are in place. Select the
intended supported MobileGL renderer and launch the game.
2. Trace mode forces the game to `854x480`; account for that when reproducing
and comparing output.
3. Reproduce the issue. Start close to the target scene because tracing starts
when the game launches and trace files can grow rapidly.
4. At the desired visual state, open FCL's right-side game menu and press
**MobileGL Trace → Capture**. Let at least one frame present afterward.
5. Exit the game cleanly, then pull the latest session:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" pull-latest
```
The default local result is:
```text
.trace-work/pulled-mobilegl-captures/capture-YYYYMMDD-HHMMSS-<renderer>/
full.trace
capture-status.json
capture-result.json
```
`capture-result.json` must show `"status": "captured"`. Its `targetFrame`
is the one-based swap count used by `gltrim`; `zeroBasedFrame` is included for
tools that use zero-based indexing.
## Diagnose setup failures
Inspect the active device session directly:
```sh
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/latest-session.txt
adb -s "$SERIAL" shell ls -lh /sdcard/FCL/mobilegl-trace
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-status.json
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-result.json
```
If `capture-status.json` reports a missing `egltrace.so`, rebuild/push the
correct ABI and relaunch. If `capture-result.json` is absent, Capture was
pressed without an active trace session, or no subsequent `eglSwapBuffers`
occurred. The menu button itself only writes `capture-once.request`.
Disable tracing when finished; otherwise the next supported MobileGL launch
will trace again:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" disable
```
## Create a replay fixture (optional)
Keep the raw `full.trace` until replay validation succeeds. To frame-trim and
package the marked frame for MobileGL trace replay, use the existing helper:
```sh
python3 "$CAPTURE/package_capture_fixture.py" \
--serial "$SERIAL" \
--case <case-name> \
--apitrace <path-to-in-tree-apitrace>
```
It pulls the latest capture if necessary, uses `capture-result.json` to select
the frame, runs `apitrace gltrim`, creates a golden image, and enforces the
fixture archive-size limit. Follow `../trace-fixture-authoring/SKILL.md` for
deterministic scene setup, verification, and registry changes.
@@ -0,0 +1,4 @@
interface:
display_name: "Trace Fixture Authoring on Android (FCL)"
short_description: "Capture and package a MobileGL trace fixture on Android FCL"
default_prompt: "Use $trace-fixture-authoring-on-android-fcl to capture a MobileGL trace on my Android device and package it into a replay fixture."
@@ -0,0 +1,3 @@
# Build output produced by build_android_egltrace.py (ABI-specific, regenerated).
out/
__pycache__/
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import argparse
import subprocess
from pathlib import Path
REMOTE_ROOT = "/sdcard/FCL/mobilegl-trace"
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_WRAPPER = SCRIPT_DIR / "out" / "egltrace.so"
def adb(serial, args):
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
cmd += args
print("+", " ".join(cmd), flush=True)
subprocess.run(cmd, check=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--serial")
sub = parser.add_subparsers(dest="command", required=True)
install = sub.add_parser("install-wrapper")
install.add_argument("--wrapper", default=str(DEFAULT_WRAPPER))
sub.add_parser("enable")
sub.add_parser("disable")
sub.add_parser("capture-once")
pull = sub.add_parser("pull-latest")
pull.add_argument("--output", default=".trace-work/pulled-mobilegl-captures")
args = parser.parse_args()
serial = args.serial
if args.command == "install-wrapper":
wrapper = Path(args.wrapper)
if not wrapper.exists():
raise SystemExit(f"missing wrapper: {wrapper}")
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["push", str(wrapper), f"{REMOTE_ROOT}/egltrace.so"])
elif args.command == "enable":
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["shell", f"printf enabled > {REMOTE_ROOT}/enable"])
elif args.command == "disable":
adb(serial, ["shell", "rm", "-f", f"{REMOTE_ROOT}/enable"])
elif args.command == "capture-once":
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["shell", f"date +%s%3N > {REMOTE_ROOT}/capture-once.request"])
elif args.command == "pull-latest":
tmp = subprocess.check_output((["adb"] + (["-s", serial] if serial else []) +
["shell", "cat", f"{REMOTE_ROOT}/latest-session.txt"]),
text=True, encoding="utf-8", errors="replace").strip()
if not tmp:
raise SystemExit("no latest-session.txt on device")
output = Path(args.output)
output.mkdir(parents=True, exist_ok=True)
adb(serial, ["pull", tmp, str(output / Path(tmp).name)])
if __name__ == "__main__":
main()
@@ -0,0 +1,191 @@
cmake_minimum_required(VERSION 3.22.1)
project(mobilegl_android_egltrace)
set(APITRACE_ROOT "" CACHE PATH "Path to apitrace source tree")
set(PATCHED_EGLTRACE_CPP "" CACHE FILEPATH "Generated and patched egltrace.cpp")
set(PATCHED_GLPROC_EGL_CPP "" CACHE FILEPATH "Patched glproc_egl.cpp")
if(NOT EXISTS "${APITRACE_ROOT}/wrappers")
message(FATAL_ERROR "APITRACE_ROOT must point to apitrace")
endif()
if(NOT EXISTS "${PATCHED_EGLTRACE_CPP}")
message(FATAL_ERROR "PATCHED_EGLTRACE_CPP is required")
endif()
if(NOT EXISTS "${PATCHED_GLPROC_EGL_CPP}")
message(FATAL_ERROR "PATCHED_GLPROC_EGL_CPP is required")
endif()
set(APITRACE_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/apitrace")
set(APITRACE_VERSION "mobilegl-capture")
find_package(Python3 REQUIRED)
find_package(Threads REQUIRED)
include("${APITRACE_ROOT}/cmake/ConvenienceLibrary.cmake")
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_STATIC_SNAPPY ON CACHE BOOL "" FORCE)
set(DOC_INSTALL_DIR "doc" CACHE PATH "" FORCE)
set(HAVE_X86 OFF CACHE BOOL "" FORCE)
set(ZLIB_FOUND OFF CACHE BOOL "" FORCE)
set(PNG_FOUND OFF CACHE BOOL "" FORCE)
set(Snappy_FOUND OFF CACHE BOOL "" FORCE)
set(BROTLIDEC_FOUND OFF CACHE BOOL "" FORCE)
set(BROTLIENC_FOUND OFF CACHE BOOL "" FORCE)
set(ZSTD_FOUND OFF CACHE BOOL "" FORCE)
set(CMAKE_EXECUTABLE_FORMAT "MobileGLAndroid" CACHE INTERNAL "" FORCE)
add_custom_target(check)
add_subdirectory("${APITRACE_ROOT}/thirdparty" "${APITRACE_BINARY_DIR}/thirdparty")
set(APITRACE_GENERATED_DIR "${APITRACE_BINARY_DIR}/generated")
file(MAKE_DIRECTORY "${APITRACE_GENERATED_DIR}")
configure_file("${APITRACE_ROOT}/version.h.in" "${APITRACE_GENERATED_DIR}/version.h" @ONLY)
add_custom_command(
OUTPUT
"${APITRACE_GENERATED_DIR}/glproc.hpp"
"${APITRACE_GENERATED_DIR}/glproc.cpp"
COMMAND ${Python3_EXECUTABLE}
"${APITRACE_ROOT}/dispatch/glproc.py"
"${APITRACE_GENERATED_DIR}/glproc.hpp"
"${APITRACE_GENERATED_DIR}/glproc.cpp"
DEPENDS
"${APITRACE_ROOT}/dispatch/glproc.py"
"${APITRACE_ROOT}/dispatch/dispatch.py"
"${APITRACE_ROOT}/specs/wglapi.py"
"${APITRACE_ROOT}/specs/glxapi.py"
"${APITRACE_ROOT}/specs/cglapi.py"
"${APITRACE_ROOT}/specs/eglapi.py"
"${APITRACE_ROOT}/specs/glapi.py"
"${APITRACE_ROOT}/specs/gltypes.py"
"${APITRACE_ROOT}/specs/stdapi.py")
add_library(apitrace_os STATIC
"${APITRACE_ROOT}/lib/os/os_backtrace.cpp"
"${APITRACE_ROOT}/lib/os/os_crtdbg.cpp"
"${APITRACE_ROOT}/lib/os/os_posix.cpp")
target_include_directories(apitrace_os PUBLIC
"${APITRACE_ROOT}/compat"
"${APITRACE_ROOT}/thirdparty"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace")
target_link_libraries(apitrace_os PUBLIC Threads::Threads)
add_library(glproc STATIC
"${APITRACE_GENERATED_DIR}/glproc.cpp"
"${PATCHED_GLPROC_EGL_CPP}")
target_include_directories(glproc PUBLIC
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/wrappers"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(glproc PUBLIC apitrace_os dl)
add_library(highlight STATIC "${APITRACE_ROOT}/lib/highlight/highlight.cpp")
target_include_directories(highlight PUBLIC "${APITRACE_ROOT}/lib/highlight")
add_library(guids STATIC "${APITRACE_ROOT}/lib/guids/guids.cpp")
target_include_directories(guids PUBLIC
"${APITRACE_ROOT}/lib/guids"
"${APITRACE_ROOT}/lib/os")
add_library(common STATIC
"${APITRACE_ROOT}/lib/trace/trace_callset.cpp"
"${APITRACE_ROOT}/lib/trace/trace_dump.cpp"
"${APITRACE_ROOT}/lib/trace/trace_fast_callset.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_read.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zlib.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_brotli.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_snappy.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zstd.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zstd_seekable.cpp"
"${APITRACE_ROOT}/lib/trace/trace_model.cpp"
"${APITRACE_ROOT}/lib/trace/trace_option.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_snappy.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_zlib.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_zstd.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser_flags.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser_loop.cpp"
"${APITRACE_ROOT}/lib/trace/trace_profiler.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer_local.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer_model.cpp")
target_include_directories(common PUBLIC
"${APITRACE_ROOT}/compat"
"${APITRACE_ROOT}/thirdparty"
"${APITRACE_ROOT}/lib/guids"
"${APITRACE_ROOT}/lib/highlight"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace"
"${APITRACE_ROOT}/lib/ubjson")
target_link_libraries(common PUBLIC
guids
highlight
apitrace_os
Snappy::snappy
ZLIB::ZLIB
PkgConfig::BROTLIDEC
PkgConfig::ZSTD
zstd_seekable)
add_convenience_library(trace
"${APITRACE_ROOT}/wrappers/memtrace.hpp"
"${APITRACE_ROOT}/wrappers/memtrace.cpp")
target_include_directories(trace PUBLIC
"${APITRACE_ROOT}/thirdparty/crc32c")
target_link_libraries(trace
common
guids
crc32c)
add_library(glhelpers STATIC
"${APITRACE_ROOT}/helpers/glfeatures.cpp"
"${APITRACE_ROOT}/helpers/eglsize.cpp")
target_include_directories(glhelpers PUBLIC
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(glhelpers PUBLIC glproc apitrace_os)
add_convenience_library(gltrace_common
"${APITRACE_ROOT}/wrappers/glcaps.cpp"
"${APITRACE_ROOT}/wrappers/config.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_arrays.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_state.cpp"
"${APITRACE_ROOT}/wrappers/glmemshadow.hpp"
"${APITRACE_ROOT}/wrappers/glmemshadow.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.hpp"
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.cpp")
add_dependencies(gltrace_common glproc)
target_include_directories(gltrace_common PUBLIC
"${APITRACE_ROOT}/wrappers")
target_link_libraries(gltrace_common
glhelpers
trace)
add_library(egltrace SHARED
"${PATCHED_EGLTRACE_CPP}"
"${APITRACE_ROOT}/wrappers/dlsym.cpp"
"${PATCHED_GLPROC_EGL_CPP}")
add_dependencies(egltrace glproc)
set_target_properties(egltrace PROPERTIES PREFIX "")
target_compile_definitions(egltrace PRIVATE -DEGLTRACE=1)
target_include_directories(egltrace PRIVATE
"${APITRACE_ROOT}/wrappers"
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(egltrace
gltrace_common
glproc
Threads::Threads
dl)
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
# This script is bundled inside the trace-fixture-authoring-on-android-fcl skill at
# <FCL>/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
DEFAULT_OUTPUT = SCRIPT_DIR / "out" / "egltrace.so"
def run(cmd, cwd=None):
print("+", " ".join(str(part) for part in cmd), flush=True)
subprocess.run(cmd, cwd=cwd, check=True)
def find_ndk(repo):
for key in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT"):
value = os.environ.get(key)
if value:
return Path(value)
for key in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
value = os.environ.get(key)
if value:
ndk_root = Path(value) / "ndk"
if ndk_root.exists():
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
if versions:
return versions[-1]
local = repo / "local.properties"
if local.exists():
sdk = None
ndk = None
for line in local.read_text(encoding="utf-8", errors="ignore").splitlines():
if line.startswith("sdk.dir="):
sdk = Path(line.split("=", 1)[1].replace("\\:", ":"))
if line.startswith("ndk.dir="):
ndk = Path(line.split("=", 1)[1].replace("\\:", ":"))
if ndk:
return ndk
if sdk:
ndk_root = sdk / "ndk"
if ndk_root.exists():
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
if versions:
return versions[-1]
raise SystemExit("Android NDK not found; set ANDROID_NDK_HOME or local.properties sdk.dir/ndk.dir")
def generate_and_patch(repo, build_dir):
wrapper_dir = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers"
generated = build_dir / "patched" / "egltrace.cpp"
generated.parent.mkdir(parents=True, exist_ok=True)
with generated.open("w", encoding="utf-8", newline="\n") as out:
subprocess.run([sys.executable, str(wrapper_dir / "egltrace.py")], cwd=wrapper_dir, stdout=out, check=True)
text = generated.read_text(encoding="utf-8")
helper = r'''
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
static unsigned long long mobilegl_capture_swap_count = 0;
static long long mobilegl_capture_time_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (long long) ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
}
static int mobilegl_capture_exists(const char *path) {
return path != NULL && path[0] != '\0' && access(path, F_OK) == 0;
}
static void mobilegl_capture_record_request(void) {
++mobilegl_capture_swap_count;
const char *request = getenv("MOBILEGL_TRACE_CAPTURE_REQUEST_FILE");
const char *output = getenv("MOBILEGL_TRACE_CAPTURE_FRAME_FILE");
if (!mobilegl_capture_exists(request) || output == NULL || output[0] == '\0') {
return;
}
unlink(request);
FILE *file = fopen(output, "w");
if (file == NULL) {
return;
}
const char *trace_file = getenv("TRACE_FILE");
fprintf(file,
"{\n"
" \"status\": \"captured\",\n"
" \"swapCount\": %llu,\n"
" \"targetFrame\": %llu,\n"
" \"zeroBasedFrame\": %llu,\n"
" \"capturedAtMs\": %lld,\n"
" \"traceFile\": \"%s\"\n"
"}\n",
mobilegl_capture_swap_count,
mobilegl_capture_swap_count,
mobilegl_capture_swap_count == 0 ? 0 : mobilegl_capture_swap_count - 1,
mobilegl_capture_time_ms(),
trace_file == NULL ? "" : trace_file);
fclose(file);
}
'''
insert_at = text.find("#include")
if insert_at < 0:
raise SystemExit("generated egltrace.cpp has no include block")
next_block = text.find("\n\n", insert_at)
text = text[:next_block] + "\n" + helper + text[next_block:]
needle = "EGLBoolean EGLAPIENTRY eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)"
start = text.find(needle)
if start < 0:
raise SystemExit("generated egltrace.cpp has no eglSwapBuffers wrapper to patch")
brace = text.find("{", start)
if brace < 0:
raise SystemExit("eglSwapBuffers wrapper has no function body")
text = text[:brace + 1] + "\n mobilegl_capture_record_request();" + text[brace + 1:]
generated.write_text(text, encoding="utf-8", newline="\n")
return generated
def patch_glproc_egl(repo, build_dir):
source = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers" / "glproc_egl.cpp"
patched = build_dir / "patched" / "glproc_egl.cpp"
patched.parent.mkdir(parents=True, exist_ok=True)
text = source.read_text(encoding="utf-8")
text = text.replace('#include "dlopen.hpp"\n', '#include "dlopen.hpp"\n#include <stdlib.h>\n')
needle = """void *
_getPublicProcAddress(const char *procName)
{
void *proc;
"""
replacement = """void *
_getPublicProcAddress(const char *procName)
{
void *proc;
static void *traceLibGL = NULL;
static bool triedTraceLibGL = false;
if (!triedTraceLibGL) {
triedTraceLibGL = true;
const char *traceLibGLName = getenv("TRACE_LIBGL");
if (traceLibGLName && traceLibGLName[0]) {
traceLibGL = _dlopen(traceLibGLName, RTLD_GLOBAL | RTLD_LAZY | RTLD_DEEPBIND);
}
}
if (traceLibGL) {
proc = dlsym(traceLibGL, procName);
if (proc) {
return proc;
}
}
"""
if needle not in text:
raise SystemExit("glproc_egl.cpp patch point not found")
text = text.replace(needle, replacement, 1)
patched.write_text(text, encoding="utf-8", newline="\n")
return patched
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
parser.add_argument("--abi", default="arm64-v8a")
parser.add_argument("--android-platform", default="android-23")
parser.add_argument("--build-dir", default=".trace-work/build-android-egltrace")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
args = parser.parse_args()
repo = Path(args.repo).resolve()
ndk = find_ndk(repo)
build_dir = (repo / args.build_dir / args.abi).resolve()
apitrace = repo / "MobileGL" / "3rdparty" / "apitrace"
source_dir = SCRIPT_DIR / "android_egltrace"
toolchain = ndk / "build" / "cmake" / "android.toolchain.cmake"
if not apitrace.exists():
raise SystemExit(f"missing apitrace checkout: {apitrace}")
if not source_dir.exists():
raise SystemExit(f"missing wrapper CMake project: {source_dir}")
if not toolchain.exists():
raise SystemExit(f"missing Android toolchain: {toolchain}")
cache = build_dir / "CMakeCache.txt"
if cache.exists() and "CMAKE_GENERATOR:INTERNAL=Ninja" not in cache.read_text(encoding="utf-8", errors="ignore"):
shutil.rmtree(build_dir)
elif cache.exists() and str(source_dir).replace("\\", "/") not in cache.read_text(encoding="utf-8", errors="ignore").replace("\\", "/"):
shutil.rmtree(build_dir)
ninja = shutil.which("ninja")
if ninja is None:
cmake_ninjas = sorted((Path(os.environ.get("ANDROID_HOME", "")) / "cmake").glob("*/bin/ninja.exe"))
ninja = str(cmake_ninjas[-1]) if cmake_ninjas else None
if ninja is None:
raise SystemExit("ninja not found; install Ninja or Android SDK CMake")
patched_egltrace = generate_and_patch(repo, build_dir)
patched_glproc_egl = patch_glproc_egl(repo, build_dir)
run([
"cmake", "-G", "Ninja", "-S", str(source_dir), "-B", str(build_dir),
"-DCMAKE_BUILD_TYPE=Release",
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
f"-DCMAKE_MAKE_PROGRAM={ninja}",
f"-DANDROID_ABI={args.abi}",
f"-DANDROID_PLATFORM={args.android_platform}",
f"-DAPITRACE_ROOT={apitrace}",
f"-DPATCHED_EGLTRACE_CPP={patched_egltrace}",
f"-DPATCHED_GLPROC_EGL_CPP={patched_glproc_egl}",
])
run(["cmake", "--build", str(build_dir), "--target", "egltrace", "--parallel"])
output = Path(args.output)
if not output.is_absolute():
output = repo / output
output = output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
candidates = list(build_dir.rglob("egltrace.so"))
if not candidates:
raise SystemExit("egltrace.so was not produced")
shutil.copy2(candidates[0], output)
print(output)
if __name__ == "__main__":
main()
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
import argparse
import json
import re
import shutil
import subprocess
import tarfile
from pathlib import Path
DEFAULT_MAX_ARCHIVE_BYTES = 20 * 1024 * 1024
# Bundled under the skill at .../skills/trace-fixture-authoring-on-android-fcl/scripts/.
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
def run(cmd, cwd=None, capture=False):
print("+", " ".join(str(part) for part in cmd), flush=True)
if capture:
return subprocess.check_output(cmd, cwd=cwd, text=True, encoding="utf-8", errors="replace")
subprocess.run(cmd, cwd=cwd, check=True)
return ""
def adb(args, serial=None):
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
cmd += args
return run(cmd, capture=True)
def pull_latest(serial, dest):
latest = adb(["shell", "cat", "/sdcard/FCL/mobilegl-trace/latest-session.txt"], serial).strip()
if not latest:
raise SystemExit("device has no /sdcard/FCL/mobilegl-trace/latest-session.txt")
dest.mkdir(parents=True, exist_ok=True)
local = dest / Path(latest).name
if local.exists():
shutil.rmtree(local)
adb(["pull", latest, str(local)], serial)
return local
def choose_target_frame(capture_dir, explicit_frame):
if explicit_frame is not None:
return explicit_frame
result = capture_dir / "capture-result.json"
if not result.exists():
raise SystemExit(f"missing {result}; press the FCL capture button or create capture-once.request first")
data = json.loads(result.read_text(encoding="utf-8"))
if "targetFrame" not in data:
raise SystemExit(f"{result} has no targetFrame")
return int(data["targetFrame"])
def choose_snapshot(golden_dir):
pngs = sorted(golden_dir.glob("*.png"))
if not pngs:
raise SystemExit(f"no snapshots produced in {golden_dir}")
def call_no(path):
match = re.search(r"\.(\d+)\.png$", path.name)
return int(match.group(1)) if match else -1
return max(pngs, key=call_no)
def choose_target_call(explicit_call, golden):
if explicit_call is not None:
return explicit_call
if golden is None:
return None
match = re.search(r"\.(\d+)\.png$", golden.name)
return int(match.group(1)) if match else None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
parser.add_argument("--serial", help="adb serial; when set, pull latest capture from device")
parser.add_argument("--capture-dir", help="local capture directory; defaults to pulled latest")
parser.add_argument("--pull-root", default=".trace-work/pulled-mobilegl-captures")
parser.add_argument("--case", required=True)
parser.add_argument("--target-frame", type=int)
parser.add_argument("--target-call", type=int)
parser.add_argument("--golden", help="existing golden PNG, normally produced by Android replay")
parser.add_argument("--skip-desktop-golden", action="store_true",
help="skip apitrace replay --headless; requires --golden and --target-call")
parser.add_argument("--apitrace", default="apitrace")
parser.add_argument("--fixtures-dir", default="MobileGL/tools/trace_replay/fixtures")
parser.add_argument("--width", type=int, default=854)
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--ssim-threshold", default="0.99")
parser.add_argument("--max-archive-bytes", type=int, default=DEFAULT_MAX_ARCHIVE_BYTES)
args = parser.parse_args()
repo = Path(args.repo).resolve()
if args.capture_dir:
capture_dir = Path(args.capture_dir).resolve()
elif args.serial:
capture_dir = pull_latest(args.serial, repo / args.pull_root)
else:
raise SystemExit("pass --capture-dir or --serial")
full_trace = capture_dir / "full.trace"
if not full_trace.exists():
raise SystemExit(f"missing trace: {full_trace}")
target_frame = choose_target_frame(capture_dir, args.target_frame)
work = capture_dir / "fixture-work"
if work.exists():
shutil.rmtree(work)
work.mkdir(parents=True)
frames_txt = work / "frames.txt"
frames_txt.write_text(run([args.apitrace, "dump", "--calls=frame", str(full_trace)], capture=True), encoding="utf-8")
trimmed = work / "trace.trace"
run([args.apitrace, "gltrim", "-f", str(target_frame), "--output", str(trimmed), str(full_trace)])
supplied_golden = Path(args.golden).resolve() if args.golden else None
target_call = choose_target_call(args.target_call, supplied_golden)
if args.skip_desktop_golden:
if supplied_golden is None or target_call is None:
raise SystemExit("--skip-desktop-golden requires --golden and --target-call")
golden = supplied_golden
else:
golden_dir = work / "golden"
golden_dir.mkdir()
prefix = golden_dir / f"{args.case}."
run([args.apitrace, "replay", "--headless", "--snapshot-prefix", str(prefix), "--call-nos", str(trimmed)])
golden = choose_snapshot(golden_dir)
target_call = choose_target_call(args.target_call, golden)
if target_call is None:
raise SystemExit(f"cannot infer target call from {golden}")
fixtures = (repo / args.fixtures_dir).resolve()
fixtures.mkdir(parents=True, exist_ok=True)
archive_root = work / "archive"
archive_root.mkdir()
shutil.copy2(trimmed, archive_root / "trace.trace")
tgz = fixtures / f"{args.case}.tgz"
with tarfile.open(tgz, "w:gz") as tar:
tar.add(archive_root / "trace.trace", arcname="trace.trace")
archive_size = tgz.stat().st_size
if archive_size > args.max_archive_bytes:
raise SystemExit(
f"{tgz} is {archive_size} bytes, over the {args.max_archive_bytes} byte fixture limit; "
"choose an earlier/smaller frame and re-run gltrim"
)
golden_out = fixtures / f"{args.case}.{target_call:010d}.png"
shutil.copy2(golden, golden_out)
manifest = {
"name": args.case,
"trace_archive": tgz.name,
"trace_file": "trace.trace",
"golden": golden_out.name,
"target_call": target_call,
"width": args.width,
"height": args.height,
"ssim_threshold": float(args.ssim_threshold),
"archive_size": archive_size,
}
manifest_path = capture_dir / f"{args.case}.fixture.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(json.dumps(manifest, indent=2))
if __name__ == "__main__":
main()
@@ -1,3 +1,8 @@
---
name: trace-fixture-authoring
description: Author a deterministic MobileGL trace-replay fixture from a captured apitrace - build the in-tree apitrace fork, capture a reproducible scene, frame-trim with gltrim, generate and verify a golden image, package under the archive-size budget, register the case in trace_cases.json, and validate on Linux and Android. Use when adding or re-trimming a trace_replay regression fixture.
---
# Trace fixture authoring
## Variables
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL Trace Fixture Authoring"
short_description: "Author and register a MobileGL trace-replay fixture"
default_prompt: "Use $trace-fixture-authoring to author and register a MobileGL trace replay fixture."