From 4b8248b130c0750a0955a27036c39afebed96fb3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Apr 2026 00:43:48 -0600 Subject: [PATCH] Integrate Sentry crash reporting for public builds --- CMakeLists.txt | 72 +++++++++++-- files.cmake | 1 + include/dusk/crash_reporting.h | 8 ++ include/dusk/settings.h | 1 + src/dusk/crash_reporting.cpp | 173 +++++++++++++++++++++++++++++++ src/dusk/imgui/ImGuiEngine.cpp | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 3 + src/dusk/settings.cpp | 4 +- src/m_Do/m_Do_main.cpp | 4 + version.h.in | 12 ++- 10 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 include/dusk/crash_reporting.h create mode 100644 src/dusk/crash_reporting.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c740479b07..7a213b819b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,10 @@ option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) +option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) +set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") +set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") + if (DUSK_MOVIE_SUPPORT) find_package(libjpeg-turbo QUIET) if (libjpeg-turbo_FOUND) @@ -148,21 +152,23 @@ elseif (APPLE) set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_BUILD_RPATH "$ORIGIN") elseif (MSVC) - add_compile_options(/bigobj) - add_compile_options(/Zc:strictStrings-) - add_compile_options(/MP) - add_compile_options(/FS) + add_compile_options( + $<$:/bigobj> + $<$:/Zc:strictStrings-> + $<$:/MP> + $<$:/FS> + ) if (NOT DUSK_BUILD_WARNINGS) - add_compile_options(/W0) + add_compile_options($<$:/W0>) else () # Disable warnings - add_compile_options(/wd4068) # unknown pragma - add_compile_options(/wd4291) # no matching delete operator, leaks if exception thrown + add_compile_options($<$:/wd4068>) # unknown pragma + add_compile_options($<$:/wd4291>) # no matching delete operator, leaks if exception thrown # Only show warnings once - add_compile_options(/wo4244) # narrowing conversion, possible data loss + add_compile_options($<$:/wo4244>) # narrowing conversion, possible data loss endif () - add_compile_options(/utf-8) + add_compile_options($<$:/utf-8>) endif () @@ -183,6 +189,46 @@ FetchContent_Declare(json ) FetchContent_MakeAvailable(cxxopts json) +if (DUSK_ENABLE_SENTRY_NATIVE) + message(STATUS "dusk: Fetching sentry-native") + set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE) + if (WIN32) + set(SENTRY_TRANSPORT winhttp CACHE STRING "" FORCE) + endif () + set(SENTRY_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(SENTRY_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SENTRY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) + FetchContent_Declare(sentry_native + GIT_REPOSITORY https://github.com/getsentry/sentry-native.git + GIT_TAG 0.13.6 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + GIT_SUBMODULES_RECURSE TRUE + ) + if (NOT sentry_native_POPULATED) + FetchContent_Populate(sentry_native) + set(_dusk_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES}) + set(CMAKE_SKIP_INSTALL_RULES ON) + add_subdirectory(${sentry_native_SOURCE_DIR} ${sentry_native_BINARY_DIR} EXCLUDE_FROM_ALL) + set(CMAKE_SKIP_INSTALL_RULES ${_dusk_skip_install_rules}) + endif () +endif () + +if (CMAKE_SYSTEM_NAME STREQUAL Windows) + set(PLATFORM_NAME win32) +elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin) + if (IOS) + set(PLATFORM_NAME ios) + elseif (TVOS) + set(PLATFORM_NAME tvos) + else () + set(PLATFORM_NAME macos) + endif () +else () + string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME) +endif () + configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h) include(files.cmake) @@ -222,6 +268,11 @@ set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::p list(APPEND GAME_LIBS libzstd_static) +if (DUSK_ENABLE_SENTRY_NATIVE) + list(APPEND GAME_LIBS sentry) + list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1) +endif () + if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) @@ -274,6 +325,9 @@ add_executable(dusk src/dusk/main.cpp) target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) target_include_directories(dusk PRIVATE include) target_link_libraries(dusk PRIVATE game aurora::main) +if (TARGET crashpad_handler) + add_dependencies(dusk crashpad_handler) +endif () add_custom_command(TARGET dusk POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/files.cmake b/files.cmake index 5ba33086f1..2178c1066c 100644 --- a/files.cmake +++ b/files.cmake @@ -1338,6 +1338,7 @@ set(DUSK_FILES src/d/actor/d_a_alink_dusk.cpp src/dusk/asserts.cpp src/dusk/config.cpp + src/dusk/crash_reporting.cpp src/dusk/endian.cpp src/dusk/extras.c src/dusk/extras.cpp diff --git a/include/dusk/crash_reporting.h b/include/dusk/crash_reporting.h new file mode 100644 index 0000000000..dfc5bde817 --- /dev/null +++ b/include/dusk/crash_reporting.h @@ -0,0 +1,8 @@ +#pragma once + +namespace dusk { + +void InitializeCrashReporting(); +void ShutdownCrashReporting(); + +} // namespace dusk diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 9fbb17c845..8e57b9db99 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -103,6 +103,7 @@ struct UserSettings { ConfigVar skipPreLaunchUI; ConfigVar showPipelineCompilation; ConfigVar wasPresetChosen; + ConfigVar enableCrashReporting; } backend; }; diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp new file mode 100644 index 0000000000..1b1adbf774 --- /dev/null +++ b/src/dusk/crash_reporting.cpp @@ -0,0 +1,173 @@ +#include "dusk/crash_reporting.h" + +#include "dusk/app_info.hpp" +#include "dusk/dusk.h" +#include "dusk/logging.h" +#include "dusk/settings.h" +#include "version.h" + +#include +#include +#include +#include +#include + +#include "SDL3/SDL_filesystem.h" + +#if DUSK_ENABLE_SENTRY_NATIVE +#include +#endif + +namespace dusk { + +namespace { + +#if DUSK_ENABLE_SENTRY_NATIVE +bool g_sentryInitialized = false; + +bool IsTruthy(std::string_view value) { + return value == "1" || value == "true" || value == "TRUE" || value == "yes" + || value == "YES" || value == "on" || value == "ON"; +} + +std::string GetEnvOrEmpty(const char* name) { + if (const char* value = std::getenv(name)) { + return value; + } + return {}; +} + +bool GetEffectiveEnabled() { + const std::string env = GetEnvOrEmpty("DUSK_SENTRY_ENABLED"); + if (!env.empty()) { + return IsTruthy(env); + } + return getSettings().backend.enableCrashReporting; +} + +std::string GetEffectiveDsn() { + const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DSN"); + if (!env.empty()) { + return env; + } + return DUSK_SENTRY_DSN; +} + +bool GetEffectiveDebug() { + const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DEBUG"); + if (!env.empty()) { + return IsTruthy(env); + } + return false; +} + +std::string GetReleaseName() { + return std::string(AppName) + "@" DUSK_WC_DESCRIBE; +} + +std::filesystem::path GetSentryDatabasePath() { + return std::filesystem::path(configPath) / "sentry"; +} + +std::filesystem::path GetCrashpadHandlerPath() { + const char* basePath = SDL_GetBasePath(); + if (!basePath) { + return {}; + } + + const std::filesystem::path handlerDir(basePath); + SDL_free(const_cast(basePath)); + +#if _WIN32 + return handlerDir / "crashpad_handler.exe"; +#else + return handlerDir / "crashpad_handler"; +#endif +} + +void ConfigurePathOptions(sentry_options_t* options) { + const auto databasePath = GetSentryDatabasePath(); + std::error_code ec; + std::filesystem::create_directories(databasePath, ec); + if (ec) { + DuskLog.warn("Unable to create Sentry database path '{}': {}", + databasePath.string(), ec.message()); + } + +#if _WIN32 + const std::wstring databasePathWide = databasePath.wstring(); + sentry_options_set_database_pathw(options, databasePathWide.c_str()); + + const auto handlerPath = GetCrashpadHandlerPath(); + if (!handlerPath.empty()) { + const std::wstring handlerPathWide = handlerPath.wstring(); + sentry_options_set_handler_pathw(options, handlerPathWide.c_str()); + } +#else + const std::string databasePathUtf8 = databasePath.string(); + sentry_options_set_database_path(options, databasePathUtf8.c_str()); + + const auto handlerPath = GetCrashpadHandlerPath(); + if (!handlerPath.empty()) { + const std::string handlerPathUtf8 = handlerPath.string(); + sentry_options_set_handler_path(options, handlerPathUtf8.c_str()); + } +#endif +} +#endif + +} // namespace + +void InitializeCrashReporting() { +#if DUSK_ENABLE_SENTRY_NATIVE + if (g_sentryInitialized) { + return; + } + + if (!GetEffectiveEnabled()) { + return; + } + + const std::string dsn = GetEffectiveDsn(); + if (dsn.empty()) { + DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured"); + return; + } + + const std::string release = GetReleaseName(); + + sentry_options_t* options = sentry_options_new(); + sentry_options_set_dsn(options, dsn.c_str()); + sentry_options_set_release(options, release.c_str()); + sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT); + sentry_options_set_debug(options, GetEffectiveDebug() ? 1 : 0); + sentry_options_set_cache_keep(options, 1); + sentry_options_set_max_breadcrumbs(options, 100); + ConfigurePathOptions(options); + + if (sentry_init(options) != 0) { + DuskLog.warn("Failed to initialize Sentry crash reporting"); + return; + } + + sentry_set_tag("git_branch", DUSK_WC_BRANCH); + sentry_set_tag("build_type", DUSK_BUILD_TYPE); + sentry_set_tag("tp_version", DUSK_TP_VERSION); + g_sentryInitialized = true; + + DuskLog.info("Initialized Sentry crash reporting"); +#endif +} + +void ShutdownCrashReporting() { +#if DUSK_ENABLE_SENTRY_NATIVE + if (!g_sentryInitialized) { + return; + } + + sentry_close(); + g_sentryInitialized = false; +#endif +} + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 00701af02b..0c0b5e934d 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -158,6 +158,7 @@ void ImGuiEngine_Initialize(float scale) { Image GetImage(const std::string& path) { if (!AssetExists(path)) { + DuskLog.warn("Image '{}' does not exist", path); return {}; } diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 4b5aa38959..27e036ee2e 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -133,6 +133,9 @@ namespace dusk { if (ImGui::BeginMenu("Interface")) { config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); +#if DUSK_ENABLE_SENTRY_NATIVE + config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); +#endif ImGui::EndMenu(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 6e7c96a818..4a4f6186f0 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -76,7 +76,8 @@ UserSettings g_userSettings = { .graphicsBackend {"backend.graphicsBackend", "auto"}, .skipPreLaunchUI {"backend.skipPreLaunchUI", false}, .showPipelineCompilation {"backend.showPipelineCompilation", false}, - .wasPresetChosen {"backend.wasPresetChosen", false} + .wasPresetChosen {"backend.wasPresetChosen", false}, + .enableCrashReporting {"backend.enableCrashReporting", true} } }; @@ -139,6 +140,7 @@ void registerSettings() { Register(g_userSettings.backend.skipPreLaunchUI); Register(g_userSettings.backend.showPipelineCompilation); Register(g_userSettings.backend.wasPresetChosen); + Register(g_userSettings.backend.enableCrashReporting); } // Transient settings diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 31114d04ee..2c6278e202 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -46,6 +46,7 @@ #include #include "SSystem/SComponent/c_API.h" #include "dusk/app_info.hpp" +#include "dusk/crash_reporting.h" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" #include "dusk/gyro_aim.h" @@ -446,6 +447,7 @@ int game_main(int argc, char* argv[]) { dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); + dusk::InitializeCrashReporting(); AuroraConfig config{}; config.appName = dusk::AppName; @@ -501,6 +503,7 @@ int game_main(int argc, char* argv[]) { if (!dvd_opened) { // pre game launch ui main loop if (!launchUILoop()) { + dusk::ShutdownCrashReporting(); aurora_shutdown(); return 0; } @@ -538,6 +541,7 @@ int game_main(int argc, char* argv[]) { main01(); + dusk::ShutdownCrashReporting(); fflush(stdout); fflush(stderr); diff --git a/version.h.in b/version.h.in index c85a6dc659..51297bcd46 100644 --- a/version.h.in +++ b/version.h.in @@ -10,11 +10,17 @@ #define DUSK_BUILD_TYPE "@CMAKE_BUILD_TYPE@" #if defined(__x86_64__) || defined(_M_AMD64) -#define DUSK_DLPACKAGE "dusk-@DUSK_WC_DESCRIBE@-@PLATFORM_NAME@-x86_64" +#define DUSK_ARCH "x86_64" #elif defined(__i386__) || defined(_M_IX86) -#define DUSK_DLPACKAGE "dusk-@DUSK_WC_DESCRIBE@-@PLATFORM_NAME@-x86" +#define DUSK_ARCH "x86" #elif defined(__aarch64__) || defined(_M_ARM64) -#define DUSK_DLPACKAGE "dusk-@DUSK_WC_DESCRIBE@-@PLATFORM_NAME@-arm64" +#define DUSK_ARCH "arm64" #endif +#define DUSK_PLATFORM_NAME "@PLATFORM_NAME@" +#define DUSK_DLPACKAGE "dusk-@DUSK_WC_DESCRIBE@-" DUSK_PLATFORM_NAME "-" DUSK_ARCH + +#define DUSK_SENTRY_DSN "@DUSK_SENTRY_DSN@" +#define DUSK_SENTRY_ENVIRONMENT "@DUSK_SENTRY_ENVIRONMENT@" + #endif