debug_fix_screen_change #2

Merged
xsl merged 35 commits from debug_fix_screen_change into main 2026-04-26 10:58:01 +08:00
680 changed files with 8697 additions and 2992 deletions
+1
View File
@@ -12,4 +12,5 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
.idea
local.properties local.properties
+8 -1
View File
@@ -26,6 +26,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
add_library(face_sdk SHARED add_library(face_sdk SHARED
app/src/main/cpp/main.cpp app/src/main/cpp/main.cpp
app/src/main/cpp/AndroidOut.cpp app/src/main/cpp/AndroidOut.cpp
app/src/main/cpp/DebugLog.h
app/src/main/cpp/DebugLog.cpp
app/src/main/cpp/CrashHandler.h
app/src/main/cpp/CrashHandler.cpp
vulkan/AppBase.h vulkan/AppBase.h
vulkan/AppBase.cpp vulkan/AppBase.cpp
vulkan/Application.h vulkan/Application.h
@@ -49,7 +53,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
game-activity::game-activity_static game-activity::game-activity_static
Vulkan::Vulkan Vulkan::Vulkan
android android
log) log
# dl: needed by CrashHandler::dumpFrame -> dladdr() to map a
# PC back to "module + symbol + offset" in the crash log.
dl)
target_compile_definitions(face_sdk PRIVATE target_compile_definitions(face_sdk PRIVATE
VMA_STATIC_VULKAN_FUNCTIONS=0 VMA_STATIC_VULKAN_FUNCTIONS=0
+191
View File
@@ -0,0 +1,191 @@
# Face SDK 崩溃调试指南
本文档说明如何配合 AI 定位 `libface_sdk.so` 里的崩溃 / 异常。核心思路:**把 Vulkan 每一次异常的上下文写进一个持久日志文件里,崩溃或画面卡住后把这份文件发给 AI 分析。**
---
## 一、已经加好的东西(不需要你做什么)
### 1. `DebugLog` 模块(`app/src/main/cpp/DebugLog.{h,cpp}`
- 线程安全;
- 每条日志带**时间戳 + 线程 ID(tid)**
- 写到 App **私有目录下的文件** —— 不会被 logcat 自动清掉;
- 每写一行 `fflush`,进程挂了也不丢最后几行;
- 文件大小超过 2MB 自动滚动到 `.old`
- 同步把日志镜像到 logcat,tag 是 `FACE_DBG`
### 2. 打点位置
| 位置 | 打点内容 | 用途 |
|---|---|---|
| `android_main` 启动 | pid、tid、日志文件绝对路径 | 每次启动一条,作为分段锚点 |
| 渲染循环心跳 | 每 600 帧一条:`frame=X exceptions_so_far=Y` | 判断启动多久后崩溃 / 异常持续性 |
| `try/catch` 兜底 | 首 10 次异常每次都打,之后每 120 次打一次 | **Vulkan 异常不会再让进程硬崩**,留住现场 |
| `handle_cmd` | 每个 `APP_CMD_*` 事件(尤其 `INIT_WINDOW`/`TERM_WINDOW`/`CONFIG_CHANGED`) | 旋转 / 切后台 / 锁屏场景的时间线 |
| `FaceApp::initVulkan` 入/出口 | 调用次数、`_applicationInited` / `_faceAppInited` / `_sceondInited` 状态 | 判断是否被多次重复调用 |
| `processImageNative` / `passDataToNative` | 每 300 次调用打一条 tid | 验证"Java 两个回调在不同线程并发调 Vulkan"假设 |
| `Application::drawFrame` 里的 4 次 Vulkan 调用 | 非 `VK_SUCCESS` 时打印数值 + 字符串名(例如 `-4 (VK_ERROR_DEVICE_LOST)` | **最关键的证据** |
### 3. 关键行为变化
**⚠️ 现在 `drawFrame` 里 Vulkan 抛出的异常会被 `try/catch` 吃掉 —— App 不会再因为这类问题硬崩溃。**
- 好处:你能继续跑、继续复现、日志一直在写;
- 代价:出问题时表现从"崩溃"变成"**画面卡住 / 黑屏 / 不再更新**"。
- 如果想暂时回到硬崩行为:把 `app/src/main/cpp/main.cpp` 里那段 `try { g_Application->drawFrame(...); } catch (...) { ... }` 改回直接 `g_Application->drawFrame(frameTime);` 即可。
---
## 二、怎么把日志文件取出来
### 方式 A`run-as`(推荐,不需要 root
```bash
adb shell "run-as com.inewme.uvmirror cat files/face_sdk_debug.log" > face_sdk_debug.log
adb shell "run-as com.inewme.uvmirror cat files/face_sdk_debug.log.old" > face_sdk_debug.log.old
```
> `.old` 只有文件滚动过一次才存在,没有可以忽略错误。
### 方式 B:直接看 logcat(实时)
```bash
adb logcat -s FACE_DBG
```
和文件内容基本一致。跑较久的话还是文件更可靠。
### 方式 C:从 App 里拿路径
启动后 `FACE_DBG` 第一条日志长这样:
```
[10:12:03.041][tid=12345] android_main start, pid=23456 tid=12345, logPath=/data/user/0/com.inewme.uvmirror/files/face_sdk_debug.log
```
照这个路径走就对了。
---
## 三、你现在要做的事(B 方案:两种场景都复现一次)
### 场景 1:旋转崩溃(已确认必现)
1. 打开 App,等初始化完成(画面能看到人脸渲染)。
2. **旋转一次屏幕**
3. 等 3~5 秒让日志写进去。
4.`face_sdk_debug.log`**不要清文件**,继续场景 2。
期望看到的关键行:
```
>> handle_cmd APP_CMD_TERM_WINDOW(...)
>> handle_cmd APP_CMD_INIT_WINDOW(...)
FaceApp::initVulkan enter, call#2 ...
drawFrame[NNN] vkAcquireNextImageKHR -> ??? (...)
!!! drawFrame std::exception (frame=NNN count=M): failed to ...
```
注意里面那个 `???` —— 这是整件事的核心证据。
### 场景 2:长时间运行崩溃(之前那个 10 分钟左右的)
**不要退出 App,接着跑**。现在即使旋转触发了异常,进程没死,我们希望看后续会不会再出另一种 VkResult:
1. 尽量不操作(不切后台、不旋转、不锁屏,让 App 持续渲染)。
2. 连续跑 **15~20 分钟**,观察是否复现之前的"无外部事件崩溃"。
3. 复现后(或稳定 20 分钟未复现也行),再取一次 `face_sdk_debug.log`(这次会覆盖场景 1 的那份,所以务必**先把场景 1 的单独保存好**)。
期望看到的关键行(如果真的是 DEVICE_LOST):
```
drawFrame[NNN] vkQueueSubmit -> -4 (VK_ERROR_DEVICE_LOST) ...
```
或者某个 Vulkan 调用返回其他错误码。
### 辅助信息(手边有就顺便记一下)
| 项 | 为什么要 |
|---|---|
| 机型 + Android 版本 | GPU 驱动差异;Adreno / Mali / Xclipse 表现差别大 |
| 复现时手机是不是很烫 | 排查 GPU 温控 / thermal throttling |
| 旋转时是"横屏 → 竖屏"还是"竖屏 → 横屏" / 多次连续旋转? | 某些机型一次 vs 多次触发行为不同 |
| 10 分钟崩溃时 App 在做什么(有在切换 motion 吗?) | 帮助排除 `changeMotionList` 相关路径 |
---
## 四、发给 AI 的时候包含什么
最小集:
1. `face_sdk_debug.log`(场景 1 旋转那份)
2. `face_sdk_debug.log`(场景 2 长跑那份)
3. 如果文件很大,**完整发**比截断发强(AI 主要关心最后几千行)
4. 一句话说明这份日志对应场景 1 还是场景 2,是否复现了
不需要的:
- logcat 完整 dump(体积太大且大多无关,除非 AI 主动问特定 tag)
- 录屏
- 源代码(AI 已经能看到仓库)
---
## 五、当前未解决的假设,等日志验证
| 假设 | 对应 VkResult | 修法 |
|---|---|---|
| 旋转导致 surface 失效未处理 | `VK_ERROR_OUT_OF_DATE_KHR` (-1000001004) 或 `VK_ERROR_SURFACE_LOST_KHR` (-1000000000) 或 `VK_SUBOPTIMAL_KHR` (1000001003) | 实现 `APP_CMD_TERM_WINDOW` 销毁流程 + swapchain 重建 |
| Java 多线程并发提交 graphicsQueue 导致 GPU 长时间后挂 | `VK_ERROR_DEVICE_LOST` (-4),且 `processImageNative` / `passDataToNative` 日志里 tid 不同 | 给所有 `vkQueueSubmit`/`vkQueuePresentKHR` 加全局 queue mutex |
| 驱动 bug / 温度导致 GPU hang | `VK_ERROR_DEVICE_LOST` (-4),单线程也出 | 捕获并尝试重建 device,或上报给机型厂商 |
| 资源累积泄漏 | `VK_ERROR_OUT_OF_DEVICE_MEMORY` (-2) 或 `VK_ERROR_TOO_MANY_OBJECTS` | 排查 `beginSingleTimeCommands`/`processWithVulkan` 路径 |
---
## 六、日志样例(便于你确认打印是否正常)
正常启动应该看到:
```
========== DebugLog opened @ 2026-04-23 14:05:12 (pid=12345) ==========
[14:05:12.019][tid=12345] android_main start, pid=12345 tid=12345, logPath=/data/user/0/com.inewme.uvmirror/files/face_sdk_debug.log
[14:05:12.155][tid=12345] >> handle_cmd APP_CMD_START(10) tid=12345
[14:05:12.155][tid=12345] << handle_cmd APP_CMD_START done
[14:05:12.210][tid=12345] >> handle_cmd APP_CMD_INIT_WINDOW(1) tid=12345
[14:05:12.210][tid=12345] handle_cmd APP_CMD_INIT_WINDOW: calling initVulkan()
[14:05:12.210][tid=12345] FaceApp::initVulkan enter, call#1 _applicationInited=0 _faceAppInited=0 _secondfaceAppInited=0
[14:05:13.420][tid=12345] FaceApp::initVulkan exit, call#1 _applicationInited=1 _faceAppInited=1 _secondfaceAppInited=1
[14:05:13.420][tid=12345] handle_cmd APP_CMD_INIT_WINDOW: initVulkan() returned, isInited=1
[14:05:13.421][tid=12345] << handle_cmd APP_CMD_INIT_WINDOW done
[14:05:14.005][tid=67890] processImageNative #1 tid=67890 w=480 h=480
[14:05:14.008][tid=67891] passDataToNative #1 tid=67891 point_count=468 w=480 h=480
[14:05:35.200][tid=12345] heartbeat: frame=600 exceptions_so_far=0
```
旋转时如果触发异常应该看到:
```
[14:06:10.112][tid=12345] >> handle_cmd APP_CMD_CONFIG_CHANGED(8) tid=12345
[14:06:10.113][tid=12345] >> handle_cmd APP_CMD_TERM_WINDOW(2) tid=12345
[14:06:10.113][tid=12345] handle_cmd APP_CMD_TERM_WINDOW: (no handler yet, ...)
[14:06:10.113][tid=12345] << handle_cmd APP_CMD_TERM_WINDOW done
[14:06:10.250][tid=12345] >> handle_cmd APP_CMD_INIT_WINDOW(1) tid=12345
[14:06:10.250][tid=12345] FaceApp::initVulkan enter, call#2 _applicationInited=1 _faceAppInited=1 _secondfaceAppInited=0
[14:06:10.250][tid=12345] FaceApp::initVulkan exit, call#2 _applicationInited=1 _faceAppInited=1 _secondfaceAppInited=1
[14:06:10.280][tid=12345] drawFrame[1234] vkAcquireNextImageKHR -> -1000001004 (VK_ERROR_OUT_OF_DATE_KHR) currentFrame=0
[14:06:10.280][tid=12345] !!! drawFrame std::exception (frame=1234 count=1): failed to acquire swap chain image!
```
**只要日志里能看到 `vkXxxKHR -> <数值> (<名字>)` 这一行,诊断就成立了。**
---
## 七、文件清单(仅供参考,日常不用动)
```
app/src/main/cpp/
DebugLog.h 新增
DebugLog.cpp 新增
main.cpp 改:日志初始化 / 心跳 / try-catch / cmd 日志 / JNI tid
vulkan/
Application.cpp 改:drawFrame 4 处 VkResult 日志 + VkResultStr
FaceApp.cpp 改:initVulkan 入/出口日志
CMakeLists.txt 改:加入 DebugLog.cpp 到 Android 构建
```
+23 -7
View File
@@ -1,7 +1,8 @@
plugins { plugins {
//alias(libs.plugins.android.application) //alias(libs.plugins.android.application)
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
id 'org.jetbrains.kotlin.android' alias(libs.plugins.kotlin.android)
//id 'org.jetbrains.kotlin.android'
} }
ext.vvl_version='1.4.321.0' ext.vvl_version='1.4.321.0'
@@ -30,6 +31,17 @@ android {
debug { debug {
debuggable true debuggable true
jniDebuggable true jniDebuggable true
externalNativeBuild {
cmake {
arguments "-DCMAKE_BUILD_TYPE=Debug"
cppFlags "-DDEBUG -O0 -g"
}
}
ndk {
debugSymbolLevel 'FULL' // Debug 版本包含完整符号
}
} }
release { release {
@@ -43,13 +55,13 @@ android {
} }
// 添加 Kotlin 编译选项 // 添加 Kotlin 编译选项
kotlinOptions { // kotlinOptions {
jvmTarget = '17' // 与 Java 版本保持一致 // jvmTarget = '17' // 与 Java 版本保持一致
} // }
kotlin { // kotlin {
jvmToolchain(17) // jvmToolchain(17)
} // }
buildFeatures { buildFeatures {
prefab true prefab true
@@ -83,6 +95,9 @@ android {
packagingOptions { packagingOptions {
pickFirst '**/*.jar' pickFirst '**/*.jar'
} }
kotlinOptions {
jvmTarget = '17'
}
} }
dependencies { dependencies {
@@ -90,6 +105,7 @@ dependencies {
implementation libs.appcompat implementation libs.appcompat
implementation libs.material implementation libs.material
implementation libs.games.activity implementation libs.games.activity
implementation libs.androidx.core.core.ktx
testImplementation libs.junit testImplementation libs.junit
androidTestImplementation libs.ext.junit androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core androidTestImplementation libs.espresso.core
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+28 -2
View File
@@ -9,7 +9,33 @@ layout(location = 0) out vec4 outColor;
// 纹理采样器 // 纹理采样器
layout(binding = 0) uniform sampler2D texSampler; layout(binding = 0) uniform sampler2D texSampler;
layout(push_constant) uniform PushConstants {
float zoom;
float r;
float g;
float b;
float radius;
float ux;
float uy;
} pushConstants;
void main() { void main() {
// 采样纹理 // outFragColor = color;
outColor = texture(texSampler, inTexCoord); // 获取当前片元的屏幕坐标(左下角为原点)
vec2 fragCoord = gl_FragCoord.xy;
// 屏幕中心(480x480 的中心是 (240, 240)
vec2 center = vec2(240.0, 240.0);
// 计算到中心的距离(欧氏距离,单位:像素)
float dist = length(fragCoord - center);
// 判断是否在半径之外
if (dist > pushConstants.radius) {
// 使用 push constant 中的 RGB 颜色(注意 alpha 设为 1.0
outColor = vec4(pushConstants.r, pushConstants.g, pushConstants.b, 1.0);
} else {
// 采样纹理
outColor = texture(texSampler, inTexCoord);
}
} }
Binary file not shown.
+12 -2
View File
@@ -38,7 +38,15 @@ const vec2 texCoords[6] = vec2[6](
// 定义 Push Constant,只有一个 float // 定义 Push Constant,只有一个 float
layout(push_constant) uniform PushConstants { layout(push_constant) uniform PushConstants {
float myValue; float zoom;
float r;
float g;
float b;
float radius;
float ux;
float uy;
float offset_x;
float offset_y;
} pushConstants; } pushConstants;
@@ -48,7 +56,9 @@ layout(location = 0) out vec2 outTexCoord;
void main() { void main() {
// 获取顶点位置 // 获取顶点位置
vec2 position = positions[gl_VertexIndex]; vec2 position = positions[gl_VertexIndex];
position = position * pushConstants.myValue; position = position * pushConstants.zoom;
position.x = position.x + (pushConstants.offset_x/480);
position.y = position.y + (pushConstants.offset_y/480);
// 设置输出位置(Vulkan使用不同的坐标系) // 设置输出位置(Vulkan使用不同的坐标系)
gl_Position = vec4(position, 0.0, 1.0); gl_Position = vec4(position, 0.0, 1.0);
Binary file not shown.
+29 -2
View File
@@ -5,13 +5,40 @@ layout (binding = 1) uniform sampler2D samplerColor;
layout (location = 0) in vec2 inUV; layout (location = 0) in vec2 inUV;
layout (location = 1) in vec3 inNormal; layout (location = 1) in vec3 inNormal;
layout(push_constant) uniform PushConstants {
float zoom;
float r;
float g;
float b;
float radius;
float ux;
float uy;
} pushConstants;
layout (location = 0) out vec4 outFragColor; layout (location = 0) out vec4 outFragColor;
void main() void main()
{ {
vec4 color = texture(samplerColor, inUV); // outFragColor = color;
// 获取当前片元的屏幕坐标(左下角为原点)
vec2 fragCoord = gl_FragCoord.xy;
outFragColor = color; // 屏幕中心(480x480 的中心是 (240, 240)
vec2 center = vec2(240.0, 240.0);
// 计算到中心的距离(欧氏距离,单位:像素)
float dist = length(fragCoord - center);
// 判断是否在半径之外
if (dist > pushConstants.radius) {
// 使用 push constant 中的 RGB 颜色(注意 alpha 设为 1.0
outFragColor = vec4(pushConstants.r, pushConstants.g, pushConstants.b, 1.0);
} else {
vec2 pos = vec2(pushConstants.ux/4096.f, pushConstants.uy/2048.f);
vec2 uv = pos + vec2(inUV.x/8.f, inUV.y/4.f);
vec4 color = texture(samplerColor, uv);
outFragColor = color;
}
} }
Binary file not shown.
+12 -2
View File
@@ -17,7 +17,15 @@ layout (location = 0) out vec2 outUV;
layout (location = 1) out vec3 outNormal; layout (location = 1) out vec3 outNormal;
layout(push_constant) uniform PushConstants { layout(push_constant) uniform PushConstants {
float myValue; float zoom;
float r;
float g;
float b;
float radius;
float ux;
float uy;
float offset_x;
float offset_y;
} pushConstants; } pushConstants;
out gl_PerVertex out gl_PerVertex
@@ -30,7 +38,9 @@ void main()
outUV = inUV; outUV = inUV;
vec2 position = vec2(inPos.xy*2 -1); vec2 position = vec2(inPos.xy*2 -1);
position = position * pushConstants.myValue; position = position * pushConstants.zoom;
position.x = position.x + (pushConstants.offset_x/480);
position.y = position.y + (pushConstants.offset_y/480);
gl_Position = vec4(position, 0.5, 1.0); gl_Position = vec4(position, 0.5, 1.0);
vec4 pos = ubo.model * vec4(inPos, 1.0); vec4 pos = ubo.model * vec4(inPos, 1.0);
Binary file not shown.
+512
View File
@@ -0,0 +1,512 @@
// CrashHandler.cpp
//
// Async-signal-safe crash dumper for the face_sdk native library. The goal
// is that the next time the app dies (e.g. another FORTIFY: pthread_mutex_lock
// called on a destroyed mutex from inside the Vulkan driver) we don't only
// have logcat — we also have a self-contained dump on the device's app data
// directory that the user can pull off and send back, even after a reboot.
//
// Why we need this:
// - DebugLog only writes "happy path" events. The Vulkan crash happens
// synchronously inside vkQueueSubmit / vkFreeCommandBuffers — there is
// no DebugLog call near the crash site, so the file log just stops.
// - logcat survives across the crash (debuggerd dumps the backtrace there)
// but logcat is volatile: a couple of reboots, a logcat -c, or a long
// idle period and it's gone. We want a persistent file.
// - tombstones in /data/tombstones/ are root-only on consumer devices.
//
// Implementation notes:
// - Inside a signal handler we MUST stick to async-signal-safe APIs
// (man 7 signal-safety). That rules out fprintf / snprintf / malloc.
// We use write(), our own integer-to-string conversion, and a single
// pre-opened fd. dladdr() is technically not on the POSIX safe list but
// bionic's implementation only takes one rwlock and is widely used in
// other crash dumpers (breakpad, crashpad, libunwindstack). We accept
// that risk because the alternative is no symbol at all.
// - We use <unwind.h> (_Unwind_Backtrace) instead of execinfo.h because
// bionic doesn't ship execinfo.h on all NDK levels, and _Unwind_Backtrace
// is the same primitive Android's own tombstoned uses.
// - We re-raise the original signal with the default handler at the end so
// the OS still produces a tombstone / ANR record for vendors that
// read /data/tombstones/.
#include "CrashHandler.h"
#ifndef _WIN32
#include <android/log.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <unwind.h>
#include <atomic>
namespace {
// ---------- Globals (touched from the handler -> only POD/atomics) ---------
constexpr size_t kCrashStackSize = 64 * 1024; // sigaltstack
constexpr size_t kMaxFrames = 64;
constexpr size_t kNoteCapacity = 256;
uint8_t g_sigStack[kCrashStackSize];
int g_crashFd = -1; // crash log fd (append, sync)
int g_debugFd = -1; // optional: also dup write to debug log
std::atomic<bool> g_installed{false};
std::atomic<bool> g_handlingCrash{false};
// Single writer of g_note: setNote() (uses memcpy under a tiny lock, but the
// handler reads byte-by-byte so a torn read at most produces a truncated
// note, never a deref of bad memory).
char g_note[kNoteCapacity] = {0};
pthread_mutex_t g_noteMtx = PTHREAD_MUTEX_INITIALIZER;
const int g_signals[] = { SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSYS };
constexpr size_t kNumSignals = sizeof(g_signals) / sizeof(g_signals[0]);
// ----------------------- Async-signal-safe writers -------------------------
// Write a NUL-terminated string. Drops the trailing NUL.
void sigWrite(int fd, const char* s) {
if (fd < 0 || !s) return;
size_t n = 0;
while (s[n]) ++n;
if (n == 0) return;
// Loop until everything is written or we error out. Async-safe.
while (n > 0) {
ssize_t w = write(fd, s, n);
if (w <= 0) {
if (w < 0 && errno == EINTR) continue;
return;
}
s += w;
n -= (size_t)w;
}
}
// Write n bytes from buf. Used for the note (may contain anything).
void sigWriteN(int fd, const char* buf, size_t n) {
if (fd < 0 || !buf) return;
while (n > 0) {
ssize_t w = write(fd, buf, n);
if (w <= 0) {
if (w < 0 && errno == EINTR) continue;
return;
}
buf += w;
n -= (size_t)w;
}
}
// Write the same string to both the crash log and the debug log (if any).
void sigDump(const char* s) {
sigWrite(g_crashFd, s);
sigWrite(g_debugFd, s);
}
// Convert an unsigned integer to its decimal representation. Returns the
// number of characters written into buf (without a NUL).
size_t u64ToDec(uint64_t v, char* buf, size_t cap) {
if (cap == 0) return 0;
char tmp[32];
size_t i = 0;
if (v == 0) {
tmp[i++] = '0';
} else {
while (v && i < sizeof(tmp)) {
tmp[i++] = (char)('0' + (v % 10));
v /= 10;
}
}
size_t out = (i < cap) ? i : cap;
for (size_t k = 0; k < out; ++k) {
buf[k] = tmp[i - 1 - k];
}
return out;
}
// Convert an unsigned 64-bit value to a fixed-width 16-digit hex string.
// Useful for PC values.
size_t u64ToHex16(uint64_t v, char* buf, size_t cap) {
static const char digits[] = "0123456789abcdef";
if (cap < 16) return 0;
for (int i = 15; i >= 0; --i) {
buf[i] = digits[v & 0xF];
v >>= 4;
}
return 16;
}
// Write "<key>=<u64>\n".
void sigWriteKV_u64(int fd, const char* key, uint64_t v) {
sigWrite(fd, key);
sigWrite(fd, "=");
char num[24];
size_t n = u64ToDec(v, num, sizeof(num));
sigWriteN(fd, num, n);
sigWrite(fd, "\n");
}
// Write "<key>=0x<hex>\n".
void sigWriteKV_ptr(int fd, const char* key, uint64_t v) {
sigWrite(fd, key);
sigWrite(fd, "=0x");
char hx[16];
u64ToHex16(v, hx, sizeof(hx));
sigWriteN(fd, hx, 16);
sigWrite(fd, "\n");
}
// ------------------------- Signal name lookup ------------------------------
const char* signalName(int signo) {
switch (signo) {
case SIGSEGV: return "SIGSEGV";
case SIGABRT: return "SIGABRT";
case SIGBUS: return "SIGBUS";
case SIGFPE: return "SIGFPE";
case SIGILL: return "SIGILL";
case SIGSYS: return "SIGSYS";
case SIGTRAP: return "SIGTRAP";
default: return "SIG?";
}
}
// si_code to short string. Only the most common ones; everything else falls
// back to the numeric value via sigWriteKV_u64.
const char* siCodeName(int signo, int code) {
switch (signo) {
case SIGSEGV:
if (code == SEGV_MAPERR) return "SEGV_MAPERR";
if (code == SEGV_ACCERR) return "SEGV_ACCERR";
break;
case SIGBUS:
if (code == BUS_ADRALN) return "BUS_ADRALN";
if (code == BUS_ADRERR) return "BUS_ADRERR";
if (code == BUS_OBJERR) return "BUS_OBJERR";
break;
case SIGFPE:
if (code == FPE_INTDIV) return "FPE_INTDIV";
if (code == FPE_INTOVF) return "FPE_INTOVF";
if (code == FPE_FLTDIV) return "FPE_FLTDIV";
break;
case SIGILL:
if (code == ILL_ILLOPC) return "ILL_ILLOPC";
if (code == ILL_ILLOPN) return "ILL_ILLOPN";
break;
case SIGABRT:
if (code == SI_TKILL) return "SI_TKILL";
if (code == SI_USER) return "SI_USER";
break;
}
return "?";
}
// ------------------------- Backtrace via _Unwind_Backtrace -----------------
struct UnwindCtx {
uintptr_t* frames;
size_t count;
size_t cap;
};
_Unwind_Reason_Code unwindCallback(_Unwind_Context* ctx, void* arg) {
UnwindCtx* uc = static_cast<UnwindCtx*>(arg);
if (uc->count >= uc->cap) return _URC_END_OF_STACK;
uintptr_t pc = _Unwind_GetIP(ctx);
if (pc) {
// Trim Thumb bit on 32-bit ARM. No-op on aarch64/x86_64.
pc &= ~(uintptr_t)1;
uc->frames[uc->count++] = pc;
}
return _URC_NO_REASON;
}
size_t captureBacktrace(uintptr_t* out, size_t cap) {
UnwindCtx uc{out, 0, cap};
_Unwind_Backtrace(&unwindCallback, &uc);
return uc.count;
}
// Dump one frame: " #02 pc 000000000000abcd /path/lib.so (Symbol+0x10)"
void dumpFrame(int fd, size_t idx, uintptr_t pc) {
sigWrite(fd, " #");
char num[8];
if (idx < 10) {
num[0] = '0';
num[1] = (char)('0' + idx);
sigWriteN(fd, num, 2);
} else {
size_t n = u64ToDec(idx, num, sizeof(num));
sigWriteN(fd, num, n);
}
sigWrite(fd, " pc ");
char hx[16];
u64ToHex16((uint64_t)pc, hx, sizeof(hx));
sigWriteN(fd, hx, 16);
Dl_info info;
memset(&info, 0, sizeof(info));
if (dladdr(reinterpret_cast<void*>(pc), &info) && info.dli_fname) {
sigWrite(fd, " ");
sigWrite(fd, info.dli_fname);
if (info.dli_sname) {
uintptr_t sym = reinterpret_cast<uintptr_t>(info.dli_saddr);
uintptr_t off = (sym && pc >= sym) ? (pc - sym) : 0;
sigWrite(fd, " (");
sigWrite(fd, info.dli_sname);
sigWrite(fd, "+0x");
char ohx[16];
u64ToHex16((uint64_t)off, ohx, sizeof(ohx));
sigWriteN(fd, ohx, 16);
sigWrite(fd, ")");
} else if (info.dli_fbase) {
uintptr_t base = reinterpret_cast<uintptr_t>(info.dli_fbase);
uintptr_t off = (pc >= base) ? (pc - base) : 0;
sigWrite(fd, " (offset 0x");
char ohx[16];
u64ToHex16((uint64_t)off, ohx, sizeof(ohx));
sigWriteN(fd, ohx, 16);
sigWrite(fd, ")");
}
}
sigWrite(fd, "\n");
}
// ------------------------- Time + tid helpers ------------------------------
// Builds "YYYY-MM-DD HH:MM:SS.mmm UTC" into the given buffer (no NUL).
// Returns the number of bytes written. Async-signal-safe (no stdio, no
// localtime_r tz lookups).
size_t formatTimestamp(char* b, size_t cap) {
timespec ts{};
clock_gettime(CLOCK_REALTIME, &ts);
struct tm tm_info{};
time_t s = ts.tv_sec;
gmtime_r(&s, &tm_info);
size_t i = 0;
auto putUInt = [&](unsigned v, int width) {
char tmp[8];
size_t n = u64ToDec(v, tmp, sizeof(tmp));
while ((int)n < (size_t)width) {
if (i < cap) b[i++] = '0';
++n;
}
for (size_t k = 0; k < n; ++k) {
if (i < cap) b[i++] = tmp[k];
}
};
putUInt((unsigned)(tm_info.tm_year + 1900), 4); if (i < cap) b[i++] = '-';
putUInt((unsigned)(tm_info.tm_mon + 1), 2); if (i < cap) b[i++] = '-';
putUInt((unsigned)(tm_info.tm_mday), 2); if (i < cap) b[i++] = ' ';
putUInt((unsigned)(tm_info.tm_hour), 2); if (i < cap) b[i++] = ':';
putUInt((unsigned)(tm_info.tm_min), 2); if (i < cap) b[i++] = ':';
putUInt((unsigned)(tm_info.tm_sec), 2); if (i < cap) b[i++] = '.';
putUInt((unsigned)(ts.tv_nsec / 1000000), 3);
static const char kSuffix[] = " UTC";
for (size_t k = 0; k < sizeof(kSuffix) - 1; ++k) {
if (i < cap) b[i++] = kSuffix[k];
}
return i;
}
pid_t currentTid() {
return static_cast<pid_t>(syscall(SYS_gettid));
}
// ------------------------- Signal handler ----------------------------------
void crashHandler(int signo, siginfo_t* info, void* ucontext) {
(void)ucontext;
// Re-entry guard: if a second signal fires while we're dumping (e.g. our
// own dladdr trips a SIGSEGV) just chain to the default handler.
bool expected = false;
if (!g_handlingCrash.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
// Already in handler -> default + bail.
signal(signo, SIG_DFL);
raise(signo);
return;
}
sigDump("\n========== FACE_SDK CRASH ==========\n");
sigDump("time=");
{
char tsbuf[48];
size_t tn = formatTimestamp(tsbuf, sizeof(tsbuf));
sigWriteN(g_crashFd, tsbuf, tn);
sigWriteN(g_debugFd, tsbuf, tn);
}
sigDump("\n");
sigDump("signal=");
sigDump(signalName(signo));
sigDump(" code=");
sigDump(siCodeName(signo, info ? info->si_code : 0));
sigDump("\n");
if (info) {
sigWriteKV_u64(g_crashFd, "si_signo", (uint64_t)info->si_signo);
sigWriteKV_u64(g_debugFd, "si_signo", (uint64_t)info->si_signo);
sigWriteKV_u64(g_crashFd, "si_code", (uint64_t)info->si_code);
sigWriteKV_u64(g_debugFd, "si_code", (uint64_t)info->si_code);
sigWriteKV_ptr(g_crashFd, "si_addr", (uint64_t)(uintptr_t)info->si_addr);
sigWriteKV_ptr(g_debugFd, "si_addr", (uint64_t)(uintptr_t)info->si_addr);
}
sigWriteKV_u64(g_crashFd, "pid", (uint64_t)getpid());
sigWriteKV_u64(g_debugFd, "pid", (uint64_t)getpid());
sigWriteKV_u64(g_crashFd, "tid", (uint64_t)currentTid());
sigWriteKV_u64(g_debugFd, "tid", (uint64_t)currentTid());
// Note (current frame index, current motion, ...).
sigDump("note=");
sigWriteN(g_crashFd, g_note, strnlen(g_note, kNoteCapacity));
sigWriteN(g_debugFd, g_note, strnlen(g_note, kNoteCapacity));
sigDump("\n");
sigDump("backtrace:\n");
uintptr_t frames[kMaxFrames];
size_t nf = captureBacktrace(frames, kMaxFrames);
for (size_t i = 0; i < nf; ++i) {
dumpFrame(g_crashFd, i, frames[i]);
dumpFrame(g_debugFd, i, frames[i]);
}
sigDump("==================================\n");
// Make sure everything reaches disk before we self-destruct.
if (g_crashFd >= 0) fsync(g_crashFd);
if (g_debugFd >= 0) fsync(g_debugFd);
// Mirror to logcat too so a quick `adb logcat -d` after a reboot still
// shows the SIGNAL line (helps cross-checking the file).
__android_log_print(ANDROID_LOG_FATAL, "FACE_DBG_CRASH",
"fatal %s @ tid=%d, see face_sdk_crash.log",
signalName(signo), currentTid());
// Restore default handler and re-raise. This produces /data/tombstones/*
// on rooted devices and tells debuggerd to print the official Android
// backtrace into logcat (`crash_dump64 ... DEBUG`).
struct sigaction dfl{};
dfl.sa_handler = SIG_DFL;
sigemptyset(&dfl.sa_mask);
sigaction(signo, &dfl, nullptr);
raise(signo);
}
} // namespace
namespace CrashHandler {
void install(const std::string& internalDataPath,
const std::string& debugLogPath) {
bool expected = false;
if (!g_installed.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
return; // already installed
}
// Open the persistent crash log file in append mode. We keep it open
// forever so the signal handler doesn't have to call open() (which is
// safe but slow).
{
std::string path = internalDataPath.empty()
? std::string("/data/local/tmp/face_sdk_crash.log")
: internalDataPath + "/face_sdk_crash.log";
g_crashFd = open(path.c_str(),
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC,
0644);
if (g_crashFd >= 0) {
// Header for the new run.
sigWrite(g_crashFd, "\n=== CrashHandler installed pid=");
char pidbuf[16];
size_t n = u64ToDec((uint64_t)getpid(), pidbuf, sizeof(pidbuf));
sigWriteN(g_crashFd, pidbuf, n);
sigWrite(g_crashFd, " ===\n");
fsync(g_crashFd);
__android_log_print(ANDROID_LOG_INFO, "FACE_DBG",
"CrashHandler log file: %s", path.c_str());
} else {
__android_log_print(ANDROID_LOG_WARN, "FACE_DBG",
"CrashHandler failed to open %s: %s",
path.c_str(), strerror(errno));
}
}
// Also keep a writable fd to the DebugLog file (if any) so dumps land
// next to the regular tail of the log. We don't touch g_fp inside
// DebugLog because that would need its mutex — not safe in handler.
if (!debugLogPath.empty()) {
g_debugFd = open(debugLogPath.c_str(),
O_WRONLY | O_APPEND | O_CLOEXEC,
0644);
if (g_debugFd < 0) {
__android_log_print(ANDROID_LOG_WARN, "FACE_DBG",
"CrashHandler: cannot open debug log %s: %s",
debugLogPath.c_str(), strerror(errno));
}
}
// Set up an alternate stack so we still have stack space if the original
// thread ran out (very common for Vulkan crashes inside deep driver
// call chains).
stack_t ss{};
ss.ss_sp = g_sigStack;
ss.ss_size = sizeof(g_sigStack);
ss.ss_flags = 0;
if (sigaltstack(&ss, nullptr) != 0) {
__android_log_print(ANDROID_LOG_WARN, "FACE_DBG",
"CrashHandler: sigaltstack failed: %s",
strerror(errno));
}
struct sigaction sa{};
sa.sa_sigaction = &crashHandler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
sigemptyset(&sa.sa_mask);
for (size_t i = 0; i < kNumSignals; ++i) {
if (sigaction(g_signals[i], &sa, nullptr) != 0) {
__android_log_print(ANDROID_LOG_WARN, "FACE_DBG",
"CrashHandler: sigaction(%d) failed: %s",
g_signals[i], strerror(errno));
}
}
__android_log_print(ANDROID_LOG_INFO, "FACE_DBG",
"CrashHandler installed for SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGSYS");
}
void setNote(const char* note) {
if (!note) note = "";
pthread_mutex_lock(&g_noteMtx);
size_t n = strnlen(note, kNoteCapacity - 1);
memcpy(g_note, note, n);
g_note[n] = '\0';
pthread_mutex_unlock(&g_noteMtx);
}
} // namespace CrashHandler
#else // _WIN32 — no-op on host build
namespace CrashHandler {
void install(const std::string&, const std::string&) {}
void setNote(const char*) {}
} // namespace CrashHandler
#endif
+33
View File
@@ -0,0 +1,33 @@
#ifndef FACE_SDK_CRASH_HANDLER_H
#define FACE_SDK_CRASH_HANDLER_H
#include <string>
namespace CrashHandler {
// Install signal handlers for SIGSEGV / SIGABRT / SIGBUS / SIGFPE / SIGILL.
//
// On a fatal signal we:
// 1) Switch to a pre-allocated 64KB sigaltstack so we still have stack
// space even if the original thread's stack was the cause.
// 2) Write a self-contained crash record to <internalDataPath>/face_sdk_crash.log
// (and also try to append it to the DebugLog file passed in).
// The record contains: signal name, si_code, si_addr, pid, tid,
// a synchronous unwound backtrace (PC + module + offset for each frame),
// and the value of any extra context the caller registered via setNote().
// 3) Re-raise the original signal with the default handler so that the
// Android tombstone pipeline still produces /data/tombstones/* files.
//
// Safe to call once. Subsequent calls are no-ops. Must be called AFTER
// DebugLog::init() so we know the log directory.
void install(const std::string& internalDataPath,
const std::string& debugLogPath = "");
// Optional short note (<= 256 chars) appended to every crash dump from now on.
// Useful to record "current frame index", "current motion", etc. so we know
// what was happening at the moment of the crash. Async-signal-safe to read.
void setNote(const char* note);
} // namespace CrashHandler
#endif // FACE_SDK_CRASH_HANDLER_H
+165
View File
@@ -0,0 +1,165 @@
#include "DebugLog.h"
#include <android/log.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <cstring>
#include <mutex>
#include <string>
#include <unordered_map>
namespace {
std::mutex g_mtx;
FILE* g_fp = nullptr;
std::string g_path;
constexpr size_t kMaxSize = 2 * 1024 * 1024; // 2 MB before rotation
// Throttle bookkeeping: one slot per distinct key passed to log_throttled().
struct ThrottleSlot {
int64_t last_emit_ms = 0; // when we last actually wrote a line for this key
uint32_t suppressed = 0; // number of calls swallowed since last_emit_ms
};
std::unordered_map<std::string, ThrottleSlot> g_throttle;
constexpr int64_t kThrottleWindowMs = 2000; // 2 seconds
pid_t currentTid() {
return static_cast<pid_t>(syscall(SYS_gettid));
}
int64_t nowMs() {
timespec ts{};
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<int64_t>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
}
// Assumes g_mtx is held. Writes a single pre-formatted line to disk + logcat.
void writeLineLocked(const char* body) {
pid_t tid = currentTid();
timespec ts{};
clock_gettime(CLOCK_REALTIME, &ts);
struct tm tm_info{};
localtime_r(&ts.tv_sec, &tm_info);
char timebuf[32];
strftime(timebuf, sizeof(timebuf), "%H:%M:%S", &tm_info);
if (g_fp) {
std::fprintf(g_fp, "[%s.%03ld][tid=%d] %s\n",
timebuf, ts.tv_nsec / 1000000, tid, body);
std::fflush(g_fp);
}
__android_log_print(ANDROID_LOG_INFO, "FACE_DBG", "[tid=%d] %s", tid, body);
}
} // namespace
namespace DebugLog {
void init(const char* internalDataPath) {
std::lock_guard<std::mutex> lk(g_mtx);
if (g_fp) {
return; // already initialized
}
const char* base = (internalDataPath && *internalDataPath) ? internalDataPath : "/data/local/tmp";
g_path = std::string(base) + "/face_sdk_debug.log";
// Rotate once if current file is too large.
struct stat st{};
if (stat(g_path.c_str(), &st) == 0 && static_cast<size_t>(st.st_size) > kMaxSize) {
std::string backup = g_path + ".old";
std::remove(backup.c_str());
std::rename(g_path.c_str(), backup.c_str());
}
g_fp = std::fopen(g_path.c_str(), "a");
if (g_fp) {
time_t now = time(nullptr);
struct tm tm_info{};
localtime_r(&now, &tm_info);
char tbuf[64];
strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm_info);
std::fprintf(g_fp,
"\n========== DebugLog opened @ %s (pid=%d) ==========\n",
tbuf, getpid());
std::fflush(g_fp);
}
__android_log_print(ANDROID_LOG_INFO, "FACE_DBG",
"DebugLog file path: %s", g_path.c_str());
}
void log(const char* fmt, ...) {
char buf[1024];
va_list ap;
va_start(ap, fmt);
int n = std::vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n < 0) return;
std::lock_guard<std::mutex> lk(g_mtx);
writeLineLocked(buf);
}
void log_throttled(const char* key, const char* fmt, ...) {
char buf[1024];
va_list ap;
va_start(ap, fmt);
int n = std::vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n < 0) return;
const char* k = key ? key : "";
int64_t now = nowMs();
std::lock_guard<std::mutex> lk(g_mtx);
auto it = g_throttle.find(k);
if (it == g_throttle.end()) {
// First time we see this key -> always log.
ThrottleSlot slot;
slot.last_emit_ms = now;
slot.suppressed = 0;
g_throttle.emplace(k, slot);
writeLineLocked(buf);
return;
}
ThrottleSlot& slot = it->second;
int64_t elapsed = now - slot.last_emit_ms;
if (elapsed < kThrottleWindowMs) {
// Still inside the 2s window -> swallow.
++slot.suppressed;
return;
}
// Window elapsed: emit the line, annotated with how many we swallowed.
if (slot.suppressed > 0) {
char annotated[1200];
std::snprintf(annotated, sizeof(annotated),
"%s (repeated %u times in last %lldms, key=%s)",
buf, slot.suppressed, (long long)elapsed, k);
writeLineLocked(annotated);
} else {
writeLineLocked(buf);
}
slot.last_emit_ms = now;
slot.suppressed = 0;
}
void flush() {
std::lock_guard<std::mutex> lk(g_mtx);
if (g_fp) std::fflush(g_fp);
}
std::string getLogPath() {
std::lock_guard<std::mutex> lk(g_mtx);
return g_path;
}
} // namespace DebugLog
+33
View File
@@ -0,0 +1,33 @@
#ifndef FACE_SDK_DEBUGLOG_H
#define FACE_SDK_DEBUGLOG_H
#include <string>
namespace DebugLog {
// Call once (e.g. in android_main) with android_app->activity->internalDataPath.
// Safe to call multiple times; subsequent calls are no-ops.
void init(const char* internalDataPath);
// Thread-safe. Also mirrored to logcat with tag "FACE_DBG".
// Every line is prefixed with timestamp + thread id and flushed to disk immediately,
// so even a hard crash will preserve the tail.
void log(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
// Throttled variant keyed by a caller-supplied string.
// - The first call with a given key is always written.
// - Subsequent calls with the same key within 2 seconds are suppressed.
// - When the throttle window elapses, the next matching call is written with a
// "(repeated N times in last Xms)" suffix describing the suppressed ones.
// Use stable, short keys (e.g. "drawFrame.acquire", "drawFrame.present").
void log_throttled(const char* key, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
// Force flush to disk.
void flush();
// Absolute path of the log file (valid after init()).
std::string getLogPath();
} // namespace DebugLog
#endif // FACE_SDK_DEBUGLOG_H
+341 -12
View File
@@ -3,42 +3,106 @@
#include <game-activity/native_app_glue/android_native_app_glue.h> #include <game-activity/native_app_glue/android_native_app_glue.h>
#include <game-activity/GameActivity.h> #include <game-activity/GameActivity.h>
#include "AndroidOut.h" #include "AndroidOut.h"
#include "DebugLog.h"
#include "CrashHandler.h"
#include "FaceApp.h" #include "FaceApp.h"
#include <android/asset_manager.h> #include <android/asset_manager.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <atomic>
#include <chrono> #include <chrono>
#include <cstdio>
#include <thread> #include <thread>
using namespace std; using namespace std;
static inline pid_t dbg_tid() {
return static_cast<pid_t>(syscall(SYS_gettid));
}
static const char* appCmdName(int32_t cmd) {
switch (cmd) {
case APP_CMD_INIT_WINDOW: return "APP_CMD_INIT_WINDOW";
case APP_CMD_TERM_WINDOW: return "APP_CMD_TERM_WINDOW";
case APP_CMD_WINDOW_RESIZED: return "APP_CMD_WINDOW_RESIZED";
case APP_CMD_WINDOW_REDRAW_NEEDED: return "APP_CMD_WINDOW_REDRAW_NEEDED";
case APP_CMD_CONTENT_RECT_CHANGED: return "APP_CMD_CONTENT_RECT_CHANGED";
case APP_CMD_GAINED_FOCUS: return "APP_CMD_GAINED_FOCUS";
case APP_CMD_LOST_FOCUS: return "APP_CMD_LOST_FOCUS";
case APP_CMD_CONFIG_CHANGED: return "APP_CMD_CONFIG_CHANGED";
case APP_CMD_LOW_MEMORY: return "APP_CMD_LOW_MEMORY";
case APP_CMD_START: return "APP_CMD_START";
case APP_CMD_RESUME: return "APP_CMD_RESUME";
case APP_CMD_SAVE_STATE: return "APP_CMD_SAVE_STATE";
case APP_CMD_PAUSE: return "APP_CMD_PAUSE";
case APP_CMD_STOP: return "APP_CMD_STOP";
case APP_CMD_DESTROY: return "APP_CMD_DESTROY";
default: return "APP_CMD_???";
}
}
extern "C" { extern "C" {
android_app* g_android_app = nullptr; android_app* g_android_app = nullptr;
FaceApp* g_Application = nullptr; FaceApp* g_Application = nullptr;
AAssetManager* g_assetManager = nullptr; AAssetManager* g_assetManager = nullptr;
jobject g_callback = nullptr;
jobject g_callbackAnimationFinished = nullptr;
void handle_cmd(android_app *pApp, int32_t cmd) { void handle_cmd(android_app *pApp, int32_t cmd) {
DebugLog::log(">> handle_cmd %s(%d) tid=%d", appCmdName(cmd), (int)cmd, dbg_tid());
switch (cmd) { switch (cmd) {
case APP_CMD_INIT_WINDOW: case APP_CMD_INIT_WINDOW:
aout << "APP_CMD_INIT_WINDOW" << std::endl; aout << "APP_CMD_INIT_WINDOW" << std::endl;
g_Application->initVulkan(); DebugLog::log("handle_cmd APP_CMD_INIT_WINDOW: calling onWindowInit()");
if (g_Application != nullptr) {
g_Application->onWindowInit();
DebugLog::log("handle_cmd APP_CMD_INIT_WINDOW: onWindowInit() returned, isInited=%d",
(int)g_Application->isInited());
} else {
DebugLog::log("handle_cmd APP_CMD_INIT_WINDOW: g_Application is null, skipping");
}
break; break;
case APP_CMD_TERM_WINDOW: case APP_CMD_TERM_WINDOW:
aout << "APP_CMD_TERM_WINDOW" << std::endl; aout << "APP_CMD_TERM_WINDOW" << std::endl;
DebugLog::log("handle_cmd APP_CMD_TERM_WINDOW: calling onWindowLost()");
if (g_Application != nullptr) {
g_Application->onWindowLost();
}
DebugLog::log("handle_cmd APP_CMD_TERM_WINDOW: onWindowLost() returned");
break; break;
default: default:
break; break;
} }
DebugLog::log("<< handle_cmd %s done", appCmdName(cmd));
} }
const int ArgLen = 128*1024; const int ArgLen = 128*1024;
char g_InitArgString[ArgLen] = {0}; char g_InitArgString[ArgLen] = {0};
void android_main(struct android_app *pApp) { void android_main(struct android_app *pApp) {
DebugLog::init(pApp->activity->internalDataPath);
// 安装信号处理器越早越好:之前的 FORTIFY: pthread_mutex_lock 崩溃
// 是裸的 SIGABRT,没有任何 signal handler,所以 DebugLog 文件停在最后
// 一条业务日志、看不到任何崩溃栈。装上之后任何 fatal signal 都会把:
// * 信号名 / si_code / si_addr / pid / tid / 当前 note
// * 完整 backtracePC + 模块 + 符号 + 偏移)
// 同步落到 internalDataPath/face_sdk_crash.log(以及 DebugLog 文件尾),
// 然后再走默认 handler 让 debuggerd 产生 tombstone。
CrashHandler::install(pApp->activity->internalDataPath,
DebugLog::getLogPath());
DebugLog::log("android_main start, pid=%d tid=%d, logPath=%s",
getpid(), dbg_tid(), DebugLog::getLogPath().c_str());
aout << "Welcome to android_main" << std::endl; aout << "Welcome to android_main" << std::endl;
g_android_app = pApp; g_android_app = pApp;
g_assetManager = pApp->activity->assetManager; g_assetManager = pApp->activity->assetManager;
FaceApp application; //FaceApp application;
g_Application = &application; //g_Application = &application;
application.SetInitArg(g_InitArgString); if(g_Application == nullptr) {
g_Application = new FaceApp();
}
g_Application->SetInitArg(g_InitArgString);
pApp->onAppCmd = handle_cmd; pApp->onAppCmd = handle_cmd;
long long last_update_time = 0; long long last_update_time = 0;
android_app_set_motion_event_filter(pApp, nullptr); android_app_set_motion_event_filter(pApp, nullptr);
@@ -70,23 +134,67 @@ void android_main(struct android_app *pApp) {
} }
} }
} }
if(application.isInited()) // isInited() checks _applicationInited && _faceAppInited && _sceondInited,
// so while the window is gone (between TERM_WINDOW and the next
// INIT_WINDOW) this is false and we skip drawFrame entirely — no
// exceptions, no log spam. The outer sleep at the bottom already
// caps the loop frequency.
if(g_Application->isInited())
{ {
auto frameTime = (start_time - _lastDrawFrameTime); auto frameTime = (start_time - _lastDrawFrameTime);
_lastDrawFrameTime = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count(); _lastDrawFrameTime = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
application.drawFrame(frameTime);
static uint64_t s_frameIdx = 0;
static uint64_t s_excCount = 0;
++s_frameIdx;
if (s_frameIdx % 600 == 0) {
DebugLog::log("heartbeat: frame=%llu exceptions_so_far=%llu",
(unsigned long long)s_frameIdx,
(unsigned long long)s_excCount);
}
// 每 60 帧刷新一次崩溃 note,崩溃时 dump 里能看到「最后一次活着」
// 的帧号和异常数,定位是不是在某一个固定帧附近卡死。
if (s_frameIdx % 60 == 0) {
char note[128];
std::snprintf(note, sizeof(note),
"drawFrame frame=%llu exc=%llu",
(unsigned long long)s_frameIdx,
(unsigned long long)s_excCount);
CrashHandler::setNote(note);
}
try {
g_Application->drawFrame(frameTime);
} catch (const std::exception& e) {
++s_excCount;
DebugLog::log_throttled("main.drawFrame.stdexc",
"!!! drawFrame std::exception (frame=%llu count=%llu): %s",
(unsigned long long)s_frameIdx,
(unsigned long long)s_excCount,
e.what());
} catch (...) {
++s_excCount;
DebugLog::log_throttled("main.drawFrame.unknown",
"!!! drawFrame unknown exception (frame=%llu count=%llu)",
(unsigned long long)s_frameIdx,
(unsigned long long)s_excCount);
}
} }
auto end_time = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count(); auto end_time = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
auto frameTime = end_time - last_update_time; auto frameTime = end_time - last_update_time;
last_update_time = end_time; last_update_time = end_time;
auto function_time = (end_time - start_time); auto function_time = (end_time - start_time);
aout << "function_time:" << function_time << " fps:" << (1000/frameTime) << std::endl; //aout << "function_time:" << function_time << " fps:" << (1000/frameTime) << std::endl;
if(function_time < 35) if(function_time < 35)
{ {
this_thread::sleep_for(chrono::milliseconds((35-function_time))); this_thread::sleep_for(chrono::milliseconds((35-function_time)));
} }
} while (!pApp->destroyRequested); } while (!pApp->destroyRequested);
application.cleanup(); DebugLog::log("android_main: destroyRequested, running cleanup");
//application.cleanup();
g_Application->cleanupSecondInit();
g_Application->clearnSecondFaceApp();
DebugLog::log("android_main: exit");
} }
} }
@@ -106,6 +214,12 @@ Java_com_hmwl_face_1sdk_FaceActivity_processImageNative(JNIEnv *env, jobject thi
jint width, jint height, jint format, jint width, jint height, jint format,
jint row_stride, jint pixel_stride, jint row_stride, jint pixel_stride,
jint rotation) { jint rotation) {
static std::atomic<uint64_t> s_imgCount{0};
uint64_t n = s_imgCount.fetch_add(1) + 1;
if (n == 1 || n % 300 == 0) {
DebugLog::log("processImageNative #%llu tid=%d w=%d h=%d",
(unsigned long long)n, dbg_tid(), width, height);
}
// TODO: implement processImageNative() // TODO: implement processImageNative()
uint8_t* imageData = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer)); uint8_t* imageData = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
jlong capacity = env->GetDirectBufferCapacity(buffer); jlong capacity = env->GetDirectBufferCapacity(buffer);
@@ -122,6 +236,12 @@ extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz, jobject buffer, Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz, jobject buffer,
jint point_count, jint width, jint height) { jint point_count, jint width, jint height) {
static std::atomic<uint64_t> s_ptCount{0};
uint64_t n = s_ptCount.fetch_add(1) + 1;
if (n == 1 || n % 300 == 0) {
DebugLog::log("passDataToNative #%llu tid=%d point_count=%d w=%d h=%d",
(unsigned long long)n, dbg_tid(), point_count, width, height);
}
// TODO: implement passDataToNative() // TODO: implement passDataToNative()
float* pos = static_cast<float*>(env->GetDirectBufferAddress(buffer)); float* pos = static_cast<float*>(env->GetDirectBufferAddress(buffer));
@@ -137,9 +257,9 @@ Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz,
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_SetCppInitArg(JNIEnv *env, jobject thiz, jstring json) { Java_com_hmwl_face_1sdk_FaceActivity_SetCppInitArg(JNIEnv *env, jobject thiz, jstring json) {
// TODO: implement SetCppInitArg()
const char *nativeString = env->GetStringUTFChars(json, nullptr); const char *nativeString = env->GetStringUTFChars(json, nullptr);
jsize len = env->GetStringUTFLength(json); jsize len = env->GetStringUTFLength(json);
DebugLog::log("JNI SetCppInitArg tid=%d len=%d", dbg_tid(), (int)len);
if(len > ArgLen) if(len > ArgLen)
{ {
aout << "ArgLen to long:" << len << std::endl; aout << "ArgLen to long:" << len << std::endl;
@@ -147,19 +267,228 @@ Java_com_hmwl_face_1sdk_FaceActivity_SetCppInitArg(JNIEnv *env, jobject thiz, js
} }
memcpy(g_InitArgString, nativeString, len); memcpy(g_InitArgString, nativeString, len);
} }
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_ChangeStateCpp(JNIEnv *env, jobject thiz, jstring json) { Java_com_hmwl_face_1sdk_FaceActivity_StopRunning(JNIEnv *env, jobject thiz) {
// TODO: implement ChangeStateCpp() DebugLog::log("JNI StopRunning tid=%d", dbg_tid());
g_Application->Stop();
}
JavaVM* g_jvm = nullptr;
// 添加 JNI_OnLoad 函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm; // 保存 JavaVM 指针,供其他线程使用
return JNI_VERSION_1_6;
}
void CppCallback()
{
JNIEnv* env = nullptr;
bool needDetach = false;
// 获取当前线程的 JNIEnv
JavaVM* vm = g_jvm; // 你需要提前保存 JavaVM* 到全局变量 g_jvm
int status = vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED) {
// 当前线程未 attach,需 attach
if (vm->AttachCurrentThread(&env, nullptr) != 0) {
return;
}
needDetach = true;
} else if (status != JNI_OK) {
return;
}
if (g_callback == nullptr) {
if (needDetach) vm->DetachCurrentThread();
return;
}
// 1. 获取 CallbackInterface 的 class
jclass callbackClass = env->GetObjectClass(g_callback);
if (callbackClass == nullptr) {
if (needDetach) vm->DetachCurrentThread();
return;
}
// 2. 获取 OnLoadActionFinished 方法 ID(注意方法名大小写必须一致!)
jmethodID methodId = env->GetMethodID(callbackClass, "OnLoadMotionListFinished", "(Ljava/lang/String;)V");
if (methodId == nullptr) {
env->ExceptionDescribe(); // 打印异常
env->ExceptionClear();
if (needDetach) vm->DetachCurrentThread();
return;
}
// 3. 构造 jstring 参数
jstring jResult = env->NewStringUTF("ok");
// 4. 调用 Java 回调
env->CallVoidMethod(g_callback, methodId, jResult);
// 5. 检查是否抛出异常
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
// 6. 释放局部引用(可选,但推荐)
env->DeleteLocalRef(jResult);
env->DeleteLocalRef(callbackClass);
// 7. 如果是临时 attach 的线程,要 detach
if (needDetach) {
vm->DetachCurrentThread();
}
}
void CppAnimationFinishedCallback()
{
JNIEnv* env = nullptr;
bool needDetach = false;
// 获取当前线程的 JNIEnv
JavaVM* vm = g_jvm; // 你需要提前保存 JavaVM* 到全局变量 g_jvm
int status = vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED) {
// 当前线程未 attach,需 attach
if (vm->AttachCurrentThread(&env, nullptr) != 0) {
return;
}
needDetach = true;
} else if (status != JNI_OK) {
return;
}
if (g_callbackAnimationFinished == nullptr) {
if (needDetach) vm->DetachCurrentThread();
return;
}
// 1. 获取 CallbackInterface 的 class
jclass callbackClass = env->GetObjectClass(g_callbackAnimationFinished);
if (callbackClass == nullptr) {
if (needDetach) vm->DetachCurrentThread();
return;
}
// 2. 获取 OnLoadActionFinished 方法 ID(注意方法名大小写必须一致!)
jmethodID methodId = env->GetMethodID(callbackClass, "OnPlayMotionListFinished", "()V");
if (methodId == nullptr) {
env->ExceptionDescribe(); // 打印异常
env->ExceptionClear();
if (needDetach) vm->DetachCurrentThread();
return;
}
// 4. 调用 Java 回调
env->CallVoidMethod(g_callbackAnimationFinished, methodId);
// 5. 检查是否抛出异常
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
env->DeleteLocalRef(callbackClass);
// 7. 如果是临时 attach 的线程,要 detach
if (needDetach) {
vm->DetachCurrentThread();
}
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_PreReadAction(JNIEnv *env, jobject thiz, jstring motion,
jobject callback) {
DebugLog::log("JNI PreReadAction tid=%d", dbg_tid());
// 清理旧的全局引用
if (g_callback != nullptr) {
env->DeleteGlobalRef(g_callback);
g_callback = nullptr;
}
// 保存新的回调
if (callback != nullptr) {
g_callback = env->NewGlobalRef(callback);
} else {
g_callback = nullptr;
}
const char *nativeString = env->GetStringUTFChars(motion, nullptr);
jsize len = env->GetStringUTFLength(motion);
if(len > ArgLen)
{
aout << "ArgLen to long:" << len << std::endl;
}
std::string ret = g_Application->preLoadMotionList(nativeString, CppCallback);
return env->NewStringUTF(ret.c_str());
}
const char DELIMITER = 0x1F; // same as \u001F
std::vector<std::string> splitMotionString(const std::string& input) {
std::vector<std::string> result;
if (input.empty()) return result;
size_t start = 0;
size_t pos = input.find(DELIMITER);
while (pos != std::string::npos) {
result.push_back(input.substr(start, pos - start));
start = pos + 1;
pos = input.find(DELIMITER, start);
}
// Add last part
result.push_back(input.substr(start));
return result;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_ChangeMotionCpp(JNIEnv *env, jobject thiz, jstring json,
jobject callback, jboolean loop) {
const char *nativeString = env->GetStringUTFChars(json, nullptr); const char *nativeString = env->GetStringUTFChars(json, nullptr);
jsize len = env->GetStringUTFLength(json); jsize len = env->GetStringUTFLength(json);
DebugLog::log_throttled("JNI.ChangeMotionCpp",
"JNI ChangeMotionCpp tid=%d len=%d loop=%d",
dbg_tid(), (int)len, (int)loop);
if(len > ArgLen) if(len > ArgLen)
{ {
aout << "ArgLen to long:" << len << std::endl; aout << "ArgLen to long:" << len << std::endl;
return; return;
} }
// 保存新的回调
if (callback != nullptr) {
g_callbackAnimationFinished = env->NewGlobalRef(callback);
} else {
g_callbackAnimationFinished = nullptr;
}
if(g_Application->isInited()) if(g_Application->isInited())
{ {
g_Application->changeMotion(nativeString); std::vector<std::string> motions = splitMotionString(nativeString);
g_Application->changeMotionList(motions, CppAnimationFinishedCallback, loop);
} }
} }
extern "C"
JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_StopMotionNative(JNIEnv *env, jobject thiz) {
DebugLog::log("JNI StopMotionNative tid=%d", dbg_tid());
g_Application->StopMotion();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_hmwl_face_1sdk_FaceActivity_ResumeMotionNative(JNIEnv *env, jobject thiz) {
DebugLog::log("JNI ResumeMotionNative tid=%d", dbg_tid());
g_Application->ResumeMotion();
}
@@ -4,7 +4,6 @@ import android.Manifest;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -31,7 +30,6 @@ import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.LandmarkerListener { public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.LandmarkerListener {
private static final String TAG = "FaceActivity"; private static final String TAG = "FaceActivity";
@@ -50,6 +48,21 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
} }
} }
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
// 检查权限是否被授予
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户授予了权限,启动相机
startCamera();
} else {
requestCameraPermission();
}
}
}
private void startCamera() { private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
ProcessCameraProvider.getInstance(this); ProcessCameraProvider.getInstance(this);
@@ -81,12 +94,12 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
{ {
detectFace(image); detectFace(image);
} }
else // else
{ // {
image.close(); // image.close();
} // }
} finally { } finally {
//image.close(); // 确保在这里关闭 image.close(); // 确保在这里关闭
//Log.d("Camera", "Image closed, ready for next frame"); //Log.d("Camera", "Image closed, ready for next frame");
} }
} }
@@ -122,6 +135,22 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
} }
} }
public void Stop(){
StopRunning();
}
private native void StopMotionNative();
public void StopMotion(){
StopMotionNative();
}
private native void ResumeMotionNative();
public void ResumeMotion(){
ResumeMotionNative();
}
private native void StopRunning();
// Native 方法 // Native 方法
private native void processImageNative(ByteBuffer buffer, int width, int height, private native void processImageNative(ByteBuffer buffer, int width, int height,
int format, int rowStride, int pixelStride, int format, int rowStride, int pixelStride,
@@ -261,21 +290,45 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
protected native void SetCppInitArg(String json); protected native void SetCppInitArg(String json);
protected void SetInitArg(String json) public void SetInitArg(String json)
{ {
SetCppInitArg(json); SetCppInitArg(json);
} }
protected native void ChangeStateCpp(String json); public interface LoadMotionListFinishedCallback {
protected void ChangeState(String json){ void OnLoadMotionListFinished(String result);
Log.i("FaceActivity", "ChangeState:" + json); }
ChangeStateCpp(json);
public interface PlayMotionListFinishedCallback {
void OnPlayMotionListFinished();
}
protected native String PreReadAction(String motion, LoadMotionListFinishedCallback callback);
public String PreLoadAction(String json, LoadMotionListFinishedCallback callback){
return PreReadAction(json, callback);
}
protected native void ChangeMotionCpp(String json, PlayMotionListFinishedCallback callback, boolean loop);
public void PlayMotionList(List<String> motionList, boolean loop, PlayMotionListFinishedCallback callback)
{
if(callback == null)
{
callback = new PlayMotionListFinishedCallback() {
@Override
public void OnPlayMotionListFinished() {
Log.i(TAG, "OnPlayMotionListFinished: ");
}
};
}
final String DELIMITER = "\u001F"; // ASCII Unit Separator
String motion_list_str = String.join(DELIMITER, motionList);
ChangeMotionCpp(motion_list_str, callback, loop);
} }
// 添加这个缺失的方法 // 添加这个缺失的方法
@Override @Override
public void onEmpty() { public void onEmpty() {
// 当没有检测到人脸时的处理逻辑 // 当没有检测到人脸时的处理逻辑
Log.d(TAG, "No face detected in the current frame"); //Log.d(TAG, "No face detected in the current frame");
} }
} }
@@ -67,6 +67,9 @@ class FaceLandmarkerHelper(
} }
} }
minFaceDetectionConfidence = 0.1f;
minFaceTrackingConfidence = 0.1f;
minFacePresenceConfidence = 0.1f;
try { try {
val baseOptions = baseOptionBuilder.build() val baseOptions = baseOptionBuilder.build()
// Create an option builder with base options and specific // Create an option builder with base options and specific
@@ -189,7 +192,7 @@ class FaceLandmarkerHelper(
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
) )
imageProxy.use { bitmapBuffer.copyPixelsFromBuffer(imageProxy.planes[0].buffer) } imageProxy.use { bitmapBuffer.copyPixelsFromBuffer(imageProxy.planes[0].buffer) }
imageProxy.close() //imageProxy.close()
val matrix = Matrix().apply { val matrix = Matrix().apply {
// Rotate the frame received from the camera to be in the same direction as it'll be shown // Rotate the frame received from the camera to be in the same direction as it'll be shown
@@ -238,32 +241,33 @@ class FaceLandmarkerHelper(
val frameId = result.timestampMs() val frameId = result.timestampMs()
if( result.faceLandmarks().size > 0 ) { if( result.faceLandmarks().size > 0 ) {
// 计算各种延时 // 计算各种延时
val timings = frameTimings[frameId] // val timings = frameTimings[frameId]
if (timings != null) { // if (timings != null) {
val totalLatency = resultTime - timings.captureTime // val totalLatency = resultTime - timings.captureTime
val preprocessLatency = timings.preprocessEndTime - timings.preprocessStartTime // val preprocessLatency = timings.preprocessEndTime - timings.preprocessStartTime
val detectionLatency = resultTime - timings.detectionStartTime // val detectionLatency = resultTime - timings.detectionStartTime
val captureToDetectionLatency = timings.detectionStartTime - timings.captureTime // val captureToDetectionLatency = timings.detectionStartTime - timings.captureTime
//
Log.i("TimeLatency", // Log.i("TimeLatency",
"总延时: ${totalLatency}ms | " + // "总延时: ${totalLatency}ms | " +
"预处理: ${preprocessLatency}ms | " + // "预处理: ${preprocessLatency}ms | " +
"检测: ${detectionLatency}ms | " + // "检测: ${detectionLatency}ms | " +
"捕获到检测: ${captureToDetectionLatency}ms | " + // "捕获到检测: ${captureToDetectionLatency}ms | " +
"FPS: ${String.format("%.1f", currentFps)}" // "FPS: ${String.format("%.1f", currentFps)}"
) // )
//
val finishTimeMs = SystemClock.uptimeMillis() // val finishTimeMs = SystemClock.uptimeMillis()
val inferenceTime = finishTimeMs - result.timestampMs() // val inferenceTime = finishTimeMs - result.timestampMs()
faceLandmarkerHelperListener?.onResults( // faceLandmarkerHelperListener?.onResults(
ResultBundle( // ResultBundle(
result, // result,
inferenceTime, // 主要是检测时间 // inferenceTime, // 主要是检测时间
input.height, // input.height,
input.width, // input.width,
) // )
) // )
} else { // } else
//{
val finishTimeMs = SystemClock.uptimeMillis() val finishTimeMs = SystemClock.uptimeMillis()
val inferenceTime = finishTimeMs - result.timestampMs() val inferenceTime = finishTimeMs - result.timestampMs()
Log.i("TimeLatency", Log.i("TimeLatency",
@@ -277,7 +281,7 @@ class FaceLandmarkerHelper(
input.width input.width
) )
) )
} //}
} }
else { else {
faceLandmarkerHelperListener?.onEmpty() faceLandmarkerHelperListener?.onEmpty()
@@ -0,0 +1,27 @@
package com.hmwl.face_sdk;
import org.json.JSONObject;
public class Frame {
public String name;
public float x;
public float y;
public Frame(String name, float x, float y)
{
this.name = name;
this.x = x;
this.y = y;
}
public JSONObject toJsonObject(){
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", name);
jsonObject.put("x", x);
jsonObject.put("y", y);
return jsonObject;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
@@ -4,13 +4,23 @@ import org.json.JSONObject;
public class InitArg { public class InitArg {
public int action_fps; public int action_fps;
public float zoom; public float zoom;
public Motion motion; public float r;
public float g;
public float b;
public float radius;
public float offset_x;
public float offset_y;
public String toJson() { public String toJson() {
try { try {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("action_fps", action_fps); jsonObject.put("action_fps", action_fps);
jsonObject.put("zoom", zoom); jsonObject.put("zoom", zoom);
jsonObject.put("motion", motion.toJsonObject()); jsonObject.put("r", r);
jsonObject.put("g", g);
jsonObject.put("b", b);
jsonObject.put("radius", radius);
jsonObject.put("offset_x", offset_x);
jsonObject.put("offset_y", offset_y);
return jsonObject.toString(); return jsonObject.toString();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
+13 -16
View File
@@ -5,32 +5,29 @@ import org.json.JSONObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Motion { public class Motion {
public String type = "no"; public String name = "no";
public List<String> png_names = new ArrayList<>(); public List<Frame> frames = new ArrayList<>();
public Motion(String name, List<Frame> frames)
{
this.name = name;
this.frames = frames;
}
public JSONObject toJsonObject() { public JSONObject toJsonObject() {
try { try {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("type", type); jsonObject.put("name", name);
JSONArray ja = new JSONArray(); JSONArray jframes = new JSONArray();
for(int i = 0; i < png_names.size(); ++i){ for(int i = 0; i < frames.size(); ++i){
ja.put(png_names.get(i)); jframes.put(frames.get(i).toJsonObject());
} }
jsonObject.put("png_names", ja); jsonObject.put("frames", jframes);
return jsonObject; return jsonObject;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
} }
public String toJson() {
try {
return toJsonObject().toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}; };
@@ -0,0 +1,28 @@
package com.hmwl.face_sdk;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public class MotionList {
public List<Motion> motions = new ArrayList<>();
public JSONObject toJsonObject() {
try {
JSONObject jsonObject = new JSONObject();
JSONArray jMotions = new JSONArray();
for(int i = 0; i < motions.size(); ++i){
jMotions.put(motions.get(i).toJsonObject());
}
jsonObject.put("motions", jMotions);
return jsonObject;
} catch (Exception e) {
e.printStackTrace();
return new JSONObject();
}
}
public String toJsonString(){
return toJsonObject().toString();
}
}
+15 -2
View File
@@ -1,6 +1,19 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.2'
}
}
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false // 添加这行 alias(libs.plugins.android.library) apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false alias(libs.plugins.kotlin.android) apply false // 添加这行
//alias(libs.plugins.kotlin.android) apply false
//id 'org.jetbrains.kotlin.android' version '2.2.0' apply false
} }
+4 -3
View File
@@ -4,8 +4,9 @@ glslangValidator -V app/src/main/assets/shaders/bg.vert -o app/src/main/assets/s
glslangValidator -V app/src/main/assets/shaders/texture.frag -o app/src/main/assets/shaders/texture.frag.spv glslangValidator -V app/src/main/assets/shaders/texture.frag -o app/src/main/assets/shaders/texture.frag.spv
glslangValidator -V app/src/main/assets/shaders/texture.vert -o app/src/main/assets/shaders/texture.vert.spv glslangValidator -V app/src/main/assets/shaders/texture.vert -o app/src/main/assets/shaders/texture.vert.spv
glslangValidator -V app/src/main/assets/shaders/texture_thick.frag -o app/src/main/assets/shaders/texture_thick.frag.spv
glslangValidator -V app/src/main/assets/shaders/texture_thick.vert -o app/src/main/assets/shaders/texture_thick.vert.spv
glslangValidator -V app/src/main/assets/shaders/simple_shader.frag -o app/src/main/assets/shaders/simple_shader.frag.spv glslangValidator -V app/src/main/assets/shaders/simple_shader.frag -o app/src/main/assets/shaders/simple_shader.frag.spv
glslangValidator -V app/src/main/assets/shaders/simple_shader.vert -o app/src/main/assets/shaders/simple_shader.vert.spv glslangValidator -V app/src/main/assets/shaders/simple_shader.vert -o app/src/main/assets/shaders/simple_shader.vert.spv
glslangValidator -V app/src/main/assets/shaders/texture_thick.frag -o app/src/main/assets/shaders/texture_thick.frag.spv
rem glslangValidator -V app/src/main/assets/shaders/texture_thick.vert -o app/src/main/assets/shaders/texture_thick.vert.spv
+32 -2
View File
@@ -1,13 +1,15 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
//alias(libs.plugins.kotlin.android)
} }
android { android {
namespace 'com.hmwl.example' namespace 'com.inewme.uvmirror'
compileSdk 36 compileSdk 36
defaultConfig { defaultConfig {
applicationId "com.hmwl.f20251110" applicationId "com.inewme.uvmirror.f20260113"
minSdk 30 minSdk 30
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
@@ -18,6 +20,7 @@ android {
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.debug
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
@@ -26,6 +29,20 @@ android {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17
} }
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.1'
}
kotlinOptions {
jvmTarget = '17'
}
// kotlinOptions {
// jvmTarget = '17'
// }
} }
dependencies { dependencies {
@@ -33,11 +50,21 @@ dependencies {
implementation libs.appcompat implementation libs.appcompat
implementation libs.material implementation libs.material
implementation libs.games.activity implementation libs.games.activity
implementation libs.core.ktx
implementation libs.androidx.core.ktx
testImplementation libs.junit testImplementation libs.junit
androidTestImplementation libs.ext.junit androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core androidTestImplementation libs.espresso.core
implementation project(':app') implementation project(':app')
// Jetpack Compose
implementation "androidx.compose.ui:ui:1.5.0"
implementation "androidx.compose.material3:material3:1.0.0"
implementation "androidx.compose.foundation:foundation:1.5.0"
implementation "androidx.compose.runtime:runtime:1.5.0"
implementation "androidx.activity:activity-compose:1.7.2" // ؼṩ setContent
implementation "androidx.compose.ui:ui-tooling:1.5.4"
def camerax_version = '1.4.2' def camerax_version = '1.4.2'
implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-core:$camerax_version"
@@ -53,4 +80,7 @@ dependencies {
implementation 'com.google.flogger:flogger-system-backend:0.7.4' implementation 'com.google.flogger:flogger-system-backend:0.7.4'
implementation 'com.google.guava:guava:31.1-android' implementation 'com.google.guava:guava:31.1-android'
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") // 提供 lifecycleScope
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
} }
+4
View File
@@ -20,6 +20,10 @@
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="face_sdk" /> android:value="face_sdk" />
</activity> </activity>
<activity
android:name=".MakeupActivity"
android:exported="false" />
</application> </application>
</manifest> </manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Some files were not shown because too many files have changed in this diff Show More