From 8ea0352fedeba9b3a670e05c29e6f4319abc7e13 Mon Sep 17 00:00:00 2001 From: Irastris Date: Mon, 13 Apr 2026 12:43:42 -0400 Subject: [PATCH 1/5] Frame interp: Initial presentation sync implementation --- include/dusk/frame_interpolation.h | 5 ++ .../src/JStudio/JStudio/jstudio-object.cpp | 19 ++++++++ src/d/d_camera.cpp | 38 ++++++++++++++- src/dusk/frame_interpolation.cpp | 46 ++++++++++++++----- src/m_Do/m_Do_main.cpp | 2 + 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 5c2dc2e1b8..423ac9af37 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -20,6 +20,11 @@ void begin_record(); void end_record(); void interpolate(float step); float get_interpolation_step(); + +void notify_presentation_frame(); +void request_presentation_sync(); +bool presentation_sync_active(); + void notify_sim_tick_complete(); uint32_t begin_presentation_ui_pass(); uint32_t get_presentation_ui_advance_ticks(); diff --git a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp index 3b87a6e34a..ee99429ee2 100644 --- a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp +++ b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp @@ -2,7 +2,11 @@ #include "JSystem/JStudio/JStudio/jstudio-object.h" +#if TARGET_PC #include "dusk/audio.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" +#endif namespace JStudio { namespace { @@ -650,10 +654,25 @@ value_or_fun: return; value: +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation && u <= 5 && + (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) + { + dusk::frame_interp::request_presentation_sync(); + } +#endif adaptor->adaptor_setVariableValue(control, u, operation, param_2, param_3); return; value_n: +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation && + (pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) && + (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) + { + dusk::frame_interp::request_presentation_sync(); + } +#endif adaptor->adaptor_setVariableValue_n(control, pN, u, operation, param_2, param_3); return; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 955683e05b..7b95a0d8ea 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -20,7 +20,6 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "m_Do/m_Do_lib.h" -#include "dusk/frame_interpolation.h" #include #include @@ -29,6 +28,11 @@ #include "d/d_debug_camera.h" #endif +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#include "dusk/logging.h" +#endif + namespace { static f32 limitf(f32 value, f32 min, f32 max) { @@ -2048,6 +2052,18 @@ s32 dCamera_c::nextType(s32 i_curType) { bool dCamera_c::onTypeChange(s32 i_curType, s32 i_nextType) { daAlink_c* unusedPlayer = daAlink_getAlinkActorClass(); +#if TARGET_PC + const s32 event_type_id = specialType[CAM_TYPE_EVENT]; + DuskLog.debug( + "frameInterp: onTypeChange {} -> {} (event_type_id={}, leaving_event={}, entering_event={})", + static_cast(i_curType), + static_cast(i_nextType), + static_cast(event_type_id), + i_curType == event_type_id, + i_nextType == event_type_id + ); +#endif + if (i_curType == specialType[CAM_TYPE_EVENT]) { if (mCamSetup.CheckFlag(0x4000)) { mGear = 0; @@ -10161,6 +10177,26 @@ bool dCamera_c::eventCamera(s32 param_0) { ActionNames[var_r29]); #endif +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + switch (var_r29) { + case 3: + case 4: + case 5: + case 12: + dusk::frame_interp::request_presentation_sync(); + break; + default: + DuskLog.debug( + "frameInterp: presentation sync not requested for ZEV event [{}] (staff idx {})", + static_cast(ActionNames[var_r29]), + static_cast(mEventData.mStaffIdx) + ); + break; + } + } +#endif + if (getEvFloatData(&sp28, "KeepDist") != 0 && mViewCache.mDirection.R() < sp28) { mViewCache.mDirection.R(sp28); diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index e9b35da1f0..8c45c034c6 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -63,6 +63,10 @@ bool s_initialized = false; bool g_enabled = false; bool g_recording = false; bool g_interpolating = false; +bool g_sync_presentation = false; +uint32_t g_presentation_counter = 0; +uint32_t g_presentation_sync_end = 0; + float g_step = 0.0f; uint32_t g_pending_presentation_ui_ticks = 0; uint32_t g_current_presentation_ui_ticks = 0; @@ -235,7 +239,7 @@ void interpolate_branch(const Path& old_path, const Path& new_path, float step) } const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { - if (!g_interpolating || source == nullptr) { + if (!g_interpolating || source == nullptr || dusk::frame_interp::presentation_sync_active()) { return source; } @@ -268,6 +272,7 @@ void begin_record() { ensure_initialized(); if (!g_enabled) { g_interpolating = false; + g_sync_presentation = false; g_previous_recording = {}; g_current_recording = {}; g_current_path.clear(); @@ -275,6 +280,10 @@ void begin_record() { return; } + if (g_sync_presentation && g_presentation_counter > g_presentation_sync_end) { + g_sync_presentation = false; + } + g_previous_recording = std::move(g_current_recording); g_current_recording = {}; g_current_path.clear(); @@ -292,21 +301,38 @@ void interpolate(float step) { ensure_initialized(); clear_replacements(); g_step = std::clamp(step, 0.0f, 1.0f); - g_interpolating = g_enabled && !g_recording && has_recording_data(g_current_recording); + g_interpolating = g_enabled && !g_recording && !g_sync_presentation && has_recording_data(g_current_recording); if (!g_interpolating) { return; } + const Path& old_root = has_recording_data(g_previous_recording) ? g_previous_recording.root : g_current_recording.root; + interpolate_branch(old_root, g_current_recording.root, g_step); +} - if (!has_recording_data(g_previous_recording)) { - interpolate_branch(g_current_recording.root, g_current_recording.root, g_step); +void notify_presentation_frame() { + ensure_initialized(); + ++g_presentation_counter; +} + +void request_presentation_sync() { + ensure_initialized(); + if (!g_enabled) { return; } + g_sync_presentation = true; + g_presentation_sync_end = g_presentation_counter + 1; +} - interpolate_branch(g_previous_recording.root, g_current_recording.root, g_step); +bool presentation_sync_active() { + if (!s_initialized || !g_enabled) { + return false; + } + return g_sync_presentation; } float get_interpolation_step() { - return g_step; + ensure_initialized(); + return presentation_sync_active() ? 1.0f : g_step; } void notify_sim_tick_complete() { @@ -371,7 +397,7 @@ void record_final_mtx_raw(const Mtx* dest, const Mtx src) { } bool lookup_replacement(const void* source, Mtx out) { - if (!s_initialized || !g_interpolating || source == nullptr) { + if (presentation_sync_active() || !g_interpolating || source == nullptr) { return false; } @@ -385,7 +411,7 @@ bool lookup_replacement(const void* source, Mtx out) { } bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { - if (!s_initialized || !g_interpolating || lhs == nullptr || rhs == nullptr) { + if (presentation_sync_active() || !g_interpolating || lhs == nullptr || rhs == nullptr) { return false; } @@ -393,9 +419,7 @@ bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { Mtx rhs_scratch; const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast(lhs), &lhs_scratch); const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast(rhs), &rhs_scratch); - if (resolved_lhs == reinterpret_cast(lhs) && - resolved_rhs == reinterpret_cast(rhs)) - { + if (resolved_lhs == reinterpret_cast(lhs) && resolved_rhs == reinterpret_cast(rhs)) { return false; } diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 7aaec4b005..ce575bd18f 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -240,6 +240,8 @@ void main01(void) { } if (dusk::getSettings().game.enableFrameInterpolation) { + dusk::frame_interp::notify_presentation_frame(); + while (accumulator >= kSimStepSeconds) { mDoCPd_c::read(); if (dusk::getSettings().game.enableGyroAim) { From 4b8248b130c0750a0955a27036c39afebed96fb3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Apr 2026 00:43:48 -0600 Subject: [PATCH 2/5] 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 From 00803b53ab480fbf2b623f1c36c2ebccbc2e37a6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Apr 2026 00:57:45 -0600 Subject: [PATCH 3/5] I'm silly --- src/dusk/crash_reporting.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 1b1adbf774..2d7534965e 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -76,8 +76,6 @@ std::filesystem::path GetCrashpadHandlerPath() { } const std::filesystem::path handlerDir(basePath); - SDL_free(const_cast(basePath)); - #if _WIN32 return handlerDir / "crashpad_handler.exe"; #else From 588910c642484cb8aae0fafb1351afec3679c025 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Apr 2026 01:13:43 -0600 Subject: [PATCH 4/5] Fix DUSK_VERSION_STRING on tag commits --- CMakeLists.txt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a213b819b..8ecd64c6ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,12 +48,17 @@ else () message(STATUS "Unable to find git, commit information will not be available") endif () -if (DUSK_WC_DESCRIBE) - string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+)\-([0-9]+).*" "\\1.\\2.\\3.\\4" DUSK_VERSION_STRING "${DUSK_WC_DESCRIBE}") - string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+).*" "\\1.\\2.\\3" DUSK_SHORT_VERSION_STRING "${DUSK_WC_DESCRIBE}") +if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$") + set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") + if (CMAKE_MATCH_5) + set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}") + else () + set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0") + endif () else () set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") - set(DUSK_VERSION_STRING "0.0.0") + set(DUSK_VERSION_STRING "0.0.0.0") + set(DUSK_SHORT_VERSION_STRING "0.0.0") endif () # Add version information to CI environment variables From 27d95c37d5cd30acb6ae2a911857760b983540db Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 14 Apr 2026 01:32:58 -0600 Subject: [PATCH 5/5] Create log files under configDir/logs; seed initial pipeline cache --- include/dusk/logging.h | 3 + src/dusk/crash_reporting.cpp | 16 ++++ src/dusk/logging.cpp | 140 +++++++++++++++++++++++++++++------ src/m_Do/m_Do_main.cpp | 50 ++++++++++++- 4 files changed, 186 insertions(+), 23 deletions(-) diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 7f239d8b45..0a9cbf238d 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -7,6 +7,9 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len); namespace dusk { + void InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel); + void ShutdownFileLogging(); + const char* GetLogFilePath(); void SendToStubLog(AuroraLogLevel level, const char* module, const char* message); } diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 2d7534965e..73f432e418 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -69,6 +69,13 @@ std::filesystem::path GetSentryDatabasePath() { return std::filesystem::path(configPath) / "sentry"; } +std::filesystem::path GetLogAttachmentPath() { + if (const char* logPath = GetLogFilePath()) { + return logPath; + } + return {}; +} + std::filesystem::path GetCrashpadHandlerPath() { const char* basePath = SDL_GetBasePath(); if (!basePath) { @@ -111,6 +118,15 @@ void ConfigurePathOptions(sentry_options_t* options) { sentry_options_set_handler_path(options, handlerPathUtf8.c_str()); } #endif + + const auto logPath = GetLogAttachmentPath(); + if (!logPath.empty()) { +#if _WIN32 + sentry_options_add_attachmentw(options, logPath.wstring().c_str()); +#else + sentry_options_add_attachment(options, logPath.string().c_str()); +#endif + } } #endif diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 37d1d4f55f..323945dd8b 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -1,6 +1,11 @@ #include "dusk/logging.h" +#include +#include #include #include +#include +#include +#include #include "tracy/Tracy.hpp" @@ -20,6 +25,60 @@ static constexpr std::string_view StubFragments[] = { "but selective updates are not implemented"sv, }; +namespace { +std::mutex g_logMutex; +FILE* g_logFile = nullptr; +std::string g_logFilePath; + +const char* LogLevelString(AuroraLogLevel level) { + switch (level) { + case LOG_DEBUG: + return "DEBUG"; + case LOG_INFO: + return "INFO"; + case LOG_WARNING: + return "WARNING"; + case LOG_ERROR: + return "ERROR"; + case LOG_FATAL: + return "FATAL"; + } + + return "??"; +} + +FILE* LogStreamForLevel(AuroraLogLevel level) { + return level >= LOG_ERROR ? stderr : stdout; +} + +std::string MakeTimestampedLogName() { + const auto now = std::chrono::system_clock::now(); + const std::time_t nowTime = std::chrono::system_clock::to_time_t(now); + + std::tm localTime{}; +#if _WIN32 + localtime_s(&localTime, &nowTime); +#else + localtime_r(&nowTime, &localTime); +#endif + + std::array buffer{}; + std::strftime(buffer.data(), buffer.size(), "dusk-%Y%m%d-%H%M%S.log", &localTime); + return buffer.data(); +} + +void WriteLogLine(FILE* out, const char* levelStr, const char* module, const char* message, unsigned int len) { + if (out == nullptr) { + return; + } + + std::fprintf(out, "[%s | %s] ", levelStr, module); + std::fwrite(message, 1, len, out); + std::fputc('\n', out); + std::fflush(out); +} +} // namespace + static bool IsForStubLog(const char* message) { std::string_view msg_view(message); @@ -40,32 +99,69 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m return; } - const char* levelStr = "??"; - FILE* out = stdout; - switch (level) { - case LOG_DEBUG: - levelStr = "DEBUG"; - break; - case LOG_INFO: - levelStr = "INFO"; - break; - case LOG_WARNING: - levelStr = "WARNING"; - break; - case LOG_ERROR: - levelStr = "ERROR"; - out = stderr; - break; - case LOG_FATAL: - levelStr = "FATAL"; - out = stderr; - break; + if (module == nullptr) { + module = ""; } - fprintf(out, "[%s | %s] %s\n", levelStr, module, message); + + const char* levelStr = LogLevelString(level); + FILE* out = LogStreamForLevel(level); + WriteLogLine(out, levelStr, module, message, len); + + { + std::lock_guard lock(g_logMutex); + if (g_logFile != nullptr) { + WriteLogLine(g_logFile, levelStr, module, message, len); + } + } + if (level == LOG_FATAL) { - fflush(out); abort(); } } aurora::Module DuskLog("dusk"); + +void dusk::InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel) { + std::lock_guard lock(g_logMutex); + if (g_logFile != nullptr || configDir == nullptr) { + return; + } + + std::error_code ec; + const std::filesystem::path logsDir = std::filesystem::path(configDir) / "logs"; + std::filesystem::create_directories(logsDir, ec); + if (ec) { + std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", + logsDir.string().c_str(), ec.message().c_str()); + return; + } + + const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); + g_logFile = std::fopen(logPath.string().c_str(), "wb"); + if (g_logFile == nullptr) { + std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", + logPath.string().c_str()); + return; + } + + g_logFilePath = logPath.string(); + aurora::g_config.logCallback = &aurora_log_callback; + aurora::g_config.logLevel = logLevel; + WriteLogLine(g_logFile, "INFO", "dusk", "File logging initialized", 24); +} + +void dusk::ShutdownFileLogging() { + std::lock_guard lock(g_logMutex); + if (g_logFile == nullptr) { + return; + } + + std::fflush(g_logFile); + std::fclose(g_logFile); + g_logFile = nullptr; +} + +const char* dusk::GetLogFilePath() { + std::lock_guard lock(g_logMutex); + return g_logFilePath.empty() ? nullptr : g_logFilePath.c_str(); +} diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 2c6278e202..fbe0bce3cc 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -43,6 +43,8 @@ #include #include +#include +#include #include #include "SSystem/SComponent/c_API.h" #include "dusk/app_info.hpp" @@ -366,6 +368,48 @@ static const char* CalculateConfigPath() { return result; } +static void EnsureInitialPipelineCache(const char* configDir) { + if (configDir == nullptr) { + return; + } + + const std::filesystem::path configPathFs(configDir); + const std::filesystem::path pipelineCachePath = configPathFs / "pipeline_cache.db"; + if (std::filesystem::exists(pipelineCachePath)) { + return; + } + + const char* basePath = SDL_GetBasePath(); + if (basePath == nullptr) { + DuskLog.warn("Unable to resolve base path while seeding pipeline cache: {}", SDL_GetError()); + return; + } + + const std::filesystem::path initialPipelineCachePath = + std::filesystem::path(basePath) / "initial_pipeline_cache.db"; + if (!std::filesystem::exists(initialPipelineCachePath)) { + DuskLog.info("No bundled initial pipeline cache found at '{}'", initialPipelineCachePath.string()); + return; + } + + std::error_code ec; + std::filesystem::create_directories(configPathFs, ec); + if (ec) { + DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}", + configPathFs.string(), ec.message()); + return; + } + + std::filesystem::copy_file(initialPipelineCachePath, pipelineCachePath, std::filesystem::copy_options::none, ec); + if (ec) { + DuskLog.warn("Failed to seed pipeline cache from '{}' to '{}': {}", + initialPipelineCachePath.string(), pipelineCachePath.string(), ec.message()); + return; + } + + DuskLog.info("Seeded pipeline cache from '{}'", initialPipelineCachePath.string()); +} + static constexpr PADDefaultMapping defaultPadMapping = { .buttons = { {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, @@ -444,10 +488,13 @@ int game_main(int argc, char* argv[]) { } configPath = CalculateConfigPath(); + const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); + dusk::InitializeFileLogging(configPath, startupLogLevel); dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::InitializeCrashReporting(); + EnsureInitialPipelineCache(configPath); AuroraConfig config{}; config.appName = dusk::AppName; @@ -460,7 +507,7 @@ int game_main(int argc, char* argv[]) { config.windowHeight = defaultWindowHeight * 2; config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); config.logCallback = &aurora_log_callback; - config.logLevel = (AuroraLogLevel)parsed_arg_options["log-level"].as(); + config.logLevel = startupLogLevel; config.mem1Size = 256 * 1024 * 1024; config.mem2Size = 24 * 1024 * 1024; config.allowJoystickBackgroundEvents = true; @@ -542,6 +589,7 @@ int game_main(int argc, char* argv[]) { main01(); dusk::ShutdownCrashReporting(); + dusk::ShutdownFileLogging(); fflush(stdout); fflush(stderr);