Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50680ee854 | ||
|
|
2066b3b124 | ||
|
|
27b7075818 | ||
|
|
fb434f09b8 |
@@ -12,4 +12,5 @@
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
.idea
|
||||
local.properties
|
||||
|
||||
+8
-1
@@ -26,6 +26,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
|
||||
add_library(face_sdk SHARED
|
||||
app/src/main/cpp/main.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.cpp
|
||||
vulkan/Application.h
|
||||
@@ -49,7 +53,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
|
||||
game-activity::game-activity_static
|
||||
Vulkan::Vulkan
|
||||
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
|
||||
VMA_STATIC_VULKAN_FUNCTIONS=0
|
||||
|
||||
+191
@@ -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 构建
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+121
-4
@@ -3,12 +3,44 @@
|
||||
#include <game-activity/native_app_glue/android_native_app_glue.h>
|
||||
#include <game-activity/GameActivity.h>
|
||||
#include "AndroidOut.h"
|
||||
#include "DebugLog.h"
|
||||
#include "CrashHandler.h"
|
||||
#include "FaceApp.h"
|
||||
#include <android/asset_manager.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
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" {
|
||||
|
||||
android_app* g_android_app = nullptr;
|
||||
@@ -19,23 +51,49 @@ jobject g_callback = nullptr;
|
||||
jobject g_callbackAnimationFinished = nullptr;
|
||||
|
||||
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) {
|
||||
case APP_CMD_INIT_WINDOW:
|
||||
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;
|
||||
case APP_CMD_TERM_WINDOW:
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
DebugLog::log("<< handle_cmd %s done", appCmdName(cmd));
|
||||
}
|
||||
|
||||
const int ArgLen = 128*1024;
|
||||
char g_InitArgString[ArgLen] = {0};
|
||||
|
||||
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
|
||||
// * 完整 backtrace(PC + 模块 + 符号 + 偏移)
|
||||
// 同步落到 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;
|
||||
g_android_app = pApp;
|
||||
g_assetManager = pApp->activity->assetManager;
|
||||
@@ -76,11 +134,51 @@ void android_main(struct android_app *pApp) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
_lastDrawFrameTime = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
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 frameTime = end_time - last_update_time;
|
||||
@@ -92,9 +190,11 @@ void android_main(struct android_app *pApp) {
|
||||
this_thread::sleep_for(chrono::milliseconds((35-function_time)));
|
||||
}
|
||||
} while (!pApp->destroyRequested);
|
||||
DebugLog::log("android_main: destroyRequested, running cleanup");
|
||||
//application.cleanup();
|
||||
g_Application->cleanupSecondInit();
|
||||
g_Application->clearnSecondFaceApp();
|
||||
DebugLog::log("android_main: exit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +214,12 @@ Java_com_hmwl_face_1sdk_FaceActivity_processImageNative(JNIEnv *env, jobject thi
|
||||
jint width, jint height, jint format,
|
||||
jint row_stride, jint pixel_stride,
|
||||
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()
|
||||
uint8_t* imageData = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
|
||||
jlong capacity = env->GetDirectBufferCapacity(buffer);
|
||||
@@ -130,6 +236,12 @@ extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz, jobject buffer,
|
||||
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()
|
||||
float* pos = static_cast<float*>(env->GetDirectBufferAddress(buffer));
|
||||
|
||||
@@ -145,9 +257,9 @@ Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz,
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_SetCppInitArg(JNIEnv *env, jobject thiz, jstring json) {
|
||||
// TODO: implement SetCppInitArg()
|
||||
const char *nativeString = env->GetStringUTFChars(json, nullptr);
|
||||
jsize len = env->GetStringUTFLength(json);
|
||||
DebugLog::log("JNI SetCppInitArg tid=%d len=%d", dbg_tid(), (int)len);
|
||||
if(len > ArgLen)
|
||||
{
|
||||
aout << "ArgLen to long:" << len << std::endl;
|
||||
@@ -159,6 +271,7 @@ Java_com_hmwl_face_1sdk_FaceActivity_SetCppInitArg(JNIEnv *env, jobject thiz, js
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_StopRunning(JNIEnv *env, jobject thiz) {
|
||||
DebugLog::log("JNI StopRunning tid=%d", dbg_tid());
|
||||
g_Application->Stop();
|
||||
}
|
||||
|
||||
@@ -294,6 +407,7 @@ 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);
|
||||
@@ -342,9 +456,11 @@ extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_ChangeMotionCpp(JNIEnv *env, jobject thiz, jstring json,
|
||||
jobject callback, jboolean loop) {
|
||||
// TODO: implement ChangeMotionCpp()
|
||||
const char *nativeString = env->GetStringUTFChars(json, nullptr);
|
||||
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)
|
||||
{
|
||||
aout << "ArgLen to long:" << len << std::endl;
|
||||
@@ -367,11 +483,12 @@ Java_com_hmwl_face_1sdk_FaceActivity_ChangeMotionCpp(JNIEnv *env, jobject thiz,
|
||||
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) {
|
||||
// TODO: implement ResumeMotionNative()
|
||||
DebugLog::log("JNI ResumeMotionNative tid=%d", dbg_tid());
|
||||
g_Application->ResumeMotion();
|
||||
}
|
||||
+428
-9
@@ -9,8 +9,47 @@
|
||||
#include <game-activity/GameActivity.h>
|
||||
#include <vulkan/vulkan_android.h>
|
||||
#include "../app/src/main/cpp/AndroidOut.h"
|
||||
#include "../app/src/main/cpp/DebugLog.h"
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#define FACE_DBG_LOG(...) ((void)0)
|
||||
#else
|
||||
#define FACE_DBG_LOG(...) DebugLog::log(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
static const char* VkResultStr(VkResult r) {
|
||||
switch (r) {
|
||||
case VK_SUCCESS: return "VK_SUCCESS";
|
||||
case VK_NOT_READY: return "VK_NOT_READY";
|
||||
case VK_TIMEOUT: return "VK_TIMEOUT";
|
||||
case VK_EVENT_SET: return "VK_EVENT_SET";
|
||||
case VK_EVENT_RESET: return "VK_EVENT_RESET";
|
||||
case VK_INCOMPLETE: return "VK_INCOMPLETE";
|
||||
case VK_SUBOPTIMAL_KHR: return "VK_SUBOPTIMAL_KHR";
|
||||
case VK_ERROR_OUT_OF_HOST_MEMORY: return "VK_ERROR_OUT_OF_HOST_MEMORY";
|
||||
case VK_ERROR_OUT_OF_DEVICE_MEMORY: return "VK_ERROR_OUT_OF_DEVICE_MEMORY";
|
||||
case VK_ERROR_INITIALIZATION_FAILED: return "VK_ERROR_INITIALIZATION_FAILED";
|
||||
case VK_ERROR_DEVICE_LOST: return "VK_ERROR_DEVICE_LOST";
|
||||
case VK_ERROR_MEMORY_MAP_FAILED: return "VK_ERROR_MEMORY_MAP_FAILED";
|
||||
case VK_ERROR_LAYER_NOT_PRESENT: return "VK_ERROR_LAYER_NOT_PRESENT";
|
||||
case VK_ERROR_EXTENSION_NOT_PRESENT: return "VK_ERROR_EXTENSION_NOT_PRESENT";
|
||||
case VK_ERROR_FEATURE_NOT_PRESENT: return "VK_ERROR_FEATURE_NOT_PRESENT";
|
||||
case VK_ERROR_INCOMPATIBLE_DRIVER: return "VK_ERROR_INCOMPATIBLE_DRIVER";
|
||||
case VK_ERROR_TOO_MANY_OBJECTS: return "VK_ERROR_TOO_MANY_OBJECTS";
|
||||
case VK_ERROR_FORMAT_NOT_SUPPORTED: return "VK_ERROR_FORMAT_NOT_SUPPORTED";
|
||||
case VK_ERROR_FRAGMENTED_POOL: return "VK_ERROR_FRAGMENTED_POOL";
|
||||
case VK_ERROR_OUT_OF_DATE_KHR: return "VK_ERROR_OUT_OF_DATE_KHR";
|
||||
case VK_ERROR_SURFACE_LOST_KHR: return "VK_ERROR_SURFACE_LOST_KHR";
|
||||
case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR";
|
||||
case VK_ERROR_OUT_OF_POOL_MEMORY: return "VK_ERROR_OUT_OF_POOL_MEMORY";
|
||||
case VK_ERROR_INVALID_EXTERNAL_HANDLE: return "VK_ERROR_INVALID_EXTERNAL_HANDLE";
|
||||
case VK_ERROR_FRAGMENTATION: return "VK_ERROR_FRAGMENTATION";
|
||||
case VK_ERROR_UNKNOWN: return "VK_ERROR_UNKNOWN";
|
||||
default: return "VK_UNMAPPED_VALUE";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Application::initWindow()
|
||||
@@ -71,11 +110,19 @@ void Application::initVulkan()
|
||||
createCommandPool(); // 创建命令池
|
||||
createCommandBuffer(); // 创建命令缓冲区
|
||||
createSyncObjects(); // 创建同步对象
|
||||
// 复用式 single-time-command 资源:从 commandPool_ex 预分配
|
||||
// kTransferSlotCount 个 cmdbuf + 同数 signaled fence,
|
||||
// 之后所有 copyBuffer / updateTexture 走 runTransferCommand,
|
||||
// 不再每次 vkAllocate/vkFree。
|
||||
createTransferResources();
|
||||
_lastDrawFrameTime = getCurrentTimeMillis();
|
||||
_applicationInited = true;
|
||||
// The first branch above already built all of the window-dependent
|
||||
// objects. Mark _sceondInited so we don't fall into the recovery
|
||||
// branch below and leak a second copy of surface/swapChain/etc.
|
||||
_sceondInited = true;
|
||||
}
|
||||
|
||||
if (!_sceondInited) {
|
||||
else if (!_sceondInited) {
|
||||
createSurface();
|
||||
createSwapChain();
|
||||
createImageViews();
|
||||
@@ -109,6 +156,155 @@ void Application::cleanupSecondInit()
|
||||
_sceondInited = false;
|
||||
}
|
||||
|
||||
void Application::cleanupForWindowLost()
|
||||
{
|
||||
if (!_applicationInited || !_sceondInited) {
|
||||
FACE_DBG_LOG("cleanupForWindowLost: skip (_applicationInited=%d _sceondInited=%d)",
|
||||
(int)_applicationInited, (int)_sceondInited);
|
||||
return;
|
||||
}
|
||||
FACE_DBG_LOG("cleanupForWindowLost: begin (imageCount=%zu MAX_FRAMES_IN_FLIGHT=%d extent=%ux%u format=%d)",
|
||||
swapChainImages.size(), MAX_FRAMES_IN_FLIGHT,
|
||||
swapChainExtent.width, swapChainExtent.height, (int)swapChainImageFormat);
|
||||
|
||||
// Snapshot before we destroy the swapchain so reinitForNewWindow() can
|
||||
// decide whether the new swapchain needs renderPass / pipelines rebuilt.
|
||||
_prevSwapChainExtent = swapChainExtent;
|
||||
_prevSwapChainImageFormat = swapChainImageFormat;
|
||||
|
||||
// Block until GPU is no longer using any of the resources we are about
|
||||
// to destroy. Anything weaker than this risks hitting VK_ERROR_DEVICE_LOST
|
||||
// on drivers that don't tolerate destroying in-flight resources.
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
for (auto framebuffer : swapChainFramebuffers) {
|
||||
if (framebuffer != VK_NULL_HANDLE) {
|
||||
vkDestroyFramebuffer(device, framebuffer, nullptr);
|
||||
}
|
||||
}
|
||||
swapChainFramebuffers.clear();
|
||||
|
||||
for (auto imageView : swapChainImageViews) {
|
||||
if (imageView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, imageView, nullptr);
|
||||
}
|
||||
}
|
||||
swapChainImageViews.clear();
|
||||
|
||||
// Free the command buffers allocated from commandPool. The pool itself is
|
||||
// kept alive so textures/staging uploads that run concurrently off the
|
||||
// render thread (via commandPool_ex) aren't disrupted.
|
||||
if (!commandBuffers.empty()) {
|
||||
vkFreeCommandBuffers(device, commandPool,
|
||||
(uint32_t)commandBuffers.size(), commandBuffers.data());
|
||||
commandBuffers.clear();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < imageAvailableSemaphores.size(); ++i) {
|
||||
if (imageAvailableSemaphores[i] != VK_NULL_HANDLE) {
|
||||
vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
|
||||
}
|
||||
}
|
||||
imageAvailableSemaphores.clear();
|
||||
for (size_t i = 0; i < renderFinishedSemaphores.size(); ++i) {
|
||||
if (renderFinishedSemaphores[i] != VK_NULL_HANDLE) {
|
||||
vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
|
||||
}
|
||||
}
|
||||
renderFinishedSemaphores.clear();
|
||||
for (size_t i = 0; i < inFlightFences.size(); ++i) {
|
||||
if (inFlightFences[i] != VK_NULL_HANDLE) {
|
||||
vkDestroyFence(device, inFlightFences[i], nullptr);
|
||||
}
|
||||
}
|
||||
inFlightFences.clear();
|
||||
imagesInFlight.clear();
|
||||
|
||||
if (swapChain != VK_NULL_HANDLE) {
|
||||
vkDestroySwapchainKHR(device, swapChain, nullptr);
|
||||
swapChain = VK_NULL_HANDLE;
|
||||
}
|
||||
swapChainImages.clear();
|
||||
|
||||
if (surface != VK_NULL_HANDLE) {
|
||||
vkDestroySurfaceKHR(instance, surface, nullptr);
|
||||
surface = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
currentFrame = 0;
|
||||
_sceondInited = false;
|
||||
FACE_DBG_LOG("cleanupForWindowLost: done");
|
||||
}
|
||||
|
||||
bool Application::reinitForNewWindow()
|
||||
{
|
||||
if (!_applicationInited) {
|
||||
FACE_DBG_LOG("reinitForNewWindow: skip, _applicationInited=0 (must go through initVulkan first)");
|
||||
return false;
|
||||
}
|
||||
if (_sceondInited) {
|
||||
FACE_DBG_LOG("reinitForNewWindow: skip, already _sceondInited=1");
|
||||
return false;
|
||||
}
|
||||
FACE_DBG_LOG("reinitForNewWindow: begin (prev extent=%ux%u format=%d)",
|
||||
_prevSwapChainExtent.width, _prevSwapChainExtent.height,
|
||||
(int)_prevSwapChainImageFormat);
|
||||
|
||||
createSurface();
|
||||
createSwapChain();
|
||||
|
||||
const bool extentChanged = (swapChainExtent.width != _prevSwapChainExtent.width) ||
|
||||
(swapChainExtent.height != _prevSwapChainExtent.height);
|
||||
const bool formatChanged = (swapChainImageFormat != _prevSwapChainImageFormat);
|
||||
const bool swapchainIncompatible = extentChanged || formatChanged;
|
||||
|
||||
if (formatChanged) {
|
||||
// renderPass bakes in swapChainImageFormat, so it must be rebuilt
|
||||
// when the surface chose a different format. The old Application
|
||||
// graphicsPipeline is tied to the old renderPass, so destroy it
|
||||
// first to make the handle invalidation explicit.
|
||||
FACE_DBG_LOG("reinitForNewWindow: format changed (%d -> %d), rebuilding renderPass",
|
||||
(int)_prevSwapChainImageFormat, (int)swapChainImageFormat);
|
||||
if (graphicsPipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(device, graphicsPipeline, nullptr);
|
||||
graphicsPipeline = VK_NULL_HANDLE;
|
||||
}
|
||||
if (pipelineLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
||||
pipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
if (renderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(device, renderPass, nullptr);
|
||||
renderPass = VK_NULL_HANDLE;
|
||||
}
|
||||
createRenderPass();
|
||||
createPipelineLayout();
|
||||
createGraphicsPipeline();
|
||||
} else if (extentChanged) {
|
||||
// renderPass is still fine, but the Application pipeline has a static
|
||||
// viewport baked to the old extent and must be rebuilt.
|
||||
FACE_DBG_LOG("reinitForNewWindow: extent changed (%ux%u -> %ux%u), rebuilding graphicsPipeline",
|
||||
_prevSwapChainExtent.width, _prevSwapChainExtent.height,
|
||||
swapChainExtent.width, swapChainExtent.height);
|
||||
if (graphicsPipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(device, graphicsPipeline, nullptr);
|
||||
graphicsPipeline = VK_NULL_HANDLE;
|
||||
}
|
||||
createGraphicsPipeline();
|
||||
}
|
||||
|
||||
createImageViews();
|
||||
createFramebuffers();
|
||||
createCommandBuffer();
|
||||
createSyncObjects();
|
||||
|
||||
_sceondInited = true;
|
||||
FACE_DBG_LOG("reinitForNewWindow: done (imageCount=%zu extent=%ux%u format=%d MAX_FRAMES_IN_FLIGHT=%d swapchainIncompatible=%d)",
|
||||
swapChainImages.size(), swapChainExtent.width, swapChainExtent.height,
|
||||
(int)swapChainImageFormat, MAX_FRAMES_IN_FLIGHT, (int)swapchainIncompatible);
|
||||
return swapchainIncompatible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Application::createImageViews() {
|
||||
@@ -545,13 +741,30 @@ void Application::render(VkCommandBuffer commandBuffer, long long frameTime)
|
||||
|
||||
void Application::drawFrame(long long frameTime)
|
||||
{
|
||||
static uint64_t s_drawFrameCount = 0;
|
||||
++s_drawFrameCount;
|
||||
|
||||
// 1. 等待前一帧完成
|
||||
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
|
||||
VkResult waitRes = vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
|
||||
if (waitRes != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("drawFrame.waitFences",
|
||||
"drawFrame[%llu] vkWaitForFences -> %d (%s) currentFrame=%u",
|
||||
(unsigned long long)s_drawFrameCount,
|
||||
(int)waitRes, VkResultStr(waitRes), currentFrame);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 2. 获取交换链图像
|
||||
uint32_t imageIndex;
|
||||
VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
|
||||
if (result != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("drawFrame.acquire",
|
||||
"drawFrame[%llu] vkAcquireNextImageKHR -> %d (%s) currentFrame=%u",
|
||||
(unsigned long long)s_drawFrameCount,
|
||||
(int)result, VkResultStr(result), currentFrame);
|
||||
#endif
|
||||
throw std::runtime_error("failed to acquire swap chain image!");
|
||||
}
|
||||
|
||||
@@ -582,7 +795,22 @@ void Application::drawFrame(long long frameTime)
|
||||
submitInfo.signalSemaphoreCount = 1;
|
||||
submitInfo.pSignalSemaphores = signalSemaphores;
|
||||
|
||||
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
|
||||
// 串行化 graphicsQueue/presentQueue 的 submit 与 present,
|
||||
// 确保与 copyBuffer / updateTexture 等其它线程的队列提交互斥,
|
||||
// 避免驱动内部 pthread_mutex 在长时间并发下被破坏。
|
||||
VkResult submitRes;
|
||||
{
|
||||
std::lock_guard<std::mutex> poolLock(poolQueueMtx);
|
||||
submitRes = vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
|
||||
}
|
||||
if (submitRes != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("drawFrame.submit",
|
||||
"drawFrame[%llu] vkQueueSubmit -> %d (%s) currentFrame=%u imageIndex=%u",
|
||||
(unsigned long long)s_drawFrameCount,
|
||||
(int)submitRes, VkResultStr(submitRes),
|
||||
currentFrame, imageIndex);
|
||||
#endif
|
||||
throw std::runtime_error("failed to submit draw command buffer!");
|
||||
}
|
||||
// 7. 呈现图像
|
||||
@@ -594,9 +822,19 @@ void Application::drawFrame(long long frameTime)
|
||||
presentInfo.swapchainCount = 1;
|
||||
presentInfo.pSwapchains = swapChains;
|
||||
presentInfo.pImageIndices = &imageIndex;
|
||||
{
|
||||
std::lock_guard<std::mutex> poolLock(poolQueueMtx);
|
||||
result = vkQueuePresentKHR(presentQueue, &presentInfo);
|
||||
}
|
||||
|
||||
if (result != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("drawFrame.present",
|
||||
"drawFrame[%llu] vkQueuePresentKHR -> %d (%s) currentFrame=%u imageIndex=%u",
|
||||
(unsigned long long)s_drawFrameCount,
|
||||
(int)result, VkResultStr(result),
|
||||
currentFrame, imageIndex);
|
||||
#endif
|
||||
throw std::runtime_error("failed to present swap chain image!");
|
||||
}
|
||||
|
||||
@@ -898,6 +1136,184 @@ void Application::endSingleTimeCommands(VkDevice device, VkCommandPool commandPo
|
||||
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
}
|
||||
|
||||
void Application::createTransferResources()
|
||||
{
|
||||
if (m_xferInited) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log("createTransferResources: already inited, skip");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (commandPool_ex == VK_NULL_HANDLE) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log("createTransferResources: commandPool_ex is null, abort");
|
||||
#endif
|
||||
throw std::runtime_error("createTransferResources: commandPool_ex not created yet");
|
||||
}
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandPool = commandPool_ex;
|
||||
allocInfo.commandBufferCount = kTransferSlotCount;
|
||||
if (vkAllocateCommandBuffers(device, &allocInfo, m_xferCmd) != VK_SUCCESS) {
|
||||
throw std::runtime_error("createTransferResources: vkAllocateCommandBuffers failed");
|
||||
}
|
||||
|
||||
// fence 创建为 SIGNALED:第一次 runTransferCommand 的 vkWaitForFences
|
||||
// 会立刻返回,避免冷启动卡顿。
|
||||
VkFenceCreateInfo fenceInfo{};
|
||||
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
||||
for (uint32_t i = 0; i < kTransferSlotCount; ++i) {
|
||||
if (vkCreateFence(device, &fenceInfo, nullptr, &m_xferFence[i]) != VK_SUCCESS) {
|
||||
for (uint32_t j = 0; j < i; ++j) {
|
||||
vkDestroyFence(device, m_xferFence[j], nullptr);
|
||||
m_xferFence[j] = VK_NULL_HANDLE;
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool_ex, kTransferSlotCount, m_xferCmd);
|
||||
for (uint32_t j = 0; j < kTransferSlotCount; ++j) m_xferCmd[j] = VK_NULL_HANDLE;
|
||||
throw std::runtime_error("createTransferResources: vkCreateFence failed");
|
||||
}
|
||||
}
|
||||
|
||||
m_xferIdx = 0;
|
||||
m_xferInited = true;
|
||||
#ifndef _WIN32
|
||||
DebugLog::log("createTransferResources: done, slots=%u pool=commandPool_ex",
|
||||
(unsigned)kTransferSlotCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::destroyTransferResources()
|
||||
{
|
||||
if (!m_xferInited) {
|
||||
return;
|
||||
}
|
||||
// 调用方必须保证 GPU 已 idle 且没有线程正在 runTransferCommand。
|
||||
// 这里再加一道 m_xferMtx,串行化潜在的最后一次 transfer。
|
||||
std::lock_guard<std::mutex> xferLock(m_xferMtx);
|
||||
|
||||
for (uint32_t i = 0; i < kTransferSlotCount; ++i) {
|
||||
if (m_xferFence[i] != VK_NULL_HANDLE) {
|
||||
vkDestroyFence(device, m_xferFence[i], nullptr);
|
||||
m_xferFence[i] = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
if (m_xferCmd[0] != VK_NULL_HANDLE && commandPool_ex != VK_NULL_HANDLE) {
|
||||
vkFreeCommandBuffers(device, commandPool_ex, kTransferSlotCount, m_xferCmd);
|
||||
}
|
||||
for (uint32_t i = 0; i < kTransferSlotCount; ++i) {
|
||||
m_xferCmd[i] = VK_NULL_HANDLE;
|
||||
}
|
||||
m_xferIdx = 0;
|
||||
m_xferInited = false;
|
||||
#ifndef _WIN32
|
||||
DebugLog::log("destroyTransferResources: done");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::runTransferCommand(const std::function<void(VkCommandBuffer)>& record)
|
||||
{
|
||||
if (!m_xferInited) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.notInited",
|
||||
"runTransferCommand: not inited, skip");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// 串行化所有 transfer 调用:
|
||||
// - 保证 m_xferIdx 推进 / m_xferCmd[idx] 录制 / m_xferFence[idx] 等待
|
||||
// 形成一组原子动作;
|
||||
// - 同时也保证 commandPool_ex 的「外部同步」语义(同一时刻只允许一个
|
||||
// 线程对它做 record/reset)。
|
||||
std::lock_guard<std::mutex> xferLock(m_xferMtx);
|
||||
|
||||
const uint32_t idx = m_xferIdx;
|
||||
VkFence fence = m_xferFence[idx];
|
||||
VkCommandBuffer cmd = m_xferCmd[idx];
|
||||
|
||||
// 等上一次该 slot 的提交真正完成。fence 是独立同步对象,不需要持
|
||||
// poolQueueMtx 就可以等待,drawFrame 的 submit 不会被阻塞。
|
||||
VkResult wr = vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
|
||||
if (wr != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.wait",
|
||||
"runTransferCommand: vkWaitForFences slot=%u -> %d",
|
||||
idx, (int)wr);
|
||||
#endif
|
||||
}
|
||||
|
||||
vkResetFences(device, 1, &fence);
|
||||
vkResetCommandBuffer(cmd, 0);
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.begin",
|
||||
"runTransferCommand: vkBeginCommandBuffer slot=%u failed", idx);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用方在这里 record 命令:vkCmdCopyBuffer / vkCmdCopyBufferToImage /
|
||||
// transitionImageLayout 等。这些是纯 record 操作,在 m_xferMtx 持有
|
||||
// 且 cmd 独占的前提下不需要再额外加锁。
|
||||
record(cmd);
|
||||
|
||||
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.end",
|
||||
"runTransferCommand: vkEndCommandBuffer slot=%u failed", idx);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
VkSubmitInfo submitInfo{};
|
||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &cmd;
|
||||
|
||||
// vkQueueSubmit 必须和 drawFrame 的 vkQueueSubmit / vkQueuePresentKHR
|
||||
// 互斥(队列要求外部同步)。其它步骤只占 m_xferMtx 即可。
|
||||
{
|
||||
std::lock_guard<std::mutex> poolLock(poolQueueMtx);
|
||||
VkResult sr = vkQueueSubmit(graphicsQueue, 1, &submitInfo, fence);
|
||||
if (sr != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.submit",
|
||||
"runTransferCommand: vkQueueSubmit slot=%u -> %d",
|
||||
idx, (int)sr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 关键:调用方代码(FaceApp::uploadVertexData / Application::updateTexture)
|
||||
// 在 runTransferCommand 之后会立刻 vkMapMemory + memcpy 覆写共享的
|
||||
// staging buffer,去做下一次拷贝。所以本接口必须像旧的 vkQueueWaitIdle
|
||||
// 一样保证 GPU **已读完** staging 才能返回,否则覆写会 race 上 GPU
|
||||
// 还在执行的 vkCmdCopyBuffer / vkCmdCopyBufferToImage,导致顶点 / 纹理
|
||||
// 数据被错位拼接(外观就是模型畸形 / 纹理花屏)。
|
||||
//
|
||||
// 这里只等自己这一次的 fence,不像旧实现 vkQueueWaitIdle 那样等整个
|
||||
// graphicsQueue(包括 drawFrame 的提交),所以不会拖慢渲染主路径。
|
||||
//
|
||||
// 不持 poolQueueMtx:fence 是独立同步对象,等它不需要外部互斥。
|
||||
VkResult er = vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
|
||||
if (er != VK_SUCCESS) {
|
||||
#ifndef _WIN32
|
||||
DebugLog::log_throttled("runTransferCommand.endWait",
|
||||
"runTransferCommand: end vkWaitForFences slot=%u -> %d",
|
||||
idx, (int)er);
|
||||
#endif
|
||||
}
|
||||
|
||||
m_xferIdx = (idx + 1) % kTransferSlotCount;
|
||||
}
|
||||
|
||||
|
||||
void Application::updateTexture(VkDevice device, VkPhysicalDevice physicalDevice,
|
||||
VkCommandPool commandPool, VkQueue queue,
|
||||
@@ -930,8 +1346,13 @@ void Application::updateTexture(VkDevice device, VkPhysicalDevice physicalDevice
|
||||
|
||||
vkUnmapMemory(device, texture.stagingBufferMemory);
|
||||
|
||||
VkCommandBuffer commandBuffer = beginSingleTimeCommands(device, commandPool);
|
||||
|
||||
// 走 runTransferCommand:复用 commandPool_ex 上预分配的 cmdbuf + fence,
|
||||
// 不再每帧 vkAllocate/vkFree,也不再 vkQueueWaitIdle。
|
||||
// 注意:传入的 commandPool / queue 参数保留只是为了兼容旧接口,
|
||||
// 实际命令池一律换成 commandPool_ex(与渲染主管线物理隔离)。
|
||||
(void)commandPool;
|
||||
(void)queue;
|
||||
runTransferCommand([&](VkCommandBuffer commandBuffer) {
|
||||
transitionImageLayout(commandBuffer, texture.image,
|
||||
VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
@@ -954,9 +1375,7 @@ void Application::updateTexture(VkDevice device, VkPhysicalDevice physicalDevice
|
||||
transitionImageLayout(commandBuffer, texture.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
|
||||
endSingleTimeCommands(device, commandPool, queue, commandBuffer);
|
||||
|
||||
});
|
||||
|
||||
texture.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "AppBase.h"
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
|
||||
struct Texture
|
||||
{
|
||||
@@ -48,8 +49,68 @@ public:
|
||||
virtual void render(VkCommandBuffer commandBuffer, long long frameTime);
|
||||
virtual bool isInited() { return _applicationInited; }
|
||||
std::mutex createTextureMtx;
|
||||
// 全局 GPU 提交互斥锁:任何对 graphicsQueue / presentQueue 的提交
|
||||
// (vkQueueSubmit / vkQueuePresentKHR / vkQueueWaitIdle)以及共享
|
||||
// commandPool 的 vkAllocateCommandBuffers / vkFreeCommandBuffers
|
||||
// 都必须在持有此锁的期间执行,否则驱动内部维护命令池与队列的
|
||||
// pthread_mutex 在长时间多线程并发下会被破坏(FORTIFY: pthread_mutex_lock
|
||||
// called on a destroyed mutex)。
|
||||
// 需要覆盖的 3 条并发线路:
|
||||
// 1) 渲染线程 drawFrame
|
||||
// 2) processImageNative → updateTexture (single-time commands)
|
||||
// 3) passDataToNative → update_face_vertex_buffer → copyBuffer
|
||||
std::mutex poolQueueMtx;
|
||||
void processWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& texture, bool srgb, VkCommandPool pool, std::string tex_path);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 复用式 single-time-command 接口(替代每帧 allocate+submit+waitIdle+free)
|
||||
//
|
||||
// ★ 同步语义(重要):
|
||||
// 本接口 **同步**,返回时 GPU 已经读完 record 里访问的源数据。
|
||||
// 行为上等价于旧的 vkQueueSubmit + vkQueueWaitIdle,但只等自己这次
|
||||
// 提交的 fence,不阻塞渲染线程在 graphicsQueue 上的其它提交。
|
||||
//
|
||||
// 为什么必须同步:调用方 (FaceApp::uploadVertexData /
|
||||
// Application::updateTexture) 紧接着会复用同一个 staging buffer:
|
||||
//
|
||||
// vkMapMemory(staging); memcpy(staging, A); vkUnmapMemory;
|
||||
// runTransferCommand([&](cmd){ vkCmdCopyBuffer(cmd, staging, dstA); });
|
||||
// vkMapMemory(staging); memcpy(staging, B); ← 必须等 dstA 拷完!
|
||||
// vkUnmapMemory;
|
||||
// runTransferCommand([&](cmd){ vkCmdCopyBuffer(cmd, staging, dstB); });
|
||||
//
|
||||
// 如果 runTransferCommand 异步返回,第二次 memcpy 会在 GPU 还没读完
|
||||
// staging 里的 A 时就把它覆盖成 B —— 顶点 / 纹理立刻畸形。
|
||||
//
|
||||
// 行为:
|
||||
// - 共 kTransferSlotCount 个 VkCommandBuffer + 同数 VkFence,
|
||||
// 从 commandPool_ex 一次性分配,在 cleanup 时一次性释放。
|
||||
// - 每次 runTransferCommand:
|
||||
// 1) m_xferMtx.lock() (串行所有 transfer 提交)
|
||||
// 2) 当前 slot 上 vkWaitForFences (等上一次该 slot 的提交完成)
|
||||
// 3) vkResetFences + vkResetCommandBuffer + vkBegin
|
||||
// 4) 调用 record(cmd) 录制命令 (vkCmdCopy* 等)
|
||||
// 5) vkEndCommandBuffer
|
||||
// 6) poolQueueMtx 内 vkQueueSubmit(graphicsQueue, fence)
|
||||
// 7) vkWaitForFences(fence) (★ 等本次 GPU 完成才返回)
|
||||
// 8) m_xferIdx 推进到下一个 slot
|
||||
//
|
||||
// 为什么这样设计:
|
||||
// - 完全消除每帧 vkAllocateCommandBuffers / vkFreeCommandBuffers,
|
||||
// 根除驱动 per-pool mutex 在长时间高频压力下被破坏导致的
|
||||
// FORTIFY: pthread_mutex_lock called on a destroyed mutex 崩溃。
|
||||
// - 用 fence 替代 vkQueueWaitIdle,等待粒度只到自己这一次提交,
|
||||
// 不会 stall 整条 graphicsQueue(包括渲染主路径的提交)。
|
||||
// - 固定使用 commandPool_ex(与渲染主用的 commandPool 物理隔离),
|
||||
// 即使驱动还有内部 contention,也不会波及渲染主管线。
|
||||
//
|
||||
// 调用约束:
|
||||
// - 调用方 **绝不能** 提前持有 poolQueueMtx;本接口内部按需短暂获取。
|
||||
// - record 函数体内只允许 vkCmd*(命令录制)这类操作,不要在里面调
|
||||
// queue / pool 级别的 API(submit / present / pool reset 等)。
|
||||
static constexpr uint32_t kTransferSlotCount = 3;
|
||||
void runTransferCommand(const std::function<void(VkCommandBuffer)>& record);
|
||||
|
||||
protected:
|
||||
|
||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; // 物理设备
|
||||
@@ -81,6 +142,44 @@ public:
|
||||
bool _applicationInited = false;
|
||||
void cleanupSecondInit();
|
||||
|
||||
// Tear down only the window-dependent Vulkan objects so we can survive an
|
||||
// Android APP_CMD_TERM_WINDOW (screen off / background / rotate).
|
||||
// Keeps renderPass / pipelines / VMA / textures / FaceApp resources intact,
|
||||
// so pipelines created by FaceApp remain valid for the new swapchain.
|
||||
// Caller MUST hold any app-level mutexes that serialize JNI -> Vulkan access.
|
||||
void cleanupForWindowLost();
|
||||
|
||||
// Rebuild the window-dependent Vulkan objects torn down above.
|
||||
// Safe to call only after cleanupForWindowLost().
|
||||
// Returns true when the new swapchain's extent or format differs from the
|
||||
// one in use before cleanupForWindowLost(). When true, any pipeline baked
|
||||
// with a static viewport/scissor from swapChainExtent -- including the
|
||||
// FaceApp pipelines -- must be destroyed and recreated against the new
|
||||
// renderPass / extent. Application's own renderPass + graphicsPipeline are
|
||||
// already handled internally.
|
||||
bool reinitForNewWindow();
|
||||
|
||||
// Extent / format captured by the most recent cleanupForWindowLost().
|
||||
// Used by reinitForNewWindow() to decide whether renderPass / pipelines
|
||||
// must be rebuilt against the new swapchain.
|
||||
VkExtent2D _prevSwapChainExtent = {0, 0};
|
||||
VkFormat _prevSwapChainImageFormat = VK_FORMAT_UNDEFINED;
|
||||
|
||||
protected:
|
||||
// Transfer command resources(详见 runTransferCommand 上方注释)。
|
||||
// 命令缓冲来自 commandPool_ex,与渲染主用的 commandPool 物理隔离。
|
||||
VkCommandBuffer m_xferCmd[kTransferSlotCount] = { VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE };
|
||||
VkFence m_xferFence[kTransferSlotCount] = { VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE };
|
||||
uint32_t m_xferIdx = 0;
|
||||
bool m_xferInited = false;
|
||||
std::mutex m_xferMtx;
|
||||
|
||||
// 创建/销毁 transfer 资源。createTransferResources 必须在 createCommandPool
|
||||
// 之后调用;destroyTransferResources 必须在销毁 commandPool_ex 之前调用,
|
||||
// 且 GPU 已 idle(vkDeviceWaitIdle 或确认所有 fence 已 signaled)。
|
||||
void createTransferResources();
|
||||
void destroyTransferResources();
|
||||
|
||||
protected:
|
||||
void loadTexture(std::string path, Texture& tex, bool srgb, VkCommandPool pool);
|
||||
void loadTexture(std::vector<unsigned char>& image_data, size_t image_size, int w, int h, Texture& tex, bool srgb, VkCommandPool pool, std::string path);
|
||||
|
||||
+137
-33
@@ -6,6 +6,15 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include "../app/src/main/cpp/DebugLog.h"
|
||||
#define FACE_DBG_LOG(...) DebugLog::log(__VA_ARGS__)
|
||||
#define FACE_DBG_LOG_THROTTLED(key, ...) DebugLog::log_throttled(key, __VA_ARGS__)
|
||||
#else
|
||||
#define FACE_DBG_LOG(...) ((void)0)
|
||||
#define FACE_DBG_LOG_THROTTLED(key, ...) ((void)0)
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
FaceApp* FaceApp::faceIns = nullptr;
|
||||
@@ -25,11 +34,14 @@ FaceApp::~FaceApp()
|
||||
|
||||
void FaceApp::Stop()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::Stop called (_running=%d worker_joinable=%d)",
|
||||
(int)_running, (int)worker_.joinable());
|
||||
_running = false;
|
||||
if(worker_.joinable())
|
||||
{
|
||||
worker_.join();
|
||||
}
|
||||
FACE_DBG_LOG("FaceApp::Stop done");
|
||||
}
|
||||
|
||||
void ReceiveFacePoint(float* pos, int pointCount, int width, int height)
|
||||
@@ -772,6 +784,11 @@ void FaceApp::createVmaAllocator()
|
||||
|
||||
void FaceApp::initVulkan()
|
||||
{
|
||||
static int s_initVulkanCallCount = 0;
|
||||
++s_initVulkanCallCount;
|
||||
FACE_DBG_LOG("FaceApp::initVulkan enter, call#%d _applicationInited=%d _faceAppInited=%d _secondfaceAppInited=%d",
|
||||
s_initVulkanCallCount, (int)_applicationInited, (int)_faceAppInited, (int)_secondfaceAppInited);
|
||||
|
||||
Application::initVulkan();
|
||||
|
||||
if (!_faceAppInited)
|
||||
@@ -839,18 +856,112 @@ void FaceApp::initVulkan()
|
||||
_playMotion = true;
|
||||
_secondfaceAppInited = true;
|
||||
}
|
||||
|
||||
FACE_DBG_LOG("FaceApp::initVulkan exit, call#%d _applicationInited=%d _faceAppInited=%d _secondfaceAppInited=%d",
|
||||
s_initVulkanCallCount, (int)_applicationInited, (int)_faceAppInited, (int)_secondfaceAppInited);
|
||||
}
|
||||
|
||||
void FaceApp::clearnSecondFaceApp()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::clearnSecondFaceApp: _secondfaceAppInited %d -> 0",
|
||||
(int)_secondfaceAppInited);
|
||||
_secondfaceAppInited = false;
|
||||
}
|
||||
|
||||
void FaceApp::Start()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::Start: _running %d -> 1", (int)_running);
|
||||
_running = true;
|
||||
}
|
||||
|
||||
void FaceApp::onWindowLost()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::onWindowLost enter _applicationInited=%d _faceAppInited=%d _sceondInited=%d _secondfaceAppInited=%d _running=%d",
|
||||
(int)_applicationInited, (int)_faceAppInited, (int)_sceondInited,
|
||||
(int)_secondfaceAppInited, (int)_running);
|
||||
|
||||
// Stop the render loop from touching Vulkan while we tear down.
|
||||
_running = false;
|
||||
|
||||
// Serialize against JNI callbacks that may concurrently submit GPU work
|
||||
// via commandPool / commandPool_ex (processImageNative -> update texture,
|
||||
// passDataToNative -> update vertex buffer, changeMotionList).
|
||||
std::unique_lock<std::mutex> lk_point(mtx_point);
|
||||
std::unique_lock<std::mutex> lk_motion(changeMotionMtx);
|
||||
std::unique_lock<std::mutex> lk_tex(createTextureMtx);
|
||||
// 同时阻塞所有并发的 GPU 提交(drawFrame / copyBuffer / updateTexture)。
|
||||
// cleanupForWindowLost() 会执行 vkDeviceWaitIdle 并销毁 swapchain / surface /
|
||||
// semaphores / fences 等窗口相关资源,如果此时有其它线程正在 vkQueueSubmit
|
||||
// 或 vkQueuePresentKHR,会触发 FORTIFY: pthread_mutex_lock called on a
|
||||
// destroyed mutex。这里持锁确保销毁与提交是互斥的。
|
||||
std::unique_lock<std::mutex> lk_pool(poolQueueMtx);
|
||||
|
||||
Application::cleanupForWindowLost();
|
||||
_secondfaceAppInited = false;
|
||||
|
||||
FACE_DBG_LOG("FaceApp::onWindowLost done");
|
||||
}
|
||||
|
||||
void FaceApp::onWindowInit()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::onWindowInit enter _applicationInited=%d _faceAppInited=%d _sceondInited=%d _secondfaceAppInited=%d",
|
||||
(int)_applicationInited, (int)_faceAppInited, (int)_sceondInited,
|
||||
(int)_secondfaceAppInited);
|
||||
|
||||
if (!_applicationInited) {
|
||||
// First-time path: go through the full initVulkan pipeline.
|
||||
initVulkan();
|
||||
} else {
|
||||
// Recovery path after an earlier onWindowLost. Rebuild only the
|
||||
// window-dependent Vulkan objects; keep renderPass / pipelines /
|
||||
// FaceApp GPU resources intact unless the new swapchain is
|
||||
// incompatible (e.g. rotation changed extent).
|
||||
std::unique_lock<std::mutex> lk_point(mtx_point);
|
||||
std::unique_lock<std::mutex> lk_motion(changeMotionMtx);
|
||||
std::unique_lock<std::mutex> lk_tex(createTextureMtx);
|
||||
|
||||
bool swapchainIncompatible = Application::reinitForNewWindow();
|
||||
|
||||
if (swapchainIncompatible && _faceAppInited) {
|
||||
FACE_DBG_LOG("FaceApp::onWindowInit: swapchain incompatible, rebuilding FaceApp pipelines");
|
||||
recreatePipelinesForSwapchain();
|
||||
}
|
||||
|
||||
if (!_secondfaceAppInited) {
|
||||
_secondfaceAppInited = true;
|
||||
_playMotion = true;
|
||||
_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FACE_DBG_LOG("FaceApp::onWindowInit done _applicationInited=%d _faceAppInited=%d _sceondInited=%d _secondfaceAppInited=%d _running=%d",
|
||||
(int)_applicationInited, (int)_faceAppInited, (int)_sceondInited,
|
||||
(int)_secondfaceAppInited, (int)_running);
|
||||
}
|
||||
|
||||
void FaceApp::recreatePipelinesForSwapchain()
|
||||
{
|
||||
// Destroy the two FaceApp pipelines. Layouts / descriptor set layouts are
|
||||
// swapchain-independent and kept as-is so existing descriptor sets keep
|
||||
// pointing at the same texture/uniform resources.
|
||||
if (m_graphicsPipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(device, m_graphicsPipeline, nullptr);
|
||||
m_graphicsPipeline = VK_NULL_HANDLE;
|
||||
}
|
||||
if (m_graphicsPipeline_bg != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(device, m_graphicsPipeline_bg, nullptr);
|
||||
m_graphicsPipeline_bg = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Recreate against the fresh renderPass (if format changed) and the new
|
||||
// swapChainExtent (viewport/scissor are baked statically into these).
|
||||
create_face_pipelines();
|
||||
create_pipelines_bg();
|
||||
|
||||
FACE_DBG_LOG("FaceApp::recreatePipelinesForSwapchain: rebuilt for extent=%ux%u",
|
||||
swapChainExtent.width, swapChainExtent.height);
|
||||
}
|
||||
|
||||
void FaceApp::update_uniform_buffers()
|
||||
{
|
||||
uint32_t width = 480;
|
||||
@@ -948,41 +1059,20 @@ void FaceApp::update_face_vertex_buffer(float* pos, int pointCount)
|
||||
|
||||
void FaceApp::copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size)
|
||||
{
|
||||
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
|
||||
|
||||
// 改造说明:
|
||||
// 旧实现是 vkAllocate(commandPool) + vkBegin + vkCmdCopyBuffer + vkEnd +
|
||||
// vkQueueSubmit(graphicsQueue) + vkQueueWaitIdle + vkFree(commandPool)
|
||||
// 每帧 update_face_vertex_buffer 会调两次 copyBuffer(顶点 + 索引),
|
||||
// 长期高频 allocate/free 会把驱动 per-pool mutex 玩坏,触发
|
||||
// FORTIFY: pthread_mutex_lock called on a destroyed mutex。
|
||||
//
|
||||
// 现在改走基类的 runTransferCommand,复用 commandPool_ex 上预分配的
|
||||
// 3 个 cmdbuf + fence,并发安全由 m_xferMtx + poolQueueMtx 联合保证。
|
||||
runTransferCommand([&](VkCommandBuffer commandBuffer) {
|
||||
VkBufferCopy copyRegion = {};
|
||||
copyRegion.size = size;
|
||||
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
|
||||
|
||||
endSingleTimeCommands(commandBuffer);
|
||||
}
|
||||
|
||||
VkCommandBuffer FaceApp::beginSingleTimeCommands() {
|
||||
VkCommandBufferAllocateInfo allocInfo = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
|
||||
VkCommandBuffer commandBuffer;
|
||||
vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
|
||||
vkBeginCommandBuffer(commandBuffer, &beginInfo);
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
void FaceApp::endSingleTimeCommands(VkCommandBuffer commandBuffer) {
|
||||
vkEndCommandBuffer(commandBuffer);
|
||||
|
||||
VkSubmitInfo submitInfo = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &commandBuffer;
|
||||
|
||||
vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
|
||||
vkQueueWaitIdle(graphicsQueue);
|
||||
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
});
|
||||
}
|
||||
|
||||
void FaceApp::createUniformBuffer()
|
||||
@@ -1325,6 +1415,7 @@ void FaceApp::cleanupResources(VkDevice device, VmaAllocator allocator) {
|
||||
|
||||
void FaceApp::cleanup()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::cleanup enter");
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
if (uniform_buffer_mapped != nullptr)
|
||||
@@ -1352,6 +1443,9 @@ void FaceApp::cleanup()
|
||||
//}
|
||||
|
||||
destroyTexture(device, tex_bg);
|
||||
// 必须在销毁 commandPool_ex 之前释放从它分配的 transfer cmdbuf + fence。
|
||||
// vkDeviceWaitIdle 已在本函数开头调过,提交不会再有 in-flight。
|
||||
destroyTransferResources();
|
||||
vkDestroyCommandPool(device, commandPool, nullptr);
|
||||
vkDestroyCommandPool(device, commandPool_ex, nullptr);
|
||||
Application::cleanup();
|
||||
@@ -1360,7 +1454,7 @@ void FaceApp::cleanup()
|
||||
// allocator = VK_NULL_HANDLE;
|
||||
//}
|
||||
|
||||
|
||||
FACE_DBG_LOG("FaceApp::cleanup done");
|
||||
}
|
||||
|
||||
|
||||
@@ -1466,6 +1560,8 @@ void FaceApp::drawFrame(long long frameTime)
|
||||
|
||||
string FaceApp::preLoadMotionList(string motion_list_str, Callback callback)
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::preLoadMotionList called str_len=%zu _isLoadMotion=%d",
|
||||
motion_list_str.size(), (int)_isLoadMotion);
|
||||
if (_isLoadMotion)
|
||||
{
|
||||
return "failue load not finished";
|
||||
@@ -1481,11 +1577,13 @@ string FaceApp::preLoadMotionList(string motion_list_str, Callback callback)
|
||||
worker_.join();
|
||||
}
|
||||
worker_ = std::thread(&FaceApp::loadMotionThread, this);
|
||||
FACE_DBG_LOG("FaceApp::preLoadMotionList worker_ started, todo_count=%zu", _curLoadMotionList.size());
|
||||
return "ok";
|
||||
}
|
||||
|
||||
void FaceApp::loadMotionThread()
|
||||
{
|
||||
FACE_DBG_LOG("FaceApp::loadMotionThread enter, todo_count=%zu", _curLoadMotionList.size());
|
||||
while (!isInited())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
@@ -1546,6 +1644,7 @@ void FaceApp::loadMotionThread()
|
||||
// }
|
||||
update_descriptor_set(m_texs_left, m_descriptor_sets_left);
|
||||
_isLoadMotion = false;
|
||||
FACE_DBG_LOG("FaceApp::loadMotionThread done, loaded_total=%zu", motion_list_map.size());
|
||||
_callback_loadfinish();
|
||||
}
|
||||
|
||||
@@ -1557,6 +1656,9 @@ Motion FaceApp::getMotionByName(string name)
|
||||
|
||||
void FaceApp::changeMotionList(vector<string> motions, AnimationFinishedCallback callback, bool loop)
|
||||
{
|
||||
FACE_DBG_LOG_THROTTLED("FaceApp.changeMotionList",
|
||||
"FaceApp::changeMotionList called count=%zu loop=%d _isLoadMotion=%d",
|
||||
motions.size(), (int)loop, (int)_isLoadMotion);
|
||||
if (_isLoadMotion)
|
||||
{
|
||||
return;
|
||||
@@ -1608,10 +1710,12 @@ void FaceApp::changeMotionList(vector<string> motions, AnimationFinishedCallback
|
||||
}
|
||||
|
||||
void FaceApp::StopMotion() {
|
||||
FACE_DBG_LOG("FaceApp::StopMotion: _playMotion %d -> 0", (int)_playMotion);
|
||||
_playMotion = false;
|
||||
}
|
||||
|
||||
void FaceApp::ResumeMotion() {
|
||||
FACE_DBG_LOG("FaceApp::ResumeMotion: _playMotion %d -> 1", (int)_playMotion);
|
||||
_playMotion = true;
|
||||
}
|
||||
|
||||
|
||||
+20
-2
@@ -112,8 +112,6 @@ private:
|
||||
const bool kThick = false;
|
||||
|
||||
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size);
|
||||
VkCommandBuffer beginSingleTimeCommands();
|
||||
void endSingleTimeCommands(VkCommandBuffer commandBuffer);
|
||||
|
||||
// 顶点缓冲区相关
|
||||
VkBuffer m_vertexBuffer = VK_NULL_HANDLE;
|
||||
@@ -182,6 +180,26 @@ public:
|
||||
void clearnSecondFaceApp();
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
// Called from main thread in response to APP_CMD_TERM_WINDOW.
|
||||
// Serializes against JNI callbacks (processImageNative / passDataToNative /
|
||||
// changeMotionList) by taking all three FaceApp mutexes + createTextureMtx,
|
||||
// then tears down window-dependent Vulkan objects via Application.
|
||||
void onWindowLost();
|
||||
|
||||
// Called from main thread in response to APP_CMD_INIT_WINDOW.
|
||||
// Does the full first-time initVulkan() on the very first call, and a
|
||||
// lightweight swapchain/surface rebuild on subsequent calls. When the
|
||||
// new swapchain's extent or format differs from the previous one, the
|
||||
// FaceApp pipelines (baked with static viewport / old renderPass) are
|
||||
// also destroyed and recreated via recreatePipelinesForSwapchain().
|
||||
void onWindowInit();
|
||||
|
||||
// Destroy and recreate m_graphicsPipeline / m_graphicsPipeline_bg so they
|
||||
// match the current renderPass and swapChainExtent. Descriptor set layouts,
|
||||
// pipeline layouts, vertex/index buffers, uniform buffers and textures are
|
||||
// all kept. Must be called with device idle and the FaceApp mutexes held.
|
||||
void recreatePipelinesForSwapchain();
|
||||
void loadMotionThread();
|
||||
std::thread worker_;
|
||||
bool _isLoadMotion = false;
|
||||
|
||||
Reference in New Issue
Block a user