diff --git a/.github/workflows/retrace-apk.yml b/.github/workflows/retrace-apk.yml new file mode 100644 index 00000000..56646cf7 --- /dev/null +++ b/.github/workflows/retrace-apk.yml @@ -0,0 +1,80 @@ +name: Retrace APK + +on: + push: + branches: + - dev + - Feat/Backend-Direct-GLES + - Feat/Backend-Direct-Vulkan + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set artifact metadata + run: | + echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV" + echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_ENV" + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.10.2 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android NDK + run: | + sdkmanager "ndk;27.3.13750724" + echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties + + - name: Update glslang external sources + working-directory: 3rdparty/glslang + run: python update_glslang_sources.py + + - name: Build retrace APK + run: | + gradle --no-daemon -p android-plugin :app:assembleEsprytTraceRelease + gradle --no-daemon -p android-plugin :app:assembleMagmaTraceRelease + env: + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + + - name: Verify signed APK + run: | + APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -n 1)" + mapfile -t APKS < <(find android-plugin/app/build/outputs/apk -type f -path '*/release/*.apk' | sort) + if [ "${#APKS[@]}" -eq 0 ]; then + echo "::error::No release APKs found under android-plugin/app/build/outputs/apk" + find android-plugin/app/build/outputs/apk -type f -print || true + exit 1 + fi + + for APK in "${APKS[@]}"; do + if [[ "$APK" == *-unsigned.apk ]]; then + echo "::error::Unsigned release APK produced: $APK" + exit 1 + fi + "$APKSIGNER" verify --verbose "$APK" + done + + - name: Upload retrace APK + uses: actions/upload-artifact@v4 + with: + name: MobileGL-retrace-apk-${{ env.date_today }}-${{ env.short_sha }} + path: android-plugin/app/build/outputs/apk/**/release/*.apk diff --git a/.github/workflows/retrace.yml b/.github/workflows/retrace.yml new file mode 100644 index 00000000..c6b24e41 --- /dev/null +++ b/.github/workflows/retrace.yml @@ -0,0 +1,80 @@ +name: Retrace + +on: + push: + branches: + - dev + - Feat/Backend-Direct-GLES + - Feat/Backend-Direct-Vulkan + workflow_dispatch: + +jobs: + retrace: + runs-on: ubuntu-latest + env: + TEST_ROOT: ${{github.workspace}} + + steps: + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 32 + + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: true + + - name: Get CMake + uses: lukka/get-cmake@latest + + - name: Prepare Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.1 + with: + vulkan-query-version: 1.4.304.1 + vulkan-components: Vulkan-Headers, Vulkan-Loader + vulkan-use-cache: true + + - name: Update glslang external sources + working-directory: ${{env.TEST_ROOT}}/3rdparty/glslang + run: python update_glslang_sources.py + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers + + - name: Show installed toolchain + run: | + clang-20 --version + clang++-20 --version + ld.lld-20 --version || ld.lld --version || true + dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' || true + + - name: Configure CMake + working-directory: ${{env.TEST_ROOT}} + run: | + if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then + cmake -S . -B build-retrace -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Debug -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + else + cmake -S . -B build-retrace -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + fi + + - name: Build trace replay + working-directory: ${{env.TEST_ROOT}}/build-retrace + run: cmake --build . --target mobilegl_trace_replay + + - name: OpenRA retrace + working-directory: ${{env.TEST_ROOT}}/build-retrace + run: ctest -V -R MobileGLTraceReplay.OpenRA.DirectGLES + + - name: Upload OpenRA actual + if: always() + uses: actions/upload-artifact@v4 + with: + name: OpenRA-retrace-actual + path: | + build-retrace/tools/trace_replay/openra/output/actual.png + build-retrace/tools/trace_replay/openra/output/result.json + build-retrace/tools/trace_replay/openra/output/retrace.log + if-no-files-found: warn diff --git a/CMakeLists.txt b/CMakeLists.txt index ccf75d3a..df3729ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ option(MOBILEGL_BUILD_TEST "Build MobileGL tests" option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON ) option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON ) option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF) +option(MOBILEGL_BUILD_TRACE_REPLAY "Build Linux apitrace replay runner" OFF) if (ANDROID) set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE) @@ -396,4 +397,9 @@ if (NOT ANDROID) if (MOBILEGL_BUILD_BENCHMARK) add_subdirectory(MobileGL/MG_Benchmark) endif() + + if (MOBILEGL_BUILD_TRACE_REPLAY) + enable_testing() + add_subdirectory(tools/trace_replay) + endif() endif() diff --git a/android-plugin/app/src/trace/cpp/CMakeLists.txt b/android-plugin/app/src/trace/cpp/CMakeLists.txt index 353278ad..52fb8aa9 100644 --- a/android-plugin/app/src/trace/cpp/CMakeLists.txt +++ b/android-plugin/app/src/trace/cpp/CMakeLists.txt @@ -170,7 +170,9 @@ add_library(retrace_common STATIC "${APITRACE_ROOT}/retrace/state_writer_ubjson.cpp" "${APITRACE_ROOT}/retrace/ws.cpp") target_compile_definitions(retrace_common PUBLIC RETRACE) -target_compile_definitions(retrace_common PRIVATE exit=mobilegl_apitrace_exit) +target_compile_definitions(retrace_common PRIVATE + exit=mobilegl_apitrace_exit + main=mobilegl_apitrace_main) target_compile_options(retrace_common PRIVATE "-include${CMAKE_CURRENT_LIST_DIR}/apitrace_exit.hpp") target_include_directories(retrace_common PUBLIC "${APITRACE_GENERATED_DIR}" @@ -216,7 +218,9 @@ target_include_directories(glretrace_common PUBLIC "${APITRACE_ROOT}/dispatch" "${APITRACE_ROOT}/helpers" "${APITRACE_ROOT}/retrace") -target_compile_definitions(glretrace_common PRIVATE exit=mobilegl_apitrace_exit) +target_compile_definitions(glretrace_common PRIVATE + exit=mobilegl_apitrace_exit + main=mobilegl_apitrace_main) target_compile_options(glretrace_common PRIVATE "-include${CMAKE_CURRENT_LIST_DIR}/apitrace_exit.hpp") target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc) @@ -225,6 +229,8 @@ add_library(trace_replay_runner SHARED trace_replay_jni.cpp) target_compile_features(trace_replay_runner PRIVATE cxx_std_17) +target_compile_definitions(trace_replay_runner PRIVATE + MOBILEGL_APITRACE_RETRACE_MAIN=mobilegl_apitrace_main) target_include_directories(trace_replay_runner PRIVATE "${APITRACE_ROOT}/lib/image") diff --git a/android-plugin/app/src/trace/cpp/apitrace_exit.hpp b/android-plugin/app/src/trace/cpp/apitrace_exit.hpp index 75b9bd01..0f02846f 100644 --- a/android-plugin/app/src/trace/cpp/apitrace_exit.hpp +++ b/android-plugin/app/src/trace/cpp/apitrace_exit.hpp @@ -1,5 +1,19 @@ #pragma once +#ifdef exit +#pragma push_macro("exit") +#undef exit +#define MOBILEGL_TRACE_RESTORE_EXIT_MACRO +#endif + +#include +#include + +#ifdef MOBILEGL_TRACE_RESTORE_EXIT_MACRO +#pragma pop_macro("exit") +#undef MOBILEGL_TRACE_RESTORE_EXIT_MACRO +#endif + struct MobileGLRetraceExit { int status; }; diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp index 36787950..d24bf8b1 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp @@ -22,7 +22,11 @@ #include #include -extern "C" int main(int argc, char** argv); +#ifndef MOBILEGL_APITRACE_RETRACE_MAIN +#define MOBILEGL_APITRACE_RETRACE_MAIN main +#endif + +extern "C" int MOBILEGL_APITRACE_RETRACE_MAIN(int argc, char** argv); namespace mobilegl_trace { namespace { @@ -108,6 +112,7 @@ std::string JsonEscape(const std::string& value) { bool LoadMobileGL(const Request& request, std::string& error) { setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1); + setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1); void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL); if (handle == nullptr) { @@ -366,7 +371,7 @@ int RunRetraceMain(const Request& request) { tracePath.data(), nullptr, }; - return main(10, argv); + return MOBILEGL_APITRACE_RETRACE_MAIN(10, argv); } bool RunRetrace(const Request& request, Result& result) { diff --git a/tools/trace_replay/CMakeLists.txt b/tools/trace_replay/CMakeLists.txt new file mode 100644 index 00000000..fa3a5aac --- /dev/null +++ b/tools/trace_replay/CMakeLists.txt @@ -0,0 +1,270 @@ +cmake_minimum_required(VERSION 3.22.1) + +if(NOT UNIX OR APPLE OR ANDROID) + message(STATUS "MobileGL trace replay runner is only enabled on Linux") + return() +endif() + +set(MOBILEGL_TRACE_ROOT "${CMAKE_CURRENT_LIST_DIR}") +set(MOBILEGL_TRACE_SHARED_CPP_DIR "${CMAKE_SOURCE_DIR}/android-plugin/app/src/trace/cpp") +set(APITRACE_ROOT "${CMAKE_SOURCE_DIR}/android-plugin/third_party/apitrace") +set(APITRACE_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/apitrace") +set(APITRACE_VERSION "mobilegl-trace") + +find_package(Python3 REQUIRED) +find_package(Threads REQUIRED) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include("${APITRACE_ROOT}/cmake/ConvenienceLibrary.cmake") + +function(target_optimize TARGET) + target_compile_definitions(${TARGET} PRIVATE -DNDEBUG) + target_compile_options(${TARGET} PRIVATE -O3) +endfunction() + +function(include_with_scope) + include(${ARGV}) +endfunction() + +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) + +if(NOT TARGET check) + add_custom_target(check) +endif() +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(mobilegl_trace_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(mobilegl_trace_apitrace_os PUBLIC + "${APITRACE_ROOT}/compat" + "${APITRACE_ROOT}/thirdparty" + "${APITRACE_ROOT}/lib/os" + "${APITRACE_ROOT}/lib/trace") +target_link_libraries(mobilegl_trace_apitrace_os PUBLIC Threads::Threads) + +add_library(mobilegl_trace_highlight STATIC "${APITRACE_ROOT}/lib/highlight/highlight.cpp") +target_include_directories(mobilegl_trace_highlight PUBLIC "${APITRACE_ROOT}/lib/highlight") + +add_library(mobilegl_trace_guids STATIC "${APITRACE_ROOT}/lib/guids/guids.cpp") +target_include_directories(mobilegl_trace_guids PUBLIC + "${APITRACE_ROOT}/lib/guids" + "${APITRACE_ROOT}/lib/os") + +add_library(mobilegl_trace_image STATIC + "${APITRACE_ROOT}/lib/image/image_bmp.cpp" + "${APITRACE_ROOT}/lib/image/image_png.cpp" + "${APITRACE_ROOT}/lib/image/image_pnm.cpp" + "${APITRACE_ROOT}/lib/image/image_raw.cpp" + "${APITRACE_ROOT}/lib/image/image_md5.cpp") +target_include_directories(mobilegl_trace_image PUBLIC "${APITRACE_ROOT}/lib/image") +target_link_libraries(mobilegl_trace_image PUBLIC md5 PNG::PNG) + +add_library(mobilegl_trace_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(mobilegl_trace_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(mobilegl_trace_common PUBLIC + mobilegl_trace_guids + mobilegl_trace_highlight + mobilegl_trace_apitrace_os + Snappy::snappy + ZLIB::ZLIB + PkgConfig::BROTLIDEC + PkgConfig::ZSTD + zstd_seekable) + +add_library(mobilegl_trace_glproc STATIC + "${APITRACE_GENERATED_DIR}/glproc.cpp" + "${MOBILEGL_TRACE_ROOT}/apitrace_glproc_mobilegl.cpp") +target_include_directories(mobilegl_trace_glproc PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/lib/os" + "${APITRACE_ROOT}/thirdparty/khronos") +target_link_libraries(mobilegl_trace_glproc PUBLIC mobilegl_trace_apitrace_os dl) + +execute_process( + COMMAND ${Python3_EXECUTABLE} "${APITRACE_ROOT}/retrace/glretrace.py" + OUTPUT_FILE "${APITRACE_GENERATED_DIR}/glretrace_gl.cpp" + COMMAND_ERROR_IS_FATAL ANY) + +execute_process( + COMMAND ${Python3_EXECUTABLE} "${APITRACE_ROOT}/retrace/glstate_params.py" + OUTPUT_FILE "${APITRACE_GENERATED_DIR}/glstate_params.cpp" + COMMAND_ERROR_IS_FATAL ANY) + +add_library(mobilegl_trace_glhelpers STATIC + "${APITRACE_ROOT}/helpers/glfeatures.cpp" + "${APITRACE_ROOT}/helpers/eglsize.cpp") +target_include_directories(mobilegl_trace_glhelpers PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/helpers" + "${APITRACE_ROOT}/lib/os" + "${APITRACE_ROOT}/thirdparty/khronos") +target_link_libraries(mobilegl_trace_glhelpers PUBLIC mobilegl_trace_glproc mobilegl_trace_apitrace_os) + +add_library(mobilegl_trace_retrace_common STATIC + "${APITRACE_ROOT}/retrace/json.cpp" + "${APITRACE_ROOT}/retrace/process_name.cpp" + "${APITRACE_ROOT}/retrace/retrace.cpp" + "${APITRACE_ROOT}/retrace/retrace_main.cpp" + "${APITRACE_ROOT}/retrace/retrace_stdc.cpp" + "${APITRACE_ROOT}/retrace/retrace_swizzle.cpp" + "${APITRACE_ROOT}/retrace/state_writer.cpp" + "${APITRACE_ROOT}/retrace/state_writer_json.cpp" + "${APITRACE_ROOT}/retrace/state_writer_ubjson.cpp" + "${APITRACE_ROOT}/retrace/ws.cpp") +target_compile_definitions(mobilegl_trace_retrace_common PUBLIC RETRACE) +target_compile_definitions(mobilegl_trace_retrace_common PRIVATE + exit=mobilegl_apitrace_exit + main=mobilegl_apitrace_main) +target_compile_options(mobilegl_trace_retrace_common PRIVATE "-include${MOBILEGL_TRACE_SHARED_CPP_DIR}/apitrace_exit.hpp") +target_include_directories(mobilegl_trace_retrace_common PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/compat" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/helpers" + "${APITRACE_ROOT}/lib/highlight" + "${APITRACE_ROOT}/lib/image" + "${APITRACE_ROOT}/lib/os" + "${APITRACE_ROOT}/lib/trace" + "${APITRACE_ROOT}/lib/ubjson" + "${APITRACE_ROOT}/retrace" + "${APITRACE_ROOT}/thirdparty" + "${APITRACE_ROOT}/thirdparty/khronos") +target_link_libraries(mobilegl_trace_retrace_common PUBLIC mobilegl_trace_image mobilegl_trace_common) + +add_library(mobilegl_trace_glretrace_common STATIC + "${APITRACE_GENERATED_DIR}/glretrace_gl.cpp" + "${APITRACE_GENERATED_DIR}/glstate_params.cpp" + "${APITRACE_ROOT}/retrace/glretrace_cgl.cpp" + "${APITRACE_ROOT}/retrace/glretrace_egl.cpp" + "${APITRACE_ROOT}/retrace/glretrace_glx.cpp" + "${APITRACE_ROOT}/retrace/glretrace_main.cpp" + "${APITRACE_ROOT}/retrace/glretrace_wgl.cpp" + "${APITRACE_ROOT}/retrace/glretrace_wgl_font_bitmaps.cpp" + "${APITRACE_ROOT}/retrace/glretrace_wgl_font_outlines.cpp" + "${APITRACE_ROOT}/retrace/glretrace_ws.cpp" + "${APITRACE_ROOT}/retrace/glstate.cpp" + "${APITRACE_ROOT}/retrace/glstate_formats.cpp" + "${APITRACE_ROOT}/retrace/glstate_images.cpp" + "${APITRACE_ROOT}/retrace/glstate_shaders.cpp" + "${APITRACE_ROOT}/retrace/glws.cpp" + "${APITRACE_ROOT}/retrace/metric_backend_amd_perfmon.cpp" + "${APITRACE_ROOT}/retrace/metric_backend_intel_perfquery.cpp" + "${APITRACE_ROOT}/retrace/metric_backend_opengl.cpp" + "${APITRACE_ROOT}/retrace/metric_helper.cpp" + "${APITRACE_ROOT}/retrace/metric_writer.cpp" + "${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp") +target_include_directories(mobilegl_trace_glretrace_common PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/helpers" + "${APITRACE_ROOT}/retrace") +target_compile_definitions(mobilegl_trace_glretrace_common PRIVATE + exit=mobilegl_apitrace_exit + main=mobilegl_apitrace_main) +target_compile_options(mobilegl_trace_glretrace_common PRIVATE "-include${MOBILEGL_TRACE_SHARED_CPP_DIR}/apitrace_exit.hpp") +target_link_libraries(mobilegl_trace_glretrace_common PUBLIC + mobilegl_trace_retrace_common + mobilegl_trace_glhelpers + mobilegl_trace_glproc) + +add_executable(mobilegl_trace_replay + "${MOBILEGL_TRACE_SHARED_CPP_DIR}/trace_replay_core.cpp" + "${MOBILEGL_TRACE_ROOT}/trace_replay_cli.cpp") +target_compile_features(mobilegl_trace_replay PRIVATE cxx_std_17) +target_compile_definitions(mobilegl_trace_replay PRIVATE + MOBILEGL_APITRACE_RETRACE_MAIN=mobilegl_apitrace_main) +target_include_directories(mobilegl_trace_replay PRIVATE + "${MOBILEGL_TRACE_SHARED_CPP_DIR}" + "${APITRACE_ROOT}/lib/image") +target_link_libraries(mobilegl_trace_replay PRIVATE + "-Wl,--start-group" + mobilegl_trace_glretrace_common + mobilegl_trace_retrace_common + mobilegl_trace_image + mobilegl_trace_common + mobilegl_trace_glhelpers + mobilegl_trace_glproc + mobilegl_trace_apitrace_os + "-Wl,--end-group" + dl) +add_dependencies(mobilegl_trace_replay MobileGL) + +enable_testing() +add_test( + NAME MobileGLTraceReplay.OpenRA.DirectGLES + COMMAND "${CMAKE_COMMAND}" + -DTRACE_REPLAY_EXE=$ + -DMOBILEGL_LIBRARY=$ + -DOPENRA_TRACE_ARCHIVE=${MOBILEGL_TRACE_ROOT}/fixtures/openra.tgz + -DOPENRA_GOLDEN=${MOBILEGL_TRACE_ROOT}/fixtures/openra.0000031249.png + -DOPENRA_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/openra + -P ${MOBILEGL_TRACE_ROOT}/run_openra_replay.cmake) +set_tests_properties(MobileGLTraceReplay.OpenRA.DirectGLES PROPERTIES + ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330") diff --git a/tools/trace_replay/README.md b/tools/trace_replay/README.md new file mode 100644 index 00000000..a9845335 --- /dev/null +++ b/tools/trace_replay/README.md @@ -0,0 +1,44 @@ +# MobileGL trace replay + +This directory builds a Linux command line replay runner for apitrace files. It is meant for CI coverage of MobileGL +without Android, APK packaging, FCL, or plugin runtime dependencies. + +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 OpenRA fixture test: + +```sh +ctest --test-dir build-test -V -R MobileGLTraceReplay.OpenRA.DirectGLES +``` + +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 \ + --tolerance 20 \ + --fuzz-percent 20 +``` + +The Linux runner uses an EGL pbuffer shim and loads MobileGL by path with `dlopen`. The current CI test covers the +OpenRA core-profile trace through the `DirectGLES` backend. `DirectVulkan` is still available as a CLI argument, but it +requires a native window/swapchain path and is not part of the headless CI test yet. diff --git a/tools/trace_replay/apitrace_glproc_mobilegl.cpp b/tools/trace_replay/apitrace_glproc_mobilegl.cpp new file mode 100644 index 00000000..20ca3534 --- /dev/null +++ b/tools/trace_replay/apitrace_glproc_mobilegl.cpp @@ -0,0 +1,142 @@ +#include "glproc.hpp" + +#include +#include +#include +#include +#include + +void *_libGlHandle = nullptr; + +namespace { + +void *LookupSymbol(const char *procName); + +void *GetMobileGlHandle() { + if (_libGlHandle != nullptr) { + return _libGlHandle; + } + + const char *library = std::getenv("MOBILEGL_TRACE_LIBRARY"); + if (library != nullptr && library[0] != '\0') { + _libGlHandle = dlopen(library, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + if (_libGlHandle == nullptr) { + _libGlHandle = dlopen(library, RTLD_NOW | RTLD_GLOBAL); + } + } + if (_libGlHandle == nullptr) { + _libGlHandle = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + } + if (_libGlHandle == nullptr) { + _libGlHandle = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL); + } + return _libGlHandle; +} + +using GlReadBuffer = void (*)(GLenum mode); +using GlReadPixels = void (*)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, + void *pixels); +using GlGetIntegerv = void (*)(GLenum pname, GLint *data); +using GlGetError = GLenum (*)(); +GlReadBuffer gRealGlReadBuffer = nullptr; +GlReadPixels gRealGlReadPixels = nullptr; +GlGetIntegerv gRealGlGetIntegerv = nullptr; +GlGetError gRealGlGetError = nullptr; +int gSuppressInvalidEnumCount = 0; + +void MobileGLTraceReadBuffer(GLenum mode) { + if (mode == GL_BACK) { + ++gSuppressInvalidEnumCount; + return; + } + if (gRealGlReadBuffer == nullptr) { + gRealGlReadBuffer = reinterpret_cast(LookupSymbol("glReadBuffer")); + } + if (gRealGlReadBuffer != nullptr) { + gRealGlReadBuffer(mode); + } +} + +void MobileGLTraceReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, + void *pixels) { + if (gRealGlReadPixels == nullptr) { + gRealGlReadPixels = reinterpret_cast(LookupSymbol("glReadPixels")); + } + if (gRealGlReadPixels != nullptr) { + gRealGlReadPixels(x, y, width, height, format, type, pixels); + } +} + +void MobileGLTraceGetIntegerv(GLenum pname, GLint *data) { + if (pname == GL_READ_BUFFER) { + if (data != nullptr) { + *data = GL_BACK; + } + ++gSuppressInvalidEnumCount; + return; + } + if (gRealGlGetIntegerv == nullptr) { + gRealGlGetIntegerv = reinterpret_cast(LookupSymbol("glGetIntegerv")); + } + if (gRealGlGetIntegerv != nullptr) { + gRealGlGetIntegerv(pname, data); + } +} + +GLenum MobileGLTraceGetError() { + if (gRealGlGetError == nullptr) { + gRealGlGetError = reinterpret_cast(LookupSymbol("glGetError")); + } + if (gRealGlGetError == nullptr) { + return GL_NO_ERROR; + } + GLenum error = gRealGlGetError(); + if (error == GL_INVALID_ENUM && gSuppressInvalidEnumCount > 0) { + --gSuppressInvalidEnumCount; + return GL_NO_ERROR; + } + return error; +} + +void *LookupSymbol(const char *procName) { + void *mobileGl = GetMobileGlHandle(); + if (mobileGl != nullptr) { + void *proc = dlsym(mobileGl, procName); + if (proc != nullptr) { + return proc; + } + } + + return dlsym(RTLD_DEFAULT, procName); +} + +} // namespace + +void *_getPublicProcAddress(const char *procName) { + if (strcmp(procName, "glReadPixels") == 0) { + return reinterpret_cast(&MobileGLTraceReadPixels); + } + if (strcmp(procName, "glReadBuffer") == 0) { + return reinterpret_cast(&MobileGLTraceReadBuffer); + } + if (strcmp(procName, "glGetIntegerv") == 0) { + return reinterpret_cast(&MobileGLTraceGetIntegerv); + } + if (strcmp(procName, "glGetError") == 0) { + return reinterpret_cast(&MobileGLTraceGetError); + } + return LookupSymbol(procName); +} + +void *_getPrivateProcAddress(const char *procName) { + void *proc = LookupSymbol(procName); + if (proc != nullptr) { + return proc; + } + + if (_eglGetProcAddress != nullptr) { + return reinterpret_cast(_eglGetProcAddress(procName)); + } + + return nullptr; +} diff --git a/tools/trace_replay/apitrace_glws_egl.cpp b/tools/trace_replay/apitrace_glws_egl.cpp new file mode 100644 index 00000000..169d0421 --- /dev/null +++ b/tools/trace_replay/apitrace_glws_egl.cpp @@ -0,0 +1,373 @@ +#include "glws.hpp" +#include "retrace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using PfnEglBindApi = EGLBoolean (*)(EGLenum); +using PfnEglChooseConfig = EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); +using PfnEglCreateContext = EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); +using PfnEglCreatePbufferSurface = EGLSurface (*)(EGLDisplay, EGLConfig, const EGLint *); +using PfnEglDestroyContext = EGLBoolean (*)(EGLDisplay, EGLContext); +using PfnEglDestroySurface = EGLBoolean (*)(EGLDisplay, EGLSurface); +using PfnEglGetConfigAttrib = EGLBoolean (*)(EGLDisplay, EGLConfig, EGLint, EGLint *); +using PfnEglGetDisplay = EGLDisplay (*)(EGLNativeDisplayType); +using PfnEglGetError = EGLint (*)(); +using PfnEglInitialize = EGLBoolean (*)(EGLDisplay, EGLint *, EGLint *); +using PfnEglMakeCurrent = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); +using PfnEglQueryString = const char *(*)(EGLDisplay, EGLint); +using PfnEglSwapBuffers = EGLBoolean (*)(EGLDisplay, EGLSurface); +using PfnEglTerminate = EGLBoolean (*)(EGLDisplay); + +struct EglFns { + PfnEglBindApi bindApi = nullptr; + PfnEglChooseConfig chooseConfig = nullptr; + PfnEglCreateContext createContext = nullptr; + PfnEglCreatePbufferSurface createPbufferSurface = nullptr; + PfnEglDestroyContext destroyContext = nullptr; + PfnEglDestroySurface destroySurface = nullptr; + PfnEglGetConfigAttrib getConfigAttrib = nullptr; + PfnEglGetDisplay getDisplay = nullptr; + PfnEglGetError getError = nullptr; + PfnEglInitialize initialize = nullptr; + PfnEglMakeCurrent makeCurrent = nullptr; + PfnEglQueryString queryString = nullptr; + PfnEglSwapBuffers swapBuffers = nullptr; + PfnEglTerminate terminate = nullptr; +}; + +EglFns gEgl; +EGLDisplay gDisplay = EGL_NO_DISPLAY; +void *gMobileGl = nullptr; +const glws::Drawable *gCurrentDrawable = nullptr; +EGLContext gCurrentContext = EGL_NO_CONTEXT; +int gRequestedWidth = 0; +int gRequestedHeight = 0; + +int ResolveWidth(int width) { + if (gRequestedWidth > 0) { + return gRequestedWidth; + } + return width > 0 ? width : 1; +} + +int ResolveHeight(int height) { + if (gRequestedHeight > 0) { + return gRequestedHeight; + } + return height > 0 ? height : 1; +} + +void *Lookup(const char *name) { + if (gMobileGl == nullptr) { + const char *library = std::getenv("MOBILEGL_TRACE_LIBRARY"); + if (library != nullptr && library[0] != '\0') { + gMobileGl = dlopen(library, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + if (gMobileGl == nullptr) { + gMobileGl = dlopen(library, RTLD_NOW | RTLD_GLOBAL); + } + } + if (gMobileGl == nullptr) { + gMobileGl = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + } + if (gMobileGl == nullptr) { + gMobileGl = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL); + } + } + if (gMobileGl != nullptr) { + void *symbol = dlsym(gMobileGl, name); + if (symbol != nullptr) { + return symbol; + } + } + return dlsym(RTLD_DEFAULT, name); +} + +template +bool Load(T &slot, const char *name) { + slot = reinterpret_cast(Lookup(name)); + if (slot == nullptr) { + std::cerr << "error: failed to resolve " << name << " from MobileGL\n"; + return false; + } + return true; +} + +bool LoadEgl() { + return Load(gEgl.bindApi, "eglBindAPI") && + Load(gEgl.chooseConfig, "eglChooseConfig") && + Load(gEgl.createContext, "eglCreateContext") && + Load(gEgl.createPbufferSurface, "eglCreatePbufferSurface") && + Load(gEgl.destroyContext, "eglDestroyContext") && + Load(gEgl.destroySurface, "eglDestroySurface") && + Load(gEgl.getConfigAttrib, "eglGetConfigAttrib") && + Load(gEgl.getDisplay, "eglGetDisplay") && + Load(gEgl.getError, "eglGetError") && + Load(gEgl.initialize, "eglInitialize") && + Load(gEgl.makeCurrent, "eglMakeCurrent") && + Load(gEgl.queryString, "eglQueryString") && + Load(gEgl.swapBuffers, "eglSwapBuffers") && + Load(gEgl.terminate, "eglTerminate"); +} + +class EglVisual final : public glws::Visual { +public: + EGLConfig config = nullptr; + EGLenum api = EGL_OPENGL_API; + + explicit EglVisual(glws::Profile prof) : Visual(prof) {} +}; + +class EglDrawable final : public glws::Drawable { +public: + EGLSurface surface = EGL_NO_SURFACE; + + EglDrawable(const EglVisual *visual, int width, int height, bool pbuffer) + : Drawable(visual, width, height, pbuffer) { + createSurface(); + } + + ~EglDrawable() override { + destroySurface(); + } + + void resize(int w, int h) override { + w = ResolveWidth(w); + h = ResolveHeight(h); + if (w == width && h == height) { + return; + } + destroySurface(); + width = w; + height = h; + createSurface(); + if (gCurrentDrawable == this && gCurrentContext != EGL_NO_CONTEXT) { + gEgl.makeCurrent(gDisplay, surface, surface, gCurrentContext); + } + } + + void show() override { + visible = true; + } + + void swapBuffers() override { + if (surface == EGL_NO_SURFACE) { + return; + } + char callNo[32]; + const char *overrideCallNo = getenv("MOBILEGL_TRACE_CURRENT_CALL_OVERRIDE"); + if (overrideCallNo != nullptr && overrideCallNo[0] != '\0') { + snprintf(callNo, sizeof(callNo), "%s", overrideCallNo); + } else { + snprintf(callNo, sizeof(callNo), "%u", retrace::callNo); + } + setenv("MOBILEGL_PRESENT_CURRENT_CALL", callNo, 1); + gEgl.swapBuffers(gDisplay, surface); + unsetenv("MOBILEGL_PRESENT_CURRENT_CALL"); + } + +private: + void createSurface() { + const EGLint attribs[] = { + EGL_WIDTH, ResolveWidth(width), + EGL_HEIGHT, ResolveHeight(height), + EGL_NONE, + }; + surface = gEgl.createPbufferSurface(gDisplay, static_cast(visual)->config, attribs); + if (surface == EGL_NO_SURFACE) { + std::cerr << "error: EGL pbuffer creation failed: 0x" << std::hex + << gEgl.getError() << std::dec << "\n"; + } + } + + void destroySurface() { + if (surface != EGL_NO_SURFACE) { + gEgl.destroySurface(gDisplay, surface); + surface = EGL_NO_SURFACE; + } + } +}; + +class EglContext final : public glws::Context { +public: + EGLContext context = EGL_NO_CONTEXT; + + EglContext(const EglVisual *visual, glws::Context *shareContext) + : Context(visual) { + const EGLContext share = shareContext == nullptr ? EGL_NO_CONTEXT : static_cast(shareContext)->context; + if (!gEgl.bindApi(visual->api)) { + std::cerr << "error: eglBindAPI failed: 0x" << std::hex << gEgl.getError() + << std::dec << "\n"; + } + + EGLint attribs[9]; + int index = 0; + if (visual->api == EGL_OPENGL_ES_API) { + attribs[index++] = EGL_CONTEXT_CLIENT_VERSION; + attribs[index++] = static_cast(std::max(profile.major, 2)); + } else { + attribs[index++] = EGL_CONTEXT_MAJOR_VERSION_KHR; + attribs[index++] = profile.major >= 3 ? profile.major : 3; + attribs[index++] = EGL_CONTEXT_MINOR_VERSION_KHR; + attribs[index++] = profile.major >= 3 ? profile.minor : 3; + attribs[index++] = EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR; + attribs[index++] = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR; + } + attribs[index++] = EGL_NONE; + + context = gEgl.createContext(gDisplay, visual->config, share, attribs); + if (context == EGL_NO_CONTEXT && visual->api == EGL_OPENGL_API) { + const EGLint fallbackAttribs[] = {EGL_NONE}; + context = gEgl.createContext(gDisplay, visual->config, share, fallbackAttribs); + } + if (context == EGL_NO_CONTEXT) { + std::cerr << "error: eglCreateContext failed: 0x" << std::hex << gEgl.getError() + << std::dec << "\n"; + } + } + + ~EglContext() override { + if (context != EGL_NO_CONTEXT) { + gEgl.destroyContext(gDisplay, context); + } + } +}; + +const EglDrawable *AsEglDrawable(const glws::Drawable *drawable) { + return static_cast(drawable); +} + +const EglContext *AsEglContext(const glws::Context *context) { + return static_cast(context); +} + +} // namespace + +namespace glws { + +void init() { + if (gDisplay != EGL_NO_DISPLAY) { + return; + } + if (!LoadEgl()) { + return; + } + gDisplay = gEgl.getDisplay(EGL_DEFAULT_DISPLAY); + if (gDisplay == EGL_NO_DISPLAY) { + std::cerr << "error: eglGetDisplay failed: 0x" << std::hex << gEgl.getError() + << std::dec << "\n"; + return; + } + EGLint major = 0; + EGLint minor = 0; + if (!gEgl.initialize(gDisplay, &major, &minor)) { + std::cerr << "error: eglInitialize failed: 0x" << std::hex << gEgl.getError() + << std::dec << "\n"; + } +} + +void cleanup() { + if (gDisplay != EGL_NO_DISPLAY) { + gEgl.makeCurrent(gDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + gEgl.terminate(gDisplay); + gDisplay = EGL_NO_DISPLAY; + } +} + +Visual *createVisual(bool doubleBuffer, unsigned samples, Profile profile) { + auto *visual = new EglVisual(profile); + visual->doubleBuffer = doubleBuffer; + visual->api = profile.api == glfeatures::API_GLES ? EGL_OPENGL_ES_API : EGL_OPENGL_API; + + if (!gEgl.bindApi(visual->api)) { + delete visual; + return nullptr; + } + + const EGLint attribs[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, visual->api == EGL_OPENGL_ES_API ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_STENCIL_SIZE, 8, + EGL_SAMPLE_BUFFERS, samples > 1 ? 1 : 0, + EGL_SAMPLES, samples > 1 ? static_cast(samples) : 0, + EGL_NONE, + }; + EGLint count = 0; + if (!gEgl.chooseConfig(gDisplay, attribs, &visual->config, 1, &count) || count < 1) { + delete visual; + return nullptr; + } + return visual; +} + +Drawable *createDrawable(const Visual *visual, int width, int height, const pbuffer_info *pbInfo) { + return new EglDrawable(static_cast(visual), ResolveWidth(width), + ResolveHeight(height), true); +} + +Context *createContext(const Visual *visual, Context *shareContext, bool) { + return new EglContext(static_cast(visual), shareContext); +} + +bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *context) { + EGLSurface drawSurface = drawable == nullptr ? EGL_NO_SURFACE : AsEglDrawable(drawable)->surface; + EGLSurface readSurface = readable == nullptr ? drawSurface : AsEglDrawable(readable)->surface; + EGLContext eglContext = context == nullptr ? EGL_NO_CONTEXT : AsEglContext(context)->context; + if (drawSurface == EGL_NO_SURFACE && readSurface == EGL_NO_SURFACE && eglContext == EGL_NO_CONTEXT) { + gEgl.makeCurrent(gDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + gCurrentDrawable = nullptr; + gCurrentContext = EGL_NO_CONTEXT; + return true; + } + if (gEgl.makeCurrent(gDisplay, drawSurface, readSurface, eglContext) != EGL_TRUE) { + return false; + } + gCurrentDrawable = drawable; + gCurrentContext = eglContext; + return true; +} + +bool processEvents() { + return false; +} + +bool bindTexImage(Drawable *, int) { + return false; +} + +bool releaseTexImage(Drawable *, int) { + return false; +} + +bool setPbufferAttrib(Drawable *, const int *) { + return false; +} + +} // namespace glws + +extern "C" bool mobilegl_trace_get_drawable_bounds(int *width, int *height) { + if (gCurrentDrawable == nullptr || width == nullptr || height == nullptr) { + return false; + } + *width = gCurrentDrawable->width; + *height = gCurrentDrawable->height; + return *width > 0 && *height > 0; +} + +extern "C" void mobilegl_trace_set_requested_size(int width, int height) { + gRequestedWidth = width > 0 ? width : 0; + gRequestedHeight = height > 0 ? height : 0; +} diff --git a/tools/trace_replay/fixtures/openra.0000031249.png b/tools/trace_replay/fixtures/openra.0000031249.png new file mode 100644 index 00000000..a4099d8b Binary files /dev/null and b/tools/trace_replay/fixtures/openra.0000031249.png differ diff --git a/tools/trace_replay/fixtures/openra.tgz b/tools/trace_replay/fixtures/openra.tgz new file mode 100644 index 00000000..1179ba1a Binary files /dev/null and b/tools/trace_replay/fixtures/openra.tgz differ diff --git a/tools/trace_replay/run_openra_replay.cmake b/tools/trace_replay/run_openra_replay.cmake new file mode 100644 index 00000000..eb88903b --- /dev/null +++ b/tools/trace_replay/run_openra_replay.cmake @@ -0,0 +1,63 @@ +foreach(required TRACE_REPLAY_EXE MOBILEGL_LIBRARY OPENRA_TRACE_ARCHIVE OPENRA_GOLDEN OPENRA_OUTPUT_DIR) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "${required} is required") + endif() +endforeach() + +if(EXISTS "${OPENRA_OUTPUT_DIR}") + file(REMOVE_RECURSE "${OPENRA_OUTPUT_DIR}") +endif() +file(MAKE_DIRECTORY "${OPENRA_OUTPUT_DIR}/input") +file(MAKE_DIRECTORY "${OPENRA_OUTPUT_DIR}/output") + +execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xzf "${OPENRA_TRACE_ARCHIVE}" + WORKING_DIRECTORY "${OPENRA_OUTPUT_DIR}/input" + RESULT_VARIABLE extract_result + OUTPUT_VARIABLE extract_stdout + ERROR_VARIABLE extract_stderr) +if(NOT extract_result EQUAL 0) + message(STATUS "${extract_stdout}") + message(STATUS "${extract_stderr}") + message(FATAL_ERROR "failed to extract ${OPENRA_TRACE_ARCHIVE}") +endif() + +set(openra_trace "${OPENRA_OUTPUT_DIR}/input/openra.trace") +if(NOT EXISTS "${openra_trace}") + message(FATAL_ERROR "extracted OpenRA trace was not found at ${openra_trace}") +endif() + +execute_process( + COMMAND "${TRACE_REPLAY_EXE}" + --trace "${openra_trace}" + --golden "${OPENRA_GOLDEN}" + --output "${OPENRA_OUTPUT_DIR}/output" + --backend DirectGLES + --mobilegl-library "${MOBILEGL_LIBRARY}" + --target-call 31249 + --width 640 + --height 480 + --tolerance 20 + --crop-x 1 + --crop-y 1 + --crop-width 638 + --crop-height 478 + --fuzz-percent 20 + RESULT_VARIABLE replay_result + OUTPUT_VARIABLE replay_stdout + ERROR_VARIABLE replay_stderr) + +message(STATUS "${replay_stdout}") +message(STATUS "${replay_stderr}") + +set(result_json "${OPENRA_OUTPUT_DIR}/output/result.json") +if(EXISTS "${result_json}") + file(READ "${result_json}" result_contents) + message(STATUS "${result_contents}") +else() + message(FATAL_ERROR "trace replay did not write ${result_json}") +endif() + +if(NOT replay_result EQUAL 0) + message(FATAL_ERROR "OpenRA trace replay failed with status ${replay_result}") +endif() diff --git a/tools/trace_replay/trace_replay_cli.cpp b/tools/trace_replay/trace_replay_cli.cpp new file mode 100644 index 00000000..661a3b40 --- /dev/null +++ b/tools/trace_replay/trace_replay_cli.cpp @@ -0,0 +1,136 @@ +#include "trace_replay_core.hpp" + +#include +#include +#include + +extern "C" void mobilegl_trace_set_requested_size(int width, int height); + +namespace { + +void PrintUsage(const char *argv0) { + std::cerr + << "Usage: " << argv0 << " --trace PATH --target-call N --output DIR [options]\n" + << "\n" + << "Options:\n" + << " --golden PATH Golden PNG to compare against\n" + << " --backend NAME DirectGLES or DirectVulkan (default: DirectGLES)\n" + << " --mobilegl-library PATH libMobileGL.so path (default: libMobileGL.so)\n" + << " --width N Replay surface width override\n" + << " --height N Replay surface height override\n" + << " --tolerance N Allowed mismatch pixels\n" + << " --fuzz-percent N Per-channel fuzz threshold, 0-100 (default: 20)\n" + << " --crop-x N Compare crop x\n" + << " --crop-y N Compare crop y\n" + << " --crop-width N Compare crop width\n" + << " --crop-height N Compare crop height\n"; +} + +bool ReadValue(int argc, char **argv, int &index, std::string &out) { + if (index + 1 >= argc) { + std::cerr << "Missing value for " << argv[index] << "\n"; + return false; + } + out = argv[++index]; + return true; +} + +bool ReadInt(int argc, char **argv, int &index, int &out) { + std::string value; + if (!ReadValue(argc, argv, index, value)) { + return false; + } + out = std::atoi(value.c_str()); + return true; +} + +bool ReadLongLong(int argc, char **argv, int &index, long long &out) { + std::string value; + if (!ReadValue(argc, argv, index, value)) { + return false; + } + out = std::strtoll(value.c_str(), nullptr, 10); + return true; +} + +bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) { + request.backend = "DirectGLES"; + request.fuzzPercent = 20; + + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg == "--trace") { + if (!ReadValue(argc, argv, i, request.tracePath)) return false; + } else if (arg == "--golden") { + if (!ReadValue(argc, argv, i, request.goldenPath)) return false; + } else if (arg == "--output") { + if (!ReadValue(argc, argv, i, request.outputDir)) return false; + } else if (arg == "--backend") { + if (!ReadValue(argc, argv, i, request.backend)) return false; + } else if (arg == "--mobilegl-library") { + if (!ReadValue(argc, argv, i, request.mobileGlLibrary)) return false; + } else if (arg == "--target-frame") { + if (!ReadInt(argc, argv, i, request.targetFrame)) return false; + } else if (arg == "--target-call") { + if (!ReadLongLong(argc, argv, i, request.targetCall)) return false; + } else if (arg == "--width") { + if (!ReadInt(argc, argv, i, request.width)) return false; + } else if (arg == "--height") { + if (!ReadInt(argc, argv, i, request.height)) return false; + } else if (arg == "--tolerance") { + if (!ReadInt(argc, argv, i, request.tolerance)) return false; + } else if (arg == "--crop-x") { + if (!ReadInt(argc, argv, i, request.cropX)) return false; + } else if (arg == "--crop-y") { + if (!ReadInt(argc, argv, i, request.cropY)) return false; + } else if (arg == "--crop-width") { + if (!ReadInt(argc, argv, i, request.cropWidth)) return false; + } else if (arg == "--crop-height") { + if (!ReadInt(argc, argv, i, request.cropHeight)) return false; + } else if (arg == "--fuzz-percent") { + if (!ReadInt(argc, argv, i, request.fuzzPercent)) return false; + } else if (arg == "--help" || arg == "-h") { + return false; + } else { + std::cerr << "Unknown argument: " << arg << "\n"; + return false; + } + } + + if (request.tracePath.empty()) { + std::cerr << "--trace is required\n"; + return false; + } + if (request.outputDir.empty()) { + std::cerr << "--output is required\n"; + return false; + } + if (request.targetCall < 0) { + std::cerr << "--target-call is required\n"; + return false; + } + return true; +} + +} // namespace + +int main(int argc, char **argv) { + mobilegl_trace::Request request; + if (!ParseArgs(argc, argv, request)) { + PrintUsage(argv[0]); + return 2; + } + + mobilegl_trace_set_requested_size(request.width, request.height); + mobilegl_trace::Result result = mobilegl_trace::RunTraceReplay(request); + mobilegl_trace_set_requested_size(0, 0); + + if (!result.resultPath.empty()) { + mobilegl_trace::WriteResultJson(request, result); + } + + std::cout << result.message << "\n" + << "result: " << result.resultPath << "\n" + << "actual: " << result.actualPath << "\n"; + return result.passed ? 0 : result.statusCode; +}