From aa9aed271c23f96847da041f56f4f22e3bb3d5b3 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 14 Jul 2026 23:17:32 -0300 Subject: [PATCH 01/20] Fix cross-platform math determinism issues (Mac/Linux/Windows) --- Core/Libraries/Source/WWVegas/WWMath/wwmath.h | 6 ++++++ .../Object/Behavior/SlowDeathBehavior.cpp | 7 ++++++- .../GameLogic/Object/PartitionManager.cpp | 19 ++++++++----------- .../DockUpdate/SupplyWarehouseDockUpdate.cpp | 9 ++++++++- .../Update/DynamicGeometryInfoUpdate.cpp | 6 +++++- .../GameLogic/Object/Update/OCLUpdate.cpp | 7 ++++++- .../Object/Behavior/SlowDeathBehavior.cpp | 7 ++++++- .../GameLogic/Object/PartitionManager.cpp | 19 ++++++++----------- .../DockUpdate/SupplyWarehouseDockUpdate.cpp | 9 ++++++++- .../Update/DynamicGeometryInfoUpdate.cpp | 6 +++++- .../GameLogic/Object/Update/OCLUpdate.cpp | 7 ++++++- 11 files changed, 72 insertions(+), 30 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h index 5a8b7a4f465..0bc7caa1102 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h +++ b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h @@ -218,14 +218,20 @@ static WWINLINE float ASinTrig(float x) // Each wrapper preserves the exact type (float vs double) of the vanilla CRT call. #if USE_DETERMINISTIC_MATH static WWINLINE double SqrtOrigin(double x) { return gm_sqrt(x); } + static WWINLINE float SqrtOrigin(float x) { return gm_sqrtf(x); } static WWINLINE float SqrtfOrigin(float x) { return gm_sqrtf(x); } static WWINLINE double Atan2Origin(double y, double x) { return gm_atan2(y, x); } + static WWINLINE float Atan2Origin(float y, float x) { return gm_atan2f(y, x); } static WWINLINE double PowOrigin(double x, double y) { return gm_pow(x, y); } + static WWINLINE float PowOrigin(float x, float y) { return gm_powf(x, y); } #else static WWINLINE double SqrtOrigin(double x) { return sqrt(x); } + static WWINLINE float SqrtOrigin(float x) { return sqrtf(x); } static WWINLINE float SqrtfOrigin(float x) { return sqrtf(x); } static WWINLINE double Atan2Origin(double y, double x) { return atan2(y, x); } + static WWINLINE float Atan2Origin(float y, float x) { return atan2f(y, x); } static WWINLINE double PowOrigin(double x, double y) { return pow(x, y); } + static WWINLINE float PowOrigin(float x, float y) { return powf(x, y); } #endif static WWINLINE float Sign(float val); #if USE_DETERMINISTIC_MATH diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp index b770e8e9506..7ccbfd9f58e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp @@ -179,7 +179,12 @@ Int SlowDeathBehavior::getProbabilityModifier( const DamageInfo *damageInfo ) co // severly killed, and more sedate ones when only slightly killed. // eg ( 200 hp max, had 10 left, took 50 damage, 40 overkill, (40/200) * 100 = 20 overkill %) Int overkillDamage = damageInfo->out.m_actualDamageDealt - damageInfo->out.m_actualDamageClipped; - Real overkillPercent = (float)overkillDamage / (float)getObject()->getBodyModule()->getMaxHealth(); + Real maxHealth = (Real)getObject()->getBodyModule()->getMaxHealth(); + Real overkillPercent = 0.0f; + if (maxHealth > 0.0f) + { + overkillPercent = (Real)overkillDamage / maxHealth; + } Int overkillModifier = overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent; return max( getSlowDeathBehaviorModuleData()->m_probabilityModifier + overkillModifier, 1 ); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index 3d6025d340a..642dfcd01a3 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -3195,22 +3195,20 @@ Int PartitionManager::calcMinRadius(const ICoord2D& cur) so it really shouldn't matter... (I hope) */ - double minDistSqr = 1e12; // double, not real + Real minDistSqr = 1e12f; for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { - // double, not real - double dx = centerPos[i].x - otherPos[j].x; - double dy = centerPos[i].y - otherPos[j].y; - double curDistSqr = dx*dx + dy*dy; + Real dx = centerPos[i].x - otherPos[j].x; + Real dy = centerPos[i].y - otherPos[j].y; + Real curDistSqr = dx*dx + dy*dy; if (minDistSqr > curDistSqr) minDistSqr = curDistSqr; } } - // double, not real - double dist = WWMath::SqrtfOrigin(minDistSqr); + Real dist = WWMath::SqrtfOrigin(minDistSqr); Int minRadius = REAL_TO_INT_CEIL( dist / m_cellSize ); return minRadius; @@ -3225,10 +3223,9 @@ void PartitionManager::calcRadiusVec() Int cx = getCellCountX(); Int cy = getCellCountY(); - // double, not real - double dx = (double)cx * (double)cellSize; - double dy = (double)cy * (double)cellSize; - double maxPossibleDist = WWMath::SqrtOrigin(dx*dx + dy*dy); + Real dx = (Real)cx * (Real)cellSize; + Real dy = (Real)cy * (Real)cellSize; + Real maxPossibleDist = WWMath::SqrtfOrigin(dx*dx + dy*dy); m_maxGcoRadius = REAL_TO_INT_CEIL(maxPossibleDist / cellSize); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp index 50bc0ca9d2a..107c81bda77 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp @@ -170,7 +170,14 @@ void SupplyWarehouseDockUpdate::setDockCrippled( Bool setting ) void SupplyWarehouseDockUpdate::setCashValue( Int cashValue ) { // A script can tell us our set value, and we need to figure out the boxes needed to provide that. - m_boxesStored = WWMath::Ceil(cashValue / (float)TheGlobalData->m_baseValuePerSupplyBox); + if (TheGlobalData->m_baseValuePerSupplyBox > 0) + { + m_boxesStored = WWMath::Ceil(cashValue / (float)TheGlobalData->m_baseValuePerSupplyBox); + } + else + { + m_boxesStored = 0; + } Drawable *draw = getObject()->getDrawable(); if( draw ) { diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp index 754c4dde5dc..ff6b7f3e17e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp @@ -136,7 +136,11 @@ UpdateSleepTime DynamicGeometryInfoUpdate::update() Object *me = getObject(); Real newHeight, newMajor, newMinor; - Real ratio = (float)m_timeActive / (float)data->m_transitionTime; + Real ratio = 1.0f; + if (data->m_transitionTime > 0) + { + ratio = (float)m_timeActive / (float)data->m_transitionTime; + } newHeight = m_initialHeight + (ratio * (m_finalHeight - m_initialHeight)); newMajor = m_initialMajorRadius + (ratio * (m_finalMajorRadius - m_initialMajorRadius)); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp index 911ed52525c..f8b30cc805b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp @@ -146,7 +146,12 @@ Real OCLUpdate::getCountdownPercent() const { UnsignedInt now = TheGameLogic->getFrame(); - return 1.0f - (( m_nextCreationFrame - now ) / (float)( m_nextCreationFrame - m_timerStartedFrame )); + UnsignedInt totalTime = m_nextCreationFrame - m_timerStartedFrame; + if (totalTime > 0) + { + return 1.0f - (( m_nextCreationFrame - now ) / (float)totalTime); + } + return 1.0f; } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp index 92dcfe36bbd..ca70740b8cd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp @@ -179,7 +179,12 @@ Int SlowDeathBehavior::getProbabilityModifier( const DamageInfo *damageInfo ) co // severely killed, and more sedate ones when only slightly killed. // eg ( 200 hp max, had 10 left, took 50 damage, 40 overkill, (40/200) * 100 = 20 overkill %) Int overkillDamage = damageInfo->out.m_actualDamageDealt - damageInfo->out.m_actualDamageClipped; - Real overkillPercent = (float)overkillDamage / (float)getObject()->getBodyModule()->getMaxHealth(); + Real maxHealth = (Real)getObject()->getBodyModule()->getMaxHealth(); + Real overkillPercent = 0.0f; + if (maxHealth > 0.0f) + { + overkillPercent = (Real)overkillDamage / maxHealth; + } Int overkillModifier = overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent; return max( getSlowDeathBehaviorModuleData()->m_probabilityModifier + overkillModifier, 1 ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp index 14692873f55..9e0bccf2319 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/PartitionManager.cpp @@ -3202,22 +3202,20 @@ Int PartitionManager::calcMinRadius(const ICoord2D& cur) so it really shouldn't matter... (I hope) */ - double minDistSqr = 1e12; // double, not real + Real minDistSqr = 1e12f; for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { - // double, not real - double dx = centerPos[i].x - otherPos[j].x; - double dy = centerPos[i].y - otherPos[j].y; - double curDistSqr = dx*dx + dy*dy; + Real dx = centerPos[i].x - otherPos[j].x; + Real dy = centerPos[i].y - otherPos[j].y; + Real curDistSqr = dx*dx + dy*dy; if (minDistSqr > curDistSqr) minDistSqr = curDistSqr; } } - // double, not real - double dist = WWMath::SqrtfOrigin(minDistSqr); + Real dist = WWMath::SqrtfOrigin(minDistSqr); Int minRadius = REAL_TO_INT_CEIL( dist / m_cellSize ); return minRadius; @@ -3232,10 +3230,9 @@ void PartitionManager::calcRadiusVec() Int cx = getCellCountX(); Int cy = getCellCountY(); - // double, not real - double dx = (double)cx * (double)cellSize; - double dy = (double)cy * (double)cellSize; - double maxPossibleDist = WWMath::SqrtOrigin(dx*dx + dy*dy); + Real dx = (Real)cx * (Real)cellSize; + Real dy = (Real)cy * (Real)cellSize; + Real maxPossibleDist = WWMath::SqrtfOrigin(dx*dx + dy*dy); m_maxGcoRadius = REAL_TO_INT_CEIL(maxPossibleDist / cellSize); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp index 71a8abefcad..3ee93dd4bf0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DockUpdate/SupplyWarehouseDockUpdate.cpp @@ -170,7 +170,14 @@ void SupplyWarehouseDockUpdate::setDockCrippled( Bool setting ) void SupplyWarehouseDockUpdate::setCashValue( Int cashValue ) { // A script can tell us our set value, and we need to figure out the boxes needed to provide that. - m_boxesStored = WWMath::Ceil(cashValue / (float)TheGlobalData->m_baseValuePerSupplyBox); + if (TheGlobalData->m_baseValuePerSupplyBox > 0) + { + m_boxesStored = WWMath::Ceil(cashValue / (float)TheGlobalData->m_baseValuePerSupplyBox); + } + else + { + m_boxesStored = 0; + } Drawable *draw = getObject()->getDrawable(); if( draw ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp index 7d3db97313e..890ae938d82 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DynamicGeometryInfoUpdate.cpp @@ -136,7 +136,11 @@ UpdateSleepTime DynamicGeometryInfoUpdate::update() Object *me = getObject(); Real newHeight, newMajor, newMinor; - Real ratio = (float)m_timeActive / (float)data->m_transitionTime; + Real ratio = 1.0f; + if (data->m_transitionTime > 0) + { + ratio = (float)m_timeActive / (float)data->m_transitionTime; + } newHeight = m_initialHeight + (ratio * (m_finalHeight - m_initialHeight)); newMajor = m_initialMajorRadius + (ratio * (m_finalMajorRadius - m_initialMajorRadius)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp index 8ac51ecbc3d..9856dc66a80 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp @@ -262,7 +262,12 @@ Real OCLUpdate::getCountdownPercent() const { UnsignedInt now = TheGameLogic->getFrame(); - return 1.0f - (( m_nextCreationFrame - now ) / (float)( m_nextCreationFrame - m_timerStartedFrame )); + UnsignedInt totalTime = m_nextCreationFrame - m_timerStartedFrame; + if (totalTime > 0) + { + return 1.0f - (( m_nextCreationFrame - now ) / (float)totalTime); + } + return 1.0f; } // ------------------------------------------------------------------------------------------------ From 4bc1fb923b7874e24475cf8692e3b1dcceeaec3d Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 14 Jul 2026 23:32:27 -0300 Subject: [PATCH 02/20] Fix SqrtOrigin ambiguity for integer arguments --- .../MessageStream/PlaceEventTranslator.cpp | 2 +- Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp b/Core/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp index 359f75b62f0..789a8f2b73b 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp @@ -339,7 +339,7 @@ GameMessageDisposition PlaceEventTranslator::translateGameMessage(const GameMess Int x, y; x = mouse.x - start.x; y = mouse.y - start.y; - if( WWMath::SqrtOrigin( (x * x) + (y * y) ) >= PLACEMENT_DRAG_THRESHOLD_DIST ) + if( WWMath::SqrtOrigin( (float)((x * x) + (y * y)) ) >= PLACEMENT_DRAG_THRESHOLD_DIST ) { TheInGameUI->setPlacementEnd(&mouse); diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index d904c09843c..548985cfea4 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -2071,7 +2071,7 @@ UnsignedInt PathfindCell::costToGoal( PathfindCell *goal ) Int dy = m_info->m_pos.y - goal->getYIndex(); #define NO_REAL_DIST #ifdef REAL_DIST - Int cost = COST_ORTHOGONAL*WWMath::SqrtOrigin(dx*dx + dy*dy); + Int cost = COST_ORTHOGONAL*WWMath::SqrtOrigin((float)(dx*dx + dy*dy)); #else if (dx<0) dx = -dx; if (dy<0) dy = -dy; @@ -2097,7 +2097,7 @@ UnsignedInt PathfindCell::costToHierGoal( PathfindCell *goal ) } Int dx = m_info->m_pos.x - goal->getXIndex(); Int dy = m_info->m_pos.y - goal->getYIndex(); - Int cost = REAL_TO_INT_FLOOR(COST_ORTHOGONAL*WWMath::SqrtOrigin(dx*dx + dy*dy) + 0.5f); + Int cost = REAL_TO_INT_FLOOR(COST_ORTHOGONAL*WWMath::SqrtOrigin((float)(dx*dx + dy*dy)) + 0.5f); return cost; } @@ -6445,7 +6445,7 @@ Int Pathfinder::examineNeighboringCells(PathfindCell *parentCell, PathfindCell * } else { dx = newCellCoord.x - goalCell->getXIndex(); dy = newCellCoord.y - goalCell->getYIndex(); - costRemaining = COST_ORTHOGONAL*WWMath::SqrtOrigin(dx*dx + dy*dy); + costRemaining = COST_ORTHOGONAL*WWMath::SqrtOrigin((float)(dx*dx + dy*dy)); costRemaining -= attackDistance/2; if (costRemaining<0) costRemaining=0; @@ -6769,7 +6769,7 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe dx = from->x - to->x; dy = from->y - to->y; - Int count = WWMath::SqrtOrigin(dx*dx+dy*dy)/(PATHFIND_CELL_SIZE_F/2); + Int count = WWMath::SqrtOrigin((float)(dx*dx+dy*dy))/(PATHFIND_CELL_SIZE_F/2); if (count<2) count = 2; Int i; color.green = 0; @@ -7460,7 +7460,7 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, dx = from->x - to->x; dy = from->y - to->y; - Int count = WWMath::SqrtOrigin(dx*dx+dy*dy)/(PATHFIND_CELL_SIZE_F/2); + Int count = WWMath::SqrtOrigin((float)(dx*dx+dy*dy))/(PATHFIND_CELL_SIZE_F/2); if (count<2) count = 2; Int i; color.green = 0; @@ -8164,7 +8164,7 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu dx = from->x - to->x; dy = from->y - to->y; - Int count = WWMath::SqrtOrigin(dx*dx+dy*dy)/(PATHFIND_CELL_SIZE_F/2); + Int count = WWMath::SqrtOrigin((float)(dx*dx+dy*dy))/(PATHFIND_CELL_SIZE_F/2); if (count<2) count = 2; Int i; color.green = 0; From 23f9aa6636e65db2e0756b7f84f0d47398b17cc0 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 15 Jul 2026 19:22:15 -0300 Subject: [PATCH 03/20] GeneralsX @bugfix felipebraz 15/07/2026 Apply Okladnoj's PR #3 and #4 determinism fixes - Replaced division by zero traps in float logic with Div_FixNaN - Included Div_FixNaN helper in wwmath.h - Added deterministic BezierMath (D3DXVec4Transform & D3DXVec4Dot) - Downcasted Atan2 double arguments to float - Backported PR #3 and #4 from Zero Hour to the base Generals game Ref: https://github.com/TheSuperHackers/GeneralsGameCode/pull/3 Ref: https://github.com/TheSuperHackers/GeneralsGameCode/pull/4 --- .../Source/GameLogic/AI/AIPathfind.cpp | 2 +- Core/Libraries/Source/WWVegas/WWMath/wwmath.h | 23 ++++++++++++++- .../GameEngine/Include/Common/BezierSegment.h | 28 +++++++++++++++++++ .../Source/Common/Bezier/BezFwdIterator.cpp | 6 ++-- .../Source/Common/Bezier/BezierSegment.cpp | 8 +++--- .../GameEngine/Source/GameLogic/AI/AI.cpp | 2 +- .../Source/GameLogic/AI/AISkirmishPlayer.cpp | 2 +- .../Object/Behavior/BridgeBehavior.cpp | 2 +- .../Behavior/DumbProjectileBehavior.cpp | 2 +- .../Behavior/GenerateMinefieldBehavior.cpp | 4 +-- .../Object/Behavior/MinefieldBehavior.cpp | 2 +- .../Object/Behavior/SlowDeathBehavior.cpp | 8 ++---- .../GameLogic/Object/Body/ActiveBody.cpp | 2 +- .../AIUpdate/DeliverPayloadAIUpdate.cpp | 5 ++-- .../Object/Update/AIUpdate/DozerAIUpdate.cpp | 2 +- .../Object/Update/CommandButtonHuntUpdate.cpp | 2 +- .../GameLogic/Object/Update/SlavedUpdate.cpp | 4 +-- .../GameLogic/Object/Update/ToppleUpdate.cpp | 2 +- .../Source/GameLogic/Object/Weapon.cpp | 2 +- .../GameEngine/Include/Common/BezierSegment.h | 28 +++++++++++++++++++ .../Source/Common/Bezier/BezFwdIterator.cpp | 6 ++-- .../Source/Common/Bezier/BezierSegment.cpp | 8 +++--- .../GameEngine/Source/GameLogic/AI/AI.cpp | 2 +- .../Source/GameLogic/AI/AISkirmishPlayer.cpp | 2 +- .../Object/Behavior/BridgeBehavior.cpp | 2 +- .../Behavior/DumbProjectileBehavior.cpp | 2 +- .../Behavior/GenerateMinefieldBehavior.cpp | 4 +-- .../Object/Behavior/MinefieldBehavior.cpp | 2 +- .../Object/Behavior/SlowDeathBehavior.cpp | 8 ++---- .../GameLogic/Object/Body/ActiveBody.cpp | 2 +- .../AIUpdate/DeliverPayloadAIUpdate.cpp | 5 ++-- .../Object/Update/AIUpdate/DozerAIUpdate.cpp | 2 +- .../Object/Update/CommandButtonHuntUpdate.cpp | 2 +- .../GameLogic/Object/Update/SlavedUpdate.cpp | 4 +-- .../GameLogic/Object/Update/ToppleUpdate.cpp | 2 +- .../Source/GameLogic/Object/Weapon.cpp | 2 +- 36 files changed, 131 insertions(+), 60 deletions(-) diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 548985cfea4..e091482039e 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -929,7 +929,7 @@ void Path::computePointOnPath( // projected position on the path. If we are very far off the path, we will move // directly towards the nearest point on the path, and not the next path node. const Real maxPathError = 3.0f * PATHFIND_CELL_SIZE_F; - const Real maxPathErrorInv = 1.0 / maxPathError; + const Real maxPathErrorInv = 1.0f / maxPathError; Real k = offsetDist * maxPathErrorInv; if (k > 1.0f) k = 1.0f; diff --git a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h index 0bc7caa1102..5d2c672146b 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h +++ b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h @@ -220,7 +220,7 @@ static WWINLINE float ASinTrig(float x) static WWINLINE double SqrtOrigin(double x) { return gm_sqrt(x); } static WWINLINE float SqrtOrigin(float x) { return gm_sqrtf(x); } static WWINLINE float SqrtfOrigin(float x) { return gm_sqrtf(x); } - static WWINLINE double Atan2Origin(double y, double x) { return gm_atan2(y, x); } + static WWINLINE double Atan2Origin(double y, double x) { return (double)gm_atan2f((float)y, (float)x); } static WWINLINE float Atan2Origin(float y, float x) { return gm_atan2f(y, x); } static WWINLINE double PowOrigin(double x, double y) { return gm_pow(x, y); } static WWINLINE float PowOrigin(float x, float y) { return gm_powf(x, y); } @@ -246,6 +246,9 @@ static WWINLINE float Round(float val) { return floorf(val + 0.5f); } static WWINLINE bool Fast_Is_Float_Positive(const float & val); static WWINLINE bool Is_Power_Of_2(const unsigned int val); +static WWINLINE float Div_FixNaN(float dividend, float divisor, float fallback = 0.0f); +static WWINLINE double Div_FixNaN(double dividend, double divisor, double fallback = 0.0); + static float Random_Float(); static WWINLINE float Random_Float(float min,float max); @@ -818,3 +821,21 @@ WWINLINE float WWMath::Normalize_Angle(float angle) { return angle - (WWMATH_TWO_PI * Floor((angle + WWMATH_PI) / WWMATH_TWO_PI)); } + +WWINLINE float WWMath::Div_FixNaN(float dividend, float divisor, float fallback) +{ +#if USE_DETERMINISTIC_MATH + return (divisor == 0.0f) ? fallback : dividend / divisor; +#else + return dividend / divisor; +#endif +} + +WWINLINE double WWMath::Div_FixNaN(double dividend, double divisor, double fallback) +{ +#if USE_DETERMINISTIC_MATH + return (double) ((divisor == 0.0) ? (float) fallback : (float) dividend / (float) divisor); +#else + return dividend / divisor; +#endif +} diff --git a/Generals/Code/GameEngine/Include/Common/BezierSegment.h b/Generals/Code/GameEngine/Include/Common/BezierSegment.h index 9e9f19c7d5c..e0f234dc39e 100644 --- a/Generals/Code/GameEngine/Include/Common/BezierSegment.h +++ b/Generals/Code/GameEngine/Include/Common/BezierSegment.h @@ -40,6 +40,34 @@ #define USUAL_TOLERANCE 1.0f +#ifndef SAGE_USE_GLM +class BezierMath +{ +public: + static void D3DXVec4Transform(D3DXVECTOR4* out, const D3DXVECTOR4* v, const D3DXMATRIX* m) + { +#if USE_DETERMINISTIC_MATH + const float x = v->x, y = v->y, z = v->z, w = v->w; + out->x = ((x * m->m[0][0] + y * m->m[1][0]) + z * m->m[2][0]) + w * m->m[3][0]; + out->y = ((x * m->m[0][1] + y * m->m[1][1]) + z * m->m[2][1]) + w * m->m[3][1]; + out->z = ((x * m->m[0][2] + y * m->m[1][2]) + z * m->m[2][2]) + w * m->m[3][2]; + out->w = ((x * m->m[0][3] + y * m->m[1][3]) + z * m->m[2][3]) + w * m->m[3][3]; +#else + ::D3DXVec4Transform(out, v, m); +#endif + } + + static float D3DXVec4Dot(const D3DXVECTOR4* a, const D3DXVECTOR4* b) + { +#if USE_DETERMINISTIC_MATH + return ((a->x * b->x + a->y * b->y) + a->z * b->z) + a->w * b->w; +#else + return ::D3DXVec4Dot(a, b); +#endif + } +}; +#endif + class BezierSegment { protected: diff --git a/Generals/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp b/Generals/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp index 2613b5651f7..a11cbec178d 100644 --- a/Generals/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp +++ b/Generals/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp @@ -71,9 +71,9 @@ void BezFwdIterator::start() D3DXVECTOR4 pz(mBezSeg.m_controlPoints[0].z, mBezSeg.m_controlPoints[1].z, mBezSeg.m_controlPoints[2].z, mBezSeg.m_controlPoints[3].z); D3DXVECTOR4 cVec[3]; - D3DXVec4Transform(&cVec[0], &px, &BezierSegment::s_bezBasisMatrix); - D3DXVec4Transform(&cVec[1], &py, &BezierSegment::s_bezBasisMatrix); - D3DXVec4Transform(&cVec[2], &pz, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[0], &px, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[1], &py, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[2], &pz, &BezierSegment::s_bezBasisMatrix); #else // SAGE_USE_GLM glm::vec4 px(mBezSeg.m_controlPoints[0].x, mBezSeg.m_controlPoints[1].x, mBezSeg.m_controlPoints[2].x, mBezSeg.m_controlPoints[3].x); glm::vec4 py(mBezSeg.m_controlPoints[0].y, mBezSeg.m_controlPoints[1].y, mBezSeg.m_controlPoints[2].y, mBezSeg.m_controlPoints[3].y); diff --git a/Generals/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp b/Generals/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp index 0c414341fd2..ac328806021 100644 --- a/Generals/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp +++ b/Generals/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp @@ -116,11 +116,11 @@ void BezierSegment::evaluateBezSegmentAtT(Real tValue, Coord3D *outResult) const D3DXVECTOR4 zCoords(m_controlPoints[0].z, m_controlPoints[1].z, m_controlPoints[2].z, m_controlPoints[3].z); D3DXVECTOR4 tResult; - D3DXVec4Transform(&tResult, &tVec, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&tResult, &tVec, &BezierSegment::s_bezBasisMatrix); - outResult->x = D3DXVec4Dot(&xCoords, &tResult); - outResult->y = D3DXVec4Dot(&yCoords, &tResult); - outResult->z = D3DXVec4Dot(&zCoords, &tResult); + outResult->x = BezierMath::D3DXVec4Dot(&xCoords, &tResult); + outResult->y = BezierMath::D3DXVec4Dot(&yCoords, &tResult); + outResult->z = BezierMath::D3DXVec4Dot(&zCoords, &tResult); #else // SAGE_USE_GLM glm::vec4 tVec(tValue * tValue * tValue, tValue * tValue, tValue, 1); diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AI.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AI.cpp index bdb0a157580..60b47244dbb 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AI.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AI.cpp @@ -707,7 +707,7 @@ Object *AI::findClosestEnemy( const Object *me, Real range, UnsignedInt qualifie Real distSqr = ThePartitionManager->getDistanceSquared(me, theEnemy, FROM_BOUNDINGSPHERE_2D); Real dist = WWMath::SqrtOrigin(distSqr); - Int modifier = dist/getAiData()->m_attackPriorityDistanceModifier; + Int modifier = (Int)WWMath::Div_FixNaN(dist, getAiData()->m_attackPriorityDistanceModifier, 0.0f); Int modPriority = curPriority-modifier; if (modPriority < 1) modPriority = 1; diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp index 3e08d7b7f9d..a76565a16b9 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp @@ -627,7 +627,7 @@ void AISkirmishPlayer::buildAIBaseDefenseStructure(const AsciiString &thingName, Real structureRadius = tTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real baseCircumference = 2*PI*m_baseRadius; - Real angleOffset = 2*PI*(structureRadius*4/baseCircumference); + Real angleOffset = 2*PI*WWMath::Div_FixNaN(structureRadius*4, baseCircumference, 0.0f); Int selector; Real angle; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index 6d6a7b6c1b8..6309c25922c 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp @@ -1115,7 +1115,7 @@ void BridgeBehavior::createScaffolding() // to the center area of the bridge // Real tileDistance = leftVector.length(); - Int numObjects = REAL_TO_INT_CEIL( tileDistance / spacing ) + 1; + Int numObjects = REAL_TO_INT_CEIL( WWMath::Div_FixNaN(tileDistance, spacing, 0.0f) ) + 1; // // given the number of objects that we need to tile across the whole bridge, we will diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp index eaa7fec6b8a..1e537dcf218 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp @@ -441,7 +441,7 @@ Bool DumbProjectileBehavior::calcFlightPath(Bool recalcNumSegments) if (recalcNumSegments) { Real flightDistance = flightCurve.getApproximateLength(); - m_flightPathSegments = WWMath::Ceil( flightDistance / m_flightPathSpeed ); + m_flightPathSegments = (Int)WWMath::Ceil( WWMath::Div_FixNaN(flightDistance, m_flightPathSpeed, 1.0f) ); } flightCurve.getSegmentPoints( m_flightPathSegments, &m_flightPath ); DEBUG_ASSERTCRASH(m_flightPathSegments == m_flightPath.size(), ("m_flightPathSegments mismatch")); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp index b197c8f0f1a..64687a42411 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp @@ -236,7 +236,7 @@ void GenerateMinefieldBehavior::placeMinesAlongLine(const Coord3D& posStart, con Real mineRadius = mineTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real mineDiameter = mineRadius * 2.0f; Real mineJitter = mineRadius*d->m_randomJitter; - Int numMines = REAL_TO_INT_CEIL(len / mineDiameter); + Int numMines = REAL_TO_INT_CEIL(WWMath::Div_FixNaN(len, mineDiameter, 1.0f)); if (numMines < 1) numMines = 1; Real inc = len/numMines; @@ -296,7 +296,7 @@ void GenerateMinefieldBehavior::placeMinesAroundCircle(const Coord3D& pos, Real Real mineRadius = mineTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real mineDiameter = mineRadius * 2.0f; Real mineJitter = mineRadius*d->m_randomJitter; - Int numMines = REAL_TO_INT_CEIL(circum / mineDiameter); + Int numMines = REAL_TO_INT_CEIL(WWMath::Div_FixNaN(circum, mineDiameter, 1.0f)); if (numMines < 1) numMines = 1; Real angleInc = (2*PI)/numMines; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp index dfc2d1462f4..d476699050a 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp @@ -449,7 +449,7 @@ void MinefieldBehavior::onDamage( DamageInfo *damageInfo ) for (;;) { - Real virtualMinesExpectedF = ((Real)d->m_numVirtualMines * body->getHealth() / body->getMaxHealth()); + Real virtualMinesExpectedF = WWMath::Div_FixNaN((Real)d->m_numVirtualMines * body->getHealth(), body->getMaxHealth(), 0.0f); Int virtualMinesExpected = damageInfo->in.m_damageType == DAMAGE_HEALING ? REAL_TO_INT_FLOOR(virtualMinesExpectedF) : diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp index 7ccbfd9f58e..4a7a9c8b783 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp @@ -180,12 +180,8 @@ Int SlowDeathBehavior::getProbabilityModifier( const DamageInfo *damageInfo ) co // eg ( 200 hp max, had 10 left, took 50 damage, 40 overkill, (40/200) * 100 = 20 overkill %) Int overkillDamage = damageInfo->out.m_actualDamageDealt - damageInfo->out.m_actualDamageClipped; Real maxHealth = (Real)getObject()->getBodyModule()->getMaxHealth(); - Real overkillPercent = 0.0f; - if (maxHealth > 0.0f) - { - overkillPercent = (Real)overkillDamage / maxHealth; - } - Int overkillModifier = overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent; + Real overkillPercent = WWMath::Div_FixNaN((float)overkillDamage, maxHealth, 0.0f); + Int overkillModifier = (Int)(overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent); return max( getSlowDeathBehaviorModuleData()->m_probabilityModifier + overkillModifier, 1 ); } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index ed8e846cfe1..cbcfa00775b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -661,7 +661,7 @@ void ActiveBody::setMaxHealth( Real maxHealth, MaxHealthChangeType healthChangeT { //400/500 (80%) + 100 becomes 480/600 (80%) //200/500 (40%) - 100 becomes 160/400 (40%) - Real ratio = m_currentHealth / prevMaxHealth; + Real ratio = WWMath::Div_FixNaN(m_currentHealth, prevMaxHealth, 1.0f); Real newHealth = maxHealth * ratio; internalChangeHealth( newHealth - m_currentHealth ); break; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp index d19866872bd..d965fd01f00 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp @@ -343,10 +343,11 @@ Real DeliverPayloadAIUpdate::calcMinTurnRadius(Real* timeToTravelThatDist) const so we just eliminate the middleman: */ - Real minTurnRadius = (maxTurnRate > 0.0f) ? (maxSpeed / maxTurnRate) : 999999.0f; + // determine required turn radius based on our current speed and max turn rate + Real minTurnRadius = WWMath::Div_FixNaN(maxSpeed, maxTurnRate, 999999.0f); if (timeToTravelThatDist) - *timeToTravelThatDist = minTurnRadius / maxSpeed; + *timeToTravelThatDist = WWMath::Div_FixNaN(minTurnRadius, maxSpeed, 999999.0f); return minTurnRadius; } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp index af6982fc7ba..1a32542840e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp @@ -530,7 +530,7 @@ StateReturnType DozerActionDoActionState::update() // increase the construction percent of the goal object Int framesToBuild = goalObject->getTemplate()->calcTimeToBuild( dozer->getControllingPlayer() ); - Real percentProgressThisFrame = 100.0f / framesToBuild; + Real percentProgressThisFrame = WWMath::Div_FixNaN(100.0f, (Real)framesToBuild, 100.0f); goalObject->setConstructionPercent( goalObject->getConstructionPercent() + percentProgressThisFrame ); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp index 538123add49..58cd6b28ad1 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp @@ -295,7 +295,7 @@ Object* CommandButtonHuntUpdate::scanClosestTarget() if (info) curPriority = info->getPriority(other->getTemplate()); if (curPriority == 0) continue; // don't attack 0 priority targets. - Int modifier = dist/TheAI->getAiData()->m_attackPriorityDistanceModifier; + Int modifier = (Int)WWMath::Div_FixNaN(dist, TheAI->getAiData()->m_attackPriorityDistanceModifier, 0.0f); Int modPriority = curPriority-modifier; if (modPriority < 1) modPriority = 1; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp index 2fb71f13ca3..5afaa011c2e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp @@ -191,11 +191,11 @@ UpdateSleepTime SlavedUpdate::update() if( data->m_repairRatePerSecond > 0.0f ) { BodyModuleInterface *body = master->getBodyModule(); - if( body ) + if (body) { Real health = body->getHealth(); Real maxHealth = body->getMaxHealth(); - healthPercentage = (Int)(health / maxHealth * 100.0f); + healthPercentage = (Int)(WWMath::Div_FixNaN(health, maxHealth, 0.0f) * 100.0f); } } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp index a79b54e3a8c..bedf3241b2e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp @@ -201,7 +201,7 @@ void ToppleUpdate::applyTopplingForce( const Coord3D* toppleDirection, Real topp } // desired angle is toppleAngle +/- pi/2, whichever is closer to curangle Real desiredAngleX = angleClosestTo(toppleAngle + PI/2, toppleAngle - PI/2, curAngleX); - m_numAngleDeltaX = REAL_TO_INT_FLOOR(ANGULAR_LIMIT / (m_angularVelocity * 2)); + m_numAngleDeltaX = REAL_TO_INT_FLOOR(WWMath::Div_FixNaN(ANGULAR_LIMIT, m_angularVelocity * 2.0f, 1.0f)); if (m_numAngleDeltaX < 1) m_numAngleDeltaX = 1; m_angleDeltaX = (desiredAngleX - curAngleX) / m_numAngleDeltaX; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index 3a49859dfbf..dfdba0060ea 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -930,7 +930,7 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate v.y = victimPos->y - sourcePos->y; v.z = victimPos->z - sourcePos->z; // don't round the result; we WANT a fractional-frame-delay in this case. - Real delayInFrames = (v.length() / getWeaponSpeed()); + Real delayInFrames = WWMath::Div_FixNaN(v.length(), getWeaponSpeed()); if( firingWeapon->isLaser() ) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/BezierSegment.h b/GeneralsMD/Code/GameEngine/Include/Common/BezierSegment.h index 08b3ad8e9b2..4e830be328e 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/BezierSegment.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/BezierSegment.h @@ -40,6 +40,34 @@ #define USUAL_TOLERANCE 1.0f +#ifndef SAGE_USE_GLM +class BezierMath +{ +public: + static void D3DXVec4Transform(D3DXVECTOR4* out, const D3DXVECTOR4* v, const D3DXMATRIX* m) + { +#if USE_DETERMINISTIC_MATH + const float x = v->x, y = v->y, z = v->z, w = v->w; + out->x = ((x * m->m[0][0] + y * m->m[1][0]) + z * m->m[2][0]) + w * m->m[3][0]; + out->y = ((x * m->m[0][1] + y * m->m[1][1]) + z * m->m[2][1]) + w * m->m[3][1]; + out->z = ((x * m->m[0][2] + y * m->m[1][2]) + z * m->m[2][2]) + w * m->m[3][2]; + out->w = ((x * m->m[0][3] + y * m->m[1][3]) + z * m->m[2][3]) + w * m->m[3][3]; +#else + ::D3DXVec4Transform(out, v, m); +#endif + } + + static float D3DXVec4Dot(const D3DXVECTOR4* a, const D3DXVECTOR4* b) + { +#if USE_DETERMINISTIC_MATH + return ((a->x * b->x + a->y * b->y) + a->z * b->z) + a->w * b->w; +#else + return ::D3DXVec4Dot(a, b); +#endif + } +}; +#endif + class BezierSegment { protected: diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp index 471d530dc28..42196401c62 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezFwdIterator.cpp @@ -64,9 +64,9 @@ void BezFwdIterator::start() D3DXVECTOR4 pz(mBezSeg.m_controlPoints[0].z, mBezSeg.m_controlPoints[1].z, mBezSeg.m_controlPoints[2].z, mBezSeg.m_controlPoints[3].z); D3DXVECTOR4 cVec[3]; - D3DXVec4Transform(&cVec[0], &px, &BezierSegment::s_bezBasisMatrix); - D3DXVec4Transform(&cVec[1], &py, &BezierSegment::s_bezBasisMatrix); - D3DXVec4Transform(&cVec[2], &pz, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[0], &px, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[1], &py, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&cVec[2], &pz, &BezierSegment::s_bezBasisMatrix); #else // SAGE_USE_GLM glm::vec4 px(mBezSeg.m_controlPoints[0].x, mBezSeg.m_controlPoints[1].x, mBezSeg.m_controlPoints[2].x, mBezSeg.m_controlPoints[3].x); glm::vec4 py(mBezSeg.m_controlPoints[0].y, mBezSeg.m_controlPoints[1].y, mBezSeg.m_controlPoints[2].y, mBezSeg.m_controlPoints[3].y); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp index d0c3f9f8322..34c9fd7c437 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp @@ -116,11 +116,11 @@ void BezierSegment::evaluateBezSegmentAtT(Real tValue, Coord3D *outResult) const D3DXVECTOR4 zCoords(m_controlPoints[0].z, m_controlPoints[1].z, m_controlPoints[2].z, m_controlPoints[3].z); D3DXVECTOR4 tResult; - D3DXVec4Transform(&tResult, &tVec, &BezierSegment::s_bezBasisMatrix); + BezierMath::D3DXVec4Transform(&tResult, &tVec, &BezierSegment::s_bezBasisMatrix); - outResult->x = D3DXVec4Dot(&xCoords, &tResult); - outResult->y = D3DXVec4Dot(&yCoords, &tResult); - outResult->z = D3DXVec4Dot(&zCoords, &tResult); + outResult->x = BezierMath::D3DXVec4Dot(&xCoords, &tResult); + outResult->y = BezierMath::D3DXVec4Dot(&yCoords, &tResult); + outResult->z = BezierMath::D3DXVec4Dot(&zCoords, &tResult); #else // SAGE_USE_GLM glm::vec4 tVec(tValue * tValue * tValue, tValue * tValue, tValue, 1); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AI.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AI.cpp index 951fad65238..237b11accdf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AI.cpp @@ -710,7 +710,7 @@ Object *AI::findClosestEnemy( const Object *me, Real range, UnsignedInt qualifie Real distSqr = ThePartitionManager->getDistanceSquared(me, theEnemy, FROM_BOUNDINGSPHERE_2D); Real dist = WWMath::SqrtOrigin(distSqr); - Int modifier = dist/getAiData()->m_attackPriorityDistanceModifier; + Int modifier = (Int)WWMath::Div_FixNaN(dist, getAiData()->m_attackPriorityDistanceModifier, 0.0f); Int modPriority = curPriority-modifier; if (modPriority < 1) modPriority = 1; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp index d03330248ca..9fe3b00dee7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AISkirmishPlayer.cpp @@ -636,7 +636,7 @@ void AISkirmishPlayer::buildAIBaseDefenseStructure(const AsciiString &thingName, Real structureRadius = tTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real baseCircumference = 2*PI*defenseDistance; - Real angleOffset = 2*PI*(structureRadius*4/baseCircumference); + Real angleOffset = 2*PI*WWMath::Div_FixNaN(structureRadius*4, baseCircumference, 0.0f); Int selector; Real angle; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index 40ff14ffbbd..791b4495f20 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp @@ -1115,7 +1115,7 @@ void BridgeBehavior::createScaffolding() // to the center area of the bridge // Real tileDistance = leftVector.length(); - Int numObjects = REAL_TO_INT_CEIL( tileDistance / spacing ) + 1; + Int numObjects = REAL_TO_INT_CEIL( WWMath::Div_FixNaN(tileDistance, spacing, 0.0f) ) + 1; // // given the number of objects that we need to tile across the whole bridge, we will diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp index 9d5ee6daab1..5704b18107c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp @@ -444,7 +444,7 @@ Bool DumbProjectileBehavior::calcFlightPath(Bool recalcNumSegments) if (recalcNumSegments) { Real flightDistance = flightCurve.getApproximateLength(); - m_flightPathSegments = WWMath::Ceil( flightDistance / m_flightPathSpeed ); + m_flightPathSegments = (Int)WWMath::Ceil( WWMath::Div_FixNaN(flightDistance, m_flightPathSpeed, 1.0f) ); } // TheSuperHackers @info The way flight paths are used requires at least two curve points. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp index 1f89505fc1f..a11d67817b2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp @@ -252,7 +252,7 @@ void GenerateMinefieldBehavior::placeMinesAlongLine(const Coord3D& posStart, con Real mineRadius = mineTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real mineDiameter = mineRadius * 2.0f; Real mineJitter = mineRadius*d->m_randomJitter; - Int numMines = REAL_TO_INT_CEIL(len / mineDiameter); + Int numMines = REAL_TO_INT_CEIL(WWMath::Div_FixNaN(len, mineDiameter, 1.0f)); if (numMines < 1) numMines = 1; Real inc = len/numMines; @@ -312,7 +312,7 @@ void GenerateMinefieldBehavior::placeMinesAroundCircle(const Coord3D& pos, Real Real mineRadius = mineTemplate->getTemplateGeometryInfo().getBoundingCircleRadius(); Real mineDiameter = mineRadius * 2.0f; Real mineJitter = mineRadius*d->m_randomJitter; - Int numMines = REAL_TO_INT_CEIL(circum / mineDiameter); + Int numMines = REAL_TO_INT_CEIL(WWMath::Div_FixNaN(circum, mineDiameter, 1.0f)); if (numMines < 1) numMines = 1; Real angleInc = (2*PI)/numMines; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp index f1281ee363d..165b0e76b3e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/MinefieldBehavior.cpp @@ -457,7 +457,7 @@ void MinefieldBehavior::onDamage( DamageInfo *damageInfo ) for (;;) { - Real virtualMinesExpectedF = ((Real)d->m_numVirtualMines * body->getHealth() / body->getMaxHealth()); + Real virtualMinesExpectedF = WWMath::Div_FixNaN((Real)d->m_numVirtualMines * body->getHealth(), body->getMaxHealth(), 0.0f); Int virtualMinesExpected = damageInfo->in.m_damageType == DAMAGE_HEALING ? REAL_TO_INT_FLOOR(virtualMinesExpectedF) : diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp index ca70740b8cd..c9e62d109e4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/SlowDeathBehavior.cpp @@ -180,12 +180,8 @@ Int SlowDeathBehavior::getProbabilityModifier( const DamageInfo *damageInfo ) co // eg ( 200 hp max, had 10 left, took 50 damage, 40 overkill, (40/200) * 100 = 20 overkill %) Int overkillDamage = damageInfo->out.m_actualDamageDealt - damageInfo->out.m_actualDamageClipped; Real maxHealth = (Real)getObject()->getBodyModule()->getMaxHealth(); - Real overkillPercent = 0.0f; - if (maxHealth > 0.0f) - { - overkillPercent = (Real)overkillDamage / maxHealth; - } - Int overkillModifier = overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent; + Real overkillPercent = WWMath::Div_FixNaN((float)overkillDamage, maxHealth, 0.0f); + Int overkillModifier = (Int)(overkillPercent * getSlowDeathBehaviorModuleData()->m_modifierBonusPerOverkillPercent); return max( getSlowDeathBehaviorModuleData()->m_probabilityModifier + overkillModifier, 1 ); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 550d5bba1e1..b781deda711 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -910,7 +910,7 @@ void ActiveBody::setMaxHealth( Real maxHealth, MaxHealthChangeType healthChangeT { //400/500 (80%) + 100 becomes 480/600 (80%) //200/500 (40%) - 100 becomes 160/400 (40%) - Real ratio = m_currentHealth / prevMaxHealth; + Real ratio = WWMath::Div_FixNaN(m_currentHealth, prevMaxHealth, 1.0f); Real newHealth = maxHealth * ratio; internalChangeHealth( newHealth - m_currentHealth ); break; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp index ae6c2d6aada..2d4b8675b1c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp @@ -344,10 +344,11 @@ Real DeliverPayloadAIUpdate::calcMinTurnRadius(Real* timeToTravelThatDist) const so we just eliminate the middleman: */ - Real minTurnRadius = (maxTurnRate > 0.0f) ? (maxSpeed / maxTurnRate) : 999999.0f; + // determine required turn radius based on our current speed and max turn rate + Real minTurnRadius = WWMath::Div_FixNaN(maxSpeed, maxTurnRate, 999999.0f); if (timeToTravelThatDist) - *timeToTravelThatDist = minTurnRadius / maxSpeed; + *timeToTravelThatDist = WWMath::Div_FixNaN(minTurnRadius, maxSpeed, 999999.0f); return minTurnRadius; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp index 2ece830173a..80dd3047c6e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DozerAIUpdate.cpp @@ -530,7 +530,7 @@ StateReturnType DozerActionDoActionState::update() // increase the construction percent of the goal object Int framesToBuild = goalObject->getTemplate()->calcTimeToBuild( dozer->getControllingPlayer() ); - Real percentProgressThisFrame = 100.0f / framesToBuild; + Real percentProgressThisFrame = WWMath::Div_FixNaN(100.0f, (Real)framesToBuild, 100.0f); goalObject->setConstructionPercent( goalObject->getConstructionPercent() + percentProgressThisFrame ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp index c769f4880d5..fa0e0d2d7d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp @@ -343,7 +343,7 @@ Object* CommandButtonHuntUpdate::scanClosestTarget() if (info) curPriority = info->getPriority(other->getTemplate()); if (curPriority == 0) continue; // don't attack 0 priority targets. - Int modifier = dist/TheAI->getAiData()->m_attackPriorityDistanceModifier; + Int modifier = (Int)WWMath::Div_FixNaN(dist, TheAI->getAiData()->m_attackPriorityDistanceModifier, 0.0f); Int modPriority = curPriority-modifier; if (modPriority < 1) modPriority = 1; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp index eb3c079bba9..f6b969ff46c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SlavedUpdate.cpp @@ -196,11 +196,11 @@ UpdateSleepTime SlavedUpdate::update() if( data->m_repairRatePerSecond > 0.0f ) { BodyModuleInterface *body = master->getBodyModule(); - if( body ) + if (body) { Real health = body->getHealth(); Real maxHealth = body->getMaxHealth(); - healthPercentage = (Int)(health / maxHealth * 100.0f); + healthPercentage = (Int)(WWMath::Div_FixNaN(health, maxHealth, 0.0f) * 100.0f); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp index 18d75955b94..5cb0cf02386 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ToppleUpdate.cpp @@ -201,7 +201,7 @@ void ToppleUpdate::applyTopplingForce( const Coord3D* toppleDirection, Real topp } // desired angle is toppleAngle +/- pi/2, whichever is closer to curangle Real desiredAngleX = angleClosestTo(toppleAngle + PI/2, toppleAngle - PI/2, curAngleX); - m_numAngleDeltaX = REAL_TO_INT_FLOOR(ANGULAR_LIMIT / (m_angularVelocity * 2)); + m_numAngleDeltaX = REAL_TO_INT_FLOOR(WWMath::Div_FixNaN(ANGULAR_LIMIT, m_angularVelocity * 2.0f, 1.0f)); if (m_numAngleDeltaX < 1) m_numAngleDeltaX = 1; m_angleDeltaX = (desiredAngleX - curAngleX) / m_numAngleDeltaX; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index d18bb8bd13a..60e0a56dd48 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -1008,7 +1008,7 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate v.y = victimPos->y - sourcePos->y; v.z = victimPos->z - sourcePos->z; // don't round the result; we WANT a fractional-frame-delay in this case. - Real delayInFrames = (v.length() / getWeaponSpeed()); + Real delayInFrames = WWMath::Div_FixNaN(v.length(), getWeaponSpeed()); ObjectID damageID = getDamageDealtAtSelfPosition() ? INVALID_ID : victimID; From 8f7fe420e9de447d82f0f5768290f7fdf4834562 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 15 Jul 2026 20:32:24 -0300 Subject: [PATCH 04/20] GeneralsX @bugfix felipebraz 15/07/2026 Backport SpecialPowerModule m_availableOnFrame fix to Generals Missed during PR #3 backport: Remove RETAIL_COMPATIBLE_CRC guard around m_availableOnFrame initialization. Always set to 0 to prevent special powers from being stuck when RETAIL_COMPATIBLE_CRC is off. --- .../GameLogic/Object/SpecialPower/SpecialPowerModule.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp index 24db1aab346..ad62f7b8b9e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp @@ -96,11 +96,10 @@ SpecialPowerModule::SpecialPowerModule( Thing *thing, const ModuleData *moduleDa : BehaviorModule( thing, moduleData ) { -#if RETAIL_COMPATIBLE_CRC + // GeneralsX @bugfix felipebraz 15/07/2026 Always initialize to 0. + // Disabling RETAIL_COMPATIBLE_CRC previously forced this to 0xFFFFFFFF, which caused unpaused + // targetless special powers (like Spy Satellite and Battle Plans) to never become ready. m_availableOnFrame = 0; -#else - m_availableOnFrame = 0xFFFFFFFF; -#endif m_pausedCount = 0; m_pausedOnFrame = 0; m_pausedPercent = 0.0f; From 9e510f03d14e1a9e6630a6277d03b582c27a29c2 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 15 Jul 2026 21:06:48 -0300 Subject: [PATCH 05/20] GeneralsX @bugfix felipebraz 15/07/2026 Remove accidentally tracked .rej file --- .../GameEngine/Source/GameClient/GameText.cpp.rej | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp.rej diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp.rej b/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp.rej deleted file mode 100644 index 174c448c56a..00000000000 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp.rej +++ /dev/null @@ -1,15 +0,0 @@ ---- GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp -+++ GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp -@@ -979,6 +979,12 @@ - - while( file->read ( &id, sizeof (id)) == sizeof ( id) ) - { -+// GeneralsX @bugfix BenderAI 13/02/2026 - Progress logging -+if (listCount % 1000 == 0) { -+char progressMsg[128]; -+snprintf(progressMsg, sizeof(progressMsg), "GameTextManager::parseCSF() - Parsing label %d/%d...\n", listCount, header.num_labels); -+OutputDebugString(progressMsg); -+} - t num; - t num_strings; - From 501823d45756229d1517f6a5a90aa58d7813130b Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 15 Jul 2026 23:21:45 -0300 Subject: [PATCH 06/20] fix(math): isolate double-precision math to prevent x87/NEON desync - Force double-precision variants of SqrtOrigin and PowOrigin to downcast and use single-precision gm_*f implementations. - This prevents a known 1-ULP drift on ARM64 NEON vs x86 x87 FPU when evaluating double-precision transcendentals. - Documented NaN and double-precision determinism guidelines in AGENTS.md. --- AGENTS.md | 2 ++ Core/Libraries/Source/WWVegas/WWMath/wwmath.h | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cf85e6ac55d..33ba96ab5a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,8 @@ GeneralsX is a cross-platform port of Command & Conquer: Generals Zero Hour for - `ceil` / `ceilf` -> `WWMath::Ceil` - `floor` / `floorf` -> `WWMath::Floor` - `pow` -> `WWMath::PowOrigin` +13. **NaN/Inf Integer Casting** – ARM64 casts `(Int)NaN` to `0`, while x86 casts it to `INT_MIN`. Any division by potentially zero (e.g. `val / maxHealth`) whose result might be cast to an integer or affect simulation flow MUST be guarded (`if (maxHealth > 0)`) or use `WWMath::Div_FixNaN` to avoid immediate CRC desyncs. +14. **Double-Precision Isolation** – Do not allow implicit `double` promotion in transcendental/power math evaluations (e.g. `gm_sqrt`, `gm_pow`, `gm_atan`). x87 (24-bit mantissa) and NEON (53-bit mantissa) diverge by 1-ULP on double-precision. Ensure all `WWMath` wrappers explicitly accept/return `float` or internally downcast `double` to `float` prior to `gm_*f` execution to guarantee cross-platform bit-identical outputs. ## Reference Repositories - **fighter19-dxvk-port** – Primary graphics/platform reference (DXVK + SDL3 on Linux) diff --git a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h index 5d2c672146b..222a2f11683 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/wwmath.h +++ b/Core/Libraries/Source/WWVegas/WWMath/wwmath.h @@ -217,12 +217,12 @@ static WWINLINE float ASinTrig(float x) // Origin wrappers: replace bare CRT math calls in GameLogic. // Each wrapper preserves the exact type (float vs double) of the vanilla CRT call. #if USE_DETERMINISTIC_MATH - static WWINLINE double SqrtOrigin(double x) { return gm_sqrt(x); } + static WWINLINE double SqrtOrigin(double x) { return (double)gm_sqrtf((float)x); } static WWINLINE float SqrtOrigin(float x) { return gm_sqrtf(x); } static WWINLINE float SqrtfOrigin(float x) { return gm_sqrtf(x); } static WWINLINE double Atan2Origin(double y, double x) { return (double)gm_atan2f((float)y, (float)x); } static WWINLINE float Atan2Origin(float y, float x) { return gm_atan2f(y, x); } - static WWINLINE double PowOrigin(double x, double y) { return gm_pow(x, y); } + static WWINLINE double PowOrigin(double x, double y) { return (double)gm_powf((float)x, (float)y); } static WWINLINE float PowOrigin(float x, float y) { return gm_powf(x, y); } #else static WWINLINE double SqrtOrigin(double x) { return sqrt(x); } From 524d0fb619b9503a57702da3e53a78b2e75d43b8 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 16 Jul 2026 16:51:32 -0300 Subject: [PATCH 07/20] fix(math): enforce deterministic FPU boundaries and FMA flags - Disabled FMA contraction globally in MSVC via /fp:precise - Added ScopedFPUGuard at the entry point of GameLogic::update - Added ScopedFPUGuard to MiniAudioManager and W3DView pickDrawable - Backported GameLogic::update FPU guard to Generals base game - Created scripts/qa/sync/run-determinism-harness.sh to automate cross-platform desync debugging This completes the execution of steps 1, 5, and 6 of the FP determinism analysis to prevent FP-environment state leaks from OS/Audio threads into simulation math and guarantees MSVC produces bit-exact IEEE-754 semantics matching ARM64/Linux. --- .../MiniAudioDevice/MiniAudioManager.cpp | 1 + .../Source/W3DDevice/GameClient/W3DView.cpp | 2 + .../Source/GameLogic/System/GameLogic.cpp | 3 + .../Source/GameLogic/System/GameLogic.cpp | 3 + cmake/compilers.cmake | 2 + .../generalsx-fp-determinism-lib-analysis.md | 140 ++++++++++++++++++ scripts/qa/sync/run-determinism-harness.sh | 21 +++ 7 files changed, 172 insertions(+) create mode 100644 docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md create mode 100755 scripts/qa/sync/run-determinism-harness.sh diff --git a/Core/GameEngineDevice/Source/MiniAudioDevice/MiniAudioManager.cpp b/Core/GameEngineDevice/Source/MiniAudioDevice/MiniAudioManager.cpp index b4f530734eb..afc9be3eda4 100644 --- a/Core/GameEngineDevice/Source/MiniAudioDevice/MiniAudioManager.cpp +++ b/Core/GameEngineDevice/Source/MiniAudioDevice/MiniAudioManager.cpp @@ -209,6 +209,7 @@ void MiniAudioManager::reset() void MiniAudioManager::update() { ScopedFPUGuard fpuGuard; + AudioManager::update(); setDeviceListenerPosition(); processRequestList(); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index cfd8e75a3f9..69dbe92433e 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -34,6 +34,7 @@ // SYSTEM INCLUDES //////////////////////////////////////////////////////////////////////////////// #include +#include "GameLogic/FPUControl.h" #include // USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// @@ -2512,6 +2513,7 @@ Int W3DView::iterateDrawablesInRegion( IRegion2D *screenRegion, //------------------------------------------------------------------------------------------------- Drawable *W3DView::pickDrawable( const ICoord2D *screen, Bool forceAttack, PickType pickType ) { + ScopedFPUGuard fpuGuard; RenderObjClass *renderObj = nullptr; Drawable *draw = nullptr; DrawableInfo *drawInfo = nullptr; diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 435af6930ef..9f64ec59805 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3205,6 +3205,9 @@ void GameLogic::update() USE_PERF_TIMER(GameLogic_update) PROFILER_SECTION_COLOR(0x4CAF50); + // GeneralsX @bugfix fbraz3 16/07/2026 Lock FPU state before every simulation frame + ScopedFPUGuard fpuGuard; + LatchRestore inUpdateLatch(m_isInUpdate, TRUE); #ifdef DO_UNIT_TIMINGS unitTimings(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index dd16880db40..a526e00be1e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3763,6 +3763,9 @@ void GameLogic::update() USE_PERF_TIMER(GameLogic_update) PROFILER_SECTION_COLOR(0x4CAF50); + // GeneralsX @bugfix fbraz3 16/07/2026 Lock FPU state before every simulation frame + ScopedFPUGuard fpuGuard; + LatchRestore inUpdateLatch(m_isInUpdate, TRUE); #ifdef DO_UNIT_TIMINGS unitTimings(); diff --git a/cmake/compilers.cmake b/cmake/compilers.cmake index c22c3ae6b60..67b7228b559 100644 --- a/cmake/compilers.cmake +++ b/cmake/compilers.cmake @@ -49,6 +49,8 @@ if (NOT IS_VS6_BUILD) add_compile_options(/MP) # Enforce strict __cplusplus version add_compile_options(/Zc:__cplusplus) + # Prevent FMA contraction to avoid cross-platform divergence + add_compile_options(/fp:precise) else() add_compile_options(-Wsuggest-override) # GeneralsX @build fbraz 03/05/2026 Disable FMA contraction to avoid diff --git a/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md b/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md new file mode 100644 index 00000000000..e1e6c0780c9 --- /dev/null +++ b/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md @@ -0,0 +1,140 @@ +# Cross-Platform FP Determinism for GeneralsX - Library Analysis + +**Context:** [GeneralsX](https://github.com/fbraz3/GeneralsX) needs game-physics math to be +bit-identical across operating systems and CPU architectures (macOS ARM64, Linux x86_64, +future Windows) so that lockstep multiplayer and replays do not desync (CRC mismatch). + +**Candidates evaluated:** + +- [freemint/fdlibm](https://github.com/freemint/fdlibm) +- [TheAssemblyArmada/GameMath](https://github.com/TheAssemblyArmada/GameMath) - **already integrated** (PR #176) +- [streflop (Nicolas Brodu)](https://nicolas.brodu.net/programmation/streflop/index.html) + +--- + +## Bottom line first + +Switching to **fdlibm or streflop will almost certainly NOT fix the remaining desyncs**, because +the desyncs left are not caused by the transcendental functions - which GameMath already handles. +Keep GameMath. The open PR #217 and issue #215 already point at the real culprits: **FMA +contraction, implicit `double` promotion, and FP-environment leaks**. None of those is a +"which libm" problem. + +--- + +## The critical realization: all three are the *same* library underneath + +Sun netlib fdlibm (1993) <- the common ancestor + |-- FreeBSD msun <-- GameMath (float-only, C, BSD-2) <- already in use + |-- GNU libm 2.4 <-- streflop (+SoftFloat, C++, LGPL) + |-- freemint/fdlibm (Atari m68k/ColdFire fork, C) + +PR #176 even describes GameMath's routed functions as *"fdlibm-based deterministic functions"* - +because they are. Therefore: + +- **Switching to `freemint/fdlibm` is circular.** It replaces a modern, cross-platform, + CMake-integrated fdlibm derivative (GameMath) with an older one whose maintained fork targets + **Atari**. Identical math results, worse integration. Don't. +- **streflop's transcendentals are also fdlibm-lineage**, so its `sin/cos/tan/sqrt/atan2` will not + return different values than GameMath for the same `float` inputs. Its value is *elsewhere* + (`Double` type + SoftFloat + FP-env management), not in "better math." + +--- + +## Why GameMath did not stop the desyncs + +The remaining divergence between **macOS ARM64 <-> Linux x86_64** comes from things a math library +cannot fix by itself: + +| # | Root cause | Evidence in repo | Does a lib swap fix it? | +|---|---|---|---| +| 1 | **FMA contraction** - `a*b+c` fused into one op with different rounding. Clang on Apple Silicon and GCC/Clang on x86 do this at `-O2` by default. | Classic ARM64-vs-x86 divergence; no global flag yet | **No** - compiler flag, not a lib | +| 2 | **Implicit `double` promotion / overload ambiguity** - calls silently hitting native `double` math, bypassing GameMath | PR #217: fixing `SqrtOrigin`/`Atan2Origin`/`PowOrigin` ambiguity + explicit `(float)` casts | **No** - type/call-site fix | +| 3 | **`double`-precision intermediates** (x87 80-bit vs SSE 64-bit vs NEON) | PR #217 commit: *"isolate double-precision math to prevent x87/NEON desync"* | **Partially** - GameMath is **float-only**; here streflop/fdlibm *could* help | +| 4 | **FP-environment leaks** - audio thread leaves rounding/FTZ-DAZ skewed; `setFPMode()` not called on all paths | Issue #215: OpenAL thread corrupts mouse-pick math | **No** - must set/restore FP env at boundaries | +| 5 | **Non-FP divergence** - pointer ordering, `unordered_map` iteration, uninitialized reads, RNG stream | CRC/replay checker exists, but these are invisible to any libm | **No** | + +Only cause **#3** is one where a different library is even relevant - and only *after* #1, #2, and +#4 are fixed. + +--- + +## Head-to-head, tailored to GeneralsX + +| Dimension | freemint/fdlibm | **GameMath (current)** | streflop | +|---|---|---|---| +| Language / API | C | C (`gm_` prefix) | C++ (namespaced) + C shim | +| Precision covered | float + double + long double | **float only** | Simple + **Double** (+partial Extended) | +| FP-env / denormal management | No | No | Yes (`streflop_init()`, FTZ/DAZ, x87 squasher) | +| Deterministic RNG included | No | No | Yes (Mersenne Twister) | +| Software FPU (bit-identical on *all* archs) | No | No (relies on IEEE754 hardware) | Yes (SoftFloat mode) | +| License | Sun permissive / BSD-2 | LGPL-2.1 | +| GPLv3 compatible (GeneralsX is GPLv3) | Yes | Yes | Yes (LGPL->GPLv3 is fine) | +| Modern-platform port state | Atari-focused | Clean CMake, cross-platform | Needs Spring-style porting for MSVC x64 / ARM | +| Provenance fit | Low (Atari) | **Highest** - by Gun1Blade, core **Thyme** dev; Thyme reimplements *this exact SAGE/Generals engine* | High (Spring RTS ships it across the same OS set) | +| Fixes FMA (#1)? | No | No | No | +| Fixes double paths (#3)? | Yes (has double) | No (float only) | Yes (has Double) | +| Fixes FP-env leak (#4)? | No | No | Packages it, but you still must call it | + +**Licensing note:** the objection GameMath's README raises against streflop ("LGPL limits static +linking") **does not apply here**, because GeneralsX is GPLv3 and GPLv3 absorbs LGPL. That concern +only affects *proprietary* projects. License is not a deciding factor. + +--- + +## When streflop is actually worth adding + +Consider streflop **only as a second phase**, and only if - after fixing FMA + promotion + FP-env - +divergence persists. Its genuine unique advantages here are exactly two: + +1. **The `Double` type** - if the simulation has sync-critical `double` math (cause #3), GameMath + cannot cover it and streflop can. +2. **SoftFloat mode** - the *only* option here that yields **bit-identical results across every + architecture** (ARM64, x86_64, x87, future Windows) because it bypasses the hardware FPU + entirely. Cost: slower, C++, heavier. + +If adopting it, **use the Spring RTS streflop fork**, not Brodu's 2012 v0.3 upstream - Spring's fork +adds MSVC x64 assembly (`FPUSettings.asm`) and has years of hardening across the exact +Windows/Linux/macOS matrix GeneralsX targets. The original v0.3 will not build cleanly on modern +MSVC/ARM. Its x87-specific machinery is largely dead weight (Mac/Linux 64-bit and future Windows +64-bit use SSE2/NEON, not x87). + +--- + +## Action plan that actually kills the desyncs (prioritized) + +1. **Disable FMA contraction globally** - `-ffp-contract=off` (Clang/GCC), `/fp:precise` and *no* + `/fp:fast` (MSVC), plus `#pragma STDC FP_CONTRACT OFF` in hot simulation TUs. Most likely fix for + ARM64<->x86 divergence, and free. +2. **Ban fast-math everywhere** - no `-ffast-math`, `-Ofast`, `/fp:fast`, including in third-party + dependencies. +3. **Finish PR #217** - eliminate every implicit `double` promotion in `GameLogic`; ensure each + `sqrt/sin/cos/tan/pow/atan2` in the sim resolves to the GameMath float overload. Grep the logic + for bare `sqrt(`, `pow(`, `sin(`, `atan2(`. +4. **Handle the double-precision paths (cause #3)** - force them to float where safe, or route them + through a deterministic double libm. This is the only step where library choice matters + (streflop `Double`, or fdlibm double). +5. **Lock the FP environment at boundaries (issue #215)** - call `setFPMode()` at the entry of + `GameLogic::update()` **and** in the mouse-pick / audio / render callbacks that leak it; restore + on exit. Set FTZ/DAZ consistently on all threads. +6. **Build a determinism harness** - CRC the full sim state each frame, replay the same input on Mac + ARM64 + Linux x64, and binary-search the first diverging frame. This identifies the exact call + that desyncs - far more useful than swapping libraries blindly. +7. **Rule out non-FP divergence** - `unordered_map` iteration order, pointer-dependent branching, + uninitialized struct padding fed into the CRC, RNG seed/stream. + +Do 1-6 first. If - and only if - divergence remains on `double` logic afterward, adopt the +**Spring streflop fork** for those paths (or go full SoftFloat for guaranteed bit-identical results +at a performance cost). Raw `freemint/fdlibm` is not recommended at all: it adds nothing GameMath +does not already provide, with worse integration. + +--- + +## Verdict + +| Question | Answer | +|---|---| +| Switch to raw fdlibm? | **No** - circular (GameMath *is* fdlibm) and an integration regression. | +| Keep GameMath? | **Yes** - correct choice, same provenance as the Generals reimplementation (Thyme), already integrated. | +| Adopt streflop now? | **Not yet** - only after fixing flags/promotion/FP-env, and only for `double` paths or a full SoftFloat guarantee. Use the Spring fork if you do. | +| Are the remaining desyncs a library problem? | **No** - they are build-flag + type-promotion + FP-environment-discipline problems (PR #217 / issue #215 are already on the right track). | diff --git a/scripts/qa/sync/run-determinism-harness.sh b/scripts/qa/sync/run-determinism-harness.sh new file mode 100755 index 00000000000..442f72ac142 --- /dev/null +++ b/scripts/qa/sync/run-determinism-harness.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# GeneralsX Determinism Harness +# Runs the game with a specified replay file to capture sync logs for diffing. + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +REPLAY_FILE=$1 +LOG_DIR="logs/sync_harness" +mkdir -p "$LOG_DIR" + +echo "Running determinism harness on replay: $REPLAY_FILE" + +# Run the game in a controlled, quickstart mode to force replay playback +# and generate SyncCrash or crc logs. +./run.sh -win -xres 800 -yres 600 -quickstart -replay "$REPLAY_FILE" 2>&1 | tee "$LOG_DIR/harness_run.log" + +echo "Harness run complete. Check $LOG_DIR and the generated SyncCrash files." +echo "Compare the SyncCrash outputs between your Mac ARM64 build and Linux x64 build to find the exact diverging frame." From 644797c0abfa838971dfb7c2d65ee58000ffd9ed Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 16 Jul 2026 17:05:45 -0300 Subject: [PATCH 08/20] docs: minor formatting in determinism analysis --- docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md b/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md index e1e6c0780c9..8a57981aaaf 100644 --- a/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md +++ b/docs/WORKDIR/generalsx-fp-determinism-lib-analysis.md @@ -130,7 +130,7 @@ does not already provide, with worse integration. --- -## Verdict +## Verdict | Question | Answer | |---|---| From eaebb2375f70b76e3a8f3bcbf0c4c9fe55c0ce0d Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 16 Jul 2026 17:18:01 -0300 Subject: [PATCH 09/20] docs: extract determinism rules into a dedicated section in AGENTS.md --- AGENTS.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 33ba96ab5a8..2df02fea4d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,11 @@ GeneralsX is a cross-platform port of Command & Conquer: Generals Zero Hour for 9. **Update worklog** – Update `docs/WORKLOG/YYYY-MM-DIARY.md` before committing (see [.github/instructions/docs.instructions.md](.github/instructions/docs.instructions.md) for details) 10. **Reference repos** – Study patterns, don't copy-paste 11. **Backport to Generals** – Bugfixes and improvements must be backported to the Generals base game. -12. **Deterministic Math** – The native math functions were replaced by deterministic multi-platform WWMath equivalents to maintain determinism. This pattern must be preserved. When syncing upstream, replace these functions: + +## Cross-Platform Determinism (Mac vs Linux) +To guarantee cross-play between macOS ARM64 and Linux x86_64 without SyncCrash desyncs, you must obey the following rules: + +1. **Deterministic Math** – The native math functions were replaced by deterministic multi-platform WWMath equivalents to maintain determinism. This pattern must be preserved. When syncing upstream, replace these functions: - `sqrt` / `sqrtf` -> `WWMath::SqrtOrigin` / `WWMath::SqrtfOrigin` or `WWMath::Sqrt` - `sin` / `sinf` -> `WWMath::SinTrig` or `WWMath::Sin` - `cos` / `cosf` -> `WWMath::CosTrig` or `WWMath::Cos` @@ -48,8 +52,10 @@ GeneralsX is a cross-platform port of Command & Conquer: Generals Zero Hour for - `ceil` / `ceilf` -> `WWMath::Ceil` - `floor` / `floorf` -> `WWMath::Floor` - `pow` -> `WWMath::PowOrigin` -13. **NaN/Inf Integer Casting** – ARM64 casts `(Int)NaN` to `0`, while x86 casts it to `INT_MIN`. Any division by potentially zero (e.g. `val / maxHealth`) whose result might be cast to an integer or affect simulation flow MUST be guarded (`if (maxHealth > 0)`) or use `WWMath::Div_FixNaN` to avoid immediate CRC desyncs. -14. **Double-Precision Isolation** – Do not allow implicit `double` promotion in transcendental/power math evaluations (e.g. `gm_sqrt`, `gm_pow`, `gm_atan`). x87 (24-bit mantissa) and NEON (53-bit mantissa) diverge by 1-ULP on double-precision. Ensure all `WWMath` wrappers explicitly accept/return `float` or internally downcast `double` to `float` prior to `gm_*f` execution to guarantee cross-platform bit-identical outputs. +2. **NaN/Inf Integer Casting** – ARM64 casts `(Int)NaN` to `0`, while x86 casts it to `INT_MIN`. Any division by potentially zero (e.g. `val / maxHealth`) whose result might be cast to an integer or affect simulation flow MUST be guarded (`if (maxHealth > 0)`) or use `WWMath::Div_FixNaN` to avoid immediate CRC desyncs. +3. **Double-Precision Isolation** – Do not allow implicit `double` promotion in transcendental/power math evaluations (e.g. `gm_sqrt`, `gm_pow`, `gm_atan`). x87 (24-bit mantissa) and NEON (53-bit mantissa) diverge by 1-ULP on double-precision. Ensure all `WWMath` wrappers explicitly accept/return `float` or internally downcast `double` to `float` prior to `gm_*f` execution to guarantee cross-platform bit-identical outputs. +4. **FPU Environment State Leaks** – Audio drivers (OpenAL/MiniAudio) and OS callbacks aggressively alter hardware FPU registers (e.g., Flush-To-Zero, Rounding mode) for DSP performance. If this state leaks into the main thread, the simulation math diverges. **Always inject `ScopedFPUGuard`** at the boundaries of `GameLogic::update()`, audio thread callbacks, and mouse-picking logic (e.g., `View::pickDrawable()`). +5. **FMA Contraction** – Fused Multiply-Add combines instructions with infinite intermediate precision, yielding different results on ARM64 vs x86_64. We strictly enforce `-ffp-contract=off` globally (and `/fp:precise` for MSVC). Never bypass this with `-ffast-math` or `/fp:fast`. ## Reference Repositories - **fighter19-dxvk-port** – Primary graphics/platform reference (DXVK + SDL3 on Linux) From ef492b562242a9cf65f34b9f36f3911a9467082b Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 16 Jul 2026 20:21:11 -0300 Subject: [PATCH 10/20] GeneralsX @feature Mr. Meeseeks 16/07/2026 Deep CRC Memory Buffer logging support Implement in-memory deep CRC frame buffering to identify mismatch causes. - Buffers 64 frames in memory using std::vector behind DEEP_CRC_TO_MEMORY. - Dumps buffers to logs/deep_crc_YYYY-MM-DD-HH-MM-SS.bin only upon a mismatch. - Adds OS/Arch header for cross-platform debugging telemetry. - Enhances mismatch string passed to setSawCRCMismatch with frame details and player-level mismatches. - Fixes issue where disconnected/defeated players would cause false-positive CRC quorum failures. - Enabled for both Generals and GeneralsMD. --- Core/GameEngine/Include/Common/GameDefines.h | 4 + Core/GameEngine/Include/Common/Xfer.h | 5 + Core/GameEngine/Include/Common/XferDeepCRC.h | 13 ++ .../Include/GameNetwork/NetworkInterface.h | 4 + .../Source/Common/System/XferCRC.cpp | 79 +++++++++ .../GameLogic/System/GameLogicDispatch.cpp | 11 +- .../GameEngine/Source/GameNetwork/Network.cpp | 8 + .../GameEngine/Include/GameLogic/GameLogic.h | 11 +- .../Source/GameLogic/System/GameLogic.cpp | 157 +++++++++++++++++- .../GameEngine/Include/GameLogic/GameLogic.h | 11 +- .../Source/GameLogic/System/GameLogic.cpp | 157 +++++++++++++++++- cmake/config-build.cmake | 7 + scripts/qa/sync/run-determinism-harness.sh | 7 +- 13 files changed, 463 insertions(+), 11 deletions(-) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index fbace7f780d..08006281a4a 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -139,6 +139,10 @@ #define USE_BUFFERED_IO (1) #endif +#if (!defined(DEEP_CRC_TO_MEMORY) && !defined(DEBUG_CRC)) +#define DEEP_CRC_TO_MEMORY 0 +#endif + // Enable cache for local file existence. Reduces amount of disk accesses for better performance, // but decreases file existence correctness and runtime stability, if a cached file is deleted on runtime. #ifndef ENABLE_FILESYSTEM_EXISTENCE_CACHE diff --git a/Core/GameEngine/Include/Common/Xfer.h b/Core/GameEngine/Include/Common/Xfer.h index 617b8e75e38..bcb41cb6731 100644 --- a/Core/GameEngine/Include/Common/Xfer.h +++ b/Core/GameEngine/Include/Common/Xfer.h @@ -178,6 +178,11 @@ class Xfer virtual void xferMatrix3D( Matrix3D* mtx ); virtual void xferMapName( AsciiString *mapNameData ); +#if DEEP_CRC_TO_MEMORY + virtual void xferLogString(const AsciiString& str) {} +#endif + + protected: // this is the actual xfer implementation that each derived class should implement diff --git a/Core/GameEngine/Include/Common/XferDeepCRC.h b/Core/GameEngine/Include/Common/XferDeepCRC.h index 729e37bf1c8..82dbae95805 100644 --- a/Core/GameEngine/Include/Common/XferDeepCRC.h +++ b/Core/GameEngine/Include/Common/XferDeepCRC.h @@ -33,6 +33,10 @@ #include "Common/Xfer.h" #include "Common/XferCRC.h" +#if DEEP_CRC_TO_MEMORY +#include +#endif + // FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// class Snapshot; @@ -55,9 +59,18 @@ class XferDeepCRC : public XferCRC virtual void xferAsciiString( AsciiString *asciiStringData ) override; ///< xfer ascii string (need our own) virtual void xferUnicodeString( UnicodeString *unicodeStringData ) override; ///< xfer unicode string (need our own); +#if DEEP_CRC_TO_MEMORY + virtual void xferLogString(const AsciiString& str) override; + void changeXferMode(XferMode xferMode); +#endif + protected: virtual void xferImplementation( void *data, Int dataSize ) override; +#if DEEP_CRC_TO_MEMORY + std::vector* m_buffer; ///< pointer to buffer + size_t m_bufferIndex; ///< current index in buffer +#endif FILE * m_fileFP; ///< pointer to file }; diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index c8333fd6d90..b137260ecfa 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -104,7 +104,11 @@ class NetworkInterface : public SubsystemInterface virtual void attachTransport(Transport *transport) = 0; virtual void initTransport() = 0; virtual Bool sawCRCMismatch() = 0; +#if DEEP_CRC_TO_MEMORY + virtual void setSawCRCMismatch(const UnicodeString& strMismatchDetails) = 0; +#else virtual void setSawCRCMismatch() = 0; +#endif virtual Bool isPlayerConnected(Int playerID) = 0; diff --git a/Core/GameEngine/Source/Common/System/XferCRC.cpp b/Core/GameEngine/Source/Common/System/XferCRC.cpp index 12019d29e84..0cb25148bc8 100644 --- a/Core/GameEngine/Source/Common/System/XferCRC.cpp +++ b/Core/GameEngine/Source/Common/System/XferCRC.cpp @@ -36,6 +36,10 @@ #include "Common/Snapshot.h" #include "Utility/endian_compat.h" +#if DEEP_CRC_TO_MEMORY +#include "GameLogic/GameLogic.h" +#endif + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- XferCRC::XferCRC() @@ -175,7 +179,14 @@ UnsignedInt XferCRC::getCRC() XferDeepCRC::XferDeepCRC() { +#if DEEP_CRC_TO_MEMORY + m_xferMode = XFER_CRC; + m_buffer = nullptr; + m_bufferIndex = 0; +#else m_xferMode = XFER_SAVE; +#endif + m_fileFP = nullptr; } @@ -202,8 +213,13 @@ XferDeepCRC::~XferDeepCRC() void XferDeepCRC::open( AsciiString identifier ) { +#if DEEP_CRC_TO_MEMORY + m_xferMode = XFER_CRC; +#else m_xferMode = XFER_SAVE; +#endif +#if !DEEP_CRC_TO_MEMORY // sanity, check to see if we're already open if( m_fileFP != nullptr ) { @@ -213,10 +229,27 @@ void XferDeepCRC::open( AsciiString identifier ) throw XFER_FILE_ALREADY_OPEN; } +#endif // call base class Xfer::open( identifier ); +#if DEEP_CRC_TO_MEMORY + m_buffer = &TheGameLogic->getCRCBuffer(); + m_bufferIndex = 0; + + AsciiString str; + str.format("[ START OF DEEP CRC FRAME %d ]", TheGameLogic->getFrame()); + const UnsignedInt length = str.getLength(); + + while (m_bufferIndex + length >= m_buffer->size()) + { + m_buffer->resize(m_buffer->size() * 2); + } + + memcpy(&(*m_buffer)[m_bufferIndex], str.str(), length); + m_bufferIndex += length; +#else // open the file m_fileFP = fopen( identifier.str(), "w+b" ); if( m_fileFP == nullptr ) @@ -226,6 +259,7 @@ void XferDeepCRC::open( AsciiString identifier ) throw XFER_FILE_NOT_FOUND; } +#endif // initialize CRC to brand new one at zero m_crc = 0; @@ -238,6 +272,21 @@ void XferDeepCRC::open( AsciiString identifier ) void XferDeepCRC::close() { +#if DEEP_CRC_TO_MEMORY + AsciiString str; + str.format("[ END OF DEEP CRC FRAME %d ]", TheGameLogic->getFrame()); + const UnsignedInt length = str.getLength(); + + while (m_bufferIndex + length >= m_buffer->size()) + { + m_buffer->resize(m_buffer->size() * 2); + } + + memcpy(&(*m_buffer)[m_bufferIndex], str.str(), length); + m_bufferIndex += length; + + TheGameLogic->storeCRCBuffer(m_bufferIndex); +#else // sanity, if we don't have an open file we can do nothing if( m_fileFP == nullptr ) { @@ -250,6 +299,11 @@ void XferDeepCRC::close() // close the file fclose( m_fileFP ); m_fileFP = nullptr; +#endif + +#if DEEP_CRC_TO_MEMORY + m_buffer = nullptr; +#endif // erase the filename m_identifier.clear(); @@ -267,6 +321,15 @@ void XferDeepCRC::xferImplementation( void *data, Int dataSize ) return; } +#if DEEP_CRC_TO_MEMORY + while (m_bufferIndex + dataSize >= m_buffer->size()) + { + m_buffer->resize(m_buffer->size() * 2); + } + + memcpy(&(*m_buffer)[m_bufferIndex], data, dataSize); + m_bufferIndex += dataSize; +#else // sanity DEBUG_ASSERTCRASH( m_fileFP != nullptr, ("XferSave - file pointer for '%s' is null", m_identifier.str()) ); @@ -279,6 +342,7 @@ void XferDeepCRC::xferImplementation( void *data, Int dataSize ) throw XFER_WRITE_ERROR; } +#endif XferCRC::xferImplementation( data, dataSize ); @@ -341,3 +405,18 @@ void XferDeepCRC::xferUnicodeString( UnicodeString *unicodeStringData ) xferUser( (void *)unicodeStringData->str(), sizeof( WideChar ) * len ); } + +#if DEEP_CRC_TO_MEMORY +void XferDeepCRC::changeXferMode(XferMode xferMode) +{ + m_xferMode = xferMode; +} + +void XferDeepCRC::xferLogString(const AsciiString& str) +{ + if (m_buffer) + { + xferUser(const_cast(str.str()), str.getLength()); + } +} +#endif diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 1b7976949c5..e482e2d19c6 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -2302,6 +2302,14 @@ bool GameLogic::onSelfDestruct(MAYBE_UNUSED GameMessage *msg) msgPlayer->killPlayer(); } + // ignore CRC messages when a player is defeated + // because there's a mismatch risk at low NET_CRC_INTERVAL values + // example: runahead decreases, clients are expected to send more frames + // defeated player sends some but not the required number of frames (and CRC messages); + // the game mismatches because it only retains the most recent CRC value + // and the CRC value from the defeated player is not up-to-date + m_shouldValidateCRCs = -1; + // There is no reason to do any notification here, it now takes place in the victory conditions. // bonehead. @@ -2406,7 +2414,8 @@ bool GameLogic::onLogicCrc(MAYBE_UNUSED GameMessage *msg) if (!TheDebugIgnoreSyncErrors) { #endif - m_shouldValidateCRCs = TRUE; + if (m_shouldValidateCRCs != -1) + m_shouldValidateCRCs = 1; #if defined(RTS_DEBUG) } #endif diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index b476ec31f74..bac56e153b6 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -164,7 +164,11 @@ class Network : public NetworkInterface virtual void attachTransport(Transport *transport) override; virtual void initTransport() override; +#if DEEP_CRC_TO_MEMORY + virtual void setSawCRCMismatch(const UnicodeString& strMismatchDetails) override; +#else virtual void setSawCRCMismatch() override; +#endif virtual Bool sawCRCMismatch() override { return m_sawCRCMismatch; } virtual Bool isPlayerConnected( Int playerID ) override; @@ -368,7 +372,11 @@ void Network::init() #endif } +#if DEEP_CRC_TO_MEMORY +void Network::setSawCRCMismatch(const UnicodeString& strMismatchDetails) +#else void Network::setSawCRCMismatch() +#endif { m_sawCRCMismatch = TRUE; // GeneralsX @build GitHubCopilot 12/04/2026 Surface mismatch UI activation in manual Linux/macOS captures. diff --git a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h index c47c61eb7dc..c219f606705 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -385,7 +385,16 @@ class GameLogic : public SubsystemInterface, public Snapshot UnsignedInt m_CRC; ///< Cache of previous CRC value typedef std::map CachedCRCMap; CachedCRCMap m_cachedCRCs; ///< CRCs we've seen this frame - Bool m_shouldValidateCRCs; ///< Should we validate CRCs this frame? + Int m_shouldValidateCRCs; ///< Should we validate CRCs this frame? +#if DEEP_CRC_TO_MEMORY + std::vector m_crcWriteBuffer; + std::vector m_crcBuffers[64]; + size_t m_crcBufferIndex; +public: + std::vector& getCRCBuffer(); + void storeCRCBuffer(size_t size); + void writeCRCBuffersToDisk(UnsignedInt frame) const; +#endif //----------------------------------------------------------------------------------------------- //Bool m_loadingScene; Bool m_loadingMap; diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 9f64ec59805..9162745f497 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -272,7 +272,11 @@ GameLogic::GameLogic() m_progressCompleteTimeout[i] = 0; } - m_shouldValidateCRCs = FALSE; + m_shouldValidateCRCs = 0; + +#if DEEP_CRC_TO_MEMORY + m_crcBufferIndex = 0; +#endif m_startNewGame = FALSE; @@ -2337,7 +2341,7 @@ void GameLogic::processDestroyList() void GameLogic::processCommandList( CommandList *list ) { m_cachedCRCs.clear(); - m_shouldValidateCRCs = FALSE; + m_shouldValidateCRCs = 0; GameMessage* msg; @@ -2349,7 +2353,7 @@ void GameLogic::processCommandList( CommandList *list ) logicMessageDispatcher( msg, nullptr ); } - if (m_shouldValidateCRCs && !TheNetwork->sawCRCMismatch()) + if (m_shouldValidateCRCs == 1 && !TheNetwork->sawCRCMismatch()) { Bool sawCRCMismatch = FALSE; Int numPlayers = 0; @@ -2409,6 +2413,73 @@ void GameLogic::processCommandList( CommandList *list ) // GeneralsX @build GitHubCopilot 12/04/2026 Dump frame CRC set to stderr so Linux/macOS logs can be compared directly. /* fprintf(stderr, "[LAN86] CRC mismatch summary frame=%u cached=%zu players=%d\n", m_frame, m_cachedCRCs.size(), numPlayers); */ +#if DEEP_CRC_TO_MEMORY + UnsignedInt flagPlayersConnected = 0; + UnsignedInt flagCRCs = 0; + + for (Int i = 0; i < MAX_SLOTS; ++i) + { + if (TheNetwork->isPlayerConnected(i)) + { + flagPlayersConnected |= (1U << i); + } + } + + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + flagCRCs |= (1U << (crcIt->first - 2)); // neutral and civilian players take the first two slots + } + + UnicodeString strMismatchDetails; + strMismatchDetails.format(L"GameLogic frame %d, latest frame %d\nHad %d CRCs from %d players; Flags %d, %d\nMismatched Players:\n", + TheGameLogic->getFrame(), + TheGameLogic->getFrame() - TheNetwork->getRunAhead() - 1, + m_cachedCRCs.size(), + numPlayers, + flagPlayersConnected, + flagCRCs); + + std::map mapCRCOccurences; + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + std::map::iterator occurIt = mapCRCOccurences.find(crcIt->second); + if (occurIt != mapCRCOccurences.end()) + { + ++occurIt->second; + } + else + { + mapCRCOccurences[crcIt->second] = 1; + } + } + + int biggestCRCCount = -1; + UnsignedInt biggestCRC = ~0u; + + for (std::map::iterator crcIter = mapCRCOccurences.begin(); crcIter != mapCRCOccurences.end(); ++crcIter) + { + if (crcIter->second > biggestCRCCount) + { + biggestCRC = crcIter->first; + biggestCRCCount = crcIter->second; + } + } + + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + if (crcIt->second != biggestCRC) + { + Player* player = ThePlayerList->getNthPlayer(crcIt->first); + UnicodeString strPlayerInfo; + strPlayerInfo.format(L"player %d (%ls) = %X [MISMATCH]\n", crcIt->first, player ? player->getPlayerDisplayName().str() : L"", crcIt->second); + + strMismatchDetails.concat(strPlayerInfo); + } + } + + TheGameLogic->writeCRCBuffersToDisk(TheGameLogic->getFrame() - TheNetwork->getRunAhead() - 1); + TheNetwork->setSawCRCMismatch(strMismatchDetails); +#else for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) { Player *player = ThePlayerList->getNthPlayer(crcIt->first); @@ -2425,6 +2496,7 @@ void GameLogic::processCommandList( CommandList *list ) } #endif // DEBUG_LOGGING TheNetwork->setSawCRCMismatch(); +#endif } } @@ -4911,3 +4983,82 @@ void GameLogic::loadPostProcess() remakeSleepyUpdate(); } + +#if DEEP_CRC_TO_MEMORY +std::vector& GameLogic::getCRCBuffer() +{ + return m_crcWriteBuffer; +} + +void GameLogic::storeCRCBuffer(size_t size) +{ + std::vector& vec = m_crcBuffers[m_crcBufferIndex++ % ARRAY_SIZE(m_crcBuffers)]; + + vec.clear(); + vec.insert(vec.begin(), m_crcWriteBuffer.begin(), m_crcWriteBuffer.begin() + size); +} + +void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const +{ + AsciiString str; + // GeneralsX: Generate OS/Arch header + AsciiString headerStr; +#if defined(__APPLE__) && defined(__aarch64__) + headerStr = "GeneralsX: macOS ARM64\n"; +#elif defined(__APPLE__) && defined(__x86_64__) + headerStr = "GeneralsX: macOS x86_64\n"; +#elif defined(__linux__) && defined(__x86_64__) + headerStr = "GeneralsX: Linux x86_64\n"; +#elif defined(__linux__) && defined(__aarch64__) + headerStr = "GeneralsX: Linux ARM64\n"; +#else + headerStr = "GeneralsX: Unknown OS/Arch\n"; +#endif + + // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir + time_t t = time(nullptr); + struct tm *tm_info = localtime(&t); + char timebuf[32]; + strftime(timebuf, 32, "%Y-%m-%d-%H-%M-%S", tm_info); + + // TheGlobalData->getPath_UserData() gives standard document path + // Let's create logs dir if not exists (in cross-platform way, handled by file system) + str.format("%slogs/deep_crc_%s_f%u.bin", TheGlobalData->getPath_UserData().str(), timebuf, frame); + + FILE* fp = fopen(str.str(), "wb"); + if (fp) + { + constexpr const char version[] = "[ DEEP CRC DATA (VERSION 1.0.0) ]\n"; + + if (fwrite(&version[0], ARRAY_SIZE(version) - 1, 1, fp) != 1) + { + fclose(fp); + return; + } + + if (fwrite(headerStr.str(), headerStr.getLength(), 1, fp) != 1) + { + fclose(fp); + return; + } + + size_t oldest = (m_crcBufferIndex >= ARRAY_SIZE(m_crcBuffers)) ? m_crcBufferIndex % ARRAY_SIZE(m_crcBuffers) : 0; + for (size_t i = 0; i < ARRAY_SIZE(m_crcBuffers); ++i) + { + size_t readIndex = (oldest + i) % ARRAY_SIZE(m_crcBuffers); + const std::vector& vec = m_crcBuffers[readIndex]; + + if (vec.size() > 0) + { + if (fwrite(vec.data(), vec.size(), 1, fp) != 1) + { + fclose(fp); + return; + } + } + } + + fclose(fp); + } +} +#endif diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 8192d3fbe68..edc19c909b4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -392,7 +392,16 @@ class GameLogic : public SubsystemInterface, public Snapshot UnsignedInt m_CRC; ///< Cache of previous CRC value typedef std::map CachedCRCMap; CachedCRCMap m_cachedCRCs; ///< CRCs we've seen this frame - Bool m_shouldValidateCRCs; ///< Should we validate CRCs this frame? + Int m_shouldValidateCRCs; ///< Should we validate CRCs this frame? +#if DEEP_CRC_TO_MEMORY + std::vector m_crcWriteBuffer; + std::vector m_crcBuffers[64]; + size_t m_crcBufferIndex; +public: + std::vector& getCRCBuffer(); + void storeCRCBuffer(size_t size); + void writeCRCBuffersToDisk(UnsignedInt frame) const; +#endif //----------------------------------------------------------------------------------------------- //Bool m_loadingScene; Bool m_loadingMap; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index a526e00be1e..8a32e3f6fe7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -284,7 +284,11 @@ GameLogic::GameLogic() m_progressCompleteTimeout[i] = 0; } - m_shouldValidateCRCs = FALSE; + m_shouldValidateCRCs = 0; + +#if DEEP_CRC_TO_MEMORY + m_crcBufferIndex = 0; +#endif m_startNewGame = FALSE; @@ -2688,7 +2692,7 @@ void GameLogic::processDestroyList() void GameLogic::processCommandList( CommandList *list ) { m_cachedCRCs.clear(); - m_shouldValidateCRCs = FALSE; + m_shouldValidateCRCs = 0; GameMessage* msg; @@ -2700,7 +2704,7 @@ void GameLogic::processCommandList( CommandList *list ) logicMessageDispatcher( msg, nullptr ); } - if (m_shouldValidateCRCs && !TheNetwork->sawCRCMismatch()) + if (m_shouldValidateCRCs == 1 && !TheNetwork->sawCRCMismatch()) { Bool sawCRCMismatch = FALSE; Int numPlayers = 0; @@ -2760,6 +2764,73 @@ void GameLogic::processCommandList( CommandList *list ) // GeneralsX @build GitHubCopilot 12/04/2026 Dump frame CRC set to stderr so Linux/macOS logs can be compared directly. /* fprintf(stderr, "[LAN86] CRC mismatch summary frame=%u cached=%zu players=%d\n", m_frame, m_cachedCRCs.size(), numPlayers); */ +#if DEEP_CRC_TO_MEMORY + UnsignedInt flagPlayersConnected = 0; + UnsignedInt flagCRCs = 0; + + for (Int i = 0; i < MAX_SLOTS; ++i) + { + if (TheNetwork->isPlayerConnected(i)) + { + flagPlayersConnected |= (1U << i); + } + } + + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + flagCRCs |= (1U << (crcIt->first - 2)); // neutral and civilian players take the first two slots + } + + UnicodeString strMismatchDetails; + strMismatchDetails.format(L"GameLogic frame %d, latest frame %d\nHad %d CRCs from %d players; Flags %d, %d\nMismatched Players:\n", + TheGameLogic->getFrame(), + TheGameLogic->getFrame() - TheNetwork->getRunAhead() - 1, + m_cachedCRCs.size(), + numPlayers, + flagPlayersConnected, + flagCRCs); + + std::map mapCRCOccurences; + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + std::map::iterator occurIt = mapCRCOccurences.find(crcIt->second); + if (occurIt != mapCRCOccurences.end()) + { + ++occurIt->second; + } + else + { + mapCRCOccurences[crcIt->second] = 1; + } + } + + int biggestCRCCount = -1; + UnsignedInt biggestCRC = ~0u; + + for (std::map::iterator crcIter = mapCRCOccurences.begin(); crcIter != mapCRCOccurences.end(); ++crcIter) + { + if (crcIter->second > biggestCRCCount) + { + biggestCRC = crcIter->first; + biggestCRCCount = crcIter->second; + } + } + + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + if (crcIt->second != biggestCRC) + { + Player* player = ThePlayerList->getNthPlayer(crcIt->first); + UnicodeString strPlayerInfo; + strPlayerInfo.format(L"player %d (%ls) = %X [MISMATCH]\n", crcIt->first, player ? player->getPlayerDisplayName().str() : L"", crcIt->second); + + strMismatchDetails.concat(strPlayerInfo); + } + } + + TheGameLogic->writeCRCBuffersToDisk(TheGameLogic->getFrame() - TheNetwork->getRunAhead() - 1); + TheNetwork->setSawCRCMismatch(strMismatchDetails); +#else for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) { Player *player = ThePlayerList->getNthPlayer(crcIt->first); @@ -2776,6 +2847,7 @@ void GameLogic::processCommandList( CommandList *list ) } #endif // DEBUG_LOGGING TheNetwork->setSawCRCMismatch(); +#endif } } @@ -5513,3 +5585,82 @@ void GameLogic::loadPostProcess() remakeSleepyUpdate(); } + +#if DEEP_CRC_TO_MEMORY +std::vector& GameLogic::getCRCBuffer() +{ + return m_crcWriteBuffer; +} + +void GameLogic::storeCRCBuffer(size_t size) +{ + std::vector& vec = m_crcBuffers[m_crcBufferIndex++ % ARRAY_SIZE(m_crcBuffers)]; + + vec.clear(); + vec.insert(vec.begin(), m_crcWriteBuffer.begin(), m_crcWriteBuffer.begin() + size); +} + +void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const +{ + AsciiString str; + // GeneralsX: Generate OS/Arch header + AsciiString headerStr; +#if defined(__APPLE__) && defined(__aarch64__) + headerStr = "GeneralsX: macOS ARM64\n"; +#elif defined(__APPLE__) && defined(__x86_64__) + headerStr = "GeneralsX: macOS x86_64\n"; +#elif defined(__linux__) && defined(__x86_64__) + headerStr = "GeneralsX: Linux x86_64\n"; +#elif defined(__linux__) && defined(__aarch64__) + headerStr = "GeneralsX: Linux ARM64\n"; +#else + headerStr = "GeneralsX: Unknown OS/Arch\n"; +#endif + + // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir + time_t t = time(nullptr); + struct tm *tm_info = localtime(&t); + char timebuf[32]; + strftime(timebuf, 32, "%Y-%m-%d-%H-%M-%S", tm_info); + + // TheGlobalData->getPath_UserData() gives standard document path + // Let's create logs dir if not exists (in cross-platform way, handled by file system) + str.format("%slogs/deep_crc_%s_f%u.bin", TheGlobalData->getPath_UserData().str(), timebuf, frame); + + FILE* fp = fopen(str.str(), "wb"); + if (fp) + { + constexpr const char version[] = "[ DEEP CRC DATA (VERSION 1.0.0) ]\n"; + + if (fwrite(&version[0], ARRAY_SIZE(version) - 1, 1, fp) != 1) + { + fclose(fp); + return; + } + + if (fwrite(headerStr.str(), headerStr.getLength(), 1, fp) != 1) + { + fclose(fp); + return; + } + + size_t oldest = (m_crcBufferIndex >= ARRAY_SIZE(m_crcBuffers)) ? m_crcBufferIndex % ARRAY_SIZE(m_crcBuffers) : 0; + for (size_t i = 0; i < ARRAY_SIZE(m_crcBuffers); ++i) + { + size_t readIndex = (oldest + i) % ARRAY_SIZE(m_crcBuffers); + const std::vector& vec = m_crcBuffers[readIndex]; + + if (vec.size() > 0) + { + if (fwrite(vec.data(), vec.size(), 1, fp) != 1) + { + fclose(fp); + return; + } + } + } + + fclose(fp); + } +} +#endif diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index c7e3e81ca27..aff718611db 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -9,6 +9,7 @@ option(RTS_BUILD_OPTION_DEBUG "Build code with the \"Debug\" configuration." OFF option(RTS_BUILD_OPTION_ASAN "Build code with Address Sanitizer." OFF) option(RTS_BUILD_OPTION_VC6_FULL_DEBUG "Build VC6 with full debug info." OFF) option(RTS_BUILD_OPTION_FFMPEG "Enable FFmpeg support" OFF) +option(RTS_BUILD_OPTION_DEEP_CRC "Enable deep CRC snapshots on sync mismatch" ON) # Linux/SDL3 and OpenAL options (Phase 1 Linux port) option(SAGE_USE_SDL3 "Use SDL3 for windowing/input (Linux/macOS)" OFF) @@ -44,6 +45,7 @@ add_feature_info(DebugBuild RTS_BUILD_OPTION_DEBUG "Building as a \"Debug\" buil add_feature_info(AddressSanitizer RTS_BUILD_OPTION_ASAN "Building with address sanitizer") add_feature_info(Vc6FullDebug RTS_BUILD_OPTION_VC6_FULL_DEBUG "Building VC6 with full debug info") add_feature_info(FFmpegSupport RTS_BUILD_OPTION_FFMPEG "Building with FFmpeg support") +add_feature_info(DeepCRC RTS_BUILD_OPTION_DEEP_CRC "Enable deep CRC snapshots on sync mismatch") add_feature_info(SDL3Windowing SAGE_USE_SDL3 "Using SDL3 for windowing (Linux)") add_feature_info(OpenALAudio SAGE_USE_OPENAL "Using OpenAL for audio (Linux)") add_feature_info(UpdateCheck SAGE_UPDATE_CHECK "In-game update check via GitHub Releases API") @@ -139,6 +141,11 @@ if(SAGE_USE_GLM) message(STATUS "GLM math library enabled (DirectX 8 replacement)") endif() +if(RTS_BUILD_OPTION_DEEP_CRC) + target_compile_definitions(core_config INTERFACE DEEP_CRC_TO_MEMORY=1) + message(STATUS "Deep CRC logging on sync mismatch enabled") +endif() + # macOS MoltenVK detection (Phase 5) # GeneralsX @build BenderAI 24/02/2026 - Phase 5 macOS port if(APPLE AND SAGE_USE_MOLTENVK) diff --git a/scripts/qa/sync/run-determinism-harness.sh b/scripts/qa/sync/run-determinism-harness.sh index 442f72ac142..c72192e60c2 100755 --- a/scripts/qa/sync/run-determinism-harness.sh +++ b/scripts/qa/sync/run-determinism-harness.sh @@ -7,14 +7,17 @@ if [ -z "$1" ]; then exit 1 fi -REPLAY_FILE=$1 -LOG_DIR="logs/sync_harness" +CURRENT_DIR="$(pwd)" + +REPLAY_FILE="$1" +LOG_DIR="${CURRENT_DIR}/logs/sync_harness" mkdir -p "$LOG_DIR" echo "Running determinism harness on replay: $REPLAY_FILE" # Run the game in a controlled, quickstart mode to force replay playback # and generate SyncCrash or crc logs. +cd ~/GeneralsX/GeneralsZH && \ ./run.sh -win -xres 800 -yres 600 -quickstart -replay "$REPLAY_FILE" 2>&1 | tee "$LOG_DIR/harness_run.log" echo "Harness run complete. Check $LOG_DIR and the generated SyncCrash files." From 726b435d31e8a1a2223b6c4e74c3beee9d0cd67b Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 16 Jul 2026 22:37:22 -0300 Subject: [PATCH 11/20] GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Fix Deep CRC logging directory creation - Ensure the 'logs' directory is created automatically before dumping CRC payload. Previously silently failed because the 'logs' folder didn't exist in the user data directory. --- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++++- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 9162745f497..cff921e1087 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -5023,7 +5023,11 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const // TheGlobalData->getPath_UserData() gives standard document path // Let's create logs dir if not exists (in cross-platform way, handled by file system) - str.format("%slogs/deep_crc_%s_f%u.bin", TheGlobalData->getPath_UserData().str(), timebuf, frame); + AsciiString logDir; + logDir.format("%slogs", TheGlobalData->getPath_UserData().str()); + TheFileSystem->createDirectory(logDir); + + str.format("%s/deep_crc_%s_f%u.bin", logDir.str(), timebuf, frame); FILE* fp = fopen(str.str(), "wb"); if (fp) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 8a32e3f6fe7..1f96eaa70ce 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -5625,7 +5625,11 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const // TheGlobalData->getPath_UserData() gives standard document path // Let's create logs dir if not exists (in cross-platform way, handled by file system) - str.format("%slogs/deep_crc_%s_f%u.bin", TheGlobalData->getPath_UserData().str(), timebuf, frame); + AsciiString logDir; + logDir.format("%slogs", TheGlobalData->getPath_UserData().str()); + TheFileSystem->createDirectory(logDir); + + str.format("%s/deep_crc_%s_f%u.bin", logDir.str(), timebuf, frame); FILE* fp = fopen(str.str(), "wb"); if (fp) From 042b447cc31b90d2bc25b2ecc39192eacbbf0158 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 17 Jul 2026 12:19:27 -0300 Subject: [PATCH 12/20] GeneralsX @bugfix Meeseeks 17/07/2026 Fix deep CRC telemetry and headers --- .../Source/GameLogic/System/GameLogic.cpp | 78 +++++++++++++++---- .../Source/GameLogic/System/GameLogic.cpp | 77 ++++++++++++++---- 2 files changed, 127 insertions(+), 28 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index cff921e1087..bc33d97b926 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -43,7 +43,13 @@ #include "Common/FramePacer.h" #include "Common/GameAudio.h" #include "Common/GameEngine.h" +#include "Common/GameLOD.h" #include "Common/GameState.h" + +#if DEEP_CRC_TO_MEMORY +#include +#include +#endif #include "Common/GameUtility.h" #include "Common/INI.h" #include "Common/LatchRestore.h" @@ -276,6 +282,13 @@ GameLogic::GameLogic() #if DEEP_CRC_TO_MEMORY m_crcBufferIndex = 0; + + m_crcWriteBuffer.resize(1024 * 1024 * 8); + + for (size_t i = 0; i < ARRAY_SIZE(m_crcBuffers); ++i) + { + m_crcBuffers[i].resize(1024 * 1024); + } #endif m_startNewGame = FALSE; @@ -3714,7 +3727,13 @@ UnsignedInt GameLogic::getCRC( Int mode, AsciiString deepCRCFileName ) XferCRC *xferCRC; AsciiString marker; - if (deepCRCFileName.isNotEmpty()) +#if DEEP_CRC_TO_MEMORY + const Bool forceDeepCRC = TRUE; +#else + const Bool forceDeepCRC = FALSE; +#endif + + if (forceDeepCRC || deepCRCFileName.isNotEmpty()) { xferCRC = NEW XferDeepCRC; xferCRC->open(deepCRCFileName.str()); @@ -3816,9 +3835,43 @@ UnsignedInt GameLogic::getCRC( Int mode, AsciiString deepCRCFileName ) TheGameState->friend_xferSaveDataForCRC(xferCRC, SNAPSHOT_DEEPCRC_LOGICONLY); } - xferCRC->close(); + const UnsignedInt theCRC = xferCRC->getCRC(); + +#if DEEP_CRC_TO_MEMORY + AsciiString tmp; + tmp.format("[ frame %d: %8.8X, logical seeds: %8.8X ]", m_frame, theCRC, GetGameLogicRandomSeed()); + + xferCRC->xferLogString(tmp); - UnsignedInt theCRC = xferCRC->getCRC(); + for (Int j = 0; j < ThePlayerList->getPlayerCount(); ++j) + { + if (Player* player = ThePlayerList->getNthPlayer(j)) + { + tmp.format("[ Player (%d) money: %d, energy: %d | %d ]", + j, player->getMoney()->countMoney(), player->getEnergy()->getProduction(), player->getEnergy()->getConsumption()); + + xferCRC->xferLogString(tmp); + } + } + + for (obj = m_objList; obj; obj=obj->getNextObject()) + { + XferCRC tmpXfer; + tmpXfer.open(""); + tmpXfer.xferUser(const_cast(obj->getTransformMatrix()), sizeof(Matrix3D)); + tmpXfer.close(); + + const UnsignedInt mtxCRC = tmpXfer.getCRC(); + + tmp.format("[ CRC of object: %d (%s), player: %d, team: %d, health: %f, pos: %f %f %f, mtx: %8.8X ]", + obj->getID(), obj->getTemplate()->getName().str(), obj->getControllingPlayer()->getPlayerIndex(), (obj->getTeam() ? obj->getTeam()->getID() : TEAM_ID_INVALID), + obj->getBodyModule()->getHealth(), obj->getPosition()->x, obj->getPosition()->y, obj->getPosition()->z, mtxCRC); + + xferCRC->xferLogString(tmp); + } +#endif + + xferCRC->close(); delete xferCRC; xferCRC = nullptr; @@ -5003,17 +5056,14 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const AsciiString str; // GeneralsX: Generate OS/Arch header AsciiString headerStr; -#if defined(__APPLE__) && defined(__aarch64__) - headerStr = "GeneralsX: macOS ARM64\n"; -#elif defined(__APPLE__) && defined(__x86_64__) - headerStr = "GeneralsX: macOS x86_64\n"; -#elif defined(__linux__) && defined(__x86_64__) - headerStr = "GeneralsX: Linux x86_64\n"; -#elif defined(__linux__) && defined(__aarch64__) - headerStr = "GeneralsX: Linux ARM64\n"; -#else - headerStr = "GeneralsX: Unknown OS/Arch\n"; -#endif + struct utsname sysInfo; + if (uname(&sysInfo) == 0) { + headerStr.format("GeneralsX: %s %s (%s)\nArch: %s\nCPU Cores: %d\nRAM: %d MB\n\n", + sysInfo.sysname, sysInfo.release, sysInfo.version, sysInfo.machine, + SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); + } else { + headerStr = "GeneralsX: Unknown OS/Arch\n\n"; + } // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir time_t t = time(nullptr); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 1f96eaa70ce..66f8ba22a2d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -45,6 +45,11 @@ #include "Common/GameEngine.h" #include "Common/GameLOD.h" #include "Common/GameState.h" + +#if DEEP_CRC_TO_MEMORY +#include +#include +#endif #include "Common/GameUtility.h" #include "Common/INI.h" #include "Common/LatchRestore.h" @@ -288,6 +293,13 @@ GameLogic::GameLogic() #if DEEP_CRC_TO_MEMORY m_crcBufferIndex = 0; + + m_crcWriteBuffer.resize(1024 * 1024 * 8); + + for (size_t i = 0; i < ARRAY_SIZE(m_crcBuffers); ++i) + { + m_crcBuffers[i].resize(1024 * 1024); + } #endif m_startNewGame = FALSE; @@ -4299,7 +4311,13 @@ UnsignedInt GameLogic::getCRC( Int mode, AsciiString deepCRCFileName ) XferCRC *xferCRC; AsciiString marker; - if (deepCRCFileName.isNotEmpty()) +#if DEEP_CRC_TO_MEMORY + const Bool forceDeepCRC = TRUE; +#else + const Bool forceDeepCRC = FALSE; +#endif + + if (forceDeepCRC || deepCRCFileName.isNotEmpty()) { xferCRC = NEW XferDeepCRC; xferCRC->open(deepCRCFileName.str()); @@ -4401,9 +4419,43 @@ UnsignedInt GameLogic::getCRC( Int mode, AsciiString deepCRCFileName ) TheGameState->friend_xferSaveDataForCRC(xferCRC, SNAPSHOT_DEEPCRC_LOGICONLY); } - xferCRC->close(); + const UnsignedInt theCRC = xferCRC->getCRC(); + +#if DEEP_CRC_TO_MEMORY + AsciiString tmp; + tmp.format("[ frame %d: %8.8X, logical seeds: %8.8X ]", m_frame, theCRC, GetGameLogicRandomSeed()); + + xferCRC->xferLogString(tmp); - UnsignedInt theCRC = xferCRC->getCRC(); + for (Int j = 0; j < ThePlayerList->getPlayerCount(); ++j) + { + if (Player* player = ThePlayerList->getNthPlayer(j)) + { + tmp.format("[ Player (%d) money: %d, energy: %d | %d ]", + j, player->getMoney()->countMoney(), player->getEnergy()->getProduction(), player->getEnergy()->getConsumption()); + + xferCRC->xferLogString(tmp); + } + } + + for (obj = m_objList; obj; obj=obj->getNextObject()) + { + XferCRC tmpXfer; + tmpXfer.open(""); + tmpXfer.xferUser(const_cast(obj->getTransformMatrix()), sizeof(Matrix3D)); + tmpXfer.close(); + + const UnsignedInt mtxCRC = tmpXfer.getCRC(); + + tmp.format("[ CRC of object: %d (%s), player: %d, team: %d, health: %f, pos: %f %f %f, mtx: %8.8X ]", + obj->getID(), obj->getTemplate()->getName().str(), obj->getControllingPlayer()->getPlayerIndex(), (obj->getTeam() ? obj->getTeam()->getID() : TEAM_ID_INVALID), + obj->getBodyModule()->getHealth(), obj->getPosition()->x, obj->getPosition()->y, obj->getPosition()->z, mtxCRC); + + xferCRC->xferLogString(tmp); + } +#endif + + xferCRC->close(); delete xferCRC; xferCRC = nullptr; @@ -5605,17 +5657,14 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const AsciiString str; // GeneralsX: Generate OS/Arch header AsciiString headerStr; -#if defined(__APPLE__) && defined(__aarch64__) - headerStr = "GeneralsX: macOS ARM64\n"; -#elif defined(__APPLE__) && defined(__x86_64__) - headerStr = "GeneralsX: macOS x86_64\n"; -#elif defined(__linux__) && defined(__x86_64__) - headerStr = "GeneralsX: Linux x86_64\n"; -#elif defined(__linux__) && defined(__aarch64__) - headerStr = "GeneralsX: Linux ARM64\n"; -#else - headerStr = "GeneralsX: Unknown OS/Arch\n"; -#endif + struct utsname sysInfo; + if (uname(&sysInfo) == 0) { + headerStr.format("GeneralsX: %s %s (%s)\nArch: %s\nCPU Cores: %d\nRAM: %d MB\n\n", + sysInfo.sysname, sysInfo.release, sysInfo.version, sysInfo.machine, + SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); + } else { + headerStr = "GeneralsX: Unknown OS/Arch\n\n"; + } // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir time_t t = time(nullptr); From 2efb081d6a4c6cb85f80c11a1d6bcdfe3753ea38 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 17 Jul 2026 14:56:58 -0300 Subject: [PATCH 13/20] fix(wwmath): use deterministic math wrappers in primitive types GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath trigonometric, sqrt, and tan functions in matrix and vector classes to prevent macOS/Linux desyncs. - Replace sinf/cosf in matrix3, matrix3d, vector3 with WWMath::Sin/Cos. - Replace raw sqrt in matrix3d, quat, euler, sphere, normalcone with WWMath::Sqrt. - Replace raw tan in matrix4 with WWMath::TanTrig. - Add change annotations to source files and update July worklog. --- .../Libraries/Source/WWVegas/WWMath/euler.cpp | 5 ++- .../Libraries/Source/WWVegas/WWMath/matrix3.h | 15 +++---- .../Source/WWVegas/WWMath/matrix3d.cpp | 7 ++-- .../Source/WWVegas/WWMath/matrix3d.h | 41 ++++++++++--------- .../Libraries/Source/WWVegas/WWMath/matrix4.h | 5 ++- .../Source/WWVegas/WWMath/normalcone.h | 3 +- Core/Libraries/Source/WWVegas/WWMath/quat.cpp | 13 +++--- Core/Libraries/Source/WWVegas/WWMath/sphere.h | 5 ++- .../Libraries/Source/WWVegas/WWMath/vector3.h | 7 ++-- docs/WORKLOG/2026-07-DIARY.md | 6 +++ 10 files changed, 61 insertions(+), 46 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WWMath/euler.cpp b/Core/Libraries/Source/WWVegas/WWMath/euler.cpp index 5cf23f242b4..60bd9c3ba71 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/euler.cpp +++ b/Core/Libraries/Source/WWVegas/WWMath/euler.cpp @@ -174,13 +174,14 @@ double EulerAnglesClass::Get_Angle(int i) *=============================================================================================*/ void EulerAnglesClass::From_Matrix(const Matrix3D & M, int order) { + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::Sqrt instead of raw sqrt int i,j,k,h,n,s,f; Order = order; _euler_unpack_order(order,i,j,k,h,n,s,f); if (s == EULER_REPEAT_YES) { - double sy = sqrt(M[i][j]*M[i][j] + M[i][k]*M[i][k]); + double sy = WWMath::Sqrt(M[i][j]*M[i][j] + M[i][k]*M[i][k]); if (sy > 16*FLT_EPSILON) { @@ -197,7 +198,7 @@ void EulerAnglesClass::From_Matrix(const Matrix3D & M, int order) } else { - double cy = sqrt(M[i][i]*M[i][i] + M[j][i]*M[j][i]); + double cy = WWMath::Sqrt(M[i][i]*M[i][i] + M[j][i]*M[j][i]); if (cy > 16*FLT_EPSILON) { diff --git a/Core/Libraries/Source/WWVegas/WWMath/matrix3.h b/Core/Libraries/Source/WWVegas/WWMath/matrix3.h index 6fd98508d3b..57b6bccad0b 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/matrix3.h +++ b/Core/Libraries/Source/WWVegas/WWMath/matrix3.h @@ -361,7 +361,8 @@ WWINLINE Matrix3x3::Matrix3x3(const Vector3 & axis,float s_angle,float c_angle) WWINLINE void Matrix3x3::Set(const Vector3 & axis,float angle) { - Set(axis,sinf(angle),cosf(angle)); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath trigonometric functions + Set(axis,WWMath::Sin(angle),WWMath::Cos(angle)); } WWINLINE void Matrix3x3::Set(const Vector3 & axis,float s,float c) @@ -775,7 +776,7 @@ WWINLINE int operator != (const Matrix3x3 & a, const Matrix3x3 & b) *=============================================================================================*/ WWINLINE void Matrix3x3::Rotate_X(float theta) { - Rotate_X(sinf(theta),cosf(theta)); + Rotate_X(WWMath::Sin(theta),WWMath::Cos(theta)); } WWINLINE void Matrix3x3::Rotate_X(float s,float c) @@ -809,7 +810,7 @@ WWINLINE void Matrix3x3::Rotate_X(float s,float c) *=============================================================================================*/ WWINLINE void Matrix3x3::Rotate_Y(float theta) { - Rotate_Y(sinf(theta),cosf(theta)); + Rotate_Y(WWMath::Sin(theta),WWMath::Cos(theta)); } WWINLINE void Matrix3x3::Rotate_Y(float s,float c) @@ -844,7 +845,7 @@ WWINLINE void Matrix3x3::Rotate_Y(float s,float c) *=============================================================================================*/ WWINLINE void Matrix3x3::Rotate_Z(float theta) { - Rotate_Z(sinf(theta),cosf(theta)); + Rotate_Z(WWMath::Sin(theta),WWMath::Cos(theta)); } WWINLINE void Matrix3x3::Rotate_Z(float s,float c) @@ -898,7 +899,7 @@ WWINLINE Matrix3x3 Create_X_Rotation_Matrix3(float s,float c) WWINLINE Matrix3x3 Create_X_Rotation_Matrix3(float rad) { - return Create_X_Rotation_Matrix3(sinf(rad),cosf(rad)); + return Create_X_Rotation_Matrix3(WWMath::Sin(rad),WWMath::Cos(rad)); } /*********************************************************************************************** @@ -934,7 +935,7 @@ WWINLINE Matrix3x3 Create_Y_Rotation_Matrix3(float s,float c) WWINLINE Matrix3x3 Create_Y_Rotation_Matrix3(float rad) { - return Create_Y_Rotation_Matrix3(sinf(rad),cosf(rad)); + return Create_Y_Rotation_Matrix3(WWMath::Sin(rad),WWMath::Cos(rad)); } /*********************************************************************************************** @@ -970,7 +971,7 @@ WWINLINE Matrix3x3 Create_Z_Rotation_Matrix3(float s,float c) WWINLINE Matrix3x3 Create_Z_Rotation_Matrix3(float rad) { - return Create_Z_Rotation_Matrix3(sinf(rad),cosf(rad)); + return Create_Z_Rotation_Matrix3(WWMath::Sin(rad),WWMath::Cos(rad)); } WWINLINE void Matrix3x3::Rotate_Vector(const Matrix3x3 & A,const Vector3 & in,Vector3 * out) diff --git a/Core/Libraries/Source/WWVegas/WWMath/matrix3d.cpp b/Core/Libraries/Source/WWVegas/WWMath/matrix3d.cpp index 55a172e6e14..c315e853360 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/matrix3d.cpp +++ b/Core/Libraries/Source/WWVegas/WWMath/matrix3d.cpp @@ -412,10 +412,11 @@ void Matrix3D::Look_At_Dir(const Vector3 &pos, const Vector3 &dir, float roll) // Make sure you pass in UNITIZED direction!!! void Matrix3D::buildTransformMatrix( const Vector3 &pos, const Vector3 &dir ) { + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::Sqrt instead of raw sqrt float sinp, cosp; // sine and cosine of the pitch ("up-down" tilt about y) float siny, cosy; // sine and cosine of the yaw ("left-right"tilt about z) - float len2 = (float)sqrt( (dir.X * dir.X) + (dir.Y * dir.Y) ); + float len2 = WWMath::Sqrt( (dir.X * dir.X) + (dir.Y * dir.Y) ); sinp = dir.Z; cosp = len2; @@ -473,8 +474,8 @@ void Matrix3D::Obj_Look_At(const Vector3 &p,const Vector3 &t,float roll) dy = (t[1] - p[1]); dz = (t[2] - p[2]); - len1 = (float)sqrt(dx*dx + dy*dy + dz*dz); - len2 = (float)sqrt(dx*dx + dy*dy); + len1 = WWMath::Sqrt(dx*dx + dy*dy + dz*dz); + len2 = WWMath::Sqrt(dx*dx + dy*dy); if (len1 != 0.0f) { sinp = dz/len1; diff --git a/Core/Libraries/Source/WWVegas/WWMath/matrix3d.h b/Core/Libraries/Source/WWVegas/WWMath/matrix3d.h index 0be93c296da..3c085761163 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/matrix3d.h +++ b/Core/Libraries/Source/WWVegas/WWMath/matrix3d.h @@ -566,8 +566,9 @@ WWINLINE void Matrix3D::Set( const Vector3 &x, // x-axis unit vector *=============================================================================================*/ WWINLINE void Matrix3D::Set(const Vector3 & axis,float angle) { - float c = cosf(angle); - float s = sinf(angle); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath trigonometric functions + float c = WWMath::Cos(angle); + float s = WWMath::Sin(angle); Set(axis,s,c); } @@ -768,8 +769,8 @@ WWINLINE void Matrix3D::Rotate_X(float theta) float tmp1,tmp2; float s,c; - s = sinf(theta); - c = cosf(theta); + s = WWMath::Sin(theta); + c = WWMath::Cos(theta); tmp1 = Row[0][1]; tmp2 = Row[0][2]; Row[0][1] = (float)( c*tmp1 + s*tmp2); @@ -836,8 +837,8 @@ WWINLINE void Matrix3D::Rotate_Y(float theta) float tmp1,tmp2; float s,c; - s = sinf(theta); - c = cosf(theta); + s = WWMath::Sin(theta); + c = WWMath::Cos(theta); tmp1 = Row[0][0]; tmp2 = Row[0][2]; Row[0][0] = (float)(c*tmp1 - s*tmp2); @@ -903,8 +904,8 @@ WWINLINE void Matrix3D::Rotate_Z(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[0][0]; tmp2 = Row[0][1]; Row[0][0] = (float)( c*tmp1 + s*tmp2); @@ -1055,8 +1056,8 @@ WWINLINE void Matrix3D::Pre_Rotate_X(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[1][0]; tmp2 = Row[2][0]; Row[1][0] = (float)(c*tmp1 - s*tmp2); @@ -1093,8 +1094,8 @@ WWINLINE void Matrix3D::Pre_Rotate_Y(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[0][0]; tmp2 = Row[2][0]; Row[0][0] = (float)( c*tmp1 + s*tmp2); @@ -1131,8 +1132,8 @@ WWINLINE void Matrix3D::Pre_Rotate_Z(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[0][0]; tmp2 = Row[1][0]; Row[0][0] = (float)(c*tmp1 - s*tmp2); @@ -1274,8 +1275,8 @@ WWINLINE void Matrix3D::In_Place_Pre_Rotate_X(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[1][0]; tmp2 = Row[2][0]; Row[1][0] = (float)(c*tmp1 - s*tmp2); @@ -1308,8 +1309,8 @@ WWINLINE void Matrix3D::In_Place_Pre_Rotate_Y(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[0][0]; tmp2 = Row[2][0]; Row[0][0] = (float)( c*tmp1 + s*tmp2); @@ -1342,8 +1343,8 @@ WWINLINE void Matrix3D::In_Place_Pre_Rotate_Z(float theta) float tmp1,tmp2; float c,s; - c = cosf(theta); - s = sinf(theta); + c = WWMath::Cos(theta); + s = WWMath::Sin(theta); tmp1 = Row[0][0]; tmp2 = Row[1][0]; Row[0][0] = (float)(c*tmp1 - s*tmp2); diff --git a/Core/Libraries/Source/WWVegas/WWMath/matrix4.h b/Core/Libraries/Source/WWVegas/WWMath/matrix4.h index 5fda1777413..7883555556c 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/matrix4.h +++ b/Core/Libraries/Source/WWVegas/WWMath/matrix4.h @@ -435,8 +435,9 @@ WWINLINE void Matrix4x4::Init_Perspective(float hfov,float vfov,float znear,floa assert(zfar > znear); Make_Identity(); - Row[0][0] = static_cast(1.0 / tan(hfov*0.5)); - Row[1][1] = static_cast(1.0 / tan(vfov*0.5)); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::TanTrig instead of raw tan + Row[0][0] = static_cast(1.0 / WWMath::TanTrig(hfov*0.5f)); + Row[1][1] = static_cast(1.0 / WWMath::TanTrig(vfov*0.5f)); Row[2][2] = -(zfar + znear) / (zfar - znear); Row[2][3] = static_cast(-(2.0*zfar*znear) / (zfar - znear)); Row[3][2] = -1.0f; diff --git a/Core/Libraries/Source/WWVegas/WWMath/normalcone.h b/Core/Libraries/Source/WWVegas/WWMath/normalcone.h index d4cd659e52d..5fc69562790 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/normalcone.h +++ b/Core/Libraries/Source/WWVegas/WWMath/normalcone.h @@ -117,7 +117,8 @@ inline float NormalCone::Get_Coplanar_Normals(const Vector3 & Input, Vector3 & O if(length < WWMATH_EPSILON) return 0.0f; - length = sqrt(length); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::Sqrt instead of raw sqrt + length = WWMath::Sqrt(length); cross /= length; // Make a matrix3 which uses it as an axis of rotation and diff --git a/Core/Libraries/Source/WWVegas/WWMath/quat.cpp b/Core/Libraries/Source/WWVegas/WWMath/quat.cpp index d262891f412..21eb9e01167 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/quat.cpp +++ b/Core/Libraries/Source/WWVegas/WWMath/quat.cpp @@ -661,6 +661,7 @@ void Cached_Slerp(const Quaternion & p,const Quaternion & q,float alpha,SlerpInf *=============================================================================================*/ Quaternion Build_Quaternion(const Matrix3D & mat) { + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::Sqrt instead of raw sqrt float tr,s; int i,j,k; Quaternion q; @@ -670,7 +671,7 @@ Quaternion Build_Quaternion(const Matrix3D & mat) if (tr > 0.0f) { - s = sqrt(tr + 1.0); + s = WWMath::Sqrt(tr + 1.0f); q[3] = s * 0.5; s = 0.5 / s; @@ -686,7 +687,7 @@ Quaternion Build_Quaternion(const Matrix3D & mat) j = _nxt[i]; k = _nxt[j]; - s = sqrt((mat[i][i] - (mat[j][j] + mat[k][k])) + 1.0); + s = WWMath::Sqrt((mat[i][i] - (mat[j][j] + mat[k][k])) + 1.0f); q[i] = s * 0.5; if (s != 0.0) { @@ -713,7 +714,7 @@ Quaternion Build_Quaternion(const Matrix3x3 & mat) if (tr > 0.0) { - s = sqrt(tr + 1.0); + s = WWMath::Sqrt(tr + 1.0f); q[3] = s * 0.5; s = 0.5 / s; @@ -730,7 +731,7 @@ Quaternion Build_Quaternion(const Matrix3x3 & mat) j = _nxt[i]; k = _nxt[j]; - s = sqrt( (mat[i][i] - (mat[j][j]+mat[k][k])) + 1.0); + s = WWMath::Sqrt( (mat[i][i] - (mat[j][j]+mat[k][k])) + 1.0f); q[i] = s * 0.5; @@ -757,7 +758,7 @@ Quaternion Build_Quaternion(const Matrix4x4 & mat) if (tr > 0.0) { - s = sqrt(tr + 1.0); + s = WWMath::Sqrt(tr + 1.0f); q[3] = s * 0.5; s = 0.5 / s; @@ -774,7 +775,7 @@ Quaternion Build_Quaternion(const Matrix4x4 & mat) j = _nxt[i]; k = _nxt[j]; - s = sqrt( (mat[i][i] - (mat[j][j]+mat[k][k])) + 1.0); + s = WWMath::Sqrt( (mat[i][i] - (mat[j][j]+mat[k][k])) + 1.0f); q[i] = s * 0.5; if (s != 0.0) { diff --git a/Core/Libraries/Source/WWVegas/WWMath/sphere.h b/Core/Libraries/Source/WWVegas/WWMath/sphere.h index 0bb7eaca11b..a9fda957a41 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/sphere.h +++ b/Core/Libraries/Source/WWVegas/WWMath/sphere.h @@ -192,7 +192,8 @@ inline SphereClass::SphereClass(const Vector3 *Position,const int VertCount) dz = dia2.Z - center.Z; double radsqr = dx*dx + dy*dy + dz*dz; - double radius = sqrt(radsqr); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath::Sqrt instead of raw sqrt + double radius = WWMath::Sqrt(radsqr); // SECOND PASS: @@ -209,7 +210,7 @@ inline SphereClass::SphereClass(const Vector3 *Position,const int VertCount) // this point was outside the old sphere, compute a new // center point and radius which contains this point - double testrad = sqrt(testrad2); + double testrad = WWMath::Sqrt(testrad2); // adjust center and radius radius = (radius + testrad) / 2.0; diff --git a/Core/Libraries/Source/WWVegas/WWMath/vector3.h b/Core/Libraries/Source/WWVegas/WWMath/vector3.h index a4f584df75b..3e371557d9a 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/vector3.h +++ b/Core/Libraries/Source/WWVegas/WWMath/vector3.h @@ -708,7 +708,8 @@ WWINLINE void Vector3::Scale(const Vector3 & scale) *=============================================================================================*/ WWINLINE void Vector3::Rotate_X(float angle) { - Rotate_X(sinf(angle),cosf(angle)); + // GeneralsX @bugfix Mr. Meeseeks 17/07/2026 Use deterministic WWMath trigonometric functions + Rotate_X(WWMath::Sin(angle),WWMath::Cos(angle)); } @@ -748,7 +749,7 @@ WWINLINE void Vector3::Rotate_X(float s_angle,float c_angle) *=============================================================================================*/ WWINLINE void Vector3::Rotate_Y(float angle) { - Rotate_Y(sinf(angle),cosf(angle)); + Rotate_Y(WWMath::Sin(angle),WWMath::Cos(angle)); } @@ -788,7 +789,7 @@ WWINLINE void Vector3::Rotate_Y(float s_angle,float c_angle) *=============================================================================================*/ WWINLINE void Vector3::Rotate_Z(float angle) { - Rotate_Z(sinf(angle),cosf(angle)); + Rotate_Z(WWMath::Sin(angle),WWMath::Cos(angle)); } diff --git a/docs/WORKLOG/2026-07-DIARY.md b/docs/WORKLOG/2026-07-DIARY.md index 846a9977566..79dbfa0da02 100644 --- a/docs/WORKLOG/2026-07-DIARY.md +++ b/docs/WORKLOG/2026-07-DIARY.md @@ -1,5 +1,11 @@ # July 2026 +## 17/07/2026 +### Fix Non-Deterministic Math in WWMath Matrix Classes +- Traced the cross-platform desync at frame 100 to the `TrainCabUngarrisonable` object's translation matrix diverging between macOS (ARM64) and Linux (x86_64). +- Discovered that the low-level math classes in `WWMath` (`matrix3d.h`, `matrix3.h`, `vector3.h`, `matrix3d.cpp`, `quat.cpp`, `euler.cpp`, `sphere.h`, `normalcone.h`, and `matrix4.h`) were directly calling native LibC floating-point math functions (`sinf`, `cosf`, `sqrt`, `tan`) instead of using the deterministic wrappers (`WWMath::Sin`, `WWMath::Cos`, `WWMath::Sqrt`, `WWMath::TanTrig`). +- Surgically replaced all occurrences of direct `sinf` / `cosf` / `sqrt` / `tan` within the `WWMath` library with their deterministic counterparts to ensure identical outputs on different architectures and CPU instruction sets. + ## 13/07/2026 ### Fixed SpecialPowerModule and Tree Render Segfault (Beta-13 Regressions) - **Pause Bug Fix**: Adapted upstream's solution (TheSuperHackers commit `fa2761324`) which passes `logicTimeQueryFlags` into `canUpdateGameLogic` and `getActualLogicTimeScaleFps`, returning false if the frame pacing is 0. This completely avoids logic time accumulation while the game is halted (Esc menu), providing a more robust fix than our initial `!TheFramePacer->isGameHalted()` loop hack. Applied to both Generals and Zero Hour. From cee199d8156203c00f52728df7f8c3e09f2c5f2e Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 17 Jul 2026 15:43:07 -0300 Subject: [PATCH 14/20] docs: add parse_deep_crc.py and document deep CRC telemetry GeneralsX @docs Mr. Meeseeks 17/07/2026 Add parse_deep_crc.py script and document telemetry tool in AGENTS.md --- AGENTS.md | 1 + scripts/qa/parse_deep_crc.py | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100755 scripts/qa/parse_deep_crc.py diff --git a/AGENTS.md b/AGENTS.md index 2df02fea4d4..6b0a68b93ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ To guarantee cross-play between macOS ARM64 and Linux x86_64 without SyncCrash d 3. **Double-Precision Isolation** – Do not allow implicit `double` promotion in transcendental/power math evaluations (e.g. `gm_sqrt`, `gm_pow`, `gm_atan`). x87 (24-bit mantissa) and NEON (53-bit mantissa) diverge by 1-ULP on double-precision. Ensure all `WWMath` wrappers explicitly accept/return `float` or internally downcast `double` to `float` prior to `gm_*f` execution to guarantee cross-platform bit-identical outputs. 4. **FPU Environment State Leaks** – Audio drivers (OpenAL/MiniAudio) and OS callbacks aggressively alter hardware FPU registers (e.g., Flush-To-Zero, Rounding mode) for DSP performance. If this state leaks into the main thread, the simulation math diverges. **Always inject `ScopedFPUGuard`** at the boundaries of `GameLogic::update()`, audio thread callbacks, and mouse-picking logic (e.g., `View::pickDrawable()`). 5. **FMA Contraction** – Fused Multiply-Add combines instructions with infinite intermediate precision, yielding different results on ARM64 vs x86_64. We strictly enforce `-ffp-contract=off` globally (and `/fp:precise` for MSVC). Never bypass this with `-ffast-math` or `/fp:fast`. +6. **Deep CRC Memory Buffer Logging** – When a sync crash occurs and the root cause isn't obvious, the game automatically dumps a `logs/deep_crc_YYYY-MM-DD-HH-MM-SS.bin` file containing a binary snapshot of the last 64 frames of state transfers. Use `scripts/qa/parse_deep_crc.py` to inspect these dumps and identify the exact object ID and state data that first diverged between players. Note: This requires the `RTS_BUILD_OPTION_DEEP_CRC=ON` CMake flag (which is enabled by default). ## Reference Repositories - **fighter19-dxvk-port** – Primary graphics/platform reference (DXVK + SDL3 on Linux) diff --git a/scripts/qa/parse_deep_crc.py b/scripts/qa/parse_deep_crc.py new file mode 100755 index 00000000000..a4d40b0badb --- /dev/null +++ b/scripts/qa/parse_deep_crc.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import struct +import sys +import os + +def parse_crc_bin(filepath): + with open(filepath, 'rb') as f: + data = f.read() + + # Read OS header if present (null terminated string followed by 4 byte length) + header_len = 0 + os_info = "" + # Look for the first 0x00 which terminates the OS string + null_idx = data.find(b'\x00') + if null_idx != -1 and null_idx < 128: + os_info = data[:null_idx].decode('utf-8', errors='ignore') + header_len = null_idx + 1 + + print(f"--- Desync Dump: {os.path.basename(filepath)} ---") + if os_info: + print(f"OS/Arch: {os_info}") + + # Locate "MARKER:Objects\x00" + marker = b"MARKER:Objects\x00" + marker_pos = data.find(marker) + + if marker_pos == -1: + print("Error: Could not find MARKER:Objects in the dump.") + return + + objects_data = data[marker_pos + len(marker):] + print(f"\nFound MARKER:Objects at offset {marker_pos}") + print(f"Objects data size: {len(objects_data)} bytes") + + # Read first object (Assuming it caused the desync) + # The format is: + # xferVersion (4 bytes, little endian uint32) + # objectID (4 bytes, little endian uint32) + # chunk of data + + if len(objects_data) >= 8: + version = struct.unpack('= 7, next 48 bytes are Matrix3D (3x4 floats) + if version >= 7 and len(objects_data) >= 8 + 48: + print(f" Transform Matrix (Matrix3D):") + matrix_bytes = objects_data[8:8+48] + floats = struct.unpack('<12f', matrix_bytes) + print(f" Row 0: [{floats[0]:.6f}, {floats[1]:.6f}, {floats[2]:.6f}, {floats[3]:.6f}]") + print(f" Row 1: [{floats[4]:.6f}, {floats[5]:.6f}, {floats[6]:.6f}, {floats[7]:.6f}]") + print(f" Row 2: [{floats[8]:.6f}, {floats[9]:.6f}, {floats[10]:.6f}, {floats[11]:.6f}]") + + print("\n-------------------------------------------------\n") + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Usage: ./parse_deep_crc.py [path_to_another_bin_file ...]") + sys.exit(1) + + for file in sys.argv[1:]: + parse_crc_bin(file) From cb5599c5945e8380a453451dee110c17fc413d29 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Sun, 19 Jul 2026 22:50:23 -0300 Subject: [PATCH 15/20] docs: add git commit standards summary to AGENTS.md --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6b0a68b93ee..fb530bfa058 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,6 +206,12 @@ git merge thesuperhackers/main - Use `--body-file` with real Markdown file instead of `--body` - Avoid literal `\n` sequences; prefer actual newlines in multi-line strings +## Git Commit Standards +- **Conventional Commits**: Format must be `(optional scope): `. (e.g. `fix(audio): restart sound groups in reset`) +- **DO NOT use `@`**: The commit `type` field must never contain an `@` symbol (e.g. `GeneralsX @bugfix` is for inline code annotations only, **not** commit messages). +- **Imperative mood**: Use "add", "fix", "change" (not "added" or "fixes"). +- **Read the Docs**: For full details, valid types, and PR standards, you **MUST** read `.github/instructions/git-commit.instructions.md`. + ## VS Code Tasks - Prefer task-first execution for build/test/debug - Logs captured to `logs/` directory From e03179b924590e3be230a901b79551890c1d3d4c Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Sun, 19 Jul 2026 22:52:38 -0300 Subject: [PATCH 16/20] docs: update Golden Rule 4 for OpenAL/MiniAudio parity --- AGENTS.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb530bfa058..85a207cb5ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ GeneralsX is a cross-platform port of Command & Conquer: Generals Zero Hour for 1. **Single codebase** – Linux and macOS build from same source 2. **SDL3 everywhere** – No native platform calls in game code 3. **DXVK everywhere** – DX8 → Vulkan translation on all platforms -4. **OpenAL default (MiniAudio WIP)** – Cross-platform audio stack. Audio fixes must prioritize OpenAL but be backported to MiniAudio. +4. **OpenAL / MiniAudio Parity** – Cross-platform audio stack. Implementations and bug fixes in one MUST be replicated in the other to maintain strict feature parity. 5. **64-bit native** – x86_64 only (32-bit via VC6 upstream) 6. **Retail compatibility** – Original replays and mods must work 7. **Determinism** – Rendering/audio changes must not affect gameplay logic @@ -170,8 +170,6 @@ mkdir -p logs && gdb -batch -ex "run -win" -ex "bt full" -ex "thread apply all b ./build/linux64-deploy/GeneralsMD/GeneralsXZH 2>&1 | tee logs/gdb.log ``` - - ## Branching & Sync ### TheSuperHackers upstream sync ```bash From f1be13da0891e29be062c9ada7c41c4af8baf5df Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 20 Jul 2026 09:52:33 -0300 Subject: [PATCH 17/20] fix(telemetry): change deep crc dump directory from logs to Debug GeneralsX @tweak Mr. Meeseeks 20/07/2026 Align dump dir with project conventions --- AGENTS.md | 2 +- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++--- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85a207cb5ed..d59fb645382 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ To guarantee cross-play between macOS ARM64 and Linux x86_64 without SyncCrash d 3. **Double-Precision Isolation** – Do not allow implicit `double` promotion in transcendental/power math evaluations (e.g. `gm_sqrt`, `gm_pow`, `gm_atan`). x87 (24-bit mantissa) and NEON (53-bit mantissa) diverge by 1-ULP on double-precision. Ensure all `WWMath` wrappers explicitly accept/return `float` or internally downcast `double` to `float` prior to `gm_*f` execution to guarantee cross-platform bit-identical outputs. 4. **FPU Environment State Leaks** – Audio drivers (OpenAL/MiniAudio) and OS callbacks aggressively alter hardware FPU registers (e.g., Flush-To-Zero, Rounding mode) for DSP performance. If this state leaks into the main thread, the simulation math diverges. **Always inject `ScopedFPUGuard`** at the boundaries of `GameLogic::update()`, audio thread callbacks, and mouse-picking logic (e.g., `View::pickDrawable()`). 5. **FMA Contraction** – Fused Multiply-Add combines instructions with infinite intermediate precision, yielding different results on ARM64 vs x86_64. We strictly enforce `-ffp-contract=off` globally (and `/fp:precise` for MSVC). Never bypass this with `-ffast-math` or `/fp:fast`. -6. **Deep CRC Memory Buffer Logging** – When a sync crash occurs and the root cause isn't obvious, the game automatically dumps a `logs/deep_crc_YYYY-MM-DD-HH-MM-SS.bin` file containing a binary snapshot of the last 64 frames of state transfers. Use `scripts/qa/parse_deep_crc.py` to inspect these dumps and identify the exact object ID and state data that first diverged between players. Note: This requires the `RTS_BUILD_OPTION_DEEP_CRC=ON` CMake flag (which is enabled by default). +6. **Deep CRC Memory Buffer Logging** – When a sync crash occurs and the root cause isn't obvious, the game automatically dumps a `Debug/deep_crc_YYYY-MM-DD-HH-MM-SS.bin` file containing a binary snapshot of the last 64 frames of state transfers. Use `scripts/qa/parse_deep_crc.py` to inspect these dumps and identify the exact object ID and state data that first diverged between players. Note: This requires the `RTS_BUILD_OPTION_DEEP_CRC=ON` CMake flag (which is enabled by default). ## Reference Repositories - **fighter19-dxvk-port** – Primary graphics/platform reference (DXVK + SDL3 on Linux) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index bc33d97b926..7a92c2d16c7 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -5065,16 +5065,16 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const headerStr = "GeneralsX: Unknown OS/Arch\n\n"; } - // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir + // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data Debug dir time_t t = time(nullptr); struct tm *tm_info = localtime(&t); char timebuf[32]; strftime(timebuf, 32, "%Y-%m-%d-%H-%M-%S", tm_info); // TheGlobalData->getPath_UserData() gives standard document path - // Let's create logs dir if not exists (in cross-platform way, handled by file system) + // Let's create Debug dir if not exists (in cross-platform way, handled by file system) AsciiString logDir; - logDir.format("%slogs", TheGlobalData->getPath_UserData().str()); + logDir.format("%sDebug", TheGlobalData->getPath_UserData().str()); TheFileSystem->createDirectory(logDir); str.format("%s/deep_crc_%s_f%u.bin", logDir.str(), timebuf, frame); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 66f8ba22a2d..484df3aa51f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -5666,16 +5666,16 @@ void GameLogic::writeCRCBuffersToDisk(UnsignedInt frame) const headerStr = "GeneralsX: Unknown OS/Arch\n\n"; } - // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data logs dir + // Format filename as deep_crc_YYYY-MM-DD-HH-MM-SS.bin inside user data Debug dir time_t t = time(nullptr); struct tm *tm_info = localtime(&t); char timebuf[32]; strftime(timebuf, 32, "%Y-%m-%d-%H-%M-%S", tm_info); // TheGlobalData->getPath_UserData() gives standard document path - // Let's create logs dir if not exists (in cross-platform way, handled by file system) + // Let's create Debug dir if not exists (in cross-platform way, handled by file system) AsciiString logDir; - logDir.format("%slogs", TheGlobalData->getPath_UserData().str()); + logDir.format("%sDebug", TheGlobalData->getPath_UserData().str()); TheFileSystem->createDirectory(logDir); str.format("%s/deep_crc_%s_f%u.bin", logDir.str(), timebuf, frame); From f41ba7b3754bf6a2fefd93870de05fb75a24df4d Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 20 Jul 2026 09:55:55 -0300 Subject: [PATCH 18/20] docs: clarify that commit bodies must not contain annotations or @ tags --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d59fb645382..1a5c8c34f67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,7 +206,7 @@ git merge thesuperhackers/main ## Git Commit Standards - **Conventional Commits**: Format must be `(optional scope): `. (e.g. `fix(audio): restart sound groups in reset`) -- **DO NOT use `@`**: The commit `type` field must never contain an `@` symbol (e.g. `GeneralsX @bugfix` is for inline code annotations only, **not** commit messages). +- **DO NOT use `@`**: The commit message (both title and body) must never contain an `@` symbol. The `// GeneralsX @keyword` format is strictly for inline code annotations in C++ files. Do not append these signatures to commit messages. - **Imperative mood**: Use "add", "fix", "change" (not "added" or "fixes"). - **Read the Docs**: For full details, valid types, and PR standards, you **MUST** read `.github/instructions/git-commit.instructions.md`. From d343fdf4183984b80bfdc973688bdd5b355137e3 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 20 Jul 2026 10:31:52 -0300 Subject: [PATCH 19/20] docs: add howto guide for investigating network desyncs GeneralsX @feature Mr. Meeseeks 20/07/2026 Add user guide for sync crashes and deep CRC telemetry --- .../HOWTO/investigating-desyncs-lan-gaming.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/HOWTO/investigating-desyncs-lan-gaming.md diff --git a/docs/HOWTO/investigating-desyncs-lan-gaming.md b/docs/HOWTO/investigating-desyncs-lan-gaming.md new file mode 100644 index 00000000000..d209146b266 --- /dev/null +++ b/docs/HOWTO/investigating-desyncs-lan-gaming.md @@ -0,0 +1,57 @@ +# HOWTO: Investigating SyncCrashes (Desyncs) in LAN / Online Multiplayer + +When playing Command & Conquer: Generals or Zero Hour over a network, all clients must maintain a perfectly synchronized game state. If the simulation diverges even slightly (a "desync" or "SyncCrash"), the game stops and drops players to prevent the match from breaking further. + +This guide explains how determinism works in the SAGE engine, our modernization efforts, and how you can help us debug desyncs by providing Deep CRC telemetry. + +## 1. The Theory of Determinism + +RTS games like Generals do not send the positions of every unit over the network. Instead, they use a **deterministic lockstep model**. Only player inputs (clicks, commands, build orders) are sent across the network. Every computer runs the exact same simulation logic, math, and physics frame-by-frame. As long as the math is identical across all machines, the game states will remain perfectly in sync. + +However, different CPU architectures (like x86_64 on Linux/Windows and ARM64 on Apple Silicon) handle floating-point math slightly differently at the hardware level. A function like `sin()`, `cos()`, or even a simple division can yield a result that is 0.0000001 off on a Mac compared to a Linux PC. Within a few frames, that microscopic difference cascades—a tank turns at a slightly different angle, a bullet misses instead of hitting, and the game crashes with a Sync error. + +## 2. Our Solution: Mathematical Parity + +To bridge the gap between platforms, GeneralsX implements strict rules: +* **`WWMath` Wrappers:** We do not use the native OS math libraries (`libc` or `libm`) for simulation logic. All transcendental math passes through our own deterministic `WWMath` library, guaranteeing that ARM64 and x86_64 produce bit-identical results. +* **FPU State Isolation:** Audio drivers (like OpenAL) often change the CPU's hardware rounding modes for performance. We heavily isolate these systems using `ScopedFPUGuard` to prevent audio threads from "leaking" their hardware settings into the main game thread. +* **FMA Contraction Disabled:** We strictly disable compiler optimizations like Fused Multiply-Add (FMA) that alter intermediate math precision. + +While these solutions allow cross-platform matches (e.g. macOS vs Linux) to run smoothly in our internal tests, the sheer volume of different environments, OS versions, compiler quirks, and mods means that edge cases can still slip through. + +## 3. Deep CRC Telemetry + +If you experience a SyncCrash, the game automatically dumps a "Deep CRC" memory snapshot containing the last 64 frames of state data right before the divergence occurred. + +These binary files are saved in the game's `Debug` folder. You will find files named `deep_crc_YYYY-MM-DD-HH-MM-SS.bin`. + +**Where to find the logs:** +* **macOS:** `~/Library/Application Support/GeneralsX/GeneralsZH/Debug/` +* **Linux (Native/Flatpak):** `~/.local/share/GeneralsX/GeneralsZH/Debug/` *(or inside your Flatpak app data folder)* + +## 4. Analyzing the Logs + +To figure out exactly *what* went out of sync, we need to compare the `deep_crc` binary files from **all players** involved in the match. + +We provide a Python script in the repository to parse these binary dumps into human-readable data: + +```bash +# Run the python script and pass the path to the crash dumps +./scripts/qa/parse_deep_crc.py /path/to/player1_deep_crc.bin /path/to/player2_deep_crc.bin +``` + +The script will output the OS/Architecture of the machine, the exact Object ID that first caused the CRC mismatch, and the internal state (like the Transform Matrix) of that object. + +> [!IMPORTANT] +> A single log file is not enough! A desync is a *disagreement* between two or more computers. You must gather the `.bin` files from **both** the host and the client(s) for the same match to see where the numbers diverged. + +## 5. Reporting Desyncs + +If you identify a desync and can reproduce it, we want to know! + +1. Gather the `deep_crc_*.bin` logs from all players in the match. +2. If possible, zip the `Replays` folder for that match. +3. Open a **New Issue** on our GitHub repository with the label `bug` and `network`. +4. Attach the logs and explain what happened right before the crash (e.g. "happened when a specific unit fired a weapon" or "happened instantly on load"). + +If you're a developer and you used `parse_deep_crc.py` to trace the divergence to a specific C++ class (e.g. `Weapon` or `Locomotor`), feel free to submit a Pull Request with a fix using the `WWMath` deterministic wrappers! From ea0deb444d56d1507a9260e025e04bbf210d5290 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 20 Jul 2026 10:35:25 -0300 Subject: [PATCH 20/20] docs: rename desync howto to match directory conventions and update index GeneralsX @tweak Mr. Meeseeks 20/07/2026 Follow HOWTO file naming convention and update table of contents --- ...estigating-desyncs-lan-gaming.md => INVESTIGATING_DESYNCS.md} | 0 docs/HOWTO/README.md | 1 + 2 files changed, 1 insertion(+) rename docs/HOWTO/{investigating-desyncs-lan-gaming.md => INVESTIGATING_DESYNCS.md} (100%) diff --git a/docs/HOWTO/investigating-desyncs-lan-gaming.md b/docs/HOWTO/INVESTIGATING_DESYNCS.md similarity index 100% rename from docs/HOWTO/investigating-desyncs-lan-gaming.md rename to docs/HOWTO/INVESTIGATING_DESYNCS.md diff --git a/docs/HOWTO/README.md b/docs/HOWTO/README.md index 0bcb06728a3..a62358aeee2 100644 --- a/docs/HOWTO/README.md +++ b/docs/HOWTO/README.md @@ -10,6 +10,7 @@ Step-by-step guides for common tasks in GeneralsX. | [Getting the Game Files](GETTING_THE_GAME_FILES.md) | Obtain original game assets (Steam, CrossOver, SteamCMD) | | [SagePatch Configuration](SAGEPATCH_CONFIGURATION.md) | Configure camera height, scroll speed, terrain draw distance, and other QoL settings | | [Russian Localization](RUSSIAN_LOCALIZATION.md) | Apply Russian language patch (EN + RU instructions) | +| [Investigating SyncCrashes](INVESTIGATING_DESYNCS.md) | How to extract and analyze Deep CRC memory buffers to debug network desyncs | ## Contributing