mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat] (android-plugin): add standalone trace replay profile
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
+1
Submodule android-plugin/third_party/apitrace added at 055c4573af
Reference in New Issue
Block a user