From 2f221cfa57a64294dd7b1d1e5c565991a802fc6b Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 28 May 2026 20:22:03 -0700 Subject: [PATCH 01/19] Pin hitMOAtoms iteration order in AtomGroup Swap the per-tick hitMOAtoms thread-locals from unordered_map to std::map so impulse accumulation iterates MOID-ascending, removing a source of cross-run bit-noise in the physics step. --- Source/Entities/AtomGroup.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Entities/AtomGroup.cpp b/Source/Entities/AtomGroup.cpp index 2f6ceadf5a..dd25bd5b40 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(); @@ -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(); From 5d85d6de35ee3622506e71f8e184008029ff7ea1 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 10 Jun 2026 10:15:17 -0700 Subject: [PATCH 02/19] Tie-break devicesToPickUp sort on deviceId Add deviceId as a secondary sort key in WeaponSearch and ToolSearch so the pickup ordering is stable when scores collide; scheduler-dependent async callback order otherwise leaves the table order non-deterministic. --- Data/Base.rte/AI/HumanBehaviors.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 From 476818120c31cdad338cf307b2b052d2c107a1f3 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 28 May 2026 20:26:18 -0700 Subject: [PATCH 03/19] Cross-platform UB: message-box deadlock + file_time_type On macOS, route RTEError message/abort/assert boxes off worker threads to stderr: Cocoa dispatches the dialog onto the main thread, which deadlocks when that thread is blocked waiting on the worker. Windows/Linux tolerate cross-thread boxes and keep their behaviour. Guard the save-list file_clock -> system_clock conversion on macOS libc++, whose __int128 file_clock rep is rejected when constructing the time_point directly; an explicit duration_cast lands it in range. --- Source/Menus/SaveLoadMenuGUI.cpp | 6 ++++++ Source/System/RTEError.cpp | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Source/Menus/SaveLoadMenuGUI.cpp b/Source/Menus/SaveLoadMenuGUI.cpp index 03350d905e..df55514f2d 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 + // macOS libc++ file_clock rep is __int128; duration_cast lands it in system_clock's range first. +#if defined(__APPLE__) && 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/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, From 89da856d876fea1c4d12f4e99a172f50a96a8d99 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 28 May 2026 20:27:44 -0700 Subject: [PATCH 04/19] LuaAdapters: atomic callback-id counter CalculatePathAsync runs on the parallel ThreadedUpdateAI workers (AI scripts call Scene:CalculatePathAsync from HumanBehaviors); the static-int increment could hand two concurrent requests the same id and silently drop one Lua callback slot. Make the counter std::atomic with relaxed fetch_add. --- Source/Lua/LuaAdapters.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); } From b6cd4c32866597aef4bd3b360471495899a040bb Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 28 May 2026 20:30:17 -0700 Subject: [PATCH 05/19] Standardize FPU contract across platforms Pin floating-point so same-arch Win/Linux/macOS builds produce bit-identical results. meson: -ffp-contract=off plus the no-fast-math family for GCC/Clang, /fp:precise + /arch:SSE2 for MSVC. RTEA.vcxproj: FloatingPointModel=Precise on every config (was Fast). Document the FPU + libm path in Source/CI. --- RTEA.vcxproj | 20 ++++++++++---------- meson.build | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) 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/meson.build b/meson.build index 171c895161..0b4ea13f45 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,21 @@ 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' @@ -70,7 +85,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' From 0c667d07ed78bf67d807659f83593fa5063b731b Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 3 Jun 2026 17:25:37 -0700 Subject: [PATCH 06/19] meson: keep the FP-determinism flags in release builds The release branch reassigned extra_args to ['-w'], discarding -ffp-contract=off and the rest of the cross-platform FP block. Append instead of overwrite. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 0b4ea13f45..8625ffa1aa 100644 --- a/meson.build +++ b/meson.build @@ -75,7 +75,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') From db9137a9e117c7ccb989b9af38b1a1404202ca04 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 3 Jun 2026 17:25:38 -0700 Subject: [PATCH 07/19] Initialize Atom step state in Clear() Atom::Clear left m_IntPos and the Bresenham step fields uninitialized. SetupPos branches on m_IntPos, and StepForward steps on the Bresenham state, before SetupSeg runs for a freshly-pooled Atom, so the stale pool value (which varies with allocation order) made collision stepping nondeterministic run to run. This was the dominant source of the cross-platform determinism divergence on the combat scenarios. --- Source/System/Atom.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) 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) { From bda2903ed0cc0d2f9cf5169322082796c0a6f47c Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 3 Jun 2026 17:25:38 -0700 Subject: [PATCH 08/19] Initialize MovableMan::m_SimUpdateFrameNumber The sim-frame counter was incremented each tick but never initialized, so HitWhatMOID and HitWhatTerrMaterial read an uninitialized value when comparing a per-MO collision frame against it. --- Source/Managers/MovableMan.cpp | 2 ++ 1 file changed, 2 insertions(+) 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() { From d5b04614800a6756da8088fc4f261fb8b6abdf1e Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 3 Jun 2026 17:25:38 -0700 Subject: [PATCH 09/19] Disconnect destroyed SoundContainers from their playing channels A SoundContainer's FMOD channels keep its address in userData, so the positional and channel-ended passes read freed memory once the container is destroyed. Null the back-reference on destroy and skip channels whose container is gone. --- Source/Entities/SoundContainer.cpp | 1 + Source/Managers/AudioMan.cpp | 23 ++++++++++++++++++++--- Source/Managers/AudioMan.h | 4 ++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Source/Entities/SoundContainer.cpp b/Source/Entities/SoundContainer.cpp index c085df5ca1..03ff79b175 100644 --- a/Source/Entities/SoundContainer.cpp +++ b/Source/Entities/SoundContainer.cpp @@ -27,6 +27,7 @@ SoundContainer::SoundContainer(const SoundContainer& reference) { } SoundContainer::~SoundContainer() { + g_AudioMan.DisownSoundContainerPlayingChannels(this); Destroy(true); } diff --git a/Source/Managers/AudioMan.cpp b/Source/Managers/AudioMan.cpp index 4839b0af5f..b3679d8620 100644 --- a/Source/Managers/AudioMan.cpp +++ b/Source/Managers/AudioMan.cpp @@ -603,6 +603,19 @@ bool AudioMan::StopSoundContainerPlayingChannels(SoundContainer* soundContainer, return result == FMOD_OK; } +void AudioMan::DisownSoundContainerPlayingChannels(const SoundContainer* soundContainer) { + if (!m_AudioEnabled || !soundContainer || !soundContainer->IsBeingPlayed()) { + return; + } + // Leave the channels playing, but null their back-reference so the positional/ended passes don't read the freed container. + FMOD::Channel* soundChannel; + for (int channelIndex: *soundContainer->GetPlayingChannels()) { + if (m_AudioSystem->getChannel(channelIndex, &soundChannel) == FMOD_OK) { + soundChannel->setUserData(nullptr); + } + } +} + void AudioMan::FadeOutSoundContainerPlayingChannels(SoundContainer* soundContainer, int fadeOutTime) { if (!m_AudioEnabled || !soundContainer || !soundContainer->IsBeingPlayed()) { return; @@ -673,7 +686,7 @@ void AudioMan::Update3DEffectsForSFXChannels() { float doubleMinimumDistanceForPanning = m_MinimumDistanceForPanning * 2.0F; void* userData; result = result == FMOD_OK ? soundChannel->getUserData(&userData) : result; - if (result == FMOD_OK) { + if (result == FMOD_OK && userData != nullptr) { const SoundContainer* soundContainer = static_cast(userData); if (sqrDistanceToPlayer < (m_MinimumDistanceForPanning * m_MinimumDistanceForPanning) || soundContainer->GetCustomPanValue() != 0.0f) { result = soundChannel->set3DLevel(0); @@ -702,6 +715,9 @@ FMOD_RESULT AudioMan::UpdatePositionalEffectsForSoundChannel(FMOD::Channel* soun } const SoundContainer* channelSoundContainer = static_cast(userData); + if (channelSoundContainer == nullptr) { + return FMOD_OK; // The owning SoundContainer was destroyed; leave the channel playing where it is. + } bool sceneWraps = g_SceneMan.SceneWrapsX(); @@ -802,7 +818,7 @@ FMOD_RESULT F_CALLBACK AudioMan::SoundChannelEndedCallback(FMOD_CHANNELCONTROL* result = (result == FMOD_OK) ? channel->getUserData(&userData) : result; if (result == FMOD_OK) { SoundContainer* channelSoundContainer = static_cast(userData); - if (channelSoundContainer->IsBeingPlayed()) { + if (channelSoundContainer != nullptr && channelSoundContainer->IsBeingPlayed()) { channelSoundContainer->RemovePlayingChannel(channelIndex); } result = (result == FMOD_OK) ? channel->setUserData(nullptr) : result; @@ -812,7 +828,8 @@ FMOD_RESULT F_CALLBACK AudioMan::SoundChannelEndedCallback(FMOD_CHANNELCONTROL* } if (result != FMOD_OK) { - g_ConsoleMan.PrintString("ERROR: An error occurred when Ending a sound in SoundContainer " + channelSoundContainer->GetPresetName() + ": " + std::string(FMOD_ErrorString(result))); + const std::string containerName = channelSoundContainer != nullptr ? channelSoundContainer->GetPresetName() : ""; + g_ConsoleMan.PrintString("ERROR: An error occurred when Ending a sound in SoundContainer " + containerName + ": " + std::string(FMOD_ErrorString(result))); return result; } } else { diff --git a/Source/Managers/AudioMan.h b/Source/Managers/AudioMan.h index f95ee170a0..5bd65b3e4c 100644 --- a/Source/Managers/AudioMan.h +++ b/Source/Managers/AudioMan.h @@ -396,6 +396,10 @@ namespace RTE { /// @return bool StopSoundContainerPlayingChannels(SoundContainer* soundContainer, int player); + /// Clears the back-reference from a destroyed SoundContainer's still-playing channels so they don't dangle. + /// @param soundContainer A pointer to the SoundContainer being destroyed. Ownership is NOT transferred! + void DisownSoundContainerPlayingChannels(const SoundContainer* soundContainer); + /// Fades out playback a SoundContainer. /// @param soundContainer A pointer to a SoundContainer object. Ownership is NOT transferred! /// @param fadeOutTime The amount of time, in ms, to fade out over. From 0d1be2488de7a9eeda526d81b1a5dec6e26c86c8 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Wed, 3 Jun 2026 17:25:38 -0700 Subject: [PATCH 10/19] Avoid a 0/0 step ratio for a zero-step AtomGroup segment When every atom rasterizes to zero steps, stepsOnSeg is 0 and the step ratio divided 0/0. Use 0 in that case. --- Source/Entities/AtomGroup.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Entities/AtomGroup.cpp b/Source/Entities/AtomGroup.cpp index dd25bd5b40..30ff2a91ff 100644 --- a/Source/Entities/AtomGroup.cpp +++ b/Source/Entities/AtomGroup.cpp @@ -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 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// From 4e792c4a7dbcf49014228ebd84eebe7ec2136174 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Fri, 5 Jun 2026 13:11:06 -0700 Subject: [PATCH 11/19] Guard Tracy broadcast activeTime against a pre-epoch timestamp broadcastMsg.activeTime goes negative when the broadcast timestamp predates m_epoch, tripping Tracy's own assert(activeTime >= 0) on startup. Clamp to 0. --- external/sources/tracy/public/client/TracyProfiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/sources/tracy/public/client/TracyProfiler.cpp b/external/sources/tracy/public/client/TracyProfiler.cpp index 22830765e5..056f531155 100644 --- a/external/sources/tracy/public/client/TracyProfiler.cpp +++ b/external/sources/tracy/public/client/TracyProfiler.cpp @@ -1891,7 +1891,7 @@ void Profiler::Worker() lastBroadcast = t; const auto ts = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); - broadcastMsg.activeTime = int32_t( ts - m_epoch ); + broadcastMsg.activeTime = ts > m_epoch ? int32_t( ts - m_epoch ) : 0; assert( broadcastMsg.activeTime >= 0 ); m_broadcast->Send( broadcastPort, &broadcastMsg, broadcastLen ); } From 19bb321b092a3800fa3b3c3e764bbd7012f6fb43 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Fri, 5 Jun 2026 13:11:06 -0700 Subject: [PATCH 12/19] meson: enable libc++ removed-API macros for the macOS build Vendored luabind 0.7.1 and boost 1.75 still use the C++17-removed APIs (auto_ptr, unary/binary_function, binders) that libc++ 19+ hides behind these macros, so the macOS build fails to compile without them. --- meson.build | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/meson.build b/meson.build index 8625ffa1aa..10d5b6a10c 100644 --- a/meson.build +++ b/meson.build @@ -49,6 +49,18 @@ if compiler.get_argument_syntax()== 'gcc' # used for gcc compatible compilers 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. + # Apple libc++ dropped the C++17-removed APIs that vendored luabind 0.7.1 + boost 1.75 still use; re-enable them. + add_global_arguments( + '-D_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR', + '-D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION', + '-D_LIBCPP_ENABLE_CXX17_REMOVED_BINDERS', + '-D_LIBCPP_ENABLE_CXX17_REMOVED_RANDOM_SHUFFLE', + '-D_LIBCPP_ENABLE_CXX17_REMOVED_UNEXPECTED_FUNCTIONS', + '-D_LIBCPP_ENABLE_CXX20_REMOVED_BINDER_TYPEDEFS', + '-D_LIBCPP_ENABLE_CXX20_REMOVED_NEGATORS', + '-D_LIBCPP_ENABLE_EXPERIMENTAL', + '-D_LIBCPP_PSTL_BACKEND_SERIAL', + language: 'cpp') endif #suffix = 'x86_64' From 98ad52e2620aecdc89f514b292ee093f30ecfceb Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Sat, 20 Jun 2026 14:28:08 -0700 Subject: [PATCH 13/19] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625e36fbde..4412c487ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,6 +220,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Updated SDL2 to SDL3 +- Swapped LuaJIT to the [WohlSoft fork](https://github.com/WohlSoft/LuaJIT). Vendored from [dfcb8651](https://github.com/WohlSoft/LuaJIT/commit/dfcb8651). + +- LuaJIT is now built from source on Windows as part of the solution, instead of shipping prebuilt libraries. +
Fixed @@ -264,6 +268,14 @@ 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 startup hang on Linux and macOS where a message box could deadlock, along with a `file_time_type` build error on those platforms. + +- Fixed an issue where destroying a `SoundContainer` while it was still playing could leave dangling sound channels, which could cause audio glitches or a crash. + +- 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 From 8f2fc3c5bfa3cf30dd3a3832b66b2f09344b17c0 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:17 -0700 Subject: [PATCH 14/19] meson: gate the libc++ macros on clang Drop the global removed-API block (luabind covers those) and keep only the two that expose , behind a clang gate so they reach Apple's clang/libc++ but stay off the GCC command line. --- meson.build | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/meson.build b/meson.build index 10d5b6a10c..4f33be11e5 100644 --- a/meson.build +++ b/meson.build @@ -49,18 +49,11 @@ if compiler.get_argument_syntax()== 'gcc' # used for gcc compatible compilers 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. - # Apple libc++ dropped the C++17-removed APIs that vendored luabind 0.7.1 + boost 1.75 still use; re-enable them. - add_global_arguments( - '-D_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR', - '-D_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION', - '-D_LIBCPP_ENABLE_CXX17_REMOVED_BINDERS', - '-D_LIBCPP_ENABLE_CXX17_REMOVED_RANDOM_SHUFFLE', - '-D_LIBCPP_ENABLE_CXX17_REMOVED_UNEXPECTED_FUNCTIONS', - '-D_LIBCPP_ENABLE_CXX20_REMOVED_BINDER_TYPEDEFS', - '-D_LIBCPP_ENABLE_CXX20_REMOVED_NEGATORS', - '-D_LIBCPP_ENABLE_EXPERIMENTAL', - '-D_LIBCPP_PSTL_BACKEND_SERIAL', - language: 'cpp') + 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' From 078853b8f7ec346e56d09f9aef74795fb84e2571 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:17 -0700 Subject: [PATCH 15/19] Broaden the save-menu libc++ guard to all libc++ The wider file_clock rep is a libc++ trait, not Apple-specific, so a libc++ build on any OS takes the duration_cast path. --- Source/Menus/SaveLoadMenuGUI.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Menus/SaveLoadMenuGUI.cpp b/Source/Menus/SaveLoadMenuGUI.cpp index df55514f2d..46b9b3b275 100644 --- a/Source/Menus/SaveLoadMenuGUI.cpp +++ b/Source/Menus/SaveLoadMenuGUI.cpp @@ -180,8 +180,8 @@ void SaveLoadMenuGUI::UpdateSaveGamesGUIList() { const auto saveTime = std::chrono::system_clock::to_time_t(saveFsTime); #else // TODO - kill this monstrosity when we move to GCC13 - // macOS libc++ file_clock rep is __int128; duration_cast lands it in system_clock's range first. -#if defined(__APPLE__) && defined(_LIBCPP_VERSION) + // 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 From de0e71a43f5108a988fd3ce96547c140f0244191 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:17 -0700 Subject: [PATCH 16/19] Remove the Tracy broadcast guard Vendored-profiler change, unrelated to the cross-platform and FP work in this PR. --- external/sources/tracy/public/client/TracyProfiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/sources/tracy/public/client/TracyProfiler.cpp b/external/sources/tracy/public/client/TracyProfiler.cpp index 056f531155..22830765e5 100644 --- a/external/sources/tracy/public/client/TracyProfiler.cpp +++ b/external/sources/tracy/public/client/TracyProfiler.cpp @@ -1891,7 +1891,7 @@ void Profiler::Worker() lastBroadcast = t; const auto ts = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); - broadcastMsg.activeTime = ts > m_epoch ? int32_t( ts - m_epoch ) : 0; + broadcastMsg.activeTime = int32_t( ts - m_epoch ); assert( broadcastMsg.activeTime >= 0 ); m_broadcast->Send( broadcastPort, &broadcastMsg, broadcastLen ); } From 841f0cc844ad9c9dabc1cf6810262e72f4bf0d8a Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:17 -0700 Subject: [PATCH 17/19] Remove the SoundContainer channel-disconnect The disown does not close the async-GC teardown race, and it is off-topic for the cross-platform and FP work here. --- Source/Entities/SoundContainer.cpp | 1 - Source/Managers/AudioMan.cpp | 23 +++-------------------- Source/Managers/AudioMan.h | 4 ---- 3 files changed, 3 insertions(+), 25 deletions(-) diff --git a/Source/Entities/SoundContainer.cpp b/Source/Entities/SoundContainer.cpp index 03ff79b175..c085df5ca1 100644 --- a/Source/Entities/SoundContainer.cpp +++ b/Source/Entities/SoundContainer.cpp @@ -27,7 +27,6 @@ SoundContainer::SoundContainer(const SoundContainer& reference) { } SoundContainer::~SoundContainer() { - g_AudioMan.DisownSoundContainerPlayingChannels(this); Destroy(true); } diff --git a/Source/Managers/AudioMan.cpp b/Source/Managers/AudioMan.cpp index b3679d8620..4839b0af5f 100644 --- a/Source/Managers/AudioMan.cpp +++ b/Source/Managers/AudioMan.cpp @@ -603,19 +603,6 @@ bool AudioMan::StopSoundContainerPlayingChannels(SoundContainer* soundContainer, return result == FMOD_OK; } -void AudioMan::DisownSoundContainerPlayingChannels(const SoundContainer* soundContainer) { - if (!m_AudioEnabled || !soundContainer || !soundContainer->IsBeingPlayed()) { - return; - } - // Leave the channels playing, but null their back-reference so the positional/ended passes don't read the freed container. - FMOD::Channel* soundChannel; - for (int channelIndex: *soundContainer->GetPlayingChannels()) { - if (m_AudioSystem->getChannel(channelIndex, &soundChannel) == FMOD_OK) { - soundChannel->setUserData(nullptr); - } - } -} - void AudioMan::FadeOutSoundContainerPlayingChannels(SoundContainer* soundContainer, int fadeOutTime) { if (!m_AudioEnabled || !soundContainer || !soundContainer->IsBeingPlayed()) { return; @@ -686,7 +673,7 @@ void AudioMan::Update3DEffectsForSFXChannels() { float doubleMinimumDistanceForPanning = m_MinimumDistanceForPanning * 2.0F; void* userData; result = result == FMOD_OK ? soundChannel->getUserData(&userData) : result; - if (result == FMOD_OK && userData != nullptr) { + if (result == FMOD_OK) { const SoundContainer* soundContainer = static_cast(userData); if (sqrDistanceToPlayer < (m_MinimumDistanceForPanning * m_MinimumDistanceForPanning) || soundContainer->GetCustomPanValue() != 0.0f) { result = soundChannel->set3DLevel(0); @@ -715,9 +702,6 @@ FMOD_RESULT AudioMan::UpdatePositionalEffectsForSoundChannel(FMOD::Channel* soun } const SoundContainer* channelSoundContainer = static_cast(userData); - if (channelSoundContainer == nullptr) { - return FMOD_OK; // The owning SoundContainer was destroyed; leave the channel playing where it is. - } bool sceneWraps = g_SceneMan.SceneWrapsX(); @@ -818,7 +802,7 @@ FMOD_RESULT F_CALLBACK AudioMan::SoundChannelEndedCallback(FMOD_CHANNELCONTROL* result = (result == FMOD_OK) ? channel->getUserData(&userData) : result; if (result == FMOD_OK) { SoundContainer* channelSoundContainer = static_cast(userData); - if (channelSoundContainer != nullptr && channelSoundContainer->IsBeingPlayed()) { + if (channelSoundContainer->IsBeingPlayed()) { channelSoundContainer->RemovePlayingChannel(channelIndex); } result = (result == FMOD_OK) ? channel->setUserData(nullptr) : result; @@ -828,8 +812,7 @@ FMOD_RESULT F_CALLBACK AudioMan::SoundChannelEndedCallback(FMOD_CHANNELCONTROL* } if (result != FMOD_OK) { - const std::string containerName = channelSoundContainer != nullptr ? channelSoundContainer->GetPresetName() : ""; - g_ConsoleMan.PrintString("ERROR: An error occurred when Ending a sound in SoundContainer " + containerName + ": " + std::string(FMOD_ErrorString(result))); + g_ConsoleMan.PrintString("ERROR: An error occurred when Ending a sound in SoundContainer " + channelSoundContainer->GetPresetName() + ": " + std::string(FMOD_ErrorString(result))); return result; } } else { diff --git a/Source/Managers/AudioMan.h b/Source/Managers/AudioMan.h index 5bd65b3e4c..f95ee170a0 100644 --- a/Source/Managers/AudioMan.h +++ b/Source/Managers/AudioMan.h @@ -396,10 +396,6 @@ namespace RTE { /// @return bool StopSoundContainerPlayingChannels(SoundContainer* soundContainer, int player); - /// Clears the back-reference from a destroyed SoundContainer's still-playing channels so they don't dangle. - /// @param soundContainer A pointer to the SoundContainer being destroyed. Ownership is NOT transferred! - void DisownSoundContainerPlayingChannels(const SoundContainer* soundContainer); - /// Fades out playback a SoundContainer. /// @param soundContainer A pointer to a SoundContainer object. Ownership is NOT transferred! /// @param fadeOutTime The amount of time, in ms, to fade out over. From 81b4a40d84755b7da37916f3f109fe6cdcf73eb6 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:18 -0700 Subject: [PATCH 18/19] Tidy the changelog Drop two stale LuaJIT lines that rode in from a rebase, drop the SoundContainer entry, and narrow the message-box note to macOS (the worker-thread guard is macOS-only). --- CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4412c487ab..caedf40c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,10 +220,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Updated SDL2 to SDL3 -- Swapped LuaJIT to the [WohlSoft fork](https://github.com/WohlSoft/LuaJIT). Vendored from [dfcb8651](https://github.com/WohlSoft/LuaJIT/commit/dfcb8651). - -- LuaJIT is now built from source on Windows as part of the solution, instead of shipping prebuilt libraries. -
Fixed @@ -268,9 +264,7 @@ 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 startup hang on Linux and macOS where a message box could deadlock, along with a `file_time_type` build error on those platforms. - -- Fixed an issue where destroying a `SoundContainer` while it was still playing could leave dangling sound channels, which could cause audio glitches or a crash. +- 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. From c26a70e5d22cd1b4454445658ce97ad15665b4e6 Mon Sep 17 00:00:00 2001 From: Erol Germain-Gomuc Date: Thu, 25 Jun 2026 10:16:18 -0700 Subject: [PATCH 19/19] luabind: re-enable the libc++ removed APIs via individual macros The CXX17-removed umbrella is a no-op on libc++ 19+, so set the two individual macros luabind 0.7.1 and boost 1.75 use: std::auto_ptr and std::unary_function. --- external/sources/luabind-0.7.1/meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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')]