[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
@@ -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);
}