diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d308863..f61b4024 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 - $<$:-g3 -Og -fno-omit-frame-pointer> - $<$:-g -fno-omit-frame-pointer> + -fno-omit-frame-pointer + $<$:-g3 -Og> + $<$:-g> ) endif() diff --git a/src/core/application.cpp b/src/core/application.cpp index 1a16e5ce..f0fea809 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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() { diff --git a/src/main.cpp b/src/main.cpp index 8ae707e8..8d7f92b2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // 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 ==="); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 74ff2194..7ebb7f2c 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -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(err)); + }; ImGui_ImplVulkan_Init(&initInfo); LOG_INFO("MSAA change complete"); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d1681889..6646e0fb 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -47,6 +47,11 @@ #include #include #include + +// 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 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()); diff --git a/src/ui/ui_manager.cpp b/src/ui/ui_manager.cpp index 305bb7de..cbb8997e 100644 --- a/src/ui/ui_manager.cpp +++ b/src/ui/ui_manager.cpp @@ -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(err)); + }; ImGui_ImplVulkan_Init(&initInfo);