[Feat] (android-plugin): add standalone trace replay profile

This commit is contained in:
2026-06-15 16:57:50 +08:00
parent d51723c153
commit c2d6134cfa
16 changed files with 1997 additions and 5 deletions
+3
View File
@@ -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
@@ -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<SizeT>(std::max(texelSize.z(), 1));
const SizeT pixelCount = static_cast<SizeT>(texelSize.x()) * static_cast<SizeT>(texelSize.y()) * depth;
if (pixelCount > 0 && byteSize % pixelCount == 0) {
channels = static_cast<Uint32>(std::min<SizeT>(byteSize / pixelCount, 4));
}
}
if (!ShouldDumpTextureUploadStats() || data == nullptr || byteSize == 0 || channels == 0) {
return;
}
@@ -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<Int>(0, scissorBox.x());
const Int y0 = std::max<Int>(0, scissorBox.y());
const Int x1 = std::min<Int>(framebufferExtent.x(), scissorBox.x() + std::max<Int>(0, scissorBox.z()));
const Int y1 = std::min<Int>(framebufferExtent.y(), scissorBox.y() + std::max<Int>(0, scissorBox.w()));
VkRect2D scissor{};
scissor.offset = {x0, y0};
scissor.extent = {
static_cast<Uint32>(std::max<Int>(0, x1 - x0)),
static_cast<Uint32>(std::max<Int>(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<Int>(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<SizeT>(width) * static_cast<SizeT>(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<VkDeviceSize>(presentStatsExtent.width) *
static_cast<VkDeviceSize>(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<SizeT>(pixelCount * 3));
}
} else if (PresentStatsEnabled()) {
std::fprintf(stderr, "MOBILEGL_PRESENT_DUMP_FAILED path=%s\n", presentDumpPath);
}
}
}
+69
View File
@@ -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
```
+24 -1
View File
@@ -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')
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".trace.TraceReplayActivity"
android:exported="true"
android:theme="@style/Theme.MobileGLPlugin">
<intent-filter>
<action android:name="top.mobilegl.plugin.TRACE_REPLAY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -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)
@@ -0,0 +1,7 @@
#pragma once
struct MobileGLRetraceExit {
int status;
};
extern "C" [[noreturn]] void mobilegl_apitrace_exit(int status);
@@ -0,0 +1,156 @@
#include "glproc.hpp"
#include <GLES3/gl3.h>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <dlfcn.h>
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<GlReadBuffer>(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<GlReadPixels>(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<const unsigned char *>(pixels);
const size_t pixelCount = static_cast<size_t>(width) * static_cast<size_t>(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<void *>(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<GlGetIntegerv>(LookupSymbol("glGetIntegerv"));
}
if (gRealGlGetIntegerv != nullptr) {
gRealGlGetIntegerv(pname, data);
}
}
GLenum MobileGLTraceGetError() {
if (gRealGlGetError == nullptr) {
gRealGlGetError = reinterpret_cast<GlGetError>(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<void *>(&MobileGLTraceReadPixels);
}
if (strcmp(procName, "glReadBuffer") == 0) {
return reinterpret_cast<void *>(&MobileGLTraceReadBuffer);
}
if (strcmp(procName, "glGetIntegerv") == 0) {
return reinterpret_cast<void *>(&MobileGLTraceGetIntegerv);
}
if (strcmp(procName, "glGetError") == 0) {
return reinterpret_cast<void *>(&MobileGLTraceGetError);
}
return LookupSymbol(procName);
}
void *_getPrivateProcAddress(const char *procName) {
void *proc = LookupSymbol(procName);
if (proc != nullptr) {
return proc;
}
if (_eglGetProcAddress != nullptr) {
return reinterpret_cast<void *>(_eglGetProcAddress(procName));
}
return nullptr;
}
@@ -0,0 +1,412 @@
#include "glws.hpp"
#include "retrace.hpp"
#include <android/native_window.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <dlfcn.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
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 <typename T>
bool Load(T &slot, const char *name) {
slot = reinterpret_cast<T>(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<const AndroidVisual *>(visual)->nativeVisualId;
ANativeWindow_setBuffersGeometry(gNativeWindow, surfaceWidth, surfaceHeight, nativeVisualId);
surface = gEgl.createWindowSurface(
gDisplay,
static_cast<const AndroidVisual *>(visual)->config,
gNativeWindow,
nullptr);
} else {
const EGLint attribs[] = {
EGL_WIDTH, surfaceWidth,
EGL_HEIGHT, surfaceHeight,
EGL_NONE,
};
surface = gEgl.createPbufferSurface(
gDisplay,
static_cast<const AndroidVisual *>(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<AndroidContext *>(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<EGLint>(std::max<unsigned>(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<const AndroidDrawable *>(drawable);
}
const AndroidContext *AsAndroidContext(const glws::Context *context) {
return static_cast<const AndroidContext *>(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<EGLint>(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<const AndroidVisual *>(visual), ResolveWidth(width),
ResolveHeight(height), pbInfo != nullptr);
}
Context *createContext(const Visual *visual, Context *shareContext, bool) {
return new AndroidContext(static_cast<const AndroidVisual *>(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;
}
@@ -0,0 +1,611 @@
#include "trace_replay_core.hpp"
#include <dlfcn.h>
#include "apitrace_exit.hpp"
#include "png.h"
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cctype>
#include <cstring>
#include <exception>
#include <fstream>
#include <limits>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
extern "C" int main(int argc, char** argv);
namespace mobilegl_trace {
namespace {
struct RgbaImage {
int width = 0;
int height = 0;
std::vector<std::uint8_t> 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<bool>(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<int>(width);
image.height = static_cast<int>(height);
image.pixels.resize(static_cast<std::size_t>(image.width) * image.height * 4);
std::vector<std::uint8_t> rowsStorage;
std::vector<png_bytep> rows(height);
if (rowBytes == width * 4) {
for (png_uint_32 y = 0; y < height; ++y) {
rows[y] = image.pixels.data() + static_cast<std::size_t>(y) * image.width * 4;
}
} else {
rowsStorage.resize(static_cast<std::size_t>(rowBytes) * height);
for (png_uint_32 y = 0; y < height; ++y) {
rows[y] = rowsStorage.data() + static_cast<std::size_t>(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<std::size_t>(y) * image.width * 4,
rowsStorage.data() + static_cast<std::size_t>(y) * rowBytes,
static_cast<std::size_t>(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<png_bytep> rows(static_cast<std::size_t>(image.height));
for (int y = 0; y < image.height; ++y) {
rows[static_cast<std::size_t>(y)] =
const_cast<png_bytep>(image.pixels.data() + static_cast<std::size_t>(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<std::streamsize>::max(), '\n');
continue;
}
if (!std::isspace(static_cast<unsigned char>(ch))) {
token.push_back(ch);
break;
}
}
while (file.get(ch)) {
if (std::isspace(static_cast<unsigned char>(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<std::uint8_t> rgb(static_cast<std::size_t>(image.width) * image.height * 3);
file.read(reinterpret_cast<char*>(rgb.data()), static_cast<std::streamsize>(rgb.size()));
if (file.gcount() != static_cast<std::streamsize>(rgb.size())) {
error = "PPM payload is truncated: " + path;
return false;
}
image.pixels.resize(static_cast<std::size_t>(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<std::size_t>(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
@@ -0,0 +1,47 @@
#pragma once
#include <string>
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
@@ -0,0 +1,97 @@
#include "trace_replay_core.hpp"
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <jni.h>
#include <string>
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, "<init>", "(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);
}
@@ -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 + '\'' +
'}';
}
}
}
+3
View File
@@ -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'