fix(diagnostics): add render-phase crash markers and improve signal handling

Add signal-safe render-phase markers throughout GameScreen::render() and
Application::render() so the crash handler can report which render call was
active when a SIGSEGV occurs. The AMD RADV crash backtrace only shows 2
frames due to missing frame pointers, making it impossible to identify the
actual crash site.

Changes:
- Add volatile g_crashRenderPhase marker updated before each major render call
- Upgrade Linux signal handler to sigaction with SA_SIGINFO for faulting address
- Set ImGui CheckVkResultFn to log silent Vulkan errors in ImGui backend
- Enable -fno-omit-frame-pointer in all build configs (not just Debug/RelWithDebInfo)
This commit is contained in:
Kelsi 2026-04-03 20:19:33 -07:00
parent b092bc2e90
commit 82267320b0
6 changed files with 104 additions and 29 deletions

View file

@ -935,9 +935,11 @@ else()
# -g3 — maximum DWARF debug info (includes macro definitions)
# -Og — optimise for debugging (better than -O0, keeps most frames)
# -fno-omit-frame-pointer — preserve frame pointers so stack traces are clean
# Frame pointers in all configs (negligible perf cost, critical for crash backtraces)
target_compile_options(wowee PRIVATE
$<$<CONFIG:Debug>:-g3 -Og -fno-omit-frame-pointer>
$<$<CONFIG:RelWithDebInfo>:-g -fno-omit-frame-pointer>
-fno-omit-frame-pointer
$<$<CONFIG:Debug>:-g3 -Og>
$<$<CONFIG:RelWithDebInfo>:-g>
)
endif()

View file

