diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dd9080..ab55bea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android") 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 @@ -51,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 diff --git a/app/src/main/cpp/CrashHandler.cpp b/app/src/main/cpp/CrashHandler.cpp new file mode 100644 index 0000000..c7c997e --- /dev/null +++ b/app/src/main/cpp/CrashHandler.cpp @@ -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_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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 g_installed{false}; +std::atomic 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 "=\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 "=0x\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(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(pc), &info) && info.dli_fname) { + sigWrite(fd, " "); + sigWrite(fd, info.dli_fname); + + if (info.dli_sname) { + uintptr_t sym = reinterpret_cast(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(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(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 diff --git a/app/src/main/cpp/CrashHandler.h b/app/src/main/cpp/CrashHandler.h new file mode 100644 index 0000000..798316a --- /dev/null +++ b/app/src/main/cpp/CrashHandler.h @@ -0,0 +1,33 @@ +#ifndef FACE_SDK_CRASH_HANDLER_H +#define FACE_SDK_CRASH_HANDLER_H + +#include + +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 /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 diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/main.cpp index 012d60d..e22efc7 100644 --- a/app/src/main/cpp/main.cpp +++ b/app/src/main/cpp/main.cpp @@ -4,12 +4,14 @@ #include #include "AndroidOut.h" #include "DebugLog.h" +#include "CrashHandler.h" #include "FaceApp.h" #include #include #include #include #include +#include #include using namespace std; @@ -81,6 +83,15 @@ 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; @@ -141,6 +152,16 @@ void android_main(struct android_app *pApp) { (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); diff --git a/vulkan/Application.cpp b/vulkan/Application.cpp index 3a94757..b23940b 100644 --- a/vulkan/Application.cpp +++ b/vulkan/Application.cpp @@ -110,6 +110,11 @@ 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 @@ -790,7 +795,14 @@ void Application::drawFrame(long long frameTime) submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = signalSemaphores; - VkResult submitRes = vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]); + // 串行化 graphicsQueue/presentQueue 的 submit 与 present, + // 确保与 copyBuffer / updateTexture 等其它线程的队列提交互斥, + // 避免驱动内部 pthread_mutex 在长时间并发下被破坏。 + VkResult submitRes; + { + std::lock_guard poolLock(poolQueueMtx); + submitRes = vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]); + } if (submitRes != VK_SUCCESS) { #ifndef _WIN32 DebugLog::log_throttled("drawFrame.submit", @@ -810,7 +822,10 @@ void Application::drawFrame(long long frameTime) presentInfo.swapchainCount = 1; presentInfo.pSwapchains = swapChains; presentInfo.pImageIndices = &imageIndex; - result = vkQueuePresentKHR(presentQueue, &presentInfo); + { + std::lock_guard poolLock(poolQueueMtx); + result = vkQueuePresentKHR(presentQueue, &presentInfo); + } if (result != VK_SUCCESS) { #ifndef _WIN32 @@ -1121,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 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& 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 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 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, @@ -1153,33 +1346,36 @@ 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); - transitionImageLayout(commandBuffer, texture.image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + VkBufferImageCopy region = {}; + region.bufferOffset = 0; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = { 0, 0, 0 }; + region.imageExtent = { static_cast(width), + static_cast(height), 1 }; - VkBufferImageCopy region = {}; - region.bufferOffset = 0; - region.bufferRowLength = 0; - region.bufferImageHeight = 0; - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.mipLevel = 0; - region.imageSubresource.baseArrayLayer = 0; - region.imageSubresource.layerCount = 1; - region.imageOffset = { 0, 0, 0 }; - region.imageExtent = { static_cast(width), - static_cast(height), 1 }; - - vkCmdCopyBufferToImage(commandBuffer, texture.stagingBuffer, texture.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - - transitionImageLayout(commandBuffer, texture.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - - endSingleTimeCommands(device, commandPool, queue, commandBuffer); + vkCmdCopyBufferToImage(commandBuffer, texture.stagingBuffer, texture.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + transitionImageLayout(commandBuffer, texture.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + }); texture.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; } diff --git a/vulkan/Application.h b/vulkan/Application.h index 9b6441a..4421b19 100644 --- a/vulkan/Application.h +++ b/vulkan/Application.h @@ -5,6 +5,7 @@ #include "AppBase.h" #include #include +#include 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& record); + protected: VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; // 物理设备 @@ -104,6 +165,21 @@ public: 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& image_data, size_t image_size, int w, int h, Texture& tex, bool srgb, VkCommandPool pool, std::string path); diff --git a/vulkan/FaceApp.cpp b/vulkan/FaceApp.cpp index 6e6a5cd..0df27f0 100644 --- a/vulkan/FaceApp.cpp +++ b/vulkan/FaceApp.cpp @@ -889,6 +889,12 @@ void FaceApp::onWindowLost() std::unique_lock lk_point(mtx_point); std::unique_lock lk_motion(changeMotionMtx); std::unique_lock 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 lk_pool(poolQueueMtx); Application::cleanupForWindowLost(); _secondfaceAppInited = false; @@ -1053,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(); - - 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); + // 改造说明: + // 旧实现是 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); + }); } void FaceApp::createUniformBuffer() @@ -1458,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(); diff --git a/vulkan/FaceApp.h b/vulkan/FaceApp.h index 0df7f61..a29815f 100644 --- a/vulkan/FaceApp.h +++ b/vulkan/FaceApp.h @@ -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;