diff --git a/tools/trace_replay/renderdoc_trace_capture/agents/openai.yaml b/tools/trace_replay/renderdoc_trace_capture/agents/openai.yaml deleted file mode 100644 index 71790990..00000000 --- a/tools/trace_replay/renderdoc_trace_capture/agents/openai.yaml +++ /dev/null @@ -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." diff --git a/tools/trace_replay/skills/README.md b/tools/trace_replay/skills/README.md new file mode 100644 index 00000000..026dc795 --- /dev/null +++ b/tools/trace_replay/skills/README.md @@ -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. | diff --git a/tools/trace_replay/skills/mismatch-retrace-debugging.md b/tools/trace_replay/skills/mismatch-retrace-debugging/SKILL.md similarity index 96% rename from tools/trace_replay/skills/mismatch-retrace-debugging.md rename to tools/trace_replay/skills/mismatch-retrace-debugging/SKILL.md index 931344ce..97057f17 100644 --- a/tools/trace_replay/skills/mismatch-retrace-debugging.md +++ b/tools/trace_replay/skills/mismatch-retrace-debugging/SKILL.md @@ -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 diff --git a/tools/trace_replay/skills/mismatch-retrace-debugging/agents/openai.yaml b/tools/trace_replay/skills/mismatch-retrace-debugging/agents/openai.yaml new file mode 100644 index 00000000..6f131578 --- /dev/null +++ b/tools/trace_replay/skills/mismatch-retrace-debugging/agents/openai.yaml @@ -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." diff --git a/tools/trace_replay/renderdoc_trace_capture/SKILL.md b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/SKILL.md similarity index 82% rename from tools/trace_replay/renderdoc_trace_capture/SKILL.md rename to tools/trace_replay/skills/renderdoc-debug-on-trace-replay/SKILL.md index fd2becea..1043cf0a 100644 --- a/tools/trace_replay/renderdoc_trace_capture/SKILL.md +++ b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/SKILL.md @@ -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. diff --git a/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/agents/openai.yaml b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/agents/openai.yaml new file mode 100644 index 00000000..475cc90f --- /dev/null +++ b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/agents/openai.yaml @@ -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." diff --git a/tools/trace_replay/renderdoc_trace_capture/references/android-renderdoc-troubleshooting.md b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/references/android-renderdoc-troubleshooting.md similarity index 94% rename from tools/trace_replay/renderdoc_trace_capture/references/android-renderdoc-troubleshooting.md rename to tools/trace_replay/skills/renderdoc-debug-on-trace-replay/references/android-renderdoc-troubleshooting.md index bfd027dc..5dd8d4f8 100644 --- a/tools/trace_replay/renderdoc_trace_capture/references/android-renderdoc-troubleshooting.md +++ b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/references/android-renderdoc-troubleshooting.md @@ -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. diff --git a/tools/trace_replay/capture_android_retrace.py b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py similarity index 100% rename from tools/trace_replay/capture_android_retrace.py rename to tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py diff --git a/tools/trace_replay/queue_android_frame.py b/tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py similarity index 100% rename from tools/trace_replay/queue_android_frame.py rename to tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/SKILL.md b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/SKILL.md new file mode 100644 index 00000000..96d96cfd --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/SKILL.md @@ -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= # 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-/ + 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 \ + --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. diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/agents/openai.yaml b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/agents/openai.yaml new file mode 100644 index 00000000..27dc2948 --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/agents/openai.yaml @@ -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." diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.gitignore b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.gitignore new file mode 100644 index 00000000..9b58a898 --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.gitignore @@ -0,0 +1,3 @@ +# Build output produced by build_android_egltrace.py (ABI-specific, regenerated). +out/ +__pycache__/ diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/adb_capture.py b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/adb_capture.py new file mode 100644 index 00000000..8ac28fbe --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/adb_capture.py @@ -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() diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/android_egltrace/CMakeLists.txt b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/android_egltrace/CMakeLists.txt new file mode 100644 index 00000000..57ccb4d4 --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/android_egltrace/CMakeLists.txt @@ -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) diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/build_android_egltrace.py b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/build_android_egltrace.py new file mode 100644 index 00000000..c40b020b --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/build_android_egltrace.py @@ -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 +# /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 +#include +#include +#include +#include +#include + +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 \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() diff --git a/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/package_capture_fixture.py b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/package_capture_fixture.py new file mode 100644 index 00000000..9e1c9721 --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/package_capture_fixture.py @@ -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() diff --git a/tools/trace_replay/skills/trace-fixture-authoring.md b/tools/trace_replay/skills/trace-fixture-authoring/SKILL.md similarity index 97% rename from tools/trace_replay/skills/trace-fixture-authoring.md rename to tools/trace_replay/skills/trace-fixture-authoring/SKILL.md index 7d7a817c..4843e8d2 100644 --- a/tools/trace_replay/skills/trace-fixture-authoring.md +++ b/tools/trace_replay/skills/trace-fixture-authoring/SKILL.md @@ -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 diff --git a/tools/trace_replay/skills/trace-fixture-authoring/agents/openai.yaml b/tools/trace_replay/skills/trace-fixture-authoring/agents/openai.yaml new file mode 100644 index 00000000..51c57a3e --- /dev/null +++ b/tools/trace_replay/skills/trace-fixture-authoring/agents/openai.yaml @@ -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."