diff --git a/CHANGELOG.md b/CHANGELOG.md index 625e36fbde..caedf40c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -264,6 +264,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fixed an issue where if the first objects in the buy cart are items instead of an actor, they would be added to the first actor's inventory- even if it was an actor without an inventory (i.e a crab) +- Fixed a macOS worker-thread message box that could hang, and a libc++ build error formatting save-game timestamps. + +- Fixed a divide-by-zero on `AtomGroup` segments with no steps, which could feed bad values into the physics. + +- Fixed a couple of uninitialized values (Atom step state and the simulation frame counter) that could cause inconsistent behavior between runs. +
Removed diff --git a/Data/Base.rte/AI/HumanBehaviors.lua b/Data/Base.rte/AI/HumanBehaviors.lua index 57a41815b7..ee82b06049 100644 --- a/Data/Base.rte/AI/HumanBehaviors.lua +++ b/Data/Base.rte/AI/HumanBehaviors.lua @@ -651,7 +651,11 @@ function HumanBehaviors.WeaponSearch(AI, Owner, Abort) end AI.PickupHD = nil; - table.sort(devicesToPickUp, function(A,B) return A.score < B.score end); + -- Tie-break on deviceId so ordering is stable when scores collide. + table.sort(devicesToPickUp, function(A,B) + if A.score ~= B.score then return A.score < B.score end + return A.deviceId < B.deviceId + end); for _, deviceToPickupEntry in ipairs(devicesToPickUp) do local device = MovableMan:FindObjectByUniqueID(deviceToPickupEntry.deviceId); if MovableMan:ValidMO(device) and device:IsDevice() then @@ -767,7 +771,11 @@ function HumanBehaviors.ToolSearch(AI, Owner, Abort) end AI.PickupHD = nil; - table.sort(devicesToPickUp, function(A,B) return A.score < B.score end); -- sort the items in order of discounted distance + -- Tie-break on deviceId so ordering is stable when scores collide. + table.sort(devicesToPickUp, function(A,B) + if A.score ~= B.score then return A.score < B.score end + return A.deviceId < B.deviceId + end); -- sort the items in order of discounted distance for _, deviceToPickupEntry in ipairs(devicesToPickUp) do local device = MovableMan:FindObjectByUniqueID(deviceToPickupEntry.deviceId); if MovableMan:ValidMO(device) and device:IsDevice() then diff --git a/RTEA.vcxproj b/RTEA.vcxproj index 0610e3f5ff..b774588b95 100644 --- a/RTEA.vcxproj +++ b/RTEA.vcxproj @@ -179,7 +179,7 @@ stdcpp20 true false - Fast + Precise true #undef GetClassName false @@ -232,7 +232,7 @@ stdcpp20 true false - Fast + Precise true false #undef GetClassName @@ -286,7 +286,7 @@ stdcpp20 true false - Fast + Precise true false #undef GetClassName @@ -339,7 +339,7 @@ stdcpp20 true false - Fast + Precise true false #undef GetClassName @@ -385,7 +385,7 @@ MultiThreadedDLL false StreamingSIMDExtensions2 - Fast + Precise true Use Level2 @@ -447,7 +447,7 @@ MultiThreadedDLL false StreamingSIMDExtensions2 - Fast + Precise true Use Level2 @@ -510,7 +510,7 @@ false - Fast + Precise true Use Level2 @@ -573,7 +573,7 @@ false - Fast + Precise true Use Level2 @@ -635,7 +635,7 @@ MultiThreadedDLL false StreamingSIMDExtensions2 - Fast + Precise true Use Level2 @@ -696,7 +696,7 @@ false - Fast + Precise true Use Level2 diff --git a/Source/Entities/AtomGroup.cpp b/Source/Entities/AtomGroup.cpp index 2f6ceadf5a..30ff2a91ff 100644 --- a/Source/Entities/AtomGroup.cpp +++ b/Source/Entities/AtomGroup.cpp @@ -354,8 +354,8 @@ float AtomGroup::Travel(Vector& position, Vector& velocity, Matrix& rotation, fl HitData hitData; - // Thread locals for performance (avoid memory allocs) - thread_local std::unordered_map> hitMOAtoms; + // std::map keeps hitMOAtoms iteration MOID-ascending so impulse accumulation order is deterministic. + thread_local std::map> hitMOAtoms; hitMOAtoms.clear(); thread_local std::vector hitTerrAtoms; hitTerrAtoms.clear(); @@ -450,7 +450,7 @@ float AtomGroup::Travel(Vector& position, Vector& velocity, Matrix& rotation, fl } for (Atom* atom: m_Atoms) { - atom->SetStepRatio(static_cast(atom->GetStepsLeft()) / static_cast(stepsOnSeg)); + atom->SetStepRatio(stepsOnSeg != 0 ? static_cast(atom->GetStepsLeft()) / static_cast(stepsOnSeg) : 0.0F); } // STEP LOOP //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -800,7 +800,8 @@ Vector AtomGroup::PushTravel(Vector& position, const Vector& velocity, float pus // Thread locals for performance reasons (avoid memory allocs) thread_local std::unordered_map> MOIgnoreMap; MOIgnoreMap.clear(); - thread_local std::unordered_map>> hitMOAtoms; + // std::map keeps hitMOAtoms iteration MOID-ascending so impulse accumulation order is deterministic. + thread_local std::map>> hitMOAtoms; hitMOAtoms.clear(); thread_local std::deque> hitTerrAtoms; hitTerrAtoms.clear(); diff --git a/Source/Lua/LuaAdapters.cpp b/Source/Lua/LuaAdapters.cpp index a3f1b390df..b73f7c33cc 100644 --- a/Source/Lua/LuaAdapters.cpp +++ b/Source/Lua/LuaAdapters.cpp @@ -6,6 +6,8 @@ #include "lj_obj.h" +#include + using namespace RTE; std::unordered_map> LuaAdaptersEntityCast::s_EntityToLuabindObjectCastFunctions = {}; @@ -293,8 +295,8 @@ void LuaAdaptersScene::CalculatePathAsync(Scene* luaSelfObject, const luabind::o // As such, we need to store this function somewhere safely within our Lua state for us to access later when we need it lua_State* luaState = mainthread(G(callback.interpreter())); // Get the main thread for the state, in case we're a temp lua thread - static int currentCallbackId = 0; - int thisCallbackId = currentCallbackId++; + static std::atomic currentCallbackId{0}; + int thisCallbackId = currentCallbackId.fetch_add(1, std::memory_order_relaxed); if (luabind::type(callback) == LUA_TFUNCTION && callback.is_valid()) { luabind::call_function(luaState, "_AddAsyncPathCallback", thisCallbackId, callback); } diff --git a/Source/Managers/MovableMan.cpp b/Source/Managers/MovableMan.cpp index 904ddba508..ebaba93713 100644 --- a/Source/Managers/MovableMan.cpp +++ b/Source/Managers/MovableMan.cpp @@ -73,6 +73,8 @@ void MovableMan::Clear() { m_MaxDroppedItems = 100; m_SettlingEnabled = true; m_MOSubtractionEnabled = true; + // HitWhatMOID / HitWhatTerrMaterial compare against this each tick; it's otherwise only incremented. + m_SimUpdateFrameNumber = 0; } int MovableMan::Initialize() { diff --git a/Source/Menus/SaveLoadMenuGUI.cpp b/Source/Menus/SaveLoadMenuGUI.cpp index 03350d905e..46b9b3b275 100644 --- a/Source/Menus/SaveLoadMenuGUI.cpp +++ b/Source/Menus/SaveLoadMenuGUI.cpp @@ -180,7 +180,13 @@ void SaveLoadMenuGUI::UpdateSaveGamesGUIList() { const auto saveTime = std::chrono::system_clock::to_time_t(saveFsTime); #else // TODO - kill this monstrosity when we move to GCC13 + // libc++ file_clock rep is wider than system_clock; duration_cast lands it in range first. +#if defined(_LIBCPP_VERSION) + auto saveFsTime = std::chrono::system_clock::time_point( + std::chrono::duration_cast(save.SaveDate.time_since_epoch())); +#else auto saveFsTime = std::chrono::system_clock::time_point(save.SaveDate.time_since_epoch()); +#endif #ifdef _WIN32 // Windows epoch time are the number of seconds since... 1601-01-01 00:00:00. Seriously. saveFsTime -= std::chrono::seconds(11644473600LL); diff --git a/Source/System/Atom.cpp b/Source/System/Atom.cpp index e0f3ae690c..bfda1676b1 100644 --- a/Source/System/Atom.cpp +++ b/Source/System/Atom.cpp @@ -91,14 +91,27 @@ void Atom::Clear() { m_StepRatio = 1.0F; m_SegProgress = 0.0F; + // SetupPos branches on m_IntPos before the first step sets it. + m_IntPos[X] = m_IntPos[Y] = 0; + m_PrevIntPos[X] = m_PrevIntPos[Y] = 0; + m_IgnoreMOIDsByGroup = 0; - // Note: These fields must be cleared to avoid a very edge case bug. - // While an AtomGroup is travelling, the OnCollideWithTerrain Lua function can run, which will in turn force Create to run if it hasn't already. - // If this Create function adds to an AtomGroup (e.g. adds an Attachable to it), there will be problems. - // Setting these values in Clear doesn't help if Atoms are removed at this point, but helps if Atoms are added, since these values mean the added Atoms won't try to step forwards. - // m_Dom = 0; - // m_Delta[m_Dom] = 0; + // Bresenham step state. A fresh Atom can be stepped before SetupSeg runs (an Attachable added + // mid-travel by an OnCollideWithTerrain script), so a stale pool value makes StepForward diverge. + m_TrailPos[X] = m_TrailPos[Y] = 0; + m_HitPos[X] = m_HitPos[Y] = 0; + m_Delta[X] = m_Delta[Y] = 0; + m_Delta2[X] = m_Delta2[Y] = 0; + m_Increment[X] = m_Increment[Y] = 0; + m_Error = 0; + m_Dom = 0; + m_Sub = 0; + m_DomSteps = 0; + m_SubSteps = 0; + m_SubStepped = false; + m_StepWasTaken = false; + m_SegTraj.Reset(); } int Atom::Create(const Vector& offset, Material const* material, MovableObject* owner, Color trailColor, int trailLength) { diff --git a/Source/System/RTEError.cpp b/Source/System/RTEError.cpp index de1e78503f..b643bfbf80 100644 --- a/Source/System/RTEError.cpp +++ b/Source/System/RTEError.cpp @@ -13,6 +13,7 @@ #endif #include +#include #include #include #include @@ -30,6 +31,14 @@ #include #elif defined(__APPLE__) && defined(__MACH__) #include +#include +#endif + +#if defined(__APPLE__) && defined(__MACH__) +// Cocoa dispatches message boxes onto the main thread, which deadlocks if a worker fires one while the main thread waits on it. +static bool IsOnAppMainThread() { return pthread_main_np() != 0; } +#else +static bool IsOnAppMainThread() { return true; } #endif #include "backward/backward.hpp" @@ -205,10 +214,18 @@ void RTEError::SetExceptionHandlers() { } void RTEError::ShowMessageBox(const std::string& message) { + if (!IsOnAppMainThread()) { + std::fprintf(stderr, "RTE Warning (from worker thread): %s\n", message.c_str()); + return; + } SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, "RTE Warning! (>_<)", message.c_str(), nullptr); } bool RTEError::ShowAbortMessageBox(const std::string& message) { + if (!IsOnAppMainThread()) { + std::fprintf(stderr, "RTE Abort (from worker thread): %s\n", message.c_str()); + return false; + } enum AbortMessageButton { ButtonInvalid, ButtonExit, @@ -242,6 +259,11 @@ bool RTEError::ShowAbortMessageBox(const std::string& message) { } bool RTEError::ShowAssertMessageBox(const std::string& message) { + if (!IsOnAppMainThread()) { + // Return false (Ignore-once) so the worker can unwind; the main thread sees the assert on its next pass. + std::fprintf(stderr, "RTE Assert (from worker thread): %s\n", message.c_str()); + return false; + } enum AssertMessageButton { ButtonInvalid, ButtonAbort, diff --git a/external/sources/luabind-0.7.1/meson.build b/external/sources/luabind-0.7.1/meson.build index 989746d305..1fb34cb3f6 100644 --- a/external/sources/luabind-0.7.1/meson.build +++ b/external/sources/luabind-0.7.1/meson.build @@ -3,7 +3,8 @@ subdir('src') luabind_include = include_directories('.', 'luabind') -luabind_args = ['-D_HAS_AUTO_PTR_ETC=1', '-D_LIBCPP_ENABLE_CXX17_REMOVED_FEATURES'] +# libc++ 19 dropped the CXX17-removed umbrella; re-enable the two APIs luabind 0.7.1 + boost 1.75 use. +luabind_args = ['-D_HAS_AUTO_PTR_ETC=1', '-D_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR', '-D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION'] luabind_dependencies = [dependency('luajit', 'LuaJIT-2.1'), dependency('boost-175')] diff --git a/meson.build b/meson.build index 171c895161..4f33be11e5 100644 --- a/meson.build +++ b/meson.build @@ -30,12 +30,32 @@ if compiler.get_argument_syntax()== 'gcc' # used for gcc compatible compilers # Build against system libraries on linux message('gcc detected') + # Pin floating-point so the same source gives bit-identical results across platforms. + if host_machine.cpu_family() in ['x86', 'x86_64'] + extra_args += ['-msse2'] # baseline ISA — no x87 80-bit drift on Windows + endif + extra_args += [ + '-ffp-contract=off', # forbid FMA/contraction — required on ARM, harmless on x86 + '-fno-fast-math', + '-fno-finite-math-only', + '-fno-associative-math', + '-fno-reciprocal-math', + '-fno-unsafe-math-optimizations', + '-frounding-math', + '-fsignaling-nans', + ] + if host_machine.system() == 'linux' build_rpath = '$ORIGIN:$ORIGIN/../external/lib/linux/x86_64' # Set RUNPATH so that CCCP can find libfmod.so without needing to set LD_LIBRARY_PATH elif host_machine.system()=='darwin' build_rpath = '@executable_path/../external/lib/macos' # Add a new R_PATH CCCP can find libfmod.dylib on Darwin TODO: Confirm and validate this. endif + # Apple's libc++ needs these to expose (std::execution::par_unseq). + if compiler.get_id() == 'clang' + extra_args += ['-D_LIBCPP_ENABLE_EXPERIMENTAL', '-D_LIBCPP_PSTL_BACKEND_SERIAL'] + endif + #suffix = 'x86_64' if host_machine.system()=='linux' link_args += ['-Wl,--enable-new-dtags'] # Set RUNPATH instead of RPATH @@ -60,7 +80,7 @@ if compiler.get_argument_syntax()== 'gcc' # used for gcc compatible compilers preprocessor_flags += ['-DDEBUG_BUILD', '-DDEBUGMODE'] # enable all debug features; may slow down game endif else - extra_args = ['-w'] # Disable all warnings for release builds + extra_args += ['-w'] # Disable all warnings for release builds preprocessor_flags += ['-DRELEASE_BUILD', '-DNDEBUG'] # disable all debug features endif if compiler.get_id() =='gcc' and compiler.version().version_compare('<9') @@ -70,7 +90,8 @@ elif compiler.get_argument_syntax()== 'msvc' #TODO: add MSVC related arguments and stuff in here message('cl detected') elfname = 'Cortex Command' - extra_args += ['-D_HAS_ITERATOR_DEBUGGING=0', '-D_HAS_AUTO_PTR_ETC=1', '-bigobj'] + # /fp:precise stops MSVC rearranging FP ops; /arch:SSE2 keeps x86 off x87's 80-bit registers. + extra_args += ['-D_HAS_ITERATOR_DEBUGGING=0', '-D_HAS_AUTO_PTR_ETC=1', '-bigobj', '/fp:precise', '/arch:SSE2'] add_global_arguments('-D_ITERATOR_DEBUG_LEVEL=0', language:'cpp') add_project_link_arguments(['winmm.lib', 'ws2_32.lib', 'dinput8.lib', 'ddraw.lib', 'dxguid.lib', 'dsound.lib', 'dbghelp.lib'], language:'cpp') if host_machine.cpu_family() == 'x86'