mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Feat] (android-plugin): add standalone trace replay profile
This commit is contained in:
@@ -28,3 +28,6 @@
|
|||||||
[submodule "3rdparty/SPIRV-Reflect"]
|
[submodule "3rdparty/SPIRV-Reflect"]
|
||||||
path = 3rdparty/SPIRV-Reflect
|
path = 3rdparty/SPIRV-Reflect
|
||||||
url = https://github.com/KhronosGroup/SPIRV-Reflect.git
|
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,
|
static void DumpTextureUploadStats(Int textureId, TextureUploadTarget target, Uint32 level,
|
||||||
const IntVec3& texelSize, const void* data, SizeT byteSize, Uint32 channels) {
|
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) {
|
if (!ShouldDumpTextureUploadStats() || data == nullptr || byteSize == 0 || channels == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
vkCmdSetLineWidth(commandBuffer, lineWidth);
|
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) {
|
static void ApplyStencilState(VkCommandBuffer commandBuffer) {
|
||||||
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
|
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
|
||||||
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
|
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;
|
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,
|
static void DumpVertexInputStats(Uint32 location, const MG_State::GLState::VertexAttribute& attr,
|
||||||
Uint32 firstVertex, Uint32 vertexCount) {
|
Uint32 firstVertex, Uint32 vertexCount) {
|
||||||
if (!ShouldDumpVertexInputStats() || attr.Type != DataType::Float32 || attr.Size <= 0 || attr.Size > 4) {
|
if (!ShouldDumpVertexInputStats() || attr.Type != DataType::Float32 || attr.Size <= 0 || attr.Size > 4) {
|
||||||
@@ -3018,8 +3042,7 @@ void main() {
|
|||||||
VkRect2D scissor{};
|
VkRect2D scissor{};
|
||||||
if (scissorEnabled) {
|
if (scissorEnabled) {
|
||||||
const auto& scissorBox = MG_State::pGLContext->GetScissorBox();
|
const auto& scissorBox = MG_State::pGLContext->GetScissorBox();
|
||||||
scissor.offset = { scissorBox[0], scissorBox[1] };
|
scissor = MakeClampedScissorRect(scissorBox, renderPassEntry->extent);
|
||||||
scissor.extent = { (Uint)scissorBox[2], (Uint)scissorBox[3] };
|
|
||||||
} else {
|
} else {
|
||||||
scissor.offset = {0, 0};
|
scissor.offset = {0, 0};
|
||||||
scissor.extent = { (Uint)renderPassEntry->extent.x(), (Uint)renderPassEntry->extent.y() };
|
scissor.extent = { (Uint)renderPassEntry->extent.x(), (Uint)renderPassEntry->extent.y() };
|
||||||
@@ -4105,6 +4128,7 @@ void main() {
|
|||||||
Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) {
|
Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) {
|
||||||
if (frame.isCommandRecording) {
|
if (frame.isCommandRecording) {
|
||||||
m_frameContext.EndCommandRecording();
|
m_frameContext.EndCommandRecording();
|
||||||
|
frame.hasCommandBufferRecorded = true;
|
||||||
}
|
}
|
||||||
if (!frame.hasCommandBufferRecorded) {
|
if (!frame.hasCommandBufferRecorded) {
|
||||||
return true;
|
return true;
|
||||||
@@ -4185,6 +4209,16 @@ void main() {
|
|||||||
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
|
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
|
||||||
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
|
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
|
||||||
: *srcBinding.trackedLayout;
|
: *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) {
|
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined");
|
MGLOG_E("DirectVulkan::ReadPixels skipped: source image layout is undefined");
|
||||||
return;
|
return;
|
||||||
@@ -4261,6 +4295,23 @@ void main() {
|
|||||||
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
|
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
|
||||||
return;
|
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;
|
const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
|
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
|
||||||
}
|
}
|
||||||
@@ -4768,8 +4819,21 @@ void main() {
|
|||||||
VkDeviceSize presentStatsReadbackSize = 0;
|
VkDeviceSize presentStatsReadbackSize = 0;
|
||||||
const VkExtent2D presentStatsExtent = m_swapchainObject.GetExtent();
|
const VkExtent2D presentStatsExtent = m_swapchainObject.GetExtent();
|
||||||
const char* presentDumpPath = PresentDumpPath();
|
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;
|
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) {
|
if (collectPresentStats) {
|
||||||
presentStatsReadbackSize = static_cast<VkDeviceSize>(presentStatsExtent.width) *
|
presentStatsReadbackSize = static_cast<VkDeviceSize>(presentStatsExtent.width) *
|
||||||
static_cast<VkDeviceSize>(presentStatsExtent.height) * 4;
|
static_cast<VkDeviceSize>(presentStatsExtent.height) * 4;
|
||||||
@@ -4860,7 +4924,7 @@ void main() {
|
|||||||
nonBlack, pixelCount, colored, pixelCount, nonTransparent, pixelCount,
|
nonBlack, pixelCount, colored, pixelCount, nonTransparent, pixelCount,
|
||||||
presentStatsExtent.width, presentStatsExtent.height);
|
presentStatsExtent.width, presentStatsExtent.height);
|
||||||
}
|
}
|
||||||
if (presentDumpPath != nullptr) {
|
if (shouldDumpPresent) {
|
||||||
FILE* dump = std::fopen(presentDumpPath, "wb");
|
FILE* dump = std::fopen(presentDumpPath, "wb");
|
||||||
if (dump != nullptr) {
|
if (dump != nullptr) {
|
||||||
std::fprintf(dump, "P6\n%u %u\n255\n", presentStatsExtent.width, presentStatsExtent.height);
|
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::fwrite(rgb, 1, sizeof(rgb), dump);
|
||||||
}
|
}
|
||||||
std::fclose(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 {
|
productFlavors {
|
||||||
espryt {
|
espryt {
|
||||||
dimension 'backend'
|
dimension 'backend'
|
||||||
applicationId 'top.mobilegl.plugin.espryt'
|
applicationId 'top.mobilegl.plugin.espryt'
|
||||||
def env = mobileGlEnvString('DirectGLES')
|
def env = mobileGlEnvString('DirectGLES')
|
||||||
|
resValue 'string', 'mobilegl_default_backend', 'DirectGLES'
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [
|
||||||
appLabel: 'MobileGL Espryt',
|
appLabel: 'MobileGL Espryt',
|
||||||
des : 'MobileGL Espryt',
|
des : 'MobileGL Espryt',
|
||||||
@@ -53,6 +54,7 @@ android {
|
|||||||
dimension 'backend'
|
dimension 'backend'
|
||||||
applicationId 'top.mobilegl.plugin.magma'
|
applicationId 'top.mobilegl.plugin.magma'
|
||||||
def env = mobileGlEnvString('DirectVulkan')
|
def env = mobileGlEnvString('DirectVulkan')
|
||||||
|
resValue 'string', 'mobilegl_default_backend', 'DirectVulkan'
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [
|
||||||
appLabel: 'MobileGL Magma',
|
appLabel: 'MobileGL Magma',
|
||||||
des : 'MobileGL Magma',
|
des : 'MobileGL Magma',
|
||||||
@@ -61,6 +63,14 @@ android {
|
|||||||
pojavEnv: env,
|
pojavEnv: env,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
plugin {
|
||||||
|
dimension 'profile'
|
||||||
|
}
|
||||||
|
trace {
|
||||||
|
dimension 'profile'
|
||||||
|
applicationIdSuffix '.trace'
|
||||||
|
versionNameSuffix '-trace'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (releaseSigningReady) {
|
if (releaseSigningReady) {
|
||||||
@@ -83,6 +93,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path file('src/trace/cpp/CMakeLists.txt')
|
||||||
|
version '3.22.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
jniLibs {
|
jniLibs {
|
||||||
useLegacyPackaging true
|
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 {
|
dependencies {
|
||||||
implementation project(':MobileGL')
|
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.plugins.withId('com.android.library') {
|
||||||
subproject.android {
|
subproject.android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
ndk {
|
||||||
|
abiFilters 'arm64-v8a'
|
||||||
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
cppFlags '-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO'
|
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