@ -2133,15 +2133,22 @@ void Application::update(float deltaTime) {
}
}
// Render-phase marker from game_screen.cpp — updated here for 3D/submit phases
} } // close wowee::core temporarily
extern volatile const char* g_crashRenderPhase;
namespace wowee { namespace core {
void Application::render() {
if (!renderer) {
return;
}
g_crashRenderPhase = "beginFrame";
renderer->beginFrame();
// Only render 3D world when in-game
if (state == AppState::IN_GAME) {
g_crashRenderPhase = "renderWorld";
if (world) {
renderer->renderWorld(world.get(), gameHandler.get());
} else {
@ -2151,15 +2158,19 @@ void Application::render() {
// Render performance HUD (within ImGui frame, before UI ends the frame)
if (renderer) {
g_crashRenderPhase = "renderHUD";
renderer->renderHUD();
}
// Render UI on top (ends ImGui frame with ImGui::Render())
if (uiManager) {
g_crashRenderPhase = "uiRender";
uiManager->render(state, authHandler.get(), gameHandler.get());
}
g_crashRenderPhase = "endFrame";
renderer->endFrame();
g_crashRenderPhase = "idle";
}
void Application::setupUICallbacks() {

View file

@ -10,6 +10,7 @@
#include <X11/Xlib.h>
#include <execinfo.h>
#include <unistd.h>
#include <cstring>
// Keep a persistent X11 connection for emergency mouse release in signal handlers.
// XOpenDisplay inside a signal handler is unreliable, so we open it once at startup.
@ -26,32 +27,45 @@ static void releaseMouseGrab() {
static void releaseMouseGrab() {}
#endif
// Render-phase marker set by GameScreen::render() — lets crash handler
// identify which render call was active when backtrace is incomplete.
extern volatile const char* g_crashRenderPhase;
#ifdef __linux__
static void crashHandlerSigaction(int sig, siginfo_t* info, void* /*ucontext*/) {
releaseMouseGrab();
void* frames[64];
int n = backtrace(frames, 64);
const char* sigName = (sig == SIGSEGV) ? "SIGSEGV" :
(sig == SIGABRT) ? "SIGABRT" :
(sig == SIGFPE) ? "SIGFPE" : "UNKNOWN";
const char* phase = (const char*)g_crashRenderPhase;
void* faultAddr = info ? info->si_addr : nullptr;
fprintf(stderr, "\n=== CRASH: signal %s (%d) renderPhase=%s faultAddr=%p ===\n",
sigName, sig, phase ? phase : "?", faultAddr);
backtrace_symbols_fd(frames, n, STDERR_FILENO);
FILE* f = fopen("/tmp/wowee_debug.log", "a");
if (f) {
fprintf(f, "\n=== CRASH: signal %s (%d) renderPhase=%s faultAddr=%p ===\n",
sigName, sig, phase ? phase : "?", faultAddr);
fflush(f);
backtrace_symbols_fd(frames, n, fileno(f));
fclose(f);
}
// Re-raise with default handler
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
sigaction(sig, &sa, nullptr);
raise(sig);
}
#else
static void crashHandler(int sig) {
releaseMouseGrab();
#ifdef __linux__
// Dump backtrace to debug log
{
void* frames[64];
int n = backtrace(frames, 64);
const char* sigName = (sig == SIGSEGV) ? "SIGSEGV" :
(sig == SIGABRT) ? "SIGABRT" :
(sig == SIGFPE) ? "SIGFPE" : "UNKNOWN";
// Write to stderr and to the debug log file
fprintf(stderr, "\n=== CRASH: signal %s (%d) ===\n", sigName, sig);
backtrace_symbols_fd(frames, n, STDERR_FILENO);
FILE* f = fopen("/tmp/wowee_debug.log", "a");
if (f) {
fprintf(f, "\n=== CRASH: signal %s (%d) ===\n", sigName, sig);
fflush(f);
// Also write backtrace to the log file fd
backtrace_symbols_fd(frames, n, fileno(f));
fclose(f);
}
}
#endif
std::signal(sig, SIG_DFL);
std::raise(sig);
}
#endif
static wowee::core::LogLevel readLogLevelFromEnv() {
const char* raw = std::getenv("WOWEE_LOG_LEVEL");
@ -69,12 +83,25 @@ static wowee::core::LogLevel readLogLevelFromEnv() {
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
#ifdef __linux__
g_emergencyDisplay = XOpenDisplay(nullptr);
#endif
// Use sigaction for SIGSEGV/SIGABRT/SIGFPE to get si_addr (faulting address)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = crashHandlerSigaction;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
}
std::signal(SIGTERM, [](int) { std::_Exit(1); });
std::signal(SIGINT, [](int) { std::_Exit(1); });
#else
std::signal(SIGSEGV, crashHandler);
std::signal(SIGABRT, crashHandler);
std::signal(SIGFPE, crashHandler);
std::signal(SIGTERM, crashHandler);
std::signal(SIGINT, crashHandler);
#endif
try {
wowee::core::Logger::getInstance().setLogLevel(readLogLevelFromEnv());
LOG_INFO("=== Wowee Native Client ===");

View file

@ -792,6 +792,10 @@ void Renderer::applyMsaaChange() {
initInfo.ImageCount = vkCtx->getSwapchainImageCount();
initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass();
initInfo.PipelineInfoMain.MSAASamples = vkCtx->getMsaaSamples();
initInfo.CheckVkResultFn = [](VkResult err) {
if (err != VK_SUCCESS)
LOG_ERROR("ImGui Vulkan error: ", static_cast<int>(err));
};
ImGui_ImplVulkan_Init(&initInfo);
LOG_INFO("MSAA change complete");

View file

@ -47,6 +47,11 @@
#include <cctype>
#include <chrono>
#include <ctime>
// Signal-safe render-phase marker — crash handler reads this to identify which
// render call was active when a SIGSEGV occurs (backtrace is unreliable with
// -fomit-frame-pointer).
volatile const char* g_crashRenderPhase = "idle";
#include <unordered_set>
namespace {
@ -298,7 +303,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
// Process targeting input before UI windows
processTargetInput(gameHandler);
// Player unit frame (top-left)
g_crashRenderPhase = "playerFrame";
renderPlayerFrame(gameHandler);
// Pet frame (below player frame, only when player has an active pet)
@ -353,6 +358,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
}
// ---- New UI elements ----
g_crashRenderPhase = "actionBar";
actionBarPanel_.renderActionBar(gameHandler, settingsPanel_, chatPanel_,
inventoryScreen, spellbookScreen, questLogScreen,
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
@ -362,31 +368,43 @@ void GameScreen::render(game::GameHandler& gameHandler) {
actionBarPanel_.renderXpBar(gameHandler, settingsPanel_);
actionBarPanel_.renderRepBar(gameHandler, settingsPanel_);
auto spellIconFn = [this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); };
g_crashRenderPhase = "castBar";
combatUI_.renderCastBar(gameHandler, spellIconFn);
renderMirrorTimers(gameHandler);
combatUI_.renderCooldownTracker(gameHandler, settingsPanel_, spellIconFn);
renderQuestObjectiveTracker(gameHandler);
g_crashRenderPhase = "nameplates";
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
g_crashRenderPhase = "bgScore";
combatUI_.renderBattlegroundScore(gameHandler);
combatUI_.renderRaidWarningOverlay(gameHandler);
combatUI_.renderCombatText(gameHandler);
combatUI_.renderDPSMeter(gameHandler, settingsPanel_);
g_crashRenderPhase = "durability";
renderDurabilityWarning(gameHandler);
renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime);
toastManager_.renderEarlyToasts(ImGui::GetIO().DeltaTime, gameHandler);
g_crashRenderPhase = "partyFrames";
if (socialPanel_.showRaidFrames_) {
socialPanel_.renderPartyFrames(gameHandler, chatPanel_, spellIconFn);
}
g_crashRenderPhase = "bossFrames";
socialPanel_.renderBossFrames(gameHandler, spellbookScreen, spellIconFn);
g_crashRenderPhase = "dialogs";
dialogManager_.renderDialogs(gameHandler, inventoryScreen, chatPanel_);
g_crashRenderPhase = "guildRoster";
socialPanel_.renderGuildRoster(gameHandler, chatPanel_);
socialPanel_.renderSocialFrame(gameHandler, chatPanel_);
g_crashRenderPhase = "buffBar";
combatUI_.renderBuffBar(gameHandler, spellbookScreen, spellIconFn);
g_crashRenderPhase = "lootWindow";
windowManager_.renderLootWindow(gameHandler, inventoryScreen, chatPanel_);
windowManager_.renderGossipWindow(gameHandler, chatPanel_);
g_crashRenderPhase = "questWindows";
windowManager_.renderQuestDetailsWindow(gameHandler, chatPanel_, inventoryScreen);
windowManager_.renderQuestRequestItemsWindow(gameHandler, chatPanel_, inventoryScreen);
windowManager_.renderQuestOfferRewardWindow(gameHandler, chatPanel_, inventoryScreen);
g_crashRenderPhase = "vendorTrainer";
windowManager_.renderVendorWindow(gameHandler, inventoryScreen, chatPanel_);
windowManager_.renderTrainerWindow(gameHandler,
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
@ -398,39 +416,48 @@ void GameScreen::render(game::GameHandler& gameHandler) {
windowManager_.renderBankWindow(gameHandler, inventoryScreen, chatPanel_);
windowManager_.renderGuildBankWindow(gameHandler, inventoryScreen, chatPanel_);
windowManager_.renderAuctionHouseWindow(gameHandler, inventoryScreen, chatPanel_);
g_crashRenderPhase = "dungeonFinder";
socialPanel_.renderDungeonFinderWindow(gameHandler, chatPanel_);
windowManager_.renderInstanceLockouts(gameHandler);
socialPanel_.renderWhoWindow(gameHandler, chatPanel_);
g_crashRenderPhase = "combatLog";
combatUI_.renderCombatLog(gameHandler, spellbookScreen);
g_crashRenderPhase = "achievementSkills";
windowManager_.renderAchievementWindow(gameHandler);
windowManager_.renderSkillsWindow(gameHandler);
windowManager_.renderTitlesWindow(gameHandler);
windowManager_.renderEquipSetWindow(gameHandler);
windowManager_.renderGmTicketWindow(gameHandler);
g_crashRenderPhase = "inspectBook";
socialPanel_.renderInspectWindow(gameHandler, inventoryScreen);
windowManager_.renderBookWindow(gameHandler);
g_crashRenderPhase = "threatBg";
combatUI_.renderThreatWindow(gameHandler);
combatUI_.renderBgScoreboard(gameHandler);
g_crashRenderPhase = "minimap";
if (showMinimap_) {
renderMinimapMarkers(gameHandler);
}
g_crashRenderPhase = "deathLogout";
windowManager_.renderLogoutCountdown(gameHandler);
windowManager_.renderDeathScreen(gameHandler);
windowManager_.renderReclaimCorpseButton(gameHandler);
dialogManager_.renderLateDialogs(gameHandler);
chatPanel_.renderBubbles(gameHandler);
windowManager_.renderEscapeMenu(settingsPanel_);
g_crashRenderPhase = "settings";
settingsPanel_.renderSettingsWindow(inventoryScreen, chatPanel_, [this]() { saveSettings(); });
toastManager_.renderLateToasts(gameHandler);
g_crashRenderPhase = "weather";
renderWeatherOverlay(gameHandler);
// World map (M key toggle handled inside)
g_crashRenderPhase = "worldMap";
renderWorldMap(gameHandler);
// Quest Log (L key toggle handled inside)
g_crashRenderPhase = "questLog";
questLogScreen.render(gameHandler, inventoryScreen);
// Spellbook (P key toggle handled inside)
g_crashRenderPhase = "spellbook";
spellbookScreen.render(gameHandler, services_.assetManager);
// Insert spell link into chat if player shift-clicked a spellbook entry
@ -483,7 +510,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
windowManager_.vendorBagsOpened_ = false;
}
// Bags (B key toggle handled inside)
g_crashRenderPhase = "inventory";
inventoryScreen.setGameHandler(&gameHandler);
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());

View file

@ -78,6 +78,10 @@ bool UIManager::initialize(core::Window* win) {
initInfo.ImageCount = vkCtx->getSwapchainImageCount();
initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass();
initInfo.PipelineInfoMain.MSAASamples = vkCtx->getMsaaSamples();
initInfo.CheckVkResultFn = [](VkResult err) {
if (err != VK_SUCCESS)
LOG_ERROR("ImGui Vulkan error: ", static_cast<int>(err));
};
ImGui_ImplVulkan_Init(&initInfo);