diff --git a/.gitmodules b/.gitmodules index cab80ed4..c5d62241 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "3rdparty/SPIRV-Reflect"] path = 3rdparty/SPIRV-Reflect url = https://github.com/KhronosGroup/SPIRV-Reflect.git +[submodule "android-plugin/third_party/apitrace"] + path = android-plugin/third_party/apitrace + url = https://github.com/MobileGL-Dev/apitrace.git diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 0223d7b2..f3e3ea0f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -399,6 +399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { static void DumpTextureUploadStats(Int textureId, TextureUploadTarget target, Uint32 level, const IntVec3& texelSize, const void* data, SizeT byteSize, Uint32 channels) { + if (channels == 0 && texelSize.x() > 0 && texelSize.y() > 0) { + const SizeT depth = static_cast(std::max(texelSize.z(), 1)); + const SizeT pixelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * depth; + if (pixelCount > 0 && byteSize % pixelCount == 0) { + channels = static_cast(std::min(byteSize / pixelCount, 4)); + } + } if (!ShouldDumpTextureUploadStats() || data == nullptr || byteSize == 0 || channels == 0) { return; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 8f7853c9..e9fa3a98 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -148,6 +148,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { vkCmdSetLineWidth(commandBuffer, lineWidth); } + static VkRect2D MakeClampedScissorRect(const IntVec4& scissorBox, const IntVec2& framebufferExtent) { + const Int x0 = std::max(0, scissorBox.x()); + const Int y0 = std::max(0, scissorBox.y()); + const Int x1 = std::min(framebufferExtent.x(), scissorBox.x() + std::max(0, scissorBox.z())); + const Int y1 = std::min(framebufferExtent.y(), scissorBox.y() + std::max(0, scissorBox.w())); + + VkRect2D scissor{}; + scissor.offset = {x0, y0}; + scissor.extent = { + static_cast(std::max(0, x1 - x0)), + static_cast(std::max(0, y1 - y0)), + }; + return scissor; + } + static void ApplyStencilState(VkCommandBuffer commandBuffer) { const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front); const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back); @@ -464,6 +479,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { return value != nullptr && value[0] != '\0' ? value : nullptr; } + static Bool PresentDumpMatchesTargetCall() { + const char* target = std::getenv("MOBILEGL_PRESENT_DUMP_CALL"); + if (target == nullptr || target[0] == '\0') { + return true; + } + const char* current = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL"); + return current != nullptr && std::strcmp(target, current) == 0; + } + static void DumpVertexInputStats(Uint32 location, const MG_State::GLState::VertexAttribute& attr, Uint32 firstVertex, Uint32 vertexCount) { if (!ShouldDumpVertexInputStats() || attr.Type != DataType::Float32 || attr.Size <= 0 || attr.Size > 4) { @@ -3018,8 +3042,7 @@ void main() { VkRect2D scissor{}; if (scissorEnabled) { const auto& scissorBox = MG_State::pGLContext->GetScissorBox(); - scissor.offset = { scissorBox[0], scissorBox[1] }; - scissor.extent = { (Uint)scissorBox[2], (Uint)scissorBox[3] }; + scissor = MakeClampedScissorRect(scissorBox, renderPassEntry->extent); } else { scissor.offset = {0, 0}; scissor.extent = { (Uint)renderPassEntry->extent.x(), (Uint)renderPassEntry->extent.y() }; @@ -4105,6 +4128,7 @@ void main() { Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) { if (frame.isCommandRecording) { m_frameContext.EndCommandRecording(); + frame.hasCommandBufferRecorded = true; } if (!frame.hasCommandBufferRecorded) { return true; @@ -4185,6 +4209,16 @@ void main() { const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? m_swapchainObject.GetImageLayout(m_imageIndexAcquired) : *srcBinding.trackedLayout; + if (PresentStatsEnabled()) { + std::fprintf(stderr, + "MOBILEGL_READPIXELS_BEGIN defaultFbo=%s x=%d y=%d width=%d height=%d srcLayout=%d recording=%s recorded=%s imageIndex=%u\n", + readIsDefaultFbo ? "true" : "false", + x, y, width, height, + static_cast(srcOriginalLayout), + frame.isCommandRecording ? "true" : "false", + frame.hasCommandBufferRecorded ? "true" : "false", + m_imageIndexAcquired); + } if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined"); return; @@ -4261,6 +4295,23 @@ void main() { MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer"); return; } + if (PresentStatsEnabled()) { + const SizeT pixelCount = static_cast(width) * static_cast(height); + SizeT nonBlack = 0; + SizeT nonTransparent = 0; + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* p = mapped + i * 4; + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + ++nonBlack; + } + if (p[3] != 0) { + ++nonTransparent; + } + } + std::fprintf(stderr, + "MOBILEGL_READPIXELS_STATS nonBlack=%zu/%zu alpha=%zu/%zu\n", + nonBlack, pixelCount, nonTransparent, pixelCount); + } const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM; PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels); } @@ -4768,8 +4819,21 @@ void main() { VkDeviceSize presentStatsReadbackSize = 0; const VkExtent2D presentStatsExtent = m_swapchainObject.GetExtent(); const char* presentDumpPath = PresentDumpPath(); - const Bool collectPresentStats = (PresentStatsEnabled() || presentDumpPath != nullptr) && frame.isCommandRecording && + const Bool shouldDumpPresent = presentDumpPath != nullptr && PresentDumpMatchesTargetCall(); + const Bool collectPresentStats = (PresentStatsEnabled() || shouldDumpPresent) && frame.isCommandRecording && presentStatsExtent.width > 0 && presentStatsExtent.height > 0; + if (PresentStatsEnabled() && presentDumpPath != nullptr) { + const char* targetCall = std::getenv("MOBILEGL_PRESENT_DUMP_CALL"); + const char* currentCall = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL"); + std::fprintf(stderr, + "MOBILEGL_PRESENT_DUMP_GATE target=%s current=%s shouldDump=%s recording=%s size=%ux%u path=%s\n", + targetCall != nullptr ? targetCall : "", + currentCall != nullptr ? currentCall : "", + shouldDumpPresent ? "true" : "false", + frame.isCommandRecording ? "true" : "false", + presentStatsExtent.width, presentStatsExtent.height, + presentDumpPath); + } if (collectPresentStats) { presentStatsReadbackSize = static_cast(presentStatsExtent.width) * static_cast(presentStatsExtent.height) * 4; @@ -4860,7 +4924,7 @@ void main() { nonBlack, pixelCount, colored, pixelCount, nonTransparent, pixelCount, presentStatsExtent.width, presentStatsExtent.height); } - if (presentDumpPath != nullptr) { + if (shouldDumpPresent) { FILE* dump = std::fopen(presentDumpPath, "wb"); if (dump != nullptr) { std::fprintf(dump, "P6\n%u %u\n255\n", presentStatsExtent.width, presentStatsExtent.height); @@ -4870,6 +4934,12 @@ void main() { std::fwrite(rgb, 1, sizeof(rgb), dump); } std::fclose(dump); + if (PresentStatsEnabled()) { + std::fprintf(stderr, "MOBILEGL_PRESENT_DUMP_WRITTEN path=%s bytes=%zu\n", + presentDumpPath, static_cast(pixelCount * 3)); + } + } else if (PresentStatsEnabled()) { + std::fprintf(stderr, "MOBILEGL_PRESENT_DUMP_FAILED path=%s\n", presentDumpPath); } } } diff --git a/android-plugin/TRACE_REPLAY.md b/android-plugin/TRACE_REPLAY.md new file mode 100644 index 00000000..257b3e9e --- /dev/null +++ b/android-plugin/TRACE_REPLAY.md @@ -0,0 +1,69 @@ +# MobileGL trace replay APK + +The Android plugin app has two flavor dimensions: + +- `backend`: `espryt` uses `DirectGLES`, `magma` uses `DirectVulkan`. +- `profile`: `plugin` keeps the original FCL plugin APK behavior, `trace` adds the standalone trace runner Activity. + +Useful debug builds: + +```sh +./gradlew -p MobileGL/android-plugin :app:assembleEsprytPluginDebug +./gradlew -p MobileGL/android-plugin :app:assembleEsprytTraceDebug +./gradlew -p MobileGL/android-plugin :app:assembleMagmaTraceDebug +``` + +The trace profile keeps the existing plugin manifest metadata and adds: + +```text +action: top.mobilegl.plugin.TRACE_REPLAY +activity: top.mobilegl.plugin.trace.TraceReplayActivity +``` + +Intent extras: + +```text +trace_path absolute path to the apitrace file +golden_path optional absolute path to a golden PNG +output_dir directory for result.json and actual.png +backend DirectGLES or DirectVulkan; defaults to the backend flavor +target_frame target frame index, or -1 +target_call target call number, or -1 +width optional replay surface width override +height optional replay surface height override +tolerance allowed mismatching pixel count after fuzz is applied +fuzz_percent per-channel fuzz percentage; default is 20 +crop_x optional compare crop x +crop_y optional compare crop y +crop_width optional compare crop width +crop_height optional compare crop height +``` + +Implementation notes: + +- The trace APK builds independently from FCL and can be launched with `adb shell am start`. +- The native runner validates inputs, sets `MOBILEGL_BACKEND_TYPE`, loads `libMobileGL.so`, runs apitrace GL retrace, writes `actual.png`, and writes `result.json`. +- The runner uses a MobileGL-backed EGL window-system shim. GLX calls in PC traces are consumed by apitrace's GLX retrace frontend and mapped onto this EGL shim; the Android runner does not require or call a MobileGL GLX implementation. +- `DirectGLES` replays on an EGL pbuffer by default, avoiding Android `SurfaceView` lifetime coupling. `DirectVulkan` still uses the Activity surface because it needs a native window-backed Vulkan swapchain. +- Golden comparison is implemented in native C++ with libpng RGBA decode. The Java Activity only passes arguments and displays the native result, so the replay/compare core is not tied to Android UI or Bitmap APIs and can be ported to Linux. +- The plugin profile still excludes `libtrace_replay_runner.so`; normal plugin APK behavior is preserved. + +Example core-profile trace smoke command for a debug trace APK: + +```sh +adb push app.trace /data/local/tmp/mobilegl_app.trace +adb push app.golden.png /data/local/tmp/mobilegl_app_ref.png +adb shell run-as top.mobilegl.plugin.espryt.trace mkdir -p files/trace-replay/input files/trace-replay/output +adb shell run-as top.mobilegl.plugin.espryt.trace cp /data/local/tmp/mobilegl_app.trace files/trace-replay/input/app.trace +adb shell run-as top.mobilegl.plugin.espryt.trace cp /data/local/tmp/mobilegl_app_ref.png files/trace-replay/input/app.golden.png +adb shell am start -a top.mobilegl.plugin.TRACE_REPLAY \ + -n top.mobilegl.plugin.espryt.trace/top.mobilegl.plugin.trace.TraceReplayActivity \ + --es trace_path /data/user/0/top.mobilegl.plugin.espryt.trace/files/trace-replay/input/app.trace \ + --es golden_path /data/user/0/top.mobilegl.plugin.espryt.trace/files/trace-replay/input/app.golden.png \ + --es output_dir /data/user/0/top.mobilegl.plugin.espryt.trace/files/trace-replay/output \ + --es backend DirectGLES \ + --el target_call 31249 \ + --ei tolerance 0 \ + --ei fuzz_percent 20 +adb shell run-as top.mobilegl.plugin.espryt.trace cat files/trace-replay/output/result.json +``` diff --git a/android-plugin/app/build.gradle b/android-plugin/app/build.gradle index 0281b2c3..fbec6f9e 100644 --- a/android-plugin/app/build.gradle +++ b/android-plugin/app/build.gradle @@ -34,13 +34,14 @@ android { } } - flavorDimensions 'backend' + flavorDimensions 'backend', 'profile' productFlavors { espryt { dimension 'backend' applicationId 'top.mobilegl.plugin.espryt' def env = mobileGlEnvString('DirectGLES') + resValue 'string', 'mobilegl_default_backend', 'DirectGLES' manifestPlaceholders = [ appLabel: 'MobileGL Espryt', des : 'MobileGL Espryt', @@ -53,6 +54,7 @@ android { dimension 'backend' applicationId 'top.mobilegl.plugin.magma' def env = mobileGlEnvString('DirectVulkan') + resValue 'string', 'mobilegl_default_backend', 'DirectVulkan' manifestPlaceholders = [ appLabel: 'MobileGL Magma', des : 'MobileGL Magma', @@ -61,6 +63,14 @@ android { pojavEnv: env, ] } + plugin { + dimension 'profile' + } + trace { + dimension 'profile' + applicationIdSuffix '.trace' + versionNameSuffix '-trace' + } } if (releaseSigningReady) { @@ -83,6 +93,13 @@ android { } } + externalNativeBuild { + cmake { + path file('src/trace/cpp/CMakeLists.txt') + version '3.22.1' + } + } + packaging { jniLibs { useLegacyPackaging true @@ -97,6 +114,12 @@ android.applicationVariants.configureEach { variant -> } } +androidComponents { + onVariants(selector().withFlavor('profile', 'plugin')) { variant -> + variant.packaging.jniLibs.excludes.add('**/libtrace_replay_runner.so') + } +} + dependencies { implementation project(':MobileGL') } diff --git a/android-plugin/app/src/trace/AndroidManifest.xml b/android-plugin/app/src/trace/AndroidManifest.xml new file mode 100644 index 00000000..8f04cbc4 --- /dev/null +++ b/android-plugin/app/src/trace/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/android-plugin/app/src/trace/cpp/CMakeLists.txt b/android-plugin/app/src/trace/cpp/CMakeLists.txt new file mode 100644 index 00000000..353278ad --- /dev/null +++ b/android-plugin/app/src/trace/cpp/CMakeLists.txt @@ -0,0 +1,237 @@ +cmake_minimum_required(VERSION 3.22.1) + +project(trace_replay_runner) + +set(APITRACE_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../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) + +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) +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(glproc STATIC + "${APITRACE_GENERATED_DIR}/glproc.cpp" + apitrace_glproc_android.cpp) +target_include_directories(glproc PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/lib/os" + "${APITRACE_ROOT}/thirdparty/khronos") +target_link_libraries(glproc PUBLIC 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(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(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(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(image PUBLIC "${APITRACE_ROOT}/lib/image") +target_link_libraries(image PUBLIC md5 PNG::PNG) + +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_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_library(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(retrace_common PUBLIC RETRACE) +target_compile_definitions(retrace_common PRIVATE exit=mobilegl_apitrace_exit) +target_compile_options(retrace_common PRIVATE "-include${CMAKE_CURRENT_LIST_DIR}/apitrace_exit.hpp") +target_include_directories(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(retrace_common PUBLIC image common) + +add_library(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" + # GLX here is only the apitrace trace-call frontend. Android execution + # still goes through apitrace_glws_android.cpp and MobileGL's EGL entrypoints. + "${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" + apitrace_glws_android.cpp) +target_include_directories(glretrace_common PUBLIC + "${APITRACE_GENERATED_DIR}" + "${APITRACE_ROOT}/dispatch" + "${APITRACE_ROOT}/helpers" + "${APITRACE_ROOT}/retrace") +target_compile_definitions(glretrace_common PRIVATE exit=mobilegl_apitrace_exit) +target_compile_options(glretrace_common PRIVATE "-include${CMAKE_CURRENT_LIST_DIR}/apitrace_exit.hpp") +target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc) + +add_library(trace_replay_runner SHARED + trace_replay_core.cpp + trace_replay_jni.cpp) + +target_compile_features(trace_replay_runner PRIVATE cxx_std_17) + +target_include_directories(trace_replay_runner PRIVATE + "${APITRACE_ROOT}/lib/image") + +target_link_libraries(trace_replay_runner + glretrace_common + retrace_common + image + android + dl) diff --git a/android-plugin/app/src/trace/cpp/apitrace_exit.hpp b/android-plugin/app/src/trace/cpp/apitrace_exit.hpp new file mode 100644 index 00000000..75b9bd01 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/apitrace_exit.hpp @@ -0,0 +1,7 @@ +#pragma once + +struct MobileGLRetraceExit { + int status; +}; + +extern "C" [[noreturn]] void mobilegl_apitrace_exit(int status); diff --git a/android-plugin/app/src/trace/cpp/apitrace_glproc_android.cpp b/android-plugin/app/src/trace/cpp/apitrace_glproc_android.cpp new file mode 100644 index 00000000..784da911 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/apitrace_glproc_android.cpp @@ -0,0 +1,156 @@ +#include "glproc.hpp" + +#include +#include +#include +#include +#include + +void *_libGlHandle = nullptr; + +namespace { + +void *LookupSymbol(const char *procName); + +void *GetMobileGlHandle() { + if (_libGlHandle != nullptr) { + return _libGlHandle; + } + + _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); + } + const char *stats = getenv("MOBILEGL_PRESENT_STATS"); + if (stats == nullptr || stats[0] == '\0' || strcmp(stats, "0") == 0 || + pixels == nullptr || width <= 0 || height <= 0 || + format != GL_RGBA || type != GL_UNSIGNED_BYTE) { + return; + } + const auto *bytes = static_cast(pixels); + const size_t pixelCount = static_cast(width) * static_cast(height); + size_t nonBlack = 0; + size_t nonTransparent = 0; + for (size_t i = 0; i < pixelCount; ++i) { + const unsigned char *p = bytes + i * 4; + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + ++nonBlack; + } + if (p[3] != 0) { + ++nonTransparent; + } + } + fprintf(stderr, + "MOBILEGL_GLPROC_READPIXELS x=%d y=%d width=%d height=%d nonBlack=%zu/%zu alpha=%zu/%zu real=%p\n", + x, y, width, height, nonBlack, pixelCount, nonTransparent, pixelCount, + reinterpret_cast(gRealGlReadPixels)); +} + +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/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp b/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp new file mode 100644 index 00000000..2fd14bd5 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp @@ -0,0 +1,412 @@ +#include "glws.hpp" +#include "retrace.hpp" + +#include +#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 PfnEglCreateWindowSurface = EGLSurface (*)(EGLDisplay, EGLConfig, EGLNativeWindowType, 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; + PfnEglCreateWindowSurface createWindowSurface = 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; +ANativeWindow *gNativeWindow = nullptr; +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) { + 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.createWindowSurface, "eglCreateWindowSurface") && + 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 AndroidVisual final : public glws::Visual { +public: + EGLConfig config = nullptr; + EGLenum api = EGL_OPENGL_API; + EGLint nativeVisualId = 0; + + explicit AndroidVisual(glws::Profile prof) : Visual(prof) {} +}; + +class AndroidDrawable final : public glws::Drawable { +public: + EGLSurface surface = EGL_NO_SURFACE; + + AndroidDrawable(const AndroidVisual *visual, int width, int height, bool pbuffer) + : Drawable(visual, width, height, pbuffer) { + createSurface(); + } + + ~AndroidDrawable() 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) { + 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); + } + const char *targetCall = getenv("MOBILEGL_PRESENT_DUMP_CALL"); + const char *presentStats = getenv("MOBILEGL_PRESENT_STATS"); + if (targetCall != nullptr && targetCall[0] != '\0' && + presentStats != nullptr && presentStats[0] != '\0' && strcmp(presentStats, "0") != 0) { + std::cerr << "MOBILEGL_TRACE_SWAP call=" << callNo + << " target=" << targetCall << "\n"; + } + setenv("MOBILEGL_PRESENT_CURRENT_CALL", callNo, 1); + gEgl.swapBuffers(gDisplay, surface); + unsetenv("MOBILEGL_PRESENT_CURRENT_CALL"); + } + } + +private: + void createSurface() { + const int surfaceWidth = ResolveWidth(width); + const int surfaceHeight = ResolveHeight(height); + if (!pbuffer && gNativeWindow != nullptr) { + EGLint nativeVisualId = static_cast(visual)->nativeVisualId; + ANativeWindow_setBuffersGeometry(gNativeWindow, surfaceWidth, surfaceHeight, nativeVisualId); + surface = gEgl.createWindowSurface( + gDisplay, + static_cast(visual)->config, + gNativeWindow, + nullptr); + } else { + const EGLint attribs[] = { + EGL_WIDTH, surfaceWidth, + EGL_HEIGHT, surfaceHeight, + EGL_NONE, + }; + surface = gEgl.createPbufferSurface( + gDisplay, + static_cast(visual)->config, + attribs); + } + if (surface == EGL_NO_SURFACE) { + std::cerr << "error: EGL surface 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 AndroidContext final : public glws::Context { +public: + EGLContext context = EGL_NO_CONTEXT; + + AndroidContext(const AndroidVisual *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"; + } + } + + ~AndroidContext() override { + if (context != EGL_NO_CONTEXT) { + gEgl.destroyContext(gDisplay, context); + } + } +}; + +const AndroidDrawable *AsAndroidDrawable(const glws::Drawable *drawable) { + return static_cast(drawable); +} + +const AndroidContext *AsAndroidContext(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 AndroidVisual(profile); + visual->doubleBuffer = doubleBuffer; + // This glws implementation adapts apitrace's platform calls to EGL only. + // A GLX trace may reach this path through glretrace_glx.cpp, but MobileGL is + // still accessed through egl* and gl* symbols, never through glX* symbols. + 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, gNativeWindow == nullptr ? EGL_PBUFFER_BIT : EGL_WINDOW_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; + } + gEgl.getConfigAttrib(gDisplay, visual->config, EGL_NATIVE_VISUAL_ID, &visual->nativeVisualId); + return visual; +} + +Drawable *createDrawable(const Visual *visual, int width, int height, const pbuffer_info *pbInfo) { + return new AndroidDrawable(static_cast(visual), ResolveWidth(width), + ResolveHeight(height), pbInfo != nullptr); +} + +Context *createContext(const Visual *visual, Context *shareContext, bool) { + return new AndroidContext(static_cast(visual), shareContext); +} + +bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *context) { + EGLSurface drawSurface = drawable == nullptr ? EGL_NO_SURFACE : AsAndroidDrawable(drawable)->surface; + EGLSurface readSurface = readable == nullptr ? drawSurface : AsAndroidDrawable(readable)->surface; + EGLContext eglContext = context == nullptr ? EGL_NO_CONTEXT : AsAndroidContext(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_native_window(ANativeWindow *window) { + if (gNativeWindow == window) { + return; + } + if (gNativeWindow != nullptr) { + ANativeWindow_release(gNativeWindow); + } + gNativeWindow = window; + if (gNativeWindow != nullptr) { + ANativeWindow_acquire(gNativeWindow); + } +} + +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/android-plugin/app/src/trace/cpp/trace_replay_core.cpp b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp new file mode 100644 index 00000000..36787950 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp @@ -0,0 +1,611 @@ +#include "trace_replay_core.hpp" + +#include +#include "apitrace_exit.hpp" +#include "png.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int main(int argc, char** argv); + +namespace mobilegl_trace { +namespace { + +struct RgbaImage { + int width = 0; + int height = 0; + std::vector pixels; +}; + +class ScopedFdRedirect { +public: + explicit ScopedFdRedirect(const std::string& path) + : stdoutCopy(dup(STDOUT_FILENO)), stderrCopy(dup(STDERR_FILENO)) { + int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0664); + if (fd >= 0) { + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } + } + + ~ScopedFdRedirect() { + fflush(stdout); + fflush(stderr); + if (stdoutCopy >= 0) { + dup2(stdoutCopy, STDOUT_FILENO); + close(stdoutCopy); + } + if (stderrCopy >= 0) { + dup2(stderrCopy, STDERR_FILENO); + close(stderrCopy); + } + } + +private: + int stdoutCopy = -1; + int stderrCopy = -1; +}; + +bool Exists(const std::string& path) { + struct stat st {}; + return !path.empty() && stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode); +} + +bool EnsureDirectory(const std::string& path) { + if (path.empty()) { + return false; + } + struct stat st {}; + if (stat(path.c_str(), &st) == 0) { + return S_ISDIR(st.st_mode); + } + return mkdir(path.c_str(), 0775) == 0 || errno == EEXIST; +} + +std::string JsonEscape(const std::string& value) { + std::ostringstream out; + for (char ch : value) { + switch (ch) { + case '\\': + out << "\\\\"; + break; + case '"': + out << "\\\""; + break; + case '\n': + out << "\\n"; + break; + case '\r': + out << "\\r"; + break; + case '\t': + out << "\\t"; + break; + default: + out << ch; + break; + } + } + return out.str(); +} + +bool LoadMobileGL(const Request& request, std::string& error) { + setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1); + + void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL); + if (handle == nullptr) { + const char* dlError = dlerror(); + error = dlError == nullptr ? "dlopen(libMobileGL.so) failed" : dlError; + return false; + } + return true; +} + +bool CopyFile(const std::string& from, const std::string& to) { + std::ifstream input(from, std::ios::binary); + std::ofstream output(to, std::ios::binary | std::ios::trunc); + if (!input || !output) { + return false; + } + output << input.rdbuf(); + return static_cast(output); +} + +bool ReadPngRgba(const std::string& path, RgbaImage& image, std::string& error) { + FILE* file = fopen(path.c_str(), "rb"); + if (file == nullptr) { + error = "failed to open PNG: " + path; + return false; + } + + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (png == nullptr) { + fclose(file); + error = "png_create_read_struct failed"; + return false; + } + + png_infop info = png_create_info_struct(png); + if (info == nullptr) { + png_destroy_read_struct(&png, nullptr, nullptr); + fclose(file); + error = "png_create_info_struct failed"; + return false; + } + + if (setjmp(png_jmpbuf(png)) != 0) { + png_destroy_read_struct(&png, &info, nullptr); + fclose(file); + error = "libpng failed to decode: " + path; + return false; + } + + png_init_io(png, file); + png_read_info(png, info); + + png_uint_32 width = png_get_image_width(png, info); + png_uint_32 height = png_get_image_height(png, info); + int colorType = png_get_color_type(png, info); + int bitDepth = png_get_bit_depth(png, info); + + if (bitDepth == 16) { + png_set_strip_16(png); + } + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(png); + } + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(png); + } + if (png_get_valid(png, info, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(png); + } + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(png); + } + if ((colorType & PNG_COLOR_MASK_ALPHA) == 0) { + png_set_filler(png, 0xff, PNG_FILLER_AFTER); + } + + png_read_update_info(png, info); + png_size_t rowBytes = png_get_rowbytes(png, info); + if (width == 0 || height == 0 || rowBytes < width * 4) { + png_destroy_read_struct(&png, &info, nullptr); + fclose(file); + error = "decoded PNG has invalid dimensions: " + path; + return false; + } + + image.width = static_cast(width); + image.height = static_cast(height); + image.pixels.resize(static_cast(image.width) * image.height * 4); + + std::vector rowsStorage; + std::vector rows(height); + if (rowBytes == width * 4) { + for (png_uint_32 y = 0; y < height; ++y) { + rows[y] = image.pixels.data() + static_cast(y) * image.width * 4; + } + } else { + rowsStorage.resize(static_cast(rowBytes) * height); + for (png_uint_32 y = 0; y < height; ++y) { + rows[y] = rowsStorage.data() + static_cast(y) * rowBytes; + } + } + + png_read_image(png, rows.data()); + png_read_end(png, nullptr); + png_destroy_read_struct(&png, &info, nullptr); + fclose(file); + + if (!rowsStorage.empty()) { + for (int y = 0; y < image.height; ++y) { + memcpy(image.pixels.data() + static_cast(y) * image.width * 4, + rowsStorage.data() + static_cast(y) * rowBytes, + static_cast(image.width) * 4); + } + } + + return true; +} + +bool WritePngRgba(const std::string& path, const RgbaImage& image, std::string& error) { + FILE* file = fopen(path.c_str(), "wb"); + if (file == nullptr) { + error = "failed to open PNG for write: " + path; + return false; + } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (png == nullptr) { + fclose(file); + error = "png_create_write_struct failed"; + return false; + } + + png_infop info = png_create_info_struct(png); + if (info == nullptr) { + png_destroy_write_struct(&png, nullptr); + fclose(file); + error = "png_create_info_struct failed"; + return false; + } + + if (setjmp(png_jmpbuf(png)) != 0) { + png_destroy_write_struct(&png, &info); + fclose(file); + error = "libpng failed to write: " + path; + return false; + } + + png_init_io(png, file); + png_set_IHDR(png, info, image.width, image.height, 8, PNG_COLOR_TYPE_RGBA, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + std::vector rows(static_cast(image.height)); + for (int y = 0; y < image.height; ++y) { + rows[static_cast(y)] = + const_cast(image.pixels.data() + static_cast(y) * image.width * 4); + } + png_write_image(png, rows.data()); + png_write_end(png, nullptr); + png_destroy_write_struct(&png, &info); + fclose(file); + return true; +} + +bool ReadPpmRgbAsRgba(const std::string& path, RgbaImage& image, std::string& error) { + std::ifstream file(path, std::ios::binary); + if (!file) { + error = "failed to open PPM: " + path; + return false; + } + + auto readToken = [&file]() -> std::string { + std::string token; + char ch = 0; + while (file.get(ch)) { + if (ch == '#') { + file.ignore(std::numeric_limits::max(), '\n'); + continue; + } + if (!std::isspace(static_cast(ch))) { + token.push_back(ch); + break; + } + } + while (file.get(ch)) { + if (std::isspace(static_cast(ch))) { + break; + } + token.push_back(ch); + } + return token; + }; + + const std::string magic = readToken(); + const std::string widthToken = readToken(); + const std::string heightToken = readToken(); + const std::string maxToken = readToken(); + if (magic != "P6" || widthToken.empty() || heightToken.empty() || maxToken != "255") { + error = "unsupported PPM header: " + path; + return false; + } + + image.width = std::stoi(widthToken); + image.height = std::stoi(heightToken); + if (image.width <= 0 || image.height <= 0) { + error = "PPM has invalid dimensions: " + path; + return false; + } + + std::vector rgb(static_cast(image.width) * image.height * 3); + file.read(reinterpret_cast(rgb.data()), static_cast(rgb.size())); + if (file.gcount() != static_cast(rgb.size())) { + error = "PPM payload is truncated: " + path; + return false; + } + + image.pixels.resize(static_cast(image.width) * image.height * 4); + for (std::size_t i = 0, j = 0; i < rgb.size(); i += 3, j += 4) { + image.pixels[j + 0] = rgb[i + 0]; + image.pixels[j + 1] = rgb[i + 1]; + image.pixels[j + 2] = rgb[i + 2]; + image.pixels[j + 3] = 0xff; + } + return true; +} + +std::string SnapshotPathForCall(const Request& request) { + char call[16]; + snprintf(call, sizeof(call), "%010lld", request.targetCall); + return request.outputDir + "/actual." + call + ".png"; +} + +int RunRetraceMain(const Request& request) { + std::string prefix = request.outputDir + "/actual."; + std::string callSet = std::to_string(request.targetCall); + + std::string arg0 = "mobilegl-glretrace"; + std::string argBenchmark = "-b"; + std::string argSingleThread = "--singlethread"; + std::string argNoContextCheck = "--no-context-check"; + std::string argSnapshotAlpha = "--snapshot-alpha"; + std::string argSnapshotPrefix = "-s"; + std::string argSnapshotCall = "-S"; + std::string tracePath = request.tracePath; + + char* argv[] = { + arg0.data(), + argBenchmark.data(), + argSingleThread.data(), + argNoContextCheck.data(), + argSnapshotAlpha.data(), + argSnapshotPrefix.data(), + prefix.data(), + argSnapshotCall.data(), + callSet.data(), + tracePath.data(), + nullptr, + }; + return main(10, argv); +} + +bool RunRetrace(const Request& request, Result& result) { + int status = 0; + try { + ScopedFdRedirect redirect(request.outputDir + "/retrace.log"); + status = RunRetraceMain(request); + } catch (const MobileGLRetraceExit& retraceExit) { + status = retraceExit.status; + } catch (const std::exception& exception) { + result.statusCode = STATUS_RETRACE_FAILED; + result.message = "retrace failed with exception: " + std::string(exception.what()); + return false; + } catch (...) { + result.statusCode = STATUS_RETRACE_FAILED; + result.message = "retrace failed with unknown exception"; + return false; + } + + if (status != 0) { + std::ostringstream message; + message << "retrace failed with status " << status; + result.statusCode = STATUS_RETRACE_FAILED; + result.message = message.str(); + return false; + } + + std::string snapshotPath = SnapshotPathForCall(request); + const std::string presentPath = request.outputDir + "/present.ppm"; + const bool hasSnapshot = Exists(snapshotPath); + const bool hasPresentDump = request.backend == "DirectVulkan" && Exists(presentPath); + if (!hasSnapshot && !hasPresentDump) { + result.statusCode = STATUS_RETRACE_FAILED; + result.message = "retrace completed but did not create expected snapshot: " + snapshotPath; + return false; + } + if (hasSnapshot && !hasPresentDump) { + if (!CopyFile(snapshotPath, result.actualPath)) { + result.statusCode = STATUS_IO_ERROR; + result.message = "failed to copy snapshot to " + result.actualPath; + return false; + } + } + if (request.backend == "DirectVulkan") { + if (hasPresentDump) { + RgbaImage present; + std::string imageError; + if (!ReadPpmRgbAsRgba(presentPath, present, imageError) || + !WritePngRgba(request.outputDir + "/present.png", present, imageError)) { + result.statusCode = STATUS_IO_ERROR; + result.message = imageError.empty() + ? "failed to convert DirectVulkan present dump to PNG" + : imageError; + return false; + } + if (!WritePngRgba(result.actualPath, present, imageError)) { + result.statusCode = STATUS_IO_ERROR; + result.message = imageError.empty() + ? "failed to write DirectVulkan present dump to actual PNG" + : imageError; + return false; + } + } + } + return true; +} + +int ChannelValue(const RgbaImage& image, int x, int y, int channel) { + return image.pixels[(static_cast(y) * image.width + x) * 4 + channel]; +} + +bool CompareWithGolden(const Request& request, Result& result) { + if (request.goldenPath.empty()) { + result.passed = true; + result.statusCode = STATUS_OK; + result.message = "retrace completed; golden_path was not provided"; + result.mismatchPixels = 0; + return true; + } + if (!Exists(request.goldenPath)) { + result.statusCode = STATUS_INVALID_ARGUMENT; + result.message = "golden_path does not exist or is not a regular file"; + return false; + } + + RgbaImage actual; + RgbaImage golden; + std::string pngError; + if (!ReadPngRgba(result.actualPath, actual, pngError) || + !ReadPngRgba(request.goldenPath, golden, pngError)) { + result.statusCode = STATUS_COMPARE_FAILED; + result.message = pngError.empty() ? "failed to decode actual or golden PNG" : pngError; + return false; + } + + int x0 = request.cropX; + int y0 = request.cropY; + if (request.cropWidth <= 0 && request.cropHeight <= 0 && + (actual.width != golden.width || actual.height != golden.height)) { + result.statusCode = STATUS_COMPARE_FAILED; + std::ostringstream message; + message << "actual image size " << actual.width << "x" << actual.height + << " does not match golden image size " << golden.width << "x" << golden.height; + result.message = message.str(); + return false; + } + + int compareWidth = request.cropWidth > 0 ? request.cropWidth : actual.width; + int compareHeight = request.cropHeight > 0 ? request.cropHeight : actual.height; + if (compareWidth <= 0 || compareHeight <= 0 || + x0 < 0 || y0 < 0 || + x0 + compareWidth > actual.width || + y0 + compareHeight > actual.height || + x0 + compareWidth > golden.width || + y0 + compareHeight > golden.height) { + result.statusCode = STATUS_INVALID_ARGUMENT; + result.message = "compare crop is outside actual or golden image bounds"; + return false; + } + + const int fuzz = std::max(0, std::min(100, request.fuzzPercent)) * 255 / 100; + long long mismatch = 0; + for (int y = 0; y < compareHeight; ++y) { + for (int x = 0; x < compareWidth; ++x) { + bool different = false; + for (unsigned c = 0; c < 3; ++c) { + int a = ChannelValue(actual, x0 + x, y0 + y, c); + int g = ChannelValue(golden, x0 + x, y0 + y, c); + if (std::abs(a - g) > fuzz) { + different = true; + break; + } + } + if (different) { + ++mismatch; + } + } + } + + result.mismatchPixels = mismatch; + result.passed = mismatch <= request.tolerance; + result.statusCode = result.passed ? STATUS_OK : STATUS_COMPARE_FAILED; + std::ostringstream message; + message << "retrace completed; mismatchPixels=" << mismatch + << ", tolerance=" << request.tolerance + << ", fuzzPercent=" << request.fuzzPercent; + result.message = message.str(); + return result.passed; +} + +} // namespace + +extern "C" [[noreturn]] void mobilegl_apitrace_exit(int status) { + throw MobileGLRetraceExit{status}; +} + +bool WriteResultJson(const Request& request, const Result& result) { + std::ofstream file(result.resultPath, std::ios::out | std::ios::trunc); + if (!file) { + return false; + } + file << "{\n"; + file << " \"passed\": " << (result.passed ? "true" : "false") << ",\n"; + file << " \"statusCode\": " << result.statusCode << ",\n"; + file << " \"message\": \"" << JsonEscape(result.message) << "\",\n"; + file << " \"tracePath\": \"" << JsonEscape(request.tracePath) << "\",\n"; + file << " \"goldenPath\": \"" << JsonEscape(request.goldenPath) << "\",\n"; + file << " \"actualPath\": \"" << JsonEscape(result.actualPath) << "\",\n"; + file << " \"backend\": \"" << JsonEscape(request.backend) << "\",\n"; + file << " \"targetFrame\": " << request.targetFrame << ",\n"; + file << " \"targetCall\": " << request.targetCall << ",\n"; + file << " \"width\": " << request.width << ",\n"; + file << " \"height\": " << request.height << ",\n"; + file << " \"cropX\": " << request.cropX << ",\n"; + file << " \"cropY\": " << request.cropY << ",\n"; + file << " \"cropWidth\": " << request.cropWidth << ",\n"; + file << " \"cropHeight\": " << request.cropHeight << ",\n"; + file << " \"tolerance\": " << request.tolerance << ",\n"; + file << " \"fuzzPercent\": " << request.fuzzPercent << ",\n"; + file << " \"mismatchPixels\": " << result.mismatchPixels << "\n"; + file << "}\n"; + return true; +} + +Result RunTraceReplay(const Request& request) { + Result result; + result.resultPath = request.outputDir + "/result.json"; + result.actualPath = request.outputDir + "/actual.png"; + + if (!EnsureDirectory(request.outputDir)) { + result.statusCode = STATUS_IO_ERROR; + result.message = "failed to create output directory: " + request.outputDir; + return result; + } + + if (request.backend != "DirectGLES" && request.backend != "DirectVulkan") { + result.statusCode = STATUS_INVALID_ARGUMENT; + result.message = "backend must be DirectGLES or DirectVulkan"; + return result; + } + + if (!Exists(request.tracePath)) { + result.statusCode = STATUS_INVALID_ARGUMENT; + result.message = "trace_path does not exist or is not a regular file"; + return result; + } + + if (request.targetCall < 0) { + result.statusCode = STATUS_INVALID_ARGUMENT; + result.message = "target_call must be set for dump-images style replay"; + return result; + } + + if (request.backend == "DirectVulkan") { + std::string presentDumpPath = request.outputDir + "/present.ppm"; + std::string presentDumpCall = std::to_string(request.targetCall); + setenv("MOBILEGL_PRESENT_DUMP_PATH", presentDumpPath.c_str(), 1); + setenv("MOBILEGL_PRESENT_DUMP_CALL", presentDumpCall.c_str(), 1); + } else { + unsetenv("MOBILEGL_PRESENT_STATS"); + unsetenv("MOBILEGL_PRESENT_DUMP_PATH"); + unsetenv("MOBILEGL_PRESENT_DUMP_CALL"); + unsetenv("MOBILEGL_TEXTURE_UPLOAD_STATS"); + unsetenv("MOBILEGL_DESCRIPTOR_STATS"); + } + + std::string mobileGlError; + if (!LoadMobileGL(request, mobileGlError)) { + result.statusCode = STATUS_MOBILEGL_LOAD_ERROR; + result.message = "failed to load MobileGL: " + mobileGlError; + return result; + } + + if (!RunRetrace(request, result)) { + return result; + } + + CompareWithGolden(request, result); + return result; +} + +} // namespace mobilegl_trace diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp new file mode 100644 index 00000000..2cde9dd4 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +namespace mobilegl_trace { + +enum StatusCode { + STATUS_OK = 0, + STATUS_INVALID_ARGUMENT = 1, + STATUS_IO_ERROR = 2, + STATUS_MOBILEGL_LOAD_ERROR = 3, + STATUS_RETRACE_NOT_LINKED = 4, + STATUS_RETRACE_FAILED = 5, + STATUS_COMPARE_FAILED = 6, +}; + +struct Request { + std::string tracePath; + std::string goldenPath; + std::string outputDir; + std::string backend; + std::string mobileGlLibrary = "libMobileGL.so"; + int targetFrame = -1; + long long targetCall = -1; + int width = 0; + int height = 0; + int cropX = 0; + int cropY = 0; + int cropWidth = 0; + int cropHeight = 0; + int tolerance = 0; + int fuzzPercent = 20; +}; + +struct Result { + bool passed = false; + int statusCode = STATUS_OK; + std::string message; + std::string resultPath; + std::string actualPath; + long long mismatchPixels = -1; +}; + +Result RunTraceReplay(const Request& request); +bool WriteResultJson(const Request& request, const Result& result); + +} // namespace mobilegl_trace diff --git a/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp new file mode 100644 index 00000000..e2d3b845 --- /dev/null +++ b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp @@ -0,0 +1,97 @@ +#include "trace_replay_core.hpp" + +#include +#include +#include + +#include + +extern "C" void mobilegl_trace_set_native_window(ANativeWindow *window); +extern "C" void mobilegl_trace_set_requested_size(int width, int height); + +namespace { + +std::string ToString(JNIEnv* env, jstring value) { + if (value == nullptr) { + return {}; + } + const char* chars = env->GetStringUTFChars(value, nullptr); + std::string out = chars == nullptr ? "" : chars; + if (chars != nullptr) { + env->ReleaseStringUTFChars(value, chars); + } + return out; +} + +jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) { + jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult"); + if (clazz == nullptr) { + return nullptr; + } + jmethodID ctor = env->GetMethodID(clazz, "", "(ZILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + if (ctor == nullptr) { + return nullptr; + } + jstring message = env->NewStringUTF(result.message.c_str()); + jstring resultPath = env->NewStringUTF(result.resultPath.c_str()); + jstring actualPath = env->NewStringUTF(result.actualPath.c_str()); + jobject object = env->NewObject(clazz, ctor, result.passed ? JNI_TRUE : JNI_FALSE, result.statusCode, message, + resultPath, actualPath); + env->DeleteLocalRef(message); + env->DeleteLocalRef(resultPath); + env->DeleteLocalRef(actualPath); + return object; +} + +} // namespace + +extern "C" JNIEXPORT jobject JNICALL +Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv* env, + jclass, + jobject surface, + jstring tracePath, + jstring goldenPath, + jstring outputDir, + jstring backend, + jint targetFrame, + jlong targetCall, + jint width, + jint height, + jint tolerance, + jint cropX, + jint cropY, + jint cropWidth, + jint cropHeight, + jint fuzzPercent) { + mobilegl_trace::Request request; + request.tracePath = ToString(env, tracePath); + request.goldenPath = ToString(env, goldenPath); + request.outputDir = ToString(env, outputDir); + request.backend = ToString(env, backend); + request.targetFrame = targetFrame; + request.targetCall = targetCall; + request.width = width; + request.height = height; + request.tolerance = tolerance; + request.cropX = cropX; + request.cropY = cropY; + request.cropWidth = cropWidth; + request.cropHeight = cropHeight; + request.fuzzPercent = fuzzPercent; + + mobilegl_trace_set_requested_size(request.width, request.height); + const bool needsNativeWindow = request.backend == "DirectVulkan"; + ANativeWindow *window = needsNativeWindow && surface != nullptr ? ANativeWindow_fromSurface(env, surface) : nullptr; + mobilegl_trace_set_native_window(window); + if (window != nullptr) { + ANativeWindow_release(window); + } + + mobilegl_trace::Result result = mobilegl_trace::RunTraceReplay(request); + mobilegl_trace_set_native_window(nullptr); + mobilegl_trace_set_requested_size(0, 0); + if (!result.resultPath.empty()) { + mobilegl_trace::WriteResultJson(request, result); + } + return MakeResult(env, result); +} diff --git a/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java new file mode 100644 index 00000000..231bdad6 --- /dev/null +++ b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java @@ -0,0 +1,233 @@ +package top.mobilegl.plugin.trace; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.Window; +import android.view.WindowManager; +import android.widget.TextView; + +import java.io.File; + +public final class TraceReplayActivity extends Activity { + public static final String ACTION_TRACE_REPLAY = "top.mobilegl.plugin.TRACE_REPLAY"; + + private static final String TAG = "MobileGLTraceRunner"; + static { + System.loadLibrary("trace_replay_runner"); + } + + private TextView statusView; + private TraceReplayRequest request; + private boolean started; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Window window = getWindow(); + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ); + SurfaceView surfaceView = new SurfaceView(this); + setContentView(surfaceView); + + Intent intent = getIntent(); + request = TraceReplayRequest.from( + intent, + getFilesDir(), + getString(top.mobilegl.plugin.R.string.mobilegl_default_backend) + ); + statusView = new TextView(this); + statusView.setText("Waiting for render surface\n" + request.outputDir); + statusView.setPadding(24, 24, 24, 24); + addContentView(statusView, new android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + )); + + SurfaceHolder holder = surfaceView.getHolder(); + if (request.width > 0 && request.height > 0) { + holder.setFixedSize(request.width, request.height); + } + holder.addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + startReplay(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } + }); + } + + private void startReplay(SurfaceHolder holder) { + if (started) { + return; + } + started = true; + statusView.setText("Running trace replay\n" + request.outputDir); + new Thread(() -> runRequest(request, holder.getSurface()), "MobileGLTraceReplay").start(); + } + + private void runRequest(TraceReplayRequest request, Surface surface) { + TraceReplayResult result = nativeRunTraceReplay( + surface, + request.tracePath, + request.goldenPath, + request.outputDir, + request.backend, + request.targetFrame, + request.targetCall, + request.width, + request.height, + request.tolerance, + request.cropX, + request.cropY, + request.cropWidth, + request.cropHeight, + request.fuzzPercent + ); + Log.i(TAG, result.toString()); + TraceReplayResult finalResult = result; + runOnUiThread(() -> { + statusView.setText(finalResult.toString()); + finish(); + }); + } + + private static native TraceReplayResult nativeRunTraceReplay( + Surface surface, + String tracePath, + String goldenPath, + String outputDir, + String backend, + int targetFrame, + long targetCall, + int width, + int height, + int tolerance, + int cropX, + int cropY, + int cropWidth, + int cropHeight, + int fuzzPercent + ); + + private static final class TraceReplayRequest { + final String tracePath; + final String goldenPath; + final String outputDir; + final String backend; + final int targetFrame; + final long targetCall; + final int width; + final int height; + final int tolerance; + final int cropX; + final int cropY; + final int cropWidth; + final int cropHeight; + final int fuzzPercent; + + private TraceReplayRequest( + String tracePath, + String goldenPath, + String outputDir, + String backend, + int targetFrame, + long targetCall, + int width, + int height, + int tolerance, + int cropX, + int cropY, + int cropWidth, + int cropHeight, + int fuzzPercent + ) { + this.tracePath = tracePath; + this.goldenPath = goldenPath; + this.outputDir = outputDir; + this.backend = backend; + this.targetFrame = targetFrame; + this.targetCall = targetCall; + this.width = width; + this.height = height; + this.tolerance = tolerance; + this.cropX = cropX; + this.cropY = cropY; + this.cropWidth = cropWidth; + this.cropHeight = cropHeight; + this.fuzzPercent = fuzzPercent; + } + + static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) { + String outputDir = readString(intent, "output_dir", new File(filesDir, "trace-replay").getAbsolutePath()); + return new TraceReplayRequest( + readString(intent, "trace_path", ""), + readString(intent, "golden_path", ""), + outputDir, + readString(intent, "backend", defaultBackend), + intent.getIntExtra("target_frame", -1), + intent.getLongExtra("target_call", -1L), + intent.getIntExtra("width", 0), + intent.getIntExtra("height", 0), + intent.getIntExtra("tolerance", 0), + intent.getIntExtra("crop_x", 0), + intent.getIntExtra("crop_y", 0), + intent.getIntExtra("crop_width", 0), + intent.getIntExtra("crop_height", 0), + intent.getIntExtra("fuzz_percent", 20) + ); + } + + private static String readString(Intent intent, String key, String fallback) { + String value = intent.getStringExtra(key); + return value == null ? fallback : value; + } + } + + public static final class TraceReplayResult { + public final boolean passed; + public final int statusCode; + public final String message; + public final String resultPath; + public final String actualPath; + + public TraceReplayResult( + boolean passed, + int statusCode, + String message, + String resultPath, + String actualPath + ) { + this.passed = passed; + this.statusCode = statusCode; + this.message = message; + this.resultPath = resultPath; + this.actualPath = actualPath; + } + + @Override + public String toString() { + return "TraceReplayResult{" + + "passed=" + passed + + ", statusCode=" + statusCode + + ", message='" + message + '\'' + + ", resultPath='" + resultPath + '\'' + + ", actualPath='" + actualPath + '\'' + + '}'; + } + } +} diff --git a/android-plugin/build.gradle b/android-plugin/build.gradle index f92b08bb..705e65e8 100644 --- a/android-plugin/build.gradle +++ b/android-plugin/build.gradle @@ -7,6 +7,9 @@ subprojects { subproject -> subproject.plugins.withId('com.android.library') { subproject.android { defaultConfig { + ndk { + abiFilters 'arm64-v8a' + } externalNativeBuild { cmake { cppFlags '-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO' diff --git a/android-plugin/third_party/apitrace b/android-plugin/third_party/apitrace new file mode 160000 index 00000000..055c4573 --- /dev/null +++ b/android-plugin/third_party/apitrace @@ -0,0 +1 @@ +Subproject commit 055c4573af4435d918fc2cd34663a48735680701